improve parsing to handle non-literal options

This commit is contained in:
Tobias Koppers 2021-03-12 12:09:02 +01:00
parent e839494ff0
commit 61cc65c58e
5 changed files with 172 additions and 56 deletions

View File

@ -25,12 +25,10 @@ class WorkerDependency extends ModuleDependency {
/** /**
* @param {string} request request * @param {string} request request
* @param {[number, number]} range range * @param {[number, number]} range range
* @param {Record<string, any> | undefined} options options
*/ */
constructor(request, range, options) { constructor(request, range) {
super(request); super(request);
this.range = range; this.range = range;
this.options = options;
} }
/** /**
@ -81,9 +79,7 @@ WorkerDependency.Template = class WorkerDependencyTemplate extends (
dep.range[1] - 1, dep.range[1] - 1,
`new URL(/* worker import */ ${RuntimeGlobals.publicPath} + ${ `new URL(/* worker import */ ${RuntimeGlobals.publicPath} + ${
RuntimeGlobals.getChunkScriptFilename RuntimeGlobals.getChunkScriptFilename
}(${JSON.stringify(chunk.id)}), ${RuntimeGlobals.baseURI})${ }(${JSON.stringify(chunk.id)}), ${RuntimeGlobals.baseURI})`
dep.options ? `, ${JSON.stringify(dep.options)}` : ""
}`
); );
} }
}; };

View File

@ -14,12 +14,17 @@ const EnableChunkLoadingPlugin = require("../javascript/EnableChunkLoadingPlugin
const { equals } = require("../util/ArrayHelpers"); const { equals } = require("../util/ArrayHelpers");
const { contextify } = require("../util/identifier"); const { contextify } = require("../util/identifier");
const EnableWasmLoadingPlugin = require("../wasm/EnableWasmLoadingPlugin"); const EnableWasmLoadingPlugin = require("../wasm/EnableWasmLoadingPlugin");
const ConstDependency = require("./ConstDependency");
const { const {
harmonySpecifierTag harmonySpecifierTag
} = require("./HarmonyImportDependencyParserPlugin"); } = require("./HarmonyImportDependencyParserPlugin");
const WorkerDependency = require("./WorkerDependency"); const WorkerDependency = require("./WorkerDependency");
/** @typedef {import("estree").Expression} Expression */ /** @typedef {import("estree").Expression} Expression */
/** @typedef {import("estree").ObjectExpression} ObjectExpression */
/** @typedef {import("estree").Pattern} Pattern */
/** @typedef {import("estree").Property} Property */
/** @typedef {import("estree").SpreadElement} SpreadElement */
/** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Entrypoint").EntryOptions} EntryOptions */ /** @typedef {import("../Entrypoint").EntryOptions} EntryOptions */
/** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */ /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
@ -73,7 +78,7 @@ class WorkerPlugin {
/** /**
* @param {JavascriptParser} parser the parser * @param {JavascriptParser} parser the parser
* @param {Expression} expr expression * @param {Expression} expr expression
* @returns {BasicEvaluatedExpression} parsed * @returns {[BasicEvaluatedExpression, [number, number]]} parsed
*/ */
const parseModuleUrl = (parser, expr) => { const parseModuleUrl = (parser, expr) => {
if ( if (
@ -95,35 +100,55 @@ class WorkerPlugin {
) { ) {
return; return;
} }
return parser.evaluateExpression(arg1); const arg1Value = parser.evaluateExpression(arg1);
return [arg1Value, [arg1.range[0], arg2.range[1]]];
}; };
/** /**
* @param {JavascriptParser} parser the parser * @param {JavascriptParser} parser the parser
* @param {Expression} expr expression * @param {ObjectExpression} expr expression
* @returns {object | undefined} parsed object * @returns {{ expressions: Record<string, Expression | Pattern>, otherElements: (Property | SpreadElement)[], values: Record<string, any>, spread: boolean, insertType: "comma" | "single", insertLocation: number }} parsed object
*/ */
const parseObjectLiteral = (parser, expr) => { const parseObjectExpression = (parser, expr) => {
if (expr.type !== "ObjectExpression") return; /** @type {Record<string, any>} */
const obj = {}; const values = {};
/** @type {Record<string, Expression | Pattern>} */
const expressions = {};
/** @type {(Property | SpreadElement)[]} */
const otherElements = [];
let spread = false;
for (const prop of expr.properties) { for (const prop of expr.properties) {
if (prop.type === "Property") { if (prop.type === "SpreadElement") {
if ( spread = true;
!prop.method && } else if (
!prop.computed && prop.type === "Property" &&
!prop.shorthand && !prop.method &&
prop.key.type === "Identifier" && !prop.computed &&
!prop.value.type.endsWith("Pattern") prop.key.type === "Identifier"
) { ) {
expressions[prop.key.name] = prop.value;
if (!prop.shorthand && !prop.value.type.endsWith("Pattern")) {
const value = parser.evaluateExpression( const value = parser.evaluateExpression(
/** @type {Expression} */ (prop.value) /** @type {Expression} */ (prop.value)
); );
if (value.isCompileTimeValue()) if (value.isCompileTimeValue())
obj[prop.key.name] = value.asCompileTimeValue(); values[prop.key.name] = value.asCompileTimeValue();
} }
} else {
otherElements.push(prop);
} }
} }
return obj; const insertType = expr.properties.length > 0 ? "comma" : "single";
const insertLocation =
expr.properties[expr.properties.length - 1].range[1];
return {
expressions,
otherElements,
values,
spread,
insertType,
insertLocation
};
}; };
/** /**
@ -139,22 +164,30 @@ class WorkerPlugin {
if (expr.arguments.length === 0 || expr.arguments.length > 2) if (expr.arguments.length === 0 || expr.arguments.length > 2)
return; return;
const [arg1, arg2] = expr.arguments; const [arg1, arg2] = expr.arguments;
/** @type {[number, number]} */ if (arg1.type === "SpreadElement") return;
const range = [arg1.range[0], (arg2 || arg1).range[1]]; if (arg2 && arg2.type === "SpreadElement") return;
const url = parseModuleUrl(parser, arg1); const parsedUrl = parseModuleUrl(parser, arg1);
if (!url || !url.isString()) return; if (!parsedUrl) return;
let options; const [url, range] = parsedUrl;
if (arg2) { if (!url.isString()) return;
options = parseObjectLiteral(parser, arg2); const {
if (!options) return; expressions,
// Remove `type: "module"` if present. otherElements,
delete options.type; values: options,
// If the `options` is now an empty object, spread: hasSpreadInOptions,
// remove it from the final bundle. insertType,
if (Object.keys(options).length === 0) { insertLocation
options = undefined; } =
} arg2 && arg2.type === "ObjectExpression"
} ? parseObjectExpression(parser, arg2)
: {
expressions: {},
otherElements: [],
values: {},
spread: false,
insertType: arg2 ? "spread" : "argument",
insertLocation: arg2 ? arg2.range : arg1.range[1]
};
const { const {
options: importOptions, options: importOptions,
errors: commentErrors errors: commentErrors
@ -225,7 +258,7 @@ class WorkerPlugin {
if ( if (
!Object.prototype.hasOwnProperty.call(entryOptions, "name") && !Object.prototype.hasOwnProperty.call(entryOptions, "name") &&
options && options &&
options.name typeof options.name === "string"
) { ) {
entryOptions.name = options.name; entryOptions.name = options.name;
} }
@ -245,12 +278,52 @@ class WorkerPlugin {
} }
}); });
block.loc = expr.loc; block.loc = expr.loc;
const dep = new WorkerDependency(url.string, range, options); const dep = new WorkerDependency(url.string, range);
dep.loc = expr.loc; dep.loc = expr.loc;
block.addDependency(dep); block.addDependency(dep);
parser.state.module.addBlock(block); parser.state.module.addBlock(block);
parser.walkExpression(expr.callee); parser.walkExpression(expr.callee);
if (arg2) parser.walkExpression(arg2);
if (expressions.type) {
const expr = expressions.type;
if (options.type !== false) {
const dep = new ConstDependency("undefined", expr.range);
dep.loc = expr.loc;
parser.state.module.addPresentationalDependency(dep);
expressions.type = undefined;
}
} else if (hasSpreadInOptions && insertType === "comma") {
const dep = new ConstDependency(
",type: undefined",
insertLocation
);
dep.loc = expr.loc;
parser.state.module.addPresentationalDependency(dep);
} else if (insertType === "spread") {
const dep1 = new ConstDependency(
"Object.assign({}, ",
insertLocation[0]
);
const dep2 = new ConstDependency(
", { type: undefined })",
insertLocation[1]
);
dep1.loc = expr.loc;
dep2.loc = expr.loc;
parser.state.module.addPresentationalDependency(dep1);
parser.state.module.addPresentationalDependency(dep2);
}
for (const key of Object.keys(expressions)) {
if (expressions[key]) parser.walkExpression(expressions[key]);
}
for (const prop of otherElements) {
parser.walkProperty(prop);
}
if (insertType === "spread") {
parser.walkExpression(arg2);
}
return true; return true;
}; };
const processItem = item => { const processItem = item => {

View File

@ -2286,20 +2286,24 @@ class JavascriptParser extends Parser {
propIndex++ propIndex++
) { ) {
const prop = expression.properties[propIndex]; const prop = expression.properties[propIndex];
if (prop.type === "SpreadElement") { this.walkProperty(prop);
this.walkExpression(prop.argument); }
continue; }
}
if (prop.computed) { walkProperty(prop) {
this.walkExpression(prop.key); if (prop.type === "SpreadElement") {
} this.walkExpression(prop.argument);
if (prop.shorthand && prop.value && prop.value.type === "Identifier") { return;
this.scope.inShorthand = prop.value.name; }
this.walkIdentifier(prop.value); if (prop.computed) {
this.scope.inShorthand = false; this.walkExpression(prop.key);
} else { }
this.walkExpression(prop.value); if (prop.shorthand && prop.value && prop.value.type === "Identifier") {
} this.scope.inShorthand = prop.value.name;
this.walkIdentifier(prop.value);
this.scope.inShorthand = false;
} else {
this.walkExpression(prop.value);
} }
} }

View File

@ -12,6 +12,48 @@ it("should allow to create a WebWorker", async () => {
await worker.terminate(); await worker.terminate();
}); });
it("should allow to create a WebWorker (multiple options)", async () => {
const worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
name: "worker1"
});
worker.postMessage("ok");
const result = await new Promise(resolve => {
worker.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");
await worker.terminate();
});
it("should allow to create a WebWorker (spread type)", async () => {
const worker = new Worker(new URL("./worker.js", import.meta.url), {
...{ type: "module" }
});
worker.postMessage("ok");
const result = await new Promise(resolve => {
worker.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");
await worker.terminate();
});
it("should allow to create a WebWorker (expression)", async () => {
const options = { type: "module" };
const worker = new Worker(new URL("./worker.js", import.meta.url), options);
worker.postMessage("ok");
const result = await new Promise(resolve => {
worker.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");
await worker.terminate();
});
it("should allow to share chunks", async () => { it("should allow to share chunks", async () => {
const promise = import("./module"); const promise = import("./module");
const script = document.head._children[0]; const script = document.head._children[0];

1
types.d.ts vendored
View File

@ -4626,6 +4626,7 @@ declare class JavascriptParser extends Parser {
walkArrayExpression(expression?: any): void; walkArrayExpression(expression?: any): void;
walkSpreadElement(expression?: any): void; walkSpreadElement(expression?: any): void;
walkObjectExpression(expression?: any): void; walkObjectExpression(expression?: any): void;
walkProperty(prop?: any): void;
walkFunctionExpression(expression?: any): void; walkFunctionExpression(expression?: any): void;
walkArrowFunctionExpression(expression?: any): void; walkArrowFunctionExpression(expression?: any): void;
walkSequenceExpression(expression: SequenceExpression): void; walkSequenceExpression(expression: SequenceExpression): void;