@moloodev/cli 0.2.2 → 0.4.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/README.md +3 -0
- package/package.json +14 -15
- package/src/auth.ts +158 -0
- package/src/cli.ts +88 -34
- package/src/config.ts +36 -0
- package/src/credentials.ts +54 -0
- package/src/dev-server.ts +40 -141
- package/src/editor-dev.ts +38 -0
- package/src/editor-views.ts +23 -43
- package/src/index.ts +0 -2
- package/src/oauth.ts +190 -0
- package/src/publish.ts +6 -5
- package/src/workspace.ts +68 -0
- package/src/dev-host-app.tsx +0 -688
- package/src/dev-host-client.ts +0 -940
- package/src/dev-host-entry.tsx +0 -12
- package/src/dev-host.css +0 -19
- package/src/view-page.ts +0 -57
package/src/dev-host-client.ts
DELETED
|
@@ -1,940 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
REMOTE_ORIGIN,
|
|
3
|
-
applyUpdate,
|
|
4
|
-
createDoc,
|
|
5
|
-
encodeState,
|
|
6
|
-
fromBase64,
|
|
7
|
-
observe,
|
|
8
|
-
toBase64,
|
|
9
|
-
toJson,
|
|
10
|
-
writeJson,
|
|
11
|
-
writeTextBodyContent,
|
|
12
|
-
type Doc,
|
|
13
|
-
} from "@moloodev/editor-sdk/crdt";
|
|
14
|
-
import { editorLayoutStorageKey } from "@moloodev/editor-sdk/layout";
|
|
15
|
-
import {
|
|
16
|
-
createEditorChannel,
|
|
17
|
-
parseChromeManifest,
|
|
18
|
-
parseSessionOpen,
|
|
19
|
-
type ChromeManifest,
|
|
20
|
-
type EditorAccess,
|
|
21
|
-
type EditorAsset,
|
|
22
|
-
type EditorChannel,
|
|
23
|
-
type EditorEvent,
|
|
24
|
-
type EditorLayoutValues,
|
|
25
|
-
type EditorOperation,
|
|
26
|
-
type EditorPort,
|
|
27
|
-
type EditorRequest,
|
|
28
|
-
type EditorSession,
|
|
29
|
-
type Json,
|
|
30
|
-
} from "@moloodev/editor-sdk/protocol";
|
|
31
|
-
import type { EditorFileMatcher, EditorTemplate } from "@moloodev/editor-sdk/commands/editor-document";
|
|
32
|
-
|
|
33
|
-
export type DevHostBootstrap = {
|
|
34
|
-
editorId: string;
|
|
35
|
-
hostOrigin: string;
|
|
36
|
-
viewOrigin: string;
|
|
37
|
-
manifest: {
|
|
38
|
-
id: string;
|
|
39
|
-
title: string;
|
|
40
|
-
icon?: string;
|
|
41
|
-
description?: string;
|
|
42
|
-
acceptedTypes?: string[];
|
|
43
|
-
fileTypes?: EditorFileMatcher[];
|
|
44
|
-
templates?: EditorTemplate[];
|
|
45
|
-
importAccept?: string;
|
|
46
|
-
capabilities?: string[];
|
|
47
|
-
layoutVersion?: number;
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
type SandboxAssetRecord = {
|
|
52
|
-
asset: EditorAsset;
|
|
53
|
-
storageMode: "json" | "text" | "blob";
|
|
54
|
-
contentType: string;
|
|
55
|
-
schema: string | null;
|
|
56
|
-
docStateBase64: string | null;
|
|
57
|
-
initialPayload: Record<string, Json>;
|
|
58
|
-
fileUrl: string | null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
type CommandStateEntry = {
|
|
62
|
-
id: string;
|
|
63
|
-
visible: boolean;
|
|
64
|
-
disabled: boolean;
|
|
65
|
-
pressed: boolean;
|
|
66
|
-
label: string;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function adaptMessagePort(port: MessagePort): EditorPort {
|
|
70
|
-
const listeners = new Map<
|
|
71
|
-
(event: { readonly data: unknown }) => void,
|
|
72
|
-
(event: MessageEvent) => void
|
|
73
|
-
>();
|
|
74
|
-
return {
|
|
75
|
-
postMessage(data: unknown) {
|
|
76
|
-
port.postMessage(data);
|
|
77
|
-
},
|
|
78
|
-
start() {
|
|
79
|
-
port.start();
|
|
80
|
-
},
|
|
81
|
-
close() {
|
|
82
|
-
port.close();
|
|
83
|
-
},
|
|
84
|
-
addEventListener(_type, listener) {
|
|
85
|
-
const wrapped = (event: MessageEvent) => listener({ data: event.data });
|
|
86
|
-
listeners.set(listener, wrapped);
|
|
87
|
-
port.addEventListener("message", wrapped);
|
|
88
|
-
},
|
|
89
|
-
removeEventListener(_type, listener) {
|
|
90
|
-
const wrapped = listeners.get(listener);
|
|
91
|
-
if (!wrapped) return;
|
|
92
|
-
listeners.delete(listener);
|
|
93
|
-
port.removeEventListener("message", wrapped);
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function defaultTemplateFor(manifest: DevHostBootstrap["manifest"]): EditorTemplate {
|
|
99
|
-
const first = manifest.templates?.[0];
|
|
100
|
-
if (first) return first;
|
|
101
|
-
const matcher = manifest.fileTypes?.[0];
|
|
102
|
-
const ext = matcher?.extensions?.[0] ?? ".json";
|
|
103
|
-
const storageMode = matcher?.storageMode ?? "json";
|
|
104
|
-
const contentType = matcher?.mimeTypes?.[0] ?? (storageMode === "text" ? "text/plain" : "application/json");
|
|
105
|
-
const type = manifest.acceptedTypes?.[0] ?? manifest.id;
|
|
106
|
-
return {
|
|
107
|
-
id: "default-blank",
|
|
108
|
-
label: "빈 문서",
|
|
109
|
-
description: "기본 빈 문서",
|
|
110
|
-
type,
|
|
111
|
-
defaultName: `untitled${ext}`,
|
|
112
|
-
storageMode,
|
|
113
|
-
contentType,
|
|
114
|
-
...(matcher?.schema ? { schema: matcher.schema } : {}),
|
|
115
|
-
payload: storageMode === "text" ? { body: "" } : { version: 1 },
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function initDocFromTemplate(template: EditorTemplate): { doc: Doc; data: Record<string, Json> } {
|
|
120
|
-
const doc = createDoc();
|
|
121
|
-
const payload = (template.payload ?? (template.storageMode === "text" ? { body: "" } : { version: 1 })) as Record<string, unknown>;
|
|
122
|
-
if (template.storageMode === "text") {
|
|
123
|
-
writeTextBodyContent(doc, payload);
|
|
124
|
-
} else {
|
|
125
|
-
writeJson(doc, payload);
|
|
126
|
-
}
|
|
127
|
-
const projected = toJson(doc).root;
|
|
128
|
-
const data = (projected && typeof projected === "object" && !Array.isArray(projected)
|
|
129
|
-
? projected
|
|
130
|
-
: payload) as Record<string, Json>;
|
|
131
|
-
return { doc, data };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function createSampleCompanionAssets(nowIso: string): SandboxAssetRecord[] {
|
|
135
|
-
const catalogPayload: Record<string, Json> = {
|
|
136
|
-
version: 1,
|
|
137
|
-
components: {
|
|
138
|
-
"sample.Card": {
|
|
139
|
-
target: "react-dom",
|
|
140
|
-
package: "@moloodev/layout-basic@1.3.0",
|
|
141
|
-
export: "Panel",
|
|
142
|
-
props: { type: "object", properties: { gap: { type: "number" } } },
|
|
143
|
-
slots: ["header", "default", "footer"],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
const catalogDoc = createDoc();
|
|
148
|
-
writeJson(catalogDoc, catalogPayload);
|
|
149
|
-
|
|
150
|
-
const noteDoc = createDoc();
|
|
151
|
-
writeTextBodyContent(noteDoc, { body: "# 샘플 노트\n\n로컬 개발 샌드박스 동반 에셋입니다.\n" });
|
|
152
|
-
|
|
153
|
-
return [
|
|
154
|
-
{
|
|
155
|
-
asset: {
|
|
156
|
-
id: "dev_asset_catalog_000001",
|
|
157
|
-
name: "sample.mcat",
|
|
158
|
-
type: "moloo.layout-catalog",
|
|
159
|
-
storageMode: "json",
|
|
160
|
-
contentType: "application/json",
|
|
161
|
-
schema: "https://moloo.dev/schemas/layout-catalog/v1.json",
|
|
162
|
-
path: "/sample.mcat",
|
|
163
|
-
epoch: 1,
|
|
164
|
-
headRevision: 1,
|
|
165
|
-
updatedAt: nowIso,
|
|
166
|
-
contentBytes: 256,
|
|
167
|
-
locked: false,
|
|
168
|
-
lockHeld: false,
|
|
169
|
-
trashed: false,
|
|
170
|
-
},
|
|
171
|
-
storageMode: "json",
|
|
172
|
-
contentType: "application/json",
|
|
173
|
-
schema: "https://moloo.dev/schemas/layout-catalog/v1.json",
|
|
174
|
-
docStateBase64: toBase64(encodeState(catalogDoc)),
|
|
175
|
-
initialPayload: catalogPayload,
|
|
176
|
-
fileUrl: null,
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
asset: {
|
|
180
|
-
id: "dev_asset_markdown_00002",
|
|
181
|
-
name: "readme.md",
|
|
182
|
-
type: "moloo.markdown",
|
|
183
|
-
storageMode: "text",
|
|
184
|
-
contentType: "text/markdown",
|
|
185
|
-
schema: null,
|
|
186
|
-
path: "/readme.md",
|
|
187
|
-
epoch: 1,
|
|
188
|
-
headRevision: 1,
|
|
189
|
-
updatedAt: nowIso,
|
|
190
|
-
contentBytes: 64,
|
|
191
|
-
locked: false,
|
|
192
|
-
lockHeld: false,
|
|
193
|
-
trashed: false,
|
|
194
|
-
},
|
|
195
|
-
storageMode: "text",
|
|
196
|
-
contentType: "text/markdown",
|
|
197
|
-
schema: null,
|
|
198
|
-
docStateBase64: toBase64(encodeState(noteDoc)),
|
|
199
|
-
initialPayload: { body: "# 샘플 노트\n\n로컬 개발 샌드박스 동반 에셋입니다.\n" },
|
|
200
|
-
fileUrl: null,
|
|
201
|
-
},
|
|
202
|
-
];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export type DevHostDialog =
|
|
206
|
-
| { kind: "start" }
|
|
207
|
-
| { kind: "browser" }
|
|
208
|
-
| { kind: "pick"; candidates: EditorAsset[] }
|
|
209
|
-
| { kind: "search" };
|
|
210
|
-
|
|
211
|
-
type PickBody = Extract<EditorRequest, { type: "host.pick" }>["body"];
|
|
212
|
-
type PickResult = EditorOperation["host.pick"]["success"];
|
|
213
|
-
|
|
214
|
-
/** Host UI preference; cleared by resetStorage with the editor keys. */
|
|
215
|
-
export const INSPECTOR_STORAGE_KEY = "moloo-editor-dev:inspector";
|
|
216
|
-
|
|
217
|
-
export type DevHostSnapshot = {
|
|
218
|
-
manifest: DevHostBootstrap["manifest"];
|
|
219
|
-
theme: "dark" | "light";
|
|
220
|
-
mode: EditorSession["mode"];
|
|
221
|
-
syncStatus: EditorSession["syncStatus"];
|
|
222
|
-
canManage: boolean;
|
|
223
|
-
buildError: string | null;
|
|
224
|
-
chrome: ChromeManifest | null;
|
|
225
|
-
commandState: Readonly<Record<string, CommandStateEntry>>;
|
|
226
|
-
logs: readonly string[];
|
|
227
|
-
docJson: unknown;
|
|
228
|
-
activeAsset: EditorAsset;
|
|
229
|
-
templates: readonly EditorTemplate[];
|
|
230
|
-
currentTemplateId: string;
|
|
231
|
-
companions: readonly EditorAsset[];
|
|
232
|
-
dialog: DevHostDialog | null;
|
|
233
|
-
frameKey: number;
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
export type DevHostController = {
|
|
237
|
-
getSnapshot(): DevHostSnapshot;
|
|
238
|
-
subscribe(listener: () => void): () => void;
|
|
239
|
-
attachFrame(frame: HTMLIFrameElement | null): void;
|
|
240
|
-
frameUrl(): string;
|
|
241
|
-
setTheme(theme: "dark" | "light"): void;
|
|
242
|
-
setMode(mode: EditorSession["mode"]): void;
|
|
243
|
-
setSyncStatus(status: EditorSession["syncStatus"]): void;
|
|
244
|
-
setCanManage(canManage: boolean): void;
|
|
245
|
-
dismissBuildError(): void;
|
|
246
|
-
renameAsset(name: string): void;
|
|
247
|
-
newFromTemplate(templateId: string): void;
|
|
248
|
-
openCompanion(assetId: string): void;
|
|
249
|
-
importFile(file: File): Promise<void>;
|
|
250
|
-
resetLayout(): void;
|
|
251
|
-
restartView(): void;
|
|
252
|
-
resetStorage(): void;
|
|
253
|
-
openDialog(dialog: DevHostDialog): void;
|
|
254
|
-
closeDialog(): void;
|
|
255
|
-
resolvePick(assetId: string | null): void;
|
|
256
|
-
invokeCommand(commandId: string): void;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
export const SIMULATOR_DEFAULTS = {
|
|
260
|
-
mode: "edit",
|
|
261
|
-
syncStatus: "live",
|
|
262
|
-
canManage: true,
|
|
263
|
-
} as const;
|
|
264
|
-
|
|
265
|
-
export function simulatorIsDefault(
|
|
266
|
-
snapshot: Pick<DevHostSnapshot, "mode" | "syncStatus" | "canManage">,
|
|
267
|
-
): boolean {
|
|
268
|
-
return (
|
|
269
|
-
snapshot.mode === SIMULATOR_DEFAULTS.mode &&
|
|
270
|
-
snapshot.syncStatus === SIMULATOR_DEFAULTS.syncStatus &&
|
|
271
|
-
snapshot.canManage === SIMULATOR_DEFAULTS.canManage
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** The view runs in `sandbox="allow-scripts"`, so its messages arrive from the opaque origin "null". */
|
|
276
|
-
export function isViewHandshake(
|
|
277
|
-
event: Pick<MessageEvent, "origin" | "source">,
|
|
278
|
-
frameWindow: MessageEventSource | null | undefined,
|
|
279
|
-
): boolean {
|
|
280
|
-
return event.origin === "null" && frameWindow != null && event.source === frameWindow;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function pickCandidates(assets: readonly EditorAsset[], body: PickBody): EditorAsset[] {
|
|
284
|
-
return assets.filter((a) => {
|
|
285
|
-
if (body.excludeIds?.includes(a.id)) return false;
|
|
286
|
-
if (body.excludeTypes?.includes(a.type)) return false;
|
|
287
|
-
if (body.acceptedTypes?.length && !body.acceptedTypes.includes(a.type)) return false;
|
|
288
|
-
return true;
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function createDevHostController(config: DevHostBootstrap): DevHostController {
|
|
293
|
-
const storagePrefix = `moloo.dev-host.${config.editorId}`;
|
|
294
|
-
let manifest = config.manifest;
|
|
295
|
-
let theme: "dark" | "light" = "dark";
|
|
296
|
-
let mode: EditorSession["mode"] = SIMULATOR_DEFAULTS.mode;
|
|
297
|
-
let syncStatus: EditorSession["syncStatus"] = SIMULATOR_DEFAULTS.syncStatus;
|
|
298
|
-
let access: EditorAccess = { canManage: SIMULATOR_DEFAULTS.canManage };
|
|
299
|
-
let buildError: string | null = null;
|
|
300
|
-
let dialog: DevHostDialog | null = null;
|
|
301
|
-
let pendingPick: ((result: PickResult) => void) | null = null;
|
|
302
|
-
let frameKey = 0;
|
|
303
|
-
let frame: HTMLIFrameElement | null = null;
|
|
304
|
-
|
|
305
|
-
let activeChannel: EditorChannel | null = null;
|
|
306
|
-
let activeDocCleanup: (() => void) | null = null;
|
|
307
|
-
let submittedChrome: ChromeManifest | null = null;
|
|
308
|
-
let commandStates: Record<string, CommandStateEntry> = {};
|
|
309
|
-
let protocolLogs: string[] = [];
|
|
310
|
-
|
|
311
|
-
const listeners = new Set<() => void>();
|
|
312
|
-
let snapshot: DevHostSnapshot | null = null;
|
|
313
|
-
|
|
314
|
-
const nowIso = new Date().toISOString();
|
|
315
|
-
const companionAssets = new Map<string, SandboxAssetRecord>();
|
|
316
|
-
for (const item of createSampleCompanionAssets(nowIso)) {
|
|
317
|
-
companionAssets.set(item.asset.id, item);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let currentTemplate = defaultTemplateFor(manifest);
|
|
321
|
-
const activeAssetId = "dev_active_asset_0000001";
|
|
322
|
-
let activeAssetName = currentTemplate.defaultName;
|
|
323
|
-
let activeAssetType = currentTemplate.type || manifest.acceptedTypes?.[0] || manifest.id;
|
|
324
|
-
let activeStorageMode: "json" | "text" | "blob" = currentTemplate.storageMode ?? "json";
|
|
325
|
-
let activeContentType = currentTemplate.contentType ?? (activeStorageMode === "text" ? "text/plain" : "application/json");
|
|
326
|
-
let activeSchema = currentTemplate.schema ?? null;
|
|
327
|
-
let activeDoc: Doc = createDoc();
|
|
328
|
-
let activeData: Record<string, Json> = {};
|
|
329
|
-
|
|
330
|
-
function templates(): EditorTemplate[] {
|
|
331
|
-
return manifest.templates?.length ? manifest.templates : [defaultTemplateFor(manifest)];
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function activeAsset(): EditorAsset {
|
|
335
|
-
return {
|
|
336
|
-
id: activeAssetId,
|
|
337
|
-
name: activeAssetName,
|
|
338
|
-
type: activeAssetType,
|
|
339
|
-
storageMode: activeStorageMode,
|
|
340
|
-
contentType: activeContentType,
|
|
341
|
-
schema: activeSchema,
|
|
342
|
-
path: `/${activeAssetName}`,
|
|
343
|
-
epoch: 1,
|
|
344
|
-
headRevision: 1,
|
|
345
|
-
updatedAt: nowIso,
|
|
346
|
-
contentBytes: 0,
|
|
347
|
-
locked: false,
|
|
348
|
-
lockHeld: false,
|
|
349
|
-
trashed: false,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function notify(): void {
|
|
354
|
-
snapshot = null;
|
|
355
|
-
for (const listener of listeners) listener();
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function logProtocol(entry: string): void {
|
|
359
|
-
const stamp = new Date().toISOString().slice(11, 23);
|
|
360
|
-
protocolLogs = [`[${stamp}] ${entry}`, ...protocolLogs].slice(0, 200);
|
|
361
|
-
notify();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function saveStateToStorage(): void {
|
|
365
|
-
try {
|
|
366
|
-
const saved = {
|
|
367
|
-
name: activeAssetName,
|
|
368
|
-
type: activeAssetType,
|
|
369
|
-
storageMode: activeStorageMode,
|
|
370
|
-
contentType: activeContentType,
|
|
371
|
-
schema: activeSchema,
|
|
372
|
-
docBase64: toBase64(encodeState(activeDoc)),
|
|
373
|
-
};
|
|
374
|
-
window.localStorage.setItem(`${storagePrefix}.session`, JSON.stringify(saved));
|
|
375
|
-
} catch {
|
|
376
|
-
// Ignore quota errors in sandbox
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function projectData(doc: Doc): Record<string, Json> {
|
|
381
|
-
const root = toJson(doc).root;
|
|
382
|
-
return (root && typeof root === "object" && !Array.isArray(root) ? root : {}) as Record<string, Json>;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function bindActiveDocObserver(): void {
|
|
386
|
-
activeDocCleanup?.();
|
|
387
|
-
activeDocCleanup = observe(activeDoc, () => {
|
|
388
|
-
activeData = projectData(activeDoc);
|
|
389
|
-
saveStateToStorage();
|
|
390
|
-
notify();
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function loadOrInitTemplate(template: EditorTemplate, ignoreSaved = false): void {
|
|
395
|
-
currentTemplate = template;
|
|
396
|
-
if (!ignoreSaved) {
|
|
397
|
-
try {
|
|
398
|
-
const raw = window.localStorage.getItem(`${storagePrefix}.session`);
|
|
399
|
-
if (raw) {
|
|
400
|
-
const parsed = JSON.parse(raw) as {
|
|
401
|
-
name: string;
|
|
402
|
-
type: string;
|
|
403
|
-
storageMode: "json" | "text" | "blob";
|
|
404
|
-
contentType: string;
|
|
405
|
-
schema: string | null;
|
|
406
|
-
docBase64: string;
|
|
407
|
-
};
|
|
408
|
-
const doc = createDoc();
|
|
409
|
-
applyUpdate(doc, fromBase64(parsed.docBase64), REMOTE_ORIGIN);
|
|
410
|
-
activeAssetName = parsed.name;
|
|
411
|
-
activeAssetType = parsed.type;
|
|
412
|
-
activeStorageMode = parsed.storageMode;
|
|
413
|
-
activeContentType = parsed.contentType;
|
|
414
|
-
activeSchema = parsed.schema;
|
|
415
|
-
activeDoc = doc;
|
|
416
|
-
activeData = projectData(doc);
|
|
417
|
-
bindActiveDocObserver();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
} catch {
|
|
421
|
-
// Fall through to template initialization
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
const initialized = initDocFromTemplate(template);
|
|
425
|
-
activeAssetName = template.defaultName;
|
|
426
|
-
activeAssetType = template.type || manifest.acceptedTypes?.[0] || manifest.id;
|
|
427
|
-
activeStorageMode = template.storageMode ?? "json";
|
|
428
|
-
activeContentType = template.contentType ?? (activeStorageMode === "text" ? "text/plain" : "application/json");
|
|
429
|
-
activeSchema = template.schema ?? null;
|
|
430
|
-
activeDoc = initialized.doc;
|
|
431
|
-
activeData = initialized.data;
|
|
432
|
-
bindActiveDocObserver();
|
|
433
|
-
saveStateToStorage();
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
loadOrInitTemplate(currentTemplate, false);
|
|
437
|
-
|
|
438
|
-
function currentSessionPayload(): EditorSession {
|
|
439
|
-
if (mode === "idle") {
|
|
440
|
-
return { asset: null, mode: "idle", data: null, syncStatus };
|
|
441
|
-
}
|
|
442
|
-
return {
|
|
443
|
-
asset: {
|
|
444
|
-
id: activeAssetId,
|
|
445
|
-
type: activeAssetType,
|
|
446
|
-
name: activeAssetName,
|
|
447
|
-
storageMode: activeStorageMode,
|
|
448
|
-
contentType: activeContentType,
|
|
449
|
-
schema: activeSchema,
|
|
450
|
-
},
|
|
451
|
-
mode,
|
|
452
|
-
data: activeData,
|
|
453
|
-
syncStatus,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function publishAllToView(): void {
|
|
458
|
-
if (!activeChannel) return;
|
|
459
|
-
activeChannel.emit({ v: 1, type: "access", body: access });
|
|
460
|
-
activeChannel.emit({ v: 1, type: "theme", body: { theme } });
|
|
461
|
-
const session = currentSessionPayload();
|
|
462
|
-
activeChannel.emit({ v: 1, type: "session", body: session });
|
|
463
|
-
if (session.asset) {
|
|
464
|
-
const update = toBase64(encodeState(activeDoc));
|
|
465
|
-
activeChannel.emit({
|
|
466
|
-
v: 1,
|
|
467
|
-
type: "doc.update",
|
|
468
|
-
body: { assetId: session.asset.id, update },
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function frameUrl(): string {
|
|
474
|
-
return `${config.viewOrigin}/?parent=${encodeURIComponent(window.location.origin)}`;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function restartView(): void {
|
|
478
|
-
if (pendingPick) resolvePick(null);
|
|
479
|
-
activeChannel?.close();
|
|
480
|
-
activeChannel = null;
|
|
481
|
-
submittedChrome = null;
|
|
482
|
-
commandStates = {};
|
|
483
|
-
frameKey += 1;
|
|
484
|
-
notify();
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function openDialog(next: DevHostDialog): void {
|
|
488
|
-
if (dialog?.kind === "pick") resolvePick(null);
|
|
489
|
-
dialog = next;
|
|
490
|
-
notify();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function closeDialog(): void {
|
|
494
|
-
if (dialog?.kind === "pick") {
|
|
495
|
-
resolvePick(null);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
dialog = null;
|
|
499
|
-
notify();
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function resolvePick(assetId: string | null): void {
|
|
503
|
-
const resolve = pendingPick;
|
|
504
|
-
pendingPick = null;
|
|
505
|
-
const candidates = dialog?.kind === "pick" ? dialog.candidates : [];
|
|
506
|
-
dialog = null;
|
|
507
|
-
notify();
|
|
508
|
-
if (!resolve) return;
|
|
509
|
-
const chosen = assetId ? candidates.find((asset) => asset.id === assetId) : undefined;
|
|
510
|
-
resolve(chosen ? { cancelled: false, assets: [chosen] } : { cancelled: true });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function requestPick(body: PickBody): Promise<PickResult> {
|
|
514
|
-
const candidates = pickCandidates([...companionAssets.values()].map((item) => item.asset), body);
|
|
515
|
-
if (pendingPick) resolvePick(null);
|
|
516
|
-
return new Promise<PickResult>((resolve) => {
|
|
517
|
-
pendingPick = resolve;
|
|
518
|
-
dialog = { kind: "pick", candidates };
|
|
519
|
-
notify();
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function resetLayout(): void {
|
|
524
|
-
try {
|
|
525
|
-
window.localStorage.removeItem(editorLayoutStorageKey(manifest.id, manifest.layoutVersion));
|
|
526
|
-
} catch {
|
|
527
|
-
// ignore
|
|
528
|
-
}
|
|
529
|
-
logProtocol("layout reset");
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function invokeCommand(commandId: string): void {
|
|
533
|
-
activeChannel?.emit({ v: 1, type: "command.invoke", body: { commandId } });
|
|
534
|
-
logProtocol(`command.invoke -> ${commandId}`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async function handleRequest(
|
|
538
|
-
request: EditorRequest,
|
|
539
|
-
): Promise<{ ok: true; body: EditorOperation[EditorRequest["type"]]["success"] } | { ok: false; code: string }> {
|
|
540
|
-
logProtocol(`req ${request.type}`);
|
|
541
|
-
const layoutKey = editorLayoutStorageKey(manifest.id, manifest.layoutVersion);
|
|
542
|
-
|
|
543
|
-
switch (request.type) {
|
|
544
|
-
case "chrome.submit": {
|
|
545
|
-
const parsed = parseChromeManifest(request.body);
|
|
546
|
-
if (!parsed.ok) return { ok: false, code: "schema" };
|
|
547
|
-
submittedChrome = parsed.message;
|
|
548
|
-
notify();
|
|
549
|
-
return { ok: true, body: {} };
|
|
550
|
-
}
|
|
551
|
-
case "doc.append": {
|
|
552
|
-
if (request.body.assetId !== activeAssetId || mode !== "edit") {
|
|
553
|
-
return { ok: false, code: "forbidden" };
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
applyUpdate(activeDoc, fromBase64(request.body.update), REMOTE_ORIGIN);
|
|
557
|
-
const root = toJson(activeDoc).root;
|
|
558
|
-
if (root && typeof root === "object" && !Array.isArray(root)) {
|
|
559
|
-
activeData = root as Record<string, Json>;
|
|
560
|
-
}
|
|
561
|
-
saveStateToStorage();
|
|
562
|
-
notify();
|
|
563
|
-
return { ok: true, body: {} };
|
|
564
|
-
} catch {
|
|
565
|
-
return { ok: false, code: "invalid_update" };
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
case "layout.load": {
|
|
569
|
-
try {
|
|
570
|
-
const raw = window.localStorage.getItem(layoutKey);
|
|
571
|
-
const layout = raw ? (JSON.parse(raw) as EditorLayoutValues) : null;
|
|
572
|
-
return { ok: true, body: { layout } };
|
|
573
|
-
} catch {
|
|
574
|
-
return { ok: true, body: { layout: null } };
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
case "layout.save": {
|
|
578
|
-
try {
|
|
579
|
-
window.localStorage.setItem(layoutKey, JSON.stringify(request.body.layout));
|
|
580
|
-
} catch {
|
|
581
|
-
// ignore
|
|
582
|
-
}
|
|
583
|
-
return { ok: true, body: {} };
|
|
584
|
-
}
|
|
585
|
-
case "asset.rename": {
|
|
586
|
-
activeAssetName = request.body.name;
|
|
587
|
-
saveStateToStorage();
|
|
588
|
-
notify();
|
|
589
|
-
activeChannel?.emit({ v: 1, type: "session", body: currentSessionPayload() });
|
|
590
|
-
return { ok: true, body: { name: activeAssetName } };
|
|
591
|
-
}
|
|
592
|
-
case "asset.list": {
|
|
593
|
-
const rows = [...companionAssets.values()]
|
|
594
|
-
.map((item) => item.asset)
|
|
595
|
-
.filter((a) => !request.body.type || a.type === request.body.type);
|
|
596
|
-
return { ok: true, body: rows };
|
|
597
|
-
}
|
|
598
|
-
case "asset.read": {
|
|
599
|
-
const record = companionAssets.get(request.body.id);
|
|
600
|
-
if (!record) return { ok: false, code: "not_found" };
|
|
601
|
-
return {
|
|
602
|
-
ok: true,
|
|
603
|
-
body: {
|
|
604
|
-
asset: record.asset,
|
|
605
|
-
payload: record.initialPayload,
|
|
606
|
-
fileUrl: record.fileUrl,
|
|
607
|
-
},
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
case "asset.listRevisions": {
|
|
611
|
-
return {
|
|
612
|
-
ok: true,
|
|
613
|
-
body: [
|
|
614
|
-
{
|
|
615
|
-
id: "rev_00000000000000000001",
|
|
616
|
-
assetId: request.body.id,
|
|
617
|
-
revision: 1,
|
|
618
|
-
message: "Initial sandbox revision",
|
|
619
|
-
contentBytes: 128,
|
|
620
|
-
createdAt: nowIso,
|
|
621
|
-
},
|
|
622
|
-
],
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
case "asset.getComposition": {
|
|
626
|
-
return {
|
|
627
|
-
ok: true,
|
|
628
|
-
body: {
|
|
629
|
-
bundleId: request.body.id,
|
|
630
|
-
bundleName: activeAssetName,
|
|
631
|
-
bundleEpoch: 1,
|
|
632
|
-
bundleUpdatedAt: nowIso,
|
|
633
|
-
headRevision: 1,
|
|
634
|
-
entries: {},
|
|
635
|
-
members: [],
|
|
636
|
-
refs: [],
|
|
637
|
-
previousMembers: [],
|
|
638
|
-
previousRefs: [],
|
|
639
|
-
added: [],
|
|
640
|
-
removed: [],
|
|
641
|
-
issues: [],
|
|
642
|
-
reviewed: "rev-1",
|
|
643
|
-
totalBytes: 0,
|
|
644
|
-
deltaBytes: 0,
|
|
645
|
-
},
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
case "asset.getPublication": {
|
|
649
|
-
return {
|
|
650
|
-
ok: true,
|
|
651
|
-
body: {
|
|
652
|
-
visibility: "private",
|
|
653
|
-
},
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
case "asset.confirmBundle": {
|
|
657
|
-
return {
|
|
658
|
-
ok: true,
|
|
659
|
-
body: {
|
|
660
|
-
revision: 1,
|
|
661
|
-
composition: {
|
|
662
|
-
bundleId: request.body.id,
|
|
663
|
-
bundleName: activeAssetName,
|
|
664
|
-
bundleEpoch: 1,
|
|
665
|
-
bundleUpdatedAt: nowIso,
|
|
666
|
-
headRevision: 1,
|
|
667
|
-
entries: {},
|
|
668
|
-
members: [],
|
|
669
|
-
refs: [],
|
|
670
|
-
previousMembers: [],
|
|
671
|
-
previousRefs: [],
|
|
672
|
-
added: [],
|
|
673
|
-
removed: [],
|
|
674
|
-
issues: [],
|
|
675
|
-
reviewed: request.body.reviewed,
|
|
676
|
-
totalBytes: 0,
|
|
677
|
-
deltaBytes: 0,
|
|
678
|
-
},
|
|
679
|
-
},
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
case "asset.putPublication": {
|
|
683
|
-
return {
|
|
684
|
-
ok: true,
|
|
685
|
-
body: request.body.publication,
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
case "host.pick": {
|
|
689
|
-
return { ok: true, body: await requestPick(request.body) };
|
|
690
|
-
}
|
|
691
|
-
case "host.start": {
|
|
692
|
-
openDialog({ kind: "start" });
|
|
693
|
-
return { ok: true, body: {} };
|
|
694
|
-
}
|
|
695
|
-
case "host.openBrowser": {
|
|
696
|
-
openDialog({ kind: "browser" });
|
|
697
|
-
return { ok: true, body: {} };
|
|
698
|
-
}
|
|
699
|
-
case "host.openAsset":
|
|
700
|
-
case "host.openInfo": {
|
|
701
|
-
return { ok: true, body: {} };
|
|
702
|
-
}
|
|
703
|
-
case "host.clipboardWrite": {
|
|
704
|
-
try {
|
|
705
|
-
await navigator.clipboard.writeText(request.body.text);
|
|
706
|
-
return { ok: true, body: {} };
|
|
707
|
-
} catch {
|
|
708
|
-
return { ok: false, code: "clipboard_denied" };
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
case "host.download": {
|
|
712
|
-
if ("url" in request.body) {
|
|
713
|
-
const a = document.createElement("a");
|
|
714
|
-
a.href = request.body.url;
|
|
715
|
-
a.download = "";
|
|
716
|
-
a.click();
|
|
717
|
-
return { ok: true, body: {} };
|
|
718
|
-
}
|
|
719
|
-
const bytes =
|
|
720
|
-
request.body.content.kind === "text"
|
|
721
|
-
? new TextEncoder().encode(request.body.content.text)
|
|
722
|
-
: fromBase64(request.body.content.data);
|
|
723
|
-
const blob = new Blob([new Uint8Array(bytes)], { type: request.body.mimeType });
|
|
724
|
-
const url = URL.createObjectURL(blob);
|
|
725
|
-
const a = document.createElement("a");
|
|
726
|
-
a.href = url;
|
|
727
|
-
a.download = request.body.filename;
|
|
728
|
-
a.click();
|
|
729
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
730
|
-
return { ok: true, body: {} };
|
|
731
|
-
}
|
|
732
|
-
case "host.openPopup": {
|
|
733
|
-
window.open(request.body.url, "_blank", "noopener,noreferrer");
|
|
734
|
-
return { ok: true, body: {} };
|
|
735
|
-
}
|
|
736
|
-
case "host.render": {
|
|
737
|
-
const blob = new Blob([JSON.stringify(request.body.document, null, 2)], {
|
|
738
|
-
type: "application/pdf",
|
|
739
|
-
});
|
|
740
|
-
const url = URL.createObjectURL(blob);
|
|
741
|
-
return { ok: true, body: { url } };
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
function handleEvent(event: EditorEvent): void {
|
|
748
|
-
if (event.type === "command.state") {
|
|
749
|
-
commandStates = { ...commandStates, [event.body.id]: event.body };
|
|
750
|
-
notify();
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
if (event.type === "command.shortcut") {
|
|
754
|
-
invokeCommand(event.body.commandId);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
window.addEventListener("message", (event: MessageEvent) => {
|
|
759
|
-
if (!isViewHandshake(event, frame?.contentWindow)) return;
|
|
760
|
-
const port = event.ports?.[0];
|
|
761
|
-
if (!port) return;
|
|
762
|
-
const open = parseSessionOpen(event.data);
|
|
763
|
-
activeChannel?.close();
|
|
764
|
-
activeChannel = null;
|
|
765
|
-
if (!open.ok || open.message.body.editorId !== config.editorId) {
|
|
766
|
-
port.postMessage({ v: 1, ok: false, code: open.ok ? "rejected" : open.code });
|
|
767
|
-
port.close();
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
const channel = createEditorChannel({
|
|
771
|
-
role: "host",
|
|
772
|
-
editorId: config.editorId,
|
|
773
|
-
port: adaptMessagePort(port),
|
|
774
|
-
onRequest: handleRequest,
|
|
775
|
-
onEvent: handleEvent,
|
|
776
|
-
});
|
|
777
|
-
activeChannel = channel;
|
|
778
|
-
void channel.handshake.then((res) => {
|
|
779
|
-
if (!res.ok || activeChannel !== channel) return;
|
|
780
|
-
logProtocol(`handshake ok (${config.editorId})`);
|
|
781
|
-
publishAllToView();
|
|
782
|
-
});
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
if (typeof EventSource !== "undefined") {
|
|
786
|
-
const events = new EventSource("/events");
|
|
787
|
-
events.addEventListener("reload", async () => {
|
|
788
|
-
buildError = null;
|
|
789
|
-
try {
|
|
790
|
-
const res = await fetch("/manifest.json");
|
|
791
|
-
if (res.ok) {
|
|
792
|
-
manifest = (await res.json()) as DevHostBootstrap["manifest"];
|
|
793
|
-
}
|
|
794
|
-
} catch {
|
|
795
|
-
// ignore
|
|
796
|
-
}
|
|
797
|
-
logProtocol("SSE reload -> view restarted");
|
|
798
|
-
restartView();
|
|
799
|
-
});
|
|
800
|
-
events.addEventListener("build-error", (e: MessageEvent) => {
|
|
801
|
-
try {
|
|
802
|
-
const parsed = JSON.parse(String(e.data)) as { message?: string };
|
|
803
|
-
buildError = parsed.message ?? String(e.data);
|
|
804
|
-
} catch {
|
|
805
|
-
buildError = String(e.data);
|
|
806
|
-
}
|
|
807
|
-
notify();
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
return {
|
|
812
|
-
getSnapshot() {
|
|
813
|
-
const next: DevHostSnapshot = snapshot ?? {
|
|
814
|
-
manifest,
|
|
815
|
-
theme,
|
|
816
|
-
mode,
|
|
817
|
-
syncStatus,
|
|
818
|
-
canManage: access.canManage,
|
|
819
|
-
buildError,
|
|
820
|
-
chrome: submittedChrome,
|
|
821
|
-
commandState: commandStates,
|
|
822
|
-
logs: protocolLogs,
|
|
823
|
-
docJson: toJson(activeDoc).root,
|
|
824
|
-
activeAsset: activeAsset(),
|
|
825
|
-
templates: templates(),
|
|
826
|
-
currentTemplateId: currentTemplate.id,
|
|
827
|
-
companions: [...companionAssets.values()].map((item) => item.asset),
|
|
828
|
-
dialog,
|
|
829
|
-
frameKey,
|
|
830
|
-
};
|
|
831
|
-
snapshot = next;
|
|
832
|
-
return next;
|
|
833
|
-
},
|
|
834
|
-
subscribe(listener) {
|
|
835
|
-
listeners.add(listener);
|
|
836
|
-
return () => listeners.delete(listener);
|
|
837
|
-
},
|
|
838
|
-
attachFrame(next) {
|
|
839
|
-
frame = next;
|
|
840
|
-
},
|
|
841
|
-
frameUrl,
|
|
842
|
-
setTheme(next) {
|
|
843
|
-
theme = next;
|
|
844
|
-
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
845
|
-
publishAllToView();
|
|
846
|
-
logProtocol(`theme -> ${theme}`);
|
|
847
|
-
},
|
|
848
|
-
setMode(next) {
|
|
849
|
-
mode = next;
|
|
850
|
-
publishAllToView();
|
|
851
|
-
logProtocol(`session.mode -> ${mode}`);
|
|
852
|
-
},
|
|
853
|
-
setSyncStatus(next) {
|
|
854
|
-
syncStatus = next;
|
|
855
|
-
publishAllToView();
|
|
856
|
-
logProtocol(`session.syncStatus -> ${syncStatus}`);
|
|
857
|
-
},
|
|
858
|
-
setCanManage(next) {
|
|
859
|
-
access = { canManage: next };
|
|
860
|
-
publishAllToView();
|
|
861
|
-
logProtocol(`access.canManage -> ${String(next)}`);
|
|
862
|
-
},
|
|
863
|
-
dismissBuildError() {
|
|
864
|
-
buildError = null;
|
|
865
|
-
notify();
|
|
866
|
-
},
|
|
867
|
-
renameAsset(name) {
|
|
868
|
-
const trimmed = name.trim();
|
|
869
|
-
if (!trimmed || trimmed === activeAssetName) return;
|
|
870
|
-
activeAssetName = trimmed;
|
|
871
|
-
saveStateToStorage();
|
|
872
|
-
publishAllToView();
|
|
873
|
-
logProtocol(`asset.rename -> ${trimmed}`);
|
|
874
|
-
},
|
|
875
|
-
newFromTemplate(templateId) {
|
|
876
|
-
const chosen = templates().find((t) => t.id === templateId) ?? templates()[0]!;
|
|
877
|
-
loadOrInitTemplate(chosen, true);
|
|
878
|
-
dialog = null;
|
|
879
|
-
publishAllToView();
|
|
880
|
-
logProtocol(`template -> ${chosen.id} (${chosen.defaultName})`);
|
|
881
|
-
},
|
|
882
|
-
openCompanion(assetId) {
|
|
883
|
-
const record = companionAssets.get(assetId);
|
|
884
|
-
if (!record) return;
|
|
885
|
-
const doc = createDoc();
|
|
886
|
-
if (record.docStateBase64) applyUpdate(doc, fromBase64(record.docStateBase64), REMOTE_ORIGIN);
|
|
887
|
-
activeAssetName = record.asset.name;
|
|
888
|
-
activeAssetType = record.asset.type;
|
|
889
|
-
activeStorageMode = record.storageMode;
|
|
890
|
-
activeContentType = record.contentType;
|
|
891
|
-
activeSchema = record.schema;
|
|
892
|
-
activeDoc = doc;
|
|
893
|
-
activeData = projectData(doc);
|
|
894
|
-
bindActiveDocObserver();
|
|
895
|
-
saveStateToStorage();
|
|
896
|
-
dialog = null;
|
|
897
|
-
publishAllToView();
|
|
898
|
-
logProtocol(`open companion -> ${record.asset.name}`);
|
|
899
|
-
},
|
|
900
|
-
async importFile(file) {
|
|
901
|
-
if (!activeChannel) return;
|
|
902
|
-
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
903
|
-
activeChannel.emit({
|
|
904
|
-
v: 1,
|
|
905
|
-
type: "file.import",
|
|
906
|
-
body: { name: file.name, data: toBase64(bytes) },
|
|
907
|
-
});
|
|
908
|
-
logProtocol(`emit file.import (${file.name}, ${bytes.byteLength}B)`);
|
|
909
|
-
},
|
|
910
|
-
resetLayout,
|
|
911
|
-
restartView,
|
|
912
|
-
resetStorage() {
|
|
913
|
-
try {
|
|
914
|
-
const keys: string[] = [];
|
|
915
|
-
for (let i = 0; i < window.localStorage.length; i += 1) {
|
|
916
|
-
const key = window.localStorage.key(i);
|
|
917
|
-
if (key && key.startsWith(storagePrefix)) keys.push(key);
|
|
918
|
-
}
|
|
919
|
-
for (const key of keys) window.localStorage.removeItem(key);
|
|
920
|
-
window.localStorage.removeItem(editorLayoutStorageKey(manifest.id, manifest.layoutVersion));
|
|
921
|
-
window.localStorage.removeItem(INSPECTOR_STORAGE_KEY);
|
|
922
|
-
} catch {
|
|
923
|
-
// ignore
|
|
924
|
-
}
|
|
925
|
-
loadOrInitTemplate(templates()[0]!, true);
|
|
926
|
-
logProtocol("localStorage reset");
|
|
927
|
-
restartView();
|
|
928
|
-
},
|
|
929
|
-
openDialog,
|
|
930
|
-
closeDialog,
|
|
931
|
-
resolvePick,
|
|
932
|
-
invokeCommand,
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
declare global {
|
|
937
|
-
interface Window {
|
|
938
|
-
__MOLOO_DEV_HOST__?: DevHostBootstrap;
|
|
939
|
-
}
|
|
940
|
-
}
|