@markii/host 0.17.0-next.4 → 0.17.0-next.5
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 +28 -1
- package/dist/conformance/host/17-export-linked-set/expected.json +17 -0
- package/dist/conformance/host/17-export-linked-set/files/second.mk.md +3 -0
- package/dist/conformance/host/17-export-linked-set/note.mk.md +4 -0
- package/dist/conformance/host/17-export-linked-set/scenario.json +6 -0
- package/dist/conformance/host/README.md +13 -10
- package/dist/diagnostics/render-diagnostics.d.ts +1 -1
- package/dist/host/adapter.d.ts +9 -1
- package/dist/host/cascade-export-behavior.d.ts +24 -8
- package/dist/host/cascade-export-behavior.js +61 -1
- package/dist/host/conformance-runner.d.ts +3 -1
- package/dist/host/conformance-runner.js +40 -2
- package/dist/host/create-host.d.ts +40 -3
- package/dist/host/create-host.js +20 -0
- package/dist/host/pack-trust-behavior.js +1 -1
- package/dist/host/run-behavior.d.ts +4 -0
- package/dist/host/run-behavior.js +9 -1
- package/dist/host/terminal-packs.d.ts +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +1 -3
- package/dist/internal.js +1 -1
- package/dist/run/bundle-run.d.ts +1 -1
- package/dist/run/bundle-run.js +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -25,4 +25,31 @@ Entries:
|
|
|
25
25
|
- `@markii/host/conformance`: the host scenario corpus and its runner, to
|
|
26
26
|
prove an adapter behaves like the reference apps.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
A minimal browser host is one adapter and one call:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createMarkiiHost } from '@markii/host';
|
|
32
|
+
import {
|
|
33
|
+
createBrowserHostIsolate,
|
|
34
|
+
createFetchNetProvider,
|
|
35
|
+
} from '@markii/host/browser';
|
|
36
|
+
|
|
37
|
+
const host = createMarkiiHost({
|
|
38
|
+
id: 'my-app',
|
|
39
|
+
labels,
|
|
40
|
+
readFile,
|
|
41
|
+
exists,
|
|
42
|
+
writeFile,
|
|
43
|
+
listFolder,
|
|
44
|
+
prompt: (request) => showDialog(request),
|
|
45
|
+
memento,
|
|
46
|
+
diagnostics: (line) => console.info(line),
|
|
47
|
+
now: () => Date.now(),
|
|
48
|
+
isolate: createBrowserHostIsolate({
|
|
49
|
+
netProvider: createFetchNetProvider({ allowedOrigins }),
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See `docs/hosts.md` in the repository for the adapter fields, the host
|
|
55
|
+
responsibilities and the reference hosts.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"outcomes": [
|
|
3
|
+
{
|
|
4
|
+
"kind": "exported",
|
|
5
|
+
"noteCount": 2,
|
|
6
|
+
"entries": ["index.html", "note.html", "second.html"]
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"diagnostics": [
|
|
10
|
+
"Cascade export wrote",
|
|
11
|
+
"Exported",
|
|
12
|
+
"Rendered with the static engine because no pack components are loaded",
|
|
13
|
+
"Exported",
|
|
14
|
+
"Rendered with the static engine because no pack components are loaded"
|
|
15
|
+
],
|
|
16
|
+
"prompts": []
|
|
17
|
+
}
|
|
@@ -49,6 +49,7 @@ needs, is the runner's own `ACTION_CAPABILITY` map:
|
|
|
49
49
|
| `run` | `isolate` |
|
|
50
50
|
| `exportHtml` | `export-format` (`html`) |
|
|
51
51
|
| `exportAnsi` | `export-format` (`ansi`) |
|
|
52
|
+
| `exportLinkedSet` | `linked-set-export` (a `resolveLinkedSetTarget`) |
|
|
52
53
|
| `installPack` | `packs` (and an `installRoot`) |
|
|
53
54
|
| `loadPacks` | `packs` |
|
|
54
55
|
| `removePack` | `packs` (and an `installRoot` plus `removeFolder`) |
|
|
@@ -97,12 +98,12 @@ would have produced:
|
|
|
97
98
|
and persists exactly what it always does. Only the isolate that would
|
|
98
99
|
execute the script is faked.
|
|
99
100
|
- `requiresManualTier`: the run reports `tier-blocked` under any trigger
|
|
100
|
-
but `'manual'`, regardless of grants
|
|
101
|
+
but `'manual'`, regardless of grants, the same enforcement the real
|
|
101
102
|
sandbox applies to an effectful call under a read-only trigger.
|
|
102
103
|
|
|
103
104
|
Faking the isolate keeps this corpus fast and deterministic. It proves the
|
|
104
|
-
HOST-LEVEL plumbing
|
|
105
|
-
trigger forwarding
|
|
105
|
+
HOST-LEVEL plumbing, grant prompts, wording, capability declarations,
|
|
106
|
+
trigger forwarding, is identical across hosts. Real sandbox enforcement
|
|
106
107
|
(the tier gate, the net allowlist, the marshal boundary) is
|
|
107
108
|
`conformance/executor/`'s job and is not duplicated here.
|
|
108
109
|
|
|
@@ -137,9 +138,11 @@ coarse, matching each `kind`:
|
|
|
137
138
|
taxonomy only, never a raw message or a timing; an optional `traceOk`
|
|
138
139
|
compares the recorded run trace's `ok` flag.
|
|
139
140
|
- `exported`: `contains`/`excludes` are substrings checked against the
|
|
140
|
-
written file's content, never the whole document
|
|
141
|
+
written file's content, never the whole document, the render corpora
|
|
141
142
|
already pin bytes. `valueCount` is how many last-run values were baked
|
|
142
143
|
in, and `renderEngine` (`static` or `react`) is which engine drew the body.
|
|
144
|
+
A linked-set export is an `exported` outcome too: `noteCount` is how many
|
|
145
|
+
notes the archive holds, and `entries` is its sorted file names.
|
|
143
146
|
- `installed`: `namespace` and `replaced`, and `installedDirName`, the name
|
|
144
147
|
of the folder the pack was unzipped into.
|
|
145
148
|
- `declined`: `step` (`'consent'`/`'replace'`, or absent for the
|
|
@@ -161,7 +164,7 @@ coarse, matching each `kind`:
|
|
|
161
164
|
|
|
162
165
|
An action whose capability the adapter under test does not declare is
|
|
163
166
|
never diffed against `expected.json` at all (see "Capability handling"
|
|
164
|
-
below)
|
|
167
|
+
below), `expected.json` always describes the FULLY CAPABLE host's
|
|
165
168
|
correct behavior, even for a scenario where one real host today has
|
|
166
169
|
nothing to compare it to.
|
|
167
170
|
|
|
@@ -173,11 +176,11 @@ difference) without the corpus having to spell out three wordings for
|
|
|
173
176
|
one behavior. Diagnostics lines written by a capability decline itself
|
|
174
177
|
(`declined (<capability> unsupported on <host>): …`) are filtered out of
|
|
175
178
|
the actual list before this comparison, since that line exists only
|
|
176
|
-
because the capability was ABSENT
|
|
179
|
+
because the capability was ABSENT, it is not part of what
|
|
177
180
|
`expected.json` describes.
|
|
178
181
|
|
|
179
182
|
`prompts` is load-bearing: it proves all three hosts ask the same
|
|
180
|
-
questions, worded the same way, in the same order
|
|
183
|
+
questions, worded the same way, in the same order, the single most
|
|
181
184
|
security-relevant parity property in this batch.
|
|
182
185
|
|
|
183
186
|
## Capability handling (the crux)
|
|
@@ -217,7 +220,7 @@ rather than a skipped test or a faked empty answer.
|
|
|
217
220
|
| `14-remove-pack` | removing an installed pack deletes it; removing it again reports it is not installed |
|
|
218
221
|
| `15-load-packs-engine-views` | loading packs reports every installed component and, as `unavailable`, the ones this host's engine has no view for |
|
|
219
222
|
| `16-pack-trust` | a folder that arrives without consent is listed as present and never loaded; enable, disable, re-enable and remove move the device trust list; a host without the list reports the whole scenario `unsupported` |
|
|
223
|
+
| `17-export-linked-set` | a note and the notes it links to export as one archive with an index page; a host with no linked-set target reports `unsupported` |
|
|
220
224
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`unsupported` on every host today (a reported gap, not a corpus defect).
|
|
225
|
+
The capabilities an adapter declares decide which scenarios report
|
|
226
|
+
`unsupported` on that host. That is a reported gap and not a corpus defect.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place a render-time diagnostic event becomes a line on a host's
|
|
3
|
-
* diagnostics surface (docs/
|
|
3
|
+
* diagnostics surface (docs/hosts.md's host checklist, item 9).
|
|
4
4
|
*
|
|
5
5
|
* Both renderers report a quiet in-page marker through their `onDiagnostic`
|
|
6
6
|
* render option (`@markii/stdlib`'s `DiagnosticEvent`). The marker alone
|
package/dist/host/adapter.d.ts
CHANGED
|
@@ -161,6 +161,14 @@ export interface HostExportCapabilities {
|
|
|
161
161
|
resolveTarget(suggestedFileName: string, format: HostExportFormat): Promise<string | undefined>;
|
|
162
162
|
/** Only present on a host declaring `'pdf'`. The one Electron-touching seam. */
|
|
163
163
|
htmlToPdf?(html: string): Promise<Uint8Array>;
|
|
164
|
+
/**
|
|
165
|
+
* Names the zip archive of a linked-set export, or resolves `undefined`
|
|
166
|
+
* when the user cancels. Present only on a host that offers the
|
|
167
|
+
* linked-set export; a host can offer single-note export without it
|
|
168
|
+
* (the command line tool does), so it is a member of its own rather than
|
|
169
|
+
* a `HostExportFormat`.
|
|
170
|
+
*/
|
|
171
|
+
resolveLinkedSetTarget?(suggestedFileName: string): Promise<string | undefined>;
|
|
164
172
|
}
|
|
165
173
|
/**
|
|
166
174
|
* The wording nouns every shared behavior's message templates interpolate
|
|
@@ -210,7 +218,7 @@ export interface HostAdapter {
|
|
|
210
218
|
* through with no adapter wrapper at all.
|
|
211
219
|
*/
|
|
212
220
|
readonly memento: GrantMemento;
|
|
213
|
-
/** One diagnostics line on this host's designated surface (docs/
|
|
221
|
+
/** One diagnostics line on this host's designated surface (docs/hosts.md). */
|
|
214
222
|
diagnostics(line: string): void;
|
|
215
223
|
now(): number;
|
|
216
224
|
readonly isolate?: HostIsolate;
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* produces. Written once so the hosts cannot drift on what a cascade is
|
|
6
6
|
* or what it says.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* `createMarkiiHost`'s `exportLinkedSet` drives it: the host's picker
|
|
9
|
+
* names the archive's path (`adapter.exports.resolveLinkedSetTarget`), the
|
|
10
|
+
* notes are read through `adapter.readFile`, the archive is written through
|
|
11
|
+
* `adapter.writeFile`, and the diagnostics lines go to `adapter.diagnostics`.
|
|
12
|
+
* The per-host nouns (which surface holds the details) come from
|
|
13
|
+
* `HostLabels`, never from a literal here.
|
|
13
14
|
*
|
|
14
15
|
* BOUNDS ARE FIXED. The walk always uses `DEFAULT_CASCADE_MAX_DEPTH` and
|
|
15
16
|
* `DEFAULT_CASCADE_MAX_NOTES`; nothing here lets a caller override them.
|
|
@@ -19,7 +20,8 @@ import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
|
|
|
19
20
|
import type { CascadeLinkResolver, CascadeNoteReader, CascadeTruncation } from '../export/cascade.js';
|
|
20
21
|
import type { HtmlExportEngine, ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, StaticExportReason } from '../export/note-export.js';
|
|
21
22
|
import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
|
|
22
|
-
import type { HostLabels } from './adapter.js';
|
|
23
|
+
import type { HostAdapter, HostLabels } from './adapter.js';
|
|
24
|
+
import type { ExportLinkedSetOutcome } from './create-host.js';
|
|
23
25
|
/** One note the cascade exported, for the diagnostics surface. */
|
|
24
26
|
export interface CascadeExportedNote {
|
|
25
27
|
/** The note's own path, in the same form the reader and resolver use. */
|
|
@@ -37,7 +39,7 @@ export interface UnreadableCascadeNote {
|
|
|
37
39
|
}
|
|
38
40
|
/** What one cascade export attempt did, for both the message and the diagnostics lines. */
|
|
39
41
|
export type CascadeExportOutcome = {
|
|
40
|
-
readonly kind: '
|
|
42
|
+
readonly kind: 'exported';
|
|
41
43
|
/** The written archive's display path. */
|
|
42
44
|
readonly path: string;
|
|
43
45
|
readonly bytes: number;
|
|
@@ -109,7 +111,7 @@ export declare function buildCascadeArchive(request: CascadeArchiveRequest): Pro
|
|
|
109
111
|
* failure says what failed and points at the same surface, never at a
|
|
110
112
|
* stack trace.
|
|
111
113
|
*/
|
|
112
|
-
export declare function cascadeResultMessage(outcome:
|
|
114
|
+
export declare function cascadeResultMessage(outcome: ExportLinkedSetOutcome, labels: HostLabels, where?: string): string;
|
|
113
115
|
/**
|
|
114
116
|
* The lines for the host's diagnostics surface for one cascade export:
|
|
115
117
|
* every note reached named with the file it became, every note skipped
|
|
@@ -118,3 +120,17 @@ export declare function cascadeResultMessage(outcome: CascadeExportOutcome, labe
|
|
|
118
120
|
* deliberately omits.
|
|
119
121
|
*/
|
|
120
122
|
export declare function cascadeDiagnosticLines(outcome: CascadeExportOutcome): string[];
|
|
123
|
+
/** What `exportLinkedSetViaAdapter` needs beyond the adapter: the static engine and the request's own seams. */
|
|
124
|
+
export interface LinkedSetExportInputs extends Omit<CascadeArchiveRequest, 'readNote'> {
|
|
125
|
+
/** The suggested archive file name handed to the host's picker. */
|
|
126
|
+
readonly suggestedFileName: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Exports a note and the notes it links to as one archive: resolves the
|
|
130
|
+
* target through the adapter's picker, builds the archive reading every
|
|
131
|
+
* note through `adapter.readFile`, writes it through `adapter.writeFile`,
|
|
132
|
+
* and sends the diagnostics lines to `adapter.diagnostics`. Resolves
|
|
133
|
+
* `undefined` when the host has no linked-set target or its picker was
|
|
134
|
+
* cancelled. Never throws.
|
|
135
|
+
*/
|
|
136
|
+
export declare function exportLinkedSetViaAdapter(adapter: HostAdapter, inputs: LinkedSetExportInputs): Promise<CascadeExportOutcome | undefined>;
|
|
@@ -109,7 +109,7 @@ export async function buildCascadeArchive(request) {
|
|
|
109
109
|
* stack trace.
|
|
110
110
|
*/
|
|
111
111
|
export function cascadeResultMessage(outcome, labels, where) {
|
|
112
|
-
if (outcome.kind
|
|
112
|
+
if (outcome.kind !== 'exported') {
|
|
113
113
|
return `Markii: could not export this cascade. Open ${labels.diagnosticsSurface} for details.`;
|
|
114
114
|
}
|
|
115
115
|
const name = fileNameOf(outcome.path);
|
|
@@ -149,3 +149,63 @@ export function cascadeDiagnosticLines(outcome) {
|
|
|
149
149
|
}
|
|
150
150
|
return lines;
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Exports a note and the notes it links to as one archive: resolves the
|
|
154
|
+
* target through the adapter's picker, builds the archive reading every
|
|
155
|
+
* note through `adapter.readFile`, writes it through `adapter.writeFile`,
|
|
156
|
+
* and sends the diagnostics lines to `adapter.diagnostics`. Resolves
|
|
157
|
+
* `undefined` when the host has no linked-set target or its picker was
|
|
158
|
+
* cancelled. Never throws.
|
|
159
|
+
*/
|
|
160
|
+
export async function exportLinkedSetViaAdapter(adapter, inputs) {
|
|
161
|
+
const resolveTarget = adapter.exports?.resolveLinkedSetTarget;
|
|
162
|
+
if (resolveTarget === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
const { suggestedFileName, ...archiveRequest } = inputs;
|
|
165
|
+
let path;
|
|
166
|
+
let outcome;
|
|
167
|
+
try {
|
|
168
|
+
path = await resolveTarget.call(adapter.exports, suggestedFileName);
|
|
169
|
+
if (path === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
const target = path;
|
|
172
|
+
const archive = await buildCascadeArchive({
|
|
173
|
+
...archiveRequest,
|
|
174
|
+
readNote: async (notePath) => {
|
|
175
|
+
try {
|
|
176
|
+
return new TextDecoder().decode(await adapter.readFile(notePath));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (archive.kind === 'failed') {
|
|
184
|
+
outcome = { kind: 'failed', path: target, reason: archive.reason };
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await adapter.writeFile(target, archive.bytes);
|
|
188
|
+
outcome = {
|
|
189
|
+
kind: 'exported',
|
|
190
|
+
path: target,
|
|
191
|
+
bytes: archive.bytes.byteLength,
|
|
192
|
+
notes: archive.notes,
|
|
193
|
+
unreadable: archive.unreadable,
|
|
194
|
+
...(archive.truncated !== undefined
|
|
195
|
+
? { truncated: archive.truncated }
|
|
196
|
+
: {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
outcome = {
|
|
202
|
+
kind: 'failed',
|
|
203
|
+
...(path !== undefined ? { path } : {}),
|
|
204
|
+
reason: cascadeReasonOf(error),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
for (const line of cascadeDiagnosticLines(outcome)) {
|
|
208
|
+
adapter.diagnostics(line);
|
|
209
|
+
}
|
|
210
|
+
return outcome;
|
|
211
|
+
}
|
|
@@ -35,6 +35,8 @@ export type HostScenarioAction = {
|
|
|
35
35
|
readonly type: 'exportHtml';
|
|
36
36
|
} | {
|
|
37
37
|
readonly type: 'exportAnsi';
|
|
38
|
+
} | {
|
|
39
|
+
readonly type: 'exportLinkedSet';
|
|
38
40
|
} | {
|
|
39
41
|
readonly type: 'installPack';
|
|
40
42
|
/** A directory under the scenario's own folder (e.g. `"files/ana-1.0.0"`), zipped into `.mkp` bytes by this module — never a committed binary. */
|
|
@@ -98,7 +100,7 @@ export interface LoadedHostScenario {
|
|
|
98
100
|
export declare function loadHostScenario(name: string, baseDir?: string): LoadedHostScenario;
|
|
99
101
|
/** Zips a scenario's `files/<name>` directory into `.mkp` archive bytes on the fly — this is how the corpus ships pack fixtures as plain files instead of a committed binary. */
|
|
100
102
|
export declare function zipArchiveDirectory(archiveDir: string): Uint8Array;
|
|
101
|
-
export type HostScenarioCapability = 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format';
|
|
103
|
+
export type HostScenarioCapability = 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format' | 'linked-set-export';
|
|
102
104
|
/** The one static map from an action's type to the capability it needs. `undefined` means every host supports it (there is nothing to decline). */
|
|
103
105
|
export declare const ACTION_CAPABILITY: Readonly<Record<HostScenarioAction['type'], HostScenarioCapability | undefined>>;
|
|
104
106
|
/** Whether `adapter` declares the capability `action` needs. Never consults the scenario or `expected.json` — only the adapter under test. */
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
* Every OTHER action runs through the real, unmocked shared behavior code.
|
|
38
38
|
*/
|
|
39
39
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
40
|
-
import { dirname } from 'node:path';
|
|
40
|
+
import { dirname, resolve } from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { createHtmlExportEngine } from '../html-engine.js';
|
|
43
43
|
import { usesPackTrust } from './pack-trust-behavior.js';
|
|
44
44
|
import { join, relative, sep } from 'node:path';
|
|
45
|
-
import { zipSync } from 'fflate';
|
|
45
|
+
import { unzipSync, zipSync } from 'fflate';
|
|
46
46
|
import { conformanceDir } from '@markii/core/corpus';
|
|
47
47
|
import { createMarkiiHost, } from './create-host.js';
|
|
48
48
|
import { scriptsDisabledDiagnosticLine, scriptsDisabledNotice, } from './script-execution.js';
|
|
@@ -101,6 +101,7 @@ export const ACTION_CAPABILITY = {
|
|
|
101
101
|
run: 'isolate',
|
|
102
102
|
exportHtml: 'export-format',
|
|
103
103
|
exportAnsi: 'export-format',
|
|
104
|
+
exportLinkedSet: 'linked-set-export',
|
|
104
105
|
installPack: 'packs',
|
|
105
106
|
loadPacks: 'packs',
|
|
106
107
|
removePack: 'packs',
|
|
@@ -136,6 +137,9 @@ export function hostDeclaresCapability(adapter, action) {
|
|
|
136
137
|
return adapter.isolate !== undefined;
|
|
137
138
|
if (capability === 'editor')
|
|
138
139
|
return adapter.editor !== undefined;
|
|
140
|
+
if (capability === 'linked-set-export') {
|
|
141
|
+
return adapter.exports?.resolveLinkedSetTarget !== undefined;
|
|
142
|
+
}
|
|
139
143
|
if (capability === 'packs') {
|
|
140
144
|
if (adapter.packs === undefined)
|
|
141
145
|
return false;
|
|
@@ -329,6 +333,24 @@ async function runOneAction(host, adapter, action, ctx) {
|
|
|
329
333
|
});
|
|
330
334
|
return augmentExportOutcome(outcome, ctx.writtenFiles);
|
|
331
335
|
}
|
|
336
|
+
case 'exportLinkedSet': {
|
|
337
|
+
const outcome = await host.exportLinkedSet({
|
|
338
|
+
rootPath: ctx.notePath,
|
|
339
|
+
// A markdown link to a note file that exists beside the linking
|
|
340
|
+
// note: the same rule for every host, so the archive's contents
|
|
341
|
+
// do not depend on which host runs the scenario.
|
|
342
|
+
resolveLink: (link, fromNotePath) => {
|
|
343
|
+
if (link.kind !== 'markdown')
|
|
344
|
+
return undefined;
|
|
345
|
+
const target = resolve(dirname(fromNotePath), link.path);
|
|
346
|
+
return target.endsWith('.md') && existsSync(target)
|
|
347
|
+
? target
|
|
348
|
+
: undefined;
|
|
349
|
+
},
|
|
350
|
+
readValues: () => ({}),
|
|
351
|
+
});
|
|
352
|
+
return augmentLinkedSetOutcome(outcome, ctx.writtenFiles);
|
|
353
|
+
}
|
|
332
354
|
case 'installPack': {
|
|
333
355
|
const archiveBytes = zipArchiveDirectory(join(ctx.scenarioDir, action.archive));
|
|
334
356
|
return host.installPack({ archiveBytes, archivePath: action.archive });
|
|
@@ -361,6 +383,13 @@ async function runOneAction(host, adapter, action, ctx) {
|
|
|
361
383
|
return host.insertComponent({ name: action.name });
|
|
362
384
|
}
|
|
363
385
|
}
|
|
386
|
+
function augmentLinkedSetOutcome(outcome, writtenFiles) {
|
|
387
|
+
if (outcome.kind !== 'exported')
|
|
388
|
+
return outcome;
|
|
389
|
+
const bytes = writtenFiles.get(outcome.path);
|
|
390
|
+
const entries = bytes === undefined ? undefined : Object.keys(unzipSync(bytes)).sort();
|
|
391
|
+
return { ...outcome, noteCount: outcome.notes.length, entries };
|
|
392
|
+
}
|
|
364
393
|
function augmentExportOutcome(outcome, writtenFiles) {
|
|
365
394
|
if (outcome.kind !== 'exported')
|
|
366
395
|
return outcome;
|
|
@@ -457,6 +486,15 @@ export function outcomeMismatches(actual, expected) {
|
|
|
457
486
|
mismatches.push(`render engine: expected "${String(expected['renderEngine'])}", got "${String(render?.engine)}"`);
|
|
458
487
|
}
|
|
459
488
|
}
|
|
489
|
+
if (expected['noteCount'] !== undefined &&
|
|
490
|
+
actualRecord['noteCount'] !== expected['noteCount']) {
|
|
491
|
+
mismatches.push(`noteCount: expected ${String(expected['noteCount'])}, got ${String(actualRecord['noteCount'])}`);
|
|
492
|
+
}
|
|
493
|
+
if (expected['entries'] !== undefined &&
|
|
494
|
+
JSON.stringify(actualRecord['entries']) !==
|
|
495
|
+
JSON.stringify(expected['entries'])) {
|
|
496
|
+
mismatches.push(`archive entries: expected ${JSON.stringify(expected['entries'])}, got ${JSON.stringify(actualRecord['entries'])}`);
|
|
497
|
+
}
|
|
460
498
|
const content = String(actualRecord['content'] ?? '');
|
|
461
499
|
for (const needle of expected['contains'] ?? []) {
|
|
462
500
|
if (!content.includes(needle)) {
|
|
@@ -21,6 +21,8 @@ import { type RunViaAdapterRequest, type RunViaAdapterResult } from './run-behav
|
|
|
21
21
|
import { type PackDiscoveryIo } from './pack-load.js';
|
|
22
22
|
import type { ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, HtmlExportEngine, StaticExportReason } from '../export/note-export.js';
|
|
23
23
|
import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
|
|
24
|
+
import type { CascadeLinkResolver } from '../export/cascade.js';
|
|
25
|
+
import { type CascadeExportOutcome } from './cascade-export-behavior.js';
|
|
24
26
|
import type { DiscoveredPack, SkippedPackFolder } from '../packs/discover.js';
|
|
25
27
|
import { type TerminalPackScript } from './terminal-packs.js';
|
|
26
28
|
import { type RemovePackOutcome } from './pack-remove.js';
|
|
@@ -28,7 +30,7 @@ import type { DisablePackOutcome, EnablePackOutcome, InstalledPacksListing } fro
|
|
|
28
30
|
/** The shared decline shape every behavior returns when the adapter lacks the capability it needs. */
|
|
29
31
|
export interface Unsupported {
|
|
30
32
|
readonly kind: 'unsupported';
|
|
31
|
-
readonly capability: 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format';
|
|
33
|
+
readonly capability: 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format' | 'linked-set-export';
|
|
32
34
|
readonly detail?: string;
|
|
33
35
|
}
|
|
34
36
|
export interface OpenRequest {
|
|
@@ -94,6 +96,41 @@ export type ExportOutcome = {
|
|
|
94
96
|
| {
|
|
95
97
|
readonly kind: 'cancelled';
|
|
96
98
|
} | Unsupported;
|
|
99
|
+
export interface ExportLinkedSetRequest {
|
|
100
|
+
/** The root note's path, in whatever form `adapter.readFile` and `resolveLink` understand. */
|
|
101
|
+
readonly rootPath: string;
|
|
102
|
+
/** Resolves one link in a note to the path of a note this host can export, or `undefined` to leave the link as written. */
|
|
103
|
+
readonly resolveLink: CascadeLinkResolver;
|
|
104
|
+
/** Reads one note's persisted last-run values, keyed by that note's own path. An empty object exports that note's standard empty states. */
|
|
105
|
+
readonly readValues: (notePath: string) => Record<string, StoredValue>;
|
|
106
|
+
/** The host's own render of each note's body. Omitted renders every note with the static engine, and `staticReason` says why. */
|
|
107
|
+
readonly renderBody?: ExportBodyRenderer;
|
|
108
|
+
/** Why the static engine renders when `renderBody` is omitted. Defaults to `no-packs`. */
|
|
109
|
+
readonly staticReason?: StaticExportReason;
|
|
110
|
+
/** The loaded packs' stylesheets, embedded only when `renderBody` ran. */
|
|
111
|
+
readonly packStylesheets?: readonly ExportPackStylesheet[];
|
|
112
|
+
/** How many loaded packs contributed to the render. Diagnostics only. */
|
|
113
|
+
readonly packCount?: number;
|
|
114
|
+
/** Installed pack components the static engine cannot draw. */
|
|
115
|
+
readonly unavailableComponents?: readonly UnavailableComponent[];
|
|
116
|
+
/** Receives the static engine's render diagnostics for every note. */
|
|
117
|
+
readonly onDiagnostic?: OnDiagnostic;
|
|
118
|
+
/** Builds the image reader for one note, bound to that note's own folder. Omitted embeds no images. */
|
|
119
|
+
readonly embedImagesFor?: (notePath: string) => ExportImageReader;
|
|
120
|
+
/** Hides the collapsed script marker in every exported note. Defaults to `false`. */
|
|
121
|
+
readonly hideScriptBlocks?: boolean;
|
|
122
|
+
}
|
|
123
|
+
export type ExportLinkedSetOutcome = Extract<CascadeExportOutcome, {
|
|
124
|
+
kind: 'exported';
|
|
125
|
+
}> | {
|
|
126
|
+
readonly kind: 'failed';
|
|
127
|
+
readonly path?: string;
|
|
128
|
+
readonly reason: string;
|
|
129
|
+
}
|
|
130
|
+
/** The host's picker was cancelled, so nothing was written. */
|
|
131
|
+
| {
|
|
132
|
+
readonly kind: 'cancelled';
|
|
133
|
+
} | Unsupported;
|
|
97
134
|
export interface InstallPackRequest {
|
|
98
135
|
readonly archiveBytes: Uint8Array;
|
|
99
136
|
/** The archive's own path or display name, used only in wording. Defaults to a generic phrase when omitted. */
|
|
@@ -218,6 +255,8 @@ export interface MarkiiHost {
|
|
|
218
255
|
open(request: OpenRequest): Promise<OpenOutcome>;
|
|
219
256
|
run(request: RunRequest): Promise<RunOutcome>;
|
|
220
257
|
exportNote(request: ExportRequest): Promise<ExportOutcome>;
|
|
258
|
+
/** Exports a note and the notes it links to as one zip archive of HTML files. */
|
|
259
|
+
exportLinkedSet(request: ExportLinkedSetRequest): Promise<ExportLinkedSetOutcome>;
|
|
221
260
|
installPack(request: InstallPackRequest): Promise<InstallPackOutcome>;
|
|
222
261
|
loadPacks(): Promise<LoadPacksOutcome>;
|
|
223
262
|
/**
|
|
@@ -265,7 +304,5 @@ export interface MarkiiHostOptions {
|
|
|
265
304
|
readonly packDiscoveryIo?: PackDiscoveryIo;
|
|
266
305
|
/** `@markii/ansi`'s renderer, injected because this package never depends on `@markii/ansi` (a platform renderer, never a `@markii/host` dependency). A host declaring `'ansi'` in `adapter.exports.formats` without supplying this gets `Unsupported` at export time. */
|
|
267
306
|
readonly renderAnsi?: (text: string) => Promise<string>;
|
|
268
|
-
/** Composes a pack-aware render registry (webview registration, in-process evaluation, ...). Reserved for a future slice that renders through `exportNote`'s `html` path with packs; unused today, since `buildExportDocument`'s default (no `renderBody`) already produces a correct, pack-free static export. */
|
|
269
|
-
readonly buildPackRegistration?: unknown;
|
|
270
307
|
}
|
|
271
308
|
export declare function createMarkiiHost(adapter: HostAdapter, options?: MarkiiHostOptions): MarkiiHost;
|
package/dist/host/create-host.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { runViaAdapter, } from './run-behavior.js';
|
|
2
2
|
import { installPackDiagnosticLines, installPackFromArchive, } from './pack-install.js';
|
|
3
3
|
import { loadPacksViaAdapter, packDiscoveryIoFromAdapter, } from './pack-load.js';
|
|
4
|
+
import { exportBaseName } from '../export/note-export.js';
|
|
5
|
+
import { exportLinkedSetViaAdapter, } from './cascade-export-behavior.js';
|
|
4
6
|
import { buildComponentCatalog } from '../insert/component-catalog.js';
|
|
5
7
|
import { formatPackDiagnosticLines, skippedPackFolderLine, } from '../packs/pack-diagnostics.js';
|
|
6
8
|
import { readTerminalPackScripts, } from './terminal-packs.js';
|
|
@@ -136,6 +138,24 @@ export function createMarkiiHost(adapter, options = {}) {
|
|
|
136
138
|
images: outcome.images,
|
|
137
139
|
};
|
|
138
140
|
},
|
|
141
|
+
async exportLinkedSet(request) {
|
|
142
|
+
if (adapter.exports?.resolveLinkedSetTarget === undefined) {
|
|
143
|
+
return unsupported(adapter, 'linked-set-export', `the linked-set export is not available on ${adapter.id}.`);
|
|
144
|
+
}
|
|
145
|
+
if (options.htmlEngine === undefined) {
|
|
146
|
+
return unsupported(adapter, 'linked-set-export', 'no HTML export engine was supplied, so the linked-set export cannot render.');
|
|
147
|
+
}
|
|
148
|
+
const outcome = await exportLinkedSetViaAdapter(adapter, {
|
|
149
|
+
...request,
|
|
150
|
+
engine: options.htmlEngine,
|
|
151
|
+
suggestedFileName: `${exportBaseName(request.rootPath)}.zip`,
|
|
152
|
+
});
|
|
153
|
+
if (outcome === undefined) {
|
|
154
|
+
adapter.diagnostics(`linked-set export declined: no archive target was chosen on ${adapter.id}.`);
|
|
155
|
+
return { kind: 'cancelled' };
|
|
156
|
+
}
|
|
157
|
+
return outcome;
|
|
158
|
+
},
|
|
139
159
|
async installPack(request) {
|
|
140
160
|
if (!adapter.packs?.installRoot) {
|
|
141
161
|
return unsupported(adapter, 'packs', 'this host has no pack-install destination configured.');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The shared pack-trust behavior (docs/packs.md, docs/
|
|
2
|
+
* The shared pack-trust behavior (docs/packs.md, docs/hosts.md's host
|
|
3
3
|
* checklist). A host that declares `HostPackSource.trust: 'device-list'`
|
|
4
4
|
* loads an installed pack only when this device's trust list names its
|
|
5
5
|
* namespace. A folder that sits in the install root without being named
|
|
@@ -74,6 +74,10 @@ export interface RunViaAdapterRequest {
|
|
|
74
74
|
/** A run's result plus the `RunTrace` it just recorded, so a host can show its in-note marker from the same value the shared path persisted. */
|
|
75
75
|
export type RunViaAdapterResult = RunOnceResult & {
|
|
76
76
|
readonly trace: RunTrace;
|
|
77
|
+
/** How many of this run's values came back with the value store's `fresh` status. */
|
|
78
|
+
readonly freshCount: number;
|
|
79
|
+
/** How many of this run's values came back with the value store's `error` status. */
|
|
80
|
+
readonly errorCount: number;
|
|
77
81
|
};
|
|
78
82
|
/**
|
|
79
83
|
* Runs one manual, auto, or scheduled pass of `request.text`'s scripts,
|
|
@@ -185,5 +185,13 @@ export async function runViaAdapter(adapter, request, overrideSpawnRun) {
|
|
|
185
185
|
// must never turn a successful (or already-reported) run into a thrown
|
|
186
186
|
// error.
|
|
187
187
|
await writeLastRunTrace(adapter.memento, request.documentKey, trace).then(undefined, () => { });
|
|
188
|
-
|
|
188
|
+
let freshCount = 0;
|
|
189
|
+
let errorCount = 0;
|
|
190
|
+
for (const value of Object.values(result.values)) {
|
|
191
|
+
if (value.status === 'fresh')
|
|
192
|
+
freshCount += 1;
|
|
193
|
+
else if (value.status === 'error')
|
|
194
|
+
errorCount += 1;
|
|
195
|
+
}
|
|
196
|
+
return { ...result, trace, freshCount, errorCount };
|
|
189
197
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* discovers the authorized packs and reads each one's `terminal.js` text
|
|
7
7
|
* through this module; the host then hands the text to the engine's own
|
|
8
8
|
* `loadTerminalPackScript`. Everything here is the one home of the wording
|
|
9
|
-
* for the failures in between (docs/
|
|
9
|
+
* for the failures in between (docs/hosts.md's "clean is not silent":
|
|
10
10
|
* a failure reaches the diagnostics surface and the note shows the pack's
|
|
11
11
|
* components as a failed pack).
|
|
12
12
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export type { DisablePackOutcome, EnabledPack, EnablePackOutcome, InstalledPacks
|
|
|
25
25
|
export { disablePackMessage, enablePackMessage, } from './host/pack-trust-behavior.js';
|
|
26
26
|
export { removePackMessage } from './host/pack-remove.js';
|
|
27
27
|
export { exportResultMessage } from './host/export-behavior.js';
|
|
28
|
+
export { cascadeResultMessage } from './host/cascade-export-behavior.js';
|
|
29
|
+
export type { CascadeLinkResolver, CascadeTruncation, } from './export/cascade.js';
|
|
30
|
+
export type { CascadeExportedNote, UnreadableCascadeNote, } from './host/cascade-export-behavior.js';
|
|
28
31
|
export { createMarkiiHost } from './host/create-host.js';
|
|
29
|
-
export type { CompleteOutcome, EditorPositionRequest, ExportOutcome, ExportRequest, HoverOutcome, InsertComponentRequest, InstallPackOutcome, InstallPackRequest, LoadPacksOutcome, LoadTerminalPacksOutcome, MarkiiHost, MarkiiHostOptions, OpenOutcome, OpenRequest, RemovePackResult, DisablePackResult, EnablePackResult, InstalledPacksOutcome, RunOutcome, RunRequest, Unsupported, } from './host/create-host.js';
|
|
32
|
+
export type { CompleteOutcome, EditorPositionRequest, ExportLinkedSetOutcome, ExportLinkedSetRequest, ExportOutcome, ExportRequest, HoverOutcome, InsertComponentRequest, InstallPackOutcome, InstallPackRequest, LoadPacksOutcome, LoadTerminalPacksOutcome, MarkiiHost, MarkiiHostOptions, OpenOutcome, OpenRequest, RemovePackResult, DisablePackResult, EnablePackResult, InstalledPacksOutcome, RunOutcome, RunRequest, Unsupported, } from './host/create-host.js';
|
|
30
33
|
export { netRefusal } from './run/net-refusal.js';
|
package/dist/index.js
CHANGED
|
@@ -21,5 +21,6 @@ export { installPackMessage } from './host/pack-install.js';
|
|
|
21
21
|
export { disablePackMessage, enablePackMessage, } from './host/pack-trust-behavior.js';
|
|
22
22
|
export { removePackMessage } from './host/pack-remove.js';
|
|
23
23
|
export { exportResultMessage } from './host/export-behavior.js';
|
|
24
|
+
export { cascadeResultMessage } from './host/cascade-export-behavior.js';
|
|
24
25
|
export { createMarkiiHost } from './host/create-host.js';
|
|
25
26
|
export { netRefusal } from './run/net-refusal.js';
|
package/dist/internal.d.ts
CHANGED
|
@@ -10,9 +10,7 @@ export { readLastRunTrace } from './run/run-trace.js';
|
|
|
10
10
|
export { staleValuesForRehydration } from './run/stale-values.js';
|
|
11
11
|
export { MARK_EXTENSION, buildNoteHtmlExport, composeNoteHtmlExport, noteHasScripts, exportBaseName, exportedSiblingPath, } from './export/note-export.js';
|
|
12
12
|
export { EMPTY_IMAGE_REPORT, MAX_EMBEDDED_IMAGE_BYTES, embedImagesInHtml, } from './export/image-embed.js';
|
|
13
|
-
export
|
|
14
|
-
export type { CascadeArchiveRequest, CascadeExportOutcome, } from './host/cascade-export-behavior.js';
|
|
15
|
-
export { buildCascadeArchive, cascadeDiagnosticLines, cascadeReasonOf, cascadeResultMessage, } from './host/cascade-export-behavior.js';
|
|
13
|
+
export { cascadeDiagnosticLines } from './host/cascade-export-behavior.js';
|
|
16
14
|
export { componentsWithoutEngineView, declaredPackComponents, installedNamespaces, } from './packs/discover.js';
|
|
17
15
|
export { loadPackModules } from './packs/pack-scripts.js';
|
|
18
16
|
export { relativePackEntries, resolvePackPaths } from './packs/pack-paths.js';
|
package/dist/internal.js
CHANGED
|
@@ -14,7 +14,7 @@ export { readLastRunTrace } from './run/run-trace.js';
|
|
|
14
14
|
export { staleValuesForRehydration } from './run/stale-values.js';
|
|
15
15
|
export { MARK_EXTENSION, buildNoteHtmlExport, composeNoteHtmlExport, noteHasScripts, exportBaseName, exportedSiblingPath, } from './export/note-export.js';
|
|
16
16
|
export { EMPTY_IMAGE_REPORT, MAX_EMBEDDED_IMAGE_BYTES, embedImagesInHtml, } from './export/image-embed.js';
|
|
17
|
-
export {
|
|
17
|
+
export { cascadeDiagnosticLines } from './host/cascade-export-behavior.js';
|
|
18
18
|
export { componentsWithoutEngineView, declaredPackComponents, installedNamespaces, } from './packs/discover.js';
|
|
19
19
|
export { loadPackModules } from './packs/pack-scripts.js';
|
|
20
20
|
export { relativePackEntries, resolvePackPaths } from './packs/pack-paths.js';
|
package/dist/run/bundle-run.d.ts
CHANGED
|
@@ -141,7 +141,7 @@ export declare function manifestNetHosts(manifest: BundleManifest): string[];
|
|
|
141
141
|
*
|
|
142
142
|
* A mismatch between the two is not silently absorbed: this pure function
|
|
143
143
|
* returns one diagnostics line per mismatched host, in both directions, for
|
|
144
|
-
* the host's diagnostics surface (docs/
|
|
144
|
+
* the host's diagnostics surface (docs/hosts.md) — never the rendered
|
|
145
145
|
* page. Wording lives here, the one place both apps' adapters read it from,
|
|
146
146
|
* so it can never drift between VS Code's output channel and Obsidian's
|
|
147
147
|
* console/notice surface.
|
package/dist/run/bundle-run.js
CHANGED
|
@@ -142,7 +142,7 @@ export function manifestNetHosts(manifest) {
|
|
|
142
142
|
*
|
|
143
143
|
* A mismatch between the two is not silently absorbed: this pure function
|
|
144
144
|
* returns one diagnostics line per mismatched host, in both directions, for
|
|
145
|
-
* the host's diagnostics surface (docs/
|
|
145
|
+
* the host's diagnostics surface (docs/hosts.md) — never the rendered
|
|
146
146
|
* page. Wording lives here, the one place both apps' adapters read it from,
|
|
147
147
|
* so it can never drift between VS Code's output channel and Obsidian's
|
|
148
148
|
* console/notice surface.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/host",
|
|
3
|
-
"version": "0.17.0-next.
|
|
3
|
+
"version": "0.17.0-next.5",
|
|
4
4
|
"description": "Shared, host-neutral script-running layer for Markii hosts: tier gating, the grant model, address pinning, and the terminatable-isolate watchdog, plus the HostAdapter contract and createMarkiiHost. Entries: the neutral main entry, /internal (no semver promise), /node, /html-engine, /browser, /browser-worker-classic, /conformance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -71,20 +71,20 @@
|
|
|
71
71
|
"build:dist": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-dist-assets.mjs"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@markii/bundle": "^0.17.0-next.
|
|
75
|
-
"@markii/core": "^0.17.0-next.
|
|
76
|
-
"@markii/lua": "^0.17.0-next.
|
|
77
|
-
"@markii/pack": "^0.17.0-next.
|
|
78
|
-
"@markii/runtime": "^0.17.0-next.
|
|
79
|
-
"@markii/stdlib": "^0.17.0-next.
|
|
74
|
+
"@markii/bundle": "^0.17.0-next.5",
|
|
75
|
+
"@markii/core": "^0.17.0-next.5",
|
|
76
|
+
"@markii/lua": "^0.17.0-next.5",
|
|
77
|
+
"@markii/pack": "^0.17.0-next.5",
|
|
78
|
+
"@markii/runtime": "^0.17.0-next.5",
|
|
79
|
+
"@markii/stdlib": "^0.17.0-next.5",
|
|
80
80
|
"fflate": "^0.8.3"
|
|
81
81
|
},
|
|
82
82
|
"files": [
|
|
83
83
|
"dist"
|
|
84
84
|
],
|
|
85
85
|
"peerDependencies": {
|
|
86
|
-
"@markii/html": "^0.17.0-next.
|
|
87
|
-
"@markii/react": "^0.17.0-next.
|
|
86
|
+
"@markii/html": "^0.17.0-next.5",
|
|
87
|
+
"@markii/react": "^0.17.0-next.5"
|
|
88
88
|
},
|
|
89
89
|
"peerDependenciesMeta": {
|
|
90
90
|
"@markii/html": {
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
|
-
"@markii/html": "^0.17.0-next.
|
|
99
|
-
"@markii/react": "^0.17.0-next.
|
|
98
|
+
"@markii/html": "^0.17.0-next.5",
|
|
99
|
+
"@markii/react": "^0.17.0-next.5",
|
|
100
100
|
"esbuild": "^0.28.2"
|
|
101
101
|
},
|
|
102
102
|
"publishConfig": {
|