Skip to content

Commit cf2e498

Browse files
authored
fix: aws-cdk-lib imports from ESM modules are broken (#23846)
PR #23813 made imports lazy, but in the resulting code, Nodejs no longer recognizes the exports when importing `aws-cdk-lib` from an ESM module. To solve this, vend two different index files: one for use by CJS imports, one for use by ESM imports. ESM modules will still try to load the entire library, so they don't benefit from the speed boost. This is unavoidable: we tried a more complex method that forced ESM to recognize the lazy module references anyway (by tricking the backwards compatibility lexer), but ESM did not experience a speed boost, indicating that it was crawling the entire module irrespective of the submodule accessor's laziness. So, we are opting for the simpler solution of vending two index files instead. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent dd21069 commit cf2e498

File tree

5 files changed

+53
-22
lines changed

5 files changed

+53
-22
lines changed

Diff for: packages/aws-cdk-lib/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
!NOTICE
77
!README.md
88
!scripts/
9+
!scripts/*.ts
10+
!scripts/*.sh
11+
scripts/*.d.ts
912

1013
.LAST_BUILD
1114
*.snk

Diff for: packages/aws-cdk-lib/.npmignore

+2-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ coverage
1414
# Build gear
1515
build-tools
1616
dist
17+
scripts
1718
.LAST_BUILD
1819
.LAST_PACKAGE
1920

@@ -23,6 +24,7 @@ tsconfig.json
2324
!.jsii
2425
!.jsii.gz
2526
.eslintrc.js
27+
2628
# exclude cdk artifacts
2729
**/cdk.out
2830
junit.xml
@@ -31,8 +33,3 @@ junit.xml
3133

3234
# exclude source maps as they only work locally
3335
*.map
34-
35-
# exclude tempory export files
36-
exports.d.ts
37-
exports.js
38-
exports.js.map

Diff for: packages/aws-cdk-lib/package.json

+13-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
"stripDeprecated": true,
3939
"compressAssembly": true,
4040
"post": [
41-
"cp exports.js index.js",
4241
"node ./scripts/verify-imports-resolve-same.js",
4342
"node ./scripts/verify-imports-shielded.js",
4443
"/bin/bash ./scripts/minify-sources.sh"
@@ -407,7 +406,10 @@
407406
"excludeExperimentalModules": true
408407
},
409408
"exports": {
410-
".": "./index.js",
409+
".": {
410+
"import": "./index.js",
411+
"require": "./lazy-index.js"
412+
},
411413
"./package.json": "./package.json",
412414
"./.jsii": "./.jsii",
413415
"./.warnings.jsii.js": "./.warnings.jsii.js",
@@ -481,6 +483,7 @@
481483
"./aws-dlm": "./aws-dlm/index.js",
482484
"./aws-dms": "./aws-dms/index.js",
483485
"./aws-docdb": "./aws-docdb/index.js",
486+
"./aws-docdbelastic": "./aws-docdbelastic/index.js",
484487
"./aws-dynamodb": "./aws-dynamodb/index.js",
485488
"./aws-ec2": "./aws-ec2/index.js",
486489
"./aws-ecr": "./aws-ecr/index.js",
@@ -513,6 +516,7 @@
513516
"./aws-globalaccelerator": "./aws-globalaccelerator/index.js",
514517
"./aws-globalaccelerator-endpoints": "./aws-globalaccelerator-endpoints/index.js",
515518
"./aws-glue": "./aws-glue/index.js",
519+
"./aws-grafana": "./aws-grafana/index.js",
516520
"./aws-greengrass": "./aws-greengrass/index.js",
517521
"./aws-greengrassv2": "./aws-greengrassv2/index.js",
518522
"./aws-groundstation": "./aws-groundstation/index.js",
@@ -537,6 +541,7 @@
537541
"./aws-ivs": "./aws-ivs/index.js",
538542
"./aws-kafkaconnect": "./aws-kafkaconnect/index.js",
539543
"./aws-kendra": "./aws-kendra/index.js",
544+
"./aws-kendraranking": "./aws-kendraranking/index.js",
540545
"./aws-kinesis": "./aws-kinesis/index.js",
541546
"./aws-kinesisanalytics": "./aws-kinesisanalytics/index.js",
542547
"./aws-kinesisanalyticsv2": "./aws-kinesisanalyticsv2/index.js",
@@ -573,13 +578,17 @@
573578
"./aws-networkfirewall": "./aws-networkfirewall/index.js",
574579
"./aws-networkmanager": "./aws-networkmanager/index.js",
575580
"./aws-nimblestudio": "./aws-nimblestudio/index.js",
581+
"./aws-oam": "./aws-oam/index.js",
582+
"./aws-opensearchserverless": "./aws-opensearchserverless/index.js",
576583
"./aws-opensearchservice": "./aws-opensearchservice/index.js",
577584
"./aws-opsworks": "./aws-opsworks/index.js",
578585
"./aws-opsworkscm": "./aws-opsworkscm/index.js",
586+
"./aws-organizations": "./aws-organizations/index.js",
579587
"./aws-panorama": "./aws-panorama/index.js",
580588
"./aws-personalize": "./aws-personalize/index.js",
581589
"./aws-pinpoint": "./aws-pinpoint/index.js",
582590
"./aws-pinpointemail": "./aws-pinpointemail/index.js",
591+
"./aws-pipes": "./aws-pipes/index.js",
583592
"./aws-qldb": "./aws-qldb/index.js",
584593
"./aws-quicksight": "./aws-quicksight/index.js",
585594
"./aws-ram": "./aws-ram/index.js",
@@ -589,6 +598,7 @@
589598
"./aws-refactorspaces": "./aws-refactorspaces/index.js",
590599
"./aws-rekognition": "./aws-rekognition/index.js",
591600
"./aws-resiliencehub": "./aws-resiliencehub/index.js",
601+
"./aws-resourceexplorer2": "./aws-resourceexplorer2/index.js",
592602
"./aws-resourcegroups": "./aws-resourcegroups/index.js",
593603
"./aws-robomaker": "./aws-robomaker/index.js",
594604
"./aws-rolesanywhere": "./aws-rolesanywhere/index.js",
@@ -607,6 +617,7 @@
607617
"./aws-s3outposts": "./aws-s3outposts/index.js",
608618
"./aws-sagemaker": "./aws-sagemaker/index.js",
609619
"./aws-sam": "./aws-sam/index.js",
620+
"./aws-scheduler": "./aws-scheduler/index.js",
610621
"./aws-sdb": "./aws-sdb/index.js",
611622
"./aws-secretsmanager": "./aws-secretsmanager/index.js",
612623
"./aws-securityhub": "./aws-securityhub/index.js",

Diff for: packages/aws-cdk-lib/scripts/minify-sources.sh

+1-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
scriptdir=$(cd $(dirname $0) && pwd)
1717
cd ${scriptdir}/..
1818

19-
find . -name '*.js' ! -name 'exports.js' ! -name '.eslintrc.js' ! -path '*node_modules*' | xargs npx esbuild \
20-
--sourcemap \
19+
find . -name '*.js' ! -name '.eslintrc.js' ! -path '*node_modules*' | xargs npx esbuild \
2120
--platform=node \
2221
--format=cjs \
2322
--minify-whitespace \

Diff for: tools/@aws-cdk/ubergen/bin/ubergen.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ interface LibraryReference {
4747
readonly shortName: string;
4848
}
4949

50+
type Export = string | {
51+
readonly import?: string;
52+
readonly require?: string;
53+
};
54+
5055
interface PackageJson {
5156
readonly main?: string;
5257
readonly description?: string;
@@ -103,7 +108,7 @@ interface PackageJson {
103108
*/
104109
readonly exports?: Record<string, string>;
105110
};
106-
exports?: Record<string, string>;
111+
exports?: Record<string, Export>;
107112
}
108113

109114
/**
@@ -269,7 +274,10 @@ async function prepareSourceFiles(libraries: readonly LibraryReference[], packag
269274
// Control 'exports' field of the 'package.json'. This will control what kind of 'import' statements are
270275
// allowed for this package: we only want to allow the exact import statements that we want to support.
271276
packageJson.exports = {
272-
'.': './index.js',
277+
'.': {
278+
import: './index.js',
279+
require: './lazy-index.js',
280+
},
273281

274282
// We need to expose 'package.json' and '.jsii' because 'jsii' and 'jsii-reflect' load them using
275283
// require(). (-_-). Can be removed after https://github.com/aws/jsii/pull/3205 gets merged.
@@ -281,12 +289,15 @@ async function prepareSourceFiles(libraries: readonly LibraryReference[], packag
281289
};
282290

283291
// We use the index.ts to compile type definitions.
284-
// At the very end we replace the compiled index.js file with our fixed version exports.js.
285-
// exports.js has the top level submodules exports defined as a getter function,
286-
// so they are not automatically loaded when importing from `aws-cdk-lib`.
292+
//
293+
// We build two indexes: one for eager loading (used by ESM modules), and one
294+
// for lazy loading (used by CJS modules). The lazy loading will result in faster
295+
// loading times, because we don't have to load and parse all submodules right away,
296+
// but is not compatible with ESM's loading algorithm.
297+
//
287298
// This improves AWS CDK app performance by ~400ms.
288299
const indexStatements = new Array<string>();
289-
const exportsStatements = new Array<string>();
300+
const lazyExports = new Array<string>();
290301

291302
for (const library of libraries) {
292303
const libDir = path.join(libRoot, library.shortName);
@@ -297,19 +308,21 @@ async function prepareSourceFiles(libraries: readonly LibraryReference[], packag
297308
}
298309
if (library.shortName === 'core') {
299310
indexStatements.push(`export * from './${library.shortName}';`);
300-
exportsStatements.unshift(`export * from './${library.shortName}';`);
311+
lazyExports.unshift(`export * from './${library.shortName}';`);
301312
} else {
302-
indexStatements.push(`export * as ${library.shortName.replace(/-/g, '_')} from './${library.shortName}';`);
303-
exportsStatements.push(`Object.defineProperty(exports, '${library.shortName.replace(/-/g, '_')}', { get: function () { return require('./${library.shortName}'); } });`);
313+
const exportName = library.shortName.replace(/-/g, '_');
314+
315+
indexStatements.push(`export * as ${exportName} from './${library.shortName}';`);
316+
lazyExports.push(`Object.defineProperty(exports, '${exportName}', { get: function () { return require('./${library.shortName}'); } });`);
304317
}
305318
copySubmoduleExports(packageJson.exports, library, library.shortName);
306319
}
307320

308321
// make the exports.ts file pass linting
309-
exportsStatements.unshift('/* eslint-disable @typescript-eslint/no-require-imports */');
322+
lazyExports.unshift('/* eslint-disable @typescript-eslint/no-require-imports */');
310323

311324
await fs.writeFile(path.join(libRoot, 'index.ts'), indexStatements.join('\n'), { encoding: 'utf8' });
312-
await fs.writeFile(path.join(libRoot, 'exports.ts'), exportsStatements.join('\n'), { encoding: 'utf8' });
325+
await fs.writeFile(path.join(libRoot, 'lazy-index.ts'), lazyExports.join('\n'), { encoding: 'utf8' });
313326

314327
console.log('\t🍺 Success!');
315328
}
@@ -320,13 +333,21 @@ async function prepareSourceFiles(libraries: readonly LibraryReference[], packag
320333
* Replace the original 'main' export with an export of the new '<submodule>/index.ts` file we've written
321334
* in 'transformPackage'.
322335
*/
323-
function copySubmoduleExports(targetExports: Record<string, string>, library: LibraryReference, subdirectory: string) {
336+
function copySubmoduleExports(targetExports: Record<string, Export>, library: LibraryReference, subdirectory: string) {
324337
const visibleName = library.shortName;
325338

326339
// Do both REAL "exports" section, as well as virtual, ubergen-only "exports" section
327340
for (const exportSet of [library.packageJson.exports, library.packageJson.ubergen?.exports]) {
328341
for (const [relPath, relSource] of Object.entries(exportSet ?? {})) {
329-
targetExports[`./${unixPath(path.join(visibleName, relPath))}`] = `./${unixPath(path.join(subdirectory, relSource))}`;
342+
targetExports[`./${unixPath(path.join(visibleName, relPath))}`] = resolveExport(relSource);
343+
}
344+
}
345+
346+
function resolveExport<A extends Export>(exp: A): A {
347+
if (typeof exp === 'string') {
348+
return `./${unixPath(path.join(subdirectory, exp))}` as any;
349+
} else {
350+
return Object.fromEntries(Object.entries(exp).map(([k, v]) => [k, v ? resolveExport(v) : undefined])) as any;
330351
}
331352
}
332353

0 commit comments

Comments
 (0)