315 lines
15 KiB
JavaScript
315 lines
15 KiB
JavaScript
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
// ┃ 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,
|
|
};
|
|
};
|