@markii/host 0.17.0-next.2 → 0.17.0-next.4
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 +10 -2
- package/dist/browser-worker.classic.js +640 -0
- package/dist/browser-worker.js +40 -40
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +16 -4
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +2 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +6 -1
- package/dist/conformance/host/README.md +4 -2
- package/dist/export/note-export.d.ts +1 -1
- package/dist/host/adapter.d.ts +22 -25
- package/dist/host/conformance-runner.d.ts +1 -1
- package/dist/host/conformance-runner.js +21 -11
- package/dist/host/create-host.d.ts +29 -11
- package/dist/host/create-host.js +34 -23
- package/dist/host/editor-behavior.d.ts +2 -6
- package/dist/host/editor-behavior.js +5 -9
- package/dist/host/export-behavior.d.ts +6 -7
- package/dist/host/export-behavior.js +1 -1
- package/dist/host/labels.d.ts +5 -15
- package/dist/host/labels.js +8 -22
- package/dist/host/pack-archive.d.ts +4 -10
- package/dist/host/pack-archive.js +12 -21
- package/dist/host/pack-install.d.ts +11 -23
- package/dist/host/pack-install.js +26 -23
- package/dist/host/pack-load.d.ts +1 -2
- package/dist/host/pack-trust-behavior.js +1 -1
- package/dist/host/refresh-interval.d.ts +1 -3
- package/dist/host/refresh-interval.js +1 -3
- package/dist/host/run-behavior.d.ts +3 -4
- package/dist/host/run-behavior.js +23 -2
- package/dist/host/script-execution.d.ts +1 -1
- package/dist/html-engine.js +4 -4
- package/dist/index.d.ts +15 -80
- package/dist/index.js +18 -63
- package/dist/internal.d.ts +30 -0
- package/dist/internal.js +31 -0
- package/dist/node-fs.d.ts +0 -3
- package/dist/node-fs.js +1 -21
- package/dist/node-worker.d.ts +8 -0
- package/dist/node-worker.js +20678 -0
- package/dist/node.d.ts +3 -3
- package/dist/node.js +5 -4
- package/dist/packs/discover.d.ts +2 -0
- package/dist/packs/discover.js +2 -1
- package/dist/packs/pack-diagnostics.d.ts +6 -8
- package/dist/packs/pack-diagnostics.js +6 -8
- package/dist/packs/pack-paths.js +1 -2
- package/dist/packs/pack-render-registry.d.ts +9 -10
- package/dist/run/browser-isolate.d.ts +5 -5
- package/dist/run/browser-isolate.js +5 -1
- package/dist/run/isolate.d.ts +3 -1
- package/dist/run/net-bridge.d.ts +5 -5
- package/dist/run/net-bridge.js +7 -5
- package/dist/run/net-pinning.d.ts +2 -0
- package/dist/run/net-pinning.js +17 -3
- package/dist/run/net-provider.d.ts +3 -1
- package/dist/run/net-provider.js +40 -15
- package/dist/run/net-refusal.d.ts +4 -0
- package/dist/run/net-refusal.js +24 -0
- package/dist/run/node-isolate.d.ts +13 -21
- package/dist/run/node-isolate.js +31 -80
- package/dist/run/run-flow.d.ts +1 -2
- package/dist/run/run-host.d.ts +3 -3
- package/dist/run/worker-entry-browser.d.ts +24 -4
- package/dist/run/worker-entry-browser.js +99 -4
- package/dist/run/worker-entry.js +2 -0
- package/package.json +31 -12
package/dist/browser.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { NetProvider } from '@markii/lua';
|
|
2
2
|
import type { HostIsolate } from './host/adapter.js';
|
|
3
3
|
/** Builds the provider for one run from the job's host allowlist, byte cap and pin policy (the policy is meaningless in a browser and ignored). */
|
|
4
|
-
export type BrowserNetProviderFactory = (netAllowlist: string[], maxFetchBytes: number, netPolicy: unknown
|
|
4
|
+
export type BrowserNetProviderFactory = (netAllowlist: string[], maxFetchBytes: number, netPolicy: unknown,
|
|
5
|
+
/** Aborted when the isolate is killed or disposed. A provider cancels its in-flight requests on it. */
|
|
6
|
+
signal?: AbortSignal) => NetProvider;
|
|
5
7
|
export interface FetchNetProviderOptions {
|
|
6
8
|
/**
|
|
7
9
|
* The origins (`https://api.example.com`, `http://127.0.0.1:8080`) a
|
|
@@ -9,7 +11,7 @@ export interface FetchNetProviderOptions {
|
|
|
9
11
|
* every request. Entries that do not parse as an http(s) URL are ignored.
|
|
10
12
|
*/
|
|
11
13
|
allowedOrigins?: readonly string[];
|
|
12
|
-
/** How a policy refusal is built. Defaults to a plain `Error
|
|
14
|
+
/** How a policy refusal is built. Defaults to a plain `Error`. The provider marks every refusal it throws, so the isolate bridge sends it as a denial. A transport failure is never marked. */
|
|
13
15
|
denial?: (message: string) => Error;
|
|
14
16
|
/** The `fetch` to use. Defaults to the global one; injectable for tests. */
|
|
15
17
|
fetch?: typeof fetch;
|
|
@@ -33,7 +35,9 @@ export interface FetchNetProviderOptions {
|
|
|
33
35
|
* manual redirect's target, so it cannot be checked against the allowlist,
|
|
34
36
|
* and a redirect is therefore a denial.
|
|
35
37
|
*
|
|
36
|
-
* Returns a factory because the byte cap arrives per job.
|
|
38
|
+
* Returns a factory because the byte cap arrives per job. Every request
|
|
39
|
+
* carries the run's `AbortSignal`, so killing the isolate cancels requests
|
|
40
|
+
* still in flight.
|
|
37
41
|
*/
|
|
38
42
|
export declare function createFetchNetProvider(options?: FetchNetProviderOptions): BrowserNetProviderFactory;
|
|
39
43
|
export interface BrowserHostIsolateOptions {
|
package/dist/browser.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// `new URL(..., import.meta.url)` references below resolve in a consumer's
|
|
15
15
|
// bundler without configuration where that bundler treats them as assets.
|
|
16
16
|
import { createBrowserIsolate } from './run/browser-isolate.js';
|
|
17
|
+
import { netRefusal } from './run/net-refusal.js';
|
|
17
18
|
function originOf(entry) {
|
|
18
19
|
try {
|
|
19
20
|
const url = new URL(entry);
|
|
@@ -74,14 +75,19 @@ async function readCapped(response, maxBytes, denial) {
|
|
|
74
75
|
* manual redirect's target, so it cannot be checked against the allowlist,
|
|
75
76
|
* and a redirect is therefore a denial.
|
|
76
77
|
*
|
|
77
|
-
* Returns a factory because the byte cap arrives per job.
|
|
78
|
+
* Returns a factory because the byte cap arrives per job. Every request
|
|
79
|
+
* carries the run's `AbortSignal`, so killing the isolate cancels requests
|
|
80
|
+
* still in flight.
|
|
78
81
|
*/
|
|
79
82
|
export function createFetchNetProvider(options = {}) {
|
|
80
83
|
const allowed = new Set((options.allowedOrigins ?? [])
|
|
81
84
|
.map(originOf)
|
|
82
85
|
.filter((origin) => origin !== undefined));
|
|
83
|
-
const
|
|
84
|
-
|
|
86
|
+
const build = options.denial ?? ((message) => new Error(message));
|
|
87
|
+
// Every policy refusal is marked so the net bridge can tell it from a
|
|
88
|
+
// transport failure (a plain `Error`).
|
|
89
|
+
const denial = (message) => netRefusal(build(message));
|
|
90
|
+
return (netAllowlist, maxFetchBytes, _policy, signal) => {
|
|
85
91
|
// The run's granted hosts, re-checked here on the page side: the worker
|
|
86
92
|
// checks them too, but the worker is the isolate a hostile script runs
|
|
87
93
|
// in, so the page is the boundary (the same rule the Node provider
|
|
@@ -107,6 +113,8 @@ export function createFetchNetProvider(options = {}) {
|
|
|
107
113
|
if (!allowed.has(url.origin)) {
|
|
108
114
|
throw denial(`origin "${url.origin}" is not on the host's allowlist`);
|
|
109
115
|
}
|
|
116
|
+
if (signal?.aborted)
|
|
117
|
+
throw new Error('the run has ended');
|
|
110
118
|
const doFetch = options.fetch ?? fetch;
|
|
111
119
|
let response;
|
|
112
120
|
try {
|
|
@@ -118,10 +126,14 @@ export function createFetchNetProvider(options = {}) {
|
|
|
118
126
|
redirect: 'manual',
|
|
119
127
|
credentials: 'omit',
|
|
120
128
|
referrerPolicy: 'no-referrer',
|
|
129
|
+
...(signal !== undefined ? { signal } : {}),
|
|
121
130
|
});
|
|
122
131
|
}
|
|
123
132
|
catch (error) {
|
|
124
|
-
|
|
133
|
+
// A transport failure (network down, connection refused, CORS,
|
|
134
|
+
// abort): an ordinary runtime failure with the reason, not a policy
|
|
135
|
+
// refusal.
|
|
136
|
+
throw new Error(`request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
137
|
}
|
|
126
138
|
if (response.type === 'opaqueredirect') {
|
|
127
139
|
throw denial('redirects are not followed in a browser');
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"outcomes": [
|
|
3
3
|
{ "kind": "opened" },
|
|
4
|
-
{
|
|
4
|
+
{
|
|
5
|
+
"kind": "installed",
|
|
6
|
+
"namespace": "ana",
|
|
7
|
+
"replaced": false,
|
|
8
|
+
"installedDirName": "ana"
|
|
9
|
+
},
|
|
5
10
|
{ "kind": "loaded", "namespaces": ["ana"] },
|
|
6
11
|
{ "kind": "installed", "namespace": "ana", "replaced": true }
|
|
7
12
|
],
|
|
@@ -138,8 +138,10 @@ coarse, matching each `kind`:
|
|
|
138
138
|
compares the recorded run trace's `ok` flag.
|
|
139
139
|
- `exported`: `contains`/`excludes` are substrings checked against the
|
|
140
140
|
written file's content, never the whole document — the render corpora
|
|
141
|
-
already pin bytes.
|
|
142
|
-
|
|
141
|
+
already pin bytes. `valueCount` is how many last-run values were baked
|
|
142
|
+
in, and `renderEngine` (`static` or `react`) is which engine drew the body.
|
|
143
|
+
- `installed`: `namespace` and `replaced`, and `installedDirName`, the name
|
|
144
|
+
of the folder the pack was unzipped into.
|
|
143
145
|
- `declined`: `step` (`'consent'`/`'replace'`, or absent for the
|
|
144
146
|
scripts-disabled synthetic outcome) and `reasonContains`, a substring of
|
|
145
147
|
the user-facing reason.
|
|
@@ -71,7 +71,7 @@ export declare const EXPORT_HIDE_SCRIPT_BLOCKS_CLASS = "mk-export--hide-scripts"
|
|
|
71
71
|
export declare const EXPORT_PAGE_CSS = "\nbody {\n margin: 0;\n background: #fff;\n}\n.doc {\n box-sizing: border-box;\n max-width: 46rem;\n margin: 0 auto;\n padding: 2.5rem 1.5rem 4rem;\n}\n.doc.mk-export--hide-scripts .mk-script {\n display: none;\n}\n@page {\n margin: 2cm;\n}\n@media print {\n .doc {\n max-width: none;\n margin: 0;\n padding: 0;\n }\n .doc .mk-card,\n .doc .mk-callout,\n .doc .mk-stat,\n .doc .mk-figure,\n .doc .mk-unknown,\n .doc table {\n break-inside: avoid;\n }\n /* A collapsed `details` prints as its summary alone, which loses the\n content the author wrote. On paper there is nothing to expand, so\n every disclosure prints open. */\n .doc details > *:not(summary) {\n display: block;\n }\n .doc .mk-script,\n .doc .mk-preview__run-marker {\n display: none;\n }\n}\n";
|
|
72
72
|
/**
|
|
73
73
|
* The static HTML engine, injected so this module imports no renderer.
|
|
74
|
-
* A host passes `createHtmlExportEngine()` from `@markii/host/
|
|
74
|
+
* A host passes `createHtmlExportEngine()` from `@markii/host/html-engine`, which
|
|
75
75
|
* wraps `@markii/html`; a test may pass a stub.
|
|
76
76
|
*/
|
|
77
77
|
export interface HtmlExportEngine {
|
package/dist/host/adapter.d.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* the CLI, or a future one) provides so `createMarkiiHost` can run every
|
|
4
4
|
* shared behavior once. Nothing in this file names a `vscode`, `obsidian`,
|
|
5
5
|
* Electron or TTY type — an adapter is built from injected dependencies,
|
|
6
|
-
* so a test can construct the REAL adapter over fakes
|
|
7
|
-
* `tmp/W11-adapter-design.md`, section 2).
|
|
6
|
+
* so a test can construct the REAL adapter over fakes.
|
|
8
7
|
*
|
|
9
8
|
* A capability group (`isolate`, `editor`, `packs`, `exports`) is absent
|
|
10
9
|
* when a host does not offer it. `createMarkiiHost` never assumes one is
|
|
@@ -23,9 +22,7 @@ export interface HostDirEntry {
|
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
25
24
|
* One yes/no question. Every grant, consent, and replace prompt in the
|
|
26
|
-
* whole app surface routes through this single method
|
|
27
|
-
* nine near-identical prompt adapters across three apps collapse to one
|
|
28
|
-
* call each). `message`, `allowLabel`, and `denyLabel` are already fully
|
|
25
|
+
* whole app surface routes through this single method. `message`, `allowLabel`, and `denyLabel` are already fully
|
|
29
26
|
* built by `@markii/host`; the adapter only has to show them.
|
|
30
27
|
*/
|
|
31
28
|
export interface HostPromptRequest {
|
|
@@ -43,7 +40,7 @@ export interface HostPromptRequest {
|
|
|
43
40
|
}
|
|
44
41
|
/**
|
|
45
42
|
* A discriminated union: `workerPath` and `workerBytes` are mutually
|
|
46
|
-
* exclusive per host
|
|
43
|
+
* exclusive per host. A Node host (VS Code, the CLI)
|
|
47
44
|
* gives a synchronous path lookup; a host whose runtime cannot create
|
|
48
45
|
* worker threads (an Electron renderer, which is what the Obsidian plugin
|
|
49
46
|
* runs in) gives an async, disposable Web Worker spawner instead.
|
|
@@ -51,15 +48,14 @@ export interface HostPromptRequest {
|
|
|
51
48
|
export type HostIsolate = {
|
|
52
49
|
readonly kind: 'node';
|
|
53
50
|
/**
|
|
54
|
-
* The worker entry on disk. A host with
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* adapter (the shared run path has no Node dependency to do it).
|
|
51
|
+
* The worker entry on disk. A host with its own bundled worker
|
|
52
|
+
* resolves it here. `createNodeHostIsolate()` from
|
|
53
|
+
* `@markii/host/node` supplies the packaged one.
|
|
58
54
|
*/
|
|
59
55
|
workerPath(): string;
|
|
60
56
|
/**
|
|
61
|
-
* Creates the worker-thread isolate
|
|
62
|
-
*
|
|
57
|
+
* Creates the worker-thread isolate: `workerThreadIsolate` from
|
|
58
|
+
* `@markii/host/node`. Injected so the shared run path imports
|
|
63
59
|
* nothing from `node:*`.
|
|
64
60
|
*/
|
|
65
61
|
readonly spawnIsolate: IsolateSpawner;
|
|
@@ -106,7 +102,7 @@ export interface HostEditor {
|
|
|
106
102
|
}
|
|
107
103
|
/**
|
|
108
104
|
* Pack POLICY (which folders are even candidates) is irreducibly
|
|
109
|
-
* host-specific
|
|
105
|
+
* host-specific; everything after the list exists is
|
|
110
106
|
* already shared in `../packs/discover.ts` and friends. The adapter
|
|
111
107
|
* supplies only the list and the install destination.
|
|
112
108
|
*/
|
|
@@ -124,12 +120,13 @@ export interface HostPackSource {
|
|
|
124
120
|
* `'device-list'`: a folder in `installRoot` loads only when the trust
|
|
125
121
|
* list in the adapter's `memento` names its namespace, and the host
|
|
126
122
|
* gains `installedPacks`, `enablePack` and `disablePack`. An install
|
|
127
|
-
* trusts the pack it installs. Requires `installRoot`. `'none'
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
123
|
+
* trusts the pack it installs. Requires `installRoot`. `'none'`: every
|
|
124
|
+
* folder this host's own policy authorizes loads, which is right for a
|
|
125
|
+
* host that trusts its own install store (VS Code, the CLI). A web host
|
|
126
|
+
* that installs packs from users picks `'device-list'`. Required: a
|
|
127
|
+
* host states its choice explicitly.
|
|
131
128
|
*/
|
|
132
|
-
readonly trust
|
|
129
|
+
readonly trust: 'device-list' | 'none';
|
|
133
130
|
/**
|
|
134
131
|
* Folders this host currently AUTHORIZES. Under `trust: 'device-list'`
|
|
135
132
|
* these are the folders beyond the install root (the host adds the
|
|
@@ -153,7 +150,7 @@ export interface HostPackSource {
|
|
|
153
150
|
* `authorizedFolders` names.
|
|
154
151
|
*/
|
|
155
152
|
bundledPacks?(): readonly DiscoveredPack[];
|
|
156
|
-
/** Removes an installed pack folder. Required when `installRoot` is present. */
|
|
153
|
+
/** Removes an installed pack folder and everything under it. Required when `installRoot` is present: a replacing install clears the old folder through it, and removing a pack uses it. */
|
|
157
154
|
removeFolder?(path: string): Promise<void>;
|
|
158
155
|
}
|
|
159
156
|
/** Every export format any host offers today. */
|
|
@@ -167,10 +164,8 @@ export interface HostExportCapabilities {
|
|
|
167
164
|
}
|
|
168
165
|
/**
|
|
169
166
|
* The wording nouns every shared behavior's message templates interpolate
|
|
170
|
-
* (`./labels.ts`).
|
|
171
|
-
*
|
|
172
|
-
* template plus these per-host nouns, instead of a per-host
|
|
173
|
-
* reimplementation of the sentence itself.
|
|
167
|
+
* (`./labels.ts`). One template plus these per-host nouns
|
|
168
|
+
* replaces a per-host copy of each sentence.
|
|
174
169
|
*/
|
|
175
170
|
export interface HostLabels {
|
|
176
171
|
/** "extension" | "plugin" | "command line tool" */
|
|
@@ -196,13 +191,15 @@ export interface HostLabels {
|
|
|
196
191
|
* Everything an adapter provides. An adapter is constructible from
|
|
197
192
|
* injected dependencies (no module-scope global reads), which is what
|
|
198
193
|
* lets a conformance test build the REAL adapter over a fake editor
|
|
199
|
-
|
|
194
|
+
*.
|
|
200
195
|
*/
|
|
201
196
|
export interface HostAdapter {
|
|
202
|
-
|
|
197
|
+
/** A short name for the host, used in diagnostics wording. */
|
|
198
|
+
readonly id: string;
|
|
203
199
|
readonly labels: HostLabels;
|
|
204
200
|
readFile(path: string): Promise<Uint8Array>;
|
|
205
201
|
exists(path: string): Promise<boolean>;
|
|
202
|
+
/** Writes `bytes` to `path` and creates any missing parent folder first. Exports and pack installs both write through it. */
|
|
206
203
|
writeFile(path: string, bytes: Uint8Array): Promise<void>;
|
|
207
204
|
/** A missing folder resolves to an empty list — never throws, matching the existing `PackDirectoryLister` contract. */
|
|
208
205
|
listFolder(path: string): Promise<readonly HostDirEntry[]>;
|
|
@@ -110,7 +110,7 @@ export declare function hostDeclaresCapability(adapter: HostAdapter, action: Hos
|
|
|
110
110
|
* `next()` THROWS once exhausted rather than returning a default: an
|
|
111
111
|
* unanswered prompt means the scenario and the implementation disagree
|
|
112
112
|
* about how many questions get asked, which must never be silently read
|
|
113
|
-
* as a deny
|
|
113
|
+
* as a deny.
|
|
114
114
|
*/
|
|
115
115
|
export declare function createAnswerQueue(scenario: HostScenario): {
|
|
116
116
|
next(): boolean;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Executes one `conformance/host/<scenario>` case
|
|
3
|
-
* `tmp/W11-corpus-design.md`) against any real `HostAdapter`, so a single
|
|
2
|
+
* Executes one `conformance/host/<scenario>` case against any real `HostAdapter`, so a single
|
|
4
3
|
* plain-data corpus proves the three apps' adapters behave the same.
|
|
5
4
|
*
|
|
6
5
|
* `conformance/host/` itself stays plain data (no TypeScript, matching the
|
|
@@ -26,8 +25,7 @@
|
|
|
26
25
|
* pre-check here, over the same shared functions and the SAME
|
|
27
26
|
* `adapter.labels` a real call site would use, so the scenario proves
|
|
28
27
|
* wording parity across hosts without inventing a capability the contract
|
|
29
|
-
* does not have.
|
|
30
|
-
* finding, not a silent workaround.
|
|
28
|
+
* does not have. The scenario `10-scripts-disabled` holds the behavior.
|
|
31
29
|
*
|
|
32
30
|
* SIMULATED EXECUTION. A `run` action's `simulate` field (plain data) tells
|
|
33
31
|
* this module what the isolate would have produced, instead of spawning a
|
|
@@ -42,7 +40,6 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
42
40
|
import { dirname } from 'node:path';
|
|
43
41
|
import { fileURLToPath } from 'node:url';
|
|
44
42
|
import { createHtmlExportEngine } from '../html-engine.js';
|
|
45
|
-
import { createNodeArchiveExtractFs } from '../node-fs.js';
|
|
46
43
|
import { usesPackTrust } from './pack-trust-behavior.js';
|
|
47
44
|
import { join, relative, sep } from 'node:path';
|
|
48
45
|
import { zipSync } from 'fflate';
|
|
@@ -142,10 +139,7 @@ export function hostDeclaresCapability(adapter, action) {
|
|
|
142
139
|
if (capability === 'packs') {
|
|
143
140
|
if (adapter.packs === undefined)
|
|
144
141
|
return false;
|
|
145
|
-
if (action.type === 'installPack') {
|
|
146
|
-
return adapter.packs.installRoot !== undefined;
|
|
147
|
-
}
|
|
148
|
-
if (action.type === 'removePack') {
|
|
142
|
+
if (action.type === 'installPack' || action.type === 'removePack') {
|
|
149
143
|
return (adapter.packs.installRoot !== undefined &&
|
|
150
144
|
adapter.packs.removeFolder !== undefined);
|
|
151
145
|
}
|
|
@@ -162,7 +156,7 @@ export function hostDeclaresCapability(adapter, action) {
|
|
|
162
156
|
* `next()` THROWS once exhausted rather than returning a default: an
|
|
163
157
|
* unanswered prompt means the scenario and the implementation disagree
|
|
164
158
|
* about how many questions get asked, which must never be silently read
|
|
165
|
-
* as a deny
|
|
159
|
+
* as a deny.
|
|
166
160
|
*/
|
|
167
161
|
export function createAnswerQueue(scenario) {
|
|
168
162
|
const answers = [];
|
|
@@ -264,7 +258,6 @@ export async function runHostScenario(adapter, scenario, noteText, scenarioDir,
|
|
|
264
258
|
// The runner is Node-only test tooling, so it supplies the real Node
|
|
265
259
|
// implementations of the seams the host layer takes by injection.
|
|
266
260
|
htmlEngine: createHtmlExportEngine(),
|
|
267
|
-
archiveExtractFs: createNodeArchiveExtractFs(),
|
|
268
261
|
spawnRun: (spawnOptions) => Promise.resolve(simulatedRunResult(currentSimulation, spawnOptions)),
|
|
269
262
|
...(options.renderAnsi ? { renderAnsi: options.renderAnsi } : {}),
|
|
270
263
|
});
|
|
@@ -454,6 +447,16 @@ export function outcomeMismatches(actual, expected) {
|
|
|
454
447
|
break;
|
|
455
448
|
}
|
|
456
449
|
case 'exported': {
|
|
450
|
+
if (expected['valueCount'] !== undefined &&
|
|
451
|
+
actualRecord['valueCount'] !== expected['valueCount']) {
|
|
452
|
+
mismatches.push(`valueCount: expected ${String(expected['valueCount'])}, got ${String(actualRecord['valueCount'])}`);
|
|
453
|
+
}
|
|
454
|
+
if (expected['renderEngine'] !== undefined) {
|
|
455
|
+
const render = actualRecord['render'];
|
|
456
|
+
if (render?.engine !== expected['renderEngine']) {
|
|
457
|
+
mismatches.push(`render engine: expected "${String(expected['renderEngine'])}", got "${String(render?.engine)}"`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
457
460
|
const content = String(actualRecord['content'] ?? '');
|
|
458
461
|
for (const needle of expected['contains'] ?? []) {
|
|
459
462
|
if (!content.includes(needle)) {
|
|
@@ -468,6 +471,13 @@ export function outcomeMismatches(actual, expected) {
|
|
|
468
471
|
break;
|
|
469
472
|
}
|
|
470
473
|
case 'installed': {
|
|
474
|
+
if (expected['installedDirName'] !== undefined) {
|
|
475
|
+
const dir = String(actualRecord['installedDir'] ?? '');
|
|
476
|
+
const name = dir.split(/[/\\]/).pop();
|
|
477
|
+
if (name !== expected['installedDirName']) {
|
|
478
|
+
mismatches.push(`installedDir: expected a folder named "${String(expected['installedDirName'])}", got "${dir}"`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
471
481
|
if (expected['namespace'] !== undefined &&
|
|
472
482
|
actualRecord['namespace'] !== expected['namespace']) {
|
|
473
483
|
mismatches.push(`namespace: expected "${String(expected['namespace'])}", got "${String(actualRecord['namespace'])}"`);
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
* its own run/export/pack/editor wiring. Everything it touches (files, prompts,
|
|
4
4
|
* the isolate) arrives through the adapter, so the main entry stays
|
|
5
5
|
* environment-neutral. VS Code's webview imports only the pure pieces and
|
|
6
|
-
* never calls this function
|
|
6
|
+
* never calls this function.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* this same pass (`./export-behavior.js`, `./pack-install.js`,
|
|
12
|
-
* `./pack-load.js`, `./editor-behavior.js`). Every one of them still
|
|
8
|
+
* It provides `run`, `diagnostics`, `open`, `exportNote`, `installPack`, `loadPacks`, `completeAt`,
|
|
9
|
+
* `hoverAt`, and `insertComponent`, over the behavior modules (`./export-behavior.js`, `./pack-install.js`,
|
|
10
|
+
* `./pack-load.js`, `./editor-behavior.js`). Every one of them
|
|
13
11
|
* returns the shared `Unsupported` outcome, with one diagnostics line,
|
|
14
12
|
* when the adapter lacks the capability it needs — AGENTS.md's "clean is
|
|
15
13
|
* not silent": a decline is never a silent no-op.
|
|
@@ -20,9 +18,9 @@ import type { StoredValue } from '@markii/runtime';
|
|
|
20
18
|
import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
|
|
21
19
|
import type { CompletionItem, ComponentDocumentation, LineColumn } from '@markii/stdlib/editor';
|
|
22
20
|
import { type RunViaAdapterRequest, type RunViaAdapterResult } from './run-behavior.js';
|
|
23
|
-
import { type ArchiveExtractFs } from './pack-archive.js';
|
|
24
21
|
import { type PackDiscoveryIo } from './pack-load.js';
|
|
25
|
-
import type { HtmlExportEngine } from '../export/note-export.js';
|
|
22
|
+
import type { ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, HtmlExportEngine, StaticExportReason } from '../export/note-export.js';
|
|
23
|
+
import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
|
|
26
24
|
import type { DiscoveredPack, SkippedPackFolder } from '../packs/discover.js';
|
|
27
25
|
import { type TerminalPackScript } from './terminal-packs.js';
|
|
28
26
|
import { type RemovePackOutcome } from './pack-remove.js';
|
|
@@ -63,11 +61,31 @@ export interface ExportRequest {
|
|
|
63
61
|
readonly unavailableComponents?: readonly UnavailableComponent[];
|
|
64
62
|
/** Receives the static engine's render diagnostics, such as the `no-engine-view` event. */
|
|
65
63
|
readonly onDiagnostic?: OnDiagnostic;
|
|
64
|
+
/** The host's own render of the body (a React pass bound to its loaded packs). Omitted means the static engine renders, and `staticReason` says why. Used by the `html` and `pdf` formats. */
|
|
65
|
+
readonly renderBody?: ExportBodyRenderer;
|
|
66
|
+
/** Why the static engine renders when `renderBody` is omitted. Defaults to `no-packs`. */
|
|
67
|
+
readonly staticReason?: StaticExportReason;
|
|
68
|
+
/** The loaded packs' stylesheets, embedded only when `renderBody` ran. */
|
|
69
|
+
readonly packStylesheets?: readonly ExportPackStylesheet[];
|
|
70
|
+
/** How many loaded packs contributed to the render. Diagnostics only. */
|
|
71
|
+
readonly packCount?: number;
|
|
72
|
+
/** Reads one of the note's local images so the export embeds it as a `data:` URI. Omitted leaves every image source as written. */
|
|
73
|
+
readonly embedImages?: ExportImageReader;
|
|
74
|
+
/** Hides the collapsed script marker in the exported file. Defaults to `false`. */
|
|
75
|
+
readonly hideScriptBlocks?: boolean;
|
|
66
76
|
}
|
|
67
77
|
export type ExportOutcome = {
|
|
68
78
|
readonly kind: 'exported';
|
|
69
79
|
readonly path: string;
|
|
70
80
|
readonly bytes: number;
|
|
81
|
+
/** How many last-run values were baked into the file. */
|
|
82
|
+
readonly valueCount: number;
|
|
83
|
+
/** `true` when the note contains a script fence. */
|
|
84
|
+
readonly hasScripts?: boolean;
|
|
85
|
+
/** Which engine rendered the body, and why when it was the static one. */
|
|
86
|
+
readonly render: ExportRenderInfo;
|
|
87
|
+
/** What image embedding did. */
|
|
88
|
+
readonly images: EmbeddedImageReport;
|
|
71
89
|
} | {
|
|
72
90
|
readonly kind: 'failed';
|
|
73
91
|
readonly reason: string;
|
|
@@ -84,6 +102,8 @@ export interface InstallPackRequest {
|
|
|
84
102
|
export type InstallPackOutcome = {
|
|
85
103
|
readonly kind: 'installed';
|
|
86
104
|
readonly namespace: string;
|
|
105
|
+
/** The folder the pack now lives in, under the adapter's install root. */
|
|
106
|
+
readonly installedDir: string;
|
|
87
107
|
readonly replaced: boolean;
|
|
88
108
|
} | {
|
|
89
109
|
readonly kind: 'declined';
|
|
@@ -239,9 +259,7 @@ export interface MarkiiHost {
|
|
|
239
259
|
*/
|
|
240
260
|
export interface MarkiiHostOptions {
|
|
241
261
|
readonly spawnRun?: (options: SpawnRunRequest) => Promise<RunResult>;
|
|
242
|
-
/** The
|
|
243
|
-
readonly archiveExtractFs?: ArchiveExtractFs;
|
|
244
|
-
/** The static HTML engine behind the `html` and `pdf` exports (`createHtmlExportEngine()` from `@markii/host/node`). Without it those formats decline, like `renderAnsi` for `ansi`. */
|
|
262
|
+
/** The static HTML engine behind the `html` and `pdf` exports (`createHtmlExportEngine()` from `@markii/host/html-engine`). Without it those formats decline, like `renderAnsi` for `ansi`. */
|
|
245
263
|
readonly htmlEngine?: HtmlExportEngine;
|
|
246
264
|
/** Pack discovery's file reads. Defaults to reads through the adapter's own `readFile` and `listFolder`. */
|
|
247
265
|
readonly packDiscoveryIo?: PackDiscoveryIo;
|
package/dist/host/create-host.js
CHANGED
|
@@ -95,6 +95,24 @@ export function createMarkiiHost(adapter, options = {}) {
|
|
|
95
95
|
...(request.onDiagnostic !== undefined
|
|
96
96
|
? { onDiagnostic: request.onDiagnostic }
|
|
97
97
|
: {}),
|
|
98
|
+
...(request.renderBody !== undefined
|
|
99
|
+
? { renderBody: request.renderBody }
|
|
100
|
+
: {}),
|
|
101
|
+
...(request.staticReason !== undefined
|
|
102
|
+
? { staticReason: request.staticReason }
|
|
103
|
+
: {}),
|
|
104
|
+
...(request.packStylesheets !== undefined
|
|
105
|
+
? { packStylesheets: request.packStylesheets }
|
|
106
|
+
: {}),
|
|
107
|
+
...(request.packCount !== undefined
|
|
108
|
+
? { packCount: request.packCount }
|
|
109
|
+
: {}),
|
|
110
|
+
...(request.embedImages !== undefined
|
|
111
|
+
? { embedImages: request.embedImages }
|
|
112
|
+
: {}),
|
|
113
|
+
...(request.hideScriptBlocks !== undefined
|
|
114
|
+
? { hideScriptBlocks: request.hideScriptBlocks }
|
|
115
|
+
: {}),
|
|
98
116
|
}, options.renderAnsi);
|
|
99
117
|
if (outcome === undefined) {
|
|
100
118
|
adapter.diagnostics(`export declined: format "${request.format}" has no working export path on ${adapter.id} (a cancelled picker, or a missing renderer/PDF seam).`);
|
|
@@ -106,47 +124,40 @@ export function createMarkiiHost(adapter, options = {}) {
|
|
|
106
124
|
if (outcome.kind === 'failed') {
|
|
107
125
|
return { kind: 'failed', reason: outcome.reason };
|
|
108
126
|
}
|
|
109
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
kind: 'exported',
|
|
129
|
+
path: outcome.path,
|
|
130
|
+
bytes: outcome.bytes,
|
|
131
|
+
valueCount: outcome.valueCount,
|
|
132
|
+
...(outcome.hasScripts !== undefined
|
|
133
|
+
? { hasScripts: outcome.hasScripts }
|
|
134
|
+
: {}),
|
|
135
|
+
render: outcome.render,
|
|
136
|
+
images: outcome.images,
|
|
137
|
+
};
|
|
110
138
|
},
|
|
111
139
|
async installPack(request) {
|
|
112
140
|
if (!adapter.packs?.installRoot) {
|
|
113
141
|
return unsupported(adapter, 'packs', 'this host has no pack-install destination configured.');
|
|
114
142
|
}
|
|
115
143
|
const archivePath = request.archivePath ?? 'the selected archive';
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
return unsupported(adapter, 'packs', 'this host
|
|
144
|
+
const removeFolder = adapter.packs.removeFolder;
|
|
145
|
+
if (removeFolder === undefined) {
|
|
146
|
+
return unsupported(adapter, 'packs', 'this host has no way to remove an installed pack folder.');
|
|
119
147
|
}
|
|
120
148
|
const outcome = await installPackFromArchive(adapter, {
|
|
121
149
|
archiveBytes: request.archiveBytes,
|
|
122
150
|
archivePath,
|
|
123
151
|
installRoot: adapter.packs.installRoot(),
|
|
124
152
|
exists: (path) => adapter.exists(path),
|
|
125
|
-
|
|
153
|
+
removeFolder: (path) => removeFolder.call(adapter.packs, path),
|
|
126
154
|
reservedNamespaces: adapter.packs.reservedNamespaces(),
|
|
127
155
|
});
|
|
128
156
|
catalogCache.invalidate();
|
|
129
157
|
for (const line of installPackDiagnosticLines(outcome, archivePath)) {
|
|
130
158
|
adapter.diagnostics(line);
|
|
131
159
|
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
kind: 'installed',
|
|
135
|
-
namespace: outcome.packName,
|
|
136
|
-
replaced: outcome.replaced,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
if (outcome.kind === 'declined') {
|
|
140
|
-
return {
|
|
141
|
-
kind: 'declined',
|
|
142
|
-
step: outcome.step,
|
|
143
|
-
namespace: outcome.packName,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
if (outcome.kind === 'reserved') {
|
|
147
|
-
return { kind: 'reserved', namespace: outcome.packName };
|
|
148
|
-
}
|
|
149
|
-
return { kind: 'rejected', reason: outcome.reason };
|
|
160
|
+
return outcome;
|
|
150
161
|
},
|
|
151
162
|
async loadPacks() {
|
|
152
163
|
if (!adapter.packs) {
|
|
@@ -4,12 +4,8 @@ import type { HostEditor, HostTextEdit } from './adapter.js';
|
|
|
4
4
|
export declare const STANDARD_ORIGIN_TAG = "standard";
|
|
5
5
|
export declare const LAYOUT_ORIGIN_TAG = "layout";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* IDENTICAL function bodies, differing only in which locally-defined
|
|
10
|
-
* constant stood in for the standard/layout tag string. Both apps'
|
|
11
|
-
* constants held the same lower-case values (`'standard'`/`'layout'`),
|
|
12
|
-
* so there was never a wording difference to preserve.
|
|
7
|
+
* The origin tag a completion item carries: the lower-case
|
|
8
|
+
* `'standard'`/`'layout'` strings above, or the pack name.
|
|
13
9
|
*/
|
|
14
10
|
export declare function completionOriginTag(item: CompletionItem): string;
|
|
15
11
|
/** A small cache in front of `buildComponentCatalog` (`../insert/component-catalog.js`), so a completion/hover provider does not re-discover packs from disk on every keystroke. */
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* The `completeAt`/`hoverAt`/`insertComponent` behaviors' shared half,
|
|
3
3
|
* merged from `apps/vscode/src/completion.ts` + `completion-catalog.ts`,
|
|
4
4
|
* `apps/obsidian/src/complete-component.ts`, and both apps'
|
|
5
|
-
* `insert-component.ts
|
|
5
|
+
* `insert-component.ts`.
|
|
6
6
|
*
|
|
7
|
-
* NODE-FREE by design
|
|
7
|
+
* NODE-FREE by design: re-exported
|
|
8
8
|
* from `../browser.ts` as well as `../index.ts`, so VS Code's webview
|
|
9
9
|
* bundle can read the origin-tag vocabulary and the pure completion math
|
|
10
10
|
* directly if a future slice needs to. `CatalogCache.get`'s injected
|
|
11
11
|
* `load` is the one thing that reaches disk, and it is always supplied by
|
|
12
12
|
* the caller (a main-entry-only pack-discovery call), never imported here.
|
|
13
13
|
*
|
|
14
|
-
* What stays app-side (deliberately
|
|
14
|
+
* What stays app-side (deliberately): VS
|
|
15
15
|
* Code's sectioned quick-pick presentation (`Standard`/`Layout`/`<pack>
|
|
16
16
|
* pack` separators) and Obsidian's flat fuzzy list are a genuine product
|
|
17
17
|
* difference, not duplication — this module returns the catalog and the
|
|
@@ -25,12 +25,8 @@ import { closesOpenContainerFence, completionAt, componentDocumentation, fenceEx
|
|
|
25
25
|
export const STANDARD_ORIGIN_TAG = 'standard';
|
|
26
26
|
export const LAYOUT_ORIGIN_TAG = 'layout';
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* IDENTICAL function bodies, differing only in which locally-defined
|
|
31
|
-
* constant stood in for the standard/layout tag string. Both apps'
|
|
32
|
-
* constants held the same lower-case values (`'standard'`/`'layout'`),
|
|
33
|
-
* so there was never a wording difference to preserve.
|
|
28
|
+
* The origin tag a completion item carries: the lower-case
|
|
29
|
+
* `'standard'`/`'layout'` strings above, or the pack name.
|
|
34
30
|
*/
|
|
35
31
|
export function completionOriginTag(item) {
|
|
36
32
|
if (item.kind !== 'component')
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The `exportNote` behavior: outcome shape, wording, and format gating,
|
|
3
3
|
* merged from `apps/vscode/src/export-html.ts`, the host-neutral half of
|
|
4
4
|
* `apps/obsidian/src/export-note.ts`, and `apps/cli/src/export-note.ts`'s
|
|
5
|
-
* `exportHtml
|
|
5
|
+
* `exportHtml`.
|
|
6
6
|
*
|
|
7
7
|
* `html` and `md-plain` are fully implemented here, over `../export/note-export.js`'s
|
|
8
8
|
* `buildNoteExport` (already host-neutral, already shared) and
|
|
@@ -12,26 +12,25 @@
|
|
|
12
12
|
* AGENTS.md's package boundaries), so it is rendered through the injected
|
|
13
13
|
* `renderAnsi` option; a host that omits it gets `Unsupported`.
|
|
14
14
|
*
|
|
15
|
-
* PDF stays a seam, never an implementation here,
|
|
16
|
-
* `tmp/W11-adapter-design.md` specifies: this module builds the same HTML
|
|
15
|
+
* PDF stays a seam, never an implementation here, this module builds the same HTML
|
|
17
16
|
* document `html` would have written and hands it to
|
|
18
17
|
* `adapter.exports.htmlToPdf`, but the DEGRADE-TO-HTML-ON-FAILURE product
|
|
19
18
|
* behavior Obsidian's own `exportNoteAsPdf` layers on top (falling back to
|
|
20
19
|
* writing the HTML file when printing is unavailable or fails, and
|
|
21
20
|
* reporting which of the two happened) is Obsidian-specific policy about
|
|
22
21
|
* what to do with a seam's failure, not the seam itself — it stays
|
|
23
|
-
* app-side.
|
|
22
|
+
* app-side.
|
|
24
23
|
*
|
|
25
24
|
* Cascade orchestration (multi-note export, `walkNoteCascade` +
|
|
26
25
|
* `assignCascadeFileNames` + `rewriteCascadeLinks` + `zipExportArchive`)
|
|
27
|
-
* is
|
|
28
|
-
* section for why, and what Phase 2 (or a follow-up) still needs to do.
|
|
26
|
+
* is not part of this module: it stays with each host.
|
|
29
27
|
*/
|
|
30
28
|
import type { StoredValue } from '@markii/runtime';
|
|
31
29
|
import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
|
|
32
30
|
import type { HtmlExportEngine, ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, StaticExportReason } from '../export/note-export.js';
|
|
33
31
|
import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
|
|
34
32
|
import type { HostAdapter, HostExportFormat, HostLabels } from './adapter.js';
|
|
33
|
+
import type { ExportOutcome } from './create-host.js';
|
|
35
34
|
/** The file name an export lands under, by format: the note's own base name with the format's own extension, so the export lands beside the note unless the host's picker says otherwise. */
|
|
36
35
|
export declare function exportDefaultFileName(notePath: string, format: HostExportFormat): string;
|
|
37
36
|
/** What a `'html'` or `'pdf'` export attempt produced. Every shape except `failed` means the host got a file, and every shape but `failed` carries the same diagnostics-facing detail so the two hosts' wording can share it. */
|
|
@@ -60,7 +59,7 @@ export declare function fileNameOf(path: string): string;
|
|
|
60
59
|
* host's own diagnostics surface (`labels.diagnosticsSurface`), never at
|
|
61
60
|
* a stack trace — the verbatim reason goes there instead.
|
|
62
61
|
*/
|
|
63
|
-
export declare function exportResultMessage(outcome:
|
|
62
|
+
export declare function exportResultMessage(outcome: ExportOutcome, labels: HostLabels): string;
|
|
64
63
|
/** One diagnostic line describing HOW an export's body was rendered: the pack-vs-static distinction, kept off the result message and put here instead. */
|
|
65
64
|
export declare function renderEngineDiagnosticLine(render: ExportRenderInfo): string;
|
|
66
65
|
/** `bytes`, formatted as a human-readable size (`"1.2 KB"`, `"512 bytes"`). */
|
|
@@ -23,7 +23,7 @@ export function fileNameOf(path) {
|
|
|
23
23
|
* a stack trace — the verbatim reason goes there instead.
|
|
24
24
|
*/
|
|
25
25
|
export function exportResultMessage(outcome, labels) {
|
|
26
|
-
if (outcome.kind
|
|
26
|
+
if (outcome.kind !== 'exported') {
|
|
27
27
|
return `Markii: could not export this note. Open ${labels.diagnosticsSurface} for details.`;
|
|
28
28
|
}
|
|
29
29
|
const name = fileNameOf(outcome.path);
|