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