@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.
Files changed (192) hide show
  1. package/README.md +18 -2
  2. package/dist/browser-worker.js +639 -0
  3. package/dist/browser.d.ts +57 -0
  4. package/dist/browser.js +167 -0
  5. package/dist/conformance/host/01-plain-open/expected.json +5 -0
  6. package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
  7. package/dist/conformance/host/01-plain-open/scenario.json +6 -0
  8. package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
  9. package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
  10. package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
  11. package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
  12. package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
  13. package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
  14. package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
  15. package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
  16. package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
  17. package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
  18. package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
  19. package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
  20. package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
  21. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
  22. package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
  23. package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
  24. package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
  25. package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
  26. package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
  27. package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
  28. package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
  29. package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
  30. package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
  31. package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
  32. package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
  33. package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
  34. package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
  35. package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
  36. package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
  37. package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
  38. package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
  39. package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
  40. package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
  41. package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
  42. package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
  43. package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
  44. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
  45. package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
  46. package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
  47. package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
  48. package/dist/conformance/host/14-remove-pack/expected.json +14 -0
  49. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
  50. package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
  51. package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
  52. package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
  53. package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
  54. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
  55. package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
  56. package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
  57. package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
  58. package/dist/conformance/host/16-pack-trust/expected.json +37 -0
  59. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
  60. package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
  61. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
  62. package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
  63. package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
  64. package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
  65. package/dist/conformance/host/README.md +221 -0
  66. package/dist/conformance.d.ts +2 -0
  67. package/dist/conformance.js +5 -0
  68. package/dist/diagnostics/render-diagnostics.d.ts +41 -0
  69. package/dist/diagnostics/render-diagnostics.js +46 -0
  70. package/dist/export/cascade.d.ts +98 -0
  71. package/dist/export/cascade.js +159 -0
  72. package/dist/export/export-zip.d.ts +19 -0
  73. package/dist/export/export-zip.js +38 -0
  74. package/dist/export/image-embed.d.ts +126 -0
  75. package/dist/export/image-embed.js +274 -0
  76. package/dist/export/note-export.d.ts +332 -0
  77. package/dist/export/note-export.js +366 -0
  78. package/dist/export/note-links.d.ts +83 -0
  79. package/dist/export/note-links.js +241 -0
  80. package/dist/glue.wasm +0 -0
  81. package/dist/host/adapter.d.ts +223 -0
  82. package/dist/host/adapter.js +9 -0
  83. package/dist/host/cascade-export-behavior.d.ts +120 -0
  84. package/dist/host/cascade-export-behavior.js +151 -0
  85. package/dist/host/conformance-runner.d.ts +156 -0
  86. package/dist/host/conformance-runner.js +634 -0
  87. package/dist/host/create-host.d.ts +253 -0
  88. package/dist/host/create-host.js +329 -0
  89. package/dist/host/editor-behavior.d.ts +157 -0
  90. package/dist/host/editor-behavior.js +227 -0
  91. package/dist/host/export/md-plain.d.ts +15 -0
  92. package/dist/host/export/md-plain.js +125 -0
  93. package/dist/host/export-behavior.d.ts +113 -0
  94. package/dist/host/export-behavior.js +257 -0
  95. package/dist/host/labels.d.ts +24 -0
  96. package/dist/host/labels.js +32 -0
  97. package/dist/host/pack-archive.d.ts +30 -0
  98. package/dist/host/pack-archive.js +68 -0
  99. package/dist/host/pack-install.d.ts +88 -0
  100. package/dist/host/pack-install.js +189 -0
  101. package/dist/host/pack-load.d.ts +72 -0
  102. package/dist/host/pack-load.js +57 -0
  103. package/dist/host/pack-remove.d.ts +21 -0
  104. package/dist/host/pack-remove.js +65 -0
  105. package/dist/host/pack-trust-behavior.d.ts +79 -0
  106. package/dist/host/pack-trust-behavior.js +262 -0
  107. package/dist/host/refresh-interval.d.ts +39 -0
  108. package/dist/host/refresh-interval.js +61 -0
  109. package/dist/host/run-behavior.d.ts +89 -0
  110. package/dist/host/run-behavior.js +168 -0
  111. package/dist/host/run-diagnostics.d.ts +26 -0
  112. package/dist/host/run-diagnostics.js +43 -0
  113. package/dist/host/script-execution.d.ts +72 -0
  114. package/dist/host/script-execution.js +68 -0
  115. package/dist/host/terminal-packs.d.ts +39 -0
  116. package/dist/host/terminal-packs.js +51 -0
  117. package/dist/html-engine.d.ts +2 -0
  118. package/dist/html-engine.js +29 -0
  119. package/dist/index.d.ts +95 -0
  120. package/dist/index.js +70 -0
  121. package/dist/insert/component-catalog.d.ts +23 -0
  122. package/dist/insert/component-catalog.js +96 -0
  123. package/dist/node-fs.d.ts +14 -0
  124. package/dist/node-fs.js +70 -0
  125. package/dist/node.d.ts +5 -0
  126. package/dist/node.js +13 -0
  127. package/dist/package.json +3 -0
  128. package/dist/packs/discover.d.ts +119 -0
  129. package/dist/packs/discover.js +266 -0
  130. package/dist/packs/pack-diagnostics.d.ts +81 -0
  131. package/dist/packs/pack-diagnostics.js +105 -0
  132. package/dist/packs/pack-paths.d.ts +53 -0
  133. package/dist/packs/pack-paths.js +106 -0
  134. package/dist/packs/pack-render-registry.d.ts +83 -0
  135. package/dist/packs/pack-render-registry.js +182 -0
  136. package/dist/packs/pack-scripts.d.ts +20 -0
  137. package/dist/packs/pack-scripts.js +96 -0
  138. package/dist/packs/pack-trust.d.ts +57 -0
  139. package/dist/packs/pack-trust.js +111 -0
  140. package/dist/packs/prebuilt.d.ts +49 -0
  141. package/dist/packs/prebuilt.js +97 -0
  142. package/dist/path-util.d.ts +24 -0
  143. package/dist/path-util.js +82 -0
  144. package/dist/run/browser-isolate.d.ts +45 -0
  145. package/dist/run/browser-isolate.js +81 -0
  146. package/dist/run/bundle-document.d.ts +29 -0
  147. package/dist/run/bundle-document.js +40 -0
  148. package/dist/run/bundle-run.d.ts +188 -0
  149. package/dist/run/bundle-run.js +232 -0
  150. package/dist/run/grant-flow.d.ts +232 -0
  151. package/dist/run/grant-flow.js +390 -0
  152. package/dist/run/ip-address.d.ts +41 -0
  153. package/dist/run/ip-address.js +255 -0
  154. package/dist/run/isolate.d.ts +57 -0
  155. package/dist/run/isolate.js +22 -0
  156. package/dist/run/lua-resolver.d.ts +21 -0
  157. package/dist/run/lua-resolver.js +63 -0
  158. package/dist/run/net-bridge-worker.d.ts +26 -0
  159. package/dist/run/net-bridge-worker.js +66 -0
  160. package/dist/run/net-bridge.d.ts +56 -0
  161. package/dist/run/net-bridge.js +59 -0
  162. package/dist/run/net-pinning.d.ts +82 -0
  163. package/dist/run/net-pinning.js +137 -0
  164. package/dist/run/net-provider.d.ts +70 -0
  165. package/dist/run/net-provider.js +289 -0
  166. package/dist/run/node-isolate.d.ts +30 -0
  167. package/dist/run/node-isolate.js +117 -0
  168. package/dist/run/run-flow.d.ts +187 -0
  169. package/dist/run/run-flow.js +323 -0
  170. package/dist/run/run-host.d.ts +107 -0
  171. package/dist/run/run-host.js +241 -0
  172. package/dist/run/run-job.d.ts +130 -0
  173. package/dist/run/run-job.js +279 -0
  174. package/dist/run/run-progress.d.ts +55 -0
  175. package/dist/run/run-progress.js +42 -0
  176. package/dist/run/run-trace.d.ts +44 -0
  177. package/dist/run/run-trace.js +36 -0
  178. package/dist/run/script-requirements.d.ts +98 -0
  179. package/dist/run/script-requirements.js +212 -0
  180. package/dist/run/snapshot-storage.d.ts +25 -0
  181. package/dist/run/snapshot-storage.js +31 -0
  182. package/dist/run/stale-values.d.ts +25 -0
  183. package/dist/run/stale-values.js +32 -0
  184. package/dist/run/worker-entry-browser.d.ts +42 -0
  185. package/dist/run/worker-entry-browser.js +77 -0
  186. package/dist/run/worker-entry.d.ts +1 -0
  187. package/dist/run/worker-entry.js +96 -0
  188. package/dist/values-failure.d.ts +17 -0
  189. package/dist/values-failure.js +1 -0
  190. package/dist/values-merge.d.ts +39 -0
  191. package/dist/values-merge.js +33 -0
  192. 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
+ }