Skip to content

Commit

Permalink
Merge pull request #17212 from webpack/feat-support-custom-syntax
Browse files Browse the repository at this point in the history
feat: introduce a new syntax for worklets - `*context.audioWorklet.addModule()`
  • Loading branch information
alexander-akait committed May 31, 2023
2 parents 16660c1 + 69210bb commit 2a669ff
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 1 deletion.
26 changes: 25 additions & 1 deletion lib/dependencies/WorkerPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const getUrl = module => {
return pathToFileURL(module.resource).toString();
};

const WorkerSpecifierTag = Symbol("worker specifier tag");

const DEFAULT_SYNTAX = [
"Worker",
"SharedWorker",
Expand Down Expand Up @@ -381,7 +383,29 @@ class WorkerPlugin {
return true;
};
const processItem = item => {
if (item.endsWith("()")) {
if (
item.startsWith("*") &&
item.includes(".") &&
item.endsWith("()")
) {
const firstDot = item.indexOf(".");
const pattern = item.slice(1, firstDot);
const itemMembers = item.slice(firstDot + 1, -2);

parser.hooks.pattern.for(pattern).tap(PLUGIN_NAME, pattern => {
parser.tagVariable(pattern.name, WorkerSpecifierTag);
return true;
});
parser.hooks.callMemberChain
.for(WorkerSpecifierTag)
.tap(PLUGIN_NAME, (expression, members) => {
if (itemMembers !== members.join(".")) {
return;
}

return handleNewWorker(expression);
});
} else if (item.endsWith("()")) {
parser.hooks.call
.for(item.slice(0, -2))
.tap(PLUGIN_NAME, handleNewWorker);
Expand Down
202 changes: 202 additions & 0 deletions test/configCases/worker/worklet/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// This is a pseudo-worklet, it is not a real worklet, but it is used to test the worker logic.
// Real worklets do not have this API.

it("should allow to create a paintWorklet worklet", async () => {
let pseudoWorklet = await CSS.paintWorklet.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a layoutWorklet worklet", async () => {
let pseudoWorklet = await CSS.layoutWorklet.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a animationWorklet worklet", async () => {
let pseudoWorklet = await CSS.animationWorklet.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a audioWorklet worklet", async () => {
let context = new AudioContext();
let pseudoWorklet = await context.audioWorklet.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a paintWorklet worklet using '?.'", async () => {
let pseudoWorklet = await CSS?.paintWorklet?.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a audioWorklet worklet #2", async () => {
let audioWorklet = (new AudioContext()).audioWorklet;
let pseudoWorklet = await audioWorklet.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a audioWorklet worklet #3", async () => {
let context = {
foo: {
bar: new AudioContext()
}
};
let pseudoWorklet = await context.foo.bar.audioWorklet.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should allow to create a audioWorklet worklet using '?.'", async () => {
let context = new AudioContext();
let pseudoWorklet = await context?.audioWorklet?.addModule(new URL("./worklet.js", import.meta.url));

pseudoWorklet = new pseudoWorklet();

expect(pseudoWorklet.url).not.toContain("asset-");

pseudoWorklet.postMessage("ok");

const result = await new Promise(resolve => {
pseudoWorklet.onmessage = event => {
resolve(event.data);
};
});
expect(result).toBe("data: OK, thanks");

await pseudoWorklet.terminate();
});

it("should not create a audioWorklet worklet", async () => {
let workletURL;

const barContext = new class Foo {
constructor() {
this.audioWorklet = {
addModule(url) {
workletURL = url.toString();

return undefined;
}
}
}
}

await barContext.audioWorklet.addModule(new URL("./worklet-asset-1.js", import.meta.url));

expect(workletURL).toContain("asset-");
});

it("should not create a audioWorklet worklet", async () => {
let workletURL;

const barContext = new class Foo {
constructor() {
this.unknownWorklet = {
addModule(url) {
workletURL = url.toString();

return undefined;
}
}
}
}

await barContext.unknownWorklet.addModule(new URL("./worklet-asset-2.js", import.meta.url));

expect(workletURL).toContain("asset-");
});
3 changes: 3 additions & 0 deletions test/configCases/worker/worklet/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function upper(str) {
return str.toUpperCase();
}
33 changes: 33 additions & 0 deletions test/configCases/worker/worklet/test.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
let outputDirectory;

module.exports = {
moduleScope(scope) {
const FakeWorker = require("../../../helpers/createFakeWorker")({
outputDirectory
});

// Pseudo code
scope.AudioContext = class AudioContext {
constructor() {
this.audioWorklet = {
addModule: url => Promise.resolve(FakeWorker.bind(null, url))
};
}
};
scope.CSS = {
paintWorklet: {
addModule: url => Promise.resolve(FakeWorker.bind(null, url))
},
layoutWorklet: {
addModule: url => Promise.resolve(FakeWorker.bind(null, url))
},
animationWorklet: {
addModule: url => Promise.resolve(FakeWorker.bind(null, url))
}
};
},
findBundle: function (i, options) {
outputDirectory = options.output.path;
return ["main.js"];
}
};
6 changes: 6 additions & 0 deletions test/configCases/worker/worklet/test.filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var supportsWorker = require("../../../helpers/supportsWorker");
var supportsOptionalChaining = require("../../../helpers/supportsOptionalChaining");

module.exports = function (config) {
return supportsWorker() && supportsOptionalChaining();
};
27 changes: 27 additions & 0 deletions test/configCases/worker/worklet/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
assetModuleFilename: "asset-[name][ext]",
filename: "[name].js"
},
target: "web",
module: {
rules: [
{
test: /\.[cm]?js$/,
parser: {
worker: [
"CSS.paintWorklet.addModule()",
"CSS.layoutWorklet.addModule()",
"CSS.animationWorklet.addModule()",
"*context.audioWorklet.addModule()",
"*context.foo.bar.audioWorklet.addModule()",
"*audioWorklet.addModule()",
// *addModule() is not valid syntax
"..."
]
}
}
]
}
};
1 change: 1 addition & 0 deletions test/configCases/worker/worklet/worklet-asset-1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
function test1() {}
1 change: 1 addition & 0 deletions test/configCases/worker/worklet/worklet-asset-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
function test2() {}
4 changes: 4 additions & 0 deletions test/configCases/worker/worklet/worklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
onmessage = async event => {
const { upper } = await import("./module");
postMessage(`data: ${upper(event.data)}, thanks`);
};
1 change: 1 addition & 0 deletions test/helpers/createFakeWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = ({ outputDirectory }) =>
expect(url).toBeInstanceOf(URL);
expect(url.origin).toBe("https://test.cases");
expect(url.pathname.startsWith("/path/")).toBe(true);
this.url = url;
const file = url.pathname.slice(6);
const workerBootstrap = `
const { parentPort } = require("worker_threads");
Expand Down

0 comments on commit 2a669ff

Please sign in to comment.