chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# `@perspective-dev/esbuild-plugin`
Applications bundled with `esbuild` can make use of the
`@perspective-dev/esbuild-plugin` module. A full example can be found in the
repo under
[`examples/esbuild-example`](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-example).
```javascript
const esbuild = require("esbuild");
const { PerspectiveEsbuildPlugin } = require("@perspective-dev/esbuild-plugin");
esbuild.build({
entryPoints: ["src/index.js"],
plugins: [PerspectiveEsbuildPlugin()],
format: "esm",
bundle: true,
loader: {
".ttf": "file",
".wasm": "file",
},
});
```
When bundling via `esbuild`, you must also:
- Use the `type="module"` attribute in your app's `<script>` tag, as this build
mode is only supported via ES modules.
- Use the direct imports for the `esm` versions Perspective, specifically
`@perspective-dev/client/dist/esm/perspective.js` and
`@perspective-dev/viewer/dist/esm/perspective-viewer.js`
+51
View File
@@ -0,0 +1,51 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
const get_banner = (deps) => ({
js: `
define(
${JSON.stringify(deps)},
function (...args) {
const exports = {};
const module = {exports};
const define = undefined;
const require = (x) => args[${JSON.stringify(deps)}.indexOf(x)];
`.trimStart(),
});
const get_footer = () => ({
js: `
return exports;
});`,
});
exports.AMDLoader = function AMDLoader(deps) {
function setup(build) {
const options = build.initialOptions;
if (deps === undefined) {
console.warn("Setting `deps` to [`@jupyter-widgets/base`]");
deps = [`@jupyter-widgets/base`];
}
options.banner = get_banner(deps);
options.footer = get_footer();
if (options.format !== "cjs" && options.format !== undefined) {
console.warn("Setting options to `cjs` for `AMDLoader`");
options.format = "cjs";
}
}
return {
name: "amd",
setup,
};
};
+50
View File
@@ -0,0 +1,50 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
const esbuild = require("esbuild");
const CUTOFF_PERCENT = 0.02;
const DEFAULT_BUILD = {
target: ["es2022"],
bundle: true,
minify: !process.env.PSP_DEBUG,
sourcemap: true,
metafile: true,
entryNames: "[name]",
chunkNames: "[name]",
assetNames: "[name]",
};
exports.build = async function build(config) {
const result = await esbuild.build({
...DEFAULT_BUILD,
...config,
});
if (result.metafile) {
for (const output of Object.keys(result.metafile.outputs)) {
const { inputs, bytes } = result.metafile.outputs[output];
for (const input of Object.keys(inputs)) {
if (inputs[input].bytesInOutput / bytes < CUTOFF_PERCENT) {
delete inputs[input];
}
}
}
const text = await esbuild.analyzeMetafile(result.metafile, {
color: true,
});
console.log(text);
}
};
+30
View File
@@ -0,0 +1,30 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
exports.NodeModulesExternal = function NodeModulesExternal(whitelist) {
function setup(build) {
build.onResolve({ filter: /^[A-Za-z0-9\@]/ }, (args) => {
if (!whitelist || !args.path.startsWith(whitelist)) {
return {
path: args.path,
external: true,
namespace: "skip-node-modules",
};
}
});
}
return {
name: "node_modules_external",
setup,
};
};
+46
View File
@@ -0,0 +1,46 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
const { WasmPlugin } = require("./wasm.js");
const { WorkerPlugin } = require("./worker.js");
exports.PerspectiveEsbuildPlugin = function PerspectiveEsbuildPlugin(
options = {},
) {
// const wasm_plugin = WasmPlugin(
// !!options.wasm?.inline,
// !options.wasm?.webpack_hack
// );
// `inline` (default true) keeps the legacy single-bundle behavior
// — worker source embedded as a string and wrapped in a Blob URL
// at runtime. Pass `worker: { inline: false }` to emit the worker
// as a sibling file (preserves source maps in DevTools).
const worker_plugin = WorkerPlugin({
inline: options.worker?.inline !== false,
});
function setup(build) {
// if (options.wasm !== false) {
// wasm_plugin.setup(build);
// }
if (options.worker !== false) {
worker_plugin.setup(build);
}
}
return {
name: "@perspective-dev/esbuild-plugin",
setup,
};
};
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@perspective-dev/esbuild-plugin",
"version": "4.5.2",
"description": "esbuild plugin for Perspective",
"author": "",
"license": "Apache-2.0",
"main": "index.js",
"publishConfig": {
"access": "public"
},
"files": [
"*.js",
"README.md"
],
"dependencies": {
"esbuild": "catalog:"
},
"repository": {
"type": "git",
"url": "https://github.com/perspective-dev/perspective/packages/build"
}
}
+55
View File
@@ -0,0 +1,55 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
exports.ResolvePlugin = function ResolvePlugin(options) {
return {
name: "resolve",
setup: (build) => {
for (const moduleName of Object.keys(options)) {
const moduleTarget = options[moduleName];
const filter = new RegExp("^" + moduleName + "$");
build.onResolve({ filter }, async (args) => {
if (args.resolveDir === "") {
return;
}
return {
path: args.path,
namespace: "resolve",
pluginData: {
resolveDir: args.resolveDir,
moduleName,
},
};
});
build.onLoad({ filter, namespace: "resolve" }, async (args) => {
const importerCode = `
export * from '${args.path.replace(
args.pluginData.moduleName,
moduleTarget
)}';
export { default } from '${args.path.replace(
args.pluginData.moduleName,
moduleTarget
)}';
`;
return {
contents: importerCode,
resolveDir: args.pluginData.resolveDir,
};
});
}
},
};
};
+138
View File
@@ -0,0 +1,138 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
const fs = require("fs");
const path = require("path");
exports.WasmPlugin = function WasmPlugin(inline, webpack_hack) {
function setup(build) {
const options = build.initialOptions;
options.metafile = true;
const KEYSET = [];
build.onResolve({ filter: /\.wasm$/ }, (args) => {
if (
args.namespace === "wasm-stub" ||
args.namespace === "wasm-inline"
) {
let entryPoint = args.path;
if (args.path.startsWith(".")) {
entryPoint = path.join(
args.pluginData.resolveDir,
entryPoint
);
}
return {
path: entryPoint,
namespace: "wasm",
};
}
return {
path: args.path,
namespace: inline ? "wasm-inline" : "wasm-stub",
pluginData: {
resolveDir: args.resolveDir,
},
};
});
build.onLoad(
{ filter: /.*/, namespace: "wasm-inline" },
async (args) => ({
pluginData: args.pluginData,
contents: `
import wasm from ${JSON.stringify(args.path)};
export default function() {
return Promise.resolve(wasm.buffer);
};
`,
})
);
build.onLoad({ filter: /.*/, namespace: "wasm-stub" }, async (args) => {
const key = `__PSP_INLINE_WASM__${Math.random()
.toString()
.slice(2)}__`;
KEYSET.push(key);
const url = webpack_hack ? `${key}(wasm)` : `wasm`;
return {
pluginData: args.pluginData,
contents: `
import wasm from ${JSON.stringify(args.path)};
export default function() {
return fetch(new URL(${url}, import.meta.url));
};
`,
};
});
build.onLoad({ filter: /.*/, namespace: "wasm" }, async (args) => {
const path = require.resolve(args.path);
const contents = await fs.promises.readFile(path);
return {
pluginData: args.pluginData,
contents,
loader: inline ? "binary" : "file",
};
});
build.onEnd(({ metafile }) => {
if (webpack_hack) {
for (const file of Object.keys(metafile.outputs)) {
if (file.endsWith(".js")) {
let contents = fs.readFileSync(file).toString();
let updated = false;
for (const key of KEYSET) {
const symbol = contents.match(
new RegExp(`${key}\\(([a-zA-Z0-9_\$]+?)\\)`)
);
if (symbol?.[1]) {
updated = true;
const escapedSymbol = symbol[1].replace(
/\$/g,
"\\$"
);
const filename = contents.match(
new RegExp(
`(?<![a-zA-Z0-9_\$])${escapedSymbol}\\s*?=\\s*?\\"(.+?)\\"`
)
);
contents = contents.replace(
new RegExp(
`${key}\\(([a-zA-Z0-9_\$]+?)\\)`,
"g"
),
`"${filename[1]}"`
);
}
}
if (updated) {
fs.writeFileSync(file, contents);
}
}
}
}
});
}
return {
name: "wasm",
setup,
};
};
+314
View File
@@ -0,0 +1,314 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
const fs = require("fs");
const path = require("path");
const esbuild = require("esbuild");
/**
* Esbuild plugin that compiles a `.worker.js` import into a
* URL-yielding module. The shim `getWorkerURL()` returns a string
* that's usable by both `new Worker(url, { type: "module" })` *and*
* `await import(url)` — the in-process renderer path uses the latter
* so the chart code lives in the worker bundle exactly once.
*
* Two output modes:
* - `inline: true` (default, prod). The subbuild's bytes are
* embedded as a JS string in the parent bundle. `getWorkerURL()`
* creates a Blob URL at runtime, cached so both consumers share
* the same URL (and the same module instance via dynamic-import
* dedup).
* - `inline: false` (debug). The subbuild's output is written to
* the parent's `outdir` next to the main bundle, with source
* maps. `getWorkerURL()` resolves `import.meta.url` to that file,
* so DevTools can show real source paths and breakpoints work.
*/
exports.WorkerPlugin = function WorkerPlugin(options = {}) {
/**
* Optional esbuild plugins to apply to the worker sub-build (e.g.
* `GlslMinify`, `LightningCssMinify`). Use when the worker entry
* imports the same custom-loader file types as the outer bundle.
*/
const subbuildPlugins = options.plugins || [];
/**
* Optional `loader` map for the worker sub-build (e.g.
* `{ ".glsl": "text", ".css": "text" }`).
*/
const subbuildLoader = options.loader || {};
/**
* `false` to emit the worker bundle as a real file alongside the
* parent bundle (debug builds — preserves source maps + real
* paths in DevTools). Defaults to inline-Blob mode for prod.
*/
const inline = options.inline !== false;
const additionalOptions = options.additionalOptions || {};
function setup(build) {
build.initialOptions.metafile = true;
build.onResolve({ filter: /\.worker(\.js)?$/ }, (args) => {
if (args.namespace === "worker-stub") {
const baseName = path
.basename(args.path)
.replace(".worker", "");
const entryPoint = path.join(
args.pluginData.resolveDir,
args.path,
);
// `outdir` is set so that file-mode subbuilds produce
// real, non-`<stdout>` paths in `outputFiles[].path`
// — we use those paths to name the on-disk artifacts
// when copying to the parent bundle's outdir. In
// inline mode the path doesn't matter (we only read
// the bytes), but setting `outdir` is harmless.
const subbuild = esbuild.build({
target: ["es2021"],
entryPoints: [entryPoint],
define: {
global: "self",
},
outdir: ".",
entryNames: "[name]",
chunkNames: "[name]",
assetNames: "[name]",
minify: !process.env.PSP_DEBUG,
bundle: true,
sourcemap: !inline,
write: false,
plugins: subbuildPlugins,
loader: subbuildLoader,
format: "esm",
...additionalOptions,
});
return {
path: args.path.replace(".worker", ""),
namespace: "worker",
pluginData: {
baseName,
subbuild,
},
};
}
return {
path: args.path,
namespace: "worker-stub",
pluginData: {
resolveDir: args.resolveDir,
},
};
});
build.onLoad(
{ filter: /.*/, namespace: "worker-stub" },
async (args) => {
if (inline) {
return {
pluginData: args.pluginData,
// Inline mode: the parent bundle imports the
// worker bytes as a text string and constructs
// a Blob URL on first call to `getWorkerURL`.
// The cached URL is reused for both
// `new Worker(url)` and `await import(url)` so
// module dedup keeps a single instance.
//
// `initialize()` adds a Worker-or-shim path:
// attempts `new Worker(blobUrl)` first; falls
// back to running the worker source text on the
// main thread via `new Function(...)` when
// Worker construction is unavailable (e.g.
// `file://` origins where module-Worker support
// is gated, or environments without the Worker
// constructor at all). The shim returned by
// \`make_host\` is MessagePort-shaped so
// downstream consumers can treat it like a real
// Worker.
contents: `
import workerSource from ${JSON.stringify(args.path)};
let cached = null;
export async function getWorkerURL() {
if (cached) return cached;
const blob = new Blob([workerSource], {
type: "application/javascript",
});
cached = URL.createObjectURL(blob);
return cached;
}
function make_host(a, b) {
return {
addEventListener(type, callback) {
if (type === "message") {
a.push(callback);
}
},
removeEventListener(type, callback) {
const idx = a.indexOf(callback);
if (idx > -1) {
a.splice(idx, 1);
}
},
postMessage(msg, ports) {
for (const listener of b) {
listener({
data: msg,
ports: ports,
});
}
},
terminate() {},
location: { href: "" },
};
}
function run_single_threaded() {
console.warn(
"Running perspective in single-threaded mode"
);
const f = Function(
"const self = arguments[0];" + workerSource
);
const workers = [];
const mains = [];
f(make_host(workers, mains));
return make_host(mains, workers);
}
export async function initialize(opts) {
const workerOpts = opts || {
type: "module",
};
try {
if (
typeof window !== "undefined" &&
window.location &&
window.location.protocol &&
window.location.protocol.startsWith(
"file"
)
) {
console.warn(
"file:// protocol does not reliably support Web Workers"
);
return run_single_threaded();
}
const url = await getWorkerURL();
const worker = new Worker(url, workerOpts);
return worker;
} catch (e) {
console.error(
"Error instantiating worker; falling back to single-threaded mode",
e
);
return run_single_threaded();
}
}
export default getWorkerURL;
`,
};
}
// File mode: the subbuild writes its output to disk
// next to the parent bundle, and `getWorkerURL`
// resolves the worker file relative to the consuming
// module's URL (so `<script type="module" src=...>`
// and dynamic imports both work without a baked-in
// absolute path). The basename comes from the
// namespace import below.
return {
pluginData: args.pluginData,
contents: `
import basename from ${JSON.stringify(args.path)};
const workerURL = new URL(
"./" + basename,
import.meta.url,
).toString();
export async function getWorkerURL() {
return workerURL;
}
export default getWorkerURL;
`,
};
},
);
build.onLoad({ filter: /.*/, namespace: "worker" }, async (args) => {
const result = await args.pluginData.subbuild;
const outpath =
build.initialOptions.outdir ||
path.dirname(build.initialOptions.outfile);
if (!fs.existsSync(outpath)) {
fs.mkdirSync(outpath, { recursive: true });
}
if (inline) {
// Embed the bundle bytes as a `text` string in the
// parent bundle; the stub above wraps them in a Blob
// URL at runtime. The `.js.map` (when present) is
// discarded — Blob URLs strip the source-map comment
// anyway since DevTools can't follow it back to disk.
const jsOut = result.outputFiles.find((o) =>
o.path.endsWith(".js"),
);
if (!jsOut) {
throw new Error("worker subbuild produced no .js output");
}
return { contents: jsOut.contents, loader: "text" };
}
// File mode: write the worker bundle (and source map)
// alongside the parent bundle. Multiple output files are
// emitted by esbuild when `sourcemap: true`. Use the
// actual emitted .js basename for the URL so the stub
// and the on-disk artifact agree (esbuild-emitted
// basename includes `.worker` from the entry filename).
let jsBaseName = null;
for (const out of result.outputFiles) {
const base = path.basename(out.path);
const dst = path.join(outpath, base);
await fs.promises.writeFile(dst, out.contents);
if (base.endsWith(".js")) {
jsBaseName = base;
}
}
if (!jsBaseName) {
throw new Error("worker subbuild produced no .js output");
}
return {
contents: `export default ${JSON.stringify(jsBaseName)};`,
loader: "js",
};
});
}
return {
name: "webworker",
setup,
};
};