packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts (373 lines of code) (raw):

import { Aws, Resource, Annotations, ValidationError } from 'aws-cdk-lib'; import { IVpc, ISubnet, SubnetSelection, SelectedSubnets, EnableVpnGatewayOptions, VpnGateway, VpnConnectionType, CfnVPCGatewayAttachment, CfnVPNGatewayRoutePropagation, VpnConnectionOptions, VpnConnection, ClientVpnEndpointOptions, ClientVpnEndpoint, InterfaceVpcEndpointOptions, InterfaceVpcEndpoint, GatewayVpcEndpointOptions, GatewayVpcEndpoint, FlowLogOptions, FlowLog, FlowLogResourceType, SubnetType, SubnetFilter } from 'aws-cdk-lib/aws-ec2'; import { allRouteTableIds, flatten, subnetGroupNameFromConstructId } from './util'; import { IDependable, Dependable, IConstruct, DependencyGroup } from 'constructs'; import { EgressOnlyInternetGateway, InternetGateway, NatConnectivityType, NatGateway, NatGatewayOptions, Route, VPCPeeringConnection, VPCPeeringConnectionOptions, VPNGatewayV2 } from './route'; import { ISubnetV2 } from './subnet-v2'; import { AccountPrincipal, Effect, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; import { IVPCCidrBlock } from './vpc-v2'; /** * Options to define EgressOnlyInternetGateway for VPC */ export interface EgressOnlyInternetGatewayOptions { /** * List of subnets where route to EGW will be added * * @default - no route created */ readonly subnets?: SubnetSelection[]; /** * Destination Ipv6 address for EGW route * * @default - '::/0' all Ipv6 traffic */ readonly destination?: string; /** * The resource name of the egress-only internet gateway. * Provided name will be used for tagging * * @default - no name tag associated and provisioned without a resource name */ readonly egressOnlyInternetGatewayName?: string; } /** * Options to define InternetGateway for VPC */ export interface InternetGatewayOptions{ /** * Destination Ipv6 address for EGW route * * @default - '0.0.0.0' all Ipv4 traffic */ readonly ipv4Destination?: string; /** * Destination Ipv6 address for EGW route * * @default - '::/0' all Ipv6 traffic */ readonly ipv6Destination?: string; /** * The resource name of the internet gateway. * Provided name will be used for tagging * * @default - provisioned without a resource name */ readonly internetGatewayName?: string; /** * List of subnets where route to IGW will be added * * @default - route created for all subnets with Type `SubnetType.Public` */ readonly subnets?: SubnetSelection[]; } /** * Options to define VPNGatewayV2 for VPC */ export interface VPNGatewayV2Options { /** * The type of VPN connection the virtual private gateway supports. * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngateway.html#cfn-ec2-vpngateway-type */ readonly type: VpnConnectionType; /** * The private Autonomous System Number (ASN) for the Amazon side of a BGP session. * * @default - no ASN set for BGP session */ readonly amazonSideAsn?: number; /** * The resource name of the VPN gateway. * * @default - resource provisioned without any name */ readonly vpnGatewayName?: string; /** * Subnets where the route propagation should be added. * * @default - no propogation for routes */ readonly vpnRoutePropagation?: SubnetSelection[]; } /** * Placeholder to see what extra props we might need, * will be added to original IVPC */ export interface IVpcV2 extends IVpc { /** * The secondary CIDR blocks associated with the VPC. * * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html#vpc-resize}. */ readonly secondaryCidrBlock?: IVPCCidrBlock[]; /** * The primary IPv4 CIDR block associated with the VPC. * Needed in order to validate the vpc range of subnet * current prop vpcCidrBlock refers to the token value * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html#vpc-sizing-ipv4}. */ readonly ipv4CidrBlock: string; /** * Optional to override inferred region * * @default - current stack's environment region */ readonly region: string; /** * The ID of the AWS account that owns the VPC * * @default - the account id of the parent stack */ readonly ownerAccountId: string; /** * IPv4 CIDR provisioned under pool * Required to check for overlapping CIDRs after provisioning * is complete under IPAM pool */ readonly ipv4IpamProvisionedCidrs?: string[]; /** * VpcName to be used for tagging its components * @attribute */ readonly vpcName?: string; /** * Add an Egress only Internet Gateway to current VPC. * Can only be used for ipv6 enabled VPCs. * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway-basics.html}. */ addEgressOnlyInternetGateway(options?: EgressOnlyInternetGatewayOptions): EgressOnlyInternetGateway; /** * Adds an Internet Gateway to current VPC. * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/userguide/vpc-igw-internet-access.html}. * * @default - defines route for all ipv4('0.0.0.0') and ipv6 addresses('::/0') */ addInternetGateway(options?: InternetGatewayOptions): InternetGateway; /** * Adds VPN Gateway to VPC and set route propogation. * For more information, see the {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpngateway.html}. * * @default - no route propogation */ enableVpnGatewayV2(options: VPNGatewayV2Options): VPNGatewayV2; /** * Adds a new NAT Gateway to VPC * A NAT gateway is a Network Address Translation (NAT) service. NAT Gateway Connectivity can be of type `Public` or `Private`. * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html}. * * @default ConnectivityType.Public */ addNatGateway(options: NatGatewayOptions): NatGateway; /** * Adds a new role to acceptor VPC account * A cross account role is required for the VPC to peer with another account. * For more information, see the {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/peer-with-vpc-in-another-account.html}. */ createAcceptorVpcRole(requestorAccountId: string): Role; /** * Creates a new peering connection * A peering connection is a private virtual network established between two VPCs. * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html}. */ createPeeringConnection(id: string, options: VPCPeeringConnectionOptions): VPCPeeringConnection; } /** * Base class for creating a VPC (Virtual Private Cloud) in AWS. * * For more information, see the {@link https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html|AWS CDK Documentation on VPCs}. */ export abstract class VpcV2Base extends Resource implements IVpcV2 { /** * Identifier for this VPC */ public abstract readonly vpcId: string; /** * Arn of this VPC */ public abstract readonly vpcArn: string; /** * CIDR range for this VPC */ public abstract readonly vpcCidrBlock: string; /** * List of public subnets in this VPC */ public readonly publicSubnets: ISubnet[] = []; /** * List of private subnets in this VPC */ public readonly privateSubnets: ISubnet[] = []; /** * List of isolated subnets in this VPC */ public abstract readonly isolatedSubnets: ISubnet[]; /** * AZs for this VPC */ public readonly availabilityZones: string[] = []; /** * Dependable that can be depended upon to force internet connectivity established on the VPC */ public abstract readonly internetConnectivityEstablished: IDependable; /** * Dependable that can be depended upon to force internet connectivity established on the VPC * Add igw to this if its a public subnet * @internal */ protected readonly _internetConnectivityEstablished = new DependencyGroup(); /** * Secondary IPs for the VPC, can be multiple Ipv4 or Ipv6 * Ipv4 should be within RFC#1918 range */ public abstract readonly secondaryCidrBlock?: IVPCCidrBlock[]; /** * The primary IPv4 CIDR block associated with the VPC. * Needed in order to validate the vpc range of subnet * current prop vpcCidrBlock refers to the token value * For more information, see the {@link https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html#vpc-sizing-ipv4}. */ public abstract readonly ipv4CidrBlock: string; /** * VpcName to be used for tagging its components */ public abstract readonly vpcName?: string; /** * Region for this VPC */ public abstract readonly region: string; /** * Identifier of the owner for this VPC */ public abstract readonly ownerAccountId: string; /** * IPv4 CIDR provisioned under pool * Required to check for overlapping CIDRs after provisioning * is complete under IPAM pool */ public abstract readonly ipv4IpamProvisionedCidrs?: string[]; /** * If this is set to true, don't error out on trying to select subnets */ protected incompleteSubnetDefinition: boolean = false; /** * Mutable private field for the vpnGatewayId * @internal */ protected _vpnGatewayId?: string; /** * Mutable private field for the internetGatewayId * @internal */ protected _internetGatewayId?: string; /** * Mutable private field for the EgressOnlyInternetGatewayId * @internal */ protected _egressOnlyInternetGatewayId?: string; /** * Return information on the subnets appropriate for the given selection strategy * * Requires that at least one subnet is matched, throws a descriptive * error message otherwise. */ public selectSubnets(selection: SubnetSelection = {}): SelectedSubnets { const subnets = this.selectSubnetObjects(selection); const pubs = new Set(this.publicSubnets); return { subnetIds: subnets.map(s => s.subnetId), get availabilityZones(): string[] { return subnets.map(s => s.availabilityZone); }, internetConnectivityEstablished: tap(new CompositeDependable(), d => subnets.forEach(s => d.add(s.internetConnectivityEstablished))), subnets, hasPublic: subnets.some(s => pubs.has(s)), isPendingLookup: this.incompleteSubnetDefinition, }; } /** * Adds a VPN Gateway to this VPC * @deprecated use enableVpnGatewayV2 for compatibility with VPCV2.Route */ public enableVpnGateway(options: EnableVpnGatewayOptions): void { if (this.vpnGatewayId) { throw new Error('The VPN Gateway has already been enabled.'); } const vpnGateway = new VpnGateway(this, 'VpnGateway', { amazonSideAsn: options.amazonSideAsn, type: VpnConnectionType.IPSEC_1, }); this._vpnGatewayId = vpnGateway.gatewayId; const attachment = new CfnVPCGatewayAttachment(this, 'VPCVPNGW', { vpcId: this.vpcId, vpnGatewayId: this._vpnGatewayId, }); // Propagate routes on route tables associated with the right subnets const vpnRoutePropagation = options.vpnRoutePropagation ?? [{}]; const routeTableIds = allRouteTableIds(flatten(vpnRoutePropagation.map(s => this.selectSubnets(s).subnets))); if (routeTableIds.length === 0) { Annotations.of(this).addError(`enableVpnGateway: no subnets matching selection: '${JSON.stringify(vpnRoutePropagation)}'. Select other subnets to add routes to.`); } const routePropagation = new CfnVPNGatewayRoutePropagation(this, 'RoutePropagation', { routeTableIds, vpnGatewayId: this._vpnGatewayId, }); // The AWS::EC2::VPNGatewayRoutePropagation resource cannot use the VPN gateway // until it has successfully attached to the VPC. // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpn-gatewayrouteprop.html routePropagation.node.addDependency(attachment); } /** * Adds VPNGAtewayV2 to this VPC */ public enableVpnGatewayV2(options: VPNGatewayV2Options): VPNGatewayV2 { if (this.vpnGatewayId) { throw new Error('The VPN Gateway has already been enabled.'); } const vpnGateway = new VPNGatewayV2(this, 'VpnGateway', { vpc: this, ...options, }); this._internetConnectivityEstablished.add(vpnGateway); this._vpnGatewayId = vpnGateway.routerTargetId; return vpnGateway; } /** * Adds a new VPN connection to this VPC */ public addVpnConnection(id: string, options: VpnConnectionOptions): VpnConnection { return new VpnConnection(this, id, { vpc: this, ...options, }); } /** * Adds a new client VPN endpoint to this VPC */ public addClientVpnEndpoint(id: string, options: ClientVpnEndpointOptions): ClientVpnEndpoint { return new ClientVpnEndpoint(this, id, { ...options, vpc: this, }); } /** * Adds a new interface endpoint to this VPC */ public addInterfaceEndpoint(id: string, options: InterfaceVpcEndpointOptions): InterfaceVpcEndpoint { return new InterfaceVpcEndpoint(this, id, { vpc: this, ...options, }); } /** * Adds a new gateway endpoint to this VPC */ public addGatewayEndpoint(id: string, options: GatewayVpcEndpointOptions): GatewayVpcEndpoint { return new GatewayVpcEndpoint(this, id, { vpc: this, ...options, }); } /** * Adds a new Egress Only Internet Gateway to this VPC and defines a new route * to the route table of given subnets. * * @default - in case of no input subnets, no route is created */ public addEgressOnlyInternetGateway(options?: EgressOnlyInternetGatewayOptions): EgressOnlyInternetGateway { const egw = new EgressOnlyInternetGateway(this, 'EgressOnlyGW', { vpc: this, egressOnlyInternetGatewayName: options?.egressOnlyInternetGatewayName, }); this._egressOnlyInternetGatewayId = egw.routerTargetId; let useIpv6; if (this.secondaryCidrBlock) { useIpv6 = (this.secondaryCidrBlock.some((secondaryAddress) => secondaryAddress.amazonProvidedIpv6CidrBlock === true || secondaryAddress.ipv6IpamPoolId !== undefined || secondaryAddress.ipv6CidrBlock !== undefined)); } if (!useIpv6) { throw new Error('Egress only IGW can only be added to Ipv6 enabled VPC'); } if (options?.subnets) { // Use helper function to ensure unique subnets const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets)); this.processSubnets(subnets, (subnet) => { this.createEgressRoute(subnet, egw, options.destination); }); } return egw; } /** * Creates a route for EGW with destination set to outbound IPv6('::/0') or custom ipv6 address. * @internal */ private createEgressRoute(subnet: ISubnetV2, egw: EgressOnlyInternetGateway, destination?: string): void { const destinationIpv6 = destination ?? '::/0'; new Route(this, `${subnet.node.id}-EgressRoute`, { routeTable: subnet.routeTable, destination: destinationIpv6, // IPv6 default route target: { gateway: egw }, routeName: 'CDKEIGWRoute', }); } /** * Adds a new Internet Gateway to this VPC * * @default - creates a new route for public subnets(with all outbound access) to the Internet Gateway. */ public addInternetGateway(options?: InternetGatewayOptions): InternetGateway { if (this._internetGatewayId) { throw new Error('The Internet Gateway has already been enabled.'); } const igw = new InternetGateway(this, 'InternetGateway', { vpc: this, internetGatewayName: options?.internetGatewayName, }); this._internetConnectivityEstablished.add(igw); this._internetGatewayId = igw.routerTargetId; // Add routes for subnets defined as an input if (options?.subnets) { const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets)); // Use helper function to ensure unique subnets this.processSubnets(subnets, (subnet) => { this.addDefaultInternetRoute(subnet, igw, options); }); // If there are no input subnets defined, default route will be added to all public subnets } else if (!options?.subnets && this.publicSubnets) { // Track route tables that have already had routes added to prevent duplicates this.processSubnets(this.publicSubnets, (subnet) => { this.addDefaultInternetRoute(subnet, igw, options); }); } return igw; } /** * Adds default route for the internet gateway * @internal */ private addDefaultInternetRoute(subnet: ISubnetV2, igw: InternetGateway, options?: InternetGatewayOptions): void { // Add default route to IGW for IPv6 if (subnet.ipv6CidrBlock) { new Route(this, `${subnet.node.id}-DefaultIPv6Route`, { routeTable: subnet.routeTable, destination: options?.ipv6Destination ?? '::/0', target: { gateway: igw }, routeName: 'CDKDefaultIPv6Route', }); } // Add default route to IGW for IPv4 new Route(this, `${subnet.node.id}-DefaultRoute`, { routeTable: subnet.routeTable, destination: options?.ipv4Destination ?? '0.0.0.0/0', target: { gateway: igw }, routeName: 'CDKDefaultIPv4Route', }); } /** * Helper function to process unique subnets and route table for adding routes */ private processSubnets( subnets: ISubnetV2[], routeHandler: (subnet: ISubnetV2) => void, ): void { const processedSubnets = new Set<string>(); const processedRouteTables = new Set<string>(); subnets.forEach((subnet) => { if (!this.publicSubnets.includes(subnet)) { Annotations.of(this).addWarningV2('InternetGatewayWarning', `Given ${subnet.node.id} is not a public subnet. Internet Gateway should be added only to public subnets.`); } if (!processedSubnets.has(subnet.node.id)) { if (subnet.routeTable && !processedRouteTables.has(subnet.routeTable.routeTableId)) { routeHandler(subnet); processedRouteTables.add(subnet.routeTable.routeTableId); } processedSubnets.add(subnet.node.id); } }); } /** * Adds a new NAT Gateway to the given subnet of this VPC * of given subnets. */ public addNatGateway(options: NatGatewayOptions): NatGateway { if (options.connectivityType === NatConnectivityType.PUBLIC && !this._internetGatewayId) { throw new ValidationError('Cannot add a Public NAT Gateway without an Internet Gateway enabled on VPC', this); } return new NatGateway(this, `NATGateway-${options.subnet.node.id}`, { vpc: this, ...options, }); } /** * Adds a new flow log to this VPC */ public addFlowLog(id: string, options?: FlowLogOptions): FlowLog { return new FlowLog(this, id, { resourceType: FlowLogResourceType.fromVpc(this), ...options, }); } /** * Creates peering connection role for acceptor VPC */ public createAcceptorVpcRole(requestorAccountId: string): Role { const peeringRole = new Role(this, 'VpcPeeringRole', { assumedBy: new AccountPrincipal(requestorAccountId), roleName: 'VpcPeeringRole', description: 'Restrictive role for VPC peering', }); peeringRole.addToPolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: ['ec2:AcceptVpcPeeringConnection'], resources: [`arn:${Aws.PARTITION}:ec2:${this.region}:${this.ownerAccountId}:vpc/${this.vpcId}`], })); peeringRole.addToPolicy(new PolicyStatement({ actions: ['ec2:AcceptVpcPeeringConnection'], effect: Effect.ALLOW, resources: [`arn:${Aws.PARTITION}:ec2:${this.region}:${this.ownerAccountId}:vpc-peering-connection/*`], conditions: { StringEquals: { 'ec2:AccepterVpc': `arn:${Aws.PARTITION}:ec2:${this.region}:${this.ownerAccountId}:vpc/${this.vpcId}`, }, }, })); return peeringRole; } /** * Creates a peering connection */ public createPeeringConnection(id: string, options: VPCPeeringConnectionOptions): VPCPeeringConnection { return new VPCPeeringConnection(this, id, { requestorVpc: this, ...options, }); } /** * Returns the id of the VPN Gateway (if enabled) */ public get vpnGatewayId(): string | undefined { return this._vpnGatewayId; } /** * Returns the id of the Internet Gateway (if enabled) */ public get internetGatewayId(): string | undefined { return this._internetGatewayId; } /** * Returns the id of the Egress Only Internet Gateway (if enabled) */ public get egressOnlyInternetGatewayId(): string | undefined { return this._egressOnlyInternetGatewayId; } /** * Return the subnets appropriate for the placement strategy */ protected selectSubnetObjects(selection: SubnetSelection = {}): ISubnet[] { selection = this.reifySelectionDefaults(selection); if (selection.subnets !== undefined) { return selection.subnets; } let subnets; if (selection.subnetGroupName !== undefined) { // Select by name subnets = this.selectSubnetObjectsByName(selection.subnetGroupName); } else { // Or specify by type const type = selection.subnetType || SubnetType.PRIVATE_WITH_EGRESS; subnets = this.selectSubnetObjectsByType(type); } // Apply all the filters subnets = this.applySubnetFilters(subnets, selection.subnetFilters ?? []); return subnets; } private applySubnetFilters(subnets: ISubnet[], filters: SubnetFilter[]): ISubnet[] { let filtered = subnets; // Apply each filter in sequence for (const filter of filters) { filtered = filter.selectSubnets(filtered); } return filtered; } private selectSubnetObjectsByName(groupName: string) { const allSubnets = [...this.publicSubnets, ...this.privateSubnets, ...this.isolatedSubnets]; const subnets = allSubnets.filter(s => subnetGroupNameFromConstructId(s) === groupName); if (subnets.length === 0 && !this.incompleteSubnetDefinition) { const names = Array.from(new Set(allSubnets.map(subnetGroupNameFromConstructId))); throw new Error(`There are no subnet groups with name '${groupName}' in this VPC. Available names: ${names}`); } return subnets; } private selectSubnetObjectsByType(subnetType: SubnetType): ISubnet[] { type DeprecatedSubnetType = 'Deprecated_Isolated' | 'Deprecated_Private'; const allSubnets: { [key in SubnetType | DeprecatedSubnetType]?: ISubnet[] } = { [SubnetType.PRIVATE_ISOLATED]: this.isolatedSubnets, ['Deprecated_Isolated']: this.isolatedSubnets, [SubnetType.PRIVATE_WITH_NAT]: this.privateSubnets, [SubnetType.PRIVATE_WITH_EGRESS]: this.privateSubnets, ['Deprecated_Private']: this.privateSubnets, [SubnetType.PUBLIC]: this.publicSubnets, }; const subnets = allSubnets[subnetType]!; // Force merge conflict here with https://github.com/aws/aws-cdk/pull/4089 // see ImportedVpc if (subnets.length === 0 && !this.incompleteSubnetDefinition) { const availableTypes = Object.entries(allSubnets).filter(([_, subs]) => subs.length > 0).map(([typeName, _]) => typeName); throw new Error(`There are no '${subnetType}' subnet groups in this VPC. Available types: ${availableTypes}`); } return subnets; } /** * Validate the fields in a SubnetSelection object, and reify defaults if necessary * * In case of default selection, select the first type of PRIVATE, ISOLATED, * PUBLIC (in that order) that has any subnets. */ private reifySelectionDefaults(placement: SubnetSelection): SubnetSelection { // TODO: throw error as new VpcV2 cannot support subnetName or subnetGroupName anymore if ('subnetName' in placement && placement.subnetName !== undefined) { if (placement.subnetGroupName !== undefined) { throw new Error('Please use only \'subnetGroupName\' (\'subnetName\' is deprecated and has the same behavior)'); } else { Annotations.of(this).addWarningV2('@aws-cdk/aws-ec2:subnetNameDeprecated', 'Usage of \'subnetName\' in SubnetSelection is deprecated, use \'subnetGroupName\' instead'); } placement = { ...placement, subnetGroupName: placement.subnetName as string }; } const exclusiveSelections: Array<keyof SubnetSelection> = ['subnets', 'subnetType', 'subnetGroupName']; const providedSelections = exclusiveSelections.filter(key => placement[key] !== undefined); if (providedSelections.length > 1) { throw new Error(`Only one of '${providedSelections}' can be supplied to subnet selection.`); } if (placement.subnetType === undefined && placement.subnetGroupName === undefined && placement.subnets === undefined) { // Return default subnet type based on subnets that actually exist let subnetType = this.privateSubnets.length ? SubnetType.PRIVATE_WITH_EGRESS : this.isolatedSubnets.length ? SubnetType.PRIVATE_ISOLATED : SubnetType.PUBLIC; placement = { ...placement, subnetType: subnetType }; } // Establish which subnet filters are going to be used let subnetFilters = placement.subnetFilters ?? []; // Backwards compatibility with existing `availabilityZones` and `onePerAz` functionality if (placement.availabilityZones !== undefined) { // Filter by AZs, if specified subnetFilters.push(SubnetFilter.availabilityZones(placement.availabilityZones)); } if (!!placement.onePerAz) { // Ensure one per AZ if specified subnetFilters.push(SubnetFilter.onePerAz()); } // Overwrite the provided placement filters and remove the availabilityZones and onePerAz properties placement = { ...placement, subnetFilters: subnetFilters, availabilityZones: undefined, onePerAz: undefined }; const { availabilityZones, onePerAz, ...rest } = placement; return rest; } } class CompositeDependable implements IDependable { private readonly dependables = new Array<IDependable>(); constructor() { const self = this; Dependable.implement(this, { get dependencyRoots() { const ret = new Array<IConstruct>(); for (const dep of self.dependables) { ret.push(...Dependable.of(dep).dependencyRoots); } return ret; }, }); } /** * Add a construct to the dependency roots */ public add(dep: IDependable) { this.dependables.push(dep); } } /** * Invoke a function on a value (for its side effect) and return the value */ function tap<T>(x: T, fn: (x: T) => void): T { fn(x); return x; }