@@ -60,6 +60,8 @@ export function transformFileContents(filename: string, contents: string, progre
60
60
for ( const [ stmt , binding , moduleName ] of topLevelRequires ) {
61
61
const result = ts . transform ( file , [ ( ctx : ts . TransformationContext ) : ts . Transformer < ts . SourceFile > => {
62
62
const factory = ctx . factory ;
63
+ const gen = new ExpressionGenerator ( factory ) ;
64
+
63
65
const visit : ts . Visitor = node => {
64
66
// If this is the statement, replace it with a function definition
65
67
@@ -82,7 +84,7 @@ export function transformFileContents(filename: string, contents: string, progre
82
84
createVariable ( factory , 'tmp' , factory . createCallExpression ( factory . createIdentifier ( 'require' ) , [ ] , [ factory . createStringLiteral ( moduleName ) ] ) ) ,
83
85
84
86
// <this_fn> = () => tmp
85
- createAssignment ( factory , binding . text ,
87
+ gen . assignmentStatement ( binding . text ,
86
88
factory . createArrowFunction ( undefined , undefined , [ ] , undefined , undefined , factory . createIdentifier ( 'tmp' ) ) ) ,
87
89
88
90
// return tmp
@@ -139,7 +141,7 @@ export function transformFileContents(filename: string, contents: string, progre
139
141
140
142
file = ts . transform ( file , [ ( ctx : ts . TransformationContext ) : ts . Transformer < ts . SourceFile > => {
141
143
const factory = ctx . factory ;
142
- const alreadyEmittedExports = new Set < string > ( ) ;
144
+ const gen = new ExpressionGenerator ( factory ) ;
143
145
144
146
const visit : ts . Visitor = node => {
145
147
if ( node . parent && ts . isSourceFile ( node . parent )
@@ -162,7 +164,7 @@ export function transformFileContents(filename: string, contents: string, progre
162
164
const entries = Object . keys ( module ) ;
163
165
164
166
return entries . flatMap ( ( entry ) =>
165
- createModuleGetterOnce ( alreadyEmittedExports ) ( factory , entry , requiredModule , ( mod ) =>
167
+ gen . moduleGetterOnce ( entry , requiredModule , ( mod ) =>
166
168
factory . createPropertyAccessExpression ( mod , entry ) )
167
169
) ;
168
170
}
@@ -182,7 +184,7 @@ export function transformFileContents(filename: string, contents: string, progre
182
184
183
185
const exportName = node . expression . left . name . text ;
184
186
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 ) ;
186
188
}
187
189
188
190
return ts . visitEachChild ( node , child => visit ( child ) , ctx ) ;
@@ -206,75 +208,135 @@ function createVariable(factory: ts.NodeFactory, name: string | ts.BindingName,
206
208
] ) ) ;
207
209
}
208
210
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 ;
216
214
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
+ }
265
217
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 ) ) {
275
326
return [ ] ;
276
327
}
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
+ }
280
342
}
0 commit comments