Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions tools/@aws-cdk/lazify/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 `<x> = <y>` expression
* returns `<y>`, 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,
Expand All @@ -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;
}
Expand All @@ -291,7 +313,31 @@ class ExpressionGenerator {
ts.SyntaxKind.EqualsToken,
factory.createVoidZero())),
// Object.defineProperty(exports, _noFold = "<name>", { 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,
[
Expand All @@ -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;
));
}

/**
Expand Down
49 changes: 49 additions & 0 deletions tools/@aws-cdk/lazify/test/no-double-getter.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading