@nubjs/nub-linux-arm64 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nub +0 -0
- package/bin/nubx +0 -0
- package/package.json +1 -1
- package/runtime/addons/nub-native.node +0 -0
- package/runtime/preload-common.cjs +130 -2
- package/runtime/preload.cjs +11 -0
- package/runtime/version.mjs +1 -1
- package/runtime/worker-blob-url.cjs +116 -0
- package/runtime/worker-polyfill.mjs +241 -38
package/bin/nub
CHANGED
|
Binary file
|
package/bin/nubx
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -257,6 +257,76 @@ function registerLoaderWorker(specifier, parentURL, options) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// Is this error Node's "an async `module.register` loader cannot service a SYNCHRONOUS
|
|
261
|
+
// resolve/load" stub? On Node 22.15–~24.11 the async-hooks proxy's `resolveSync`/
|
|
262
|
+
// `loadSync` are stubs that unconditionally `throw new ERR_METHOD_NOT_IMPLEMENTED(...)`.
|
|
263
|
+
// nub's fast-tier SYNC `module.registerHooks` hooks force EVERY resolve/load onto the
|
|
264
|
+
// synchronous chain; when a USER async loader (e.g. @tailwindcss/node's
|
|
265
|
+
// esm-cache.loader.mjs under Turbopack) is ALSO registered, the chain's default step
|
|
266
|
+
// reaches that stub and throws — killing the build. We must detect this WITHOUT the
|
|
267
|
+
// `userAsyncLoaderActive()` flag: the very FIRST throwing resolution is the loader
|
|
268
|
+
// module's own specifier, resolved DURING `module.register` before the detector flag is
|
|
269
|
+
// observable, so the flag is false exactly when recovery is needed. The error code +
|
|
270
|
+
// message is the reliable signal. (Node 24.12+/25.2+/26 implement these methods, so the
|
|
271
|
+
// stub never throws there and this never fires.)
|
|
272
|
+
function isAsyncLoaderSyncStub(err) {
|
|
273
|
+
if (!err || typeof err.message !== "string") return false;
|
|
274
|
+
// Two shapes across the affected Node band: (a) the method exists but is a stub that
|
|
275
|
+
// throws ERR_METHOD_NOT_IMPLEMENTED('resolveSync()'/'loadSync()') (e.g. 24.3); (b) the
|
|
276
|
+
// method is ENTIRELY ABSENT, so Node's `this[#customizations].resolveSync/loadSync(...)`
|
|
277
|
+
// throws a TypeError "... is not a function" (e.g. 22.16/24.11). Match both.
|
|
278
|
+
if (err.code === "ERR_METHOD_NOT_IMPLEMENTED" &&
|
|
279
|
+
(err.message.includes("resolveSync") || err.message.includes("loadSync"))) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
return err instanceof TypeError &&
|
|
283
|
+
(err.message.includes("resolveSync is not a function") ||
|
|
284
|
+
err.message.includes("loadSync is not a function"));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Resolve a specifier to a URL the registerHooks resolve chain can return, as the
|
|
288
|
+
// recovery path when Node's default resolve step throws the async-loader stub (see
|
|
289
|
+
// isAsyncLoaderSyncStub). Returns `{ url, shortCircuit }`, or null if it cannot resolve
|
|
290
|
+
// (caller re-throws the original error so behavior is unchanged).
|
|
291
|
+
//
|
|
292
|
+
// Uses the parent's CommonJS resolver (`createRequire().resolve`) — the only fully-sync,
|
|
293
|
+
// conditions-capable resolver available in this `--require` CJS preload. It honors
|
|
294
|
+
// `node_modules`, package `exports`, relative + absolute paths, and bare/scoped
|
|
295
|
+
// specifiers, but under the REQUIRE condition; a DUAL package whose `exports` differ by
|
|
296
|
+
// `import`/`require` gets its `require` build where Node's default ESM resolve would
|
|
297
|
+
// return the `import` build (latent — the in-the-wild trigger resolves non-dual internal
|
|
298
|
+
// modules). Builtins MUST be mapped back to `node:`: `require.resolve("fs")` returns the
|
|
299
|
+
// bare name `"fs"`, which must not become a bogus `file://<cwd>/fs` URL.
|
|
300
|
+
function resolveViaParentRequire(specifier, parentURL) {
|
|
301
|
+
try {
|
|
302
|
+
// An ALREADY-RESOLVED specifier needs no resolution — return it verbatim. This is the
|
|
303
|
+
// common Turbopack/Tailwind trigger: Node resolves the loader module's OWN absolute
|
|
304
|
+
// `file://` URL (`@tailwindcss/node/dist/esm-cache.loader.mjs`) synchronously during
|
|
305
|
+
// `module.register`, with `parentURL` = `data:` (no useful base). `require.resolve`
|
|
306
|
+
// cannot take a `file://` URL string, so pass these through directly. Builtins and
|
|
307
|
+
// `data:` specifiers are likewise already-resolved.
|
|
308
|
+
if (specifier.startsWith("file:") || specifier.startsWith("data:")) {
|
|
309
|
+
return { url: specifier, shortCircuit: true };
|
|
310
|
+
}
|
|
311
|
+
if (specifier.startsWith("node:") || module_.isBuiltin(specifier)) {
|
|
312
|
+
return { url: specifier.startsWith("node:") ? specifier : `node:${specifier}`, shortCircuit: true };
|
|
313
|
+
}
|
|
314
|
+
const base = parentURL && String(parentURL).startsWith("file:")
|
|
315
|
+
? String(parentURL)
|
|
316
|
+
: pathToFileURL(join(process.cwd(), "noop.js")).href;
|
|
317
|
+
const resolved = module_.createRequire(base).resolve(specifier);
|
|
318
|
+
if (module_.isBuiltin(resolved)) {
|
|
319
|
+
return { url: resolved.startsWith("node:") ? resolved : `node:${resolved}`, shortCircuit: true };
|
|
320
|
+
}
|
|
321
|
+
const url = resolved.startsWith("node:") || resolved.startsWith("data:")
|
|
322
|
+
? resolved
|
|
323
|
+
: pathToFileURL(resolved).href;
|
|
324
|
+
return { url, shortCircuit: true };
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
260
330
|
function makeHooks(core, watchReporting) {
|
|
261
331
|
installUserHookDetector();
|
|
262
332
|
installUserAsyncLoaderDetector();
|
|
@@ -278,7 +348,41 @@ function makeHooks(core, watchReporting) {
|
|
|
278
348
|
if (res) return res;
|
|
279
349
|
} catch { /* fall through to Node's resolver */ }
|
|
280
350
|
}
|
|
281
|
-
|
|
351
|
+
try {
|
|
352
|
+
return nextResolve(specifier, context);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (isAsyncLoaderSyncStub(err)) {
|
|
355
|
+
const fallback = resolveViaParentRequire(specifier, context.parentURL);
|
|
356
|
+
if (fallback) return fallback;
|
|
357
|
+
}
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Recovery for the async-loader `loadSync` stub: load a `file:` module ourselves when
|
|
363
|
+
// Node's default load step throws ERR_METHOD_NOT_IMPLEMENTED (see isAsyncLoaderSyncStub).
|
|
364
|
+
// Reads source from disk and derives the format from extension + nearest package `type`
|
|
365
|
+
// (nub's own moduleFormatFor — the same source-of-truth as the transpile path). For a
|
|
366
|
+
// transpilable TS/JSX file outside node_modules, route through nub's transpiler; for a
|
|
367
|
+
// data-extension, through loadData; otherwise hand back raw source with the derived
|
|
368
|
+
// format. Returns null if it cannot (caller re-throws). A CJS result keeps source:null
|
|
369
|
+
// and hands off to the native CommonJS loader, matching the default-load contract.
|
|
370
|
+
function loadViaDisk(url, ext) {
|
|
371
|
+
try {
|
|
372
|
+
const path = fileURLToPath(url);
|
|
373
|
+
if (core.TRANSPILE_EXTS.has(ext) && !core.isNodeModules(url)) {
|
|
374
|
+
return core.loadTranspile(url, ext);
|
|
375
|
+
}
|
|
376
|
+
if (ext in core.DATA_EXTS) return core.loadData(url, ext);
|
|
377
|
+
const { readFileSync } = require("node:fs");
|
|
378
|
+
const source = readFileSync(path);
|
|
379
|
+
const pkgType = core.getPackageType(dirname(path));
|
|
380
|
+
const format = core.moduleFormatFor(ext, pkgType, path, source.toString("utf8"));
|
|
381
|
+
if (format === "commonjs") return { format: "commonjs", source: null, shortCircuit: true };
|
|
382
|
+
return { format, source, shortCircuit: true };
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
282
386
|
}
|
|
283
387
|
|
|
284
388
|
function load(url, context, nextLoad) {
|
|
@@ -354,7 +458,31 @@ function makeHooks(core, watchReporting) {
|
|
|
354
458
|
return { format: undefined, source: "", shortCircuit: true };
|
|
355
459
|
}
|
|
356
460
|
|
|
357
|
-
|
|
461
|
+
let r;
|
|
462
|
+
try {
|
|
463
|
+
r = nextLoad(url, context);
|
|
464
|
+
} catch (err) {
|
|
465
|
+
// Async-loader `loadSync` stub (the load-hook twin of the resolve recovery): with a
|
|
466
|
+
// USER async `module.register` loader present, nub's sync load hook forces the default
|
|
467
|
+
// step onto the synchronous chain, which calls the async-hooks `loadSync` stub →
|
|
468
|
+
// ERR_METHOD_NOT_IMPLEMENTED. Recover by loading the module ourselves: read source
|
|
469
|
+
// from disk and derive the format from the URL/extension (the same source-of-truth
|
|
470
|
+
// nub's own transpile path uses), so the synchronous module-job gets real source
|
|
471
|
+
// instead of crashing. node:/data:/non-file URLs and the no-format case fall through
|
|
472
|
+
// to a re-throw (we can't synthesize those here). Re-throw anything that isn't the stub.
|
|
473
|
+
if (isAsyncLoaderSyncStub(err) && typeof url === "string") {
|
|
474
|
+
// Builtins: hand back the `builtin` format with no source — Node loads them
|
|
475
|
+
// natively (a `node:`-scheme module the default load would have returned builtin for).
|
|
476
|
+
if (url.startsWith("node:") || module_.isBuiltin(url)) {
|
|
477
|
+
return { format: "builtin", source: null, shortCircuit: true };
|
|
478
|
+
}
|
|
479
|
+
if (url.startsWith("file:")) {
|
|
480
|
+
const recovered = loadViaDisk(url, ext);
|
|
481
|
+
if (recovered) return recovered;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
358
486
|
|
|
359
487
|
// #18 — relabel a `commonjs` result as `commonjs-sync` for `file:` URLs ON THE
|
|
360
488
|
// `import()`-OF-CJS PATH so the module (and its inner require()s) routes through
|
package/runtime/preload.cjs
CHANGED
|
@@ -218,6 +218,17 @@ function installLazyEsmPolyfills() {
|
|
|
218
218
|
return;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
// Main thread: install blob: worker support EAGERLY. It only wraps
|
|
222
|
+
// URL.createObjectURL + subclasses Blob (touches no node:worker_threads, so it's
|
|
223
|
+
// cold-start-cheap) and MUST be live before user code calls createObjectURL —
|
|
224
|
+
// which happens before the first `new Worker`, so it cannot wait for the lazy
|
|
225
|
+
// Worker load below. See runtime/worker-blob-url.cjs.
|
|
226
|
+
try {
|
|
227
|
+
__require("./worker-blob-url.cjs").installBlobUrlSupport();
|
|
228
|
+
} catch {
|
|
229
|
+
// blob: worker support is best-effort; never block startup on it.
|
|
230
|
+
}
|
|
231
|
+
|
|
221
232
|
// Main thread: lazy Worker global. Defined NON-ENUMERABLE so it stays invisible
|
|
222
233
|
// to `Object.keys(globalThis)` / for-in — the additive contract — matching how
|
|
223
234
|
// worker-polyfill.mjs defines the real one.
|
package/runtime/version.mjs
CHANGED
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
// previously lived as a literal inside preload.mjs, which `make version` patched,
|
|
10
10
|
// while the worker carried a hand-maintained "…-compat" copy that `make version`
|
|
11
11
|
// never touched — a latent staleness bug this module closes.)
|
|
12
|
-
export const NUB_VERSION = "0.
|
|
12
|
+
export const NUB_VERSION = "0.2.0";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Blob-URL worker source capture — the SYNC half of WHATWG `blob:` worker support.
|
|
2
|
+
//
|
|
3
|
+
// A `new Worker(blobUrl)` must read the Blob's source SYNCHRONOUSLY in the
|
|
4
|
+
// constructor, but a Blob's bytes are only readable via the async Blob.text /
|
|
5
|
+
// arrayBuffer. We close that gap by snapshotting the source at
|
|
6
|
+
// `URL.createObjectURL(blob)` time — which always runs BEFORE the Worker is
|
|
7
|
+
// constructed — into a registry keyed by the minted URL.
|
|
8
|
+
//
|
|
9
|
+
// This lives in its OWN tiny CJS module (no node:worker_threads dependency) so the
|
|
10
|
+
// main-thread preload can install the wrap EAGERLY: it must be live before user
|
|
11
|
+
// code calls createObjectURL, whereas worker-polyfill.mjs (which pulls
|
|
12
|
+
// worker_threads + the whole streams/worker-io builtin set) is loaded LAZILY on
|
|
13
|
+
// first `new Worker` to protect cold start. The Worker class imports THIS module to
|
|
14
|
+
// read `blobUrlSources`. Touches only URL / Blob / Buffer — all already-realized
|
|
15
|
+
// core globals — so requiring it adds nothing to the main-thread bootstrap set.
|
|
16
|
+
|
|
17
|
+
// blob: URL → source text. Shared with worker-polyfill.mjs's Worker constructor.
|
|
18
|
+
const blobUrlSources = new Map();
|
|
19
|
+
// Blob construction parts, remembered so createObjectURL can assemble source sync.
|
|
20
|
+
const blobParts = new WeakMap();
|
|
21
|
+
|
|
22
|
+
function decode(parts) {
|
|
23
|
+
let src = "";
|
|
24
|
+
for (const p of parts ?? []) {
|
|
25
|
+
if (typeof p === "string") src += p;
|
|
26
|
+
else if (typeof Buffer !== "undefined" && Buffer.isBuffer(p)) src += p.toString("utf8");
|
|
27
|
+
else if (ArrayBuffer.isView(p)) src += Buffer.from(p.buffer, p.byteOffset, p.byteLength).toString("utf8");
|
|
28
|
+
else if (p instanceof ArrayBuffer) src += Buffer.from(p).toString("utf8");
|
|
29
|
+
else if (typeof p === "object" && p && typeof p.size === "number") {
|
|
30
|
+
const nested = blobParts.get(p); // a nested Blob made via our wrapper
|
|
31
|
+
if (nested) src += decode(nested);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return src;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Wrap URL.createObjectURL/revokeObjectURL and Proxy Blob to remember parts.
|
|
38
|
+
// Idempotent + transparent for every non-worker use. No-op when URL has no
|
|
39
|
+
// createObjectURL (older floors) — blob: workers are then simply unavailable.
|
|
40
|
+
//
|
|
41
|
+
// The install-marker is a Symbol, not a string property: it sits on the native
|
|
42
|
+
// Blob constructor (a shared global), so a string key would show up in
|
|
43
|
+
// `Reflect.ownKeys(Blob)` / `"x" in Blob` — a (cosmetic) divergence from vanilla
|
|
44
|
+
// Node. A Symbol keeps the marker invisible to those reflective surfaces.
|
|
45
|
+
const INSTALLED = Symbol.for("nub.blobUrlSupport.installed");
|
|
46
|
+
function installBlobUrlSupport() {
|
|
47
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return;
|
|
48
|
+
if (URL.createObjectURL[INSTALLED]) return;
|
|
49
|
+
|
|
50
|
+
const NativeBlob = globalThis.Blob;
|
|
51
|
+
if (typeof NativeBlob === "function" && !NativeBlob[INSTALLED]) {
|
|
52
|
+
// Record construction parts WITHOUT changing Blob's identity. The earlier
|
|
53
|
+
// approach — a `class extends NativeBlob` swapped onto globalThis.Blob — broke
|
|
54
|
+
// brand identity in two ways that a structured-clone exposes:
|
|
55
|
+
// 1. A deserialized Blob (postMessage / structuredClone) is a NATIVE Blob, so
|
|
56
|
+
// `cloned instanceof globalThis.Blob` was FALSE (subclass.prototype is not
|
|
57
|
+
// in a native instance's chain) — a spec violation; native Node passes.
|
|
58
|
+
// 2. WORSE, undici's webidl `is.Blob` uses the ORDINARY `[Symbol.hasInstance]`
|
|
59
|
+
// (a prototype-chain check that ignores a custom hasInstance) against
|
|
60
|
+
// `globalThis.Blob`, so `new Response(clonedBlob).arrayBuffer()` failed the
|
|
61
|
+
// brand check and stringified the Blob to "[object Blob]" (13 bytes) instead
|
|
62
|
+
// of reading its bytes. A custom `Symbol.hasInstance` can't fix this — undici
|
|
63
|
+
// bypasses it.
|
|
64
|
+
// The ROOT cause was `globalThis.Blob !== node:buffer.Blob`. A Proxy whose target
|
|
65
|
+
// is the native Blob keeps `globalThis.Blob.prototype === NativeBlob.prototype`,
|
|
66
|
+
// so every native Blob (constructed OR deserialized) passes both `instanceof` and
|
|
67
|
+
// undici's ordinary-hasInstance check exactly as on vanilla Node — while the
|
|
68
|
+
// `construct` trap still records parts for sync blob: worker source assembly. File
|
|
69
|
+
// (which `extends` native Blob) then needs NO re-parenting: its instances already
|
|
70
|
+
// have NativeBlob.prototype in their chain.
|
|
71
|
+
const BlobProxy = new Proxy(NativeBlob, {
|
|
72
|
+
construct(target, args, newTarget) {
|
|
73
|
+
// FORWARD newTarget so a user subclass (`class X extends Blob {}`) gets ITS
|
|
74
|
+
// prototype — passing `target` here would force NativeBlob.prototype and
|
|
75
|
+
// silently break `new X() instanceof X` + the subclass's methods (an
|
|
76
|
+
// additivity violation vs vanilla Node). When `new Blob(...)` is called
|
|
77
|
+
// directly, newTarget IS this Proxy; Reflect.construct(NativeBlob, args, Proxy)
|
|
78
|
+
// resolves the new instance's proto from `Proxy.prototype`, which the Proxy
|
|
79
|
+
// forwards to NativeBlob.prototype — so a direct Blob is byte-identical to a
|
|
80
|
+
// native one (same brand, passes instanceof + undici's webidl check).
|
|
81
|
+
const inst = Reflect.construct(target, args, newTarget);
|
|
82
|
+
if (args[0] != null) blobParts.set(inst, args[0]);
|
|
83
|
+
return inst;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(NativeBlob, INSTALLED, { value: true });
|
|
87
|
+
Object.defineProperty(globalThis, "Blob", {
|
|
88
|
+
value: BlobProxy,
|
|
89
|
+
enumerable: false,
|
|
90
|
+
writable: true,
|
|
91
|
+
configurable: true,
|
|
92
|
+
});
|
|
93
|
+
// File's `extends` link still points at the real NativeBlob (unchanged), so
|
|
94
|
+
// `new File(...) instanceof Blob` holds natively — no re-parenting needed.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nativeCreate = URL.createObjectURL.bind(URL);
|
|
98
|
+
const nativeRevoke =
|
|
99
|
+
typeof URL.revokeObjectURL === "function" ? URL.revokeObjectURL.bind(URL) : null;
|
|
100
|
+
const wrappedCreate = function createObjectURL(obj) {
|
|
101
|
+
const url = nativeCreate(obj);
|
|
102
|
+
const parts = blobParts.get(obj);
|
|
103
|
+
if (parts) blobUrlSources.set(url, decode(parts));
|
|
104
|
+
return url;
|
|
105
|
+
};
|
|
106
|
+
Object.defineProperty(wrappedCreate, INSTALLED, { value: true });
|
|
107
|
+
URL.createObjectURL = wrappedCreate;
|
|
108
|
+
if (nativeRevoke) {
|
|
109
|
+
URL.revokeObjectURL = function revokeObjectURL(url) {
|
|
110
|
+
blobUrlSources.delete(url);
|
|
111
|
+
return nativeRevoke(url);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { blobUrlSources, installBlobUrlSupport };
|
|
@@ -87,76 +87,199 @@ function getErrorEventCtor() {
|
|
|
87
87
|
export function installWorkerPolyfill() {
|
|
88
88
|
const { Worker: NodeWorker, parentPort, isMainThread } = __getBuiltin("node:worker_threads");
|
|
89
89
|
const { fileURLToPath } = __getBuiltin("node:url");
|
|
90
|
+
// blob: worker source registry, shared with the eager main-thread preload that
|
|
91
|
+
// wraps URL.createObjectURL (worker-blob-url.cjs). Loaded via createRequire so
|
|
92
|
+
// both this lazily-loaded ESM module and the eager CJS preload reference the SAME
|
|
93
|
+
// module instance (Node dedupes by resolved path) — i.e. the SAME blobUrlSources.
|
|
94
|
+
const { blobUrlSources, installBlobUrlSupport } = (
|
|
95
|
+
typeof process.getBuiltinModule === "function"
|
|
96
|
+
? __getBuiltin("node:module").createRequire(import.meta.url)
|
|
97
|
+
: _bootstrapCreateRequire(import.meta.url)
|
|
98
|
+
)("./worker-blob-url.cjs");
|
|
99
|
+
|
|
100
|
+
// Resolve a worker-error stack frame to {filename,lineno,colno} so the
|
|
101
|
+
// ErrorEvent carries real source location, per WHATWG §10.2.6 (the spec
|
|
102
|
+
// requires these fields populated from where the error was raised). Node's
|
|
103
|
+
// `error` event delivers the thrown Error; we read its first stack frame.
|
|
104
|
+
// Browser-scrubbing of cross-origin frames does not apply here (all worker
|
|
105
|
+
// sources are same-origin local), so we surface the raw location.
|
|
106
|
+
//
|
|
107
|
+
// The frame is anchored at `at ` and consumes the optional `Func (` wrapper so
|
|
108
|
+
// group 1 is JUST the path (a `file://` URL, an absolute POSIX path, or a
|
|
109
|
+
// Windows `C:\…` path — the leading `C:` is NOT mistaken for the location
|
|
110
|
+
// colon because the line:col are the LAST two `:`-segments). Without the anchor
|
|
111
|
+
// + wrapper-consumption the path captured the `at Func (` prefix verbatim.
|
|
112
|
+
const STACK_FRAME = /^at\s+(?:.+?\s+\()?(.+?):(\d+):(\d+)\)?$/;
|
|
113
|
+
function locationFromError(err) {
|
|
114
|
+
let filename = "";
|
|
115
|
+
let lineno = 0;
|
|
116
|
+
let colno = 0;
|
|
117
|
+
const stack = err && typeof err.stack === "string" ? err.stack : "";
|
|
118
|
+
for (const line of stack.split("\n")) {
|
|
119
|
+
const t = line.trim();
|
|
120
|
+
if (!t.startsWith("at ")) continue;
|
|
121
|
+
const m = STACK_FRAME.exec(t);
|
|
122
|
+
if (m) {
|
|
123
|
+
filename = m[1].startsWith("file://") ? fileURLToPath(m[1]) : m[1];
|
|
124
|
+
lineno = Number(m[2]) || 0;
|
|
125
|
+
colno = Number(m[3]) || 0;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { filename, lineno, colno };
|
|
130
|
+
}
|
|
90
131
|
|
|
91
132
|
if (typeof globalThis.Worker === "undefined") {
|
|
92
133
|
class Worker extends EventTarget {
|
|
93
134
|
#worker;
|
|
135
|
+
#name;
|
|
94
136
|
|
|
95
137
|
constructor(url, options = {}) {
|
|
96
138
|
super();
|
|
97
139
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
// The WHATWG Worker constructor accepts a script URL. Per §10.2.6.3 the
|
|
141
|
+
// standard inline mechanisms are `blob:` and `data:` URLs (there is no
|
|
142
|
+
// inline-source-string form in the spec). We map each to a Node spawn:
|
|
143
|
+
// - file path / file: URL → spawn the file (transpiled by nub's preload)
|
|
144
|
+
// - data: URL → Node runs it directly (worker_threads v14.9)
|
|
145
|
+
// - blob: URL → resolve the Blob via node:buffer, spawn its
|
|
146
|
+
// source with eval:true (Node can't open blob:)
|
|
147
|
+
let spawnTarget;
|
|
148
|
+
|
|
149
|
+
const asUrlString =
|
|
150
|
+
url instanceof URL ? url.href : typeof url === "string" ? url : null;
|
|
151
|
+
if (asUrlString === null) {
|
|
152
|
+
throw new TypeError("Worker constructor: url must be a string or URL");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (asUrlString.startsWith("blob:")) {
|
|
156
|
+
// A `blob:` worker (WHATWG inline mechanism). Node cannot open a blob:
|
|
157
|
+
// URL as a worker entry, and the Blob's bytes are only readable
|
|
158
|
+
// ASYNCHRONOUSLY (Blob.text/arrayBuffer) while this constructor is sync.
|
|
159
|
+
// We close that gap by snapshotting the source SYNCHRONOUSLY at
|
|
160
|
+
// `URL.createObjectURL(blob)` time (see installBlobUrlSupport) into a
|
|
161
|
+
// module-scope registry keyed by URL, then spawn the source as a `data:`
|
|
162
|
+
// URL. We use data: (NOT eval:true) deliberately: the `--import` preload
|
|
163
|
+
// that installs nub's worker-side scope (self/postMessage) does NOT run in
|
|
164
|
+
// an eval:true worker on the compat-tier FLOOR (Node 18.19 — verified), so
|
|
165
|
+
// an eval-based blob worker has no `self` there; a data: URL worker is a
|
|
166
|
+
// real module load and DOES receive the preload on every supported tier.
|
|
167
|
+
const source = blobUrlSources.get(asUrlString);
|
|
168
|
+
if (source === undefined) {
|
|
169
|
+
throw new TypeError(
|
|
170
|
+
`Worker constructor: blob URL '${asUrlString}' is not a known object URL`
|
|
171
|
+
);
|
|
106
172
|
}
|
|
173
|
+
spawnTarget = new URL(
|
|
174
|
+
"data:text/javascript;base64," + Buffer.from(source, "utf8").toString("base64")
|
|
175
|
+
);
|
|
176
|
+
} else if (asUrlString.startsWith("data:")) {
|
|
177
|
+
spawnTarget = new URL(asUrlString);
|
|
178
|
+
} else if (asUrlString.startsWith("file://")) {
|
|
179
|
+
spawnTarget = fileURLToPath(asUrlString);
|
|
107
180
|
} else {
|
|
108
|
-
|
|
181
|
+
spawnTarget = asUrlString;
|
|
109
182
|
}
|
|
110
183
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
184
|
+
this.#name = typeof options.name === "string" ? options.name : "";
|
|
185
|
+
|
|
186
|
+
// `type: "module" | "classic"` selects the worker's module system per the
|
|
187
|
+
// spec. The worker-side scope exposes `importScripts` only for classic
|
|
188
|
+
// workers (WHATWG WorkerGlobalScope — classic-only) and throws for module
|
|
189
|
+
// workers. nub signals the choice to the worker via the internal
|
|
190
|
+
// NUB_WORKER_TYPE env (internal plumbing var — exempt from the brand
|
|
191
|
+
// boundary). Node still decides the entry's actual module/CJS PARSING by
|
|
192
|
+
// file extension + package.json "type"; this env governs only which
|
|
193
|
+
// importScripts surface the polyfill installs.
|
|
194
|
+
const workerType =
|
|
195
|
+
options.type === "classic" ? "classic" : "module";
|
|
196
|
+
|
|
197
|
+
// Node rejects flags in Worker execArgv that imply V8 `--harmony-*` staging
|
|
198
|
+
// flags (ERR_WORKER_INVALID_EXEC_ARGV): `--harmony-*` themselves, and
|
|
199
|
+
// `--experimental-shadow-realm` (implies `--harmony-shadow-realm`). Strip
|
|
200
|
+
// those from whatever execArgv we forward.
|
|
201
|
+
const stripHarmony = (argv) =>
|
|
202
|
+
argv.filter(
|
|
203
|
+
f => !f.startsWith("--harmony") && f !== "--experimental-shadow-realm"
|
|
204
|
+
);
|
|
205
|
+
// execArgv: forward nub's preload-carrying parent execArgv by DEFAULT (so a
|
|
206
|
+
// worker inherits nub's transpile augmentation), but if the user supplied
|
|
207
|
+
// their own execArgv, MERGE rather than clobber — parent flags first, user
|
|
208
|
+
// flags appended so the user's win on conflict.
|
|
209
|
+
const execArgv = stripHarmony(
|
|
210
|
+
Array.isArray(options.execArgv)
|
|
211
|
+
? [...process.execArgv, ...options.execArgv]
|
|
212
|
+
: process.execArgv
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const nodeOptions = {
|
|
129
216
|
...options,
|
|
130
217
|
eval: false,
|
|
131
|
-
execArgv
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
218
|
+
execArgv,
|
|
219
|
+
};
|
|
220
|
+
// Thread the worker type AND name to the worker via internal env vars
|
|
221
|
+
// (NUB_WORKER_TYPE / NUB_WORKER_NAME — internal plumbing, exempt from the
|
|
222
|
+
// brand boundary). NUB_WORKER_NAME is REQUIRED for self.name across the
|
|
223
|
+
// whole compat tier: worker_threads.threadName (the only native worker-side
|
|
224
|
+
// reader of the {name} option) lands in v24.6.0 / v22.20.0, so it is absent
|
|
225
|
+
// below that and the env is the sole portable carrier. We avoid disturbing
|
|
226
|
+
// the user's env semantics: `worker_threads.SHARE_ENV` is a Symbol
|
|
227
|
+
// (live-shared parent env) — spreading it would destroy the share — so in
|
|
228
|
+
// that case we leave env untouched (self.name then falls back to native
|
|
229
|
+
// threadName/"" and importScripts defaults to the classic form).
|
|
230
|
+
const userEnv = options.env;
|
|
231
|
+
if (typeof userEnv === "symbol") {
|
|
232
|
+
// SHARE_ENV: leave nodeOptions.env as the user gave it; can't inject.
|
|
233
|
+
} else {
|
|
234
|
+
nodeOptions.env = {
|
|
235
|
+
...(userEnv ?? process.env),
|
|
236
|
+
NUB_WORKER_TYPE: workerType,
|
|
237
|
+
NUB_WORKER_NAME: this.#name,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (this.#name) nodeOptions.name = this.#name;
|
|
241
|
+
|
|
242
|
+
this.#worker = new NodeWorker(spawnTarget, nodeOptions);
|
|
135
243
|
|
|
136
244
|
this.#worker.on("message", (data) => {
|
|
137
245
|
this.dispatchEvent(new MessageEvent("message", { data }));
|
|
138
246
|
});
|
|
139
247
|
|
|
140
|
-
|
|
141
|
-
|
|
248
|
+
// WHATWG: messageerror fires when an inbound message fails deserialization;
|
|
249
|
+
// it is a plain MessageEvent with `data: null` (NOT carrying the error).
|
|
250
|
+
this.#worker.on("messageerror", () => {
|
|
251
|
+
this.dispatchEvent(new MessageEvent("messageerror", { data: null }));
|
|
142
252
|
});
|
|
143
253
|
|
|
144
254
|
this.#worker.on("error", (err) => {
|
|
145
255
|
const ErrorEventCtor = getErrorEventCtor();
|
|
146
|
-
|
|
256
|
+
const { filename, lineno, colno } = locationFromError(err);
|
|
257
|
+
this.dispatchEvent(
|
|
258
|
+
new ErrorEventCtor("error", {
|
|
259
|
+
error: err,
|
|
260
|
+
message: err.message,
|
|
261
|
+
filename,
|
|
262
|
+
lineno,
|
|
263
|
+
colno,
|
|
264
|
+
})
|
|
265
|
+
);
|
|
147
266
|
});
|
|
267
|
+
// No `exit` event: WHATWG Workers have no exit event (it is a
|
|
268
|
+
// node:worker_threads concept, not part of the web Worker surface).
|
|
269
|
+
}
|
|
148
270
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
271
|
+
get name() {
|
|
272
|
+
return this.#name;
|
|
152
273
|
}
|
|
153
274
|
|
|
154
275
|
postMessage(data, transfer) {
|
|
155
276
|
this.#worker.postMessage(data, transfer);
|
|
156
277
|
}
|
|
157
278
|
|
|
279
|
+
// WHATWG terminate() returns void. Node's returns a Promise; we discard it so
|
|
280
|
+
// the surface matches the spec (the underlying termination still proceeds).
|
|
158
281
|
terminate() {
|
|
159
|
-
|
|
282
|
+
this.#worker.terminate();
|
|
160
283
|
}
|
|
161
284
|
|
|
162
285
|
#onmessageHandler = null;
|
|
@@ -194,6 +317,11 @@ export function installWorkerPolyfill() {
|
|
|
194
317
|
writable: true,
|
|
195
318
|
configurable: true,
|
|
196
319
|
});
|
|
320
|
+
|
|
321
|
+
// Enable blob: workers: wrap URL.createObjectURL so the source is captured
|
|
322
|
+
// synchronously for the constructor's blob: branch. Transparent for all other
|
|
323
|
+
// uses; installs once. Only on the main thread (where blob: URLs are minted).
|
|
324
|
+
if (isMainThread) installBlobUrlSupport();
|
|
197
325
|
}
|
|
198
326
|
|
|
199
327
|
// Worker-side bootstrap: emulate the DedicatedWorkerGlobalScope on top of
|
|
@@ -221,6 +349,81 @@ if (!isMainThread && parentPort) {
|
|
|
221
349
|
});
|
|
222
350
|
defineGlobal("self", scope);
|
|
223
351
|
|
|
352
|
+
// `self.name` — the worker's name from the constructor's {name} option (WHATWG
|
|
353
|
+
// DedicatedWorkerGlobalScope.name). Node only exposes a worker-side reader for
|
|
354
|
+
// the {name} option as `worker_threads.threadName` from v24.6.0 / v22.20.0 — it
|
|
355
|
+
// is ABSENT across nub's whole compat tier (18.19–22.19), so it cannot be the
|
|
356
|
+
// floor mechanism. We THREAD the name in ourselves via the internal
|
|
357
|
+
// NUB_WORKER_NAME env (internal plumbing var — exempt from the brand boundary),
|
|
358
|
+
// set by the main-side constructor.
|
|
359
|
+
//
|
|
360
|
+
// RESOLUTION ORDER — env FIRST (not native threadName): NUB_WORKER_NAME carries
|
|
361
|
+
// the user's EXACT intent including the empty string, whereas native
|
|
362
|
+
// `threadName` defaults to the literal sentinel "WorkerThread" for an UNNAMED
|
|
363
|
+
// worker (it's the thread DISPLAY name, not the WHATWG worker name) — surfacing
|
|
364
|
+
// that as self.name would be a spec divergence (an unnamed worker's name must be
|
|
365
|
+
// ""). So: the injected env wins when present; native threadName is the fallback
|
|
366
|
+
// ONLY on the SHARE_ENV path (where the ctor couldn't inject env), with the
|
|
367
|
+
// "WorkerThread" sentinel filtered to "".
|
|
368
|
+
{
|
|
369
|
+
const wt = __getBuiltin("node:worker_threads");
|
|
370
|
+
let name;
|
|
371
|
+
if (typeof process.env.NUB_WORKER_NAME === "string") {
|
|
372
|
+
name = process.env.NUB_WORKER_NAME;
|
|
373
|
+
} else {
|
|
374
|
+
const tn = wt && typeof wt.threadName === "string" ? wt.threadName : "";
|
|
375
|
+
name = tn === "WorkerThread" ? "" : tn;
|
|
376
|
+
}
|
|
377
|
+
Object.defineProperty(scope, "name", {
|
|
378
|
+
value: name,
|
|
379
|
+
enumerable: false,
|
|
380
|
+
writable: true,
|
|
381
|
+
configurable: true,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// `importScripts(...urls)` — WHATWG WorkerGlobalScope, CLASSIC workers only.
|
|
386
|
+
// Synchronously fetches + evaluates each script in the global scope, in order.
|
|
387
|
+
// A module worker MUST throw on importScripts (use `import` instead). nub learns
|
|
388
|
+
// the worker's type from NUB_WORKER_TYPE (set by the main-side constructor).
|
|
389
|
+
// Remote URLs are not supported (no sync network in Node); local file:/relative
|
|
390
|
+
// paths and data: URLs are read synchronously.
|
|
391
|
+
// Default to the classic (working) importScripts when the type is unset — this
|
|
392
|
+
// is the SHARE_ENV edge where the constructor couldn't inject NUB_WORKER_TYPE.
|
|
393
|
+
if (process.env.NUB_WORKER_TYPE !== "module") {
|
|
394
|
+
const fs = __getBuiltin("node:fs");
|
|
395
|
+
const { fileURLToPath: f2p, pathToFileURL } = __getBuiltin("node:url");
|
|
396
|
+
defineGlobal("importScripts", (...urls) => {
|
|
397
|
+
for (const u of urls) {
|
|
398
|
+
const s = String(u);
|
|
399
|
+
let code;
|
|
400
|
+
if (s.startsWith("data:")) {
|
|
401
|
+
const comma = s.indexOf(",");
|
|
402
|
+
const meta = s.slice(5, comma);
|
|
403
|
+
const body = s.slice(comma + 1);
|
|
404
|
+
code = meta.includes("base64")
|
|
405
|
+
? Buffer.from(body, "base64").toString("utf8")
|
|
406
|
+
: decodeURIComponent(body);
|
|
407
|
+
} else if (/^https?:/.test(s)) {
|
|
408
|
+
throw new TypeError(
|
|
409
|
+
"importScripts: remote URLs are not supported (no synchronous network)"
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
const path = s.startsWith("file://") ? f2p(s) : s;
|
|
413
|
+
code = fs.readFileSync(path, "utf8");
|
|
414
|
+
}
|
|
415
|
+
// Indirect eval → runs in global scope, matching importScripts semantics.
|
|
416
|
+
(0, eval)(code);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
// Module workers: importScripts must throw (spec). Provide the throwing form
|
|
421
|
+
// so the surface exists and the error is the spec-correct one.
|
|
422
|
+
defineGlobal("importScripts", () => {
|
|
423
|
+
throw new TypeError("importScripts is not available in module workers");
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
224
427
|
// `message`/`messageerror` are DELEGATED straight onto the native `parentPort`
|
|
225
428
|
// (a real Node MessagePort) so Node's own C++ event-loop ref-counting governs
|
|
226
429
|
// worker lifetime: a worker that never listens leaves parentPort with no
|