// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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-`` 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 `