@paged-media/plugin-sdk 0.2.5-canary.0 → 0.2.8-canary.0
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/dist/index.d.ts +480 -32
- package/dist/index.js +1312 -38
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -3,13 +3,6 @@ function defineBundle(bundle) {
|
|
|
3
3
|
return bundle;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
// src/harness.ts
|
|
7
|
-
function createHeadlessHost(_options) {
|
|
8
|
-
throw new Error(
|
|
9
|
-
"@paged-media/plugin-sdk: the headless harness is not implemented yet (tracked in plugin-draw/BREAKAGE_LOG.md). Run bundles inside the editor app, or unit-test their machines (pure, host-free) instead."
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
6
|
// src/disposables.ts
|
|
14
7
|
var DisposableStore = class {
|
|
15
8
|
items = [];
|
|
@@ -49,43 +42,88 @@ function toDisposable(fn) {
|
|
|
49
42
|
};
|
|
50
43
|
}
|
|
51
44
|
|
|
52
|
-
// src/
|
|
53
|
-
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
45
|
+
// src/widgets-fallback.tsx
|
|
46
|
+
import { createElement } from "react";
|
|
47
|
+
function TextareaCodeEditor(props) {
|
|
48
|
+
return createElement("textarea", {
|
|
49
|
+
value: props.value,
|
|
50
|
+
readOnly: props.readOnly,
|
|
51
|
+
spellCheck: false,
|
|
52
|
+
"aria-label": props.ariaLabel,
|
|
53
|
+
"data-code-editor-fallback": props.language ?? "text",
|
|
54
|
+
onChange: (e) => props.onChange(e.target.value),
|
|
55
|
+
style: {
|
|
56
|
+
width: "100%",
|
|
57
|
+
minHeight: props.minHeight ?? 96,
|
|
58
|
+
resize: "vertical",
|
|
59
|
+
font: "12px/1.5 var(--font-mono, monospace)",
|
|
60
|
+
color: "var(--pg-fg)",
|
|
61
|
+
background: "var(--pg-bg)",
|
|
62
|
+
border: "1px solid var(--pg-border)",
|
|
63
|
+
borderRadius: "var(--radius-sm, 4px)",
|
|
64
|
+
padding: "var(--space-2, 8px)",
|
|
65
|
+
boxSizing: "border-box"
|
|
71
66
|
}
|
|
72
|
-
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
var FALLBACK_WIDGETS = {
|
|
70
|
+
CodeEditor: TextareaCodeEditor
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/schema-panel.tsx
|
|
74
|
+
import { createElement as createElement2 } from "react";
|
|
75
|
+
function resolveGate(gate, lookup) {
|
|
76
|
+
if (gate === void 0) return true;
|
|
77
|
+
if (typeof gate === "boolean") return gate;
|
|
78
|
+
const raw = Boolean(lookup(gate.bind));
|
|
79
|
+
return gate.negate ? !raw : raw;
|
|
80
|
+
}
|
|
81
|
+
function makeSchemaPanelComponent(contribution, bindings, renderer) {
|
|
82
|
+
const { schema } = contribution;
|
|
83
|
+
if (renderer) {
|
|
84
|
+
let SchemaPanel2 = function(_props) {
|
|
85
|
+
return createElement2(Renderer, { schema, bindings });
|
|
86
|
+
};
|
|
87
|
+
var SchemaPanel = SchemaPanel2;
|
|
88
|
+
const Renderer = renderer;
|
|
89
|
+
SchemaPanel2.displayName = `SchemaPanel(${schema.id})`;
|
|
90
|
+
return SchemaPanel2;
|
|
73
91
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
function SchemaPanelSeam(_props) {
|
|
93
|
+
return createElement2(
|
|
94
|
+
"div",
|
|
95
|
+
{
|
|
96
|
+
"data-schema-panel-seam": schema.id,
|
|
97
|
+
style: {
|
|
98
|
+
padding: "var(--space-3, 12px)",
|
|
99
|
+
font: "12px/1.5 var(--font-sans, sans-serif)",
|
|
100
|
+
color: "var(--pg-muted-fg)"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
`Schema panel "${schema.title}" needs a host renderer (supports("schemaPanel.renderer@1") is false).`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
SchemaPanelSeam.displayName = `SchemaPanelSeam(${schema.id})`;
|
|
107
|
+
return SchemaPanelSeam;
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
// src/host-impl.ts
|
|
80
111
|
var HOST_FEATURES = [
|
|
81
112
|
"contribute.tool@1",
|
|
82
113
|
"contribute.panel@1",
|
|
114
|
+
"contribute.schemaPanel@1",
|
|
115
|
+
"bindings@1",
|
|
83
116
|
"contribute.command@1",
|
|
84
117
|
"contribute.keybinding@1",
|
|
85
118
|
"contribute.overlay@1",
|
|
119
|
+
"contribute.editContext@1",
|
|
120
|
+
"contribute.objectType@1",
|
|
121
|
+
"contribute.importer@1",
|
|
122
|
+
"contribute.exporter@1",
|
|
86
123
|
"document.mutate@1",
|
|
87
124
|
"document.undo@1",
|
|
88
125
|
"document.collection@1",
|
|
126
|
+
"document.frameChain@1",
|
|
89
127
|
"document.meta@1",
|
|
90
128
|
"document.pathAnchors@1",
|
|
91
129
|
"document.hitTest@1",
|
|
@@ -108,6 +146,21 @@ var PluginApiNotImplemented = class extends Error {
|
|
|
108
146
|
this.name = "PluginApiNotImplemented";
|
|
109
147
|
}
|
|
110
148
|
};
|
|
149
|
+
var PluginCapabilityError = class extends Error {
|
|
150
|
+
/** The host door that was called (e.g. `"contribute.tool"`). */
|
|
151
|
+
door;
|
|
152
|
+
/** The manifest declaration that would authorize it (e.g.
|
|
153
|
+
* `'contributes.tools[] must include "media.paged.web.tool.pen"'`). */
|
|
154
|
+
missingDeclaration;
|
|
155
|
+
constructor(door, missingDeclaration, pluginId) {
|
|
156
|
+
super(
|
|
157
|
+
`plugin-api: ${pluginId} called ${door} without declaring it \u2014 ${missingDeclaration}. Manifest capabilities are ENFORCED (trust-line W0.11): declare the use or the host refuses it.`
|
|
158
|
+
);
|
|
159
|
+
this.name = "PluginCapabilityError";
|
|
160
|
+
this.door = door;
|
|
161
|
+
this.missingDeclaration = missingDeclaration;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
111
164
|
function defaultStorageBacking() {
|
|
112
165
|
const ls = globalThis.localStorage;
|
|
113
166
|
if (ls) {
|
|
@@ -133,6 +186,59 @@ function defaultStorageBacking() {
|
|
|
133
186
|
keys: () => Array.from(map.keys())
|
|
134
187
|
};
|
|
135
188
|
}
|
|
189
|
+
var ASSET_BUDGETS = {
|
|
190
|
+
/** Largest font face the door will serve, in bytes (8 MiB). */
|
|
191
|
+
maxFontFaceBytes: 8 * 1024 * 1024
|
|
192
|
+
};
|
|
193
|
+
var BLOB_BUDGETS = {
|
|
194
|
+
/** Default per-plugin blob ceiling, in bytes (64 MiB). */
|
|
195
|
+
defaultQuotaBytes: 64 * 1024 * 1024
|
|
196
|
+
};
|
|
197
|
+
function createDataProviderRegistry() {
|
|
198
|
+
const providers = /* @__PURE__ */ new Map();
|
|
199
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
200
|
+
return {
|
|
201
|
+
register(registration) {
|
|
202
|
+
providers.set(registration.id, {
|
|
203
|
+
reg: registration,
|
|
204
|
+
revision: registration.revision
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
update(revision) {
|
|
208
|
+
const entry = providers.get(registration.id);
|
|
209
|
+
if (entry) entry.revision = revision;
|
|
210
|
+
for (const l of listeners.get(registration.id) ?? []) l(revision);
|
|
211
|
+
},
|
|
212
|
+
dispose() {
|
|
213
|
+
providers.delete(registration.id);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
discover(category) {
|
|
218
|
+
return [...providers.values()].filter((e) => category === void 0 || e.reg.category === category).map((e) => ({
|
|
219
|
+
id: e.reg.id,
|
|
220
|
+
category: e.reg.category,
|
|
221
|
+
schema: e.reg.schema,
|
|
222
|
+
revision: e.revision
|
|
223
|
+
}));
|
|
224
|
+
},
|
|
225
|
+
async get(id) {
|
|
226
|
+
const entry = providers.get(id);
|
|
227
|
+
if (!entry) return null;
|
|
228
|
+
const records = await entry.reg.getSnapshot();
|
|
229
|
+
return { id, revision: entry.revision, records };
|
|
230
|
+
},
|
|
231
|
+
onDidChange(id, listener) {
|
|
232
|
+
let set = listeners.get(id);
|
|
233
|
+
if (!set) {
|
|
234
|
+
set = /* @__PURE__ */ new Set();
|
|
235
|
+
listeners.set(id, set);
|
|
236
|
+
}
|
|
237
|
+
set.add(listener);
|
|
238
|
+
return toDisposable(() => set.delete(listener));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
136
242
|
function createBundleHost(getEditor, manifest, options) {
|
|
137
243
|
const store = new DisposableStore();
|
|
138
244
|
const sink = options?.console ?? console;
|
|
@@ -152,41 +258,276 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
152
258
|
warn: (m, ...a) => sink.warn(`${tag} ${m}`, ...a),
|
|
153
259
|
error: (m, ...a) => sink.error(`${tag} ${m}`, ...a)
|
|
154
260
|
};
|
|
261
|
+
const capabilityMode = options?.capabilityMode ?? "enforce";
|
|
262
|
+
const caps = manifest.capabilities;
|
|
263
|
+
const declared = manifest.contributes;
|
|
264
|
+
const hasDoc = (dir) => caps?.document?.[dir] !== void 0;
|
|
265
|
+
const hasRendering = (s) => caps?.rendering?.includes(s) ?? false;
|
|
266
|
+
const hasAsset = (k) => caps?.assets?.includes(k) ?? false;
|
|
267
|
+
const hasBlobStore = () => caps?.storage?.blob === true;
|
|
268
|
+
const lists = (arr, id) => arr?.includes(id) ?? false;
|
|
269
|
+
const declaresType = (arr, type) => arr?.some((e) => e.type === type) ?? false;
|
|
270
|
+
const requireDeclared = (ok, door, missing) => {
|
|
271
|
+
if (ok) return;
|
|
272
|
+
if (capabilityMode === "warn") {
|
|
273
|
+
log.warn(
|
|
274
|
+
`${door} used without declaring it \u2014 ${missing} (capabilityMode: 'warn'; would refuse in 'enforce')`
|
|
275
|
+
);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
throw new PluginCapabilityError(door, missing, manifest.id);
|
|
279
|
+
};
|
|
280
|
+
const denyWrite = (ok, door, missing) => {
|
|
281
|
+
if (ok) return null;
|
|
282
|
+
const reason = `${door} requires ${missing} (trust-line W0.11)`;
|
|
283
|
+
if (capabilityMode === "warn") {
|
|
284
|
+
log.warn(`${reason} \u2014 proceeding (capabilityMode: 'warn')`);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return reason;
|
|
288
|
+
};
|
|
155
289
|
const contribute = {
|
|
156
290
|
tool(c) {
|
|
157
291
|
assertNamespaced(c.id, "tool");
|
|
292
|
+
requireDeclared(
|
|
293
|
+
lists(declared?.tools, c.id),
|
|
294
|
+
"contribute.tool",
|
|
295
|
+
`contributes.tools[] must include "${c.id}"`
|
|
296
|
+
);
|
|
158
297
|
return store.add(getEditor().registries.tools.register(c));
|
|
159
298
|
},
|
|
160
299
|
panel(c) {
|
|
161
300
|
assertNamespaced(c.id, "panel");
|
|
301
|
+
requireDeclared(
|
|
302
|
+
lists(declared?.panels, c.id),
|
|
303
|
+
"contribute.panel",
|
|
304
|
+
`contributes.panels[] must include "${c.id}"`
|
|
305
|
+
);
|
|
162
306
|
return store.add(getEditor().registries.panels.register(c));
|
|
163
307
|
},
|
|
308
|
+
schemaPanel(c) {
|
|
309
|
+
assertNamespaced(c.id, "schemaPanel");
|
|
310
|
+
requireDeclared(
|
|
311
|
+
lists(declared?.panels, c.id),
|
|
312
|
+
"contribute.schemaPanel",
|
|
313
|
+
`contributes.panels[] must include "${c.id}"`
|
|
314
|
+
);
|
|
315
|
+
const panel = {
|
|
316
|
+
id: c.id,
|
|
317
|
+
title: c.title,
|
|
318
|
+
icon: c.icon,
|
|
319
|
+
defaultDock: c.defaultDock,
|
|
320
|
+
defaultGroup: c.defaultGroup,
|
|
321
|
+
closable: c.closable,
|
|
322
|
+
movable: c.movable,
|
|
323
|
+
component: makeSchemaPanelComponent(
|
|
324
|
+
c,
|
|
325
|
+
bindings,
|
|
326
|
+
options?.schemaPanelRenderer
|
|
327
|
+
)
|
|
328
|
+
};
|
|
329
|
+
const reg = store.add(getEditor().registries.panels.register(panel));
|
|
330
|
+
const recorded = options?.onSchemaPanelRegistered?.(c);
|
|
331
|
+
if (recorded) {
|
|
332
|
+
const d = store.add(recorded);
|
|
333
|
+
return toDisposable(() => {
|
|
334
|
+
reg.dispose();
|
|
335
|
+
d.dispose();
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return reg;
|
|
339
|
+
},
|
|
164
340
|
command(c) {
|
|
165
341
|
assertNamespaced(c.id, "command");
|
|
342
|
+
requireDeclared(
|
|
343
|
+
lists(declared?.commands, c.id),
|
|
344
|
+
"contribute.command",
|
|
345
|
+
`contributes.commands[] must include "${c.id}"`
|
|
346
|
+
);
|
|
166
347
|
return store.add(getEditor().registries.commands.register(c));
|
|
167
348
|
},
|
|
168
349
|
keybinding(c) {
|
|
350
|
+
requireDeclared(
|
|
351
|
+
caps?.keybindings === true,
|
|
352
|
+
"contribute.keybinding",
|
|
353
|
+
"capabilities.keybindings must be true"
|
|
354
|
+
);
|
|
169
355
|
return store.add(getEditor().registries.keybindings.register(c));
|
|
170
356
|
},
|
|
171
357
|
overlay(c) {
|
|
172
358
|
assertNamespaced(c.id, "overlay");
|
|
359
|
+
requireDeclared(
|
|
360
|
+
hasRendering("overlay"),
|
|
361
|
+
"contribute.overlay",
|
|
362
|
+
'capabilities.rendering must include "overlay"'
|
|
363
|
+
);
|
|
173
364
|
return store.add(getEditor().registries.overlays.register(c));
|
|
174
365
|
},
|
|
175
|
-
|
|
176
|
-
|
|
366
|
+
// W3.2 (un-reserved — B-02 / W-03): the last two reserved doors. The
|
|
367
|
+
// capability gate keys off the OBJECT arrays in `contributes`
|
|
368
|
+
// (`editContexts[]` / `objectTypes[]` carry `{type,…}`, not flat
|
|
369
|
+
// ids). The shell owns the stack / chrome / write-scope; the SDK
|
|
370
|
+
// adapter just hands the contribution to the editor's registry (or,
|
|
371
|
+
// when the host hasn't wired one — headless / not-yet-adopted —
|
|
372
|
+
// records it through the harness hook). The `type` is a content-type
|
|
373
|
+
// name, NOT a namespaced id, so the namespace rule does NOT apply
|
|
374
|
+
// (the capability gate is the only gate).
|
|
375
|
+
editContext(c) {
|
|
376
|
+
requireDeclared(
|
|
377
|
+
declaresType(declared?.editContexts, c.type),
|
|
177
378
|
"contribute.editContext",
|
|
178
|
-
|
|
379
|
+
`contributes.editContexts[] must declare { type: "${c.type}" }`
|
|
179
380
|
);
|
|
381
|
+
const stamped = {
|
|
382
|
+
...c,
|
|
383
|
+
metadataKey: metadataKey(manifest)
|
|
384
|
+
};
|
|
385
|
+
const reg = getEditor().registries.editContexts;
|
|
386
|
+
const recorded = options?.onEditContextRegistered?.(stamped);
|
|
387
|
+
if (reg) {
|
|
388
|
+
const d = store.add(reg.register(stamped));
|
|
389
|
+
if (recorded) {
|
|
390
|
+
const r = store.add(recorded);
|
|
391
|
+
return toDisposable(() => {
|
|
392
|
+
d.dispose();
|
|
393
|
+
r.dispose();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return d;
|
|
397
|
+
}
|
|
398
|
+
if (recorded) return store.add(recorded);
|
|
399
|
+
return store.add(toDisposable(() => {
|
|
400
|
+
}));
|
|
180
401
|
},
|
|
181
|
-
objectType() {
|
|
182
|
-
|
|
402
|
+
objectType(c) {
|
|
403
|
+
requireDeclared(
|
|
404
|
+
declaresType(declared?.objectTypes, c.type),
|
|
183
405
|
"contribute.objectType",
|
|
184
|
-
|
|
406
|
+
`contributes.objectTypes[] must declare { type: "${c.type}" }`
|
|
407
|
+
);
|
|
408
|
+
const stamped = {
|
|
409
|
+
...c,
|
|
410
|
+
metadataKey: metadataKey(manifest)
|
|
411
|
+
};
|
|
412
|
+
const reg = getEditor().registries.objectTypes;
|
|
413
|
+
const recorded = options?.onObjectTypeRegistered?.(stamped);
|
|
414
|
+
if (reg) {
|
|
415
|
+
const d = store.add(reg.register(stamped));
|
|
416
|
+
if (recorded) {
|
|
417
|
+
const r = store.add(recorded);
|
|
418
|
+
return toDisposable(() => {
|
|
419
|
+
d.dispose();
|
|
420
|
+
r.dispose();
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return d;
|
|
424
|
+
}
|
|
425
|
+
if (recorded) return store.add(recorded);
|
|
426
|
+
return store.add(toDisposable(() => {
|
|
427
|
+
}));
|
|
428
|
+
},
|
|
429
|
+
// K-2 / S-06 — document IO. Mirrors `command`: namespaced id the
|
|
430
|
+
// manifest must list, routed to the shell registry. The registry is
|
|
431
|
+
// OPTIONAL on the handle (a host that hasn't wired it stays
|
|
432
|
+
// assignable) — when absent the door is a tracked no-op; the headless
|
|
433
|
+
// harness injects a recording registry so conformance can assert it.
|
|
434
|
+
importer(c) {
|
|
435
|
+
assertNamespaced(c.id, "importer");
|
|
436
|
+
requireDeclared(
|
|
437
|
+
lists(declared?.importers, c.id),
|
|
438
|
+
"contribute.importer",
|
|
439
|
+
`contributes.importers[] must include "${c.id}"`
|
|
440
|
+
);
|
|
441
|
+
const reg = getEditor().registries.importers;
|
|
442
|
+
if (reg) return store.add(reg.register(c));
|
|
443
|
+
return store.add(toDisposable(() => {
|
|
444
|
+
}));
|
|
445
|
+
},
|
|
446
|
+
exporter(c) {
|
|
447
|
+
assertNamespaced(c.id, "exporter");
|
|
448
|
+
requireDeclared(
|
|
449
|
+
lists(declared?.exporters, c.id),
|
|
450
|
+
"contribute.exporter",
|
|
451
|
+
`contributes.exporters[] must include "${c.id}"`
|
|
452
|
+
);
|
|
453
|
+
const reg = getEditor().registries.exporters;
|
|
454
|
+
if (reg) return store.add(reg.register(c));
|
|
455
|
+
return store.add(toDisposable(() => {
|
|
456
|
+
}));
|
|
457
|
+
},
|
|
458
|
+
// C-1 — the in-frame scene-layer surface. Capability-gated on
|
|
459
|
+
// `rendering ∋ sceneLayer`; routes submit/clear to the editor's scene
|
|
460
|
+
// channel (`getEditor().sceneLayers` → canvas-wasm submit/clear). When
|
|
461
|
+
// no channel is wired (headless / older editor) the surface warns +
|
|
462
|
+
// no-ops (probe `supports("rendering.sceneLayer@1")`). Disposing the
|
|
463
|
+
// surface clears every layer it submitted (tracked so host.dispose()
|
|
464
|
+
// tears them down).
|
|
465
|
+
sceneLayer() {
|
|
466
|
+
requireDeclared(
|
|
467
|
+
hasRendering("sceneLayer"),
|
|
468
|
+
"contribute.sceneLayer",
|
|
469
|
+
'capabilities.rendering must include "sceneLayer"'
|
|
185
470
|
);
|
|
471
|
+
const submitted = /* @__PURE__ */ new Set();
|
|
472
|
+
const channel = () => getEditor().sceneLayers;
|
|
473
|
+
const surface = {
|
|
474
|
+
async submit(elementId, layer) {
|
|
475
|
+
const ch = channel();
|
|
476
|
+
if (!ch) {
|
|
477
|
+
log.warn(
|
|
478
|
+
`contribute.sceneLayer().submit("${elementId}") ignored \u2014 the host wired no scene channel (probe supports("rendering.sceneLayer@1"))`
|
|
479
|
+
);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
submitted.add(elementId);
|
|
483
|
+
await ch.submit(elementId, layer);
|
|
484
|
+
},
|
|
485
|
+
async clear(elementId) {
|
|
486
|
+
submitted.delete(elementId);
|
|
487
|
+
await channel()?.clear(elementId);
|
|
488
|
+
},
|
|
489
|
+
dispose() {
|
|
490
|
+
const ch = channel();
|
|
491
|
+
if (ch) {
|
|
492
|
+
for (const id of submitted) void ch.clear(id);
|
|
493
|
+
}
|
|
494
|
+
submitted.clear();
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
return store.add(surface);
|
|
186
498
|
}
|
|
187
499
|
};
|
|
500
|
+
const foreignMetadataKey = (m) => {
|
|
501
|
+
if (m.op === "setPluginMetadata") {
|
|
502
|
+
return m.args.key === metadataKey(manifest) ? null : m.args.key;
|
|
503
|
+
}
|
|
504
|
+
if (m.op === "batch") {
|
|
505
|
+
for (const child of m.args.ops) {
|
|
506
|
+
const bad = foreignMetadataKey(child);
|
|
507
|
+
if (bad !== null) return bad;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
};
|
|
512
|
+
const requireDocRead = (door) => requireDeclared(
|
|
513
|
+
hasDoc("read"),
|
|
514
|
+
door,
|
|
515
|
+
"capabilities.document.read must be declared"
|
|
516
|
+
);
|
|
188
517
|
const document = {
|
|
189
518
|
async mutate(mutation) {
|
|
519
|
+
const denied = denyWrite(
|
|
520
|
+
hasDoc("write"),
|
|
521
|
+
"document.mutate",
|
|
522
|
+
"capabilities.document.write"
|
|
523
|
+
);
|
|
524
|
+
if (denied !== null) return { applied: false, error: denied };
|
|
525
|
+
const foreign = foreignMetadataKey(mutation);
|
|
526
|
+
if (foreign !== null) {
|
|
527
|
+
const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
|
|
528
|
+
log.warn(error);
|
|
529
|
+
return { applied: false, error };
|
|
530
|
+
}
|
|
190
531
|
try {
|
|
191
532
|
const reply = await getEditor().client.mutate(mutation);
|
|
192
533
|
if (reply.kind === "mutationApplied") {
|
|
@@ -205,21 +546,40 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
205
546
|
}
|
|
206
547
|
},
|
|
207
548
|
async undo() {
|
|
549
|
+
requireDeclared(
|
|
550
|
+
hasDoc("write"),
|
|
551
|
+
"document.undo",
|
|
552
|
+
"capabilities.document.write"
|
|
553
|
+
);
|
|
208
554
|
await getEditor().client.undo();
|
|
209
555
|
},
|
|
210
556
|
async redo() {
|
|
557
|
+
requireDeclared(
|
|
558
|
+
hasDoc("write"),
|
|
559
|
+
"document.redo",
|
|
560
|
+
"capabilities.document.write"
|
|
561
|
+
);
|
|
211
562
|
await getEditor().client.redo();
|
|
212
563
|
},
|
|
213
564
|
collection(name) {
|
|
565
|
+
requireDocRead("document.collection");
|
|
214
566
|
return getEditor().client.collection(name);
|
|
215
567
|
},
|
|
216
568
|
meta() {
|
|
569
|
+
requireDocRead("document.meta");
|
|
217
570
|
return getEditor().client.documentMeta();
|
|
218
571
|
},
|
|
219
572
|
pathAnchors(id) {
|
|
573
|
+
requireDocRead("document.pathAnchors");
|
|
220
574
|
return getEditor().client.pathAnchors(id).catch(() => null);
|
|
221
575
|
},
|
|
222
576
|
async hitTest(pageId, point, filter = "any") {
|
|
577
|
+
requireDocRead("document.hitTest");
|
|
578
|
+
requireDeclared(
|
|
579
|
+
hasRendering("hitTest"),
|
|
580
|
+
"document.hitTest",
|
|
581
|
+
'capabilities.rendering must include "hitTest"'
|
|
582
|
+
);
|
|
223
583
|
try {
|
|
224
584
|
const reply = await getEditor().client.send({
|
|
225
585
|
kind: "hitTest",
|
|
@@ -231,15 +591,18 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
231
591
|
}
|
|
232
592
|
},
|
|
233
593
|
elementGeometry(ids) {
|
|
594
|
+
requireDocRead("document.elementGeometry");
|
|
234
595
|
return getEditor().client.elementGeometry(ids);
|
|
235
596
|
},
|
|
236
597
|
async tree() {
|
|
598
|
+
requireDocRead("document.tree");
|
|
237
599
|
const reply = await getEditor().client.send({
|
|
238
600
|
kind: "requestSceneTree"
|
|
239
601
|
});
|
|
240
602
|
return reply.kind === "sceneTree" ? reply.payload.roots : [];
|
|
241
603
|
},
|
|
242
604
|
async getMetadata(id) {
|
|
605
|
+
requireDocRead("document.getMetadata");
|
|
243
606
|
const key = metadataKey(manifest);
|
|
244
607
|
const reply = await getEditor().client.send({
|
|
245
608
|
kind: "requestElementProperties",
|
|
@@ -266,14 +629,29 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
266
629
|
args: {
|
|
267
630
|
elementId: id,
|
|
268
631
|
key: metadataKey(manifest),
|
|
269
|
-
value: envelope === null ? null : JSON.stringify(envelope)
|
|
632
|
+
value: envelope === null ? null : JSON.stringify(envelope),
|
|
633
|
+
caller: manifest.id
|
|
270
634
|
}
|
|
271
635
|
});
|
|
272
636
|
},
|
|
637
|
+
async frameChain(storyId) {
|
|
638
|
+
requireDocRead("document.frameChain");
|
|
639
|
+
const reply = await getEditor().client.send({
|
|
640
|
+
kind: "requestFrameChain",
|
|
641
|
+
payload: { storyId }
|
|
642
|
+
});
|
|
643
|
+
return reply.kind === "frameChainResult" ? reply.payload.links : [];
|
|
644
|
+
},
|
|
273
645
|
onDidChange(listener) {
|
|
646
|
+
requireDocRead("document.onDidChange");
|
|
274
647
|
const off = getEditor().client.subscribe((msg) => {
|
|
275
648
|
if (msg.kind === "mutationApplied" || msg.kind === "undoApplied" || msg.kind === "redoApplied") {
|
|
276
|
-
|
|
649
|
+
const reflow = msg.kind === "mutationApplied" ? msg.payload.reflow : void 0;
|
|
650
|
+
listener({
|
|
651
|
+
kind: msg.kind,
|
|
652
|
+
pageIds: msg.payload.pageIds,
|
|
653
|
+
...reflow ? { reflow: { frameId: reflow.frameId, contentBox: reflow.contentBox } } : {}
|
|
654
|
+
});
|
|
277
655
|
}
|
|
278
656
|
});
|
|
279
657
|
return store.add(toDisposable(off));
|
|
@@ -284,6 +662,11 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
284
662
|
return getEditor().selection.elementSelection;
|
|
285
663
|
},
|
|
286
664
|
async set(ids, mode = "replace") {
|
|
665
|
+
requireDeclared(
|
|
666
|
+
hasDoc("write"),
|
|
667
|
+
"selection.set",
|
|
668
|
+
"capabilities.document.write"
|
|
669
|
+
);
|
|
287
670
|
const editor = getEditor();
|
|
288
671
|
const applied = await editor.client.setElementSelection(ids, mode);
|
|
289
672
|
editor.selection.setElementSelection(applied);
|
|
@@ -308,8 +691,26 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
308
691
|
return px / (scale > 0 ? scale : 1);
|
|
309
692
|
}
|
|
310
693
|
};
|
|
694
|
+
const text = {
|
|
695
|
+
async measureString(family, style, str, sizePt) {
|
|
696
|
+
const editorText = getEditor().text;
|
|
697
|
+
if (editorText) {
|
|
698
|
+
return editorText.measure(family, style, str, sizePt);
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
advance: str.length * sizePt * 0.5,
|
|
702
|
+
ascender: sizePt * 0.8,
|
|
703
|
+
descender: -sizePt * 0.2
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
};
|
|
311
707
|
const overlay = {
|
|
312
708
|
setToolPreview(shape) {
|
|
709
|
+
requireDeclared(
|
|
710
|
+
hasRendering("overlay"),
|
|
711
|
+
"overlay.setToolPreview",
|
|
712
|
+
'capabilities.rendering must include "overlay"'
|
|
713
|
+
);
|
|
313
714
|
getEditor().overlaySignals.setToolPreview(shape);
|
|
314
715
|
}
|
|
315
716
|
};
|
|
@@ -335,6 +736,98 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
335
736
|
return backing.keys().filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
|
|
336
737
|
}
|
|
337
738
|
};
|
|
739
|
+
const declaredNetwork = manifest.capabilities?.network;
|
|
740
|
+
const networkDeclared = declaredNetwork === true || typeof declaredNetwork === "object" && declaredNetwork !== null;
|
|
741
|
+
const mayRequest = (origin) => {
|
|
742
|
+
if (declaredNetwork === true) return true;
|
|
743
|
+
if (typeof declaredNetwork === "object" && declaredNetwork !== null) {
|
|
744
|
+
const o = declaredNetwork.origins;
|
|
745
|
+
return o === "consent" || Array.isArray(o) && o.includes(origin);
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
};
|
|
749
|
+
const CONSENT_KEY = "network.consentedOrigins";
|
|
750
|
+
const granted = new Set(storage.get(CONSENT_KEY) ?? []);
|
|
751
|
+
const network = {
|
|
752
|
+
async requestConsent(origins, purpose) {
|
|
753
|
+
requireDeclared(
|
|
754
|
+
networkDeclared,
|
|
755
|
+
"network.requestConsent",
|
|
756
|
+
"capabilities.network must declare the network capability (boolean or { origins })"
|
|
757
|
+
);
|
|
758
|
+
const inScope = origins.filter(mayRequest);
|
|
759
|
+
const outOfScope = origins.filter((o) => !mayRequest(o));
|
|
760
|
+
if (outOfScope.length > 0) {
|
|
761
|
+
log.warn(
|
|
762
|
+
`network.requestConsent: ${outOfScope.length} origin(s) outside the declared capabilities.network allow-list \u2014 denied: ${outOfScope.join(", ")}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const need = inScope.filter((o) => !granted.has(o));
|
|
766
|
+
let prompted = { granted: [], denied: [], remembered: false };
|
|
767
|
+
if (need.length > 0) {
|
|
768
|
+
if (options?.consent) {
|
|
769
|
+
prompted = await options.consent.request(need, purpose);
|
|
770
|
+
} else {
|
|
771
|
+
log.warn(
|
|
772
|
+
"network.requestConsent: no consent backend wired \u2014 denying (supports('network.consent@1') is false; the editor injects one)"
|
|
773
|
+
);
|
|
774
|
+
prompted = { granted: [], denied: need, remembered: false };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (const o of prompted.granted) granted.add(o);
|
|
778
|
+
if (prompted.remembered) storage.set(CONSENT_KEY, [...granted]);
|
|
779
|
+
return {
|
|
780
|
+
granted: inScope.filter((o) => granted.has(o)),
|
|
781
|
+
denied: [...outOfScope, ...inScope.filter((o) => !granted.has(o))],
|
|
782
|
+
remembered: prompted.remembered
|
|
783
|
+
};
|
|
784
|
+
},
|
|
785
|
+
consentedOrigins() {
|
|
786
|
+
return [...granted];
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
const dpCap = manifest.capabilities?.dataProviders;
|
|
790
|
+
const mayPublish = (category) => Array.isArray(dpCap?.publish) && dpCap.publish.includes(category);
|
|
791
|
+
const mayConsume = (category) => Array.isArray(dpCap?.consume) && (category === void 0 || dpCap.consume.includes(category));
|
|
792
|
+
const consumeDeclared = Array.isArray(dpCap?.consume) && dpCap.consume.length > 0;
|
|
793
|
+
const dataProviders = {
|
|
794
|
+
register(registration) {
|
|
795
|
+
requireDeclared(
|
|
796
|
+
mayPublish(registration.category),
|
|
797
|
+
"dataProviders.register",
|
|
798
|
+
`capabilities.dataProviders.publish must include "${registration.category}"`
|
|
799
|
+
);
|
|
800
|
+
if (!options?.dataProviders) {
|
|
801
|
+
log.warn(
|
|
802
|
+
`dataProviders.register("${registration.id}") \u2014 no shared registry wired (supports('dataProviders@1') is false; the editor injects one)`
|
|
803
|
+
);
|
|
804
|
+
return { update() {
|
|
805
|
+
}, dispose() {
|
|
806
|
+
} };
|
|
807
|
+
}
|
|
808
|
+
return options.dataProviders.register(registration);
|
|
809
|
+
},
|
|
810
|
+
discover(category) {
|
|
811
|
+
requireDeclared(
|
|
812
|
+
mayConsume(category),
|
|
813
|
+
"dataProviders.discover",
|
|
814
|
+
"capabilities.dataProviders.consume must include the category"
|
|
815
|
+
);
|
|
816
|
+
return options?.dataProviders?.discover(category) ?? [];
|
|
817
|
+
},
|
|
818
|
+
async get(id) {
|
|
819
|
+
requireDeclared(
|
|
820
|
+
consumeDeclared,
|
|
821
|
+
"dataProviders.get",
|
|
822
|
+
"capabilities.dataProviders.consume must be declared"
|
|
823
|
+
);
|
|
824
|
+
return await options?.dataProviders?.get(id) ?? null;
|
|
825
|
+
},
|
|
826
|
+
onDidChange(id, listener) {
|
|
827
|
+
if (!options?.dataProviders) return toDisposable(() => void 0);
|
|
828
|
+
return store.add(options.dataProviders.onDidChange(id, listener));
|
|
829
|
+
}
|
|
830
|
+
};
|
|
338
831
|
const diagnosticStore = /* @__PURE__ */ new Map();
|
|
339
832
|
const diagnosticListeners = /* @__PURE__ */ new Set();
|
|
340
833
|
const emitDiagnostics = (key) => {
|
|
@@ -349,11 +842,13 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
349
842
|
else if (d.severity === "warning") sink.warn(line);
|
|
350
843
|
else sink.info(line);
|
|
351
844
|
}
|
|
845
|
+
options?.diagnosticsSink?.publish(manifest.id, key, items);
|
|
352
846
|
emitDiagnostics(key);
|
|
353
847
|
},
|
|
354
848
|
clear(key) {
|
|
355
849
|
if (key !== void 0) diagnosticStore.delete(key);
|
|
356
850
|
else diagnosticStore.clear();
|
|
851
|
+
options?.diagnosticsSink?.clear(manifest.id, key);
|
|
357
852
|
emitDiagnostics(key ?? "");
|
|
358
853
|
},
|
|
359
854
|
get(key) {
|
|
@@ -364,6 +859,27 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
364
859
|
return store.add(toDisposable(() => diagnosticListeners.delete(listener)));
|
|
365
860
|
}
|
|
366
861
|
};
|
|
862
|
+
const bindingStore = /* @__PURE__ */ new Map();
|
|
863
|
+
const bindingListeners = /* @__PURE__ */ new Set();
|
|
864
|
+
const emitBinding = (name) => {
|
|
865
|
+
for (const l of bindingListeners) l(name);
|
|
866
|
+
};
|
|
867
|
+
const bindings = {
|
|
868
|
+
publish(name, value) {
|
|
869
|
+
bindingStore.set(name, value);
|
|
870
|
+
emitBinding(name);
|
|
871
|
+
},
|
|
872
|
+
get(name) {
|
|
873
|
+
return bindingStore.get(name);
|
|
874
|
+
},
|
|
875
|
+
delete(name) {
|
|
876
|
+
if (bindingStore.delete(name)) emitBinding(name);
|
|
877
|
+
},
|
|
878
|
+
onDidChange(listener) {
|
|
879
|
+
bindingListeners.add(listener);
|
|
880
|
+
return store.add(toDisposable(() => bindingListeners.delete(listener)));
|
|
881
|
+
}
|
|
882
|
+
};
|
|
367
883
|
const shell = options?.shell ?? {
|
|
368
884
|
openPanel(panelId) {
|
|
369
885
|
log.warn(
|
|
@@ -371,11 +887,119 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
371
887
|
);
|
|
372
888
|
},
|
|
373
889
|
closePanel() {
|
|
890
|
+
},
|
|
891
|
+
async pickFile() {
|
|
892
|
+
log.warn(
|
|
893
|
+
`shell.pickFile() ignored \u2014 the host app provided no shell actions (probe with supports("shell.pickFile@1"))`
|
|
894
|
+
);
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
const widgets = options?.widgets ?? FALLBACK_WIDGETS;
|
|
899
|
+
const assetSource = options?.assetSource;
|
|
900
|
+
const assets = {
|
|
901
|
+
async getFontFace(family, style) {
|
|
902
|
+
requireDeclared(
|
|
903
|
+
hasAsset("fonts"),
|
|
904
|
+
"assets.getFontFace",
|
|
905
|
+
'capabilities.assets must include "fonts"'
|
|
906
|
+
);
|
|
907
|
+
if (!assetSource) return null;
|
|
908
|
+
let face;
|
|
909
|
+
try {
|
|
910
|
+
face = await assetSource.getFontFace(family, style);
|
|
911
|
+
} catch {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
if (!face) return null;
|
|
915
|
+
if (face.bytes.byteLength > ASSET_BUDGETS.maxFontFaceBytes) {
|
|
916
|
+
log.warn(
|
|
917
|
+
`assets.getFontFace("${family}"${style ? `, "${style}"` : ""}) served ${face.bytes.byteLength} bytes, over the ${ASSET_BUDGETS.maxFontFaceBytes}-byte per-face cap \u2014 refused`
|
|
918
|
+
);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
return face;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
const blobBackend = options?.blobStore;
|
|
925
|
+
const blobQuota = Math.min(
|
|
926
|
+
BLOB_BUDGETS.defaultQuotaBytes,
|
|
927
|
+
caps?.storage?.quotaBytes ?? BLOB_BUDGETS.defaultQuotaBytes
|
|
928
|
+
);
|
|
929
|
+
const blobGate = (door) => requireDeclared(
|
|
930
|
+
hasBlobStore(),
|
|
931
|
+
door,
|
|
932
|
+
"capabilities.storage must include { blob: true }"
|
|
933
|
+
);
|
|
934
|
+
const blob = {
|
|
935
|
+
async write(key, bytes) {
|
|
936
|
+
blobGate("blob.write");
|
|
937
|
+
if (!blobBackend) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`host.blob.write("${key}") \u2014 no blob store wired (supports("storage.blob@1") is false; the editor injects one)`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
await blobBackend.delete(manifest.id, key);
|
|
943
|
+
const used = await blobBackend.used(manifest.id);
|
|
944
|
+
if (used + bytes.byteLength > blobQuota) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`host.blob.write("${key}") \u2014 ${bytes.byteLength} bytes would exceed the ${blobQuota}-byte quota (used ${used}) \u2014 refused`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
await blobBackend.write(manifest.id, key, bytes);
|
|
950
|
+
},
|
|
951
|
+
async read(key) {
|
|
952
|
+
blobGate("blob.read");
|
|
953
|
+
if (!blobBackend) return null;
|
|
954
|
+
return blobBackend.read(manifest.id, key);
|
|
955
|
+
},
|
|
956
|
+
async delete(key) {
|
|
957
|
+
blobGate("blob.delete");
|
|
958
|
+
if (!blobBackend) return;
|
|
959
|
+
await blobBackend.delete(manifest.id, key);
|
|
960
|
+
},
|
|
961
|
+
async keys() {
|
|
962
|
+
blobGate("blob.keys");
|
|
963
|
+
if (!blobBackend) return [];
|
|
964
|
+
return blobBackend.keys(manifest.id);
|
|
965
|
+
},
|
|
966
|
+
async usage() {
|
|
967
|
+
blobGate("blob.usage");
|
|
968
|
+
if (!blobBackend) return { used: 0, quota: 0 };
|
|
969
|
+
return { used: await blobBackend.used(manifest.id), quota: blobQuota };
|
|
374
970
|
}
|
|
375
971
|
};
|
|
376
972
|
const featureSet = new Set(HOST_FEATURES);
|
|
973
|
+
if (getEditor().text) {
|
|
974
|
+
featureSet.add("text.measure@1");
|
|
975
|
+
}
|
|
976
|
+
if (getEditor().sceneLayers) {
|
|
977
|
+
featureSet.add("rendering.sceneLayer@1");
|
|
978
|
+
}
|
|
377
979
|
if (options?.shell) {
|
|
378
980
|
featureSet.add("shell.openPanel@1");
|
|
981
|
+
featureSet.add("shell.pickFile@1");
|
|
982
|
+
}
|
|
983
|
+
if (options?.widgets) {
|
|
984
|
+
featureSet.add("widgets.codeEditor@1");
|
|
985
|
+
}
|
|
986
|
+
if (options?.schemaPanelRenderer) {
|
|
987
|
+
featureSet.add("schemaPanel.renderer@1");
|
|
988
|
+
}
|
|
989
|
+
if (options?.diagnosticsSink) {
|
|
990
|
+
featureSet.add("diagnostics.publish@1");
|
|
991
|
+
}
|
|
992
|
+
if (options?.assetSource) {
|
|
993
|
+
featureSet.add("assets.fonts@1");
|
|
994
|
+
}
|
|
995
|
+
if (options?.consent) {
|
|
996
|
+
featureSet.add("network.consent@1");
|
|
997
|
+
}
|
|
998
|
+
if (options?.dataProviders) {
|
|
999
|
+
featureSet.add("dataProviders@1");
|
|
1000
|
+
}
|
|
1001
|
+
if (options?.blobStore) {
|
|
1002
|
+
featureSet.add("storage.blob@1");
|
|
379
1003
|
}
|
|
380
1004
|
const host = {
|
|
381
1005
|
manifest,
|
|
@@ -384,10 +1008,17 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
384
1008
|
document,
|
|
385
1009
|
selection,
|
|
386
1010
|
viewport,
|
|
1011
|
+
text,
|
|
387
1012
|
overlay,
|
|
388
1013
|
shell,
|
|
389
1014
|
storage,
|
|
1015
|
+
blob,
|
|
1016
|
+
network,
|
|
1017
|
+
dataProviders,
|
|
390
1018
|
diagnostics,
|
|
1019
|
+
bindings,
|
|
1020
|
+
widgets,
|
|
1021
|
+
assets,
|
|
391
1022
|
supports: (feature) => featureSet.has(feature),
|
|
392
1023
|
get editor() {
|
|
393
1024
|
return getEditor();
|
|
@@ -401,6 +1032,538 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
401
1032
|
};
|
|
402
1033
|
}
|
|
403
1034
|
|
|
1035
|
+
// src/version.ts
|
|
1036
|
+
var API_VERSION = "0.2.0";
|
|
1037
|
+
function parse(v) {
|
|
1038
|
+
const m = /^(\d+)\.(\d+)(?:\.(\d+))?$/.exec(v.trim());
|
|
1039
|
+
if (!m) return null;
|
|
1040
|
+
return [Number(m[1]), Number(m[2]), Number(m[3] ?? "0")];
|
|
1041
|
+
}
|
|
1042
|
+
function satisfiesApiVersion(range, version = API_VERSION) {
|
|
1043
|
+
const r = range.trim();
|
|
1044
|
+
if (r === "*") return true;
|
|
1045
|
+
const v = parse(version);
|
|
1046
|
+
if (!v) return false;
|
|
1047
|
+
if (r.startsWith("^")) {
|
|
1048
|
+
const base = parse(r.slice(1));
|
|
1049
|
+
if (!base) return false;
|
|
1050
|
+
if (base[0] > 0) {
|
|
1051
|
+
if (v[0] !== base[0]) return false;
|
|
1052
|
+
if (v[1] !== base[1]) return v[1] > base[1];
|
|
1053
|
+
return v[2] >= base[2];
|
|
1054
|
+
}
|
|
1055
|
+
return v[0] === 0 && v[1] === base[1] && v[2] >= base[2];
|
|
1056
|
+
}
|
|
1057
|
+
const exact = parse(r);
|
|
1058
|
+
if (!exact) return false;
|
|
1059
|
+
return v[0] === exact[0] && v[1] === exact[1] && v[2] === exact[2];
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/wasm-loader.ts
|
|
1063
|
+
var CANVAS_WASM_PKG = "@paged-media/canvas-wasm";
|
|
1064
|
+
async function nodeBuiltins() {
|
|
1065
|
+
const [{ createRequire }, { readFileSync }, path, url] = await Promise.all([
|
|
1066
|
+
import("module"),
|
|
1067
|
+
import("fs"),
|
|
1068
|
+
import("path"),
|
|
1069
|
+
import("url")
|
|
1070
|
+
]);
|
|
1071
|
+
return {
|
|
1072
|
+
createRequire,
|
|
1073
|
+
readFileSync,
|
|
1074
|
+
dirname: path.dirname,
|
|
1075
|
+
resolve: path.resolve,
|
|
1076
|
+
fileURLToPath: url.fileURLToPath,
|
|
1077
|
+
pathToFileURL: url.pathToFileURL
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
var STAMP_PREFIX = `// Synced from ${CANVAS_WASM_PKG}@`;
|
|
1081
|
+
async function modulePaths() {
|
|
1082
|
+
const { dirname, resolve, fileURLToPath } = await nodeBuiltins();
|
|
1083
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1084
|
+
return {
|
|
1085
|
+
here,
|
|
1086
|
+
wireDts: resolve(here, "../../plugin-api/src/wire.d.ts")
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
async function readVendoredWireVersion(wireDtsPath) {
|
|
1090
|
+
const { readFileSync } = await nodeBuiltins();
|
|
1091
|
+
const path = wireDtsPath ?? (await modulePaths()).wireDts;
|
|
1092
|
+
let text;
|
|
1093
|
+
try {
|
|
1094
|
+
text = readFileSync(path, "utf8");
|
|
1095
|
+
} catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
for (const line of text.split("\n", 8)) {
|
|
1099
|
+
if (line.startsWith(STAMP_PREFIX)) {
|
|
1100
|
+
return line.slice(STAMP_PREFIX.length).trim();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
function protocolFromVersion(version) {
|
|
1106
|
+
const m = /^\d+\.(\d+)\.\d+/.exec(version.trim());
|
|
1107
|
+
return m ? Number(m[1]) : null;
|
|
1108
|
+
}
|
|
1109
|
+
async function resolveCanvasWasm(resolveFrom) {
|
|
1110
|
+
const { createRequire, readFileSync, dirname, resolve, pathToFileURL } = await nodeBuiltins();
|
|
1111
|
+
const { here } = await modulePaths();
|
|
1112
|
+
const anchors = [
|
|
1113
|
+
resolveFrom,
|
|
1114
|
+
here,
|
|
1115
|
+
resolve(here, "../../../../editor/packages/client"),
|
|
1116
|
+
resolve(here, "../../..")
|
|
1117
|
+
].filter((a) => Boolean(a));
|
|
1118
|
+
let lastErr;
|
|
1119
|
+
for (const anchor of anchors) {
|
|
1120
|
+
try {
|
|
1121
|
+
const req = createRequire(pathToFileURL(resolve(anchor, "package.json")));
|
|
1122
|
+
const pkgJsonPath = req.resolve(`${CANVAS_WASM_PKG}/package.json`);
|
|
1123
|
+
const dir = dirname(pkgJsonPath);
|
|
1124
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
1125
|
+
const loaderPath = resolve(dir, pkg.main ?? "paged_canvas_wasm.js");
|
|
1126
|
+
const wasmPath = resolve(dir, "paged_canvas_wasm_bg.wasm");
|
|
1127
|
+
return {
|
|
1128
|
+
loaderUrl: pathToFileURL(loaderPath).href,
|
|
1129
|
+
wasmPath,
|
|
1130
|
+
version: pkg.version ?? "unknown",
|
|
1131
|
+
dir
|
|
1132
|
+
};
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
lastErr = err;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`plugin-sdk: ${CANVAS_WASM_PKG} is not resolvable for the headless harness (tried ${anchors.length} anchors). Install it as a devDependency, or run inside a workspace where the editor's packages/client provides it. No warn-skip: a headless host with no real engine is the fiction createHeadlessHost exists to prevent (B-13). Last error: ${String(lastErr)}`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
async function loadHeadlessEngine(options = {}) {
|
|
1142
|
+
const { readFileSync } = await nodeBuiltins();
|
|
1143
|
+
const { loaderUrl, wasmPath, version } = await resolveCanvasWasm(
|
|
1144
|
+
options.resolveFrom
|
|
1145
|
+
);
|
|
1146
|
+
const mod = await import(loaderUrl);
|
|
1147
|
+
const bytes = readFileSync(wasmPath);
|
|
1148
|
+
mod.initSync({
|
|
1149
|
+
module: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
1150
|
+
});
|
|
1151
|
+
const worker = new mod.CanvasWorker();
|
|
1152
|
+
const stampedVersion = await readVendoredWireVersion();
|
|
1153
|
+
const expectedProtocol = options.expectedProtocol ?? (stampedVersion ? protocolFromVersion(stampedVersion) : null);
|
|
1154
|
+
if (expectedProtocol !== null && worker.protocolVersion !== expectedProtocol) {
|
|
1155
|
+
const booted = worker.protocolVersion;
|
|
1156
|
+
try {
|
|
1157
|
+
worker.free();
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
throw new Error(
|
|
1161
|
+
`plugin-sdk: headless engine protocol mismatch \u2014 booted ${CANVAS_WASM_PKG}@${version} reports protocol v${booted}, but the vendored wire types are stamped @${stampedVersion ?? "?"} (protocol v${expectedProtocol}). Re-run scripts/sync-wire.mjs and reinstall the matching package.`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return { worker, version, protocolVersion: worker.protocolVersion };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/harness.ts
|
|
1168
|
+
function inMemoryBlobStore() {
|
|
1169
|
+
const byPlugin = /* @__PURE__ */ new Map();
|
|
1170
|
+
const dir = (id) => {
|
|
1171
|
+
let d = byPlugin.get(id);
|
|
1172
|
+
if (!d) byPlugin.set(id, d = /* @__PURE__ */ new Map());
|
|
1173
|
+
return d;
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
async write(id, key, bytes) {
|
|
1177
|
+
dir(id).set(key, bytes.slice());
|
|
1178
|
+
},
|
|
1179
|
+
async read(id, key) {
|
|
1180
|
+
const v = dir(id).get(key);
|
|
1181
|
+
return v ? v.slice() : null;
|
|
1182
|
+
},
|
|
1183
|
+
async delete(id, key) {
|
|
1184
|
+
dir(id).delete(key);
|
|
1185
|
+
},
|
|
1186
|
+
async keys(id) {
|
|
1187
|
+
return Array.from(dir(id).keys());
|
|
1188
|
+
},
|
|
1189
|
+
async used(id) {
|
|
1190
|
+
let n = 0;
|
|
1191
|
+
for (const v of dir(id).values()) n += v.byteLength;
|
|
1192
|
+
return n;
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
var seqCounter = 1;
|
|
1197
|
+
function makeEngineEditor(worker, recorder, onToolPreview) {
|
|
1198
|
+
const protocol = worker.protocolVersion;
|
|
1199
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1200
|
+
const fanOut = (reply) => {
|
|
1201
|
+
for (const l of listeners) l(reply);
|
|
1202
|
+
};
|
|
1203
|
+
const dispatch = (kind, payload) => {
|
|
1204
|
+
const envelope = JSON.stringify(
|
|
1205
|
+
payload === void 0 ? { seq: seqCounter++, protocol, kind } : { seq: seqCounter++, protocol, kind, payload }
|
|
1206
|
+
);
|
|
1207
|
+
const raw = worker.handleMessage(envelope);
|
|
1208
|
+
return JSON.parse(raw);
|
|
1209
|
+
};
|
|
1210
|
+
const recordingRegistry = (kind) => ({
|
|
1211
|
+
register(contribution) {
|
|
1212
|
+
const entry = {
|
|
1213
|
+
kind,
|
|
1214
|
+
id: contribution.id,
|
|
1215
|
+
value: contribution
|
|
1216
|
+
};
|
|
1217
|
+
recorder.push(entry);
|
|
1218
|
+
return {
|
|
1219
|
+
dispose() {
|
|
1220
|
+
const i = recorder.indexOf(entry);
|
|
1221
|
+
if (i >= 0) recorder.splice(i, 1);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
let keybindingSeq = 0;
|
|
1227
|
+
const keybindingRegistry = {
|
|
1228
|
+
register(contribution) {
|
|
1229
|
+
const entry = {
|
|
1230
|
+
kind: "keybinding",
|
|
1231
|
+
id: `${contribution.command}#${keybindingSeq++}`,
|
|
1232
|
+
value: contribution
|
|
1233
|
+
};
|
|
1234
|
+
recorder.push(entry);
|
|
1235
|
+
return {
|
|
1236
|
+
dispose() {
|
|
1237
|
+
const i = recorder.indexOf(entry);
|
|
1238
|
+
if (i >= 0) recorder.splice(i, 1);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
let elementSelection = [];
|
|
1244
|
+
let toolPreview = null;
|
|
1245
|
+
const client = {
|
|
1246
|
+
async mutate(mutation) {
|
|
1247
|
+
const reply = dispatch("mutate", mutation);
|
|
1248
|
+
fanOut(reply);
|
|
1249
|
+
return reply;
|
|
1250
|
+
},
|
|
1251
|
+
async undo() {
|
|
1252
|
+
const reply = dispatch("undo");
|
|
1253
|
+
fanOut(reply);
|
|
1254
|
+
return reply;
|
|
1255
|
+
},
|
|
1256
|
+
async redo() {
|
|
1257
|
+
const reply = dispatch("redo");
|
|
1258
|
+
fanOut(reply);
|
|
1259
|
+
return reply;
|
|
1260
|
+
},
|
|
1261
|
+
async collection(name) {
|
|
1262
|
+
const reply = dispatch("requestCollection", { name });
|
|
1263
|
+
if (reply.kind === "collectionReply") {
|
|
1264
|
+
const items = reply.payload.items;
|
|
1265
|
+
return Array.isArray(items) ? items : [];
|
|
1266
|
+
}
|
|
1267
|
+
return [];
|
|
1268
|
+
},
|
|
1269
|
+
async documentMeta() {
|
|
1270
|
+
const reply = dispatch("requestDocumentMeta");
|
|
1271
|
+
if (reply.kind === "documentMetaReply") {
|
|
1272
|
+
return reply.payload.meta;
|
|
1273
|
+
}
|
|
1274
|
+
throw new Error(`unexpected reply: ${reply.kind}`);
|
|
1275
|
+
},
|
|
1276
|
+
async pathAnchors(id) {
|
|
1277
|
+
const reply = dispatch("requestPathAnchors", { id });
|
|
1278
|
+
if (reply.kind === "pathAnchors") {
|
|
1279
|
+
return reply.payload.result;
|
|
1280
|
+
}
|
|
1281
|
+
return null;
|
|
1282
|
+
},
|
|
1283
|
+
async elementGeometry(ids) {
|
|
1284
|
+
const reply = dispatch("requestElementGeometry", { ids });
|
|
1285
|
+
if (reply.kind === "elementGeometry") {
|
|
1286
|
+
return reply.payload.items;
|
|
1287
|
+
}
|
|
1288
|
+
return [];
|
|
1289
|
+
},
|
|
1290
|
+
async setElementSelection(ids, mode) {
|
|
1291
|
+
const reply = dispatch("setElementSelection", { ids, mode });
|
|
1292
|
+
if (reply.kind === "elementSelectionApplied") {
|
|
1293
|
+
fanOut(reply);
|
|
1294
|
+
return reply.payload.ids;
|
|
1295
|
+
}
|
|
1296
|
+
throw new Error(`unexpected reply: ${reply.kind}`);
|
|
1297
|
+
},
|
|
1298
|
+
async send(message) {
|
|
1299
|
+
const m = message;
|
|
1300
|
+
const reply = dispatch(m.kind, m.payload);
|
|
1301
|
+
return reply;
|
|
1302
|
+
},
|
|
1303
|
+
subscribe(listener) {
|
|
1304
|
+
listeners.add(listener);
|
|
1305
|
+
return () => listeners.delete(listener);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
const editor = {
|
|
1309
|
+
client,
|
|
1310
|
+
registries: {
|
|
1311
|
+
tools: recordingRegistry("tool"),
|
|
1312
|
+
panels: recordingRegistry("panel"),
|
|
1313
|
+
commands: recordingRegistry("command"),
|
|
1314
|
+
keybindings: keybindingRegistry,
|
|
1315
|
+
overlays: recordingRegistry("overlay"),
|
|
1316
|
+
importers: recordingRegistry("importer"),
|
|
1317
|
+
exporters: recordingRegistry("exporter")
|
|
1318
|
+
},
|
|
1319
|
+
selection: {
|
|
1320
|
+
get elementSelection() {
|
|
1321
|
+
return elementSelection;
|
|
1322
|
+
},
|
|
1323
|
+
setElementSelection(ids) {
|
|
1324
|
+
elementSelection = ids;
|
|
1325
|
+
},
|
|
1326
|
+
setElementGeometry() {
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
camera: { camera: { scale: 1, tx: 0, ty: 0 } },
|
|
1330
|
+
overlaySignals: {
|
|
1331
|
+
setToolPreview(value) {
|
|
1332
|
+
toolPreview = value;
|
|
1333
|
+
onToolPreview(toolPreview);
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
// No tool spine + no content caret headlessly — both are inert
|
|
1337
|
+
// members of the narrow handle, present so the cast is total.
|
|
1338
|
+
tool: {
|
|
1339
|
+
setBaseTool() {
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
contentSelection: { contentSelection: null }
|
|
1343
|
+
};
|
|
1344
|
+
return editor;
|
|
1345
|
+
}
|
|
1346
|
+
async function createHeadlessHost(options = {}) {
|
|
1347
|
+
const engine = await loadHeadlessEngine(options);
|
|
1348
|
+
const worker = engine.worker;
|
|
1349
|
+
const contributions = [];
|
|
1350
|
+
let lastPreview = null;
|
|
1351
|
+
const editor = makeEngineEditor(worker, contributions, (value) => {
|
|
1352
|
+
lastPreview = value;
|
|
1353
|
+
});
|
|
1354
|
+
let active = null;
|
|
1355
|
+
let currentHost = null;
|
|
1356
|
+
let disposed = false;
|
|
1357
|
+
const blobStore = options.blobStore ?? inMemoryBlobStore();
|
|
1358
|
+
const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
|
|
1359
|
+
console: options.console,
|
|
1360
|
+
storage: options.storage,
|
|
1361
|
+
blobStore,
|
|
1362
|
+
capabilityMode: mode,
|
|
1363
|
+
// W-06 — a recordable fake asset source the conformance harness
|
|
1364
|
+
// can pass so a bundle's `@font-face` byte path is exercisable
|
|
1365
|
+
// headlessly (the editor's real adapter currently serves null;
|
|
1366
|
+
// DESIGN.md §13.4). Absent → the no-bytes door.
|
|
1367
|
+
assetSource: options.assetSource,
|
|
1368
|
+
// Record the SCHEMA verbatim at registration — the panel registry
|
|
1369
|
+
// only ever sees the synthesized React panel, so the conformance
|
|
1370
|
+
// log gets the schema through this adapter seam (no host renderer
|
|
1371
|
+
// headlessly; visibility/enabled gates are asserted off `bindings`
|
|
1372
|
+
// directly, not through a mounted UI).
|
|
1373
|
+
onSchemaPanelRegistered: (c) => {
|
|
1374
|
+
const entry = {
|
|
1375
|
+
kind: "schemaPanel",
|
|
1376
|
+
id: c.id,
|
|
1377
|
+
value: c
|
|
1378
|
+
};
|
|
1379
|
+
contributions.push(entry);
|
|
1380
|
+
return {
|
|
1381
|
+
dispose() {
|
|
1382
|
+
const i = contributions.indexOf(entry);
|
|
1383
|
+
if (i >= 0) contributions.splice(i, 1);
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
},
|
|
1387
|
+
// W3.2 — the editContext/objectType registries are not wired
|
|
1388
|
+
// headlessly (no shell stack / chrome), so the adapter takes the
|
|
1389
|
+
// recording-stub path and these hooks ARE the registration log.
|
|
1390
|
+
onEditContextRegistered: (c) => {
|
|
1391
|
+
const entry = {
|
|
1392
|
+
kind: "editContext",
|
|
1393
|
+
id: c.type,
|
|
1394
|
+
value: c
|
|
1395
|
+
};
|
|
1396
|
+
contributions.push(entry);
|
|
1397
|
+
return {
|
|
1398
|
+
dispose() {
|
|
1399
|
+
const i = contributions.indexOf(entry);
|
|
1400
|
+
if (i >= 0) contributions.splice(i, 1);
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
},
|
|
1404
|
+
onObjectTypeRegistered: (c) => {
|
|
1405
|
+
const entry = {
|
|
1406
|
+
kind: "objectType",
|
|
1407
|
+
id: c.type,
|
|
1408
|
+
value: c
|
|
1409
|
+
};
|
|
1410
|
+
contributions.push(entry);
|
|
1411
|
+
return {
|
|
1412
|
+
dispose() {
|
|
1413
|
+
const i = contributions.indexOf(entry);
|
|
1414
|
+
if (i >= 0) contributions.splice(i, 1);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
const NEUTRAL = {
|
|
1420
|
+
id: "media.paged.harness",
|
|
1421
|
+
name: "harness",
|
|
1422
|
+
version: "0.0.0",
|
|
1423
|
+
apiVersion: `^${API_VERSION.slice(0, 3)}`,
|
|
1424
|
+
capabilities: {
|
|
1425
|
+
document: { read: "broad", write: "broad" },
|
|
1426
|
+
rendering: ["overlay", "hitTest", "sceneLayer"],
|
|
1427
|
+
keybindings: true,
|
|
1428
|
+
storage: { blob: true }
|
|
1429
|
+
},
|
|
1430
|
+
// Broad contribution declarations so the neutral DRIVER host (which
|
|
1431
|
+
// registers arbitrary contributions directly in 'warn' mode) never
|
|
1432
|
+
// trips the capability gate. A loaded BUNDLE is the subject — its
|
|
1433
|
+
// OWN manifest is enforced.
|
|
1434
|
+
contributes: {
|
|
1435
|
+
editContexts: [
|
|
1436
|
+
{ type: "vectorGraphic", entry: "doubleClick" },
|
|
1437
|
+
{ type: "webFrame", entry: "doubleClick" }
|
|
1438
|
+
],
|
|
1439
|
+
objectTypes: [{ type: "webFrame", bakedFallback: "rectangle" }],
|
|
1440
|
+
importers: ["media.paged.harness.importer.xlsx"],
|
|
1441
|
+
exporters: ["media.paged.harness.exporter.xlsx"]
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
let { host, dispose: disposeHostFacades } = buildHost(NEUTRAL, "warn");
|
|
1445
|
+
currentHost = host;
|
|
1446
|
+
const headless = {
|
|
1447
|
+
get host() {
|
|
1448
|
+
return currentHost;
|
|
1449
|
+
},
|
|
1450
|
+
engineVersion: engine.version,
|
|
1451
|
+
protocolVersion: engine.protocolVersion,
|
|
1452
|
+
contributions,
|
|
1453
|
+
toolsContributed() {
|
|
1454
|
+
return contributions.filter((c) => c.kind === "tool").map((c) => c.value);
|
|
1455
|
+
},
|
|
1456
|
+
panelsContributed() {
|
|
1457
|
+
return contributions.filter((c) => c.kind === "panel").map((c) => c.value);
|
|
1458
|
+
},
|
|
1459
|
+
schemaPanelsContributed() {
|
|
1460
|
+
return contributions.filter((c) => c.kind === "schemaPanel").map((c) => c.value);
|
|
1461
|
+
},
|
|
1462
|
+
editContextsContributed() {
|
|
1463
|
+
return contributions.filter((c) => c.kind === "editContext").map((c) => c.value);
|
|
1464
|
+
},
|
|
1465
|
+
objectTypesContributed() {
|
|
1466
|
+
return contributions.filter((c) => c.kind === "objectType").map((c) => c.value);
|
|
1467
|
+
},
|
|
1468
|
+
importersContributed() {
|
|
1469
|
+
return contributions.filter((c) => c.kind === "importer").map((c) => c.value);
|
|
1470
|
+
},
|
|
1471
|
+
exportersContributed() {
|
|
1472
|
+
return contributions.filter((c) => c.kind === "exporter").map((c) => c.value);
|
|
1473
|
+
},
|
|
1474
|
+
lastToolPreview() {
|
|
1475
|
+
return lastPreview;
|
|
1476
|
+
},
|
|
1477
|
+
async load(idml) {
|
|
1478
|
+
const raw = worker.loadDocumentDirect(seqCounter++, idml);
|
|
1479
|
+
const reply = JSON.parse(raw);
|
|
1480
|
+
if (reply.kind === "documentLoaded") {
|
|
1481
|
+
try {
|
|
1482
|
+
worker.runResolveJson();
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1485
|
+
return reply.payload.pageIds;
|
|
1486
|
+
}
|
|
1487
|
+
const errKind = reply.kind === "loadFailed" ? reply.payload.error.kind : reply.kind;
|
|
1488
|
+
throw new Error(`headless load failed (${reply.kind}: ${errKind})`);
|
|
1489
|
+
},
|
|
1490
|
+
loadBundle(bundle) {
|
|
1491
|
+
if (active) {
|
|
1492
|
+
throw new Error(
|
|
1493
|
+
"headless host: a bundle is already loaded \u2014 dispose it first (one bundle per headless host in v1)"
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
const { manifest } = bundle;
|
|
1497
|
+
if (!satisfiesApiVersion(manifest.apiVersion)) {
|
|
1498
|
+
throw new Error(
|
|
1499
|
+
`headless host: ${manifest.id}@${manifest.version} requires plugin-api "${manifest.apiVersion}", host implements ${API_VERSION}`
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
disposeHostFacades();
|
|
1503
|
+
({ host, dispose: disposeHostFacades } = buildHost(
|
|
1504
|
+
manifest,
|
|
1505
|
+
options.capabilityMode ?? "enforce"
|
|
1506
|
+
));
|
|
1507
|
+
currentHost = host;
|
|
1508
|
+
const handle = bundle.activate(host);
|
|
1509
|
+
let bundleActive = true;
|
|
1510
|
+
active = {
|
|
1511
|
+
dispose() {
|
|
1512
|
+
if (!bundleActive) return;
|
|
1513
|
+
bundleActive = false;
|
|
1514
|
+
try {
|
|
1515
|
+
handle.dispose();
|
|
1516
|
+
} finally {
|
|
1517
|
+
disposeHostFacades();
|
|
1518
|
+
({ host, dispose: disposeHostFacades } = buildHost(
|
|
1519
|
+
NEUTRAL,
|
|
1520
|
+
"warn"
|
|
1521
|
+
));
|
|
1522
|
+
currentHost = host;
|
|
1523
|
+
active = null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
return { dispose: () => active?.dispose() };
|
|
1528
|
+
},
|
|
1529
|
+
dispose() {
|
|
1530
|
+
if (disposed) return;
|
|
1531
|
+
disposed = true;
|
|
1532
|
+
try {
|
|
1533
|
+
if (active) active.dispose();
|
|
1534
|
+
else disposeHostFacades();
|
|
1535
|
+
} finally {
|
|
1536
|
+
contributions.length = 0;
|
|
1537
|
+
lastPreview = null;
|
|
1538
|
+
worker.free();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
return headless;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/asset-source-fake.ts
|
|
1546
|
+
function createRecordableAssetSource(seeds = []) {
|
|
1547
|
+
const requests = [];
|
|
1548
|
+
return {
|
|
1549
|
+
requests,
|
|
1550
|
+
async getFontFace(family, style) {
|
|
1551
|
+
requests.push(style === void 0 ? { family } : { family, style });
|
|
1552
|
+
const key = family.trim().toLowerCase();
|
|
1553
|
+
for (const seed of seeds) {
|
|
1554
|
+
if (seed.family.trim().toLowerCase() !== key) continue;
|
|
1555
|
+
if (seed.matchStyle !== void 0 && seed.matchStyle !== style) {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const { matchStyle: _unused, ...face } = seed;
|
|
1559
|
+
void _unused;
|
|
1560
|
+
return face;
|
|
1561
|
+
}
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
404
1567
|
// src/load.ts
|
|
405
1568
|
var ID_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
|
|
406
1569
|
function loadBundle(getEditor, bundle, options) {
|
|
@@ -439,6 +1602,88 @@ function loadBundle(getEditor, bundle, options) {
|
|
|
439
1602
|
};
|
|
440
1603
|
}
|
|
441
1604
|
|
|
1605
|
+
// src/wasm-bundle-loader.ts
|
|
1606
|
+
var WASM_BUDGETS = {
|
|
1607
|
+
/** Hard per-artifact byte ceiling. A release-optimised wasm layout
|
|
1608
|
+
* engine (Blitz-class) lands in the low-single-digit MiB; 8 MiB
|
|
1609
|
+
* rejects an accidentally-bundled debug build while leaving headroom
|
|
1610
|
+
* for one real engine. A manifest `maxBytes` may only TIGHTEN this. */
|
|
1611
|
+
maxArtifactBytes: 8 * 1024 * 1024,
|
|
1612
|
+
/** Total declared wasm across one bundle. Bounds a bundle that ships
|
|
1613
|
+
* several modules (engine + a codec, say). */
|
|
1614
|
+
maxTotalBytes: 16 * 1024 * 1024,
|
|
1615
|
+
/** Wall-clock budget for fetch + compile + instantiate. Protects the
|
|
1616
|
+
* editor's main flow from a pathological module; advisory, the loader
|
|
1617
|
+
* aborts with a clear error when exceeded. */
|
|
1618
|
+
loadTimeBudgetMs: 3e3,
|
|
1619
|
+
/** Linear-memory growth ceiling, in 64 KiB wasm pages (4096 = 256 MiB).
|
|
1620
|
+
* Passed as `WebAssembly.Memory({ maximum })` when the host owns the
|
|
1621
|
+
* memory; a per-page layout pass should sit far under this. */
|
|
1622
|
+
maxMemoryPages: 4096
|
|
1623
|
+
};
|
|
1624
|
+
function findArtifact(bundle, name) {
|
|
1625
|
+
return bundle.manifest.capabilities?.wasm?.find((a) => a.name === name);
|
|
1626
|
+
}
|
|
1627
|
+
function isGranted(grant, name) {
|
|
1628
|
+
if (grant === void 0) return false;
|
|
1629
|
+
if (grant === "*") return true;
|
|
1630
|
+
if (Array.isArray(grant)) return grant.includes(name);
|
|
1631
|
+
return grant.has(name);
|
|
1632
|
+
}
|
|
1633
|
+
async function loadBundleWasm(bundle, name, options) {
|
|
1634
|
+
const id = bundle.manifest.id;
|
|
1635
|
+
const artifact = findArtifact(bundle, name);
|
|
1636
|
+
if (!artifact) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`loadBundleWasm: ${id} has no declared wasm artifact "${name}" \u2014 only artifacts listed in manifest.capabilities.wasm are loadable (declared-only).`
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
if (!isGranted(options.grant, name)) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`loadBundleWasm: wasm artifact "${name}" of ${id} is not granted by the host \u2014 wasm carries no ambient authority; the host must grant it explicitly (pass grant: "*" or a name set).`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
const now = options.now ?? (() => Date.now());
|
|
1647
|
+
const budgetMs = options.loadTimeBudgetMs ?? WASM_BUDGETS.loadTimeBudgetMs;
|
|
1648
|
+
const started = now();
|
|
1649
|
+
const overBudget = (stage) => {
|
|
1650
|
+
throw new Error(
|
|
1651
|
+
`loadBundleWasm: "${name}" of ${id} exceeded the ${budgetMs}ms load-time budget at ${stage} (${Math.round(now() - started)}ms).`
|
|
1652
|
+
);
|
|
1653
|
+
};
|
|
1654
|
+
const bytes = await options.assetSource(artifact.path);
|
|
1655
|
+
if (now() - started > budgetMs) overBudget("fetch");
|
|
1656
|
+
const src = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
1657
|
+
const view = new Uint8Array(src.byteLength);
|
|
1658
|
+
view.set(src);
|
|
1659
|
+
const ceiling = typeof artifact.maxBytes === "number" && artifact.maxBytes > 0 ? Math.min(artifact.maxBytes, WASM_BUDGETS.maxArtifactBytes) : WASM_BUDGETS.maxArtifactBytes;
|
|
1660
|
+
if (view.byteLength > ceiling) {
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
`loadBundleWasm: "${name}" of ${id} is ${view.byteLength} bytes, over its ${ceiling}-byte ceiling.`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
let memory;
|
|
1666
|
+
const imports = { ...options.imports ?? {} };
|
|
1667
|
+
const provideMemory = options.provideMemory ?? true;
|
|
1668
|
+
if (provideMemory) {
|
|
1669
|
+
const env = { ...imports.env };
|
|
1670
|
+
if (!("memory" in env)) {
|
|
1671
|
+
memory = new WebAssembly.Memory({
|
|
1672
|
+
initial: options.initialMemoryPages ?? 16,
|
|
1673
|
+
maximum: WASM_BUDGETS.maxMemoryPages
|
|
1674
|
+
// shared is intentionally absent — non-shared memory only (v1).
|
|
1675
|
+
});
|
|
1676
|
+
env.memory = memory;
|
|
1677
|
+
}
|
|
1678
|
+
imports.env = env;
|
|
1679
|
+
}
|
|
1680
|
+
const module = await WebAssembly.compile(view);
|
|
1681
|
+
if (now() - started > budgetMs) overBudget("compile");
|
|
1682
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
1683
|
+
if (now() - started > budgetMs) overBudget("instantiate");
|
|
1684
|
+
return { artifact, module, instance, memory, byteLength: view.byteLength };
|
|
1685
|
+
}
|
|
1686
|
+
|
|
442
1687
|
// src/gestures.ts
|
|
443
1688
|
var CLICK_DRAG_THRESHOLD_PX = 4;
|
|
444
1689
|
function beginPageDrag(e) {
|
|
@@ -502,22 +1747,51 @@ function contributeTool(host, tool) {
|
|
|
502
1747
|
function contributePanel(host, panel) {
|
|
503
1748
|
return host.contribute.panel(panel);
|
|
504
1749
|
}
|
|
1750
|
+
function contributeSchemaPanel(host, panel) {
|
|
1751
|
+
return host.contribute.schemaPanel(panel);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/edit-context.ts
|
|
1755
|
+
function contributeEditContext(host, contribution) {
|
|
1756
|
+
return host.contribute.editContext(contribution);
|
|
1757
|
+
}
|
|
1758
|
+
function contributeObjectType(host, contribution) {
|
|
1759
|
+
return host.contribute.objectType(contribution);
|
|
1760
|
+
}
|
|
505
1761
|
export {
|
|
506
1762
|
API_VERSION,
|
|
1763
|
+
ASSET_BUDGETS,
|
|
1764
|
+
BLOB_BUDGETS,
|
|
1765
|
+
CANVAS_WASM_PKG,
|
|
507
1766
|
CLICK_DRAG_THRESHOLD_PX,
|
|
508
1767
|
DisposableStore,
|
|
1768
|
+
FALLBACK_WIDGETS,
|
|
509
1769
|
HOST_FEATURES,
|
|
510
1770
|
PluginApiNotImplemented,
|
|
1771
|
+
PluginCapabilityError,
|
|
1772
|
+
WASM_BUDGETS,
|
|
511
1773
|
beginPageDrag,
|
|
512
1774
|
commitAndSelect,
|
|
1775
|
+
contributeEditContext,
|
|
1776
|
+
contributeObjectType,
|
|
513
1777
|
contributePanel,
|
|
1778
|
+
contributeSchemaPanel,
|
|
514
1779
|
contributeTool,
|
|
515
1780
|
createBundleHost,
|
|
1781
|
+
createDataProviderRegistry,
|
|
516
1782
|
createHeadlessHost,
|
|
1783
|
+
createRecordableAssetSource,
|
|
517
1784
|
defineBundle,
|
|
518
1785
|
endLocalFor,
|
|
519
1786
|
loadBundle,
|
|
1787
|
+
loadBundleWasm,
|
|
1788
|
+
loadHeadlessEngine,
|
|
1789
|
+
makeSchemaPanelComponent,
|
|
1790
|
+
protocolFromVersion,
|
|
520
1791
|
pxToPt,
|
|
1792
|
+
readVendoredWireVersion,
|
|
1793
|
+
resolveCanvasWasm,
|
|
1794
|
+
resolveGate,
|
|
521
1795
|
satisfiesApiVersion,
|
|
522
1796
|
toDisposable
|
|
523
1797
|
};
|