@markii/host 0.17.0-next.3 → 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 +6 -0
- package/dist/browser-worker.classic.js +640 -0
- package/dist/browser-worker.js +19 -19
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +10 -3
- 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 +4 -2
- package/dist/host/conformance-runner.d.ts +0 -3
- package/dist/host/conformance-runner.js +18 -6
- package/dist/host/create-host.d.ts +25 -5
- package/dist/host/create-host.js +34 -23
- package/dist/host/export-behavior.d.ts +2 -1
- package/dist/host/export-behavior.js +1 -1
- package/dist/host/labels.d.ts +5 -14
- package/dist/host/labels.js +8 -22
- package/dist/host/pack-archive.d.ts +4 -10
- package/dist/host/pack-archive.js +11 -14
- package/dist/host/pack-install.d.ts +10 -21
- package/dist/host/pack-install.js +22 -16
- package/dist/html-engine.js +4 -4
- package/dist/index.d.ts +15 -80
- package/dist/index.js +18 -62
- 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 +36 -15
- package/dist/node.d.ts +1 -2
- package/dist/node.js +4 -3
- package/dist/packs/discover.d.ts +2 -0
- package/dist/packs/discover.js +1 -0
- 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.js +23 -12
- package/dist/run/net-refusal.d.ts +4 -0
- package/dist/run/net-refusal.js +24 -0
- package/package.json +28 -12
package/dist/browser.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface FetchNetProviderOptions {
|
|
|
11
11
|
* every request. Entries that do not parse as an http(s) URL are ignored.
|
|
12
12
|
*/
|
|
13
13
|
allowedOrigins?: readonly string[];
|
|
14
|
-
/** 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. */
|
|
15
15
|
denial?: (message: string) => Error;
|
|
16
16
|
/** The `fetch` to use. Defaults to the global one; injectable for tests. */
|
|
17
17
|
fetch?: typeof fetch;
|
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);
|
|
@@ -82,7 +83,10 @@ export function createFetchNetProvider(options = {}) {
|
|
|
82
83
|
const allowed = new Set((options.allowedOrigins ?? [])
|
|
83
84
|
.map(originOf)
|
|
84
85
|
.filter((origin) => origin !== undefined));
|
|
85
|
-
const
|
|
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));
|
|
86
90
|
return (netAllowlist, maxFetchBytes, _policy, signal) => {
|
|
87
91
|
// The run's granted hosts, re-checked here on the page side: the worker
|
|
88
92
|
// checks them too, but the worker is the isolate a hostile script runs
|
|
@@ -110,7 +114,7 @@ export function createFetchNetProvider(options = {}) {
|
|
|
110
114
|
throw denial(`origin "${url.origin}" is not on the host's allowlist`);
|
|
111
115
|
}
|
|
112
116
|
if (signal?.aborted)
|
|
113
|
-
throw
|
|
117
|
+
throw new Error('the run has ended');
|
|
114
118
|
const doFetch = options.fetch ?? fetch;
|
|
115
119
|
let response;
|
|
116
120
|
try {
|
|
@@ -126,7 +130,10 @@ export function createFetchNetProvider(options = {}) {
|
|
|
126
130
|
});
|
|
127
131
|
}
|
|
128
132
|
catch (error) {
|
|
129
|
-
|
|
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)}`);
|
|
130
137
|
}
|
|
131
138
|
if (response.type === 'opaqueredirect') {
|
|
132
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
|
@@ -150,7 +150,7 @@ export interface HostPackSource {
|
|
|
150
150
|
* `authorizedFolders` names.
|
|
151
151
|
*/
|
|
152
152
|
bundledPacks?(): readonly DiscoveredPack[];
|
|
153
|
-
/** 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. */
|
|
154
154
|
removeFolder?(path: string): Promise<void>;
|
|
155
155
|
}
|
|
156
156
|
/** Every export format any host offers today. */
|
|
@@ -194,10 +194,12 @@ export interface HostLabels {
|
|
|
194
194
|
*.
|
|
195
195
|
*/
|
|
196
196
|
export interface HostAdapter {
|
|
197
|
-
|
|
197
|
+
/** A short name for the host, used in diagnostics wording. */
|
|
198
|
+
readonly id: string;
|
|
198
199
|
readonly labels: HostLabels;
|
|
199
200
|
readFile(path: string): Promise<Uint8Array>;
|
|
200
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. */
|
|
201
203
|
writeFile(path: string, bytes: Uint8Array): Promise<void>;
|
|
202
204
|
/** A missing folder resolves to an empty list — never throws, matching the existing `PackDirectoryLister` contract. */
|
|
203
205
|
listFolder(path: string): Promise<readonly HostDirEntry[]>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ArchiveExtractFs } from './pack-archive.js';
|
|
2
1
|
import type { RunTrigger } from '@markii/runtime';
|
|
3
2
|
import type { HostAdapter } from './adapter.js';
|
|
4
3
|
/**
|
|
@@ -135,8 +134,6 @@ export interface HostScenarioRunResult {
|
|
|
135
134
|
*/
|
|
136
135
|
export declare function runHostScenario(adapter: HostAdapter, scenario: HostScenario, noteText: string, scenarioDir: string, options?: {
|
|
137
136
|
readonly renderAnsi?: (text: string) => Promise<string>;
|
|
138
|
-
/** Where an installed pack archive is extracted. Defaults to the Node file system, which is right for an adapter that reads and writes real files. An adapter over another store (memory, a virtual file system) passes the matching writer. */
|
|
139
|
-
readonly archiveExtractFs?: ArchiveExtractFs;
|
|
140
137
|
}): Promise<HostScenarioRunResult>;
|
|
141
138
|
/** `expected[i]` compared against `actual[i]` for each index up to `expected`'s own length; a length mismatch is reported once rather than index-by-index noise. */
|
|
142
139
|
export declare function containsInOrder(actual: readonly string[], expected: readonly string[]): string[];
|
|
@@ -40,7 +40,6 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
40
40
|
import { dirname } from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { createHtmlExportEngine } from '../html-engine.js';
|
|
43
|
-
import { createNodeArchiveExtractFs } from '../node-fs.js';
|
|
44
43
|
import { usesPackTrust } from './pack-trust-behavior.js';
|
|
45
44
|
import { join, relative, sep } from 'node:path';
|
|
46
45
|
import { zipSync } from 'fflate';
|
|
@@ -140,10 +139,7 @@ export function hostDeclaresCapability(adapter, action) {
|
|
|
140
139
|
if (capability === 'packs') {
|
|
141
140
|
if (adapter.packs === undefined)
|
|
142
141
|
return false;
|
|
143
|
-
if (action.type === 'installPack') {
|
|
144
|
-
return adapter.packs.installRoot !== undefined;
|
|
145
|
-
}
|
|
146
|
-
if (action.type === 'removePack') {
|
|
142
|
+
if (action.type === 'installPack' || action.type === 'removePack') {
|
|
147
143
|
return (adapter.packs.installRoot !== undefined &&
|
|
148
144
|
adapter.packs.removeFolder !== undefined);
|
|
149
145
|
}
|
|
@@ -262,7 +258,6 @@ export async function runHostScenario(adapter, scenario, noteText, scenarioDir,
|
|
|
262
258
|
// The runner is Node-only test tooling, so it supplies the real Node
|
|
263
259
|
// implementations of the seams the host layer takes by injection.
|
|
264
260
|
htmlEngine: createHtmlExportEngine(),
|
|
265
|
-
archiveExtractFs: options.archiveExtractFs ?? createNodeArchiveExtractFs(),
|
|
266
261
|
spawnRun: (spawnOptions) => Promise.resolve(simulatedRunResult(currentSimulation, spawnOptions)),
|
|
267
262
|
...(options.renderAnsi ? { renderAnsi: options.renderAnsi } : {}),
|
|
268
263
|
});
|
|
@@ -452,6 +447,16 @@ export function outcomeMismatches(actual, expected) {
|
|
|
452
447
|
break;
|
|
453
448
|
}
|
|
454
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
|
+
}
|
|
455
460
|
const content = String(actualRecord['content'] ?? '');
|
|
456
461
|
for (const needle of expected['contains'] ?? []) {
|
|
457
462
|
if (!content.includes(needle)) {
|
|
@@ -466,6 +471,13 @@ export function outcomeMismatches(actual, expected) {
|
|
|
466
471
|
break;
|
|
467
472
|
}
|
|
468
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
|
+
}
|
|
469
481
|
if (expected['namespace'] !== undefined &&
|
|
470
482
|
actualRecord['namespace'] !== expected['namespace']) {
|
|
471
483
|
mismatches.push(`namespace: expected "${String(expected['namespace'])}", got "${String(actualRecord['namespace'])}"`);
|
|
@@ -18,9 +18,9 @@ import type { StoredValue } from '@markii/runtime';
|
|
|
18
18
|
import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
|
|
19
19
|
import type { CompletionItem, ComponentDocumentation, LineColumn } from '@markii/stdlib/editor';
|
|
20
20
|
import { type RunViaAdapterRequest, type RunViaAdapterResult } from './run-behavior.js';
|
|
21
|
-
import { type ArchiveExtractFs } from './pack-archive.js';
|
|
22
21
|
import { type PackDiscoveryIo } from './pack-load.js';
|
|
23
|
-
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';
|
|
24
24
|
import type { DiscoveredPack, SkippedPackFolder } from '../packs/discover.js';
|
|
25
25
|
import { type TerminalPackScript } from './terminal-packs.js';
|
|
26
26
|
import { type RemovePackOutcome } from './pack-remove.js';
|
|
@@ -61,11 +61,31 @@ export interface ExportRequest {
|
|
|
61
61
|
readonly unavailableComponents?: readonly UnavailableComponent[];
|
|
62
62
|
/** Receives the static engine's render diagnostics, such as the `no-engine-view` event. */
|
|
63
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;
|
|
64
76
|
}
|
|
65
77
|
export type ExportOutcome = {
|
|
66
78
|
readonly kind: 'exported';
|
|
67
79
|
readonly path: string;
|
|
68
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;
|
|
69
89
|
} | {
|
|
70
90
|
readonly kind: 'failed';
|
|
71
91
|
readonly reason: string;
|
|
@@ -82,6 +102,8 @@ export interface InstallPackRequest {
|
|
|
82
102
|
export type InstallPackOutcome = {
|
|
83
103
|
readonly kind: 'installed';
|
|
84
104
|
readonly namespace: string;
|
|
105
|
+
/** The folder the pack now lives in, under the adapter's install root. */
|
|
106
|
+
readonly installedDir: string;
|
|
85
107
|
readonly replaced: boolean;
|
|
86
108
|
} | {
|
|
87
109
|
readonly kind: 'declined';
|
|
@@ -237,9 +259,7 @@ export interface MarkiiHost {
|
|
|
237
259
|
*/
|
|
238
260
|
export interface MarkiiHostOptions {
|
|
239
261
|
readonly spawnRun?: (options: SpawnRunRequest) => Promise<RunResult>;
|
|
240
|
-
/** The
|
|
241
|
-
readonly archiveExtractFs?: ArchiveExtractFs;
|
|
242
|
-
/** 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`. */
|
|
243
263
|
readonly htmlEngine?: HtmlExportEngine;
|
|
244
264
|
/** Pack discovery's file reads. Defaults to reads through the adapter's own `readFile` and `listFolder`. */
|
|
245
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) {
|
|
@@ -30,6 +30,7 @@ import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
|
|
|
30
30
|
import type { HtmlExportEngine, ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, StaticExportReason } from '../export/note-export.js';
|
|
31
31
|
import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
|
|
32
32
|
import type { HostAdapter, HostExportFormat, HostLabels } from './adapter.js';
|
|
33
|
+
import type { ExportOutcome } from './create-host.js';
|
|
33
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. */
|
|
34
35
|
export declare function exportDefaultFileName(notePath: string, format: HostExportFormat): string;
|
|
35
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. */
|
|
@@ -58,7 +59,7 @@ export declare function fileNameOf(path: string): string;
|
|
|
58
59
|
* host's own diagnostics surface (`labels.diagnosticsSurface`), never at
|
|
59
60
|
* a stack trace — the verbatim reason goes there instead.
|
|
60
61
|
*/
|
|
61
|
-
export declare function exportResultMessage(outcome:
|
|
62
|
+
export declare function exportResultMessage(outcome: ExportOutcome, labels: HostLabels): string;
|
|
62
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. */
|
|
63
64
|
export declare function renderEngineDiagnosticLine(render: ExportRenderInfo): string;
|
|
64
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);
|
package/dist/host/labels.d.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* lets one template serve every host.
|
|
2
|
+
* The shared wording templates a host's shared behaviors interpolate
|
|
3
|
+
* `HostLabels` into live beside each behavior. This module holds the one
|
|
4
|
+
* label sentence that has no per-host noun. Each app owns its own
|
|
5
|
+
* `HostLabels` record and passes it as `HostAdapter.labels`.
|
|
7
6
|
*
|
|
8
|
-
* Pure and dependency-free
|
|
9
|
-
* `../index.ts` so VS Code's Node-free webview bundle can read it too.
|
|
7
|
+
* Pure and dependency-free.
|
|
10
8
|
*/
|
|
11
|
-
import type { HostLabels } from './adapter.js';
|
|
12
|
-
/** The VS Code extension's labels, taken from its current wording (`apps/vscode/src/script-execution.ts`, `apps/vscode/src/extension.ts`'s output channel). */
|
|
13
|
-
export declare const VSCODE_LABELS: HostLabels;
|
|
14
|
-
/** The Obsidian plugin's labels, taken from its current wording (`apps/obsidian/src/script-execution.ts`, docs/integration.md's host checklist). */
|
|
15
|
-
export declare const OBSIDIAN_LABELS: HostLabels;
|
|
16
|
-
/** The `markii` command line tool's labels: it has no persistent preview panel, and its diagnostics surface is stderr, gated behind `--verbose` for anything short of a failure (`apps/cli/src/main.ts`). */
|
|
17
|
-
export declare const CLI_LABELS: HostLabels;
|
|
18
9
|
/**
|
|
19
10
|
* The quiet marker a rendered note shows when its `uses:` list names packs
|
|
20
11
|
* this host has not installed (docs/packs.md). One sentence for every host;
|
package/dist/host/labels.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** The Obsidian plugin's labels, taken from its current wording (`apps/obsidian/src/script-execution.ts`, docs/integration.md's host checklist). */
|
|
10
|
-
export const OBSIDIAN_LABELS = {
|
|
11
|
-
appNoun: 'plugin',
|
|
12
|
-
previewNoun: 'the Markii preview',
|
|
13
|
-
diagnosticsSurface: 'the Markii diagnostics',
|
|
14
|
-
deviceNoun: 'this device',
|
|
15
|
-
};
|
|
16
|
-
/** The `markii` command line tool's labels: it has no persistent preview panel, and its diagnostics surface is stderr, gated behind `--verbose` for anything short of a failure (`apps/cli/src/main.ts`). */
|
|
17
|
-
export const CLI_LABELS = {
|
|
18
|
-
appNoun: 'command line tool',
|
|
19
|
-
previewNoun: 'the rendered note',
|
|
20
|
-
diagnosticsSurface: 'stderr',
|
|
21
|
-
deviceNoun: 'this device',
|
|
22
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* The shared wording templates a host's shared behaviors interpolate
|
|
3
|
+
* `HostLabels` into live beside each behavior. This module holds the one
|
|
4
|
+
* label sentence that has no per-host noun. Each app owns its own
|
|
5
|
+
* `HostLabels` record and passes it as `HostAdapter.labels`.
|
|
6
|
+
*
|
|
7
|
+
* Pure and dependency-free.
|
|
8
|
+
*/
|
|
23
9
|
/**
|
|
24
10
|
* The quiet marker a rendered note shows when its `uses:` list names packs
|
|
25
11
|
* this host has not installed (docs/packs.md). One sentence for every host;
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import type { PackArchiveContents, PackArchiveError } from '@markii/pack';
|
|
2
2
|
/** One line describing why `openPackArchive` rejected an archive, for a host's install-diagnostics wording. Passes `kind: 'zip'`'s and `kind: 'missing-entry'`'s message through verbatim (already specific); summarizes a manifest failure plainly. */
|
|
3
3
|
export declare function describeArchiveError(error: PackArchiveError): string;
|
|
4
|
-
/**
|
|
5
|
-
export
|
|
6
|
-
/** Removes `absolutePath` and everything under it, or resolves quietly if it does not exist. Never rejects: a directory that fails to clear is treated the same as a fresh one, and the write that follows fails loudly on its own if the path is genuinely unusable. */
|
|
7
|
-
readonly removeDirectory: (absolutePath: string) => Promise<void>;
|
|
8
|
-
/** Creates a directory and any missing parents. May reject on a genuine I/O failure. */
|
|
9
|
-
readonly makeDirectory: (absolutePath: string) => Promise<void>;
|
|
10
|
-
readonly writeFile: (absolutePath: string, bytes: Uint8Array) => Promise<void>;
|
|
11
|
-
}
|
|
4
|
+
/** Writes one file and creates its missing parent folders: `HostAdapter.writeFile`. */
|
|
5
|
+
export type ArchiveFileWriter = (absolutePath: string, bytes: Uint8Array) => Promise<void>;
|
|
12
6
|
/**
|
|
13
7
|
* Writes a validated archive's contents into `destinationDir`: `pack.json`
|
|
14
8
|
* (the parsed manifest, re-serialized: `openPackArchive` keeps only the
|
|
@@ -17,7 +11,7 @@ export interface ArchiveExtractFs {
|
|
|
17
11
|
* copy), `webview.js`, `webview.css` and `terminal.js` when the archive has them, and
|
|
18
12
|
* `scripts/*` when it ships any. Does NOT clear `destinationDir` first;
|
|
19
13
|
* a caller that needs a clean re-extract (a reinstall replacing an
|
|
20
|
-
* existing pack)
|
|
14
|
+
* existing pack) removes the folder itself before this.
|
|
21
15
|
*
|
|
22
16
|
* PATH JAIL: `@markii/pack`'s `openPackArchive` has already rejected any
|
|
23
17
|
* entry whose name escapes the archive (`../`, an absolute path) or
|
|
@@ -27,4 +21,4 @@ export interface ArchiveExtractFs {
|
|
|
27
21
|
* `webview.css`, or a `scripts/`-relative Lua module name), never from
|
|
28
22
|
* anything read back out of the zip's raw entry names.
|
|
29
23
|
*/
|
|
30
|
-
export declare function writeArchiveContents(archive: PackArchiveContents, destinationDir: string,
|
|
24
|
+
export declare function writeArchiveContents(archive: PackArchiveContents, destinationDir: string, writeFile: ArchiveFileWriter): Promise<void>;
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `.mkp` pack archive handling, merged from `apps/vscode/src/packs/archive-packs.ts`
|
|
3
3
|
* and `apps/obsidian/src/packs/archive-packs.ts`. It exports
|
|
4
|
-
* `describeArchiveError`, `writeArchiveContents` and `
|
|
4
|
+
* `describeArchiveError`, `writeArchiveContents` and `ArchiveFileWriter`.
|
|
5
5
|
*
|
|
6
6
|
* This module bridges `@markii/pack`'s in-memory archive reader
|
|
7
7
|
* (`openPackArchive`, never modified here) into the write a pack install
|
|
8
8
|
* needs: extracting a validated archive's contents onto disk, under
|
|
9
9
|
* whichever install root the calling host names (`HostPackSource.installRoot`).
|
|
10
10
|
*
|
|
11
|
-
* Environment-free
|
|
12
|
-
*
|
|
13
|
-
* and is exported from `@markii/host/node`.
|
|
11
|
+
* Environment-free: the write goes through the host adapter's `writeFile`,
|
|
12
|
+
* which creates any missing parent folder.
|
|
14
13
|
*/
|
|
15
|
-
import {
|
|
14
|
+
import { joinPath } from '../path-util.js';
|
|
16
15
|
/** One line describing why `openPackArchive` rejected an archive, for a host's install-diagnostics wording. Passes `kind: 'zip'`'s and `kind: 'missing-entry'`'s message through verbatim (already specific); summarizes a manifest failure plainly. */
|
|
17
16
|
export function describeArchiveError(error) {
|
|
18
17
|
if (error.kind === 'zip')
|
|
@@ -29,7 +28,7 @@ export function describeArchiveError(error) {
|
|
|
29
28
|
* copy), `webview.js`, `webview.css` and `terminal.js` when the archive has them, and
|
|
30
29
|
* `scripts/*` when it ships any. Does NOT clear `destinationDir` first;
|
|
31
30
|
* a caller that needs a clean re-extract (a reinstall replacing an
|
|
32
|
-
* existing pack)
|
|
31
|
+
* existing pack) removes the folder itself before this.
|
|
33
32
|
*
|
|
34
33
|
* PATH JAIL: `@markii/pack`'s `openPackArchive` has already rejected any
|
|
35
34
|
* entry whose name escapes the archive (`../`, an absolute path) or
|
|
@@ -39,24 +38,22 @@ export function describeArchiveError(error) {
|
|
|
39
38
|
* `webview.css`, or a `scripts/`-relative Lua module name), never from
|
|
40
39
|
* anything read back out of the zip's raw entry names.
|
|
41
40
|
*/
|
|
42
|
-
export async function writeArchiveContents(archive, destinationDir,
|
|
43
|
-
await fs.makeDirectory(destinationDir);
|
|
41
|
+
export async function writeArchiveContents(archive, destinationDir, writeFile) {
|
|
44
42
|
const encoder = new TextEncoder();
|
|
45
|
-
await
|
|
46
|
-
await
|
|
43
|
+
await writeFile(joinPath(destinationDir, 'pack.json'), encoder.encode(JSON.stringify(archive.manifest, null, 2) + '\n'));
|
|
44
|
+
await writeFile(joinPath(destinationDir, 'webview.js'), archive.scriptBytes);
|
|
47
45
|
if (archive.stylesheetBytes !== undefined) {
|
|
48
|
-
await
|
|
46
|
+
await writeFile(joinPath(destinationDir, 'webview.css'), archive.stylesheetBytes);
|
|
49
47
|
}
|
|
50
48
|
if (archive.terminalScriptBytes !== undefined) {
|
|
51
|
-
await
|
|
49
|
+
await writeFile(joinPath(destinationDir, 'terminal.js'), archive.terminalScriptBytes);
|
|
52
50
|
}
|
|
53
51
|
const moduleNames = Object.keys(archive.scriptModules);
|
|
54
52
|
if (moduleNames.length > 0) {
|
|
55
53
|
const scriptsDir = joinPath(destinationDir, 'scripts');
|
|
56
54
|
for (const name of moduleNames) {
|
|
57
55
|
const target = joinPath(scriptsDir, name);
|
|
58
|
-
await
|
|
59
|
-
await fs.writeFile(target, archive.scriptModules[name]);
|
|
56
|
+
await writeFile(target, archive.scriptModules[name]);
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ArchiveExtractFs } from './pack-archive.js';
|
|
2
1
|
import type { HostAdapter, HostLabels } from './adapter.js';
|
|
2
|
+
import type { InstallPackOutcome as HostInstallPackOutcome } from './create-host.js';
|
|
3
3
|
/** Whether a directory already exists under the install root, injected so this module needs no real disk to test. */
|
|
4
4
|
export type PackDirectoryExists = (absolutePath: string) => Promise<boolean>;
|
|
5
5
|
export interface InstallPackFromArchiveOptions {
|
|
@@ -9,7 +9,8 @@ export interface InstallPackFromArchiveOptions {
|
|
|
9
9
|
/** This host's own pack-install directory (`HostPackSource.installRoot()`). Each installed pack gets a subdirectory named by its namespace, since two packs can never share one (docs/packs.md's collision rule), which makes "is this namespace already installed" a plain directory check. */
|
|
10
10
|
readonly installRoot: string;
|
|
11
11
|
readonly exists: PackDirectoryExists;
|
|
12
|
-
|
|
12
|
+
/** Removes the installed folder before a replacing install (`HostPackSource.removeFolder`). Called only when the namespace is already installed. */
|
|
13
|
+
readonly removeFolder: (absolutePath: string) => Promise<void>;
|
|
13
14
|
/**
|
|
14
15
|
* Namespaces this install may never claim (`HostPackSource.reservedNamespaces()`).
|
|
15
16
|
* Empty for a host with no reserved set (VS Code). Checked before the
|
|
@@ -17,22 +18,10 @@ export interface InstallPackFromArchiveOptions {
|
|
|
17
18
|
*/
|
|
18
19
|
readonly reservedNamespaces: ReadonlySet<string>;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
readonly
|
|
23
|
-
|
|
24
|
-
readonly replaced: boolean;
|
|
25
|
-
} | {
|
|
26
|
-
readonly kind: 'declined';
|
|
27
|
-
readonly step: 'consent' | 'replace';
|
|
28
|
-
readonly packName: string;
|
|
29
|
-
} | {
|
|
30
|
-
readonly kind: 'reserved';
|
|
31
|
-
readonly packName: string;
|
|
32
|
-
} | {
|
|
33
|
-
readonly kind: 'rejected';
|
|
34
|
-
readonly reason: string;
|
|
35
|
-
};
|
|
21
|
+
/** What one install attempt produced, in the shape `createMarkiiHost(...).installPack` returns (minus `unsupported`, which only the host behavior can say). */
|
|
22
|
+
export type InstallAttemptOutcome = Exclude<HostInstallPackOutcome, {
|
|
23
|
+
readonly kind: 'unsupported';
|
|
24
|
+
}>;
|
|
36
25
|
/**
|
|
37
26
|
* Never throws: a validation failure, a declined prompt, or a write
|
|
38
27
|
* failure all come back as a structured outcome. Order: validate the
|
|
@@ -47,7 +36,7 @@ export type InstallPackOutcome = {
|
|
|
47
36
|
* a host that can only show a non-blocking surface must deny rather than
|
|
48
37
|
* assume yes.
|
|
49
38
|
*/
|
|
50
|
-
export declare function installPackFromArchive(adapter: HostAdapter, options: InstallPackFromArchiveOptions): Promise<
|
|
39
|
+
export declare function installPackFromArchive(adapter: HostAdapter, options: InstallPackFromArchiveOptions): Promise<InstallAttemptOutcome>;
|
|
51
40
|
/**
|
|
52
41
|
* The consent prompt's wording (AGENTS.md's product principles: "clean is
|
|
53
42
|
* not silent," and a consent step must say plainly what it authorizes).
|
|
@@ -82,6 +71,6 @@ export declare function installReplaceConfirmMessage(packName: string): string;
|
|
|
82
71
|
* `markii.packs` before anything loads it), so it doesn't. Defaults to
|
|
83
72
|
* `false`.
|
|
84
73
|
*/
|
|
85
|
-
export declare function installPackMessage(outcome:
|
|
74
|
+
export declare function installPackMessage(outcome: HostInstallPackOutcome, archivePath: string, labels: HostLabels, reloadsAutomatically?: boolean): string;
|
|
86
75
|
/** The full diagnostics-surface detail for one install attempt (AGENTS.md's "clean is not silent": the other of a failure's two homes). */
|
|
87
|
-
export declare function installPackDiagnosticLines(outcome:
|
|
76
|
+
export declare function installPackDiagnosticLines(outcome: InstallAttemptOutcome, archivePath: string): string[];
|