Skip to content

Commit c48caac

Browse files
authored
chore: fix pipeline by finding another generated form (#27429)
We had found an expression that the `cjs-module-lexer` would accept, but the `esbuild` pass we run during packaging destroys that form. Find another form that also works and is preserved by `esbuild`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7b0f824 commit c48caac

File tree

1 file changed

+133
-71
lines changed

1 file changed

+133
-71
lines changed

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

+133-71
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export function transformFileContents(filename: string, contents: string, progre
6060
for (const [stmt, binding, moduleName] of topLevelRequires) {
6161
const result = ts.transform(file, [(ctx: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
6262
const factory = ctx.factory;
63+
const gen = new ExpressionGenerator(factory);
64+
6365
const visit: ts.Visitor = node => {
6466
// If this is the statement, replace it with a function definition
6567

@@ -82,7 +84,7 @@ export function transformFileContents(filename: string, contents: string, progre
8284
createVariable(factory, 'tmp', factory.createCallExpression(factory.createIdentifier('require'), [], [factory.createStringLiteral(moduleName)])),
8385

8486
// <this_fn> = () => tmp
85-
createAssignment(factory, binding.text,
87+
gen.assignmentStatement(binding.text,
8688
factory.createArrowFunction(undefined, undefined, [], undefined, undefined, factory.createIdentifier('tmp'))),
8789

8890
// return tmp
@@ -139,7 +141,7 @@ export function transformFileContents(filename: string, contents: string, progre
139141

140142
file = ts.transform(file, [(ctx: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
141143
const factory = ctx.factory;
142-
const alreadyEmittedExports = new Set<string>();
144+
const gen = new ExpressionGenerator(factory);
143145

144146
const visit: ts.Visitor = node => {
145147
if (node.parent && ts.isSourceFile(node.parent)
@@ -162,7 +164,7 @@ export function transformFileContents(filename: string, contents: string, progre
162164
const entries = Object.keys(module);
163165

164166
return entries.flatMap((entry) =>
165-
createModuleGetterOnce(alreadyEmittedExports)(factory, entry, requiredModule, (mod) =>
167+
gen.moduleGetterOnce(entry, requiredModule, (mod) =>
166168
factory.createPropertyAccessExpression(mod, entry))
167169
);
168170
}
@@ -182,7 +184,7 @@ export function transformFileContents(filename: string, contents: string, progre
182184

183185
const exportName = node.expression.left.name.text;
184186
const moduleName = node.expression.right.arguments[0].text;
185-
return createModuleGetterOnce(alreadyEmittedExports)(factory, exportName, moduleName, (x) => x);
187+
return gen.moduleGetterOnce(exportName, moduleName, (x) => x);
186188
}
187189

188190
return ts.visitEachChild(node, child => visit(child), ctx);
@@ -206,75 +208,135 @@ function createVariable(factory: ts.NodeFactory, name: string | ts.BindingName,
206208
]));
207209
}
208210

209-
function createAssignment(factory: ts.NodeFactory, name: string, expression: ts.Expression) {
210-
return factory.createExpressionStatement(
211-
factory.createBinaryExpression(
212-
factory.createIdentifier(name),
213-
ts.SyntaxKind.EqualsToken,
214-
expression));
215-
}
211+
class ExpressionGenerator {
212+
private alreadyEmittedExports = new Set<string>();
213+
private emittedNoFold = false;
216214

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-
*/
228-
function createModuleGetter(
229-
factory: ts.NodeFactory,
230-
exportName: string,
231-
moduleName: string,
232-
moduleFormatter: (x: ts.Expression) => ts.Expression,
233-
) {
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-
}
215+
constructor(private readonly factory: ts.NodeFactory) {
216+
}
265217

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)) {
218+
/**
219+
* Create an lazy getter for a particular value at the module level
220+
*
221+
* Since Node statically analyzes CommonJS modules to determine its exports
222+
* (using the `cjs-module-lexer` module), we need to trick it into recognizing
223+
* these exports as legitimate.
224+
*
225+
* We do that by generating one form it will recognize that doesn't do anything,
226+
* in combination with a form that actually works, that doesn't disqualify the
227+
* export name, and that doesn't get collapsed by esbuild.
228+
*
229+
* If we do:
230+
*
231+
* ```
232+
* exports.myExport = void 0;
233+
* Object.defineProperty(exports, 'myExport', { ... });
234+
* ```
235+
*
236+
* Then the lexer detects conflicting definitions of `myExport`, one of which is
237+
* not supported, and it disqualifies the name for being exported.
238+
*
239+
* If we do:
240+
*
241+
* ```
242+
* exports.myExport = void 0;
243+
* Object.defineProperty(exports', 'm' + 'yExport', { ... });
244+
* ```
245+
*
246+
* Then the code passes the lexer: it detects `myExport` as an export, and it
247+
* doesn't detect the disqualifying export.
248+
*
249+
* However, that last syntax is detected and constant-folded by `esbuild` (which
250+
* we run to minify all files)! So esbuild turns `'m' + 'yExport'` back into
251+
* `'myExport'`, and then the lexer detects it again as a disqualifying export!
252+
*
253+
* So we need to find an expression that won't be constant-folded by esbuild, and
254+
* won't be detected by the lexer.
255+
*
256+
* This is what we'll be generating:
257+
*
258+
* ```
259+
* let _noFold;
260+
* exports.myExport = void 0;
261+
* Object.defineProperty(exports', _noFold = 'myExport', { ... });
262+
* ```
263+
*
264+
* This takes advantage of the fact that the return value of an `<x> = <y>` expression
265+
* returns `<y>`, but has a side effect so cannot be safely optimized away.
266+
*/
267+
public moduleGetter(
268+
exportName: string,
269+
moduleName: string,
270+
moduleFormatter: (x: ts.Expression) => ts.Expression,
271+
) {
272+
const factory = this.factory;
273+
274+
const ret = [];
275+
if (!this.emittedNoFold) {
276+
ret.push(
277+
factory.createVariableStatement([],
278+
factory.createVariableDeclarationList([
279+
factory.createVariableDeclaration('_noFold'),
280+
])));
281+
282+
this.emittedNoFold = true;
283+
}
284+
285+
ret.push(
286+
// exports.<name> = void 0;
287+
factory.createExpressionStatement(factory.createBinaryExpression(
288+
factory.createPropertyAccessExpression(
289+
factory.createIdentifier('exports'),
290+
factory.createIdentifier(exportName)),
291+
ts.SyntaxKind.EqualsToken,
292+
factory.createVoidZero())),
293+
// Object.defineProperty(exports, _noFold = "<name>", { get: () => ... });
294+
factory.createExpressionStatement(factory.createCallExpression(
295+
factory.createPropertyAccessExpression(factory.createIdentifier('Object'), factory.createIdentifier('defineProperty')),
296+
undefined,
297+
[
298+
factory.createIdentifier('exports'),
299+
this.assignment('_noFold', factory.createStringLiteral(exportName)),
300+
factory.createObjectLiteralExpression([
301+
factory.createPropertyAssignment('enumerable', factory.createTrue()),
302+
factory.createPropertyAssignment('configurable', factory.createTrue()),
303+
factory.createPropertyAssignment('get',
304+
factory.createArrowFunction(undefined, undefined, [], undefined, undefined,
305+
moduleFormatter(
306+
factory.createCallExpression(factory.createIdentifier('require'), undefined, [factory.createStringLiteral(moduleName)])))),
307+
]),
308+
]
309+
)
310+
));
311+
return ret;
312+
}
313+
314+
/**
315+
* Prevent emitting an export if it has already been emitted before
316+
*
317+
* This assumes that the symbols have the same definition, and are only duplicated because of
318+
* accidental multiple `export *`s.
319+
*/
320+
public moduleGetterOnce(
321+
exportName: string,
322+
moduleName: string,
323+
moduleFormatter: (x: ts.Expression) => ts.Expression,
324+
): ReturnType<ExpressionGenerator['moduleGetter']> {
325+
if (this.alreadyEmittedExports.has(exportName)) {
275326
return [];
276327
}
277-
alreadyEmittedExports.add(exportName);
278-
return createModuleGetter(factory, exportName, moduleName, moduleFormatter);
279-
};
328+
this.alreadyEmittedExports.add(exportName);
329+
return this.moduleGetter(exportName, moduleName, moduleFormatter);
330+
}
331+
332+
public assignment(name: string, expression: ts.Expression) {
333+
return this.factory.createBinaryExpression(
334+
this.factory.createIdentifier(name),
335+
ts.SyntaxKind.EqualsToken,
336+
expression);
337+
}
338+
339+
public assignmentStatement(name: string, expression: ts.Expression) {
340+
return this.factory.createExpressionStatement(this.assignment(name, expression));
341+
}
280342
}

0 commit comments

Comments
 (0)