@paged-media/plugin-sdk 0.2.6-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 +1294 -38
- package/package.json +14 -2
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,37 +258,243 @@ 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}"`
|
|
185
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"'
|
|
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
|
};
|
|
188
500
|
const foreignMetadataKey = (m) => {
|
|
@@ -197,8 +509,19 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
197
509
|
}
|
|
198
510
|
return null;
|
|
199
511
|
};
|
|
512
|
+
const requireDocRead = (door) => requireDeclared(
|
|
513
|
+
hasDoc("read"),
|
|
514
|
+
door,
|
|
515
|
+
"capabilities.document.read must be declared"
|
|
516
|
+
);
|
|
200
517
|
const document = {
|
|
201
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 };
|
|
202
525
|
const foreign = foreignMetadataKey(mutation);
|
|
203
526
|
if (foreign !== null) {
|
|
204
527
|
const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
|
|
@@ -223,21 +546,40 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
223
546
|
}
|
|
224
547
|
},
|
|
225
548
|
async undo() {
|
|
549
|
+
requireDeclared(
|
|
550
|
+
hasDoc("write"),
|
|
551
|
+
"document.undo",
|
|
552
|
+
"capabilities.document.write"
|
|
553
|
+
);
|
|
226
554
|
await getEditor().client.undo();
|
|
227
555
|
},
|
|
228
556
|
async redo() {
|
|
557
|
+
requireDeclared(
|
|
558
|
+
hasDoc("write"),
|
|
559
|
+
"document.redo",
|
|
560
|
+
"capabilities.document.write"
|
|
561
|
+
);
|
|
229
562
|
await getEditor().client.redo();
|
|
230
563
|
},
|
|
231
564
|
collection(name) {
|
|
565
|
+
requireDocRead("document.collection");
|
|
232
566
|
return getEditor().client.collection(name);
|
|
233
567
|
},
|
|
234
568
|
meta() {
|
|
569
|
+
requireDocRead("document.meta");
|
|
235
570
|
return getEditor().client.documentMeta();
|
|
236
571
|
},
|
|
237
572
|
pathAnchors(id) {
|
|
573
|
+
requireDocRead("document.pathAnchors");
|
|
238
574
|
return getEditor().client.pathAnchors(id).catch(() => null);
|
|
239
575
|
},
|
|
240
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
|
+
);
|
|
241
583
|
try {
|
|
242
584
|
const reply = await getEditor().client.send({
|
|
243
585
|
kind: "hitTest",
|
|
@@ -249,15 +591,18 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
249
591
|
}
|
|
250
592
|
},
|
|
251
593
|
elementGeometry(ids) {
|
|
594
|
+
requireDocRead("document.elementGeometry");
|
|
252
595
|
return getEditor().client.elementGeometry(ids);
|
|
253
596
|
},
|
|
254
597
|
async tree() {
|
|
598
|
+
requireDocRead("document.tree");
|
|
255
599
|
const reply = await getEditor().client.send({
|
|
256
600
|
kind: "requestSceneTree"
|
|
257
601
|
});
|
|
258
602
|
return reply.kind === "sceneTree" ? reply.payload.roots : [];
|
|
259
603
|
},
|
|
260
604
|
async getMetadata(id) {
|
|
605
|
+
requireDocRead("document.getMetadata");
|
|
261
606
|
const key = metadataKey(manifest);
|
|
262
607
|
const reply = await getEditor().client.send({
|
|
263
608
|
kind: "requestElementProperties",
|
|
@@ -284,14 +629,29 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
284
629
|
args: {
|
|
285
630
|
elementId: id,
|
|
286
631
|
key: metadataKey(manifest),
|
|
287
|
-
value: envelope === null ? null : JSON.stringify(envelope)
|
|
632
|
+
value: envelope === null ? null : JSON.stringify(envelope),
|
|
633
|
+
caller: manifest.id
|
|
288
634
|
}
|
|
289
635
|
});
|
|
290
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
|
+
},
|
|
291
645
|
onDidChange(listener) {
|
|
646
|
+
requireDocRead("document.onDidChange");
|
|
292
647
|
const off = getEditor().client.subscribe((msg) => {
|
|
293
648
|
if (msg.kind === "mutationApplied" || msg.kind === "undoApplied" || msg.kind === "redoApplied") {
|
|
294
|
-
|
|
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
|
+
});
|
|
295
655
|
}
|
|
296
656
|
});
|
|
297
657
|
return store.add(toDisposable(off));
|
|
@@ -302,6 +662,11 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
302
662
|
return getEditor().selection.elementSelection;
|
|
303
663
|
},
|
|
304
664
|
async set(ids, mode = "replace") {
|
|
665
|
+
requireDeclared(
|
|
666
|
+
hasDoc("write"),
|
|
667
|
+
"selection.set",
|
|
668
|
+
"capabilities.document.write"
|
|
669
|
+
);
|
|
305
670
|
const editor = getEditor();
|
|
306
671
|
const applied = await editor.client.setElementSelection(ids, mode);
|
|
307
672
|
editor.selection.setElementSelection(applied);
|
|
@@ -326,8 +691,26 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
326
691
|
return px / (scale > 0 ? scale : 1);
|
|
327
692
|
}
|
|
328
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
|
+
};
|
|
329
707
|
const overlay = {
|
|
330
708
|
setToolPreview(shape) {
|
|
709
|
+
requireDeclared(
|
|
710
|
+
hasRendering("overlay"),
|
|
711
|
+
"overlay.setToolPreview",
|
|
712
|
+
'capabilities.rendering must include "overlay"'
|
|
713
|
+
);
|
|
331
714
|
getEditor().overlaySignals.setToolPreview(shape);
|
|
332
715
|
}
|
|
333
716
|
};
|
|
@@ -353,6 +736,98 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
353
736
|
return backing.keys().filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
|
|
354
737
|
}
|
|
355
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
|
+
};
|
|
356
831
|
const diagnosticStore = /* @__PURE__ */ new Map();
|
|
357
832
|
const diagnosticListeners = /* @__PURE__ */ new Set();
|
|
358
833
|
const emitDiagnostics = (key) => {
|
|
@@ -367,11 +842,13 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
367
842
|
else if (d.severity === "warning") sink.warn(line);
|
|
368
843
|
else sink.info(line);
|
|
369
844
|
}
|
|
845
|
+
options?.diagnosticsSink?.publish(manifest.id, key, items);
|
|
370
846
|
emitDiagnostics(key);
|
|
371
847
|
},
|
|
372
848
|
clear(key) {
|
|
373
849
|
if (key !== void 0) diagnosticStore.delete(key);
|
|
374
850
|
else diagnosticStore.clear();
|
|
851
|
+
options?.diagnosticsSink?.clear(manifest.id, key);
|
|
375
852
|
emitDiagnostics(key ?? "");
|
|
376
853
|
},
|
|
377
854
|
get(key) {
|
|
@@ -382,6 +859,27 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
382
859
|
return store.add(toDisposable(() => diagnosticListeners.delete(listener)));
|
|
383
860
|
}
|
|
384
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
|
+
};
|
|
385
883
|
const shell = options?.shell ?? {
|
|
386
884
|
openPanel(panelId) {
|
|
387
885
|
log.warn(
|
|
@@ -389,11 +887,119 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
389
887
|
);
|
|
390
888
|
},
|
|
391
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 };
|
|
392
970
|
}
|
|
393
971
|
};
|
|
394
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
|
+
}
|
|
395
979
|
if (options?.shell) {
|
|
396
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");
|
|
397
1003
|
}
|
|
398
1004
|
const host = {
|
|
399
1005
|
manifest,
|
|
@@ -402,10 +1008,17 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
402
1008
|
document,
|
|
403
1009
|
selection,
|
|
404
1010
|
viewport,
|
|
1011
|
+
text,
|
|
405
1012
|
overlay,
|
|
406
1013
|
shell,
|
|
407
1014
|
storage,
|
|
1015
|
+
blob,
|
|
1016
|
+
network,
|
|
1017
|
+
dataProviders,
|
|
408
1018
|
diagnostics,
|
|
1019
|
+
bindings,
|
|
1020
|
+
widgets,
|
|
1021
|
+
assets,
|
|
409
1022
|
supports: (feature) => featureSet.has(feature),
|
|
410
1023
|
get editor() {
|
|
411
1024
|
return getEditor();
|
|
@@ -419,6 +1032,538 @@ function createBundleHost(getEditor, manifest, options) {
|
|
|
419
1032
|
};
|
|
420
1033
|
}
|
|
421
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
|
+
|
|
422
1567
|
// src/load.ts
|
|
423
1568
|
var ID_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
|
|
424
1569
|
function loadBundle(getEditor, bundle, options) {
|
|
@@ -457,6 +1602,88 @@ function loadBundle(getEditor, bundle, options) {
|
|
|
457
1602
|
};
|
|
458
1603
|
}
|
|
459
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
|
+
|
|
460
1687
|
// src/gestures.ts
|
|
461
1688
|
var CLICK_DRAG_THRESHOLD_PX = 4;
|
|
462
1689
|
function beginPageDrag(e) {
|
|
@@ -520,22 +1747,51 @@ function contributeTool(host, tool) {
|
|
|
520
1747
|
function contributePanel(host, panel) {
|
|
521
1748
|
return host.contribute.panel(panel);
|
|
522
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
|
+
}
|
|
523
1761
|
export {
|
|
524
1762
|
API_VERSION,
|
|
1763
|
+
ASSET_BUDGETS,
|
|
1764
|
+
BLOB_BUDGETS,
|
|
1765
|
+
CANVAS_WASM_PKG,
|
|
525
1766
|
CLICK_DRAG_THRESHOLD_PX,
|
|
526
1767
|
DisposableStore,
|
|
1768
|
+
FALLBACK_WIDGETS,
|
|
527
1769
|
HOST_FEATURES,
|
|
528
1770
|
PluginApiNotImplemented,
|
|
1771
|
+
PluginCapabilityError,
|
|
1772
|
+
WASM_BUDGETS,
|
|
529
1773
|
beginPageDrag,
|
|
530
1774
|
commitAndSelect,
|
|
1775
|
+
contributeEditContext,
|
|
1776
|
+
contributeObjectType,
|
|
531
1777
|
contributePanel,
|
|
1778
|
+
contributeSchemaPanel,
|
|
532
1779
|
contributeTool,
|
|
533
1780
|
createBundleHost,
|
|
1781
|
+
createDataProviderRegistry,
|
|
534
1782
|
createHeadlessHost,
|
|
1783
|
+
createRecordableAssetSource,
|
|
535
1784
|
defineBundle,
|
|
536
1785
|
endLocalFor,
|
|
537
1786
|
loadBundle,
|
|
1787
|
+
loadBundleWasm,
|
|
1788
|
+
loadHeadlessEngine,
|
|
1789
|
+
makeSchemaPanelComponent,
|
|
1790
|
+
protocolFromVersion,
|
|
538
1791
|
pxToPt,
|
|
1792
|
+
readVendoredWireVersion,
|
|
1793
|
+
resolveCanvasWasm,
|
|
1794
|
+
resolveGate,
|
|
539
1795
|
satisfiesApiVersion,
|
|
540
1796
|
toDisposable
|
|
541
1797
|
};
|