@moloodev/cli 0.0.0-stage → 0.2.2

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.
@@ -0,0 +1,12 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import { DevHostApp } from "./dev-host-app";
3
+ import { createDevHostController } from "./dev-host-client";
4
+
5
+ const config = typeof window === "undefined" ? undefined : window.__MOLOO_DEV_HOST__;
6
+ if (config) {
7
+ document.documentElement.classList.add("dark");
8
+ const container = document.createElement("div");
9
+ container.id = "moloo-dev-host";
10
+ document.body.replaceChildren(container);
11
+ createRoot(container).render(<DevHostApp controller={createDevHostController(config)} />);
12
+ }
@@ -0,0 +1,19 @@
1
+ @import "@moloodev/ui/fonts.css";
2
+ @import "tailwindcss";
3
+ @import "tw-animate-css";
4
+ @import "shadcn/tailwind.css";
5
+ @import "@moloodev/ui/styles.css";
6
+ @import "@moloodev/ui/typeset.css";
7
+ @import "@moloodev/ui/moloo.css";
8
+
9
+ @custom-variant dark (&:is(.dark *));
10
+
11
+ html,
12
+ body {
13
+ height: 100%;
14
+ margin: 0;
15
+ overflow: hidden;
16
+ font-family: var(--font-sans);
17
+ background: var(--background);
18
+ color: var(--foreground);
19
+ }
@@ -0,0 +1,365 @@
1
+ import { existsSync, watch, type FSWatcher } from "node:fs";
2
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import esbuild from "esbuild";
6
+ import {
7
+ allowedFrameAncestor,
8
+ editorOriginHeaders,
9
+ type EditorOriginHeaderPolicy,
10
+ } from "@moloodev/editor-sdk/origin-headers";
11
+ import { buildEditorScript, readPackageEditorView, styleHeader } from "./editor-views";
12
+
13
+ export type StartEditorDevServerOptions = {
14
+ readonly packageRoot: string;
15
+ readonly hostPort?: number;
16
+ readonly viewPort?: number;
17
+ readonly watch?: boolean;
18
+ };
19
+
20
+ export type EditorDevServer = {
21
+ readonly editorId: string;
22
+ readonly hostOrigin: string;
23
+ readonly viewOrigin: string;
24
+ readonly hostPort: number;
25
+ readonly viewPort: number;
26
+ rebuild(): Promise<{ build: number }>;
27
+ close(): Promise<void>;
28
+ };
29
+
30
+ let cachedHostScript: string | null = null;
31
+
32
+ function packageSourceDir(specifier: string): string {
33
+ let dir = dirname(fileURLToPath(import.meta.resolve(specifier)));
34
+ while (!existsSync(join(dir, "package.json"))) {
35
+ const parent = dirname(dir);
36
+ if (parent === dir) throw new Error(`Cannot locate package root for ${specifier}`);
37
+ dir = parent;
38
+ }
39
+ return join(dir, "src");
40
+ }
41
+
42
+ async function buildHostClientScript(): Promise<string> {
43
+ if (cachedHostScript !== null) return cachedHostScript;
44
+ const here = dirname(fileURLToPath(import.meta.url));
45
+ const css = new Set<string>();
46
+ const built = await esbuild.build({
47
+ entryPoints: [join(here, "dev-host-entry.tsx")],
48
+ bundle: true,
49
+ format: "iife",
50
+ platform: "browser",
51
+ jsx: "automatic",
52
+ write: false,
53
+ logLevel: "error",
54
+ legalComments: "none",
55
+ define: { "process.env.NODE_ENV": '"development"' },
56
+ alias: { "lib0/logging": join(here, "yjs-log.ts") },
57
+ plugins: [
58
+ {
59
+ name: "collect-host-css",
60
+ setup(builder) {
61
+ builder.onLoad({ filter: /\.css$/ }, (args) => {
62
+ css.add(args.path);
63
+ return { contents: "", loader: "js" };
64
+ });
65
+ },
66
+ },
67
+ ],
68
+ });
69
+ const file = built.outputFiles?.[0];
70
+ if (!file) throw new Error("Failed to bundle dev-host-entry.tsx");
71
+ const header = await styleHeader(join(here, "dev-host.css"), [...css], [
72
+ here,
73
+ packageSourceDir("@moloodev/ui"),
74
+ packageSourceDir("@moloodev/editor-sdk/layout"),
75
+ ]);
76
+ cachedHostScript = header + file.text;
77
+ return cachedHostScript;
78
+ }
79
+
80
+ function viewDocumentCsp(network: readonly string[], ancestor: string | null): string {
81
+ const connect = ["'self'", ...network];
82
+ const frameAncestors = ancestor ? [ancestor] : ["'none'"];
83
+ const directives: readonly (readonly [string, readonly string[]])[] = [
84
+ ["default-src", ["'none'"]],
85
+ ["script-src", ["'self'"]],
86
+ ["style-src", ["'self'", "'unsafe-inline'"]],
87
+ ["img-src", ["'self'", "data:", "blob:"]],
88
+ ["font-src", ["'self'", "https://ui.moloo.dev"]],
89
+ ["connect-src", connect],
90
+ ["frame-ancestors", frameAncestors],
91
+ ["base-uri", ["'none'"]],
92
+ ["form-action", ["'none'"]],
93
+ ];
94
+ return directives.map(([name, sources]) => `${name} ${sources.join(" ")}`).join("; ");
95
+ }
96
+
97
+ function applyOriginPolicy(res: ServerResponse, policy: EditorOriginHeaderPolicy): void {
98
+ for (const dropped of policy.drop) {
99
+ res.removeHeader(dropped);
100
+ }
101
+ res.setHeader("content-security-policy", policy["content-security-policy"]);
102
+ res.setHeader("x-content-type-options", policy["x-content-type-options"]);
103
+ }
104
+
105
+ function singleQueryParam(url: URL, key: string): string | null {
106
+ const values = url.searchParams.getAll(key);
107
+ if (values.length !== 1) return null;
108
+ const value = values[0];
109
+ return value && value.length > 0 ? value : null;
110
+ }
111
+
112
+ function listenOnLoopback(server: Server, port: number): Promise<number> {
113
+ return new Promise((resolve, reject) => {
114
+ function onError(err: Error) {
115
+ server.close(() => reject(err));
116
+ }
117
+ server.once("error", onError);
118
+ server.listen(port, "127.0.0.1", () => {
119
+ server.removeListener("error", onError);
120
+ const addr = server.address();
121
+ if (!addr || typeof addr === "string") {
122
+ server.close(() => reject(new Error("Did not bind TCP port")));
123
+ return;
124
+ }
125
+ resolve(addr.port);
126
+ });
127
+ });
128
+ }
129
+
130
+ function closeServer(server: Server): Promise<void> {
131
+ return new Promise((resolve) => {
132
+ server.close(() => resolve());
133
+ });
134
+ }
135
+
136
+ export async function startEditorDevServer(
137
+ options: StartEditorDevServerOptions,
138
+ ): Promise<EditorDevServer> {
139
+ const initialView = readPackageEditorView(options.packageRoot);
140
+ const hostScript = await buildHostClientScript();
141
+ let currentView = initialView;
142
+ let currentEditorScript = await buildEditorScript(initialView);
143
+ let buildCount = 1;
144
+
145
+ const sseClients = new Set<ServerResponse>();
146
+ let watcher: FSWatcher | null = null;
147
+ let watchTimer: ReturnType<typeof setTimeout> | null = null;
148
+
149
+ let hostOrigin = "";
150
+ let viewOrigin = "";
151
+
152
+ const viewServer = createServer((req: IncomingMessage, res: ServerResponse) => {
153
+ const method = req.method ?? "GET";
154
+ const url = new URL(req.url ?? "/", viewOrigin || "http://127.0.0.1");
155
+ const refused = editorOriginHeaders({ parent: null, verifiedOrigins: [] });
156
+
157
+ if (method !== "GET" && method !== "HEAD") {
158
+ applyOriginPolicy(res, refused);
159
+ res.writeHead(405, {
160
+ "content-type": "text/plain; charset=utf-8",
161
+ "cache-control": "no-store",
162
+ });
163
+ res.end(method === "HEAD" ? undefined : "method not allowed");
164
+ return;
165
+ }
166
+
167
+ if (url.pathname === "/") {
168
+ const parent = singleQueryParam(url, "parent");
169
+ const verifiedOrigins = [hostOrigin, `http://localhost:${String(boundHostPort)}`];
170
+ const ancestor = allowedFrameAncestor(parent, verifiedOrigins);
171
+ const policy: EditorOriginHeaderPolicy = {
172
+ ...editorOriginHeaders({ parent, verifiedOrigins }),
173
+ "content-security-policy": viewDocumentCsp(currentView.network, ancestor),
174
+ };
175
+ applyOriginPolicy(res, policy);
176
+ const html = [
177
+ "<!doctype html>",
178
+ `<html data-editor-id="${currentView.id}">`,
179
+ "<head>",
180
+ '<meta charset="utf-8">',
181
+ `<title>${currentView.id}</title>`,
182
+ "</head>",
183
+ "<body>",
184
+ '<script src="/editor.js"></script>',
185
+ "</body>",
186
+ "</html>",
187
+ ].join("\n");
188
+ res.writeHead(200, {
189
+ "content-type": "text/html; charset=utf-8",
190
+ "cache-control": "no-store",
191
+ });
192
+ res.end(method === "HEAD" ? undefined : html);
193
+ return;
194
+ }
195
+
196
+ if (url.pathname === "/editor.js") {
197
+ applyOriginPolicy(res, refused);
198
+ res.writeHead(200, {
199
+ "content-type": "text/javascript; charset=utf-8",
200
+ "cache-control": "no-store",
201
+ });
202
+ res.end(method === "HEAD" ? undefined : currentEditorScript);
203
+ return;
204
+ }
205
+
206
+ applyOriginPolicy(res, refused);
207
+ res.writeHead(404, {
208
+ "content-type": "text/plain; charset=utf-8",
209
+ "cache-control": "no-store",
210
+ });
211
+ res.end(method === "HEAD" ? undefined : "not found");
212
+ });
213
+
214
+ const hostServer = createServer((req: IncomingMessage, res: ServerResponse) => {
215
+ const method = req.method ?? "GET";
216
+ const url = new URL(req.url ?? "/", hostOrigin || "http://127.0.0.1");
217
+
218
+ if (method !== "GET" && method !== "HEAD") {
219
+ res.writeHead(405, { "content-type": "text/plain; charset=utf-8" });
220
+ res.end("method not allowed");
221
+ return;
222
+ }
223
+
224
+ if (url.pathname === "/") {
225
+ const bootstrap = {
226
+ editorId: currentView.id,
227
+ hostOrigin,
228
+ viewOrigin,
229
+ manifest: currentView.manifest,
230
+ };
231
+ const html = [
232
+ "<!doctype html>",
233
+ '<html lang="ko" class="dark">',
234
+ "<head>",
235
+ '<meta charset="utf-8">',
236
+ `<title>${String(currentView.manifest.title ?? currentView.id)} — moloo dev</title>`,
237
+ "</head>",
238
+ "<body>",
239
+ `<script>window.__MOLOO_DEV_HOST__ = ${JSON.stringify(bootstrap).replace(/</g, "\\u003c")};</script>`,
240
+ '<script src="/host.js"></script>',
241
+ "</body>",
242
+ "</html>",
243
+ ].join("\n");
244
+ res.writeHead(200, {
245
+ "content-type": "text/html; charset=utf-8",
246
+ "cache-control": "no-store",
247
+ });
248
+ res.end(method === "HEAD" ? undefined : html);
249
+ return;
250
+ }
251
+
252
+ if (url.pathname === "/host.js") {
253
+ res.writeHead(200, {
254
+ "content-type": "text/javascript; charset=utf-8",
255
+ "cache-control": "no-store",
256
+ });
257
+ res.end(method === "HEAD" ? undefined : hostScript);
258
+ return;
259
+ }
260
+
261
+ if (url.pathname === "/manifest.json") {
262
+ res.writeHead(200, {
263
+ "content-type": "application/json; charset=utf-8",
264
+ "cache-control": "no-store",
265
+ });
266
+ res.end(method === "HEAD" ? undefined : JSON.stringify(currentView.manifest, null, 2));
267
+ return;
268
+ }
269
+
270
+ if (url.pathname === "/events") {
271
+ res.writeHead(200, {
272
+ "content-type": "text/event-stream; charset=utf-8",
273
+ "cache-control": "no-store",
274
+ connection: "keep-alive",
275
+ });
276
+ res.write(`event: connected\ndata: ${JSON.stringify({ build: buildCount })}\n\n`);
277
+ sseClients.add(res);
278
+ req.on("close", () => {
279
+ sseClients.delete(res);
280
+ });
281
+ return;
282
+ }
283
+
284
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
285
+ res.end("not found");
286
+ });
287
+
288
+ const requestedHostPort = options.hostPort ?? 4300;
289
+ const boundHostPort = await listenOnLoopback(hostServer, requestedHostPort);
290
+ const requestedViewPort =
291
+ options.viewPort ?? (requestedHostPort === 0 ? 0 : boundHostPort + 1);
292
+ let boundViewPort: number;
293
+ try {
294
+ boundViewPort = await listenOnLoopback(viewServer, requestedViewPort);
295
+ } catch (err) {
296
+ if (options.viewPort === undefined) {
297
+ boundViewPort = await listenOnLoopback(viewServer, 0);
298
+ } else {
299
+ await closeServer(hostServer);
300
+ throw err;
301
+ }
302
+ }
303
+
304
+ hostOrigin = `http://127.0.0.1:${String(boundHostPort)}`;
305
+ viewOrigin = `http://127.0.0.1:${String(boundViewPort)}`;
306
+
307
+ function broadcastSse(event: string, payload: unknown): void {
308
+ const message = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
309
+ for (const client of sseClients) {
310
+ client.write(message);
311
+ }
312
+ }
313
+
314
+ async function rebuild(): Promise<{ build: number }> {
315
+ try {
316
+ const nextView = readPackageEditorView(options.packageRoot);
317
+ const nextScript = await buildEditorScript(nextView);
318
+ currentView = nextView;
319
+ currentEditorScript = nextScript;
320
+ buildCount += 1;
321
+ broadcastSse("reload", { build: buildCount, editorId: currentView.id });
322
+ return { build: buildCount };
323
+ } catch (err) {
324
+ const message = err instanceof Error ? err.message : String(err);
325
+ broadcastSse("build-error", { message });
326
+ throw err;
327
+ }
328
+ }
329
+
330
+ const srcDir = join(options.packageRoot, "src");
331
+ if (options.watch !== false && existsSync(srcDir)) {
332
+ watcher = watch(srcDir, { recursive: true }, (_event, filename) => {
333
+ if (filename && String(filename).includes(".view-css-")) return;
334
+ if (watchTimer !== null) clearTimeout(watchTimer);
335
+ watchTimer = setTimeout(() => {
336
+ watchTimer = null;
337
+ void rebuild().catch(() => {});
338
+ }, 60);
339
+ });
340
+ }
341
+
342
+ return {
343
+ editorId: currentView.id,
344
+ hostOrigin,
345
+ viewOrigin,
346
+ hostPort: boundHostPort,
347
+ viewPort: boundViewPort,
348
+ rebuild,
349
+ async close() {
350
+ if (watchTimer !== null) {
351
+ clearTimeout(watchTimer);
352
+ watchTimer = null;
353
+ }
354
+ if (watcher !== null) {
355
+ watcher.close();
356
+ watcher = null;
357
+ }
358
+ for (const client of sseClients) {
359
+ client.end();
360
+ }
361
+ sseClients.clear();
362
+ await Promise.all([closeServer(hostServer), closeServer(viewServer)]);
363
+ },
364
+ };
365
+ }
@@ -0,0 +1,201 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import esbuild from "esbuild";
6
+ import { build, type Rollup } from "vite";
7
+ import tailwind from "@tailwindcss/vite";
8
+ import { parseEditorId } from "@moloodev/editor-sdk/protocol";
9
+ import { validateEditorManifest } from "@moloodev/editor-sdk/commands/editor-document";
10
+ import { parseNetwork } from "@moloodev/platform/network";
11
+ import { publishBundlePlugin } from "./publish-bundle";
12
+ import { readEditorManifest } from "./read-editor-manifest";
13
+
14
+ export type EditorView = {
15
+ id: string;
16
+ editor: string;
17
+ packageRoot: string;
18
+ network: readonly string[];
19
+ };
20
+
21
+ const defaultFallback = join(dirname(fileURLToPath(import.meta.url)), "view-page.ts");
22
+
23
+ export function readPackageEditorView(packageRoot: string): EditorView & { manifest: Record<string, unknown> } {
24
+ const manifestPath = join(packageRoot, "src/manifest.ts");
25
+ if (!existsSync(manifestPath)) {
26
+ throw new Error(`Missing editor manifest at ${manifestPath}`);
27
+ }
28
+ const manifest = readEditorManifest(manifestPath);
29
+ validateEditorManifest(manifest);
30
+ if (typeof manifest.id !== "string") {
31
+ throw new Error(`Invalid editor manifest id in ${manifestPath}`);
32
+ }
33
+ const parsed = parseEditorId(manifest.id, true);
34
+ if (parsed === undefined) {
35
+ throw new Error(`Invalid editor id ${manifest.id}`);
36
+ }
37
+ const network = parseNetwork("network" in manifest ? manifest.network : undefined);
38
+ return { id: manifest.id, editor: parsed.editor, packageRoot, network, manifest };
39
+ }
40
+
41
+ export function findEditorViews(repo: string): EditorView[] {
42
+ const singleManifest = join(repo, "src/manifest.ts");
43
+ if (existsSync(singleManifest)) {
44
+ const view = readPackageEditorView(repo);
45
+ return [{ id: view.id, editor: view.editor, packageRoot: view.packageRoot, network: view.network }];
46
+ }
47
+ const dir = join(repo, "editors");
48
+ if (!existsSync(dir)) return [];
49
+ const found: EditorView[] = [];
50
+ for (const name of readdirSync(dir)) {
51
+ const packageRoot = join(dir, name);
52
+ const manifestPath = join(packageRoot, "src/manifest.ts");
53
+ if (!existsSync(manifestPath)) continue;
54
+ const manifest: unknown = readEditorManifest(manifestPath);
55
+ if (!isRecord(manifest) || typeof manifest.id !== "string") continue;
56
+ const parsed = parseEditorId(manifest.id, true);
57
+ if (parsed === undefined || parsed.publisher !== "moloo") continue;
58
+ const network = parseNetwork("network" in manifest ? manifest.network : undefined);
59
+ if (found.some((entry) => entry.id === manifest.id)) throw new Error(`Duplicate editor ${manifest.id}`);
60
+ found.push({ id: manifest.id, editor: parsed.editor, packageRoot, network });
61
+ }
62
+ return found.sort((left, right) => left.editor.localeCompare(right.editor));
63
+ }
64
+
65
+ export function resolveViewEntry(packageRoot: string, fallback: string = defaultFallback): string {
66
+ const manifest: unknown = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
67
+ if (!isRecord(manifest)) throw new Error("Invalid editor package.json");
68
+ const exports = manifest.exports;
69
+ if (exports === undefined || typeof exports === "string") return fallback;
70
+ if (!isRecord(exports)) throw new Error("Invalid editor exports");
71
+ const target = exports["./view"];
72
+ if (target === undefined) return fallback;
73
+ if (
74
+ typeof target !== "string" ||
75
+ !target.startsWith("./") ||
76
+ target.includes("\\") ||
77
+ target.split("/").includes("..") ||
78
+ isAbsolute(target)
79
+ ) {
80
+ throw new Error("Editor ./view must be a string path inside its package");
81
+ }
82
+ const root = realpathSync(packageRoot);
83
+ const entry = realpathSync(resolve(root, target));
84
+ const fromRoot = relative(root, entry);
85
+ if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) throw new Error("Editor ./view escapes its package");
86
+ return entry;
87
+ }
88
+
89
+ export async function buildEditorView(entry: string, output: string, metafile: string, styles?: string): Promise<void> {
90
+ const css = new Set<string>();
91
+ const view = await esbuild.build({
92
+ entryPoints: [entry],
93
+ bundle: true,
94
+ format: "iife",
95
+ platform: "browser",
96
+ jsx: "automatic",
97
+ write: false,
98
+ metafile: true,
99
+ logLevel: "error",
100
+ legalComments: "none",
101
+ define: productionDefine(),
102
+ alias: { "lib0/logging": join(dirname(fileURLToPath(import.meta.url)), "yjs-log.ts") },
103
+ plugins: [
104
+ publishBundlePlugin(),
105
+ {
106
+ name: "collect-view-css",
107
+ setup(builder) {
108
+ builder.onLoad({ filter: /\.css$/ }, (args) => {
109
+ css.add(args.path);
110
+ return { contents: "", loader: "js" };
111
+ });
112
+ },
113
+ },
114
+ ],
115
+ });
116
+ const file = view.outputFiles?.[0];
117
+ if (file === undefined) throw new Error("editor.js bundle was not produced.");
118
+ if (view.metafile === undefined) throw new Error("editor.js metafile was not produced.");
119
+ mkdirSync(join(output, ".."), { recursive: true });
120
+ const header = styles === undefined ? "" : await styleHeader(styles, [...css]);
121
+ writeFileSync(output, header + file.text);
122
+ writeFileSync(metafile, JSON.stringify(view.metafile));
123
+ }
124
+
125
+ export async function buildEditorScript(editor: EditorView, fallback: string = defaultFallback): Promise<string> {
126
+ const directory = mkdtempSync(join(tmpdir(), "moloo-editor-"));
127
+ try {
128
+ const output = join(directory, "editor.js");
129
+ const packageStyles = join(editor.packageRoot, "src/styles.css");
130
+ const styles = existsSync(packageStyles) ? packageStyles : undefined;
131
+ await buildEditorView(
132
+ resolveViewEntry(editor.packageRoot, fallback),
133
+ output,
134
+ join(directory, "meta.json"),
135
+ styles,
136
+ );
137
+ return readFileSync(output, "utf8");
138
+ } finally {
139
+ rmSync(directory, { recursive: true, force: true });
140
+ }
141
+ }
142
+
143
+ export async function buildEditorPackage(
144
+ packageRoot: string,
145
+ outDir: string = join(packageRoot, "dist"),
146
+ ): Promise<{ id: string; scriptPath: string; manifestPath: string; metafilePath: string; script: string; manifest: Record<string, unknown> }> {
147
+ const view = readPackageEditorView(packageRoot);
148
+ mkdirSync(outDir, { recursive: true });
149
+ const scriptPath = join(outDir, "editor.js");
150
+ const manifestPath = join(outDir, "manifest.json");
151
+ const metafilePath = join(outDir, "meta.json");
152
+ const styles = join(packageRoot, "src/styles.css");
153
+ await buildEditorView(
154
+ resolveViewEntry(packageRoot, defaultFallback),
155
+ scriptPath,
156
+ metafilePath,
157
+ existsSync(styles) ? styles : undefined,
158
+ );
159
+ writeFileSync(manifestPath, JSON.stringify(view.manifest, null, 2) + "\n");
160
+ const script = readFileSync(scriptPath, "utf8");
161
+ return { id: view.id, scriptPath, manifestPath, metafilePath, script, manifest: view.manifest };
162
+ }
163
+
164
+ export async function styleHeader(styles: string, collected: string[], sources: readonly string[] = []): Promise<string> {
165
+ const directory = mkdtempSync(join(dirname(styles), ".view-css-"));
166
+ try {
167
+ const entry = join(directory, "input.css");
168
+ writeFileSync(
169
+ entry,
170
+ [
171
+ ...[styles, ...collected].map((file) => `@import ${JSON.stringify(file)};`),
172
+ ...sources.map((dir) => `@source ${JSON.stringify(dir)};`),
173
+ ].join("\n"),
174
+ );
175
+ const compiled = await build({
176
+ configFile: false,
177
+ root: directory,
178
+ plugins: [tailwind()],
179
+ logLevel: "error",
180
+ build: { write: false, cssMinify: false, rollupOptions: { input: entry } },
181
+ });
182
+ const results = Array.isArray(compiled) ? compiled : [compiled];
183
+ const text = results
184
+ .flatMap((result) => ("output" in result ? result.output : []))
185
+ .filter((asset): asset is Rollup.OutputAsset => asset.type === "asset" && asset.fileName.endsWith(".css"))
186
+ .map((asset) => (typeof asset.source === "string" ? asset.source : Buffer.from(asset.source).toString("utf8")))
187
+ .join("\n");
188
+ if (!text) throw new Error("view CSS was not produced.");
189
+ return `(() => { const style = document.createElement("style"); style.textContent = ${JSON.stringify(text)}; document.head.append(style); })();\n`;
190
+ } finally {
191
+ rmSync(directory, { recursive: true, force: true });
192
+ }
193
+ }
194
+
195
+ function productionDefine(): Record<string, string> {
196
+ return { "process.env.NODE_ENV": '"production"' };
197
+ }
198
+
199
+ function isRecord(value: unknown): value is Record<string, unknown> {
200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
201
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export {
2
+ buildEditorPackage,
3
+ buildEditorScript,
4
+ buildEditorView,
5
+ findEditorViews,
6
+ readPackageEditorView,
7
+ resolveViewEntry,
8
+ type EditorView,
9
+ } from "./editor-views";
10
+ export {
11
+ startEditorDevServer,
12
+ type EditorDevServer,
13
+ type StartEditorDevServerOptions,
14
+ } from "./dev-server";
15
+ export { publishBundlePlugin } from "./publish-bundle";
16
+ export {
17
+ publishEditor,
18
+ publishEditorPackage,
19
+ publishEndpoint,
20
+ reservedEditorId,
21
+ uploadEndpoint,
22
+ } from "./publish";
23
+ export { readEditorManifest } from "./read-editor-manifest";
24
+ export { rewriteThreeLoaders } from "./three-local-loaders";
25
+ export { runCli } from "./cli";