1
1
import { readFile , writeFile , mkdir , rm , readdir , copyFile } from 'fs/promises' ;
2
2
import { exit } from 'process' ;
3
3
import { dirname , join , relative , resolve } from 'path' ;
4
- import type { Node } from 'acorn' ;
5
4
import { parse } from 'acorn' ;
6
5
import { generate } from 'astring' ;
7
6
import {
@@ -15,6 +14,8 @@ import {
15
14
import type { CliOptions } from '../cli' ;
16
15
import { cliError , cliWarn } from '../cli' ;
17
16
import { tmpdir } from 'os' ;
17
+ import type * as AST from 'ast-types/gen/kinds' ;
18
+ import assert from 'node:assert' ;
18
19
19
20
/**
20
21
* Creates new files containing the Vercel built functions but adjusted so that they can be later
@@ -283,69 +284,53 @@ function extractWebpackChunks(
283
284
const parsedContents = parse ( functionContents , {
284
285
ecmaVersion : 'latest' ,
285
286
sourceType : 'module' ,
286
- } ) as Node & { body : LooseNode [ ] } ;
287
-
288
- const expressions = parsedContents . body
289
- . filter (
290
- ( { type, expression } ) =>
291
- type === 'ExpressionStatement' &&
292
- expression ?. type === 'CallExpression' &&
293
- expression . callee ?. type === 'MemberExpression' &&
294
- expression . callee . object ?. type === 'AssignmentExpression' &&
295
- expression . callee . object . left ?. object ?. name === 'self' &&
296
- expression . callee . object . left . property ?. name === 'webpackChunk_N_E' &&
297
- expression . arguments ?. [ 0 ] ?. elements ?. [ 1 ] ?. type === 'ObjectExpression'
298
- )
299
- . map (
300
- node => node ?. expression ?. arguments ?. [ 0 ] ?. elements ?. [ 1 ] ?. properties
301
- ) as LooseNode [ ] [ ] ;
302
-
303
- for ( const objectOfChunks of expressions ) {
304
- for ( const chunkExpression of objectOfChunks ) {
305
- const key = chunkExpression ?. key ?. value ;
306
- if ( key in existingWebpackChunks ) {
307
- if (
308
- existingWebpackChunks . get ( key ) !== generate ( chunkExpression . value )
309
- ) {
310
- cliError (
311
- `
287
+ } ) as unknown as AST . ProgramKind ;
288
+
289
+ const chunks = parsedContents . body . flatMap ( getWebpackChunksFromStatement ) ;
290
+
291
+ for ( const chunk of chunks ) {
292
+ const key = ( chunk . key as AST . NumericLiteralKind ) . value ;
293
+
294
+ if ( key in existingWebpackChunks ) {
295
+ if ( existingWebpackChunks . get ( key ) !== generate ( chunk . value ) ) {
296
+ cliError (
297
+ `
312
298
ERROR: Detected a collision with '--experimental-minify'.
313
299
Try removing the '--experimental-minify' argument.
314
300
` ,
315
- { spaced : true }
316
- ) ;
317
- exit ( 1 ) ;
318
- }
301
+ { spaced : true }
302
+ ) ;
303
+ exit ( 1 ) ;
319
304
}
305
+ }
320
306
321
- webpackChunks . set ( key , generate ( chunkExpression . value ) ) ;
307
+ webpackChunks . set ( key , generate ( chunk . value ) ) ;
322
308
323
- const chunkFilePath = join ( tmpWebpackDir , `${ key } .js` ) ;
309
+ const chunkFilePath = join ( tmpWebpackDir , `${ key } .js` ) ;
324
310
325
- const newValue = {
326
- type : 'MemberExpression' ,
327
- object : {
328
- type : 'CallExpression' ,
329
- callee : {
330
- type : 'Identifier' ,
331
- name : 'require' ,
332
- } ,
333
- arguments : [
334
- {
335
- type : 'Literal' ,
336
- value : chunkFilePath ,
337
- raw : JSON . stringify ( chunkFilePath ) ,
338
- } ,
339
- ] ,
340
- } ,
341
- property : {
311
+ const newValue = {
312
+ type : 'MemberExpression' ,
313
+ object : {
314
+ type : 'CallExpression' ,
315
+ callee : {
342
316
type : 'Identifier' ,
343
- name : 'default ' ,
317
+ name : 'require ' ,
344
318
} ,
345
- } ;
319
+ arguments : [
320
+ {
321
+ type : 'Literal' ,
322
+ value : chunkFilePath ,
323
+ raw : JSON . stringify ( chunkFilePath ) ,
324
+ } ,
325
+ ] ,
326
+ } ,
327
+ property : {
328
+ type : 'Identifier' ,
329
+ name : 'default' ,
330
+ } ,
331
+ } ;
346
332
347
- chunkExpression . value = newValue ;
348
- }
333
+ ( chunk as unknown as { value : unknown } ) . value = newValue ;
349
334
}
350
335
351
336
return {
@@ -418,21 +403,71 @@ type DirectoryProcessingResults = {
418
403
webpackChunks : Map < number , string > ;
419
404
} ;
420
405
421
- type LooseNode = Node & {
422
- expression ?: LooseNode ;
423
- callee ?: LooseNode ;
424
- object ?: LooseNode ;
425
- left ?: LooseNode ;
426
- right ?: LooseNode ;
427
- property ?: LooseNode ;
428
- arguments ?: LooseNode [ ] ;
429
- elements ?: LooseNode [ ] ;
430
- properties ?: LooseNode [ ] ;
431
- key ?: LooseNode ;
432
- name ?: string ;
433
- /*
434
- eslint-disable-next-line @typescript-eslint/no-explicit-any
435
- -- TODO: improve the type of value
436
- */
437
- value : any ;
438
- } ;
406
+ /**
407
+ * Verifies wether the provided AST statement represents a javascript code
408
+ * of the following format:
409
+ * ```
410
+ * (self.webpackChunk_N_E = self.webpackChunk_N_E || []).push(...,
411
+ * {
412
+ * [chunkNumberA]: e => { ... },
413
+ * [chunkNumberB]: e => { ... },
414
+ * [chunkNumberC]: e => { ... },
415
+ * ...
416
+ * }
417
+ * ]);
418
+ * ```
419
+ * and in such case it extracts the various chunk properties.
420
+ *
421
+ * @param statement the AST statement to check
422
+ *
423
+ * @returns the chunks as an array of AST Properties if the statement represent the target javascript code, an empty array otherwise
424
+ */
425
+ function getWebpackChunksFromStatement (
426
+ statement : AST . StatementKind
427
+ ) : AST . PropertyKind [ ] {
428
+ try {
429
+ assert ( statement . type === 'ExpressionStatement' ) ;
430
+ const expr = statement . expression ;
431
+
432
+ assert ( expr . type === 'CallExpression' ) ;
433
+ assert ( expr . callee . type === 'MemberExpression' ) ;
434
+ assert ( expr . callee . property . type === 'Identifier' ) ;
435
+ assert ( expr . callee . property . name === 'push' ) ;
436
+ const calleeObj = expr . callee . object ;
437
+
438
+ assert ( calleeObj . type === 'AssignmentExpression' ) ;
439
+
440
+ assertSelfWebpackChunk_N_E ( calleeObj . left ) ;
441
+
442
+ assert ( calleeObj . right . type === 'LogicalExpression' ) ;
443
+ assert ( calleeObj . right . operator === '||' ) ;
444
+ assertSelfWebpackChunk_N_E ( calleeObj . right . left ) ;
445
+ assert ( calleeObj . right . right . type === 'ArrayExpression' ) ;
446
+ assert ( calleeObj . right . right . elements . length === 0 ) ;
447
+
448
+ assert ( expr . arguments [ 0 ] ?. type === 'ArrayExpression' ) ;
449
+ assert ( expr . arguments [ 0 ] . elements [ 1 ] ?. type === 'ObjectExpression' ) ;
450
+
451
+ return expr . arguments [ 0 ] . elements [ 1 ] . properties . filter (
452
+ p =>
453
+ p . type === 'Property' &&
454
+ p . key . type === 'Literal' &&
455
+ typeof p . key . value === 'number' &&
456
+ p . value . type === 'ArrowFunctionExpression'
457
+ ) as AST . PropertyKind [ ] ;
458
+ } catch {
459
+ return [ ] ;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Asserts whether the provided AST node represents `self.webpackChunk_N_E`
465
+ * (throws an AssertionError it doesn't)
466
+ */
467
+ function assertSelfWebpackChunk_N_E ( expression : AST . NodeKind ) : void {
468
+ assert ( expression . type === 'MemberExpression' ) ;
469
+ assert ( expression . object . type === 'Identifier' ) ;
470
+ assert ( expression . object . name === 'self' ) ;
471
+ assert ( expression . property . type === 'Identifier' ) ;
472
+ assert ( expression . property . name === 'webpackChunk_N_E' ) ;
473
+ }
0 commit comments