Skip to content

Commit 31d135f

Browse files
authored
feat(cli): bundle dependencies (#18667)
Use `esbuild` via a custom new tool to bundle CLI dependencies and release a package with no runtime dependencies. More details as to reasoning and implementation [here](https://github.com/aws/aws-cdk/blob/epolon/cli-bundle/tools/%40aws-cdk/node-bundle/README.md). ## Note This PR has some implications on programmatic usage of the CLI. Namely, deep imports like so: ```ts import { PluginHost } from 'aws-cdk/lib/plugin' ``` Will no longer be available. These imports are considered private and should not have been used in the first place. Instead, switch to: ```ts import { PluginHost } from 'aws-cdk' ``` If your import isn't available from the top-level, it means that export is actually private, and should be avoided. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 14b6c9c commit 31d135f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+7803
-143
lines changed

packages/@aws-cdk/cloudformation-diff/lib/format-table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as chalk from 'chalk';
2-
import * as stringWidth from 'string-width';
2+
import stringWidth from 'string-width';
33
import * as table from 'table';
44

55
/**

packages/@aws-cdk/cloudformation-diff/lib/iam/statement.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import * as deepEqual from 'fast-deep-equal';
21
import { deepRemoveUndefined } from '../util';
32

3+
// namespace object imports won't work in the bundle for function exports
4+
// eslint-disable-next-line @typescript-eslint/no-require-imports
5+
const deepEqual = require('fast-deep-equal');
6+
47
export class Statement {
58
/**
69
* Statement ID

packages/aws-cdk/NOTICE

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
11
AWS Cloud Development Kit (AWS CDK)
22
Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
Third party attributions of this package can be found in the THIRD_PARTY_LICENSES file

packages/aws-cdk/THIRD_PARTY_LICENSES

Lines changed: 3768 additions & 0 deletions
Large diffs are not rendered by default.

packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import * as cxapi from '@aws-cdk/cx-api';
44
import { warning } from '../../logging';
55
import { loadStructuredFile, toYAML } from '../../serialize';
6+
import { rootDir } from '../../util/directories';
67
import { SdkProvider } from '../aws-auth';
78
import { DeployStackResult } from '../deploy-stack';
89
import { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props';
@@ -170,7 +171,7 @@ export class Bootstrapper {
170171
case 'custom':
171172
return loadStructuredFile(this.source.templateFile);
172173
case 'default':
173-
return loadStructuredFile(path.join(__dirname, 'bootstrap-template.yaml'));
174+
return loadStructuredFile(path.join(rootDir(), 'lib', 'api', 'bootstrap', 'bootstrap-template.yaml'));
174175
case 'legacy':
175176
return legacyBootstrapTemplate(params);
176177
}

packages/aws-cdk/lib/api/cloudformation-deployments.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import * as fs from 'fs-extra';
55
import { Tag } from '../cdk-toolkit';
66
import { debug, warning } from '../logging';
77
import { publishAssets } from '../util/asset-publishing';
8-
import { Mode, SdkProvider, ISDK } from './aws-auth';
8+
import { Mode } from './aws-auth/credentials';
9+
import { ISDK } from './aws-auth/sdk';
10+
import { SdkProvider } from './aws-auth/sdk-provider';
911
import { deployStack, DeployStackResult, destroyStack } from './deploy-stack';
1012
import { LazyListStackResources, ListStackResources } from './evaluate-cloudformation-template';
1113
import { ToolkitInfo } from './toolkit-info';

packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import * as cxapi from '@aws-cdk/cx-api';
22
import * as chalk from 'chalk';
3-
import * as minimatch from 'minimatch';
43
import * as semver from 'semver';
54
import { error, print, warning } from '../../logging';
65
import { flatten } from '../../util';
76
import { versionNumber } from '../../version';
87

8+
// namespace object imports won't work in the bundle for function exports
9+
// eslint-disable-next-line @typescript-eslint/no-require-imports
10+
const minimatch = require('minimatch');
11+
12+
913
export enum DefaultSelection {
1014
/**
1115
* Returns an empty selection in case there are no selectors.

packages/aws-cdk/lib/api/cxapp/environments.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import * as cxapi from '@aws-cdk/cx-api';
2-
import * as minimatch from 'minimatch';
32
import { SdkProvider } from '../aws-auth';
43
import { StackCollection } from './cloud-assembly';
54

5+
// namespace object imports won't work in the bundle for function exports
6+
// eslint-disable-next-line @typescript-eslint/no-require-imports
7+
const minimatch = require('minimatch');
8+
69
export function looksLikeGlob(environment: string) {
710
return environment.indexOf('*') > -1;
811
}

packages/aws-cdk/lib/api/hotswap/lambda-functions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Writable } from 'stream';
2-
import * as archiver from 'archiver';
32
import * as AWS from 'aws-sdk';
43
import { flatMap } from '../../util';
54
import { ISDK } from '../aws-auth';
65
import { CfnEvaluationException, EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
76
import { ChangeHotswapImpact, ChangeHotswapResult, HotswapOperation, HotswappableChangeCandidate } from './common';
87

8+
// namespace object imports won't work in the bundle for function exports
9+
// eslint-disable-next-line @typescript-eslint/no-require-imports
10+
const archiver = require('archiver');
11+
912
/**
1013
* Returns `ChangeHotswapImpact.REQUIRES_FULL_DEPLOYMENT` if the change cannot be short-circuited,
1114
* `ChangeHotswapImpact.IRRELEVANT` if the change is irrelevant from a short-circuit perspective
@@ -366,7 +369,7 @@ function zipString(fileName: string, rawString: string): Promise<Buffer> {
366369

367370
const archive = archiver('zip');
368371

369-
archive.on('error', (err) => {
372+
archive.on('error', (err: any) => {
370373
reject(err);
371374
});
372375

packages/aws-cdk/lib/api/util/display.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import * as wrapAnsi from 'wrap-ansi';
1+
// namespace object imports won't work in the bundle for function exports
2+
// eslint-disable-next-line @typescript-eslint/no-require-imports
3+
const wrapAnsi = require('wrap-ansi');
24

35
/**
46
* A class representing rewritable display lines

packages/aws-cdk/lib/cli.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import 'source-map-support/register';
22
import * as cxapi from '@aws-cdk/cx-api';
33
import '@jsii/check-node/run';
44
import * as chalk from 'chalk';
5-
import * as yargs from 'yargs';
65

6+
import type { Argv } from 'yargs';
77
import { SdkProvider } from '../lib/api/aws-auth';
88
import { BootstrapSource, Bootstrapper } from '../lib/api/bootstrap';
99
import { CloudFormationDeployments } from '../lib/api/cloudformation-deployments';
@@ -24,6 +24,11 @@ import { PluginHost } from '../lib/plugin';
2424
import { Command, Configuration, Settings } from '../lib/settings';
2525
import * as version from '../lib/version';
2626

27+
// https://github.com/yargs/yargs/issues/1929
28+
// https://github.com/evanw/esbuild/issues/1492
29+
// eslint-disable-next-line @typescript-eslint/no-require-imports
30+
const yargs = require('yargs');
31+
2732
/* eslint-disable max-len */
2833
/* eslint-disable @typescript-eslint/no-shadow */ // yargs
2934

@@ -73,14 +78,14 @@ async function parseCommandLineArguments() {
7378
.option('output', { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true })
7479
.option('notices', { type: 'boolean', desc: 'Show relevant notices' })
7580
.option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false })
76-
.command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', yargs => yargs
81+
.command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs
7782
.option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }),
7883
)
79-
.command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', yargs => yargs
84+
.command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => yargs
8085
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' })
8186
.option('validation', { type: 'boolean', desc: 'After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION)', default: true })
8287
.option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not output CloudFormation Template to stdout', default: false }))
83-
.command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs
88+
.command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => yargs
8489
.option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined })
8590
.option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' })
8691
.option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' })
@@ -97,7 +102,7 @@ async function parseCommandLineArguments() {
97102
.option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true })
98103
.option('template', { type: 'string', requiresArg: true, desc: 'Use the template from the given file instead of the built-in one (use --show-template to obtain an example)' }),
99104
)
100-
.command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs
105+
.command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', (yargs: Argv) => yargs
101106
.option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' })
102107
.option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] })
103108
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' })
@@ -142,7 +147,7 @@ async function parseCommandLineArguments() {
142147
"Only in effect if specified alongside the '--watch' option",
143148
}),
144149
)
145-
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs
150+
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", (yargs: Argv) => yargs
146151
// I'm fairly certain none of these options, present for 'deploy', make sense for 'watch':
147152
// .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' })
148153
// .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined })
@@ -182,11 +187,11 @@ async function parseCommandLineArguments() {
182187
"'true' by default, use --no-logs to turn off",
183188
}),
184189
)
185-
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
190+
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', (yargs: Argv) => yargs
186191
.option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' })
187192
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' })
188193
.option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' }))
189-
.command('diff [STACKS..]', 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', yargs => yargs
194+
.command('diff [STACKS..]', 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', (yargs: Argv) => yargs
190195
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only diff requested stacks, don\'t include dependencies' })
191196
.option('context-lines', { type: 'number', desc: 'Number of context lines to include in arbitrary JSON diff rendering', default: 3, requiresArg: true })
192197
.option('template', { type: 'string', desc: 'The path to the CloudFormation template to compare with', requiresArg: true })
@@ -196,15 +201,15 @@ async function parseCommandLineArguments() {
196201
.command('metadata [STACK]', 'Returns all metadata associated with this stack')
197202
.command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore')
198203
.command('notices', 'Returns a list of relevant notices')
199-
.command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', yargs => yargs
204+
.command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', (yargs: Argv) => yargs
200205
.option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanguages })
201206
.option('list', { type: 'boolean', desc: 'List the available templates' })
202207
.option('generate-only', { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }),
203208
)
204-
.command('context', 'Manage cached context values', yargs => yargs
209+
.command('context', 'Manage cached context values', (yargs: Argv) => yargs
205210
.option('reset', { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true })
206211
.option('clear', { desc: 'Clear all context', type: 'boolean' }))
207-
.command(['docs', 'doc'], 'Opens the reference documentation in a browser', yargs => yargs
212+
.command(['docs', 'doc'], 'Opens the reference documentation in a browser', (yargs: Argv) => yargs
208213
.option('browser', {
209214
alias: 'b',
210215
desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open',

packages/aws-cdk/lib/command-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as yargs from 'yargs';
1+
import type { Arguments } from 'yargs';
22
import { SdkProvider } from './api/aws-auth';
33
import { Configuration } from './settings';
44

@@ -15,7 +15,7 @@ import { Configuration } from './settings';
1515
* The parts of the world that our command functions have access to
1616
*/
1717
export interface CommandOptions {
18-
args: yargs.Arguments;
18+
args: Arguments;
1919
configuration: Configuration;
2020
aws: SdkProvider;
2121
}

packages/aws-cdk/lib/init.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as chalk from 'chalk';
55
import * as fs from 'fs-extra';
66
import * as semver from 'semver';
77
import { error, print, warning } from './logging';
8-
import { cdkHomeDir } from './util/directories';
8+
import { cdkHomeDir, rootDir } from './util/directories';
99
import { versionNumber } from './version';
1010

1111
export type InvokeHook = (targetDirectory: string) => Promise<void>;
@@ -156,11 +156,11 @@ export class InitTemplate {
156156
}
157157

158158
private expand(template: string, project: ProjectInfo) {
159-
const MATCH_VER_BUILD = /\+[a-f0-9]+$/; // Matches "+BUILD" in "x.y.z-beta+BUILD"
160-
// eslint-disable-next-line @typescript-eslint/no-require-imports
161-
const cdkVersion = require('../package.json').version.replace(MATCH_VER_BUILD, '');
162159
// eslint-disable-next-line @typescript-eslint/no-require-imports
163-
const constructsVersion = require('../package.json').devDependencies.constructs.replace(MATCH_VER_BUILD, '');
160+
const manifest = require(path.join(rootDir(), 'package.json'));
161+
const MATCH_VER_BUILD = /\+[a-f0-9]+$/; // Matches "+BUILD" in "x.y.z-beta+BUILD"
162+
const cdkVersion = manifest.version.replace(MATCH_VER_BUILD, '');
163+
const constructsVersion = manifest.devDependencies.constructs.replace(MATCH_VER_BUILD, '');
164164
return template.replace(/%name%/g, project.name)
165165
.replace(/%name\.camelCased%/g, camelCase(project.name))
166166
.replace(/%name\.PascalCased%/g, camelCase(project.name, { pascalCase: true }))
@@ -212,7 +212,7 @@ function versionedTemplatesDir(): Promise<string> {
212212
currentVersion = '1.0.0';
213213
}
214214
const majorVersion = semver.major(currentVersion);
215-
resolve(path.join(__dirname, 'init-templates', `v${majorVersion}`));
215+
resolve(path.join(rootDir(), 'lib', 'init-templates', `v${majorVersion}`));
216216
});
217217
}
218218

packages/aws-cdk/lib/logging.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ const logger = (stream: Writable, styles?: StyleFn[]) => (fmt: string, ...args:
1313
stream.write(str + '\n');
1414
};
1515

16+
export enum LogLevel {
17+
/** Not verbose at all */
18+
DEFAULT = 0,
19+
/** Pretty verbose */
20+
DEBUG = 1,
21+
/** Extremely verbose */
22+
TRACE = 2
23+
}
24+
25+
1626
export let logLevel = LogLevel.DEFAULT;
1727

1828
export function setLogLevel(newLogLevel: LogLevel) {
@@ -47,12 +57,3 @@ export type LoggerFunction = (fmt: string, ...args: any[]) => void;
4757
export function prefix(prefixString: string, fn: LoggerFunction): LoggerFunction {
4858
return (fmt: string, ...args: any[]) => fn(`%s ${fmt}`, prefixString, ...args);
4959
}
50-
51-
export const enum LogLevel {
52-
/** Not verbose at all */
53-
DEFAULT = 0,
54-
/** Pretty verbose */
55-
DEBUG = 1,
56-
/** Extremely verbose */
57-
TRACE = 2
58-
}

packages/aws-cdk/lib/util/directories.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from 'fs';
12
import * as os from 'os';
23
import * as path from 'path';
34

@@ -9,4 +10,20 @@ export function cdkHomeDir() {
910

1011
export function cdkCacheDir() {
1112
return path.join(cdkHomeDir(), 'cache');
13+
}
14+
15+
export function rootDir() {
16+
17+
function _rootDir(dirname: string): string {
18+
const manifestPath = path.join(dirname, 'package.json');
19+
if (fs.existsSync(manifestPath)) {
20+
return dirname;
21+
}
22+
if (path.dirname(dirname) === dirname) {
23+
throw new Error('Unable to find package manifest');
24+
}
25+
return _rootDir(path.dirname(dirname));
26+
}
27+
28+
return _rootDir(__dirname);
1229
}

packages/aws-cdk/lib/version.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as fs from 'fs-extra';
44
import * as semver from 'semver';
55
import { debug, print } from '../lib/logging';
66
import { formatAsBanner } from '../lib/util/console-formatters';
7-
import { cdkCacheDir } from './util/directories';
7+
import { cdkCacheDir, rootDir } from './util/directories';
88
import { getLatestVersionFromNpm } from './util/npm';
99

1010
const ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60;
@@ -17,12 +17,12 @@ export const DISPLAY_VERSION = `${versionNumber()} (build ${commit()})`;
1717

1818
export function versionNumber(): string {
1919
// eslint-disable-next-line @typescript-eslint/no-require-imports
20-
return require('../package.json').version.replace(/\+[0-9a-f]+$/, '');
20+
return require(path.join(rootDir(), 'package.json')).version.replace(/\+[0-9a-f]+$/, '');
2121
}
2222

2323
function commit(): string {
2424
// eslint-disable-next-line @typescript-eslint/no-require-imports
25-
return require('../build-info.json').commit;
25+
return require(path.join(rootDir(), 'build-info.json')).commit;
2626
}
2727

2828
export class VersionCheckTTL {

packages/aws-cdk/package.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"scripts": {
1111
"build": "cdk-build",
1212
"watch": "cdk-watch",
13-
"lint": "cdk-lint && madge --circular --extensions js lib",
13+
"lint": "cdk-lint",
1414
"pkglint": "pkglint -f",
1515
"test": "cdk-test",
1616
"integ": "jest --testMatch '**/?(*.)+(integ-test).js'",
@@ -28,7 +28,27 @@
2828
"build+test+extract": "yarn build+test"
2929
},
3030
"cdk-package": {
31-
"shrinkWrap": true
31+
"bundle": {
32+
"externals": {
33+
"optionalDependencies": [
34+
"fsevents"
35+
]
36+
},
37+
"resources": {
38+
"../../node_modules/vm2/lib/bridge.js": "lib/bridge.js",
39+
"../../node_modules/vm2/lib/setup-sandbox.js": "lib/setup-sandbox.js"
40+
},
41+
"allowedLicenses": [
42+
"Apache-2.0",
43+
"MIT",
44+
"BSD-3-Clause",
45+
"ISC",
46+
"BSD-2-Clause",
47+
"0BSD"
48+
],
49+
"dontAttribute": "^@aws-cdk/|^cdk-assets$",
50+
"test": "bin/cdk --version"
51+
}
3252
},
3353
"author": {
3454
"name": "Amazon Web Services",

0 commit comments

Comments
 (0)