-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement webpack chunks file updating using ast manipulation
this allows us not to have to require the use of the `serverMinification: false` option (as we're no longer relying on known variable names)
- Loading branch information
1 parent
b84cd5f
commit ecff3e2
Showing
15 changed files
with
1,575 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
...atches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { readFile } from "node:fs/promises"; | ||
|
||
import { expect, test, describe } from "vitest"; | ||
|
||
import { getChunkInstallationIdentifiers } from "./get-chunk-installation-identifiers"; | ||
import { getWebpackChunksFileTsSource } from "./get-webpack-chunks-file-ts-source"; | ||
|
||
describe("getChunkInstallationIdentifiers", () => { | ||
test("the solution works as expected on unminified code", async () => { | ||
const fileContent = await readFile( | ||
`${import.meta.dirname}/test-fixtures/unminified-webpacks-file.js`, | ||
"utf8" | ||
); | ||
const tsSourceFile = getWebpackChunksFileTsSource(fileContent); | ||
const { installChunk, installedChunks } = await getChunkInstallationIdentifiers(tsSourceFile); | ||
expect(installChunk).toEqual("installChunk"); | ||
expect(installedChunks).toEqual("installedChunks"); | ||
}); | ||
|
||
test("the solution works as expected on minified code", async () => { | ||
const fileContent = await readFile( | ||
`${import.meta.dirname}/test-fixtures/minified-webpacks-file.js`, | ||
"utf8" | ||
); | ||
const tsSourceFile = getWebpackChunksFileTsSource(fileContent); | ||
const { installChunk, installedChunks } = await getChunkInstallationIdentifiers(tsSourceFile); | ||
expect(installChunk).toEqual("r"); | ||
expect(installedChunks).toEqual("e"); | ||
}); | ||
}); |
77 changes: 77 additions & 0 deletions
77
...ild/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import * as ts from "ts-morph"; | ||
|
||
export async function getChunkInstallationIdentifiers(sourceFile: ts.SourceFile): Promise<{ | ||
installedChunks: string; | ||
installChunk: string; | ||
}> { | ||
const installChunkDeclaration = getInstallChunkDeclaration(sourceFile); | ||
const installedChunksDeclaration = getInstalledChunksDeclaration(sourceFile, installChunkDeclaration); | ||
|
||
return { | ||
installChunk: installChunkDeclaration.getName(), | ||
installedChunks: installedChunksDeclaration.getName(), | ||
}; | ||
} | ||
|
||
function getInstallChunkDeclaration(sourceFile: ts.SourceFile) { | ||
const installChunkDeclaration = sourceFile | ||
.getDescendantsOfKind(ts.SyntaxKind.VariableDeclaration) | ||
.find((declaration) => { | ||
const arrowFunction = declaration.getInitializerIfKind(ts.SyntaxKind.ArrowFunction); | ||
// we're looking for an arrow function | ||
if (!arrowFunction) return false; | ||
|
||
const functionParameters = arrowFunction.getParameters(); | ||
// the arrow function we're looking for has a single parameter (the chunkId) | ||
if (functionParameters.length !== 1) return false; | ||
|
||
const arrowFunctionBodyBlock = arrowFunction.getFirstChildByKind(ts.SyntaxKind.Block); | ||
|
||
// the arrow function we're looking for has a block body | ||
if (!arrowFunctionBodyBlock) return false; | ||
|
||
const statementKinds = arrowFunctionBodyBlock.getStatements().map((statement) => statement.getKind()); | ||
|
||
// the function we're looking for has 2 for loops (a standard one and a for-in one) | ||
const forInStatements = statementKinds.filter((s) => s === ts.SyntaxKind.ForInStatement); | ||
const forStatements = statementKinds.filter((s) => s === ts.SyntaxKind.ForStatement); | ||
if (forInStatements.length !== 1 || forStatements.length !== 1) return false; | ||
|
||
// the function we're looking for accesses its parameter three times, and it | ||
// accesses its `modules`, `ids` and `runtime` properties (in this order) | ||
const parameterName = functionParameters[0].getText(); | ||
const functionParameterAccessedProperties = arrowFunctionBodyBlock | ||
.getDescendantsOfKind(ts.SyntaxKind.PropertyAccessExpression) | ||
.filter( | ||
(propertyAccessExpression) => propertyAccessExpression.getExpression().getText() === parameterName | ||
) | ||
.map((propertyAccessExpression) => propertyAccessExpression.getName()); | ||
if (functionParameterAccessedProperties.join(", ") !== "modules, ids, runtime") return false; | ||
|
||
return true; | ||
}); | ||
|
||
if (!installChunkDeclaration) { | ||
throw new Error("ERROR: unable to find the installChunk function declaration"); | ||
} | ||
|
||
return installChunkDeclaration; | ||
} | ||
|
||
function getInstalledChunksDeclaration( | ||
sourceFile: ts.SourceFile, | ||
installChunkDeclaration: ts.VariableDeclaration | ||
) { | ||
const allVariableDeclarations = sourceFile.getDescendantsOfKind(ts.SyntaxKind.VariableDeclaration); | ||
const installChunkDeclarationIdx = allVariableDeclarations.findIndex( | ||
(declaration) => declaration === installChunkDeclaration | ||
); | ||
|
||
// the installedChunks declaration is comes right before the installChunk one | ||
const installedChunksDeclaration = allVariableDeclarations[installChunkDeclarationIdx - 1]; | ||
|
||
if (!installedChunksDeclaration?.getInitializer()?.isKind(ts.SyntaxKind.ObjectLiteralExpression)) { | ||
throw new Error("ERROR: unable to find the installedChunks declaration"); | ||
} | ||
return installedChunksDeclaration; | ||
} |
44 changes: 44 additions & 0 deletions
44
...d/update-webpack-chunks-file/get-file-content-with-updated-webpack-require-f-code.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { readFile } from "node:fs/promises"; | ||
|
||
import { expect, test, describe } from "vitest"; | ||
|
||
import { getFileContentWithUpdatedWebpackFRequireCode } from "./get-file-content-with-updated-webpack-require-f-code"; | ||
import { getWebpackChunksFileTsSource } from "./get-webpack-chunks-file-ts-source"; | ||
|
||
describe("getFileContentWithUpdatedWebpackFRequireCode", () => { | ||
test("the solution works as expected on unminified code", async () => { | ||
const fileContent = await readFile( | ||
`${import.meta.dirname}/test-fixtures/unminified-webpacks-file.js`, | ||
"utf8" | ||
); | ||
const tsSourceFile = getWebpackChunksFileTsSource(fileContent); | ||
const updatedFCode = await getFileContentWithUpdatedWebpackFRequireCode( | ||
tsSourceFile, | ||
{ installChunk: "installChunk", installedChunks: "installedChunks" }, | ||
["658"] | ||
); | ||
expect(unstyleCode(updatedFCode)).toContain(`if (installedChunks[chunkId]) return;`); | ||
expect(unstyleCode(updatedFCode)).toContain( | ||
`if (chunkId === 658) return installChunk(require("./chunks/658.js"));` | ||
); | ||
}); | ||
|
||
test("the solution works as expected on minified code", async () => { | ||
const fileContent = await readFile( | ||
`${import.meta.dirname}/test-fixtures/minified-webpacks-file.js`, | ||
"utf8" | ||
); | ||
const tsSourceFile = getWebpackChunksFileTsSource(fileContent); | ||
const updatedFCode = await getFileContentWithUpdatedWebpackFRequireCode( | ||
tsSourceFile, | ||
{ installChunk: "r", installedChunks: "e" }, | ||
["658"] | ||
); | ||
expect(unstyleCode(updatedFCode)).toContain("if (e[o]) return;"); | ||
expect(unstyleCode(updatedFCode)).toContain(`if (o === 658) return r(require("./chunks/658.js"));`); | ||
}); | ||
}); | ||
|
||
function unstyleCode(text: string): string { | ||
return text.replace(/\n\s+/g, "\n").replace(/\n/g, " "); | ||
} |
74 changes: 74 additions & 0 deletions
74
...igated/update-webpack-chunks-file/get-file-content-with-updated-webpack-require-f-code.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import * as ts from "ts-morph"; | ||
|
||
export async function getFileContentWithUpdatedWebpackFRequireCode( | ||
sourceFile: ts.SourceFile, | ||
{ installChunk, installedChunks }: { installChunk: string; installedChunks: string }, | ||
chunks: string[] | ||
): Promise<string> { | ||
const webpackFRequireFunction = sourceFile | ||
.getDescendantsOfKind(ts.SyntaxKind.BinaryExpression) | ||
.map((binaryExpression) => { | ||
const binaryExpressionLeft = binaryExpression.getLeft(); | ||
if (!binaryExpressionLeft.getText().endsWith(".f.require")) return; | ||
|
||
const binaryExpressionOperator = binaryExpression.getOperatorToken(); | ||
if (binaryExpressionOperator.getText() !== "=") return; | ||
|
||
const binaryExpressionRight = binaryExpression.getRight(); | ||
const binaryExpressionRightText = binaryExpressionRight.getText(); | ||
|
||
if ( | ||
!binaryExpressionRightText.includes(installChunk) || | ||
!binaryExpressionRightText.includes(installedChunks) | ||
) | ||
return; | ||
|
||
if (!binaryExpressionRight.isKind(ts.SyntaxKind.ArrowFunction)) return; | ||
|
||
const arrowFunctionBody = binaryExpressionRight.getBody(); | ||
if (!arrowFunctionBody.isKind(ts.SyntaxKind.Block)) return; | ||
|
||
const arrowFunction = binaryExpressionRight; | ||
const functionParameters = arrowFunction.getParameters(); | ||
if (functionParameters.length !== 2) return; | ||
|
||
const callsInstallChunk = arrowFunctionBody | ||
.getDescendantsOfKind(ts.SyntaxKind.CallExpression) | ||
.some((callExpression) => callExpression.getExpression().getText() === installChunk); | ||
if (!callsInstallChunk) return; | ||
|
||
const functionFirstParameterName = functionParameters[0]?.getName(); | ||
const accessesInstalledChunksUsingItsFirstParameter = arrowFunctionBody | ||
.getDescendantsOfKind(ts.SyntaxKind.ElementAccessExpression) | ||
.some((elementAccess) => { | ||
return ( | ||
elementAccess.getExpression().getText() === installedChunks && | ||
elementAccess.getArgumentExpression()?.getText() === functionFirstParameterName | ||
); | ||
}); | ||
if (!accessesInstalledChunksUsingItsFirstParameter) return; | ||
|
||
return arrowFunction; | ||
}) | ||
.find(Boolean); | ||
|
||
if (!webpackFRequireFunction) { | ||
throw new Error("ERROR: unable to find the webpack f require function declaration"); | ||
} | ||
|
||
const functionParameterNames = webpackFRequireFunction | ||
.getParameters() | ||
.map((parameter) => parameter.getName()); | ||
const chunkId = functionParameterNames[0]; | ||
|
||
const functionBody = webpackFRequireFunction.getBody() as ts.Block; | ||
|
||
functionBody.insertStatements(0, [ | ||
`if (${installedChunks}[${chunkId}]) return;`, | ||
...chunks.map( | ||
(chunk) => `\nif(${chunkId} === ${chunk}) return ${installChunk}(require("./chunks/${chunk}.js"));` | ||
), | ||
]); | ||
|
||
return sourceFile.print(); | ||
} |
23 changes: 23 additions & 0 deletions
23
...s/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { readFile } from "node:fs/promises"; | ||
|
||
import { expect, test } from "vitest"; | ||
|
||
import { getUpdatedWebpackChunksFileContent } from "./get-updated-webpack-chunks-file-content"; | ||
|
||
test("the solution works as expected on unminified code", async () => { | ||
const fileContent = await readFile( | ||
`${import.meta.dirname}/test-fixtures/unminified-webpacks-file.js`, | ||
"utf8" | ||
); | ||
const updatedContent = await getUpdatedWebpackChunksFileContent(fileContent, ["658"]); | ||
expect(updatedContent).toMatchFileSnapshot("./test-snapshots/unminified-webpacks-file.js"); | ||
}); | ||
|
||
test("the solution works as expected on minified code", async () => { | ||
const fileContent = await readFile( | ||
`${import.meta.dirname}/test-fixtures/minified-webpacks-file.js`, | ||
"utf8" | ||
); | ||
const updatedContent = await getUpdatedWebpackChunksFileContent(fileContent, ["658"]); | ||
expect(updatedContent).toMatchFileSnapshot("./test-snapshots/minified-webpacks-file.js"); | ||
}); |
22 changes: 22 additions & 0 deletions
22
...atches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import * as ts from "ts-morph"; | ||
|
||
import { getChunkInstallationIdentifiers } from "./get-chunk-installation-identifiers"; | ||
import { getFileContentWithUpdatedWebpackFRequireCode } from "./get-file-content-with-updated-webpack-require-f-code"; | ||
import { getWebpackChunksFileTsSource } from "./get-webpack-chunks-file-ts-source"; | ||
|
||
export async function getUpdatedWebpackChunksFileContent( | ||
fileContent: string, | ||
chunks: string[] | ||
): Promise<string> { | ||
const tsSourceFile = getWebpackChunksFileTsSource(fileContent); | ||
|
||
const chunkInstallationIdentifiers = await getChunkInstallationIdentifiers(tsSourceFile); | ||
|
||
const updatedFileContent = getFileContentWithUpdatedWebpackFRequireCode( | ||
tsSourceFile, | ||
chunkInstallationIdentifiers, | ||
chunks | ||
); | ||
|
||
return updatedFileContent; | ||
} |
17 changes: 17 additions & 0 deletions
17
...uild/patches/investigated/update-webpack-chunks-file/get-webpack-chunks-file-ts-source.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import * as ts from "ts-morph"; | ||
|
||
export function getWebpackChunksFileTsSource(fileContent: string): ts.SourceFile { | ||
const project = new ts.Project({ | ||
compilerOptions: { | ||
target: ts.ScriptTarget.ES2023, | ||
lib: ["ES2023"], | ||
module: ts.ModuleKind.CommonJS, | ||
moduleResolution: ts.ModuleResolutionKind.NodeNext, | ||
allowJs: true, | ||
}, | ||
}); | ||
|
||
const sourceFile = project.createSourceFile("webpack-runtime.js", fileContent); | ||
|
||
return sourceFile; | ||
} |
Oops, something went wrong.