@markii/host 0.0.1 → 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/README.md +18 -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 +77 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a host's pack-loading outcome as plain text lines for that host's
|
|
3
|
+
* own diagnostics surface (AGENTS.md's "clean is not silent": every
|
|
4
|
+
* failure needs a full diagnostic somewhere a user can find it, not just a
|
|
5
|
+
* quiet marker in the preview).
|
|
6
|
+
*
|
|
7
|
+
* This module owns the STRUCTURAL wording — one line per loaded pack, one
|
|
8
|
+
* per skipped folder, the CSS-warning lines, and the pack-registration
|
|
9
|
+
* lines (invalid registration reasons and the namespace-collision line) —
|
|
10
|
+
* which is identical across every host. It deliberately does NOT format
|
|
11
|
+
* the relative-entry note or the prebuilt-shadow note: which setting a
|
|
12
|
+
* relative entry belongs to (VS Code's `markii.packs`, this plugin's
|
|
13
|
+
* device-local pack-folder list) and what "relative" means for that host (a
|
|
14
|
+
* different workspace window vs. a different vault) is host-specific
|
|
15
|
+
* knowledge, and naming a host's own "build pack for distribution" command
|
|
16
|
+
* in the shadow note is host-specific the same way, so each host formats
|
|
17
|
+
* those lines itself and passes them in already-rendered
|
|
18
|
+
* (`relativeEntryLines`/`prebuiltShadowLines` below) — see
|
|
19
|
+
* `apps/vscode/src/packs/pack-diagnostics.ts` and
|
|
20
|
+
* `apps/obsidian/src/packs/pack-diagnostics.ts` for each host's own wording
|
|
21
|
+
* and thin wrapper around this function.
|
|
22
|
+
*/
|
|
23
|
+
import { packComponents, packComponentsForEngine } from '@markii/pack';
|
|
24
|
+
/** The one line for a folder `discoverPacks` could not turn into a usable pack. */
|
|
25
|
+
export function skippedPackFolderLine(entry) {
|
|
26
|
+
return `Skipped pack folder "${entry.folder}": ${entry.reason}`;
|
|
27
|
+
}
|
|
28
|
+
/** One line for each folder `discoverPacks`/a host's `loadPackContext` could not turn into a usable pack. */
|
|
29
|
+
function skippedLines(context) {
|
|
30
|
+
return context.skipped.map(skippedPackFolderLine);
|
|
31
|
+
}
|
|
32
|
+
/** One line for each pack that loaded successfully, naming what a user would want to confirm: its name, namespace, and how many components it registered. */
|
|
33
|
+
function loadedLines(context) {
|
|
34
|
+
return context.packs.map((pack) => {
|
|
35
|
+
const componentCount = context.engine === undefined
|
|
36
|
+
? packComponents(pack.manifest).length
|
|
37
|
+
: packComponentsForEngine(pack.manifest, context.engine).length;
|
|
38
|
+
const plural = componentCount === 1 ? 'component' : 'components';
|
|
39
|
+
return `Loaded pack "${pack.manifest.name}" (namespace: ${pack.manifest.name}, ${String(componentCount)} ${plural})`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** The one summary line for a namespace collision among registered packs, or `[]` when there was none. */
|
|
43
|
+
function collisionLines(context) {
|
|
44
|
+
const collisions = context.registrationCollisions ?? [];
|
|
45
|
+
if (collisions.length === 0)
|
|
46
|
+
return [];
|
|
47
|
+
return [
|
|
48
|
+
`Installed packs share a namespace (${collisions.join(', ')}); none of them were installed.`,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/** One line per composed directive name two different packs both claimed, naming the pack that kept it and the pack whose component was skipped — `[]` when there was none (the ordinary case). */
|
|
52
|
+
function duplicateComposedNameLines(context) {
|
|
53
|
+
const duplicates = context.duplicateComposedNames ?? [];
|
|
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
|
+
}
|
|
56
|
+
/** One line per pack dropped because this renderer cannot run its declared engine (batch 7 #46) — `[]` when there was none. */
|
|
57
|
+
function droppedEngineLines(context) {
|
|
58
|
+
const dropped = context.droppedEngines ?? [];
|
|
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).`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The full set of diagnostic lines for one pack-loading outcome: loaded
|
|
63
|
+
* packs first (the confirmation that the setting is working at all), then
|
|
64
|
+
* every skipped folder with its reason, then relative-entry lines, then
|
|
65
|
+
* prebuilt-shadow lines, then pack CSS lint warnings, then any
|
|
66
|
+
* invalid-registration, namespace-collision, or duplicate-composed-name
|
|
67
|
+
* lines. Empty when nothing is configured at all — the caller decides
|
|
68
|
+
* whether an empty result is worth writing anything to its own
|
|
69
|
+
* diagnostics surface.
|
|
70
|
+
*/
|
|
71
|
+
export function formatPackDiagnosticLines(context) {
|
|
72
|
+
return [
|
|
73
|
+
...loadedLines(context),
|
|
74
|
+
...skippedLines(context),
|
|
75
|
+
...(context.relativeEntryLines ?? []),
|
|
76
|
+
...(context.prebuiltShadowLines ?? []),
|
|
77
|
+
...context.cssWarnings,
|
|
78
|
+
...(context.invalidRegistrationReasons ?? []),
|
|
79
|
+
...collisionLines(context),
|
|
80
|
+
...duplicateComposedNameLines(context),
|
|
81
|
+
...droppedEngineLines(context),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
/** How many configured folders failed to produce a usable pack — what a preview's quiet marker counts. */
|
|
85
|
+
export function skippedPackCount(context) {
|
|
86
|
+
return context.skipped.length;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One informational line for a pack whose prebuilt `webview.js` shadows
|
|
90
|
+
* component sources still present in the same folder — survey finding A9:
|
|
91
|
+
* `apps/vscode/src/packs/pack-diagnostics.ts` and
|
|
92
|
+
* `apps/obsidian/src/packs/pack-diagnostics.ts` each defined this exact
|
|
93
|
+
* sentence shape, differing only in the tail clause naming how a user
|
|
94
|
+
* would rebuild the prebuilt artifact from those sources (VS Code's own
|
|
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).
|
|
97
|
+
*
|
|
98
|
+
* Never a failure: shipping both the built artifact and its sources is a
|
|
99
|
+
* supported distribution shape. `rebuildInstruction` is the one thing that
|
|
100
|
+
* differs per host — a short clause completing "Edits to them take effect
|
|
101
|
+
* only after you delete webview.js, or ...".
|
|
102
|
+
*/
|
|
103
|
+
export function prebuiltShadowLine(pack, rebuildInstruction) {
|
|
104
|
+
return `Pack "${pack.name}" is using its prebuilt webview.js, so the component sources in that folder are not compiled. Edits to them take effect only after you delete webview.js, or ${rebuildInstruction}.`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves each entry in `packs` to an absolute path. `homeDir` is a plain
|
|
3
|
+
* parameter (not read via `os.homedir()` internally) so this stays testable
|
|
4
|
+
* against a fixed value instead of the real host's home directory; the real
|
|
5
|
+
* caller (each host's own `pack-context.ts`) passes `os.homedir()` itself.
|
|
6
|
+
* Omitted (or `undefined`) disables `~` expansion entirely — a `~` entry is
|
|
7
|
+
* then simply relative like any other.
|
|
8
|
+
*
|
|
9
|
+
* - A `~`/`~/...` entry is expanded against `homeDir` FIRST (`expandHome`)
|
|
10
|
+
* — this makes an absolute path pleasant to write, which matters because
|
|
11
|
+
* a relative entry can mean a different folder in every window/vault it
|
|
12
|
+
* happens to be open in, so an absolute (or `~`-relative) entry is the
|
|
13
|
+
* only spelling that means the same folder everywhere.
|
|
14
|
+
* - An entry that is (now) already absolute passes through UNCHANGED —
|
|
15
|
+
* never re-rooted, and never rejected for pointing outside `baseDir`,
|
|
16
|
+
* since an absolute path is the user explicitly naming a folder wherever
|
|
17
|
+
* it is (docs/packs.md: packs are installed into the application, not
|
|
18
|
+
* jailed the way a bundle's contents are).
|
|
19
|
+
* - A relative entry (still relative after `~` expansion) is joined
|
|
20
|
+
* against `baseDir` and NORMALIZED (`path.normalize` via `path.join`,
|
|
21
|
+
* which collapses `..` segments syntactically). A relative entry that
|
|
22
|
+
* climbs out of `baseDir` via `..` is intentionally still honored, for
|
|
23
|
+
* the same reason an absolute entry is: this setting is a user-authored
|
|
24
|
+
* trust decision, not a content-derived path that needs jailing — unlike
|
|
25
|
+
* a bundle's `scripts/`/`assets/` paths (`@markii/bundle`'s
|
|
26
|
+
* `normalizeBundlePath`), which DO get jailed because they come from
|
|
27
|
+
* potentially-hostile bundle content, not from the user directly editing
|
|
28
|
+
* their own settings.
|
|
29
|
+
* - `baseDir === undefined` (no workspace/vault open) leaves every relative
|
|
30
|
+
* entry UNRESOLVED (returned as-is, unjoined) rather than guessing a
|
|
31
|
+
* base — a relative entry with nothing open cannot mean anything, so the
|
|
32
|
+
* caller's later folder-read simply fails to find a `pack.json` there
|
|
33
|
+
* and `discoverPacks` quietly skips it, the same cleanliness posture as
|
|
34
|
+
* any other unreadable folder.
|
|
35
|
+
*
|
|
36
|
+
* Never throws — every input is a configuration string a user typed by
|
|
37
|
+
* hand, and a malformed one should degrade to "not found", not to a crash.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolvePackPaths(packs: readonly string[], baseDir: string | undefined, homeDir?: string): string[];
|
|
40
|
+
/**
|
|
41
|
+
* The detection half of the relative-entry diagnostics: the ORIGINAL
|
|
42
|
+
* (unexpanded) entries in
|
|
43
|
+
* `packs` that are relative once `~` expansion is accounted for — i.e.
|
|
44
|
+
* exactly the entries `resolvePackPaths` resolves against `baseDir` rather
|
|
45
|
+
* than using as-is. A relative entry can silently mean a different folder
|
|
46
|
+
* in every window/vault it happens to be open in, so a caller surfaces
|
|
47
|
+
* these to its own diagnostics surface, in its own wording (each host
|
|
48
|
+
* names its setting and the ambiguity differently — see this module's top
|
|
49
|
+
* doc comment); `resolvePackPaths` keeps resolving them exactly as it
|
|
50
|
+
* always has: this function only ever adds a warning, never changes
|
|
51
|
+
* behavior.
|
|
52
|
+
*/
|
|
53
|
+
export declare function relativePackEntries(packs: readonly string[], homeDir?: string): string[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a host's pack-folder setting entries to absolute folder paths.
|
|
3
|
+
* Pure (paths in, a base directory in, absolute paths out) so it is
|
|
4
|
+
* unit-testable without a real workspace/vault.
|
|
5
|
+
*
|
|
6
|
+
* `baseDir` is deliberately a generic name: what it MEANS differs per host
|
|
7
|
+
* (VS Code resolves a relative entry against the open workspace folder,
|
|
8
|
+
* since `markii.packs` is a user-scoped setting; an Obsidian plugin
|
|
9
|
+
* resolves it against the open vault, since its pack-folder list is a
|
|
10
|
+
* device-local setting that can be opened against several vaults) but the
|
|
11
|
+
* resolution RULE is identical, so only the base directory's identity is a
|
|
12
|
+
* host concern — reading the setting itself, and naming what "relative"
|
|
13
|
+
* means in a diagnostic message, stays in each host's own app code.
|
|
14
|
+
*
|
|
15
|
+
* DECISION (per orchestrator direction, carried over from the original VS
|
|
16
|
+
* Code module this was hoisted from): a relative entry is resolved against
|
|
17
|
+
* `baseDir` as given by the caller; an absolute entry is used as-is. This
|
|
18
|
+
* matches how a user reads the setting's own description: "this pack
|
|
19
|
+
* folder, relative to my project/vault".
|
|
20
|
+
*/
|
|
21
|
+
import { isAbsolutePath, joinPath } from '../path-util.js';
|
|
22
|
+
/**
|
|
23
|
+
* Expands a leading `~` in `entry` against `homeDir` — `~` alone, or
|
|
24
|
+
* `~/...`/`~\...` (both separators accepted regardless of host platform, so
|
|
25
|
+
* a setting written on one OS still expands sensibly if synced to
|
|
26
|
+
* another). Any other entry (including one that merely CONTAINS a `~`
|
|
27
|
+
* elsewhere, or has no `homeDir` to expand against) passes through
|
|
28
|
+
* unchanged. This is convenience, not path resolution: the result still
|
|
29
|
+
* goes through the ordinary absolute/relative handling in
|
|
30
|
+
* `resolvePackPaths` below, exactly like a path the user typed out in
|
|
31
|
+
* full.
|
|
32
|
+
*/
|
|
33
|
+
function expandHome(entry, homeDir) {
|
|
34
|
+
if (homeDir === undefined)
|
|
35
|
+
return entry;
|
|
36
|
+
if (entry === '~')
|
|
37
|
+
return homeDir;
|
|
38
|
+
if (entry.startsWith('~/') || entry.startsWith('~\\')) {
|
|
39
|
+
return joinPath(homeDir, entry.slice(2));
|
|
40
|
+
}
|
|
41
|
+
return entry;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves each entry in `packs` to an absolute path. `homeDir` is a plain
|
|
45
|
+
* parameter (not read via `os.homedir()` internally) so this stays testable
|
|
46
|
+
* against a fixed value instead of the real host's home directory; the real
|
|
47
|
+
* caller (each host's own `pack-context.ts`) passes `os.homedir()` itself.
|
|
48
|
+
* Omitted (or `undefined`) disables `~` expansion entirely — a `~` entry is
|
|
49
|
+
* then simply relative like any other.
|
|
50
|
+
*
|
|
51
|
+
* - A `~`/`~/...` entry is expanded against `homeDir` FIRST (`expandHome`)
|
|
52
|
+
* — this makes an absolute path pleasant to write, which matters because
|
|
53
|
+
* a relative entry can mean a different folder in every window/vault it
|
|
54
|
+
* happens to be open in, so an absolute (or `~`-relative) entry is the
|
|
55
|
+
* only spelling that means the same folder everywhere.
|
|
56
|
+
* - An entry that is (now) already absolute passes through UNCHANGED —
|
|
57
|
+
* never re-rooted, and never rejected for pointing outside `baseDir`,
|
|
58
|
+
* since an absolute path is the user explicitly naming a folder wherever
|
|
59
|
+
* it is (docs/packs.md: packs are installed into the application, not
|
|
60
|
+
* jailed the way a bundle's contents are).
|
|
61
|
+
* - A relative entry (still relative after `~` expansion) is joined
|
|
62
|
+
* against `baseDir` and NORMALIZED (`path.normalize` via `path.join`,
|
|
63
|
+
* which collapses `..` segments syntactically). A relative entry that
|
|
64
|
+
* climbs out of `baseDir` via `..` is intentionally still honored, for
|
|
65
|
+
* the same reason an absolute entry is: this setting is a user-authored
|
|
66
|
+
* trust decision, not a content-derived path that needs jailing — unlike
|
|
67
|
+
* a bundle's `scripts/`/`assets/` paths (`@markii/bundle`'s
|
|
68
|
+
* `normalizeBundlePath`), which DO get jailed because they come from
|
|
69
|
+
* potentially-hostile bundle content, not from the user directly editing
|
|
70
|
+
* their own settings.
|
|
71
|
+
* - `baseDir === undefined` (no workspace/vault open) leaves every relative
|
|
72
|
+
* entry UNRESOLVED (returned as-is, unjoined) rather than guessing a
|
|
73
|
+
* base — a relative entry with nothing open cannot mean anything, so the
|
|
74
|
+
* caller's later folder-read simply fails to find a `pack.json` there
|
|
75
|
+
* and `discoverPacks` quietly skips it, the same cleanliness posture as
|
|
76
|
+
* any other unreadable folder.
|
|
77
|
+
*
|
|
78
|
+
* Never throws — every input is a configuration string a user typed by
|
|
79
|
+
* hand, and a malformed one should degrade to "not found", not to a crash.
|
|
80
|
+
*/
|
|
81
|
+
export function resolvePackPaths(packs, baseDir, homeDir) {
|
|
82
|
+
return packs.map((entry) => {
|
|
83
|
+
const expanded = expandHome(entry, homeDir);
|
|
84
|
+
if (isAbsolutePath(expanded))
|
|
85
|
+
return expanded;
|
|
86
|
+
if (baseDir === undefined)
|
|
87
|
+
return expanded;
|
|
88
|
+
return joinPath(baseDir, expanded);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The detection half of the relative-entry diagnostics: the ORIGINAL
|
|
93
|
+
* (unexpanded) entries in
|
|
94
|
+
* `packs` that are relative once `~` expansion is accounted for — i.e.
|
|
95
|
+
* exactly the entries `resolvePackPaths` resolves against `baseDir` rather
|
|
96
|
+
* than using as-is. A relative entry can silently mean a different folder
|
|
97
|
+
* in every window/vault it happens to be open in, so a caller surfaces
|
|
98
|
+
* these to its own diagnostics surface, in its own wording (each host
|
|
99
|
+
* names its setting and the ambiguity differently — see this module's top
|
|
100
|
+
* doc comment); `resolvePackPaths` keeps resolving them exactly as it
|
|
101
|
+
* always has: this function only ever adds a warning, never changes
|
|
102
|
+
* behavior.
|
|
103
|
+
*/
|
|
104
|
+
export function relativePackEntries(packs, homeDir) {
|
|
105
|
+
return packs.filter((entry) => !isAbsolutePath(expandHome(entry, homeDir)));
|
|
106
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { PackManifest } from '@markii/pack';
|
|
2
|
+
import type { Registry, RegistryEntry } from '@markii/react';
|
|
3
|
+
/**
|
|
4
|
+
* The renderer-specific half of building a pack-aware registry: the
|
|
5
|
+
* renderer's own `loadPack`, `createRegistry` and `mergeRegistries`. A
|
|
6
|
+
* React host passes `@markii/react`'s `reactPackEngine`; the host layer
|
|
7
|
+
* itself names no renderer.
|
|
8
|
+
*/
|
|
9
|
+
export interface PackRegistryEngine {
|
|
10
|
+
loadPack(manifest: PackManifest, componentModules: Record<string, RegistryEntry>): {
|
|
11
|
+
registry: Registry;
|
|
12
|
+
dropped?: {
|
|
13
|
+
name: string;
|
|
14
|
+
engine: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
createRegistry(entries?: Record<string, RegistryEntry>): Registry;
|
|
18
|
+
mergeRegistries(...registries: Registry[]): Registry;
|
|
19
|
+
}
|
|
20
|
+
/** 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
|
+
export interface QueuedPackRegistration {
|
|
22
|
+
readonly manifestJson: unknown;
|
|
23
|
+
readonly componentModules: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One composed directive name two DIFFERENT packs both claimed. Under
|
|
27
|
+
* `@markii/pack`'s current underscore join (issue #19) this cannot arise
|
|
28
|
+
* from two packs' names and local names composing to the same string — the
|
|
29
|
+
* join is bijective (see `namespace.ts`'s `composeDirectiveName` doc
|
|
30
|
+
* comment) — so this is a defense-in-depth invariant, not a reachable case
|
|
31
|
+
* of ordinary pack composition. It stays because a hand-written pack
|
|
32
|
+
* registration script (or a future regression in the composition rule)
|
|
33
|
+
* could still hand `buildRenderRegistry` two registrations that carry the
|
|
34
|
+
* identical composed name; when that happens the FIRST claimant keeps the
|
|
35
|
+
* name and the later one is dropped, exactly like a malformed registration.
|
|
36
|
+
*/
|
|
37
|
+
export interface DuplicateComposedName {
|
|
38
|
+
readonly composedName: string;
|
|
39
|
+
/** The pack whose component kept the name. */
|
|
40
|
+
readonly keptPack: string;
|
|
41
|
+
/** The pack whose component was skipped because the name was already taken. */
|
|
42
|
+
readonly skippedPack: string;
|
|
43
|
+
}
|
|
44
|
+
/** 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: Registry;
|
|
47
|
+
/** One line per malformed registration, dropped rather than installed. */
|
|
48
|
+
readonly invalidReasons: readonly string[];
|
|
49
|
+
/** 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. */
|
|
50
|
+
readonly collisions: readonly string[];
|
|
51
|
+
/** One entry per composed directive name claimed by two different packs — see `DuplicateComposedName`. Always empty for registrations that went through ordinary composition; kept as an executable invariant, not dead code (see `./pack-render-registry.test.ts`'s constructed-collision test). */
|
|
52
|
+
readonly duplicateComposedNames: readonly DuplicateComposedName[];
|
|
53
|
+
/**
|
|
54
|
+
* One entry per validated, non-colliding pack whose manifest declares an
|
|
55
|
+
* engine `@markii/react`'s `loadPack` cannot run (batch 7 #46). Before
|
|
56
|
+
* this field existed, such a pack installed as an empty contribution
|
|
57
|
+
* with no record anywhere that it had even been attempted — every
|
|
58
|
+
* directive under its namespace fell through to the ordinary
|
|
59
|
+
* unknown-component fallback, indistinguishable from a directive that
|
|
60
|
+
* was never a pack component at all. Always empty for an ordinary
|
|
61
|
+
* all-react-engine install.
|
|
62
|
+
*/
|
|
63
|
+
readonly droppedEngines: readonly {
|
|
64
|
+
name: string;
|
|
65
|
+
engine: string;
|
|
66
|
+
}[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Builds the render registry: `defaultRegistry` merged with every
|
|
70
|
+
* successfully validated, non-colliding pack registration in `queued`.
|
|
71
|
+
* Never throws:
|
|
72
|
+
*
|
|
73
|
+
* - an individual malformed registration is dropped (reason recorded, never
|
|
74
|
+
* shown on the page);
|
|
75
|
+
* - a namespace collision between two loaded packs (the same pack name
|
|
76
|
+
* registered twice) rejects the WHOLE install (matching docs/packs.md's
|
|
77
|
+
* install-time rejection rule) and this function falls back to
|
|
78
|
+
* `defaultRegistry` alone;
|
|
79
|
+
* - a composed-directive-name collision between two DIFFERENTLY named packs
|
|
80
|
+
* (see `DuplicateComposedName`) drops only the later claimant's
|
|
81
|
+
* component, not the whole pack.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildRenderRegistry(queued: readonly QueuedPackRegistration[], defaultRegistry: Registry, engine: PackRegistryEngine): BuildRenderRegistryResult;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a batch of `QueuedPackRegistration`s and merges them into a
|
|
3
|
+
* render `Registry` on top of a base registry, using `@markii/react`'s
|
|
4
|
+
* `loadPack` per pack (namespace-collision rejection reimplemented here so
|
|
5
|
+
* this module can also guard against a same-composed-name collision
|
|
6
|
+
* between two differently named packs — see `DuplicateComposedName` and
|
|
7
|
+
* `mergePacksKeepingFirstClaim`).
|
|
8
|
+
*
|
|
9
|
+
* Every entry here crossed a boundary from a SEPARATELY EVALUATED script (a
|
|
10
|
+
* pack's compiled artifact, not code a host's own bundle compiled) — same
|
|
11
|
+
* trust as that bundle once loaded (docs/security.md: "packs are
|
|
12
|
+
* user-installed/trusted"), but still validated structurally before use,
|
|
13
|
+
* because "trusted" does not mean "guaranteed well-formed": a buggy or
|
|
14
|
+
* half-built pack script must degrade the SAME way an unsupported engine or
|
|
15
|
+
* a missing component module already does in `@markii/react`'s `loadPack`
|
|
16
|
+
* (an empty contribution, never a crash), not bring down the rest of a
|
|
17
|
+
* preview.
|
|
18
|
+
*
|
|
19
|
+
* Host-neutral by design: this module takes an already-collected array of
|
|
20
|
+
* queued registrations as a plain parameter, and returns the outcome as
|
|
21
|
+
* data (never logs or throws), so a host wires up wherever those
|
|
22
|
+
* registrations came from — VS Code reads `window.__markiiPackRegistrations`
|
|
23
|
+
* off its webview and logs the result to its output channel; an Obsidian
|
|
24
|
+
* plugin evaluates each pack's compiled script in-process
|
|
25
|
+
* (`collectPackRegistrations`) and folds the result into its own
|
|
26
|
+
* diagnostics. Reading a host-specific global and deciding where to log a
|
|
27
|
+
* diagnostic line both stay in that host's own app code.
|
|
28
|
+
*/
|
|
29
|
+
import { detectNamespaceCollisions, parsePackManifest } from '@markii/pack';
|
|
30
|
+
function isPlainObject(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
function hasOwn(value, key) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
35
|
+
}
|
|
36
|
+
/** Structurally validates one `componentModules` object: every OWN entry has a function `component` and an optional boolean `inline`. Read with `Object.hasOwn` only — the same hostile-map discipline `@markii/pack`/`@markii/react` use throughout, so a `componentModules` object with a poisoned `__proto__` can't leak an inherited value in as a registered component. */
|
|
37
|
+
function isPackComponentModules(value) {
|
|
38
|
+
if (!isPlainObject(value))
|
|
39
|
+
return false;
|
|
40
|
+
for (const key of Object.keys(value)) {
|
|
41
|
+
if (!hasOwn(value, key))
|
|
42
|
+
continue;
|
|
43
|
+
const entry = value[key];
|
|
44
|
+
if (!isPlainObject(entry))
|
|
45
|
+
return false;
|
|
46
|
+
if (typeof entry.component !== 'function')
|
|
47
|
+
return false;
|
|
48
|
+
if (hasOwn(entry, 'inline') && typeof entry.inline !== 'boolean') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One queued entry, validated into a `PackToInstall`, or `undefined` if
|
|
56
|
+
* either half fails validation — the reason is returned rather than logged
|
|
57
|
+
* directly, so the caller can fold it into its own diagnostics the same way
|
|
58
|
+
* every other pack failure is reported.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately NOT exported, including from `../browser.ts`: every caller,
|
|
61
|
+
* VS Code's webview included, reaches this through `buildRenderRegistry`,
|
|
62
|
+
* which runs it per queued entry. A host reads its own registration queue
|
|
63
|
+
* off its own global and hands over the batch; validating that batch is
|
|
64
|
+
* this module's job, and keeping the per-entry step private means there is
|
|
65
|
+
* one entry point into it rather than two.
|
|
66
|
+
*/
|
|
67
|
+
function toPackToInstall(entry, index) {
|
|
68
|
+
if (typeof entry.manifestJson !== 'string') {
|
|
69
|
+
return {
|
|
70
|
+
invalidReason: `pack registration #${String(index)} did not provide a manifest JSON string; ignored.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const parsed = parsePackManifest(entry.manifestJson);
|
|
74
|
+
if (!parsed.ok) {
|
|
75
|
+
return {
|
|
76
|
+
invalidReason: `pack registration #${String(index)}'s manifest is invalid (${parsed.errors.join('; ')}); ignored.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (!isPackComponentModules(entry.componentModules)) {
|
|
80
|
+
return {
|
|
81
|
+
invalidReason: `pack "${parsed.manifest.name}"'s registered components are malformed; ignored.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const manifest = parsed.manifest;
|
|
85
|
+
return { pack: { manifest, componentModules: entry.componentModules } };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Loads every pack's own registry (`@markii/react`'s `loadPack`, which
|
|
89
|
+
* already gates on engine and skips an unsatisfied component) and merges
|
|
90
|
+
* them onto `base` left to right, but — unlike `@markii/react`'s ordinary
|
|
91
|
+
* `mergeRegistries` last-wins semantics — keeps the FIRST pack to claim a
|
|
92
|
+
* given composed directive name and skips any later pack that claims the
|
|
93
|
+
* same name, recording the skip as a `DuplicateComposedName`. `base`'s own
|
|
94
|
+
* entries are not part of this check: a pack's composed name overriding an
|
|
95
|
+
* unrelated `base` entry is ordinary last-wins behavior, the same as
|
|
96
|
+
* `installPacks` already gives it.
|
|
97
|
+
*/
|
|
98
|
+
function mergePacksKeepingFirstClaim(packs, base, engine) {
|
|
99
|
+
const merged = { ...base };
|
|
100
|
+
const owner = new Map();
|
|
101
|
+
const duplicateComposedNames = [];
|
|
102
|
+
const droppedEngines = [];
|
|
103
|
+
for (const pack of packs) {
|
|
104
|
+
const { registry: loaded, dropped } = engine.loadPack(pack.manifest, pack.componentModules);
|
|
105
|
+
if (dropped)
|
|
106
|
+
droppedEngines.push(dropped);
|
|
107
|
+
for (const composedName of Object.keys(loaded)) {
|
|
108
|
+
if (!Object.hasOwn(loaded, composedName))
|
|
109
|
+
continue;
|
|
110
|
+
const existingOwner = owner.get(composedName);
|
|
111
|
+
if (existingOwner !== undefined) {
|
|
112
|
+
duplicateComposedNames.push({
|
|
113
|
+
composedName,
|
|
114
|
+
keptPack: existingOwner,
|
|
115
|
+
skippedPack: pack.manifest.name,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
owner.set(composedName, pack.manifest.name);
|
|
120
|
+
merged[composedName] = loaded[composedName];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
registry: engine.createRegistry(merged),
|
|
125
|
+
duplicateComposedNames,
|
|
126
|
+
droppedEngines,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Builds the render registry: `defaultRegistry` merged with every
|
|
131
|
+
* successfully validated, non-colliding pack registration in `queued`.
|
|
132
|
+
* Never throws:
|
|
133
|
+
*
|
|
134
|
+
* - an individual malformed registration is dropped (reason recorded, never
|
|
135
|
+
* shown on the page);
|
|
136
|
+
* - a namespace collision between two loaded packs (the same pack name
|
|
137
|
+
* registered twice) rejects the WHOLE install (matching docs/packs.md's
|
|
138
|
+
* install-time rejection rule) and this function falls back to
|
|
139
|
+
* `defaultRegistry` alone;
|
|
140
|
+
* - a composed-directive-name collision between two DIFFERENTLY named packs
|
|
141
|
+
* (see `DuplicateComposedName`) drops only the later claimant's
|
|
142
|
+
* component, not the whole pack.
|
|
143
|
+
*/
|
|
144
|
+
export function buildRenderRegistry(queued, defaultRegistry, engine) {
|
|
145
|
+
const packs = [];
|
|
146
|
+
const invalidReasons = [];
|
|
147
|
+
queued.forEach((entry, index) => {
|
|
148
|
+
const { pack, invalidReason } = toPackToInstall(entry, index);
|
|
149
|
+
if (pack)
|
|
150
|
+
packs.push(pack);
|
|
151
|
+
if (invalidReason)
|
|
152
|
+
invalidReasons.push(invalidReason);
|
|
153
|
+
});
|
|
154
|
+
if (packs.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
registry: defaultRegistry,
|
|
157
|
+
invalidReasons,
|
|
158
|
+
collisions: [],
|
|
159
|
+
duplicateComposedNames: [],
|
|
160
|
+
droppedEngines: [],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const namespaces = packs.map((pack) => pack.manifest.name);
|
|
164
|
+
const namespaceCollisions = detectNamespaceCollisions(namespaces);
|
|
165
|
+
if (namespaceCollisions.length > 0) {
|
|
166
|
+
return {
|
|
167
|
+
registry: engine.mergeRegistries(engine.createRegistry(), defaultRegistry),
|
|
168
|
+
invalidReasons,
|
|
169
|
+
collisions: namespaceCollisions.map((collision) => collision.namespace),
|
|
170
|
+
duplicateComposedNames: [],
|
|
171
|
+
droppedEngines: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const { registry, duplicateComposedNames, droppedEngines } = mergePacksKeepingFirstClaim(packs, defaultRegistry, engine);
|
|
175
|
+
return {
|
|
176
|
+
registry,
|
|
177
|
+
invalidReasons,
|
|
178
|
+
collisions: [],
|
|
179
|
+
duplicateComposedNames,
|
|
180
|
+
droppedEngines,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DiscoveredPack } from './discover.js';
|
|
2
|
+
import type { PackModulesMap } from '../run/lua-resolver.js';
|
|
3
|
+
export interface PackScriptsReader {
|
|
4
|
+
/** Lists one directory's immediate entries, or `[]` if it does not exist / cannot be read (never throws — a pack with no `scripts/` folder is the ordinary case). */
|
|
5
|
+
readDirectory(absoluteDir: string): Promise<ReadonlyArray<{
|
|
6
|
+
name: string;
|
|
7
|
+
isDirectory: boolean;
|
|
8
|
+
}>>;
|
|
9
|
+
/** Reads one file's UTF-8 text, or `undefined` if it cannot be read. */
|
|
10
|
+
readFile(absolutePath: string): Promise<string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds the full `PackModulesMap` for `packs`: every discovered pack's
|
|
14
|
+
* namespace mapped to its `scripts/*.lua` files, keyed by their
|
|
15
|
+
* jail-normalized relative path. A pack with no `scripts/` directory (or an
|
|
16
|
+
* unreadable one) simply contributes an empty module map — a
|
|
17
|
+
* `require "thatPack/anything"` then resolves as an ordinary "no such
|
|
18
|
+
* module", never a crash.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadPackModules(packs: readonly DiscoveredPack[], reader: PackScriptsReader): Promise<PackModulesMap>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side, UI-free loader that reads every pack's `scripts/*.lua` files
|
|
3
|
+
* ahead of a run, producing the `PackModulesMap` `./run/lua-resolver.ts`'s
|
|
4
|
+
* `createPackModuleResolver` looks up synchronously inside the Run worker —
|
|
5
|
+
* see that module's doc comment for why the reading happens here, on the
|
|
6
|
+
* host, rather than inside the worker.
|
|
7
|
+
*
|
|
8
|
+
* Filesystem access is injected (`PackScriptsReader`) so this module is
|
|
9
|
+
* testable with an in-memory fake; `createNodeReader` in `../node-fs.ts`
|
|
10
|
+
* (exported from `@markii/host/node`) is the real `node:fs/promises`-backed
|
|
11
|
+
* implementation a Node host's pack-loading composition wires up.
|
|
12
|
+
*/
|
|
13
|
+
import { joinPath } from '../path-util.js';
|
|
14
|
+
import { normalizeBundlePath } from '@markii/bundle';
|
|
15
|
+
/** A sane upper bound on how many `.lua` files one pack's `scripts/` tree may contribute — defense in depth against a pathological pack folder (a symlink cycle a directory walk would otherwise never terminate on, or simply an absurd number of files) turning "load the installed packs" into an unbounded scan. */
|
|
16
|
+
const MAX_SCRIPT_FILES_PER_PACK = 500;
|
|
17
|
+
/** Recursion depth cap for the same reason — a plain bound alongside the file-count cap, independent of it. */
|
|
18
|
+
const MAX_SCRIPT_DIR_DEPTH = 16;
|
|
19
|
+
/**
|
|
20
|
+
* A per-file size cap on a pack's `scripts/*.lua` (H-2, pass-3 pentest report
|
|
21
|
+
* section 10.2). Packs are user-installed, trusted folders, so this is
|
|
22
|
+
* defense in depth rather than a trust boundary — but it matches the posture
|
|
23
|
+
* `buildBundleSnapshot` already takes for bundle files: a single file's whole
|
|
24
|
+
* text is read into the host process and then structured-cloned into the
|
|
25
|
+
* Run worker's job, so an absurdly large `.lua` (accidental or hostile) would
|
|
26
|
+
* otherwise pull an unbounded amount into host memory. A file over this cap
|
|
27
|
+
* is skipped exactly like an unreadable one — `require`-ing it then resolves
|
|
28
|
+
* as an ordinary "no such module", never a crash. 1 MB is far above any real
|
|
29
|
+
* shared Lua module while still bounding a pathological one.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_SCRIPT_FILE_BYTES = 1_000_000;
|
|
32
|
+
/**
|
|
33
|
+
* Walks `pack.scriptsDir` (relative to it) collecting every `.lua` file's
|
|
34
|
+
* bundle-jail-normalized relative path -> source text. A relative path the
|
|
35
|
+
* jail rejects (should not arise from a real directory listing, but this is
|
|
36
|
+
* defense in depth, matching every other filesystem-adjacent module in this
|
|
37
|
+
* codebase) is silently skipped rather than throwing — same cleanliness
|
|
38
|
+
* posture as `./discover.ts`. An unreadable individual file is skipped the
|
|
39
|
+
* same way.
|
|
40
|
+
*/
|
|
41
|
+
async function collectPackModules(pack, reader) {
|
|
42
|
+
const result = Object.create(null);
|
|
43
|
+
let fileCount = 0;
|
|
44
|
+
async function walk(relativeDir, depth) {
|
|
45
|
+
if (depth > MAX_SCRIPT_DIR_DEPTH)
|
|
46
|
+
return;
|
|
47
|
+
const absoluteDir = relativeDir
|
|
48
|
+
? joinPath(pack.scriptsDir, relativeDir)
|
|
49
|
+
: pack.scriptsDir;
|
|
50
|
+
const entries = await reader.readDirectory(absoluteDir);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (fileCount >= MAX_SCRIPT_FILES_PER_PACK)
|
|
53
|
+
return;
|
|
54
|
+
const relativePath = relativeDir
|
|
55
|
+
? `${relativeDir}/${entry.name}`
|
|
56
|
+
: entry.name;
|
|
57
|
+
if (entry.isDirectory) {
|
|
58
|
+
await walk(relativePath, depth + 1);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!entry.name.endsWith('.lua'))
|
|
62
|
+
continue;
|
|
63
|
+
const normalized = normalizeBundlePath(relativePath);
|
|
64
|
+
if (!normalized.ok)
|
|
65
|
+
continue;
|
|
66
|
+
const absolutePath = joinPath(pack.scriptsDir, relativePath);
|
|
67
|
+
const source = await reader.readFile(absolutePath);
|
|
68
|
+
if (source === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
// H-2: skip a file whose UTF-8 size exceeds the per-file cap, same as an
|
|
71
|
+
// unreadable one. `TextEncoder` measures the encoded bytes (what
|
|
72
|
+
// actually crosses into the worker), not the JS string's UTF-16 length.
|
|
73
|
+
if (new TextEncoder().encode(source).length > MAX_SCRIPT_FILE_BYTES)
|
|
74
|
+
continue;
|
|
75
|
+
result[normalized.path] = source;
|
|
76
|
+
fileCount += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await walk('', 0);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds the full `PackModulesMap` for `packs`: every discovered pack's
|
|
84
|
+
* namespace mapped to its `scripts/*.lua` files, keyed by their
|
|
85
|
+
* jail-normalized relative path. A pack with no `scripts/` directory (or an
|
|
86
|
+
* unreadable one) simply contributes an empty module map — a
|
|
87
|
+
* `require "thatPack/anything"` then resolves as an ordinary "no such
|
|
88
|
+
* module", never a crash.
|
|
89
|
+
*/
|
|
90
|
+
export async function loadPackModules(packs, reader) {
|
|
91
|
+
const map = Object.create(null);
|
|
92
|
+
for (const pack of packs) {
|
|
93
|
+
map[pack.manifest.name] = await collectPackModules(pack, reader);
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|