@markii/host 0.17.0-next.2 → 0.17.0-next.3
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/README.md +4 -2
- package/dist/browser-worker.js +36 -36
- package/dist/browser.d.ts +6 -2
- package/dist/browser.js +7 -2
- package/dist/host/adapter.d.ts +18 -23
- package/dist/host/conformance-runner.d.ts +4 -1
- package/dist/host/conformance-runner.js +4 -6
- package/dist/host/create-host.d.ts +4 -6
- package/dist/host/editor-behavior.d.ts +2 -6
- package/dist/host/editor-behavior.js +5 -9
- package/dist/host/export-behavior.d.ts +4 -6
- package/dist/host/labels.d.ts +1 -2
- package/dist/host/pack-archive.js +2 -8
- package/dist/host/pack-install.d.ts +1 -2
- package/dist/host/pack-install.js +4 -7
- package/dist/host/pack-load.d.ts +1 -2
- package/dist/host/pack-trust-behavior.js +1 -1
- package/dist/host/refresh-interval.d.ts +1 -3
- package/dist/host/refresh-interval.js +1 -3
- package/dist/host/run-behavior.d.ts +3 -4
- package/dist/host/run-behavior.js +23 -2
- package/dist/host/script-execution.d.ts +1 -1
- package/dist/index.js +1 -2
- package/dist/node-worker.js +20657 -0
- package/dist/node.d.ts +2 -1
- package/dist/node.js +1 -1
- package/dist/packs/discover.js +1 -1
- package/dist/packs/pack-diagnostics.d.ts +6 -8
- package/dist/packs/pack-diagnostics.js +6 -8
- package/dist/packs/pack-paths.js +1 -2
- package/dist/packs/pack-render-registry.d.ts +9 -10
- package/dist/run/browser-isolate.d.ts +5 -5
- package/dist/run/browser-isolate.js +5 -1
- package/dist/run/isolate.d.ts +3 -1
- package/dist/run/net-bridge.d.ts +5 -5
- package/dist/run/net-provider.d.ts +3 -1
- package/dist/run/net-provider.js +20 -6
- package/dist/run/node-isolate.d.ts +13 -21
- package/dist/run/node-isolate.js +31 -80
- package/dist/run/run-flow.d.ts +1 -2
- package/dist/run/run-host.d.ts +3 -3
- package/dist/run/worker-entry-browser.d.ts +24 -4
- package/dist/run/worker-entry-browser.js +99 -4
- package/dist/run/worker-entry.js +2 -0
- package/package.json +14 -11
package/dist/node.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { SpawnRunOptions } from './run/run-host.js';
|
|
2
|
-
export {
|
|
2
|
+
export type { NodeHostIsolateOptions } from './run/node-isolate.js';
|
|
3
|
+
export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
|
|
3
4
|
export { createNetProvider } from './run/net-provider.js';
|
|
4
5
|
export { createNodeArchiveExtractFs, createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
|
|
5
6
|
export { createHtmlExportEngine } from './html-engine.js';
|
package/dist/node.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// - the `@markii/html`-backed static export engine;
|
|
8
8
|
//
|
|
9
9
|
// A Node host imports from both entries. No browser bundle imports this one.
|
|
10
|
-
export {
|
|
10
|
+
export { createNodeHostIsolate, workerThreadIsolate, } from './run/node-isolate.js';
|
|
11
11
|
export { createNetProvider } from './run/net-provider.js';
|
|
12
12
|
export { createNodeArchiveExtractFs, createNodeDirectoryLister, createNodeFileReader, createNodePackDiscoveryIo, createNodeReader as createNodePackScriptsReader, } from './node-fs.js';
|
|
13
13
|
export { createHtmlExportEngine } from './html-engine.js';
|
package/dist/packs/discover.js
CHANGED
|
@@ -131,7 +131,7 @@ async function tryLoadPackAt(folder, readFile) {
|
|
|
131
131
|
*/
|
|
132
132
|
/**
|
|
133
133
|
* When a `ManifestAttempt` succeeded (`kind: 'found'`) but the manifest's
|
|
134
|
-
* `engine` does not match `supportedEngine
|
|
134
|
+
* `engine` does not match `supportedEngine`, downgrades it to
|
|
135
135
|
* a `SkippedPackFolder` instead of a `DiscoveredPack` — same effect
|
|
136
136
|
* `@markii/react`'s `loadPack` already has for an unsupported engine
|
|
137
137
|
* (nothing registered for that namespace, unknown-component fallback for
|
|
@@ -34,7 +34,7 @@ export interface PackDiagnosticsContext {
|
|
|
34
34
|
/**
|
|
35
35
|
* One entry per validated, non-colliding pack whose declared engine
|
|
36
36
|
* `@markii/react`'s `loadPack` cannot run (`./pack-render-registry.ts`'s
|
|
37
|
-
* `BuildRenderRegistryResult.droppedEngines
|
|
37
|
+
* `BuildRenderRegistryResult.droppedEngines`). Omitted (or
|
|
38
38
|
* empty) contributes nothing — the ordinary case where every registered
|
|
39
39
|
* pack's engine is supported.
|
|
40
40
|
*/
|
|
@@ -62,13 +62,11 @@ export declare function skippedPackCount(context: {
|
|
|
62
62
|
}): number;
|
|
63
63
|
/**
|
|
64
64
|
* One informational line for a pack whose prebuilt `webview.js` shadows
|
|
65
|
-
* component sources still present in the same folder
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* Export Pack command versus, from Obsidian, VS Code's Export Pack command
|
|
71
|
-
* by name, since Obsidian has no build command of its own).
|
|
65
|
+
* component sources still present in the same folder.
|
|
66
|
+
* The tail clause names how a user would rebuild the prebuilt artifact
|
|
67
|
+
* from those sources: VS Code's own Export Pack command, or, from
|
|
68
|
+
* Obsidian, VS Code's Export Pack command by name, since Obsidian has no
|
|
69
|
+
* build command of its own.
|
|
72
70
|
*
|
|
73
71
|
* Never a failure: shipping both the built artifact and its sources is a
|
|
74
72
|
* supported distribution shape. `rebuildInstruction` is the one thing that
|
|
@@ -53,7 +53,7 @@ function duplicateComposedNameLines(context) {
|
|
|
53
53
|
const duplicates = context.duplicateComposedNames ?? [];
|
|
54
54
|
return duplicates.map((duplicate) => `Pack "${duplicate.skippedPack}"'s component composed to the directive name "${duplicate.composedName}", already claimed by pack "${duplicate.keptPack}"; the later component was skipped.`);
|
|
55
55
|
}
|
|
56
|
-
/** One line per pack dropped because this renderer cannot run its declared engine
|
|
56
|
+
/** One line per pack dropped because this renderer cannot run its declared engine — `[]` when there was none. */
|
|
57
57
|
function droppedEngineLines(context) {
|
|
58
58
|
const dropped = context.droppedEngines ?? [];
|
|
59
59
|
return dropped.map((entry) => `Pack "${entry.name}" declares engine "${entry.engine}", which this renderer cannot run; it installed with zero components (every directive under its namespace falls back to the unknown-component box).`);
|
|
@@ -87,13 +87,11 @@ export function skippedPackCount(context) {
|
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* One informational line for a pack whose prebuilt `webview.js` shadows
|
|
90
|
-
* component sources still present in the same folder
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* Export Pack command versus, from Obsidian, VS Code's Export Pack command
|
|
96
|
-
* by name, since Obsidian has no build command of its own).
|
|
90
|
+
* component sources still present in the same folder.
|
|
91
|
+
* The tail clause names how a user would rebuild the prebuilt artifact
|
|
92
|
+
* from those sources: VS Code's own Export Pack command, or, from
|
|
93
|
+
* Obsidian, VS Code's Export Pack command by name, since Obsidian has no
|
|
94
|
+
* build command of its own.
|
|
97
95
|
*
|
|
98
96
|
* Never a failure: shipping both the built artifact and its sources is a
|
|
99
97
|
* supported distribution shape. `rebuildInstruction` is the one thing that
|
package/dist/packs/pack-paths.js
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* host concern — reading the setting itself, and naming what "relative"
|
|
13
13
|
* means in a diagnostic message, stays in each host's own app code.
|
|
14
14
|
*
|
|
15
|
-
* DECISION
|
|
16
|
-
* Code module this was hoisted from): a relative entry is resolved against
|
|
15
|
+
* DECISION: a relative entry is resolved against
|
|
17
16
|
* `baseDir` as given by the caller; an absolute entry is used as-is. This
|
|
18
17
|
* matches how a user reads the setting's own description: "this pack
|
|
19
18
|
* folder, relative to my project/vault".
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import type { PackManifest } from '@markii/pack';
|
|
2
|
-
import type { Registry, RegistryEntry } from '@markii/react';
|
|
3
2
|
/**
|
|
4
3
|
* The renderer-specific half of building a pack-aware registry: the
|
|
5
4
|
* renderer's own `loadPack`, `createRegistry` and `mergeRegistries`. A
|
|
6
5
|
* React host passes `@markii/react`'s `reactPackEngine`; the host layer
|
|
7
6
|
* itself names no renderer.
|
|
8
7
|
*/
|
|
9
|
-
export interface PackRegistryEngine {
|
|
10
|
-
loadPack(manifest: PackManifest, componentModules: Record<string,
|
|
11
|
-
registry:
|
|
8
|
+
export interface PackRegistryEngine<TRegistry extends object = object, TEntry = unknown> {
|
|
9
|
+
loadPack(manifest: PackManifest, componentModules: Record<string, TEntry>): {
|
|
10
|
+
registry: TRegistry;
|
|
12
11
|
dropped?: {
|
|
13
12
|
name: string;
|
|
14
13
|
engine: string;
|
|
15
14
|
};
|
|
16
15
|
};
|
|
17
|
-
createRegistry(entries?: Record<string,
|
|
18
|
-
mergeRegistries(...registries:
|
|
16
|
+
createRegistry(entries?: Record<string, TEntry>): TRegistry;
|
|
17
|
+
mergeRegistries(...registries: TRegistry[]): TRegistry;
|
|
19
18
|
}
|
|
20
19
|
/** One registration a compiled pack script queued by calling `window.__markiiRegisterPack(manifestJson, componentModules)` (see `./pack-build.ts`'s top doc comment for the full registration convention). Structurally identical across every host that satisfies the convention. */
|
|
21
20
|
export interface QueuedPackRegistration {
|
|
@@ -42,8 +41,8 @@ export interface DuplicateComposedName {
|
|
|
42
41
|
readonly skippedPack: string;
|
|
43
42
|
}
|
|
44
43
|
/** What `buildRenderRegistry` reports alongside the merged registry — developer-facing diagnostics for a caller to fold into its own surface, never shown as page content. */
|
|
45
|
-
export interface BuildRenderRegistryResult {
|
|
46
|
-
readonly registry:
|
|
44
|
+
export interface BuildRenderRegistryResult<TRegistry extends object = object> {
|
|
45
|
+
readonly registry: TRegistry;
|
|
47
46
|
/** One line per malformed registration, dropped rather than installed. */
|
|
48
47
|
readonly invalidReasons: readonly string[];
|
|
49
48
|
/** Non-empty only when two or more validated registrations shared a namespace — the WHOLE install was then rejected (docs/packs.md's install-time rejection rule) and `registry` falls back to `defaultRegistry` alone. */
|
|
@@ -52,7 +51,7 @@ export interface BuildRenderRegistryResult {
|
|
|
52
51
|
readonly duplicateComposedNames: readonly DuplicateComposedName[];
|
|
53
52
|
/**
|
|
54
53
|
* One entry per validated, non-colliding pack whose manifest declares an
|
|
55
|
-
* engine `@markii/react`'s `loadPack` cannot run
|
|
54
|
+
* engine `@markii/react`'s `loadPack` cannot run. Before
|
|
56
55
|
* this field existed, such a pack installed as an empty contribution
|
|
57
56
|
* with no record anywhere that it had even been attempted — every
|
|
58
57
|
* directive under its namespace fell through to the ordinary
|
|
@@ -80,4 +79,4 @@ export interface BuildRenderRegistryResult {
|
|
|
80
79
|
* (see `DuplicateComposedName`) drops only the later claimant's
|
|
81
80
|
* component, not the whole pack.
|
|
82
81
|
*/
|
|
83
|
-
export declare function buildRenderRegistry(queued: readonly QueuedPackRegistration[], defaultRegistry:
|
|
82
|
+
export declare function buildRenderRegistry<TRegistry extends object, TEntry>(queued: readonly QueuedPackRegistration[], defaultRegistry: TRegistry, engine: PackRegistryEngine<TRegistry, TEntry>): BuildRenderRegistryResult<TRegistry>;
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* there is no `__dirname` inside a worker to find it beside the bundle.
|
|
14
14
|
*
|
|
15
15
|
* Network is served by the HOST, not the worker: `serveNetRequests` wires
|
|
16
|
-
* every `markii:net-request` to the caller's own `NetProvider
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* every `markii:net-request` to the caller's own `NetProvider`. Lua cannot
|
|
17
|
+
* reach JavaScript, and the worker entry removes its own network globals
|
|
18
|
+
* before the first script runs (`./worker-entry-browser.ts`), so the
|
|
19
|
+
* provider's allowlist is the only path to the network.
|
|
20
20
|
*
|
|
21
21
|
* What this CANNOT do, stated plainly because it is a real difference: a
|
|
22
22
|
* Web Worker takes no `maxOldGenerationSizeMb`, so `options`'s heap cap is
|
|
@@ -37,7 +37,7 @@ export interface BrowserIsolateOptions {
|
|
|
37
37
|
/** Creates the worker. Injected so a test never needs a real one, and so a host can decide how it mints the URL. */
|
|
38
38
|
createWorker: (entryPath: string) => WorkerLike;
|
|
39
39
|
/** The host's own pinned provider. Every `net.*` call the script makes is answered by this, in the host, never in the isolate. */
|
|
40
|
-
netProvider: (netAllowlist: string[], maxFetchBytes: number, netPolicy: unknown) => NetProvider;
|
|
40
|
+
netProvider: (netAllowlist: string[], maxFetchBytes: number, netPolicy: unknown, signal?: AbortSignal) => NetProvider;
|
|
41
41
|
/** Where the worker should fetch wasmoon's `glue.wasm`; sent along with the job. */
|
|
42
42
|
wasmUri?: string;
|
|
43
43
|
}
|
|
@@ -19,6 +19,9 @@ export function createBrowserIsolate(options) {
|
|
|
19
19
|
const worker = options.createWorker(spawn.entryPath);
|
|
20
20
|
let provider;
|
|
21
21
|
let alive = true;
|
|
22
|
+
// Aborted by `kill()`: a request still in flight when the run ends is
|
|
23
|
+
// cancelled rather than left running in the host.
|
|
24
|
+
const requests = new AbortController();
|
|
22
25
|
const messageListeners = [];
|
|
23
26
|
const errorListeners = [];
|
|
24
27
|
worker.addEventListener('message', (event) => {
|
|
@@ -52,7 +55,7 @@ export function createBrowserIsolate(options) {
|
|
|
52
55
|
return {
|
|
53
56
|
send: (job) => {
|
|
54
57
|
const { allowlist, maxFetchBytes, policy } = netArgsFrom(job);
|
|
55
|
-
provider = options.netProvider(allowlist, maxFetchBytes, policy);
|
|
58
|
+
provider = options.netProvider(allowlist, maxFetchBytes, policy, requests.signal);
|
|
56
59
|
worker.postMessage({
|
|
57
60
|
...job,
|
|
58
61
|
...(options.wasmUri !== undefined
|
|
@@ -64,6 +67,7 @@ export function createBrowserIsolate(options) {
|
|
|
64
67
|
if (!alive)
|
|
65
68
|
return;
|
|
66
69
|
alive = false;
|
|
70
|
+
requests.abort();
|
|
67
71
|
worker.terminate();
|
|
68
72
|
},
|
|
69
73
|
onMessage: (listener) => {
|
package/dist/run/isolate.d.ts
CHANGED
|
@@ -46,10 +46,12 @@ export interface RunIsolate {
|
|
|
46
46
|
onExit(listener: (code: number | null) => void): void;
|
|
47
47
|
}
|
|
48
48
|
export interface SpawnIsolateOptions {
|
|
49
|
-
/** The entry the isolate loads — resolved by the host, never guessed here (
|
|
49
|
+
/** The entry the isolate loads — resolved by the host, never guessed here (`createNodeHostIsolate` supplies the packaged one). */
|
|
50
50
|
entryPath: string;
|
|
51
51
|
/** Node `execArgv` for a worker-thread isolate; meaningless to a Web Worker, which ignores it. */
|
|
52
52
|
execArgv?: string[];
|
|
53
|
+
/** Environment for a worker-thread isolate; a Web Worker ignores it. */
|
|
54
|
+
env?: Record<string, string | undefined>;
|
|
53
55
|
/** V8 old-space cap in MB. A worker thread honors it; a Web Worker cannot, and says so in its own doc comment. */
|
|
54
56
|
maxOldGenerationSizeMb: number;
|
|
55
57
|
}
|
package/dist/run/net-bridge.d.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* host to perform it, and the host, which still has Node, runs exactly the
|
|
9
9
|
* same `createNetProvider` the worker-thread isolate always used.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* The allowlist and the pinning policy live somewhere the sandboxed code
|
|
12
|
+
* cannot reach: Lua has no path to JavaScript, and the worker entry removes
|
|
13
|
+
* its network globals after startup (`./worker-entry-browser.ts`). What the
|
|
14
|
+
* worker can do is ask, and every ask is answered by the host's own
|
|
15
|
+
* provider, against the host's own allowlist.
|
|
16
16
|
*
|
|
17
17
|
* This file is the protocol and the HOST half. The worker half lives in
|
|
18
18
|
* `./net-bridge-worker.ts` because it needs `@markii/lua`'s denial brand,
|
|
@@ -59,7 +59,9 @@ export declare function createNetProvider(allowlist: readonly string[], maxFetch
|
|
|
59
59
|
* host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
|
|
60
60
|
* flag) and the worker rebuilds a properly branded error on arrival.
|
|
61
61
|
*/
|
|
62
|
-
denial: (message: string) => Error, lookup?: HostLookup
|
|
62
|
+
denial: (message: string) => Error, lookup?: HostLookup,
|
|
63
|
+
/** Aborted when the run ends: an in-flight request is destroyed and no further hop is made. */
|
|
64
|
+
signal?: AbortSignal): NetProvider;
|
|
63
65
|
/**
|
|
64
66
|
* An in-memory `CacheProvider` (`@markii/lua`) seeded from `snapshot` at
|
|
65
67
|
* construction and readable back out afterward via `.snapshot()` — the
|
package/dist/run/net-provider.js
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* import from `@markii/lua` — the brand that marks a policy refusal — is
|
|
15
15
|
* injected as `denial` instead. Types are `import type` and erase.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* here
|
|
17
|
+
* A Web Worker isolate asks this provider through the net bridge. The
|
|
18
|
+
* worker entry removes its own network globals after startup, so the
|
|
19
|
+
* allowlist enforced here is the only path to the network.
|
|
20
20
|
*/
|
|
21
21
|
import * as http from 'node:http';
|
|
22
22
|
import * as https from 'node:https';
|
|
@@ -80,7 +80,7 @@ function bareHostname(hostname) {
|
|
|
80
80
|
*/
|
|
81
81
|
function issueRequest(url, method, body, pinned, maxFetchBytes,
|
|
82
82
|
/** See `createNetProvider`'s own `denial` parameter — injected for the same reason. */
|
|
83
|
-
denial) {
|
|
83
|
+
denial, signal) {
|
|
84
84
|
return new Promise((resolve, reject) => {
|
|
85
85
|
const mod = url.protocol === 'https:' ? https : http;
|
|
86
86
|
const bodyBuffer = body !== undefined ? Buffer.from(body, 'utf8') : undefined;
|
|
@@ -163,6 +163,16 @@ denial) {
|
|
|
163
163
|
req.on('error', (err) => {
|
|
164
164
|
settleReject(denial(`request failed: ${describeThrown(err)}`));
|
|
165
165
|
});
|
|
166
|
+
if (signal !== undefined) {
|
|
167
|
+
const onAbort = () => {
|
|
168
|
+
req.destroy();
|
|
169
|
+
settleReject(denial('the run has ended'));
|
|
170
|
+
};
|
|
171
|
+
if (signal.aborted)
|
|
172
|
+
onAbort();
|
|
173
|
+
else
|
|
174
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
175
|
+
}
|
|
166
176
|
if (bodyBuffer)
|
|
167
177
|
req.write(bodyBuffer);
|
|
168
178
|
req.end();
|
|
@@ -227,7 +237,9 @@ export function createNetProvider(allowlist, maxFetchBytes, policy,
|
|
|
227
237
|
* host marks every refusal on the wire (`./net-bridge.ts`'s `denied`
|
|
228
238
|
* flag) and the worker rebuilds a properly branded error on arrival.
|
|
229
239
|
*/
|
|
230
|
-
denial, lookup = defaultHostLookup
|
|
240
|
+
denial, lookup = defaultHostLookup,
|
|
241
|
+
/** Aborted when the run ends: an in-flight request is destroyed and no further hop is made. */
|
|
242
|
+
signal) {
|
|
231
243
|
const allowed = new Set(allowlist.map((host) => host.toLowerCase()));
|
|
232
244
|
async function fetchAllowed(startUrl, method, body) {
|
|
233
245
|
let currentUrl = startUrl;
|
|
@@ -246,6 +258,8 @@ denial, lookup = defaultHostLookup) {
|
|
|
246
258
|
if (parsed.username !== '' || parsed.password !== '') {
|
|
247
259
|
throw denial(`URL "${currentUrl}" embeds credentials, which is not allowed`);
|
|
248
260
|
}
|
|
261
|
+
if (signal?.aborted)
|
|
262
|
+
throw denial('the run has ended');
|
|
249
263
|
const host = parsed.hostname.toLowerCase();
|
|
250
264
|
if (!allowed.has(host)) {
|
|
251
265
|
throw denial(`host "${host}" is not on the run's allowlist`);
|
|
@@ -256,7 +270,7 @@ denial, lookup = defaultHostLookup) {
|
|
|
256
270
|
if (!pinResult.ok) {
|
|
257
271
|
throw denial(pinResult.reason);
|
|
258
272
|
}
|
|
259
|
-
const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, denial);
|
|
273
|
+
const response = await issueRequest(parsed, method, body, pinResult.pinned, maxFetchBytes, denial, signal);
|
|
260
274
|
if (response.status >= 300 && response.status < 400) {
|
|
261
275
|
if (!response.location) {
|
|
262
276
|
throw denial('redirect response carried no Location header');
|
|
@@ -1,30 +1,22 @@
|
|
|
1
|
+
import type { HostIsolate } from '../host/adapter.js';
|
|
1
2
|
import type { IsolateSpawner } from './isolate.js';
|
|
2
|
-
import type { SpawnRunOptions } from './run-host.js';
|
|
3
|
-
import type { RunResult } from './worker-entry.js';
|
|
4
|
-
/**
|
|
5
|
-
* The ONLY fallback this shared package supplies for an unset `workerPath`:
|
|
6
|
-
* the sibling `worker-entry.ts`, run straight from source via `tsx`
|
|
7
|
-
* (`TSX_DEV_EXEC_ARGV`, above). That is correct for this package's own
|
|
8
|
-
* dev/Vitest runs — `__dirname` at that point is this real source
|
|
9
|
-
* directory — but it says nothing about where a packaged host's bundled
|
|
10
|
-
* worker lives, because this package does not know that host's `dist/`
|
|
11
|
-
* layout. A packaged host (e.g. `apps/vscode`) MUST resolve its own bundled
|
|
12
|
-
* worker path itself and pass it as `SpawnRunOptions.workerPath`; see
|
|
13
|
-
* `apps/vscode/src/worker-path.ts` for that host's resolution and
|
|
14
|
-
* `apps/vscode/esbuild.config.mjs`'s worker build for where it bundles to.
|
|
15
|
-
*/
|
|
16
|
-
export declare function defaultWorkerPath(): string;
|
|
17
3
|
/**
|
|
18
4
|
* The default isolate: a `node:worker_threads` worker, which is what every
|
|
19
|
-
* Node host
|
|
20
|
-
* is the only kind that accepts `resourceLimits`, so it is also the only
|
|
5
|
+
* Node host uses. It is the only kind that accepts `resourceLimits`, so it is also the only
|
|
21
6
|
* kind whose V8 heap is bounded — see `./isolate.ts` for why a host might
|
|
22
7
|
* still have to supply the other one.
|
|
23
8
|
*/
|
|
24
9
|
export declare const workerThreadIsolate: IsolateSpawner;
|
|
10
|
+
export interface NodeHostIsolateOptions {
|
|
11
|
+
/** The worker entry on disk. Defaults to `node-worker.js` in the package's `dist/`. */
|
|
12
|
+
workerPath?: string;
|
|
13
|
+
}
|
|
25
14
|
/**
|
|
26
|
-
* `
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
15
|
+
* A `HostIsolate` of kind `node`: each run starts a `worker_threads` worker
|
|
16
|
+
* from the packaged, self-contained `node-worker.js`. A host that bundles
|
|
17
|
+
* the worker itself passes its own `workerPath`. The default is resolved
|
|
18
|
+
* when a run asks for it, so constructing the isolate never throws.
|
|
29
19
|
*/
|
|
30
|
-
export declare function
|
|
20
|
+
export declare function createNodeHostIsolate(options?: NodeHostIsolateOptions): Extract<HostIsolate, {
|
|
21
|
+
kind: 'node';
|
|
22
|
+
}>;
|
package/dist/run/node-isolate.js
CHANGED
|
@@ -1,81 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Node half of the Run path's isolate seam: a `node:worker_threads`
|
|
3
|
-
* worker
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* worker. Split out of `./run-host.ts` so the watchdog, the exactly-once
|
|
4
|
+
* settlement and the never-rejects contract stay in a module with no
|
|
5
|
+
* `node:*` import and run unchanged for every host kind. Reached only
|
|
6
|
+
* through `@markii/host/node`.
|
|
7
7
|
*/
|
|
8
8
|
import { Worker } from 'node:worker_threads';
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
|
-
import
|
|
11
|
-
import { spawnRun } from './run-host.js';
|
|
12
|
-
/** `execArgv` needed to run a `.ts` worker entry directly (dev/Vitest only) — see `./worker-entry.ts`'s doc comment on why a plain `node` `.ts` import can't resolve this repo's `@markii/*` bare specifiers or its `./foo.js` -> `./foo.ts` import convention on its own. `--require tsx/cjs` (rather than `tsx/esm`) is deliberate: empirically, the ESM loader hook deadlocks Node's CJS/ESM interop translator for a worker thread requiring `.js`-suffixed relative imports that resolve to `.ts` files (this repo's convention throughout `@markii/*`), while the CJS `require` hook has no such issue. */
|
|
13
|
-
const TSX_DEV_EXEC_ARGV = ['--require', 'tsx/cjs'];
|
|
14
|
-
/**
|
|
15
|
-
* The ONLY fallback this shared package supplies for an unset `workerPath`:
|
|
16
|
-
* the sibling `worker-entry.ts`, run straight from source via `tsx`
|
|
17
|
-
* (`TSX_DEV_EXEC_ARGV`, above). That is correct for this package's own
|
|
18
|
-
* dev/Vitest runs — `__dirname` at that point is this real source
|
|
19
|
-
* directory — but it says nothing about where a packaged host's bundled
|
|
20
|
-
* worker lives, because this package does not know that host's `dist/`
|
|
21
|
-
* layout. A packaged host (e.g. `apps/vscode`) MUST resolve its own bundled
|
|
22
|
-
* worker path itself and pass it as `SpawnRunOptions.workerPath`; see
|
|
23
|
-
* `apps/vscode/src/worker-path.ts` for that host's resolution and
|
|
24
|
-
* `apps/vscode/esbuild.config.mjs`'s worker build for where it bundles to.
|
|
25
|
-
*/
|
|
26
|
-
export function defaultWorkerPath() {
|
|
27
|
-
// The built package is ESM, where `__dirname` does not exist: a packaged
|
|
28
|
-
// host always passes its own bundled worker, so say so instead of
|
|
29
|
-
// throwing a ReferenceError.
|
|
30
|
-
const devSource = typeof __dirname === 'undefined'
|
|
31
|
-
? undefined
|
|
32
|
-
: path.join(__dirname, 'worker-entry.ts');
|
|
33
|
-
if (devSource !== undefined && existsSync(devSource))
|
|
34
|
-
return devSource;
|
|
35
|
-
throw new Error('spawnRun: could not resolve the worker entry file (looked for ' +
|
|
36
|
-
`${devSource ?? 'worker-entry.ts beside the package source'}) — pass workerPath explicitly`);
|
|
37
|
-
}
|
|
38
|
-
function execArgvFor(workerPath) {
|
|
39
|
-
return workerPath.endsWith('.ts') ? TSX_DEV_EXEC_ARGV : undefined;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Dev/Vitest only: point `tsx` at the repo's `tsconfig.dev.json` so a
|
|
43
|
-
* `.ts` worker entry resolves `@markii/*` to each package's SOURCE.
|
|
44
|
-
*
|
|
45
|
-
* Without this the worker is a plain Node resolution: a bare `@markii/lua`
|
|
46
|
-
* goes through `node_modules` to that package's `exports`, which name the
|
|
47
|
-
* built `dist/`. `npm run build` only type-checks (`tsc --noEmit`); the
|
|
48
|
-
* emitting script is `build:dist`, which runs at release. So `dist/` is
|
|
49
|
-
* whatever a past release left behind, and a real-isolate probe would
|
|
50
|
-
* silently exercise that stale code instead of the source it is written to
|
|
51
|
-
* test. A capability added since the last `build:dist` simply would not
|
|
52
|
-
* exist inside the worker.
|
|
53
|
-
*
|
|
54
|
-
* Gated on a `.ts` entry, the same signal `execArgvFor` already uses for
|
|
55
|
-
* the tsx hook, so a packaged host passing its own bundled `.js` worker is
|
|
56
|
-
* never touched by this.
|
|
57
|
-
*/
|
|
58
|
-
function devTsconfigEnv(entryPath) {
|
|
59
|
-
if (!entryPath.endsWith('.ts') || typeof __dirname === 'undefined') {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
const tsconfig = path.resolve(__dirname, '../../../..', 'tsconfig.dev.json');
|
|
63
|
-
if (!existsSync(tsconfig))
|
|
64
|
-
return undefined;
|
|
65
|
-
return { env: { ...process.env, TSX_TSCONFIG_PATH: tsconfig } };
|
|
66
|
-
}
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
67
11
|
/**
|
|
68
12
|
* The default isolate: a `node:worker_threads` worker, which is what every
|
|
69
|
-
* Node host
|
|
70
|
-
* is the only kind that accepts `resourceLimits`, so it is also the only
|
|
13
|
+
* Node host uses. It is the only kind that accepts `resourceLimits`, so it is also the only
|
|
71
14
|
* kind whose V8 heap is bounded — see `./isolate.ts` for why a host might
|
|
72
15
|
* still have to supply the other one.
|
|
73
16
|
*/
|
|
74
17
|
export const workerThreadIsolate = (options) => {
|
|
75
|
-
const execArgv = options.execArgv ?? execArgvFor(options.entryPath);
|
|
76
18
|
const worker = new Worker(options.entryPath, {
|
|
77
|
-
...(execArgv ? { execArgv } : {}),
|
|
78
|
-
...(
|
|
19
|
+
...(options.execArgv ? { execArgv: options.execArgv } : {}),
|
|
20
|
+
...(options.env ? { env: options.env } : {}),
|
|
79
21
|
resourceLimits: {
|
|
80
22
|
maxOldGenerationSizeMb: options.maxOldGenerationSizeMb,
|
|
81
23
|
},
|
|
@@ -88,11 +30,11 @@ export const workerThreadIsolate = (options) => {
|
|
|
88
30
|
void worker.terminate();
|
|
89
31
|
},
|
|
90
32
|
onMessage: (listener) => {
|
|
91
|
-
// `on`, not `once`: a run
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
33
|
+
// `on`, not `once`: a run sends one progress message per script
|
|
34
|
+
// ahead of its single result message, and `spawnRun` tells the two
|
|
35
|
+
// apart. Messages arriving after the run settles are harmless
|
|
36
|
+
// (`settled` guards every path) and the worker is terminated on
|
|
37
|
+
// settle anyway.
|
|
96
38
|
worker.on('message', listener);
|
|
97
39
|
},
|
|
98
40
|
onError: (listener) => {
|
|
@@ -104,14 +46,23 @@ export const workerThreadIsolate = (options) => {
|
|
|
104
46
|
};
|
|
105
47
|
};
|
|
106
48
|
/**
|
|
107
|
-
* `
|
|
108
|
-
* the
|
|
109
|
-
*
|
|
49
|
+
* A `HostIsolate` of kind `node`: each run starts a `worker_threads` worker
|
|
50
|
+
* from the packaged, self-contained `node-worker.js`. A host that bundles
|
|
51
|
+
* the worker itself passes its own `workerPath`. The default is resolved
|
|
52
|
+
* when a run asks for it, so constructing the isolate never throws.
|
|
110
53
|
*/
|
|
111
|
-
export function
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
workerPath:
|
|
115
|
-
|
|
116
|
-
|
|
54
|
+
export function createNodeHostIsolate(options = {}) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'node',
|
|
57
|
+
workerPath: () => {
|
|
58
|
+
if (options.workerPath !== undefined)
|
|
59
|
+
return options.workerPath;
|
|
60
|
+
const packaged = fileURLToPath(new URL('../node-worker.js', import.meta.url));
|
|
61
|
+
if (!existsSync(packaged)) {
|
|
62
|
+
throw new Error(`the packaged worker was not found at ${packaged}: pass workerPath`);
|
|
63
|
+
}
|
|
64
|
+
return packaged;
|
|
65
|
+
},
|
|
66
|
+
spawnIsolate: workerThreadIsolate,
|
|
67
|
+
};
|
|
117
68
|
}
|
package/dist/run/run-flow.d.ts
CHANGED
|
@@ -163,8 +163,7 @@ export interface RunOnceResult {
|
|
|
163
163
|
/**
|
|
164
164
|
* Consent unification (SECURITY-RELEVANT): lines describing any mismatch
|
|
165
165
|
* between what is DECLARED (a bundle manifest's `permissions.net`, and/or
|
|
166
|
-
* an individual script's own `permissions` fence attribute
|
|
167
|
-
* #47) and the hosts this run's scripts actually reach, from
|
|
166
|
+
* an individual script's own `permissions` fence attribute) and the hosts this run's scripts actually reach, from
|
|
168
167
|
* `./bundle-run.ts`'s `netDeclarationDiagnostics`. `[]` when there is
|
|
169
168
|
* nothing to compare against (no bundle manifest and no script declares
|
|
170
169
|
* `permissions`) or every declared host agrees with the scan. For the
|
package/dist/run/run-host.d.ts
CHANGED
|
@@ -85,9 +85,9 @@ export interface SpawnRunOptions {
|
|
|
85
85
|
/**
|
|
86
86
|
* The entry the isolate loads. This package cannot know how a given host
|
|
87
87
|
* bundles or lays out its own `dist/` (see e.g.
|
|
88
|
-
* `apps/vscode/src/worker-path.ts`), so the host always passes it.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* `apps/vscode/src/worker-path.ts`), so the host always passes it.
|
|
89
|
+
* `createNodeHostIsolate` from `@markii/host/node` supplies the packaged
|
|
90
|
+
* worker's path.
|
|
91
91
|
*/
|
|
92
92
|
workerPath: string;
|
|
93
93
|
}
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* the failure classification cannot drift between hosts. This file supplies
|
|
11
11
|
* only the two things a Web Worker has to do differently:
|
|
12
12
|
*
|
|
13
|
-
* 1. Network goes through `./net-bridge.ts` to the host, which
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* 1. Network goes through `./net-bridge.ts` to the host, which runs the
|
|
14
|
+
* same pinned provider as before. A Web Worker does have network
|
|
15
|
+
* globals, so `lockdownWorkerGlobals` (below) removes them before any
|
|
16
|
+
* script runs. Lua cannot reach JavaScript at all; the removal is a
|
|
17
|
+
* second wall for the case where an interpreter escape does.
|
|
17
18
|
* 2. wasmoon's `glue.wasm` arrives as a URL on the job, because there is
|
|
18
19
|
* no `__dirname` here to find it beside the bundle.
|
|
19
20
|
*
|
|
@@ -29,6 +30,25 @@ interface WorkerGlobal {
|
|
|
29
30
|
data: unknown;
|
|
30
31
|
}) => void) => void;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Removes the worker's own ways to reach a network, once, before the first
|
|
35
|
+
* script runs.
|
|
36
|
+
*
|
|
37
|
+
* - Every name in `REMOVED_GLOBALS` (the connection and storage
|
|
38
|
+
* constructors, and `Worker`, which would start an unlocked scope) is
|
|
39
|
+
* shadowed with `undefined`.
|
|
40
|
+
* - `importScripts` becomes a function that throws. It stays a function
|
|
41
|
+
* because the Lua engine's start-up detects a worker by
|
|
42
|
+
* `typeof importScripts`.
|
|
43
|
+
* - `fetch` keeps working for exactly one URL, the `glue.wasm` the job
|
|
44
|
+
* named, because the engine loads its WebAssembly through it for every
|
|
45
|
+
* script. Any other URL is refused.
|
|
46
|
+
*
|
|
47
|
+
* Net traffic is unaffected: it goes to the host with `postMessage`.
|
|
48
|
+
* Idempotent. Covered by `worker-entry-browser.test.ts` and, in a real
|
|
49
|
+
* browser, by the e2e suite.
|
|
50
|
+
*/
|
|
51
|
+
export declare function lockdownWorkerGlobals(scope: object, wasmUri: string | undefined): void;
|
|
32
52
|
/**
|
|
33
53
|
* A job for this entry additionally carries where `glue.wasm` can be
|
|
34
54
|
* fetched from — a blob URL the host minted from the file on disk. Absent,
|