@markii/host 0.0.0-stage → 0.17.0-next.2
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/LICENSE +21 -0
- package/README.md +19 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +167 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +223 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +156 -0
- package/dist/host/conformance-runner.js +634 -0
- package/dist/host/create-host.d.ts +253 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +157 -0
- package/dist/host/editor-behavior.js +227 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +113 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +24 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +68 -0
- package/dist/host/pack-install.d.ts +88 -0
- package/dist/host/pack-install.js +189 -0
- package/dist/host/pack-load.d.ts +72 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +39 -0
- package/dist/host/refresh-interval.js +61 -0
- package/dist/host/run-behavior.d.ts +89 -0
- package/dist/host/run-behavior.js +168 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +70 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +81 -0
- package/dist/packs/pack-diagnostics.js +105 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +106 -0
- package/dist/packs/pack-render-registry.d.ts +83 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +81 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +57 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +70 -0
- package/dist/run/net-provider.js +289 -0
- package/dist/run/node-isolate.d.ts +30 -0
- package/dist/run/node-isolate.js +117 -0
- package/dist/run/run-flow.d.ts +187 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +42 -0
- package/dist/run/worker-entry-browser.js +77 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +96 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +84 -4
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { extractScripts, parse } from '@markii/core';
|
|
2
|
+
/** The three host-reaching capability calls this scan looks for (docs/scripting.md's `net` table). */
|
|
3
|
+
const NET_CALL_NAMES = ['fetch_json', 'post', 'patch'];
|
|
4
|
+
const CALL_SITE_PATTERN = new RegExp(`\\bnet\\.(${NET_CALL_NAMES.join('|')})\\s*\\(`, 'g');
|
|
5
|
+
/**
|
|
6
|
+
* Given `source` and the index of the character immediately after a call's
|
|
7
|
+
* opening `(` (already matched by `CALL_SITE_PATTERN`), returns the raw
|
|
8
|
+
* text of that call's FIRST argument — up to (not including) the first
|
|
9
|
+
* top-level `,` or the call's closing `)`, tracking nested
|
|
10
|
+
* `(`/`)`/`[`/`]`/`{`/`}` depth and quoted strings so a comma or paren
|
|
11
|
+
* INSIDE a nested expression or a quoted string never ends the scan early.
|
|
12
|
+
* Returns `undefined` if the call is unterminated (no matching close found
|
|
13
|
+
* before the source ends) — a malformed/incomplete script, treated the
|
|
14
|
+
* same as any other non-literal argument by the caller.
|
|
15
|
+
*/
|
|
16
|
+
function extractFirstArgument(source, argStart) {
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let quote;
|
|
19
|
+
for (let i = argStart; i < source.length; i++) {
|
|
20
|
+
const ch = source[i];
|
|
21
|
+
if (quote) {
|
|
22
|
+
if (ch === '\\') {
|
|
23
|
+
i++; // skip the escaped character, whatever it is
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (ch === quote)
|
|
27
|
+
quote = undefined;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ch === '"' || ch === "'") {
|
|
31
|
+
quote = ch;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
35
|
+
depth++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
39
|
+
if (depth === 0 && ch === ')') {
|
|
40
|
+
return source.slice(argStart, i);
|
|
41
|
+
}
|
|
42
|
+
depth--;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === ',' && depth === 0) {
|
|
46
|
+
return source.slice(argStart, i);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether `text` is EXACTLY one quoted string literal with nothing else
|
|
53
|
+
* around it (only leading/trailing whitespace) — the one shape this scan
|
|
54
|
+
* trusts as "a literal URL", per this module's top doc comment. Anything
|
|
55
|
+
* else (a `..` concatenation, a bare identifier, a function call, a second
|
|
56
|
+
* literal joined to the first) fails this and is reported as unknown
|
|
57
|
+
* rather than guessed at.
|
|
58
|
+
*/
|
|
59
|
+
const SOLE_STRING_LITERAL = /^\s*(["'])((?:\\.|(?!\1)[^\\])*)\1\s*$/;
|
|
60
|
+
function literalStringValue(argument) {
|
|
61
|
+
const match = SOLE_STRING_LITERAL.exec(argument);
|
|
62
|
+
if (!match)
|
|
63
|
+
return undefined;
|
|
64
|
+
// Unescape only the two escapes Lua string literals actually need for a
|
|
65
|
+
// URL to round-trip (`\\` and the literal's own quote character) — this
|
|
66
|
+
// is a best-effort textual scan, not a full Lua string-literal decoder;
|
|
67
|
+
// any other backslash escape is left as-is (harmless: it would simply
|
|
68
|
+
// fail `new URL(...)` below, falling through to "not a literal we can
|
|
69
|
+
// use" as intended for `hasUnknownHosts`, rather than corrupting a URL
|
|
70
|
+
// that didn't have JavaScript-different escapes to begin with).
|
|
71
|
+
return (match[2] ?? '').replace(/\\(.)/g, '$1');
|
|
72
|
+
}
|
|
73
|
+
/** Bare, lowercased hostname from a URL string, or `undefined` if it doesn't parse as an absolute URL. */
|
|
74
|
+
function hostnameOf(url) {
|
|
75
|
+
try {
|
|
76
|
+
return new URL(url).hostname.toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Scans one script block's Lua source for `net.*` call sites and folds
|
|
84
|
+
* their findings into `hosts`/`markUnknown` — see this module's top doc
|
|
85
|
+
* comment for the literal-only matching rule.
|
|
86
|
+
*/
|
|
87
|
+
function scanForHosts(code, hosts, markUnknown) {
|
|
88
|
+
CALL_SITE_PATTERN.lastIndex = 0;
|
|
89
|
+
let match;
|
|
90
|
+
while ((match = CALL_SITE_PATTERN.exec(code)) !== null) {
|
|
91
|
+
const argStart = match.index + match[0].length;
|
|
92
|
+
const argument = extractFirstArgument(code, argStart);
|
|
93
|
+
if (argument === undefined) {
|
|
94
|
+
markUnknown();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const literal = literalStringValue(argument);
|
|
98
|
+
if (literal === undefined) {
|
|
99
|
+
// Present but not a sole literal -- a concatenation, a variable, a
|
|
100
|
+
// nested call, ... exactly the "cannot resolve statically" case.
|
|
101
|
+
markUnknown();
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const host = hostnameOf(literal);
|
|
105
|
+
if (host === undefined) {
|
|
106
|
+
// Syntactically a literal, but not a URL `new URL()` can parse
|
|
107
|
+
// (e.g. a bare path or template-ish string) -- still unknown to us.
|
|
108
|
+
markUnknown();
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
hosts.add(host);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Bare, already-lowercased hostnames every script block in `scripts`
|
|
116
|
+
* declares via its own `permissions` fence attribute
|
|
117
|
+
* (`@markii/core`'s `ScriptBlock.permissions`), deduplicated, in
|
|
118
|
+
* first-seen (document, then per-script list) order. Mirrors
|
|
119
|
+
* `./bundle-run.ts`'s `manifestNetHosts` for the bundle-manifest
|
|
120
|
+
* equivalent: this is DECLARED INTENT only, exactly like a manifest's
|
|
121
|
+
* `permissions.net` — `scanRunRequirementHosts` above stays the ONLY
|
|
122
|
+
* source of the hostnames a run is actually prompted for. A caller
|
|
123
|
+
* compares this against the scanned set with
|
|
124
|
+
* `./bundle-run.ts`'s `netDeclarationDiagnostics` (the same
|
|
125
|
+
* declared-vs-scanned diff a bundle manifest already gets), and may show
|
|
126
|
+
* it next to the scanned hosts in a grant prompt as "declares: ...".
|
|
127
|
+
*/
|
|
128
|
+
export function scriptDeclaredHosts(scripts) {
|
|
129
|
+
const hosts = new Set();
|
|
130
|
+
for (const block of scripts) {
|
|
131
|
+
for (const host of block.permissions ?? []) {
|
|
132
|
+
hosts.add(host);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...hosts];
|
|
136
|
+
}
|
|
137
|
+
function toGrantClosureScript(block) {
|
|
138
|
+
const script = {
|
|
139
|
+
name: block.name,
|
|
140
|
+
lang: block.lang,
|
|
141
|
+
code: block.code,
|
|
142
|
+
};
|
|
143
|
+
if (block.src !== undefined)
|
|
144
|
+
script.src = block.src;
|
|
145
|
+
return script;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parses `text` and returns its script blocks, in both the raw
|
|
149
|
+
* `@markii/core` `ScriptBlock` shape and the `GrantClosureScript` shape
|
|
150
|
+
* `computeGrantKey` hashes. Split out from `extractRunRequirements` (below)
|
|
151
|
+
* so a caller that needs to resolve `src=` script content BEFORE scanning
|
|
152
|
+
* for hosts — a bundle run, which must read the referenced file out of its
|
|
153
|
+
* snapshot first — can do so without parsing the document twice.
|
|
154
|
+
*/
|
|
155
|
+
export function extractScriptBlocks(text) {
|
|
156
|
+
const tree = parse(text);
|
|
157
|
+
const scripts = extractScripts(tree);
|
|
158
|
+
return { scripts, grantScripts: scripts.map(toGrantClosureScript) };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Scans `scripts` for statically-resolvable `net.*` hosts — an INLINE
|
|
162
|
+
* block's own `code`, or (when `resolveScriptSource` is given and resolves
|
|
163
|
+
* it) a `src=` block's referenced source. A `src=` block with no resolution
|
|
164
|
+
* available folds into `hasUnknownHosts`, same as this module has always
|
|
165
|
+
* done for a bare `.mk.md` document's `src=` references (their source
|
|
166
|
+
* genuinely isn't in `text` at all).
|
|
167
|
+
*/
|
|
168
|
+
export function scanRunRequirementHosts(scripts, resolveScriptSource) {
|
|
169
|
+
const hosts = new Set();
|
|
170
|
+
let hasUnknownHosts = false;
|
|
171
|
+
const markUnknown = () => {
|
|
172
|
+
hasUnknownHosts = true;
|
|
173
|
+
};
|
|
174
|
+
for (const block of scripts) {
|
|
175
|
+
if (block.src !== undefined) {
|
|
176
|
+
const resolved = resolveScriptSource?.(block);
|
|
177
|
+
if (resolved === undefined) {
|
|
178
|
+
// The referenced file's source isn't available to this scan --
|
|
179
|
+
// its net usage, if any, is entirely unknown. See this module's
|
|
180
|
+
// top doc comment.
|
|
181
|
+
markUnknown();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
scanForHosts(resolved, hosts, markUnknown);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
scanForHosts(block.code, hosts, markUnknown);
|
|
188
|
+
}
|
|
189
|
+
return { hosts: [...hosts], hasUnknownHosts };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Computes `RunRequirements` for `text` — parses it with `@markii/core`'s
|
|
193
|
+
* `parse`, pulls its script blocks with `extractScripts`, and scans each
|
|
194
|
+
* INLINE block's own source for statically-resolvable `net.*` hosts. Pure:
|
|
195
|
+
* no I/O, no `vscode`, safe to call on every keystroke if a future slice
|
|
196
|
+
* wants a live preview of what a run would ask for.
|
|
197
|
+
*
|
|
198
|
+
* `resolveScriptSource`, when given, lets a `src=` block's hosts be
|
|
199
|
+
* resolved too (a bundle run passes one backed by its snapshot); omitted,
|
|
200
|
+
* every `src=` block folds into `hasUnknownHosts` exactly as before this
|
|
201
|
+
* option existed.
|
|
202
|
+
*/
|
|
203
|
+
export function extractRunRequirements(text, resolveScriptSource) {
|
|
204
|
+
const { scripts, grantScripts } = extractScriptBlocks(text);
|
|
205
|
+
const { hosts, hasUnknownHosts } = scanRunRequirementHosts(scripts, resolveScriptSource);
|
|
206
|
+
return {
|
|
207
|
+
scripts,
|
|
208
|
+
hosts,
|
|
209
|
+
hasUnknownHosts,
|
|
210
|
+
grantScripts,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `@markii/bundle` `BundleStorage` backed by a plain in-memory snapshot
|
|
3
|
+
* (a `path -> bytes` record) — never a live zip handle or a disk path. This
|
|
4
|
+
* is what the worker (`./worker-entry.ts`) hands to `@markii/bundle`'s
|
|
5
|
+
* `createScriptView`, so a run's `bundle.read`/`write`/`exists` calls never
|
|
6
|
+
* touch anything outside the bytes the host chose to include in the
|
|
7
|
+
* snapshot (`./bundle-run.ts`'s `buildBundleSnapshot`).
|
|
8
|
+
*
|
|
9
|
+
* `write` is reachable only through `ScriptView` (`@markii/bundle`), which
|
|
10
|
+
* already gates every write with `isWriteAllowed` before ever calling
|
|
11
|
+
* here — this storage does not re-derive that policy, matching
|
|
12
|
+
* `./storage.ts`'s own division of labor (`ScriptView` decides WHETHER a
|
|
13
|
+
* path may be written; a `BundleStorage` only knows HOW). What this module
|
|
14
|
+
* DOES enforce, unconditionally, is the same path-jail every
|
|
15
|
+
* `BundleStorage` implementation must (`@markii/bundle`'s own doc comment
|
|
16
|
+
* on `BundleStorage`): every path is normalized via `normalizeBundlePath`
|
|
17
|
+
* before it ever touches the underlying `Map`.
|
|
18
|
+
*/
|
|
19
|
+
import type { BundleStorage } from '@markii/bundle';
|
|
20
|
+
export interface SnapshotStorage extends BundleStorage {
|
|
21
|
+
/** The storage's current contents, after any writes a run performed — used by the host to extract `.cache/` output once a run finishes. */
|
|
22
|
+
currentFiles(): Record<string, Uint8Array>;
|
|
23
|
+
}
|
|
24
|
+
/** Builds a `BundleStorage` over `initialFiles`, copying it so the caller's own object is never mutated by a later write. */
|
|
25
|
+
export declare function createSnapshotStorage(initialFiles: Record<string, Uint8Array>): SnapshotStorage;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BundlePathError, normalizeBundlePath } from '@markii/bundle';
|
|
2
|
+
function normalizeOrThrow(path) {
|
|
3
|
+
const result = normalizeBundlePath(path);
|
|
4
|
+
if (!result.ok) {
|
|
5
|
+
throw new BundlePathError(path, result.reason);
|
|
6
|
+
}
|
|
7
|
+
return result.path;
|
|
8
|
+
}
|
|
9
|
+
/** Builds a `BundleStorage` over `initialFiles`, copying it so the caller's own object is never mutated by a later write. */
|
|
10
|
+
export function createSnapshotStorage(initialFiles) {
|
|
11
|
+
const map = new Map(Object.entries(initialFiles));
|
|
12
|
+
return {
|
|
13
|
+
async read(path) {
|
|
14
|
+
return map.get(normalizeOrThrow(path));
|
|
15
|
+
},
|
|
16
|
+
async write(path, data) {
|
|
17
|
+
map.set(normalizeOrThrow(path), data);
|
|
18
|
+
},
|
|
19
|
+
async list() {
|
|
20
|
+
return [...map.keys()].sort();
|
|
21
|
+
},
|
|
22
|
+
async exists(path) {
|
|
23
|
+
return map.has(normalizeOrThrow(path));
|
|
24
|
+
},
|
|
25
|
+
async size(path) {
|
|
26
|
+
const data = map.get(normalizeOrThrow(path));
|
|
27
|
+
return data === undefined ? undefined : data.length;
|
|
28
|
+
},
|
|
29
|
+
currentFiles: () => Object.fromEntries(map),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tiny, vscode-free transform that turns a document's PERSISTED value
|
|
3
|
+
* store (`./run-flow.ts`'s `readPersistedValues`) into the value store the
|
|
4
|
+
* host re-posts to the webview when a note is (re)opened — GitHub issue #11,
|
|
5
|
+
* gap 1. "Opening a note ... showing its last data marked as stale where
|
|
6
|
+
* appropriate" (docs/spec.md, docs/scripting.md): a reopened monitoring note
|
|
7
|
+
* renders its last figures instantly and offline, visibly stale, before (or
|
|
8
|
+
* without) any re-run.
|
|
9
|
+
*
|
|
10
|
+
* The only transform is on `status`: a value that was `'fresh'` becomes
|
|
11
|
+
* `'stale'` (it is last-known data, not the product of the current session's
|
|
12
|
+
* run); a value that is already `'stale'` stays stale; an `'error'`/
|
|
13
|
+
* `'missing'` value is left untouched (there is nothing usable to mark
|
|
14
|
+
* stale — the consuming component shows its own error/empty state). Every
|
|
15
|
+
* other field is passed through verbatim, and the raw `error` text was
|
|
16
|
+
* already stripped before these values were ever persisted
|
|
17
|
+
* (`./run-flow.ts`'s `scrubValuesForWire`), so this never has to scrub again.
|
|
18
|
+
*/
|
|
19
|
+
import type { StoredValue } from '@markii/runtime';
|
|
20
|
+
/**
|
|
21
|
+
* A copy of `persisted` with every `'fresh'` value demoted to `'stale'` —
|
|
22
|
+
* see this module's doc comment. Returns a fresh object; never mutates the
|
|
23
|
+
* input (which may be the caller's live persisted snapshot).
|
|
24
|
+
*/
|
|
25
|
+
export declare function staleValuesForRehydration(persisted: Record<string, StoredValue>): Record<string, StoredValue>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The `status` a persisted value is shown with on rehydration — `'fresh'` demotes to `'stale'`; everything else is unchanged. */
|
|
2
|
+
function staleStatus(status) {
|
|
3
|
+
return status === 'fresh' ? 'stale' : status;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A copy of `persisted` with every `'fresh'` value demoted to `'stale'` —
|
|
7
|
+
* see this module's doc comment. Returns a fresh object; never mutates the
|
|
8
|
+
* input (which may be the caller's live persisted snapshot).
|
|
9
|
+
*/
|
|
10
|
+
export function staleValuesForRehydration(persisted) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
for (const [name, value] of Object.entries(persisted)) {
|
|
13
|
+
// A persisted store is only guarded down to "plain object" when it is
|
|
14
|
+
// read back (`./run-flow.ts`'s `isStoredValueRecord`), so a corrupt or
|
|
15
|
+
// foreign ENTRY reaches here unchecked. Reading `.status` off a `null`
|
|
16
|
+
// entry would throw, and this transform runs on the host before the
|
|
17
|
+
// protocol guard ever sees the values, so the throw would land in the
|
|
18
|
+
// rehydration path rather than being rejected on the wire. Skip an
|
|
19
|
+
// unusable entry instead: a value that cannot be understood is exactly
|
|
20
|
+
// a value with nothing to re-show.
|
|
21
|
+
if (typeof value !== 'object' || value === null)
|
|
22
|
+
continue;
|
|
23
|
+
entries.push([name, { ...value, status: staleStatus(value.status) }]);
|
|
24
|
+
}
|
|
25
|
+
// `Object.fromEntries`, not `out[name] = ...`: a plain assignment of the
|
|
26
|
+
// name `__proto__` hits `Object.prototype`'s setter, which sets the
|
|
27
|
+
// object's prototype and creates NO own property, silently dropping that
|
|
28
|
+
// value. `__proto__` is a legal script name (docs/spec.md's
|
|
29
|
+
// `[A-Za-z_][A-Za-z0-9_-]*`), so this is reachable from an ordinary note.
|
|
30
|
+
// `fromEntries` defines each key as an own property, `__proto__` included.
|
|
31
|
+
return Object.fromEntries(entries);
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Web Worker isolate entry, for a host that cannot create a worker
|
|
3
|
+
* thread. An Obsidian plugin is the case that forced it: its Electron
|
|
4
|
+
* renderer rejects `node:worker_threads` outright ("The V8 platform used by
|
|
5
|
+
* this instance of Node does not support creating Workers"), and forking a
|
|
6
|
+
* Node child through `ELECTRON_RUN_AS_NODE` was measured failing there too.
|
|
7
|
+
*
|
|
8
|
+
* Everything a run actually DOES lives in `./run-job.ts` and is shared with
|
|
9
|
+
* the Node entry, so the sandbox, the tier gate, the marshaling limits, and
|
|
10
|
+
* the failure classification cannot drift between hosts. This file supplies
|
|
11
|
+
* only the two things a Web Worker has to do differently:
|
|
12
|
+
*
|
|
13
|
+
* 1. Network goes through `./net-bridge.ts` to the host, which still has
|
|
14
|
+
* Node and runs the same pinned provider as before. The isolate itself
|
|
15
|
+
* has no network stack, which makes the allowlist unbypassable from
|
|
16
|
+
* inside it.
|
|
17
|
+
* 2. wasmoon's `glue.wasm` arrives as a URL on the job, because there is
|
|
18
|
+
* no `__dirname` here to find it beside the bundle.
|
|
19
|
+
*
|
|
20
|
+
* The protocol is the same one-job-in, one-result-out shape `run-host.ts`
|
|
21
|
+
* expects, with net traffic interleaved: any message that is not a net
|
|
22
|
+
* reply is the job.
|
|
23
|
+
*/
|
|
24
|
+
import { type RunJob } from './run-job.js';
|
|
25
|
+
/** The Web Worker global surface this entry uses, named so the file typechecks without DOM lib types. */
|
|
26
|
+
interface WorkerGlobal {
|
|
27
|
+
postMessage: (message: unknown) => void;
|
|
28
|
+
addEventListener: (type: 'message', listener: (event: {
|
|
29
|
+
data: unknown;
|
|
30
|
+
}) => void) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A job for this entry additionally carries where `glue.wasm` can be
|
|
34
|
+
* fetched from — a blob URL the host minted from the file on disk. Absent,
|
|
35
|
+
* wasmoon falls back to its own resolution, which in a Web Worker will not
|
|
36
|
+
* find anything; the host is expected to always send it.
|
|
37
|
+
*/
|
|
38
|
+
export interface BrowserRunJob extends RunJob {
|
|
39
|
+
wasmUri?: string;
|
|
40
|
+
}
|
|
41
|
+
declare function start(scope: WorkerGlobal): void;
|
|
42
|
+
export { start as startBrowserWorker };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Web Worker isolate entry, for a host that cannot create a worker
|
|
3
|
+
* thread. An Obsidian plugin is the case that forced it: its Electron
|
|
4
|
+
* renderer rejects `node:worker_threads` outright ("The V8 platform used by
|
|
5
|
+
* this instance of Node does not support creating Workers"), and forking a
|
|
6
|
+
* Node child through `ELECTRON_RUN_AS_NODE` was measured failing there too.
|
|
7
|
+
*
|
|
8
|
+
* Everything a run actually DOES lives in `./run-job.ts` and is shared with
|
|
9
|
+
* the Node entry, so the sandbox, the tier gate, the marshaling limits, and
|
|
10
|
+
* the failure classification cannot drift between hosts. This file supplies
|
|
11
|
+
* only the two things a Web Worker has to do differently:
|
|
12
|
+
*
|
|
13
|
+
* 1. Network goes through `./net-bridge.ts` to the host, which still has
|
|
14
|
+
* Node and runs the same pinned provider as before. The isolate itself
|
|
15
|
+
* has no network stack, which makes the allowlist unbypassable from
|
|
16
|
+
* inside it.
|
|
17
|
+
* 2. wasmoon's `glue.wasm` arrives as a URL on the job, because there is
|
|
18
|
+
* no `__dirname` here to find it beside the bundle.
|
|
19
|
+
*
|
|
20
|
+
* The protocol is the same one-job-in, one-result-out shape `run-host.ts`
|
|
21
|
+
* expects, with net traffic interleaved: any message that is not a net
|
|
22
|
+
* reply is the job.
|
|
23
|
+
*/
|
|
24
|
+
import { isRunJob, resultForInternalError, runJob, } from './run-job.js';
|
|
25
|
+
import { createNetBridge } from './net-bridge-worker.js';
|
|
26
|
+
function start(scope) {
|
|
27
|
+
const bridge = createNetBridge((message) => {
|
|
28
|
+
scope.postMessage(message);
|
|
29
|
+
});
|
|
30
|
+
let running = false;
|
|
31
|
+
scope.addEventListener('message', (event) => {
|
|
32
|
+
const message = event.data;
|
|
33
|
+
// Net replies arrive throughout the run; the bridge ignores anything
|
|
34
|
+
// that is not one of its own.
|
|
35
|
+
bridge.handleMessage(message);
|
|
36
|
+
if (running)
|
|
37
|
+
return;
|
|
38
|
+
if (typeof message === 'object' &&
|
|
39
|
+
message !== null &&
|
|
40
|
+
message.kind === 'markii:net-reply') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
running = true;
|
|
44
|
+
void (async () => {
|
|
45
|
+
if (!isRunJob(message)) {
|
|
46
|
+
scope.postMessage(resultForInternalError(new Error('worker received a malformed job message'), {}));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const job = message;
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
result = await runJob(job, {
|
|
53
|
+
createNet: () => bridge.provider,
|
|
54
|
+
wasmUri: job.wasmUri,
|
|
55
|
+
// GitHub issue #35: the same per-script message the Node entry
|
|
56
|
+
// sends, on the same channel this worker posts its result on, so
|
|
57
|
+
// ordering (progress before result) holds here too. Built in
|
|
58
|
+
// `./run-job.ts`, never here.
|
|
59
|
+
postProgress: (progress) => {
|
|
60
|
+
scope.postMessage(progress);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
result = resultForInternalError(err, job.cacheSnapshot ?? {});
|
|
66
|
+
}
|
|
67
|
+
scope.postMessage(result);
|
|
68
|
+
})();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// Guarded so importing this module in a test (or anywhere that is not a
|
|
72
|
+
// worker) is inert rather than throwing on a missing `self`.
|
|
73
|
+
if (typeof self !== 'undefined' &&
|
|
74
|
+
typeof self.addEventListener === 'function') {
|
|
75
|
+
start(self);
|
|
76
|
+
}
|
|
77
|
+
export { start as startBrowserWorker };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { RunJob, RunFailure, RunProgress, RunResult } from './run-job.js';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `worker_thread` entry point for slice 1 of the extension's v2 Run
|
|
3
|
+
* arc (docs: GitHub issue #1's locked design comment). This file is
|
|
4
|
+
* vscode-free — it never imports `vscode` and knows nothing about VS
|
|
5
|
+
* Code's API — so it can be unit-tested directly with Vitest (spawned as a
|
|
6
|
+
* REAL `node:worker_threads` worker, exercising the real wasmoon sandbox)
|
|
7
|
+
* and, unmodified, be the bundled worker `esbuild.config.mjs` produces for
|
|
8
|
+
* the packaged extension.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a whole worker per run
|
|
11
|
+
*
|
|
12
|
+
* The design (docs/security.md's isolate requirement) is "one ephemeral
|
|
13
|
+
* worker per run": the host spawns a fresh thread, this file boots
|
|
14
|
+
* `@markii/lua` + `@markii/runtime` once, runs exactly one batch, posts
|
|
15
|
+
* back one progress message per script and then exactly one result
|
|
16
|
+
* message, and the whole thread is expected to be
|
|
17
|
+
* torn down by the host afterward (`run-host.ts`) regardless of how this
|
|
18
|
+
* run went. That is what makes the external wall-clock watchdog
|
|
19
|
+
* (`worker.terminate()`) an unconditional, always-available kill switch:
|
|
20
|
+
* it can never be blocked by anything this file's own code does, because
|
|
21
|
+
* `terminate()` acts on the OS/V8 thread itself, not on anything
|
|
22
|
+
* cooperative running inside it.
|
|
23
|
+
*
|
|
24
|
+
* ## Never an unhandled rejection
|
|
25
|
+
*
|
|
26
|
+
* Every path through `main()` below is wrapped so that ANY failure —
|
|
27
|
+
* a malformed job message, `parse()` throwing on pathological input,
|
|
28
|
+
* `runDocumentScripts` itself misbehaving — becomes an ordinary result
|
|
29
|
+
* message carrying a synthetic failure, never a thrown/rejected error that
|
|
30
|
+
* could surface as an "Unhandled Promise Rejection" on the worker thread
|
|
31
|
+
* (which Node would otherwise report noisily, or in the worst case treat
|
|
32
|
+
* as fatal depending on the host's process-wide `--unhandled-rejections`
|
|
33
|
+
* setting). `run-host.ts` therefore never needs an `unhandledRejection`
|
|
34
|
+
* listener on the worker to stay safe.
|
|
35
|
+
*/
|
|
36
|
+
import { parentPort } from 'node:worker_threads';
|
|
37
|
+
import { existsSync } from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import { netProviderDenial } from '@markii/lua';
|
|
40
|
+
import { createNetProvider } from './net-provider.js';
|
|
41
|
+
import { isRunJob, resultForInternalError, runJob } from './run-job.js';
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the `wasmUri` to hand `createLuaExecutor` (forwarded to
|
|
44
|
+
* `@markii/lua`'s `runScript` -> `createEmptyLuaEngine` -> wasmoon's
|
|
45
|
+
* `LuaFactory`). In the BUNDLED extension, `esbuild.config.mjs`'s worker
|
|
46
|
+
* build copies wasmoon's `glue.wasm` next to this file's compiled output
|
|
47
|
+
* (`dist/run/glue.wasm`) specifically so this lookup succeeds; in
|
|
48
|
+
* dev/Vitest (this file run straight from `src/run/`, no copy step has
|
|
49
|
+
* ever run), the file is absent and `undefined` is returned, letting
|
|
50
|
+
* wasmoon fall back to its own default Node resolution (the real
|
|
51
|
+
* `node_modules/wasmoon/dist/glue.wasm`) — see `@markii/lua`'s
|
|
52
|
+
* `createEmptyLuaEngine` doc comment for why `undefined` is always safe to
|
|
53
|
+
* pass here.
|
|
54
|
+
*/
|
|
55
|
+
function resolveWasmUri() {
|
|
56
|
+
const candidate = path.join(__dirname, 'glue.wasm');
|
|
57
|
+
return existsSync(candidate) ? candidate : undefined;
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
if (!parentPort) {
|
|
61
|
+
// Not actually running as a worker thread (e.g. required directly by
|
|
62
|
+
// mistake) — nothing to do, and nothing to post a result to.
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const port = parentPort;
|
|
66
|
+
port.once('message', (message) => {
|
|
67
|
+
void (async () => {
|
|
68
|
+
if (!isRunJob(message)) {
|
|
69
|
+
port.postMessage(resultForInternalError(new Error('worker received a malformed job message'), {}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const result = await runJob(message, {
|
|
74
|
+
// GitHub issue #35: one message per script, on the SAME port the
|
|
75
|
+
// final result goes out on, so the host receives them in order
|
|
76
|
+
// and always ahead of the result. Identical to what
|
|
77
|
+
// `./worker-entry-browser.ts` does; the message itself is built
|
|
78
|
+
// in `./run-job.ts` so the two cannot drift.
|
|
79
|
+
postProgress: (progress) => {
|
|
80
|
+
port.postMessage(progress);
|
|
81
|
+
},
|
|
82
|
+
// The Lua brand is supplied HERE, where the runtime is already
|
|
83
|
+
// present, rather than imported by the provider itself — see
|
|
84
|
+
// `./net-provider.ts`'s doc comment.
|
|
85
|
+
createNet: (allowlist, maxFetchBytes, policy) => createNetProvider(allowlist, maxFetchBytes, policy, netProviderDenial),
|
|
86
|
+
wasmUri: resolveWasmUri(),
|
|
87
|
+
});
|
|
88
|
+
port.postMessage(result);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
port.postMessage(resultForInternalError(err, message.cacheSnapshot ?? {}));
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
void main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ValuesFailure` is the wire-protocol shape a host uses to report one
|
|
3
|
+
* script's failure back to its renderer/webview: the script's name and a
|
|
4
|
+
* closed `FailureKind`, never the raw error message (the rendered page
|
|
5
|
+
* shows quiet markers, never error dumps — see AGENTS.md's cleanliness
|
|
6
|
+
* principle). Defined here, in the shared host layer, so every host
|
|
7
|
+
* (`apps/vscode`, and any future host) and this package's own `run-flow.ts`
|
|
8
|
+
* agree on a single definition instead of drifting copies; a consuming
|
|
9
|
+
* app's own wire-protocol module (e.g. `apps/vscode/src/protocol.ts`) is
|
|
10
|
+
* expected to import and re-export this type rather than restate it.
|
|
11
|
+
*/
|
|
12
|
+
import type { FailureKind } from '@markii/runtime';
|
|
13
|
+
/** One script's outcome, reduced to just its name and failure kind — never the raw error message. */
|
|
14
|
+
export interface ValuesFailure {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly kind: FailureKind;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way a host folds a single arriving value into the value store its
|
|
3
|
+
* preview is rendering from (GitHub issue #35).
|
|
4
|
+
*
|
|
5
|
+
* A run reports its values one at a time, in document order, while it is
|
|
6
|
+
* still going. Each arrival has to replace exactly one name and leave every
|
|
7
|
+
* other name exactly as it was — that is what makes the bound component go
|
|
8
|
+
* fresh while the rest of the note stays visibly stale, instead of the
|
|
9
|
+
* whole page flipping at the end of the batch.
|
|
10
|
+
*
|
|
11
|
+
* It lives here, in the shared host layer, because both hosts do it: the VS
|
|
12
|
+
* Code webview folds a `value` wire message into its React state, and the
|
|
13
|
+
* Obsidian view folds a callback's value into the store it re-renders from.
|
|
14
|
+
* Two copies of a merge this small still drift, and the `__proto__` rule
|
|
15
|
+
* below is exactly the kind of detail one copy would quietly lose.
|
|
16
|
+
*
|
|
17
|
+
* Environment-free on purpose (no `node:*`, no React), so the main entry
|
|
18
|
+
* can export it and a browser bundle can import it.
|
|
19
|
+
*/
|
|
20
|
+
import type { StoredValue } from '@markii/runtime';
|
|
21
|
+
/**
|
|
22
|
+
* `values` with `name` set to `value`, as a NEW object — the input is never
|
|
23
|
+
* mutated, so a caller holding the previous store (React state, a view's
|
|
24
|
+
* last-rendered snapshot) can still compare against it.
|
|
25
|
+
*
|
|
26
|
+
* A name already present keeps its POSITION and takes the new value; a new
|
|
27
|
+
* name is appended. Order is not semantically meaningful to a renderer, but
|
|
28
|
+
* a store whose keys reshuffle on every value would make any diff of two
|
|
29
|
+
* snapshots harder to read than it needs to be.
|
|
30
|
+
*
|
|
31
|
+
* Built through `Object.fromEntries` rather than `{...values, [name]: value}`
|
|
32
|
+
* because a script may legitimately be named `__proto__` (docs/spec.md's
|
|
33
|
+
* name grammar allows it): a computed assignment of that key hits
|
|
34
|
+
* `Object.prototype`'s setter, which sets the object's prototype and
|
|
35
|
+
* creates no own property at all, silently dropping the value. The same
|
|
36
|
+
* guard is in `run/run-flow.ts`'s `mergePersistedValues` and
|
|
37
|
+
* `run/stale-values.ts`, for the same reason.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mergeArrivingValue(values: Readonly<Record<string, StoredValue>> | undefined, name: string, value: StoredValue): Record<string, StoredValue>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `values` with `name` set to `value`, as a NEW object — the input is never
|
|
3
|
+
* mutated, so a caller holding the previous store (React state, a view's
|
|
4
|
+
* last-rendered snapshot) can still compare against it.
|
|
5
|
+
*
|
|
6
|
+
* A name already present keeps its POSITION and takes the new value; a new
|
|
7
|
+
* name is appended. Order is not semantically meaningful to a renderer, but
|
|
8
|
+
* a store whose keys reshuffle on every value would make any diff of two
|
|
9
|
+
* snapshots harder to read than it needs to be.
|
|
10
|
+
*
|
|
11
|
+
* Built through `Object.fromEntries` rather than `{...values, [name]: value}`
|
|
12
|
+
* because a script may legitimately be named `__proto__` (docs/spec.md's
|
|
13
|
+
* name grammar allows it): a computed assignment of that key hits
|
|
14
|
+
* `Object.prototype`'s setter, which sets the object's prototype and
|
|
15
|
+
* creates no own property at all, silently dropping the value. The same
|
|
16
|
+
* guard is in `run/run-flow.ts`'s `mergePersistedValues` and
|
|
17
|
+
* `run/stale-values.ts`, for the same reason.
|
|
18
|
+
*/
|
|
19
|
+
export function mergeArrivingValue(values, name, value) {
|
|
20
|
+
const entries = [];
|
|
21
|
+
let replaced = false;
|
|
22
|
+
for (const [key, existing] of Object.entries(values ?? {})) {
|
|
23
|
+
if (key === name) {
|
|
24
|
+
entries.push([key, value]);
|
|
25
|
+
replaced = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
entries.push([key, existing]);
|
|
29
|
+
}
|
|
30
|
+
if (!replaced)
|
|
31
|
+
entries.push([name, value]);
|
|
32
|
+
return Object.fromEntries(entries);
|
|
33
|
+
}
|