Skip to content

Commit 0bb49b4

Browse files
authored
feat(aws-cdk-lib): reduce JavaScript load time, second attempt (#27362)
This is a re-roll of #27217 which had to be reverted (first attempt to fix forward in #27314, then reverted in #27353). The issue with the previous change were: - The mutation was accidentally switched off at the last second. - It did not work for ESM modules, because the code were were generating was not recognized as a CommonJS export by `cjs-module-lexer`. We now generate code that passes the `cjs-module-lexer`, and have tests in place to prove it. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 121378e commit 0bb49b4

File tree

6 files changed

+109
-35
lines changed

6 files changed

+109
-35
lines changed

packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ integTest('Test importing CDK from ESM', withTemporaryDirectory(withPackages(asy
2525

2626
// Rewrite some files
2727
await fs.writeFile(path.join(context.integTestDir, 'new-entrypoint.mjs'), `
28-
// Test two styles of imports
29-
import { Stack, aws_sns as sns, aws_sns_subscriptions as subs, aws_sqs as sqs } from 'aws-cdk-lib';
28+
// Test multiple styles of imports
29+
import { Stack, aws_sns as sns } from 'aws-cdk-lib';
30+
import { SqsSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
31+
import * as sqs from 'aws-cdk-lib/aws-sqs';
3032
import * as cdk from 'aws-cdk-lib';
3133
3234
class TestjsStack extends Stack {
@@ -39,7 +41,7 @@ class TestjsStack extends Stack {
3941
4042
const topic = new sns.Topic(this, 'TestjsTopic');
4143
42-
topic.addSubscription(new subs.SqsSubscription(queue));
44+
topic.addSubscription(new SqsSubscription(queue));
4345
}
4446
}
4547

packages/aws-cdk-lib/custom-resource-handlers/.is_custom_resource

Whitespace-only changes.

tools/@aws-cdk/lazify/lib/index.ts

+62-18
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export function transformFileContents(filename: string, contents: string, progre
139139

140140
file = ts.transform(file, [(ctx: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
141141
const factory = ctx.factory;
142+
const alreadyEmittedExports = new Set<string>();
143+
142144
const visit: ts.Visitor = node => {
143145
if (node.parent && ts.isSourceFile(node.parent)
144146
&& ts.isExpressionStatement(node)
@@ -159,8 +161,8 @@ export function transformFileContents(filename: string, contents: string, progre
159161
const module = require(file);
160162
const entries = Object.keys(module);
161163

162-
return entries.map((entry) =>
163-
createModuleGetter(factory, entry, requiredModule, (mod) =>
164+
return entries.flatMap((entry) =>
165+
createModuleGetterOnce(alreadyEmittedExports)(factory, entry, requiredModule, (mod) =>
164166
factory.createPropertyAccessExpression(mod, entry))
165167
);
166168
}
@@ -180,7 +182,7 @@ export function transformFileContents(filename: string, contents: string, progre
180182

181183
const exportName = node.expression.left.name.text;
182184
const moduleName = node.expression.right.arguments[0].text;
183-
return createModuleGetter(factory, exportName, moduleName, (x) => x);
185+
return createModuleGetterOnce(alreadyEmittedExports)(factory, exportName, moduleName, (x) => x);
184186
}
185187

186188
return ts.visitEachChild(node, child => visit(child), ctx);
@@ -212,25 +214,67 @@ function createAssignment(factory: ts.NodeFactory, name: string, expression: ts.
212214
expression));
213215
}
214216

217+
/**
218+
* Create an lazy getter for a particular value at the module level
219+
*
220+
* Since Node statically analyzes CommonJS modules to determine its exports
221+
* (using the `cjs-module-lexer` module), we need to trick it into recognizing
222+
* these exports as legitimate.
223+
*
224+
* We do that by generating one form it will recognize that doesn't do anything,
225+
* in combination with a form that actually works, that doesn't disqualify the
226+
* export name.
227+
*/
215228
function createModuleGetter(
216229
factory: ts.NodeFactory,
217230
exportName: string,
218231
moduleName: string,
219232
moduleFormatter: (x: ts.Expression) => ts.Expression,
220233
) {
221-
return factory.createExpressionStatement(factory.createCallExpression(
222-
factory.createPropertyAccessExpression(factory.createIdentifier('Object'), factory.createIdentifier('defineProperty')),
223-
undefined,
224-
[
225-
factory.createIdentifier('exports'),
226-
factory.createStringLiteral(exportName),
227-
factory.createObjectLiteralExpression([
228-
factory.createPropertyAssignment('configurable', factory.createTrue()),
229-
factory.createPropertyAssignment('get',
230-
factory.createArrowFunction(undefined, undefined, [], undefined, undefined,
231-
moduleFormatter(
232-
factory.createCallExpression(factory.createIdentifier('require'), undefined, [factory.createStringLiteral(moduleName)])))),
233-
]),
234-
]
235-
));
234+
return [
235+
// exports.<name> = void 0;
236+
factory.createExpressionStatement(factory.createBinaryExpression(
237+
factory.createPropertyAccessExpression(
238+
factory.createIdentifier('exports'),
239+
factory.createIdentifier(exportName)),
240+
ts.SyntaxKind.EqualsToken,
241+
factory.createVoidZero())),
242+
// Object.defineProperty(exports, "<n>" + "<ame>", { get: () => });
243+
factory.createExpressionStatement(factory.createCallExpression(
244+
factory.createPropertyAccessExpression(factory.createIdentifier('Object'), factory.createIdentifier('defineProperty')),
245+
undefined,
246+
[
247+
factory.createIdentifier('exports'),
248+
factory.createBinaryExpression(
249+
factory.createStringLiteral(exportName.substring(0, 1)),
250+
ts.SyntaxKind.PlusToken,
251+
factory.createStringLiteral(exportName.substring(1)),
252+
),
253+
factory.createObjectLiteralExpression([
254+
factory.createPropertyAssignment('enumerable', factory.createTrue()),
255+
factory.createPropertyAssignment('configurable', factory.createTrue()),
256+
factory.createPropertyAssignment('get',
257+
factory.createArrowFunction(undefined, undefined, [], undefined, undefined,
258+
moduleFormatter(
259+
factory.createCallExpression(factory.createIdentifier('require'), undefined, [factory.createStringLiteral(moduleName)])))),
260+
]),
261+
]
262+
)
263+
)];
264+
}
265+
266+
/**
267+
* Prevent emitting an export if it has already been emitted before
268+
*
269+
* This assumes that the symbols have the same definition, and are only duplicated because of
270+
* accidental multiple `export *`s.
271+
*/
272+
function createModuleGetterOnce(alreadyEmittedExports: Set<string>): typeof createModuleGetter {
273+
return (factory, exportName, moduleName, moduleFormatter) => {
274+
if (alreadyEmittedExports.has(exportName)) {
275+
return [];
276+
}
277+
alreadyEmittedExports.add(exportName);
278+
return createModuleGetter(factory, exportName, moduleName, moduleFormatter);
279+
};
236280
}

tools/@aws-cdk/lazify/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"@aws-cdk/cdk-build-tools": "0.0.0",
2121
"jest": "^29",
2222
"ts-jest": "^29",
23-
"typescript": "^4.5.5"
23+
"typescript": "^4.5.5",
24+
"cjs-module-lexer": "^1.2.3"
2425
},
2526
"dependencies": {
2627
"esbuild": "^0.19.4",
+39-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs-extra';
22
import * as path from 'path';
33
import { transformFileContents } from '../lib';
4+
import { parse } from 'cjs-module-lexer';
45

56
// Write a .js file in this directory that will be imported by tests below
67
beforeEach(async () => {
@@ -12,21 +13,47 @@ beforeEach(async () => {
1213

1314
test('replace __exportStar with getters', () => {
1415
const fakeFile = path.join(__dirname, 'index.ts');
15-
expect(transformFileContents(fakeFile, [
16+
17+
const transformed = transformFileContents(fakeFile, [
1618
'__exportStar(require("./some-module"), exports);'
17-
].join('\n'))).toMatchInlineSnapshot(`
18-
"Object.defineProperty(exports, "foo", { configurable: true, get: () => require("./some-module").foo });
19-
Object.defineProperty(exports, "bar", { configurable: true, get: () => require("./some-module").bar });
20-
"
21-
`);
19+
].join('\n'));
20+
21+
expect(parse(transformed).exports).toEqual([
22+
'foo',
23+
'bar',
24+
]);
25+
26+
const mod = evalModule(transformed);
27+
expect(mod.foo()).toEqual('foo');
28+
expect(mod.bar).toEqual(5);
2229
});
2330

2431
test('replace re-export with getter', () => {
2532
const fakeFile = path.join(__dirname, 'index.ts');
26-
expect(transformFileContents(fakeFile, [
33+
const transformed = transformFileContents(fakeFile, [
2734
'exports.some_module = require("./some-module");',
28-
].join('\n'))).toMatchInlineSnapshot(`
29-
"Object.defineProperty(exports, "some_module", { configurable: true, get: () => require("./some-module") });
30-
"
31-
`);
32-
});
35+
].join('\n'));
36+
37+
expect(parse(transformed).exports).toEqual([
38+
'some_module',
39+
]);
40+
41+
const mod = evalModule(transformed);
42+
expect(mod.some_module.foo()).toEqual('foo');
43+
expect(mod.some_module.bar).toEqual(5);
44+
});
45+
46+
/**
47+
* Fake NodeJS evaluation of a module
48+
*/
49+
function evalModule(x: string) {
50+
const code = [
51+
'(function() {',
52+
'const exports = {};',
53+
'const module = { exports };',
54+
x,
55+
'return exports;',
56+
'})()',
57+
].join('\n');
58+
return eval(code);
59+
}

yarn.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -5468,7 +5468,7 @@ cidr-regex@^3.1.1:
54685468
dependencies:
54695469
ip-regex "^4.1.0"
54705470

5471-
cjs-module-lexer@^1.0.0:
5471+
cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.3:
54725472
version "1.2.3"
54735473
resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
54745474
integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==

0 commit comments

Comments
 (0)