diff --git a/tools/@aws-cdk/lazify/lib/index.ts b/tools/@aws-cdk/lazify/lib/index.ts index 62bfa368e17d5..298c7dd99b089 100644 --- a/tools/@aws-cdk/lazify/lib/index.ts +++ b/tools/@aws-cdk/lazify/lib/index.ts @@ -160,6 +160,7 @@ export function transformFileContents(filename: string, contents: string, progre const file = require.resolve(requiredModule, { paths: [path.dirname(filename)] }); // FIXME: Should probably do this in a subprocess + // FIXME: Maybe we should use the cjs-lexer const module = require(file); const entries = Object.keys(module); @@ -218,6 +219,9 @@ class ExpressionGenerator { /** * Create an lazy getter for a particular value at the module level * + * The name, and the lexer + * ----------------------- + * * Since Node statically analyzes CommonJS modules to determine its exports * (using the `cjs-module-lexer` module), we need to trick it into recognizing * these exports as legitimate. @@ -240,7 +244,7 @@ class ExpressionGenerator { * * ``` * exports.myExport = void 0; - * Object.defineProperty(exports', 'm' + 'yExport', { ... }); + * Object.defineProperty(exports, 'm' + 'yExport', { ... }); * ``` * * Then the code passes the lexer: it detects `myExport` as an export, and it @@ -258,11 +262,33 @@ class ExpressionGenerator { * ``` * let _noFold; * exports.myExport = void 0; - * Object.defineProperty(exports', _noFold = 'myExport', { ... }); + * Object.defineProperty(exports, _noFold = 'myExport', { ... }); * ``` * * This takes advantage of the fact that the return value of an ` = ` expression * returns ``, but has a side effect so cannot be safely optimized away. + * + * The returned value + * ------------------ + * + * If we only generate a getter: + * + * ``` + * Object.defineProperty(exports, _noFold = 'myExport', { get: () => require('./file').myExport }); + * ``` + * + * If the same member is requested more than once, the same getter will be + * executed multiple times. What we'll do instead is reify the lazy value on + * the `exports` object, so that the getter is only executed on the first access, + * and subsequent accesses and read the value directly. + * + * ``` + * Object.defineProperty(exports, _noFold = 'myExport', { get: () => { + * const value = require('./file').myExport; + * Object.defineProperty(exports, _noFold = 'myExport', { value }); + * return value; + * }); + * ``` */ public moduleGetter( exportName: string, @@ -273,11 +299,7 @@ class ExpressionGenerator { const ret = []; if (!this.emittedNoFold) { - ret.push( - factory.createVariableStatement([], - factory.createVariableDeclarationList([ - factory.createVariableDeclaration('_noFold'), - ]))); + ret.push(this.createVariables(factory.createVariableDeclaration('_noFold'))); this.emittedNoFold = true; } @@ -291,7 +313,31 @@ class ExpressionGenerator { ts.SyntaxKind.EqualsToken, factory.createVoidZero())), // Object.defineProperty(exports, _noFold = "", { get: () => ... }); - factory.createExpressionStatement(factory.createCallExpression( + this.createDefinePropertyStatement(exportName, [ + factory.createPropertyAssignment('get', + factory.createArrowFunction(undefined, undefined, [], undefined, undefined, + factory.createBlock([ + this.createVariables(factory.createVariableDeclaration('value', undefined, undefined, + moduleFormatter( + factory.createCallExpression(factory.createIdentifier('require'), undefined, [factory.createStringLiteral(moduleName)])))), + this.createDefinePropertyStatement(exportName, [factory.createShorthandPropertyAssignment(factory.createIdentifier('value'))]), + factory.createReturnStatement(factory.createIdentifier('value')), + ]), + ), + ), + ]), + ); + return ret; + } + + private createVariables(...vars: ts.VariableDeclaration[]) { + return this.factory.createVariableStatement([], this.factory.createVariableDeclarationList(vars)); + } + + private createDefinePropertyStatement(exportName: string, members: ts.ObjectLiteralElementLike[]) { + const factory = this.factory; + + return factory.createExpressionStatement(factory.createCallExpression( factory.createPropertyAccessExpression(factory.createIdentifier('Object'), factory.createIdentifier('defineProperty')), undefined, [ @@ -300,15 +346,10 @@ class ExpressionGenerator { factory.createObjectLiteralExpression([ factory.createPropertyAssignment('enumerable', factory.createTrue()), factory.createPropertyAssignment('configurable', factory.createTrue()), - factory.createPropertyAssignment('get', - factory.createArrowFunction(undefined, undefined, [], undefined, undefined, - moduleFormatter( - factory.createCallExpression(factory.createIdentifier('require'), undefined, [factory.createStringLiteral(moduleName)])))), + ...members, ]), ] - ) - )); - return ret; + )); } /** diff --git a/tools/@aws-cdk/lazify/test/no-double-getter.test.ts b/tools/@aws-cdk/lazify/test/no-double-getter.test.ts new file mode 100644 index 0000000000000..d6b0ee2752c38 --- /dev/null +++ b/tools/@aws-cdk/lazify/test/no-double-getter.test.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { transformFileContents } from '../lib'; +import { parse } from 'cjs-module-lexer'; + +// Write a .js file in this directory that will be imported by tests below +beforeEach(async () => { + await fs.writeFile(path.join(__dirname, 'some-module.js'), [ + 'Object.defineProperty(module.exports, "foo", {', + // Necessary otherwise the way we find exported symbols (by actually including the file and iterating keys) + // won't find this symbol. + ' enumerable: true,', + ' get: () => {', + ' console.log("evaluating getter");', + ' return 42;', + ' }', + '})', + ].join('\n'), { encoding: 'utf-8' }); +}); + +test('replace re-export with getter', () => { + const fakeFile = path.join(__dirname, 'index.ts'); + const transformed = transformFileContents(fakeFile, [ + '__exportStar(require("./some-module"), exports);' + ].join('\n')); + + const mod = evalModule(transformed); + + const logMock = jest.spyOn(console, 'log'); + expect(mod.foo).toEqual(42); + expect(mod.foo).toEqual(42); + + expect(logMock).toHaveBeenCalledTimes(1); +}); + +/** + * Fake NodeJS evaluation of a module + */ +function evalModule(x: string) { + const code = [ + '(function() {', + 'const exports = {};', + 'const module = { exports };', + x, + 'return exports;', + '})()', + ].join('\n'); + return eval(code); +}