Skip to content

Commit

Permalink
Correctly deal with ESM imports
Browse files Browse the repository at this point in the history
  • Loading branch information
rix0rrr committed Sep 30, 2023
1 parent 21a1277 commit 9837489
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ integTest('Test importing CDK from ESM', withTemporaryDirectory(withPackages(asy

// Rewrite some files
await fs.writeFile(path.join(context.integTestDir, 'new-entrypoint.mjs'), `
// Test two styles of imports
import { Stack, aws_sns as sns, aws_sns_subscriptions as subs, aws_sqs as sqs } from 'aws-cdk-lib';
// Test multiple styles of imports
import { Stack, aws_sns as sns } from 'aws-cdk-lib';
import { SqsSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as cdk from 'aws-cdk-lib';
class TestjsStack extends Stack {
Expand All @@ -39,7 +41,7 @@ class TestjsStack extends Stack {
const topic = new sns.Topic(this, 'TestjsTopic');
topic.addSubscription(new subs.SqsSubscription(queue));
topic.addSubscription(new SqsSubscription(queue));
}
}
Expand Down
58 changes: 42 additions & 16 deletions tools/@aws-cdk/lazify/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export function transformFileContents(filename: string, contents: string, progre
const module = require(file);
const entries = Object.keys(module);

return entries.map((entry) =>
return entries.flatMap((entry) =>
createModuleGetter(factory, entry, requiredModule, (mod) =>
factory.createPropertyAccessExpression(mod, entry))
);
Expand Down Expand Up @@ -212,25 +212,51 @@ function createAssignment(factory: ts.NodeFactory, name: string, expression: ts.
expression));
}

/**
* Create an lazy getter for a particular value at the module level
*
* 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.
*
* We do that by generating one form it will recognize that doesn't do anything,
* in combination with a form that actually works, that doesn't disqualify the
* export name.
*/
function createModuleGetter(
factory: ts.NodeFactory,
exportName: string,
moduleName: string,
moduleFormatter: (x: ts.Expression) => ts.Expression,
) {
return factory.createExpressionStatement(factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('Object'), factory.createIdentifier('defineProperty')),
undefined,
[
factory.createIdentifier('exports'),
factory.createStringLiteral(exportName),
factory.createObjectLiteralExpression([
factory.createPropertyAssignment('configurable', factory.createTrue()),
factory.createPropertyAssignment('get',
factory.createArrowFunction(undefined, undefined, [], undefined, undefined,
moduleFormatter(
factory.createCallExpression(factory.createIdentifier('require'), undefined, [factory.createStringLiteral(moduleName)])))),
]),
]
));
return [
// exports.<name> = void 0;
factory.createExpressionStatement(factory.createBinaryExpression(
factory.createPropertyAccessExpression(
factory.createIdentifier('exports'),
factory.createIdentifier(exportName)),
ts.SyntaxKind.EqualsToken,
factory.createVoidZero())),
// Object.defineProperty(exports, "<n>" + "<ame>", { get: () => });
factory.createExpressionStatement(factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('Object'), factory.createIdentifier('defineProperty')),
undefined,
[
factory.createIdentifier('exports'),
factory.createBinaryExpression(
factory.createStringLiteral(exportName.substring(0, 1)),
ts.SyntaxKind.PlusToken,
factory.createStringLiteral(exportName.substring(1)),
),
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)])))),
]),
]
)
)];
}
3 changes: 2 additions & 1 deletion tools/@aws-cdk/lazify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"@aws-cdk/cdk-build-tools": "0.0.0",
"jest": "^29",
"ts-jest": "^29",
"typescript": "^4.5.5"
"typescript": "^4.5.5",
"cjs-module-lexer": "^1.2.3"
},
"dependencies": {
"esbuild": "^0.19.2",
Expand Down
51 changes: 39 additions & 12 deletions tools/@aws-cdk/lazify/test/export-star.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 () => {
Expand All @@ -12,21 +13,47 @@ beforeEach(async () => {

test('replace __exportStar with getters', () => {
const fakeFile = path.join(__dirname, 'index.ts');
expect(transformFileContents(fakeFile, [

const transformed = transformFileContents(fakeFile, [
'__exportStar(require("./some-module"), exports);'
].join('\n'))).toMatchInlineSnapshot(`
"Object.defineProperty(exports, "foo", { configurable: true, get: () => require("./some-module").foo });
Object.defineProperty(exports, "bar", { configurable: true, get: () => require("./some-module").bar });
"
`);
].join('\n'));

expect(parse(transformed).exports).toEqual([
'foo',
'bar',
]);

const mod = evalModule(transformed);
expect(mod.foo()).toEqual('foo');
expect(mod.bar).toEqual(5);
});

test('replace re-export with getter', () => {
const fakeFile = path.join(__dirname, 'index.ts');
expect(transformFileContents(fakeFile, [
const transformed = transformFileContents(fakeFile, [
'exports.some_module = require("./some-module");',
].join('\n'))).toMatchInlineSnapshot(`
"Object.defineProperty(exports, "some_module", { configurable: true, get: () => require("./some-module") });
"
`);
});
].join('\n'));

expect(parse(transformed).exports).toEqual([
'some_module',
]);

const mod = evalModule(transformed);
expect(mod.some_module.foo()).toEqual('foo');
expect(mod.some_module.bar).toEqual(5);
});

/**
* 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);
}

0 comments on commit 9837489

Please sign in to comment.