Skip to content

Commit 758f588

Browse files
Improve AST checking and types (#189)
1 parent c1998a6 commit 758f588

File tree

4 files changed

+148
-72
lines changed

4 files changed

+148
-72
lines changed

.changeset/short-goats-grab.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@cloudflare/next-on-pages': patch
3+
---
4+
5+
improve AST checking
6+
7+
improve the way we check for webpack chunks (for the experimental minification) by
8+
improving the AST types used and also make the AST checking more robust

package-lock.json

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
],
2323
"dependencies": {
2424
"acorn": "^8.8.0",
25+
"ast-types": "^0.14.2",
2526
"astring": "^1.8.4",
2627
"chalk": "^5.2.0",
2728
"chokidar": "^3.5.3",

src/buildApplication/generateFunctionsMap.ts

+107-72
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { readFile, writeFile, mkdir, rm, readdir, copyFile } from 'fs/promises';
22
import { exit } from 'process';
33
import { dirname, join, relative, resolve } from 'path';
4-
import type { Node } from 'acorn';
54
import { parse } from 'acorn';
65
import { generate } from 'astring';
76
import {
@@ -15,6 +14,8 @@ import {
1514
import type { CliOptions } from '../cli';
1615
import { cliError, cliWarn } from '../cli';
1716
import { tmpdir } from 'os';
17+
import type * as AST from 'ast-types/gen/kinds';
18+
import assert from 'node:assert';
1819

1920
/**
2021
* Creates new files containing the Vercel built functions but adjusted so that they can be later
@@ -283,69 +284,53 @@ function extractWebpackChunks(
283284
const parsedContents = parse(functionContents, {
284285
ecmaVersion: 'latest',
285286
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+
`
312298
ERROR: Detected a collision with '--experimental-minify'.
313299
Try removing the '--experimental-minify' argument.
314300
`,
315-
{ spaced: true }
316-
);
317-
exit(1);
318-
}
301+
{ spaced: true }
302+
);
303+
exit(1);
319304
}
305+
}
320306

321-
webpackChunks.set(key, generate(chunkExpression.value));
307+
webpackChunks.set(key, generate(chunk.value));
322308

323-
const chunkFilePath = join(tmpWebpackDir, `${key}.js`);
309+
const chunkFilePath = join(tmpWebpackDir, `${key}.js`);
324310

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: {
342316
type: 'Identifier',
343-
name: 'default',
317+
name: 'require',
344318
},
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+
};
346332

347-
chunkExpression.value = newValue;
348-
}
333+
(chunk as unknown as { value: unknown }).value = newValue;
349334
}
350335

351336
return {
@@ -418,21 +403,71 @@ type DirectoryProcessingResults = {
418403
webpackChunks: Map<number, string>;
419404
};
420405

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

Comments
 (0)