mosage 0.1.0 → 0.8.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.
Files changed (146) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +16 -217
  3. package/bin.js +2 -0
  4. package/dist/build-C7NW_3Pk.js +14 -0
  5. package/dist/check-CP4873Wx.js +41 -0
  6. package/dist/cli/bin.d.ts +1 -0
  7. package/dist/cli/bin.js +228 -0
  8. package/dist/config-DPm1BBAb.js +2619 -0
  9. package/dist/config-TlTe7Ona.d.ts +24 -0
  10. package/dist/context-BqsdSrAQ.js +1084 -0
  11. package/dist/dev-Biz42qlu.js +17 -0
  12. package/dist/diagram-xlVDekYk.js +763 -0
  13. package/dist/export-Bi6nuxjT.js +31 -0
  14. package/dist/import-D2jNB07F.js +25 -0
  15. package/dist/index.d.ts +455 -0
  16. package/dist/index.js +693 -0
  17. package/dist/init-Bbtj2pxF.js +262 -0
  18. package/dist/preview-CLm51aRt.js +19 -0
  19. package/dist/sdk-DjpX6mCv.js +51 -0
  20. package/dist/vite/index.d.ts +25 -0
  21. package/dist/vite/index.js +2 -0
  22. package/env.d.ts +83 -0
  23. package/package.json +84 -64
  24. package/skills/apply-comments/SKILL.md +43 -56
  25. package/skills/create-doc/SKILL.md +106 -0
  26. package/skills/create-theme/SKILL.md +184 -0
  27. package/skills/current-doc/SKILL.md +120 -0
  28. package/skills/doc-authoring/SKILL.md +434 -0
  29. package/skills/doc-authoring/references/assets.md +47 -0
  30. package/skills/doc-authoring/references/design-system.md +81 -0
  31. package/skills/doc-authoring/references/long-form.md +131 -0
  32. package/skills/doc-authoring/references/pagination.md +118 -0
  33. package/skills/doc-authoring/references/tables-and-charts.md +161 -0
  34. package/src/app/app.tsx +42 -0
  35. package/src/app/components/data-table.tsx +196 -0
  36. package/src/app/components/design-panel/design-panel.tsx +318 -0
  37. package/src/app/components/design-panel/design-provider.tsx +121 -0
  38. package/src/app/components/design-panel/use-design.ts +85 -0
  39. package/src/app/components/diagram.tsx +76 -0
  40. package/src/app/components/doc-assets.tsx +129 -0
  41. package/src/app/components/doc-search.tsx +248 -0
  42. package/src/app/components/doc-sidebar.tsx +162 -0
  43. package/src/app/components/flow-page.tsx +93 -0
  44. package/src/app/components/footnote.tsx +204 -0
  45. package/src/app/components/image-placeholder.tsx +50 -0
  46. package/src/app/components/inspector/inspector.tsx +518 -0
  47. package/src/app/components/numbering.tsx +224 -0
  48. package/src/app/components/page-frame.tsx +70 -0
  49. package/src/app/components/sidebar/folder-item.tsx +212 -0
  50. package/src/app/components/sidebar/icon-picker.tsx +99 -0
  51. package/src/app/components/sidebar/sidebar.tsx +252 -0
  52. package/src/app/components/table-of-contents.tsx +93 -0
  53. package/src/app/components/theme-toggle.tsx +50 -0
  54. package/src/app/components/themes/markdown.tsx +249 -0
  55. package/src/app/components/themes/theme-preview.tsx +74 -0
  56. package/src/app/components/ui/menu.tsx +143 -0
  57. package/src/app/index.html +12 -0
  58. package/src/app/lib/agent-bridge.ts +140 -0
  59. package/src/app/lib/assets.ts +151 -0
  60. package/src/app/lib/design-presets.ts +109 -0
  61. package/src/app/lib/design.ts +88 -0
  62. package/src/app/lib/diagnostics.ts +282 -0
  63. package/src/app/lib/doc-preview.tsx +29 -0
  64. package/src/app/lib/docs.ts +26 -0
  65. package/src/app/lib/docx/extract.ts +1623 -0
  66. package/src/app/lib/docx/fonts.test.ts +136 -0
  67. package/src/app/lib/docx/fonts.ts +166 -0
  68. package/src/app/lib/docx/media.ts +102 -0
  69. package/src/app/lib/docx/model.ts +206 -0
  70. package/src/app/lib/docx/paragraph.test.ts +92 -0
  71. package/src/app/lib/docx/paragraph.ts +107 -0
  72. package/src/app/lib/docx/props.ts +187 -0
  73. package/src/app/lib/docx/styles.ts +306 -0
  74. package/src/app/lib/docx/units.ts +35 -0
  75. package/src/app/lib/docx/write.test.ts +507 -0
  76. package/src/app/lib/docx/write.ts +581 -0
  77. package/src/app/lib/docx/xml.ts +39 -0
  78. package/src/app/lib/export-docx.ts +289 -0
  79. package/src/app/lib/export-dom.ts +318 -0
  80. package/src/app/lib/export-html.ts +156 -0
  81. package/src/app/lib/export-image.ts +70 -0
  82. package/src/app/lib/export-pdf.ts +165 -0
  83. package/src/app/lib/flow-measure.test.ts +31 -0
  84. package/src/app/lib/flow-measure.ts +183 -0
  85. package/src/app/lib/flow.test.ts +110 -0
  86. package/src/app/lib/flow.ts +136 -0
  87. package/src/app/lib/folders.ts +192 -0
  88. package/src/app/lib/footnotes.test.tsx +102 -0
  89. package/src/app/lib/footnotes.ts +94 -0
  90. package/src/app/lib/inspector/fiber.ts +99 -0
  91. package/src/app/lib/labels.test.ts +18 -0
  92. package/src/app/lib/labels.ts +181 -0
  93. package/src/app/lib/outline.ts +118 -0
  94. package/src/app/lib/page-context.tsx +43 -0
  95. package/src/app/lib/page-range.test.ts +95 -0
  96. package/src/app/lib/page-range.ts +90 -0
  97. package/src/app/lib/print-ready.ts +69 -0
  98. package/src/app/lib/rasterize.ts +173 -0
  99. package/src/app/lib/scan.ts +26 -0
  100. package/src/app/lib/sdk.test.ts +32 -0
  101. package/src/app/lib/sdk.ts +115 -0
  102. package/src/app/lib/themes.ts +31 -0
  103. package/src/app/lib/use-doc-module.ts +53 -0
  104. package/src/app/lib/use-doc-pages.ts +147 -0
  105. package/src/app/lib/utils.ts +6 -0
  106. package/src/app/lib/view-mode.test.ts +91 -0
  107. package/src/app/lib/view-mode.ts +104 -0
  108. package/src/app/main.tsx +14 -0
  109. package/src/app/routes/assets.tsx +257 -0
  110. package/src/app/routes/doc.tsx +877 -0
  111. package/src/app/routes/home-shell.tsx +203 -0
  112. package/src/app/routes/home.tsx +269 -0
  113. package/src/app/routes/themes.tsx +121 -0
  114. package/src/app/styles.css +97 -0
  115. package/src/app/virtual.d.ts +30 -0
  116. package/template/AGENTS.md +27 -0
  117. package/template/README.md +39 -0
  118. package/template/docs/getting-started/index.tsx +230 -0
  119. package/template/mosage.config.ts +5 -0
  120. package/template/package.json +24 -0
  121. package/template/tsconfig.json +17 -0
  122. package/README.en.md +0 -57
  123. package/dist/cli.js +0 -4545
  124. package/dist/web/assets/index-Czg2WeHe.js +0 -182
  125. package/dist/web/assets/index-DKAyt92W.css +0 -1
  126. package/dist/web/index.html +0 -15
  127. package/skills/current-position/SKILL.md +0 -65
  128. package/skills/kickoff/SKILL.md +0 -105
  129. package/skills/mosage-reference/SKILL.md +0 -164
  130. package/skills/outline/SKILL.md +0 -79
  131. package/skills/review/SKILL.md +0 -64
  132. package/skills/write-chapter/SKILL.md +0 -58
  133. package/template/book/STYLE.md +0 -6
  134. package/template/book/assets/.gitkeep +0 -0
  135. package/template/book/book.yaml +0 -42
  136. package/template/book/brief.md +0 -6
  137. package/template/book/chapters/.gitkeep +0 -0
  138. package/template/book/notes/README.md +0 -7
  139. package/template/project/AGENTS.md +0 -82
  140. package/template/project/CLAUDE.md +0 -1
  141. package/template/project/README.md +0 -48
  142. package/template/project/books/.gitkeep +0 -0
  143. package/template/project/gitignore +0 -5
  144. package/template/project/mosage.yaml +0 -23
  145. package/template/project/notes/README.md +0 -8
  146. package/template/project/package.json +0 -14
@@ -0,0 +1,2619 @@
1
+ import { a as defaultDesign, n as DiagramSyntaxError, r as parseDelimited, t as compileDiagram } from "./diagram-xlVDekYk.js";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import fs from "node:fs/promises";
6
+ import tailwindcss from "@tailwindcss/vite";
7
+ import react from "@vitejs/plugin-react";
8
+ import fg from "fast-glob";
9
+ import { loadConfigFromFile, normalizePath } from "vite";
10
+ import { parse } from "@babel/parser";
11
+ import { randomUUID } from "node:crypto";
12
+ //#region src/vite/mosage-plugin.ts
13
+ const DOC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
14
+ const CONFIG_FILE = "mosage.config.ts";
15
+ const DOCS_VMOD = "virtual:mosage/docs";
16
+ const CONFIG_VMOD = "virtual:mosage/config";
17
+ const FOLDERS_VMOD = "virtual:mosage/folders";
18
+ async function readFoldersManifest(file) {
19
+ try {
20
+ const parsed = JSON.parse(await fs.readFile(file, "utf8"));
21
+ return {
22
+ folders: Array.isArray(parsed.folders) ? parsed.folders : [],
23
+ assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
24
+ };
25
+ } catch (err) {
26
+ if (err.code === "ENOENT") return {
27
+ folders: [],
28
+ assignments: {}
29
+ };
30
+ throw err;
31
+ }
32
+ }
33
+ function resolved$1(id) {
34
+ return `\0${id}`;
35
+ }
36
+ async function findDocs(userCwd, docsDir) {
37
+ const abs = path.resolve(userCwd, docsDir);
38
+ if (!existsSync(abs)) return [];
39
+ return (await fg("*/index.{tsx,jsx,ts,js}", {
40
+ cwd: abs,
41
+ absolute: true,
42
+ onlyFiles: true
43
+ })).sort();
44
+ }
45
+ function toId(absFile, docsRoot) {
46
+ return path.relative(docsRoot, absFile).split(path.sep)[0];
47
+ }
48
+ const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
49
+ const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
50
+ /**
51
+ * Reads `meta` with a brace-matched regex instead of parsing: the plugin runs on
52
+ * every discovery pass, and the two fields it needs (sort order, theme back-link)
53
+ * are contractually string literals.
54
+ */
55
+ function extractMeta(src) {
56
+ const empty = {
57
+ theme: null,
58
+ createdAt: null
59
+ };
60
+ const metaStart = src.search(/export\s+const\s+meta\b/);
61
+ if (metaStart === -1) return empty;
62
+ const openBrace = src.indexOf("{", src.indexOf("=", metaStart));
63
+ if (openBrace === -1) return empty;
64
+ let depth = 0;
65
+ let closeBrace = -1;
66
+ for (let i = openBrace; i < src.length; i++) {
67
+ const ch = src[i];
68
+ if (ch === "{") depth++;
69
+ else if (ch === "}") {
70
+ depth--;
71
+ if (depth === 0) {
72
+ closeBrace = i;
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ if (closeBrace === -1) return empty;
78
+ const body = src.slice(openBrace + 1, closeBrace);
79
+ return {
80
+ theme: body.match(META_THEME_RE)?.[1] ?? null,
81
+ createdAt: body.match(META_CREATED_AT_RE)?.[1] ?? null
82
+ };
83
+ }
84
+ function parseCreatedAtMs(iso) {
85
+ if (!iso) return null;
86
+ const ms = Date.parse(iso);
87
+ return Number.isFinite(ms) ? ms : null;
88
+ }
89
+ async function readMeta(abs) {
90
+ try {
91
+ return extractMeta(await fs.readFile(abs, "utf8"));
92
+ } catch {
93
+ return {
94
+ theme: null,
95
+ createdAt: null
96
+ };
97
+ }
98
+ }
99
+ const warnedInvalidDocIds = /* @__PURE__ */ new Set();
100
+ async function generateDocsModule(files, docsRoot, isDev) {
101
+ const scanned = await Promise.all(files.map(async (abs) => {
102
+ const meta = await readMeta(abs);
103
+ return {
104
+ id: toId(abs, docsRoot),
105
+ importPath: isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs,
106
+ theme: meta.theme,
107
+ createdAt: parseCreatedAtMs(meta.createdAt)
108
+ };
109
+ }));
110
+ const entries = scanned.filter((e) => DOC_ID_RE.test(e.id));
111
+ const ignored = scanned.filter((e) => !DOC_ID_RE.test(e.id)).map((e) => e.id);
112
+ const ids = JSON.stringify(entries.map((e) => e.id).sort());
113
+ const createdAtMap = {};
114
+ const themesMap = {};
115
+ for (const e of entries) {
116
+ if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
117
+ if (e.theme) themesMap[e.id] = e.theme;
118
+ }
119
+ const importTokens = JSON.stringify(Object.fromEntries(entries.map((e) => [e.id, 0])));
120
+ const devRuntime = isDev ? `
121
+ const docImportTokens = ${importTokens};
122
+ if (import.meta.hot) {
123
+ import.meta.hot.on('mosage:doc-changed', (data) => {
124
+ const ids = Array.isArray(data?.docIds) ? data.docIds : data?.docId ? [data.docId] : [];
125
+ const token = Date.now();
126
+ for (const id of ids) {
127
+ if (Object.prototype.hasOwnProperty.call(docImportTokens, id)) docImportTokens[id] = token;
128
+ }
129
+ });
130
+ }
131
+ ` : "";
132
+ const cases = entries.map((e) => {
133
+ const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(`${e.importPath}?t=`)} + docImportTokens[${JSON.stringify(e.id)}])` : `import(${JSON.stringify(e.importPath)})`;
134
+ return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
135
+ }).join("\n");
136
+ return {
137
+ code: `// virtual:mosage/docs — generated
138
+ export const docIds = ${ids};
139
+ export const docCreatedAt = ${JSON.stringify(createdAtMap)};
140
+ export const docThemes = ${JSON.stringify(themesMap)};
141
+ ${devRuntime}
142
+
143
+ export async function loadDoc(id) {
144
+ switch (id) {
145
+ ${cases}
146
+ default: throw new Error('Document not found: ' + id);
147
+ }
148
+ }
149
+ `,
150
+ ignored
151
+ };
152
+ }
153
+ function mosagePlugin(opts) {
154
+ const { userCwd, config, coreVersion } = opts;
155
+ const docsDir = config.docsDir ?? "docs";
156
+ const docsRoot = path.resolve(userCwd, docsDir);
157
+ const foldersManifestPath = path.join(docsRoot, ".folders.json");
158
+ let isDev = false;
159
+ const docIdForEntry = (p) => {
160
+ const rel = path.relative(docsRoot, p);
161
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
162
+ const parts = rel.split(path.sep);
163
+ if (parts.length !== 2) return null;
164
+ if (!/^index\.(tsx|jsx|ts|js)$/.test(parts[1])) return null;
165
+ return parts[0];
166
+ };
167
+ let docChangeTimer = null;
168
+ const pendingDocChanges = /* @__PURE__ */ new Set();
169
+ const queueDocChanged = (server, id) => {
170
+ pendingDocChanges.add(id);
171
+ if (docChangeTimer) clearTimeout(docChangeTimer);
172
+ docChangeTimer = setTimeout(() => {
173
+ docChangeTimer = null;
174
+ const mod = server.moduleGraph.getModuleById(resolved$1(DOCS_VMOD));
175
+ if (mod) server.moduleGraph.invalidateModule(mod);
176
+ const docIds = Array.from(pendingDocChanges);
177
+ pendingDocChanges.clear();
178
+ server.ws.send({
179
+ type: "custom",
180
+ event: "mosage:doc-changed",
181
+ data: { docIds }
182
+ });
183
+ }, 100);
184
+ };
185
+ return {
186
+ name: "mosage",
187
+ config(_c, env) {
188
+ isDev = env.command === "serve";
189
+ return { server: { fs: { allow: [userCwd] } } };
190
+ },
191
+ resolveId(id) {
192
+ if (id === DOCS_VMOD) return resolved$1(DOCS_VMOD);
193
+ if (id === CONFIG_VMOD) return resolved$1(CONFIG_VMOD);
194
+ if (id === FOLDERS_VMOD) return resolved$1(FOLDERS_VMOD);
195
+ return null;
196
+ },
197
+ async load(id) {
198
+ if (id === resolved$1(DOCS_VMOD)) {
199
+ const { code, ignored } = await generateDocsModule(await findDocs(userCwd, docsDir), docsRoot, isDev);
200
+ for (const docId of ignored) {
201
+ if (warnedInvalidDocIds.has(docId)) continue;
202
+ warnedInvalidDocIds.add(docId);
203
+ this.warn(`Ignoring document folder "${docId}": ids must match ${DOC_ID_RE} (letters, digits, "-", "_"). Rename it under "${docsDir}/" to a kebab-case id so it shows up.`);
204
+ }
205
+ return code;
206
+ }
207
+ if (id === resolved$1(CONFIG_VMOD)) {
208
+ const userBuild = config.build ?? {};
209
+ const build = isDev ? {
210
+ showDocBrowser: true,
211
+ allowHtmlExport: true
212
+ } : {
213
+ showDocBrowser: userBuild.showDocBrowser ?? true,
214
+ allowHtmlExport: userBuild.allowHtmlExport ?? true
215
+ };
216
+ return `export default ${JSON.stringify({
217
+ ...config,
218
+ build,
219
+ version: coreVersion
220
+ })};\n`;
221
+ }
222
+ if (id === resolved$1(FOLDERS_VMOD)) {
223
+ const manifest = await readFoldersManifest(foldersManifestPath);
224
+ return `export default ${JSON.stringify(manifest)};\n`;
225
+ }
226
+ return null;
227
+ },
228
+ handleHotUpdate(ctx) {
229
+ const docId = docIdForEntry(ctx.file);
230
+ if (!docId) return;
231
+ queueDocChanged(ctx.server, docId);
232
+ return [];
233
+ },
234
+ configureServer(server) {
235
+ const isDocEntry = (p) => docIdForEntry(p) !== null;
236
+ let reloadTimer = null;
237
+ const reload = () => {
238
+ if (reloadTimer) clearTimeout(reloadTimer);
239
+ reloadTimer = setTimeout(() => {
240
+ reloadTimer = null;
241
+ const mod = server.moduleGraph.getModuleById(resolved$1(DOCS_VMOD));
242
+ if (mod) server.moduleGraph.invalidateModule(mod);
243
+ server.ws.send({ type: "full-reload" });
244
+ }, 150);
245
+ };
246
+ server.watcher.add(docsRoot);
247
+ server.watcher.on("add", (p) => {
248
+ if (isDocEntry(p)) reload();
249
+ });
250
+ server.watcher.on("unlink", (p) => {
251
+ if (isDocEntry(p)) reload();
252
+ });
253
+ server.watcher.on("unlinkDir", (p) => {
254
+ if (path.dirname(p) === docsRoot) reload();
255
+ });
256
+ let foldersTimer = null;
257
+ const foldersChanged = () => {
258
+ if (foldersTimer) clearTimeout(foldersTimer);
259
+ foldersTimer = setTimeout(() => {
260
+ foldersTimer = null;
261
+ const mod = server.moduleGraph.getModuleById(resolved$1(FOLDERS_VMOD));
262
+ if (mod) server.moduleGraph.invalidateModule(mod);
263
+ server.ws.send({
264
+ type: "custom",
265
+ event: "mosage:files-changed",
266
+ data: {}
267
+ });
268
+ }, 100);
269
+ };
270
+ server.watcher.add(foldersManifestPath);
271
+ for (const event of [
272
+ "add",
273
+ "change",
274
+ "unlink"
275
+ ]) server.watcher.on(event, (p) => {
276
+ if (p === foldersManifestPath) foldersChanged();
277
+ });
278
+ }
279
+ };
280
+ }
281
+ async function loadUserConfig(userCwd) {
282
+ const file = path.join(userCwd, CONFIG_FILE);
283
+ if (!existsSync(file)) return {};
284
+ return (await loadConfigFromFile({
285
+ command: "serve",
286
+ mode: "development"
287
+ }, file, userCwd, "silent"))?.config ?? {};
288
+ }
289
+ //#endregion
290
+ //#region src/files/assets.ts
291
+ const GLOBAL_SCOPE = "@global";
292
+ const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
293
+ const MIME_BY_EXT = {
294
+ png: "image/png",
295
+ jpg: "image/jpeg",
296
+ jpeg: "image/jpeg",
297
+ gif: "image/gif",
298
+ svg: "image/svg+xml",
299
+ webp: "image/webp",
300
+ avif: "image/avif",
301
+ ico: "image/x-icon",
302
+ pdf: "application/pdf",
303
+ woff: "font/woff",
304
+ woff2: "font/woff2",
305
+ ttf: "font/ttf",
306
+ otf: "font/otf",
307
+ csv: "text/csv; charset=utf-8",
308
+ json: "application/json",
309
+ txt: "text/plain; charset=utf-8",
310
+ md: "text/markdown; charset=utf-8"
311
+ };
312
+ function mimeForFilename(name) {
313
+ const dot = name.lastIndexOf(".");
314
+ if (dot < 0) return "application/octet-stream";
315
+ return MIME_BY_EXT[name.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
316
+ }
317
+ function assetCreatedAt(birthtimeMs, mtimeMs) {
318
+ return Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : mtimeMs;
319
+ }
320
+ function validateAssetName(v) {
321
+ if (typeof v !== "string") return null;
322
+ const trimmed = v.trim();
323
+ if (trimmed.length < 1 || trimmed.length > 120) return null;
324
+ if (ASSET_FORBIDDEN_RE.test(trimmed)) return null;
325
+ if (trimmed.startsWith(".") || trimmed.startsWith("~")) return null;
326
+ if (trimmed === ".." || trimmed.split(/[/\\]/).includes("..")) return null;
327
+ const dot = trimmed.lastIndexOf(".");
328
+ if (dot <= 0 || dot === trimmed.length - 1) return null;
329
+ return trimmed;
330
+ }
331
+ function resolveAssetsDir(docsRoot, docId) {
332
+ if (!DOC_ID_RE.test(docId)) return null;
333
+ const docDir = path.resolve(docsRoot, docId);
334
+ if (!docDir.startsWith(docsRoot + path.sep)) return null;
335
+ const assetsDir = path.resolve(docDir, "assets");
336
+ if (assetsDir !== path.join(docDir, "assets")) return null;
337
+ return assetsDir;
338
+ }
339
+ function resolveScopedAssetsDir(docsRoot, globalAssetsRoot, scope) {
340
+ if (scope === "@global") return globalAssetsRoot;
341
+ return resolveAssetsDir(docsRoot, scope);
342
+ }
343
+ function resolveScopedAssetFile(docsRoot, globalAssetsRoot, scope, filename) {
344
+ if (!validateAssetName(filename)) return null;
345
+ const dir = resolveScopedAssetsDir(docsRoot, globalAssetsRoot, scope);
346
+ if (!dir) return null;
347
+ const file = path.resolve(dir, filename);
348
+ if (!file.startsWith(dir + path.sep)) return null;
349
+ return file;
350
+ }
351
+ /** How a document imports an asset — the string we look for when counting usages. */
352
+ function assetImportPath(scope, filename) {
353
+ return scope === "@global" ? `@assets/${filename}` : `./assets/${filename}`;
354
+ }
355
+ function escapeRegExp(value) {
356
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
357
+ }
358
+ /**
359
+ * Counts references to an asset in a document's source. Matches the quoted path
360
+ * in both import statements and `new URL('./assets/x.png', import.meta.url)`,
361
+ * which is every form the authoring skill sanctions.
362
+ */
363
+ function countAssetUsages(source, importPath) {
364
+ const quoted = new RegExp(`(['"\`])${escapeRegExp(importPath)}\\1`, "g");
365
+ return source.match(quoted)?.length ?? 0;
366
+ }
367
+ function findReferencedAssets(source, importPaths) {
368
+ return importPaths.filter((p) => countAssetUsages(source, p) > 0);
369
+ }
370
+ //#endregion
371
+ //#region src/http/request-guard.ts
372
+ function firstHeaderValue(value) {
373
+ if (Array.isArray(value)) return value[0] ?? null;
374
+ return value ?? null;
375
+ }
376
+ function headerValue(req, name) {
377
+ return firstHeaderValue(req.headers[name.toLowerCase()])?.trim() ?? null;
378
+ }
379
+ function firstCommaToken(value) {
380
+ if (!value) return null;
381
+ const [first] = value.split(",", 1);
382
+ return first?.trim() || null;
383
+ }
384
+ function requestProto(req) {
385
+ const forwarded = firstCommaToken(headerValue(req, "x-forwarded-proto"))?.toLowerCase();
386
+ if (forwarded === "http" || forwarded === "https") return forwarded;
387
+ return "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
388
+ }
389
+ function normalizedOrigin(origin) {
390
+ try {
391
+ const url = new URL(origin);
392
+ return `${url.protocol}//${url.host}`.toLowerCase();
393
+ } catch {
394
+ return null;
395
+ }
396
+ }
397
+ /**
398
+ * The dev server writes to the user's filesystem, so every mutating endpoint is
399
+ * guarded against a page on another origin driving it through the browser.
400
+ */
401
+ function validateMutationRequest(req, opts = {}) {
402
+ if (opts.requireJsonBody) {
403
+ if (!(headerValue(req, "content-type")?.toLowerCase())?.startsWith("application/json")) return {
404
+ ok: false,
405
+ status: 415,
406
+ error: "content-type must be application/json"
407
+ };
408
+ }
409
+ if (firstCommaToken(headerValue(req, "sec-fetch-site"))?.toLowerCase() === "cross-site") return {
410
+ ok: false,
411
+ status: 403,
412
+ error: "cross-site request blocked"
413
+ };
414
+ const originRaw = headerValue(req, "origin");
415
+ if (!originRaw) return { ok: true };
416
+ if (originRaw.toLowerCase() === "null") return {
417
+ ok: false,
418
+ status: 403,
419
+ error: "opaque origin is not allowed"
420
+ };
421
+ const actualOrigin = normalizedOrigin(originRaw);
422
+ if (!actualOrigin) return {
423
+ ok: false,
424
+ status: 403,
425
+ error: "invalid origin header"
426
+ };
427
+ const host = firstCommaToken(headerValue(req, "x-forwarded-host")) ?? headerValue(req, "host");
428
+ if (!host) return {
429
+ ok: false,
430
+ status: 400,
431
+ error: "missing host header"
432
+ };
433
+ if (actualOrigin !== `${requestProto(req)}://${host}`.toLowerCase()) return {
434
+ ok: false,
435
+ status: 403,
436
+ error: "origin mismatch"
437
+ };
438
+ return { ok: true };
439
+ }
440
+ //#endregion
441
+ //#region src/vite/routes/context.ts
442
+ function makeContext(opts) {
443
+ const userCwd = opts.userCwd;
444
+ const docsDir = opts.docsDir ?? "docs";
445
+ const assetsDir = opts.assetsDir ?? "assets";
446
+ const docsRoot = path.resolve(userCwd, docsDir);
447
+ return {
448
+ userCwd,
449
+ docsDir,
450
+ docsRoot,
451
+ globalAssetsRoot: path.resolve(userCwd, assetsDir),
452
+ manifestPath: path.join(docsRoot, ".folders.json"),
453
+ coreVersion: opts.coreVersion,
454
+ ...opts.serverOrigin !== void 0 ? { serverOrigin: opts.serverOrigin } : {}
455
+ };
456
+ }
457
+ async function readBody(req) {
458
+ return await new Promise((resolve, reject) => {
459
+ const chunks = [];
460
+ req.on("data", (c) => chunks.push(c));
461
+ req.on("end", () => {
462
+ const raw = Buffer.concat(chunks).toString("utf8");
463
+ if (!raw) return resolve({});
464
+ try {
465
+ resolve(JSON.parse(raw));
466
+ } catch (e) {
467
+ reject(e);
468
+ }
469
+ });
470
+ req.on("error", reject);
471
+ });
472
+ }
473
+ function json(res, status, body) {
474
+ res.statusCode = status;
475
+ res.setHeader("content-type", "application/json");
476
+ res.end(JSON.stringify(body));
477
+ }
478
+ const ENTRY_NAMES$1 = [
479
+ "index.tsx",
480
+ "index.jsx",
481
+ "index.ts",
482
+ "index.js"
483
+ ];
484
+ /** Absolute path of a document's entry file, or null when the id is unusable. */
485
+ function resolveDocPath(userCwd, docsDir, docId) {
486
+ if (!DOC_ID_RE.test(docId)) return null;
487
+ const docsRoot = path.resolve(userCwd, docsDir);
488
+ const full = path.resolve(docsRoot, docId, ENTRY_NAMES$1[0]);
489
+ if (!full.startsWith(docsRoot + path.sep)) return null;
490
+ return full;
491
+ }
492
+ /** The entry file that actually exists on disk for a document, or null. */
493
+ function resolveDocEntry(docsRoot, docId) {
494
+ if (!DOC_ID_RE.test(docId)) return null;
495
+ const docDir = path.resolve(docsRoot, docId);
496
+ if (!docDir.startsWith(docsRoot + path.sep)) return null;
497
+ for (const name of ENTRY_NAMES$1) {
498
+ const candidate = path.join(docDir, name);
499
+ if (existsSync(candidate)) return candidate;
500
+ }
501
+ return null;
502
+ }
503
+ //#endregion
504
+ //#region src/vite/routes/assets.ts
505
+ async function listDocIds$1(docsRoot) {
506
+ try {
507
+ return (await fs.readdir(docsRoot, { withFileTypes: true })).filter((e) => e.isDirectory() && DOC_ID_RE.test(e.name)).map((e) => e.name);
508
+ } catch {
509
+ return [];
510
+ }
511
+ }
512
+ async function readDocSource(docsRoot, docId) {
513
+ const entry = resolveDocEntry(docsRoot, docId);
514
+ if (!entry) return null;
515
+ try {
516
+ return await fs.readFile(entry, "utf8");
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+ function registerAssetRoutes(server, ctx) {
522
+ server.middlewares.use("/__assets", async (req, res, next) => {
523
+ const url = new URL(req.url ?? "/", "http://local");
524
+ const method = req.method ?? "GET";
525
+ try {
526
+ const listMatch = url.pathname.match(/^\/([^/]+)\/?$/);
527
+ const fileMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)$/);
528
+ const usagesMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)\/usages$/);
529
+ if (usagesMatch && method === "GET") {
530
+ const scope = decodeURIComponent(usagesMatch[1]);
531
+ const filename = decodeURIComponent(usagesMatch[2]);
532
+ if (!validateAssetName(filename)) return json(res, 400, { error: "invalid path" });
533
+ const isGlobal = scope === GLOBAL_SCOPE;
534
+ if (!isGlobal && !DOC_ID_RE.test(scope)) return json(res, 400, { error: "invalid scope" });
535
+ const importPath = assetImportPath(scope, filename);
536
+ const docIds = isGlobal ? await listDocIds$1(ctx.docsRoot) : [scope];
537
+ const usages = [];
538
+ let totalCount = 0;
539
+ for (const docId of docIds) {
540
+ const source = await readDocSource(ctx.docsRoot, docId);
541
+ if (source === null) continue;
542
+ const count = countAssetUsages(source, importPath);
543
+ if (count > 0) {
544
+ usages.push({
545
+ docId,
546
+ count
547
+ });
548
+ totalCount += count;
549
+ }
550
+ }
551
+ return json(res, 200, {
552
+ usages,
553
+ totalCount
554
+ });
555
+ }
556
+ if (listMatch && method === "GET") {
557
+ const scope = decodeURIComponent(listMatch[1]);
558
+ const scopedDir = resolveScopedAssetsDir(ctx.docsRoot, ctx.globalAssetsRoot, scope);
559
+ if (!scopedDir) return json(res, 400, { error: "invalid scope" });
560
+ let entries;
561
+ try {
562
+ entries = await fs.readdir(scopedDir);
563
+ } catch (err) {
564
+ if (err.code === "ENOENT") return json(res, 200, { assets: [] });
565
+ throw err;
566
+ }
567
+ const assets = [];
568
+ for (const name of entries) {
569
+ if (!validateAssetName(name)) continue;
570
+ const stat = await fs.stat(path.join(scopedDir, name));
571
+ if (!stat.isFile()) continue;
572
+ assets.push({
573
+ name,
574
+ size: stat.size,
575
+ createdAt: assetCreatedAt(stat.birthtimeMs, stat.mtimeMs),
576
+ mtime: stat.mtimeMs,
577
+ mime: mimeForFilename(name),
578
+ url: `/__assets/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
579
+ importPath: assetImportPath(scope, name),
580
+ unused: true
581
+ });
582
+ }
583
+ assets.sort((a, b) => a.name.localeCompare(b.name));
584
+ if (assets.length > 0) {
585
+ const scanIds = scope === "@global" ? await listDocIds$1(ctx.docsRoot) : [scope];
586
+ const pathToAsset = new Map(assets.map((a) => [a.importPath, a]));
587
+ const paths = assets.map((a) => a.importPath);
588
+ for (const docId of scanIds) {
589
+ const source = await readDocSource(ctx.docsRoot, docId);
590
+ if (source === null) continue;
591
+ for (const p of findReferencedAssets(source, paths)) {
592
+ const asset = pathToAsset.get(p);
593
+ if (asset) asset.unused = false;
594
+ }
595
+ }
596
+ }
597
+ return json(res, 200, { assets });
598
+ }
599
+ if (fileMatch) {
600
+ const scope = decodeURIComponent(fileMatch[1]);
601
+ const filename = decodeURIComponent(fileMatch[2]);
602
+ const file = resolveScopedAssetFile(ctx.docsRoot, ctx.globalAssetsRoot, scope, filename);
603
+ if (!file) return json(res, 400, { error: "invalid path" });
604
+ if (method === "GET") try {
605
+ const buf = await fs.readFile(file);
606
+ res.statusCode = 200;
607
+ res.setHeader("content-type", mimeForFilename(filename));
608
+ res.setHeader("cache-control", "no-store");
609
+ res.end(buf);
610
+ return;
611
+ } catch (err) {
612
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
613
+ throw err;
614
+ }
615
+ if (method === "POST") {
616
+ const requestCheck = validateMutationRequest(req);
617
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
618
+ const overwrite = url.searchParams.get("overwrite") === "1";
619
+ const lenHeader = req.headers["content-length"];
620
+ const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
621
+ if (Number.isFinite(len) && len > 26214400) return json(res, 413, { error: "file too large" });
622
+ if (!overwrite) try {
623
+ await fs.access(file);
624
+ return json(res, 409, { error: "asset exists" });
625
+ } catch {}
626
+ const scopedDir = resolveScopedAssetsDir(ctx.docsRoot, ctx.globalAssetsRoot, scope);
627
+ if (!scopedDir) return json(res, 400, { error: "invalid scope" });
628
+ await fs.mkdir(scopedDir, { recursive: true });
629
+ const chunks = [];
630
+ let total = 0;
631
+ let oversized = false;
632
+ await new Promise((resolve, reject) => {
633
+ req.on("data", (c) => {
634
+ total += c.length;
635
+ if (total > 26214400) {
636
+ oversized = true;
637
+ req.destroy();
638
+ return;
639
+ }
640
+ chunks.push(c);
641
+ });
642
+ req.on("end", () => resolve());
643
+ req.on("error", reject);
644
+ });
645
+ if (oversized) return json(res, 413, { error: "file too large" });
646
+ await fs.writeFile(file, Buffer.concat(chunks));
647
+ const stat = await fs.stat(file);
648
+ return json(res, 200, {
649
+ ok: true,
650
+ name: filename,
651
+ size: stat.size,
652
+ createdAt: assetCreatedAt(stat.birthtimeMs, stat.mtimeMs),
653
+ mtime: stat.mtimeMs,
654
+ mime: mimeForFilename(filename),
655
+ url: `/__assets/${encodeURIComponent(scope)}/${encodeURIComponent(filename)}`,
656
+ importPath: assetImportPath(scope, filename)
657
+ });
658
+ }
659
+ if (method === "PATCH") {
660
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
661
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
662
+ const target = validateAssetName((await readBody(req)).name);
663
+ if (!target) return json(res, 400, { error: "invalid name" });
664
+ if (target === filename) return json(res, 200, {
665
+ ok: true,
666
+ name: filename
667
+ });
668
+ const dest = resolveScopedAssetFile(ctx.docsRoot, ctx.globalAssetsRoot, scope, target);
669
+ if (!dest) return json(res, 400, { error: "invalid name" });
670
+ try {
671
+ await fs.access(dest);
672
+ return json(res, 409, { error: "target exists" });
673
+ } catch {}
674
+ try {
675
+ await fs.rename(file, dest);
676
+ } catch (err) {
677
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
678
+ throw err;
679
+ }
680
+ return json(res, 200, {
681
+ ok: true,
682
+ name: target
683
+ });
684
+ }
685
+ if (method === "DELETE") {
686
+ const requestCheck = validateMutationRequest(req);
687
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
688
+ try {
689
+ await fs.unlink(file);
690
+ } catch (err) {
691
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
692
+ throw err;
693
+ }
694
+ return json(res, 200, { ok: true });
695
+ }
696
+ }
697
+ return next();
698
+ } catch (err) {
699
+ json(res, 500, { error: String(err.message ?? err) });
700
+ }
701
+ });
702
+ }
703
+ //#endregion
704
+ //#region src/editing/doc-ops.ts
705
+ function parseSource$2(source) {
706
+ try {
707
+ return parse(source, {
708
+ sourceType: "module",
709
+ plugins: ["typescript", "jsx"],
710
+ errorRecovery: true
711
+ });
712
+ } catch {
713
+ return null;
714
+ }
715
+ }
716
+ function programBody$1(ast) {
717
+ return ast.program?.body ?? [];
718
+ }
719
+ function metaObjectOf(node) {
720
+ if (node.type !== "ExportNamedDeclaration") return null;
721
+ const decl = node.declaration;
722
+ if (decl?.type !== "VariableDeclaration") return null;
723
+ const declarations = decl.declarations ?? [];
724
+ for (const d of declarations) {
725
+ const id = d.id;
726
+ if (id?.type !== "Identifier" || id.name !== "meta") continue;
727
+ let init = d.init;
728
+ if (!init) return null;
729
+ if (init.type === "TSSatisfiesExpression" || init.type === "TSAsExpression") {
730
+ const expr = init.expression;
731
+ if (expr) init = expr;
732
+ }
733
+ return init.type === "ObjectExpression" ? init : null;
734
+ }
735
+ return null;
736
+ }
737
+ function findMetaObject(ast) {
738
+ for (const node of programBody$1(ast)) {
739
+ const object = metaObjectOf(node);
740
+ if (object) return object;
741
+ }
742
+ return null;
743
+ }
744
+ function jsString$1(value) {
745
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, " ")}'`;
746
+ }
747
+ function validateDocTitle(v) {
748
+ if (typeof v !== "string") return null;
749
+ const trimmed = v.trim();
750
+ if (trimmed.length < 1 || trimmed.length > 120) return null;
751
+ return trimmed;
752
+ }
753
+ /**
754
+ * Rewrites `meta.title` in place, adding the property when the meta object
755
+ * exists without one. Returns null when there is no meta object to edit —
756
+ * the caller reports that rather than inventing an export.
757
+ */
758
+ function setMetaTitle(source, title) {
759
+ const ast = parseSource$2(source);
760
+ if (!ast) return null;
761
+ const object = findMetaObject(ast);
762
+ if (!object) return null;
763
+ const properties = object.properties ?? [];
764
+ for (const prop of properties) {
765
+ if (prop.type !== "ObjectProperty") continue;
766
+ const p = prop;
767
+ if (p.computed) continue;
768
+ if ((p.key.type === "Identifier" ? p.key.name : p.key.value) !== "title") continue;
769
+ return source.slice(0, p.value.start) + jsString$1(title) + source.slice(p.value.end);
770
+ }
771
+ const open = object.start + 1;
772
+ const inserted = source.slice(open, object.end - 1).trim() === "" ? `\n title: ${jsString$1(title)},\n` : `\n title: ${jsString$1(title)},`;
773
+ return source.slice(0, open) + inserted + source.slice(open);
774
+ }
775
+ function readMetaTitle(source) {
776
+ const ast = parseSource$2(source);
777
+ if (!ast) return null;
778
+ const object = findMetaObject(ast);
779
+ if (!object) return null;
780
+ const properties = object.properties ?? [];
781
+ for (const prop of properties) {
782
+ if (prop.type !== "ObjectProperty") continue;
783
+ const p = prop;
784
+ if (p.computed) continue;
785
+ if ((p.key.type === "Identifier" ? p.key.name : p.key.value) !== "title") continue;
786
+ if (p.value.type !== "StringLiteral") return null;
787
+ return typeof p.value.value === "string" ? p.value.value : null;
788
+ }
789
+ return null;
790
+ }
791
+ /** `q3-review` → `q3-review-copy`, `q3-review-copy` → `q3-review-copy-2`, … */
792
+ function nextCopyId(baseId, taken) {
793
+ const stem = baseId.replace(/-copy(-\d+)?$/, "");
794
+ let candidate = `${stem}-copy`;
795
+ let n = 2;
796
+ while (taken.has(candidate)) {
797
+ candidate = `${stem}-copy-${n}`;
798
+ n++;
799
+ }
800
+ return candidate;
801
+ }
802
+ function copyTitle(title) {
803
+ return /\(copy( \d+)?\)$/.test(title) ? title : `${title} (copy)`;
804
+ }
805
+ //#endregion
806
+ //#region src/files/folders.ts
807
+ const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
808
+ const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
809
+ function emptyManifest() {
810
+ return {
811
+ folders: [],
812
+ assignments: {}
813
+ };
814
+ }
815
+ async function readManifest(file) {
816
+ try {
817
+ const raw = await fs.readFile(file, "utf8");
818
+ const parsed = JSON.parse(raw);
819
+ return {
820
+ folders: Array.isArray(parsed.folders) ? parsed.folders : [],
821
+ assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
822
+ };
823
+ } catch (err) {
824
+ if (err.code === "ENOENT") return emptyManifest();
825
+ throw err;
826
+ }
827
+ }
828
+ async function writeManifest(file, manifest) {
829
+ await fs.mkdir(path.dirname(file), { recursive: true });
830
+ await fs.writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
831
+ }
832
+ function newFolderId() {
833
+ return `f-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
834
+ }
835
+ function validateName(v) {
836
+ if (typeof v !== "string") return null;
837
+ const trimmed = v.trim();
838
+ if (trimmed.length < 1 || trimmed.length > 40) return null;
839
+ return trimmed;
840
+ }
841
+ function validateReorder(v, current) {
842
+ if (!Array.isArray(v) || v.length !== current.length) return null;
843
+ const known = new Set(current.map((f) => f.id));
844
+ const seen = /* @__PURE__ */ new Set();
845
+ const out = [];
846
+ for (const id of v) {
847
+ if (typeof id !== "string" || !FOLDER_ID_RE.test(id)) return null;
848
+ if (!known.has(id) || seen.has(id)) return null;
849
+ seen.add(id);
850
+ out.push(id);
851
+ }
852
+ return out;
853
+ }
854
+ function validateIcon(v) {
855
+ if (!v || typeof v !== "object") return null;
856
+ const icon = v;
857
+ if (icon.type === "emoji") {
858
+ if (typeof icon.value !== "string") return null;
859
+ if (icon.value.length < 1 || icon.value.length > 8) return null;
860
+ return {
861
+ type: "emoji",
862
+ value: icon.value
863
+ };
864
+ }
865
+ if (icon.type === "color") {
866
+ if (typeof icon.value !== "string" || !COLOR_RE.test(icon.value)) return null;
867
+ return {
868
+ type: "color",
869
+ value: icon.value
870
+ };
871
+ }
872
+ return null;
873
+ }
874
+ //#endregion
875
+ //#region src/ops/documents.ts
876
+ /**
877
+ * Operations shared by the dev API and the CLI. Everything here takes an
878
+ * `ApiContext` and touches disk directly — no HTTP, so a command and a browser
879
+ * calling `/__docs` end up in exactly the same code.
880
+ */
881
+ var OpsError = class extends Error {
882
+ status;
883
+ constructor(status, message) {
884
+ super(message);
885
+ this.status = status;
886
+ this.name = "OpsError";
887
+ }
888
+ };
889
+ const ENTRY_NAMES = [
890
+ "index.tsx",
891
+ "index.jsx",
892
+ "index.ts",
893
+ "index.js"
894
+ ];
895
+ function docDir(ctx, docId) {
896
+ if (!DOC_ID_RE.test(docId)) throw new OpsError(400, `invalid document id: ${docId}`);
897
+ const dir = path.resolve(ctx.docsRoot, docId);
898
+ if (!dir.startsWith(ctx.docsRoot + path.sep)) throw new OpsError(400, `invalid document id: ${docId}`);
899
+ return dir;
900
+ }
901
+ function resolveEntry(ctx, docId) {
902
+ const dir = docDir(ctx, docId);
903
+ for (const name of ENTRY_NAMES) {
904
+ const file = path.join(dir, name);
905
+ if (existsSync(file)) return file;
906
+ }
907
+ return null;
908
+ }
909
+ async function listDocIds(ctx) {
910
+ try {
911
+ return (await fs.readdir(ctx.docsRoot, { withFileTypes: true })).filter((e) => e.isDirectory() && DOC_ID_RE.test(e.name)).map((e) => e.name).sort();
912
+ } catch {
913
+ return [];
914
+ }
915
+ }
916
+ async function createDocument(ctx, docId, source) {
917
+ const dir = docDir(ctx, docId);
918
+ if (existsSync(dir)) throw new OpsError(409, `document already exists: ${docId}`);
919
+ await fs.mkdir(dir, { recursive: true });
920
+ const entry = path.join(dir, "index.tsx");
921
+ await fs.writeFile(entry, source, "utf8");
922
+ return {
923
+ id: docId,
924
+ entry: path.relative(ctx.userCwd, entry)
925
+ };
926
+ }
927
+ async function renameDocument(ctx, docId, rawTitle) {
928
+ const title = validateDocTitle(rawTitle);
929
+ if (!title) throw new OpsError(400, "invalid title");
930
+ const entry = resolveEntry(ctx, docId);
931
+ if (!entry) throw new OpsError(404, `document not found: ${docId}`);
932
+ const source = await fs.readFile(entry, "utf8");
933
+ const next = setMetaTitle(source, title);
934
+ if (next === null) throw new OpsError(422, "document has no `export const meta` object to rename");
935
+ if (next !== source) await fs.writeFile(entry, next, "utf8");
936
+ return {
937
+ id: docId,
938
+ title
939
+ };
940
+ }
941
+ async function duplicateDocument(ctx, docId, newId) {
942
+ const dir = docDir(ctx, docId);
943
+ if (!existsSync(dir)) throw new OpsError(404, `document not found: ${docId}`);
944
+ const taken = new Set(await listDocIds(ctx));
945
+ let targetId;
946
+ if (newId) {
947
+ if (!DOC_ID_RE.test(newId)) throw new OpsError(400, `invalid newId: ${newId}`);
948
+ if (taken.has(newId)) throw new OpsError(409, `document already exists: ${newId}`);
949
+ targetId = newId;
950
+ } else targetId = nextCopyId(docId, taken);
951
+ await fs.cp(dir, docDir(ctx, targetId), { recursive: true });
952
+ const entry = resolveEntry(ctx, targetId);
953
+ if (entry) {
954
+ const source = await fs.readFile(entry, "utf8");
955
+ const renamed = setMetaTitle(source, copyTitle(readMetaTitle(source) ?? docId));
956
+ if (renamed && renamed !== source) await fs.writeFile(entry, renamed, "utf8");
957
+ }
958
+ const manifest = await readManifest(ctx.manifestPath);
959
+ const folderId = manifest.assignments[docId];
960
+ if (folderId) {
961
+ manifest.assignments[targetId] = folderId;
962
+ await writeManifest(ctx.manifestPath, manifest);
963
+ }
964
+ return { id: targetId };
965
+ }
966
+ async function deleteDocument(ctx, docId) {
967
+ const dir = docDir(ctx, docId);
968
+ if (!existsSync(dir)) throw new OpsError(404, `document not found: ${docId}`);
969
+ await fs.rm(dir, {
970
+ recursive: true,
971
+ force: true
972
+ });
973
+ const manifest = await readManifest(ctx.manifestPath);
974
+ if (manifest.assignments[docId]) {
975
+ delete manifest.assignments[docId];
976
+ await writeManifest(ctx.manifestPath, manifest);
977
+ }
978
+ }
979
+ //#endregion
980
+ //#region src/vite/routes/docs.ts
981
+ function registerDocRoutes(server, ctx) {
982
+ server.middlewares.use("/__docs", async (req, res, next) => {
983
+ const url = new URL(req.url ?? "/", "http://local");
984
+ const method = req.method ?? "GET";
985
+ try {
986
+ const duplicateMatch = url.pathname.match(/^\/([^/]+)\/duplicate$/);
987
+ const idMatch = url.pathname.match(/^\/([^/]+)$/);
988
+ if (duplicateMatch && method === "POST") {
989
+ const requestCheck = validateMutationRequest(req);
990
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
991
+ const docId = decodeURIComponent(duplicateMatch[1]);
992
+ const body = await readBody(req).catch(() => ({}));
993
+ return json(res, 200, {
994
+ ok: true,
995
+ docId: (await duplicateDocument(ctx, docId, typeof body.newId === "string" && body.newId !== "" ? body.newId : void 0)).id
996
+ });
997
+ }
998
+ if (idMatch) {
999
+ const docId = decodeURIComponent(idMatch[1]);
1000
+ if (method === "PATCH") {
1001
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1002
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1003
+ return json(res, 200, {
1004
+ ok: true,
1005
+ title: (await renameDocument(ctx, docId, (await readBody(req)).title)).title
1006
+ });
1007
+ }
1008
+ if (method === "DELETE") {
1009
+ const requestCheck = validateMutationRequest(req);
1010
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1011
+ await deleteDocument(ctx, docId);
1012
+ return json(res, 200, { ok: true });
1013
+ }
1014
+ }
1015
+ next();
1016
+ } catch (err) {
1017
+ if (err instanceof OpsError) return json(res, err.status, { error: err.message });
1018
+ json(res, 500, { error: String(err.message ?? err) });
1019
+ }
1020
+ });
1021
+ }
1022
+ //#endregion
1023
+ //#region src/editing/babel-walk.ts
1024
+ function parseSource$1(code) {
1025
+ try {
1026
+ return parse(code, {
1027
+ sourceType: "module",
1028
+ plugins: ["typescript", "jsx"],
1029
+ errorRecovery: true
1030
+ });
1031
+ } catch {
1032
+ return null;
1033
+ }
1034
+ }
1035
+ function isNode(value) {
1036
+ return typeof value === "object" && value !== null && typeof value.type === "string";
1037
+ }
1038
+ /** Depth-first walk over every node; the visitor decides what it cares about. */
1039
+ function walkAst(node, visit) {
1040
+ if (Array.isArray(node)) {
1041
+ for (const child of node) walkAst(child, visit);
1042
+ return;
1043
+ }
1044
+ if (!isNode(node)) return;
1045
+ visit(node);
1046
+ for (const key of Object.keys(node)) {
1047
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments") continue;
1048
+ walkAst(node[key], visit);
1049
+ }
1050
+ }
1051
+ function walkJsx(ast, visit) {
1052
+ walkAst(ast, (node) => {
1053
+ if (node.type === "JSXElement") visit(node);
1054
+ });
1055
+ }
1056
+ /**
1057
+ * The JSX element whose opening tag starts at this source position.
1058
+ *
1059
+ * The match is exact on both line and column. Anything looser can resolve to a
1060
+ * different element on the same line, and this location decides where an edit
1061
+ * gets written — a near miss silently rewrites the wrong text.
1062
+ */
1063
+ function findJsxAt(ast, line, column) {
1064
+ let found = null;
1065
+ walkJsx(ast, (node) => {
1066
+ if (found) return;
1067
+ const start = node.loc?.start;
1068
+ if (start && start.line === line && start.column === column) found = node;
1069
+ });
1070
+ return found;
1071
+ }
1072
+ /**
1073
+ * Every JSX element that starts on a line, nearest column first.
1074
+ *
1075
+ * Columns cannot be trusted for locations derived from React's `_debugSource`:
1076
+ * injecting the loc attribute shifts the columns of anything later on the same
1077
+ * line, so the offset differs per element. The line survives that, so callers
1078
+ * scan the line and pick by content instead.
1079
+ */
1080
+ function findJsxOnLine(ast, line, column) {
1081
+ const hits = [];
1082
+ walkJsx(ast, (node) => {
1083
+ const start = node.loc?.start;
1084
+ if (!start || start.line !== line) return;
1085
+ hits.push({
1086
+ node,
1087
+ distance: Math.abs(start.column - column)
1088
+ });
1089
+ });
1090
+ return hits.sort((a, b) => a.distance - b.distance).map((hit) => hit.node);
1091
+ }
1092
+ //#endregion
1093
+ //#region src/editing/comments.ts
1094
+ const MARKER_RE = /\{\/\*\s*@doc-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/;
1095
+ function b64urlEncode(value) {
1096
+ return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1097
+ }
1098
+ function b64urlDecode(value) {
1099
+ const pad = value.length % 4 === 0 ? "" : "=".repeat(4 - value.length % 4);
1100
+ return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
1101
+ }
1102
+ function parseMarkers(source) {
1103
+ const comments = [];
1104
+ source.split("\n").forEach((line, index) => {
1105
+ const match = MARKER_RE.exec(line);
1106
+ if (!match) return;
1107
+ const [, id, ts, payload] = match;
1108
+ try {
1109
+ const decoded = JSON.parse(b64urlDecode(payload));
1110
+ comments.push({
1111
+ id,
1112
+ line: index + 1,
1113
+ ts,
1114
+ note: decoded.note,
1115
+ hint: decoded.hint
1116
+ });
1117
+ } catch {}
1118
+ });
1119
+ return comments;
1120
+ }
1121
+ function newCommentId() {
1122
+ return `c-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
1123
+ }
1124
+ function markerFor(id, ts, note, hint) {
1125
+ return `{/* @doc-comment id="${id}" ts="${ts}" text="${b64urlEncode(JSON.stringify(hint ? {
1126
+ note,
1127
+ hint
1128
+ } : { note }))}" */}`;
1129
+ }
1130
+ function markerPattern(id) {
1131
+ return `\\{\\/\\*\\s*@doc-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`;
1132
+ }
1133
+ /**
1134
+ * Removes a marker and the whitespace the insertion added — whichever side it
1135
+ * landed on — so deleting a comment restores the file byte for byte.
1136
+ */
1137
+ function removeMarker(source, id) {
1138
+ const pattern = markerPattern(id);
1139
+ for (const re of [
1140
+ new RegExp(`[ \\t]*${pattern}\\n`),
1141
+ new RegExp(`\\n[ \\t]*${pattern}`),
1142
+ new RegExp(pattern)
1143
+ ]) if (re.test(source)) return source.replace(re, "");
1144
+ return null;
1145
+ }
1146
+ function indentOfLine(source, offset) {
1147
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
1148
+ return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
1149
+ }
1150
+ /**
1151
+ * Splices the marker in as the **first child** of the target element. A
1152
+ * JSX-comment token outside a JSX child position parses as an empty object
1153
+ * literal and breaks the surrounding expression, so this never puts one before
1154
+ * an element.
1155
+ */
1156
+ function insertMarker(source, target, note, hint) {
1157
+ const ast = parseSource$1(source);
1158
+ if (!ast) return null;
1159
+ const element = findJsxAt(ast, target.line, target.column);
1160
+ if (!element) return null;
1161
+ const opening = element.openingElement;
1162
+ if (!opening) return null;
1163
+ if (opening.selfClosing === true) return null;
1164
+ const offset = opening.end;
1165
+ const id = newCommentId();
1166
+ const marker = markerFor(id, (/* @__PURE__ */ new Date()).toISOString(), note, hint);
1167
+ const indent = indentOfLine(source, element.start ?? offset);
1168
+ return {
1169
+ source: `${source.slice(0, offset)}\n${indent} ${marker}${source.slice(offset)}`,
1170
+ id
1171
+ };
1172
+ }
1173
+ //#endregion
1174
+ //#region src/editing/edit-ops.ts
1175
+ function escapeJsxText(text) {
1176
+ return text.replace(/[{}<>]/g, (char) => `{'${char}'}`);
1177
+ }
1178
+ function normalizeText(value) {
1179
+ return value.replace(/\s+/g, " ").trim();
1180
+ }
1181
+ function isAstNode(value) {
1182
+ return typeof value === "object" && value !== null && typeof value.type === "string";
1183
+ }
1184
+ function jsxChildren(element) {
1185
+ return element.children ?? [];
1186
+ }
1187
+ function labelOf(node) {
1188
+ if (node.type === "JSXExpressionContainer") return "{…}";
1189
+ if (node.type === "JSXElement") return `<${(node.openingElement?.name)?.name ?? "element"}>`;
1190
+ return "…";
1191
+ }
1192
+ /** Text written straight into the JSX. Its surrounding whitespace is indentation, so the slot excludes it. */
1193
+ function literalSlot(node) {
1194
+ const raw = node.value;
1195
+ const leading = (raw.match(/^\s*/)?.[0] ?? "").length;
1196
+ const trailing = (raw.match(/\s*$/)?.[0] ?? "").length;
1197
+ return {
1198
+ value: raw.trim(),
1199
+ start: node.start + leading,
1200
+ end: node.end - trailing,
1201
+ escape: escapeJsxText
1202
+ };
1203
+ }
1204
+ /** `name="…"` at a call site. Only the quote in use has to be escaped. */
1205
+ function attributeSlot(node, source) {
1206
+ const quote = source[node.start] ?? "\"";
1207
+ const entity = quote === "\"" ? "&quot;" : "&apos;";
1208
+ return {
1209
+ value: node.value,
1210
+ start: node.start + 1,
1211
+ end: node.end - 1,
1212
+ escape: (text) => text.split(quote).join(entity)
1213
+ };
1214
+ }
1215
+ /** A plain string in an array or object literal — a JS string, not JSX. */
1216
+ function stringSlot(node, source) {
1217
+ const quote = source[node.start] ?? "'";
1218
+ return {
1219
+ value: node.value,
1220
+ start: node.start + 1,
1221
+ end: node.end - 1,
1222
+ escape: (text) => text.split("\\").join("\\\\").split(quote).join(`\\${quote}`)
1223
+ };
1224
+ }
1225
+ /**
1226
+ * A code block is written `<Code>{`docs/…`}</Code>` — the words are a template
1227
+ * literal, not JSX text. Nothing has to be traced to reach them, so they are a
1228
+ * slot wherever they appear: as a child of the element, or as the children one
1229
+ * call site handed a helper.
1230
+ */
1231
+ function expressionSlot(child, source) {
1232
+ if (child.type !== "JSXExpressionContainer") return null;
1233
+ const expression = child.expression;
1234
+ if (expression?.type === "StringLiteral") return stringSlot(expression, source);
1235
+ if (expression?.type !== "TemplateLiteral") return null;
1236
+ if ((expression.expressions ?? []).length > 0) return null;
1237
+ const quasis = expression.quasis ?? [];
1238
+ const raw = (quasis[0]?.value)?.raw ?? "";
1239
+ if (quasis.length !== 1) return null;
1240
+ return {
1241
+ value: raw,
1242
+ start: expression.start + 1,
1243
+ end: expression.end - 1,
1244
+ escape: (text) => text.split("\\").join("\\\\").split("`").join("\\`").split("${").join("\\${")
1245
+ };
1246
+ }
1247
+ function identifierName(child) {
1248
+ if (child.type !== "JSXExpressionContainer") return null;
1249
+ const expression = child.expression;
1250
+ if (expression?.type !== "Identifier") return null;
1251
+ return expression.name ?? null;
1252
+ }
1253
+ /** The chain of nodes from the program down to this element — its scopes, in order. */
1254
+ function pathTo(ast, element) {
1255
+ let found = null;
1256
+ const visit = (node, trail) => {
1257
+ if (found) return;
1258
+ const here = [...trail, node];
1259
+ if (node === element) {
1260
+ found = here;
1261
+ return;
1262
+ }
1263
+ for (const key of Object.keys(node)) {
1264
+ if (key === "loc") continue;
1265
+ const value = node[key];
1266
+ if (Array.isArray(value)) {
1267
+ for (const child of value) if (isAstNode(child)) visit(child, here);
1268
+ } else if (isAstNode(value)) visit(value, here);
1269
+ }
1270
+ };
1271
+ visit(ast, []);
1272
+ return found;
1273
+ }
1274
+ function propNames(declarator) {
1275
+ const pattern = (declarator.init?.params)?.[0];
1276
+ if (pattern?.type !== "ObjectPattern") return [];
1277
+ return (pattern.properties ?? []).map((property) => property.key?.name ?? "").filter((name) => name !== "");
1278
+ }
1279
+ /** The component this element belongs to, and the props it declares. */
1280
+ function componentScope(path) {
1281
+ for (let index = path.length - 1; index >= 0; index--) {
1282
+ const node = path[index];
1283
+ if (node?.type !== "VariableDeclarator") continue;
1284
+ const name = node.id?.name;
1285
+ const props = propNames(node);
1286
+ if (name !== void 0 && props.length > 0) return {
1287
+ name,
1288
+ props
1289
+ };
1290
+ }
1291
+ return null;
1292
+ }
1293
+ /** The `xs.map(entry => …)` this element is rendered inside, if any. */
1294
+ function mapScope(path) {
1295
+ for (let index = path.length - 1; index >= 1; index--) {
1296
+ const arrow = path[index];
1297
+ const call = path[index - 1];
1298
+ if (arrow?.type !== "ArrowFunctionExpression" || call?.type !== "CallExpression") continue;
1299
+ const callee = call.callee;
1300
+ if (callee?.type !== "MemberExpression") continue;
1301
+ if (callee.property?.name !== "map") continue;
1302
+ const object = callee.object;
1303
+ const pattern = (arrow.params ?? [])[0];
1304
+ if (object?.type !== "Identifier" || !pattern) continue;
1305
+ return {
1306
+ pattern,
1307
+ array: object.name
1308
+ };
1309
+ }
1310
+ return null;
1311
+ }
1312
+ function patternNames(pattern) {
1313
+ if (pattern.type === "Identifier") return [pattern.name];
1314
+ if (pattern.type === "ArrayPattern") return (pattern.elements ?? []).filter((element) => element?.type === "Identifier").map((element) => element.name);
1315
+ if (pattern.type === "ObjectPattern") return (pattern.properties ?? []).map((property) => property.value?.name ?? "").filter((name) => name !== "");
1316
+ return [];
1317
+ }
1318
+ /** Destructure one array entry the way the map callback does. */
1319
+ function entryBindings(pattern, entry, source) {
1320
+ if (pattern.type === "Identifier") {
1321
+ if (entry.type !== "StringLiteral") return null;
1322
+ return /* @__PURE__ */ new Map([[pattern.name, stringSlot(entry, source)]]);
1323
+ }
1324
+ const bindings = /* @__PURE__ */ new Map();
1325
+ if (pattern.type === "ArrayPattern") {
1326
+ if (entry.type !== "ArrayExpression") return null;
1327
+ const values = entry.elements ?? [];
1328
+ const targets = pattern.elements ?? [];
1329
+ for (let index = 0; index < targets.length; index++) {
1330
+ const target = targets[index];
1331
+ if (!target) continue;
1332
+ const value = values[index];
1333
+ if (target.type !== "Identifier" || value?.type !== "StringLiteral") return null;
1334
+ bindings.set(target.name, stringSlot(value, source));
1335
+ }
1336
+ return bindings.size > 0 ? bindings : null;
1337
+ }
1338
+ if (pattern.type === "ObjectPattern") {
1339
+ if (entry.type !== "ObjectExpression") return null;
1340
+ for (const property of pattern.properties ?? []) {
1341
+ const key = property.key?.name ?? "";
1342
+ const local = property.value?.name ?? "";
1343
+ if (key === "" || local === "") return null;
1344
+ const value = (entry.properties ?? []).find((candidate) => candidate.key?.name === key || candidate.key?.value === key)?.value;
1345
+ if (value?.type !== "StringLiteral") return null;
1346
+ bindings.set(local, stringSlot(value, source));
1347
+ }
1348
+ return bindings.size > 0 ? bindings : null;
1349
+ }
1350
+ return null;
1351
+ }
1352
+ /**
1353
+ * Two call sites of the same component render different words, and only what
1354
+ * is on screen can say which one was clicked — without it a save rewrites
1355
+ * whichever came first in the file. An entry that cannot be told apart from
1356
+ * its neighbours is therefore not editable at all.
1357
+ */
1358
+ function fits(bindings, shown) {
1359
+ if (shown === void 0) return true;
1360
+ const visible = normalizeText(shown);
1361
+ const values = [...bindings.values()].map((slot) => normalizeText(slot.value));
1362
+ if (values.join("") === "") return false;
1363
+ return values.every((value) => visible.includes(value));
1364
+ }
1365
+ function tagName(node) {
1366
+ return (node.openingElement?.name)?.name;
1367
+ }
1368
+ /**
1369
+ * `<Section name="主旨">…</Section>` puts its words between the tags rather
1370
+ * than in an attribute. Only a lone run of text qualifies: anything nested
1371
+ * would be flattened into a string and lost on the first save.
1372
+ */
1373
+ function childrenSlot(node, source) {
1374
+ const children = jsxChildren(node).filter((child) => child.type !== "JSXText" || child.value.trim() !== "");
1375
+ const only = children[0];
1376
+ if (children.length !== 1 || !only) return null;
1377
+ if (only.type === "JSXText") return literalSlot(only);
1378
+ return expressionSlot(only, source);
1379
+ }
1380
+ function callSiteBindings(node, wanted, source) {
1381
+ const attributes = node.openingElement?.attributes ?? [];
1382
+ const bindings = /* @__PURE__ */ new Map();
1383
+ for (const name of wanted) {
1384
+ if (name === "children") {
1385
+ const slot = childrenSlot(node, source);
1386
+ if (!slot) return null;
1387
+ bindings.set(name, slot);
1388
+ continue;
1389
+ }
1390
+ const value = attributes.find((candidate) => candidate.name?.name === name)?.value;
1391
+ if (value?.type !== "StringLiteral") return null;
1392
+ bindings.set(name, attributeSlot(value, source));
1393
+ }
1394
+ return bindings;
1395
+ }
1396
+ function propBindings(ctx, component, wanted) {
1397
+ const matches = [];
1398
+ walkJsx(ctx.ast, (node) => {
1399
+ if (tagName(node) !== component) return;
1400
+ const bindings = callSiteBindings(node, wanted, ctx.source);
1401
+ if (bindings && fits(bindings, ctx.shown)) matches.push(bindings);
1402
+ });
1403
+ return matches.length === 1 ? matches[0] ?? null : null;
1404
+ }
1405
+ /** Every array literal that could be the one being mapped over. */
1406
+ function arrayCandidates(ctx, name, component) {
1407
+ const found = [];
1408
+ walkAst(ctx.ast, (node) => {
1409
+ if (node.type === "VariableDeclarator") {
1410
+ const id = node.id;
1411
+ const init = node.init;
1412
+ if (id?.type === "Identifier" && id.name === name && init?.type === "ArrayExpression") found.push(init);
1413
+ return;
1414
+ }
1415
+ if (node.type !== "JSXElement" || component === null || tagName(node) !== component) return;
1416
+ const attributes = node.openingElement.attributes ?? [];
1417
+ for (const attribute of attributes) {
1418
+ if (attribute.name?.name !== name) continue;
1419
+ const value = attribute.value;
1420
+ if (value?.type !== "JSXExpressionContainer") continue;
1421
+ const expression = value.expression;
1422
+ if (expression?.type === "ArrayExpression") found.push(expression);
1423
+ }
1424
+ });
1425
+ return found;
1426
+ }
1427
+ function mapBindings(ctx, scope, component, wanted) {
1428
+ const matches = [];
1429
+ for (const array of arrayCandidates(ctx, scope.array, component)) for (const entry of array.elements ?? []) {
1430
+ if (!entry) continue;
1431
+ const bindings = entryBindings(scope.pattern, entry, ctx.source);
1432
+ if (!bindings || !wanted.every((name) => bindings.has(name))) continue;
1433
+ if (fits(bindings, ctx.shown)) matches.push(bindings);
1434
+ }
1435
+ return matches.length === 1 ? matches[0] ?? null : null;
1436
+ }
1437
+ function bindingsFor(ctx, element, names) {
1438
+ const path = pathTo(ctx.ast, element);
1439
+ if (!path) return null;
1440
+ const map = mapScope(path);
1441
+ const component = componentScope(path);
1442
+ const mapped = map ? patternNames(map.pattern) : [];
1443
+ const bindings = /* @__PURE__ */ new Map();
1444
+ const fromMap = names.filter((name) => mapped.includes(name));
1445
+ if (map && fromMap.length > 0) {
1446
+ const entry = mapBindings(ctx, map, component?.name ?? null, fromMap);
1447
+ if (entry) for (const name of fromMap) {
1448
+ const slot = entry.get(name);
1449
+ if (slot) bindings.set(name, slot);
1450
+ }
1451
+ }
1452
+ const fromProps = names.filter((name) => !mapped.includes(name) && (component?.props.includes(name) ?? false));
1453
+ if (component && fromProps.length > 0) {
1454
+ const call = propBindings(ctx, component.name, fromProps);
1455
+ if (call) for (const [name, slot] of call) bindings.set(name, slot);
1456
+ }
1457
+ return bindings.size > 0 ? bindings : null;
1458
+ }
1459
+ /** The element's children, each resolved to a writable slot or left as markup. */
1460
+ function resolve$1(element, ctx) {
1461
+ const children = jsxChildren(element);
1462
+ const names = children.map((child) => identifierName(child)).filter((name) => name !== null);
1463
+ const bindings = ctx && names.length > 0 ? bindingsFor(ctx, element, names) : null;
1464
+ const parts = [];
1465
+ const slots = [];
1466
+ const take = (slot) => {
1467
+ parts.push({
1468
+ kind: "text",
1469
+ index: slots.length,
1470
+ value: slot.value
1471
+ });
1472
+ slots.push(slot);
1473
+ };
1474
+ for (const child of children) {
1475
+ if (child.type === "JSXText") {
1476
+ if (child.value.trim() !== "") take(literalSlot(child));
1477
+ continue;
1478
+ }
1479
+ const name = identifierName(child);
1480
+ const slot = (name === null ? void 0 : bindings?.get(name)) ?? (ctx ? expressionSlot(child, ctx.source) ?? void 0 : void 0);
1481
+ if (slot) take(slot);
1482
+ else parts.push({
1483
+ kind: "markup",
1484
+ label: labelOf(child)
1485
+ });
1486
+ }
1487
+ return {
1488
+ parts,
1489
+ slots
1490
+ };
1491
+ }
1492
+ function describe(element, ctx) {
1493
+ const { parts } = resolve$1(element, ctx);
1494
+ const texts = parts.filter((part) => part.kind === "text");
1495
+ if (texts.length === 0) return {
1496
+ editable: false,
1497
+ text: "",
1498
+ parts,
1499
+ reason: parts.some((part) => part.kind === "markup" && part.label === "{…}") ? "text is produced by code — edit whatever feeds it" : "element has no text of its own"
1500
+ };
1501
+ return {
1502
+ editable: true,
1503
+ text: texts.map((part) => part.value).join(" "),
1504
+ parts
1505
+ };
1506
+ }
1507
+ /**
1508
+ * Picks the element an inspector click meant.
1509
+ *
1510
+ * Coordinates alone are not enough: fallback candidates come from React's
1511
+ * `_debugSource`, whose columns drift once the loc-tag transform has widened
1512
+ * the line. The rendered text the user is looking at is the tiebreaker — an
1513
+ * element only wins if its source text is part of what is on screen.
1514
+ */
1515
+ function resolveTextTarget(source, candidates, expected) {
1516
+ const ast = parseSource$1(source);
1517
+ if (!ast) return null;
1518
+ const ctx = {
1519
+ ast,
1520
+ source,
1521
+ shown: expected
1522
+ };
1523
+ const shown = expected ? normalizeText(expected) : null;
1524
+ const matches = (info) => {
1525
+ if (!shown) return true;
1526
+ const runs = info.parts.filter((part) => part.kind === "text");
1527
+ if (runs.length === 0) return false;
1528
+ return runs.every((part) => part.kind === "text" && shown.includes(normalizeText(part.value)));
1529
+ };
1530
+ for (const candidate of candidates) {
1531
+ const exact = findJsxAt(ast, candidate.line, candidate.column);
1532
+ if (exact) {
1533
+ const info = describe(exact, ctx);
1534
+ if (info.editable && matches(info)) return {
1535
+ ...info,
1536
+ ...candidate
1537
+ };
1538
+ }
1539
+ if (!shown) continue;
1540
+ for (const node of findJsxOnLine(ast, candidate.line, candidate.column)) {
1541
+ const info = describe(node, ctx);
1542
+ const start = node.loc?.start;
1543
+ if (!info.editable || !matches(info) || !start) continue;
1544
+ return {
1545
+ ...info,
1546
+ line: start.line,
1547
+ column: start.column
1548
+ };
1549
+ }
1550
+ }
1551
+ const first = candidates[0];
1552
+ if (!first) return null;
1553
+ const clicked = findJsxAt(ast, first.line, first.column);
1554
+ return clicked ? {
1555
+ ...describe(clicked, ctx),
1556
+ ...first
1557
+ } : null;
1558
+ }
1559
+ /**
1560
+ * Replaces one text run of an element, leaving its markup and every other run
1561
+ * untouched. `expected` is the text the caller believes is there; a mismatch
1562
+ * means the source moved under us and the write is refused.
1563
+ */
1564
+ function replaceTextAt(source, target, text, opts = {}) {
1565
+ const ast = parseSource$1(source);
1566
+ if (!ast) return {
1567
+ ok: false,
1568
+ status: 422,
1569
+ error: "could not parse document source"
1570
+ };
1571
+ const element = findJsxAt(ast, target.line, target.column);
1572
+ if (!element) return {
1573
+ ok: false,
1574
+ status: 404,
1575
+ error: "no element at that source location"
1576
+ };
1577
+ const { slots } = resolve$1(element, {
1578
+ ast,
1579
+ source,
1580
+ shown: opts.shown
1581
+ });
1582
+ if (slots.length === 0) return {
1583
+ ok: false,
1584
+ status: 422,
1585
+ error: "element has no text to replace"
1586
+ };
1587
+ const slot = slots[opts.index ?? 0];
1588
+ if (!slot) return {
1589
+ ok: false,
1590
+ status: 404,
1591
+ error: "no such text run in this element"
1592
+ };
1593
+ if (opts.expected !== void 0 && normalizeText(slot.value) !== normalizeText(opts.expected)) return {
1594
+ ok: false,
1595
+ status: 409,
1596
+ error: "source changed since this was opened — reselect it"
1597
+ };
1598
+ return {
1599
+ ok: true,
1600
+ source: source.slice(0, slot.start) + slot.escape(text) + source.slice(slot.end)
1601
+ };
1602
+ }
1603
+ //#endregion
1604
+ //#region src/vite/routes/edit.ts
1605
+ function readLoc(body) {
1606
+ const { docId, line, column } = body;
1607
+ if (typeof docId !== "string") return null;
1608
+ if (typeof line !== "number" || typeof column !== "number") return null;
1609
+ return {
1610
+ docId,
1611
+ line,
1612
+ column
1613
+ };
1614
+ }
1615
+ function registerEditRoutes(server, ctx) {
1616
+ const entryFor = (docId) => resolveDocEntry(ctx.docsRoot, docId);
1617
+ server.middlewares.use("/__edit", async (req, res, next) => {
1618
+ const url = new URL(req.url ?? "/", "http://local");
1619
+ const method = req.method ?? "GET";
1620
+ try {
1621
+ if (method === "GET" && url.pathname === "/text") {
1622
+ const docId = url.searchParams.get("docId") ?? "";
1623
+ const entry = entryFor(docId);
1624
+ const locs = (url.searchParams.get("locs") ?? "").split(",").map((pair) => pair.split(":").map(Number)).filter(([line, column]) => Number.isFinite(line) && Number.isFinite(column)).map(([line, column]) => ({
1625
+ line,
1626
+ column
1627
+ }));
1628
+ if (!entry || locs.length === 0) return json(res, 400, { error: "invalid target" });
1629
+ const resolved = resolveTextTarget(await fs.readFile(entry, "utf8"), locs, url.searchParams.get("shown") ?? void 0);
1630
+ if (!resolved) return json(res, 404, { error: "element not found" });
1631
+ return json(res, 200, resolved);
1632
+ }
1633
+ if (method === "PUT" && url.pathname === "/text") {
1634
+ const check = validateMutationRequest(req, { requireJsonBody: true });
1635
+ if (!check.ok) return json(res, check.status, { error: check.error });
1636
+ const body = await readBody(req);
1637
+ const loc = readLoc(body);
1638
+ const text = body.text;
1639
+ if (!loc || typeof text !== "string") return json(res, 400, { error: "invalid payload" });
1640
+ const entry = entryFor(loc.docId);
1641
+ if (!entry) return json(res, 404, { error: "document not found" });
1642
+ const source = await fs.readFile(entry, "utf8");
1643
+ const result = replaceTextAt(source, loc, text, {
1644
+ index: typeof body.index === "number" ? body.index : void 0,
1645
+ expected: typeof body.expected === "string" ? body.expected : void 0,
1646
+ shown: typeof body.shown === "string" ? body.shown : void 0
1647
+ });
1648
+ if (!result.ok) return json(res, result.status, { error: result.error });
1649
+ if (result.source !== source) await fs.writeFile(entry, result.source, "utf8");
1650
+ return json(res, 200, { ok: true });
1651
+ }
1652
+ if (method === "POST" && url.pathname === "/comment") {
1653
+ const check = validateMutationRequest(req, { requireJsonBody: true });
1654
+ if (!check.ok) return json(res, check.status, { error: check.error });
1655
+ const body = await readBody(req);
1656
+ const loc = readLoc(body);
1657
+ const note = body.note;
1658
+ if (!loc || typeof note !== "string" || note.trim() === "") return json(res, 400, { error: "invalid payload" });
1659
+ const entry = entryFor(loc.docId);
1660
+ if (!entry) return json(res, 404, { error: "document not found" });
1661
+ const inserted = insertMarker(await fs.readFile(entry, "utf8"), loc, note.trim(), typeof body.hint === "string" ? body.hint : void 0);
1662
+ if (!inserted) return json(res, 422, { error: "cannot anchor a comment here — pick the surrounding element" });
1663
+ await fs.writeFile(entry, inserted.source, "utf8");
1664
+ return json(res, 200, {
1665
+ ok: true,
1666
+ id: inserted.id
1667
+ });
1668
+ }
1669
+ return next();
1670
+ } catch (err) {
1671
+ json(res, 500, { error: String(err.message ?? err) });
1672
+ }
1673
+ });
1674
+ server.middlewares.use("/__comments", async (req, res, next) => {
1675
+ const url = new URL(req.url ?? "/", "http://local");
1676
+ const method = req.method ?? "GET";
1677
+ const docId = url.searchParams.get("docId") ?? "";
1678
+ const entry = entryFor(docId);
1679
+ try {
1680
+ if (method === "GET") {
1681
+ if (!entry) return json(res, 200, { comments: [] });
1682
+ return json(res, 200, { comments: parseMarkers(await fs.readFile(entry, "utf8")) });
1683
+ }
1684
+ if (method === "DELETE") {
1685
+ const check = validateMutationRequest(req);
1686
+ if (!check.ok) return json(res, check.status, { error: check.error });
1687
+ const id = url.searchParams.get("id") ?? "";
1688
+ if (!entry || !/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
1689
+ const next = removeMarker(await fs.readFile(entry, "utf8"), id);
1690
+ if (next === null) return json(res, 404, { error: "comment not found" });
1691
+ await fs.writeFile(entry, next, "utf8");
1692
+ return json(res, 200, { ok: true });
1693
+ }
1694
+ return next();
1695
+ } catch (err) {
1696
+ json(res, 500, { error: String(err.message ?? err) });
1697
+ }
1698
+ });
1699
+ }
1700
+ //#endregion
1701
+ //#region src/vite/routes/folders.ts
1702
+ function registerFolderRoutes(server, ctx) {
1703
+ server.middlewares.use("/__folders", async (req, res, next) => {
1704
+ const url = new URL(req.url ?? "/", "http://local");
1705
+ const method = req.method ?? "GET";
1706
+ try {
1707
+ if (method === "GET" && url.pathname === "/") return json(res, 200, await readManifest(ctx.manifestPath));
1708
+ if (method === "POST" && url.pathname === "/") {
1709
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1710
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1711
+ const body = await readBody(req);
1712
+ const name = validateName(body.name);
1713
+ if (!name) return json(res, 400, { error: "invalid name" });
1714
+ const icon = validateIcon(body.icon);
1715
+ if (!icon) return json(res, 400, { error: "invalid icon" });
1716
+ const manifest = await readManifest(ctx.manifestPath);
1717
+ const folder = {
1718
+ id: newFolderId(),
1719
+ name,
1720
+ icon
1721
+ };
1722
+ manifest.folders.push(folder);
1723
+ await writeManifest(ctx.manifestPath, manifest);
1724
+ return json(res, 200, folder);
1725
+ }
1726
+ if (method === "PUT" && url.pathname === "/assign") {
1727
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1728
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1729
+ const body = await readBody(req);
1730
+ if (typeof body.docId !== "string" || !DOC_ID_RE.test(body.docId)) return json(res, 400, { error: "invalid docId" });
1731
+ const docId = body.docId;
1732
+ let folderId;
1733
+ if (body.folderId === null) folderId = null;
1734
+ else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
1735
+ else return json(res, 400, { error: "invalid folderId" });
1736
+ const manifest = await readManifest(ctx.manifestPath);
1737
+ if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
1738
+ if (folderId === null) delete manifest.assignments[docId];
1739
+ else manifest.assignments[docId] = folderId;
1740
+ await writeManifest(ctx.manifestPath, manifest);
1741
+ return json(res, 200, { ok: true });
1742
+ }
1743
+ if (method === "PUT" && url.pathname === "/reorder") {
1744
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1745
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1746
+ const body = await readBody(req);
1747
+ const manifest = await readManifest(ctx.manifestPath);
1748
+ const ids = validateReorder(body.ids, manifest.folders);
1749
+ if (!ids) return json(res, 400, { error: "invalid ids" });
1750
+ const byId = new Map(manifest.folders.map((f) => [f.id, f]));
1751
+ manifest.folders = ids.map((id) => byId.get(id));
1752
+ await writeManifest(ctx.manifestPath, manifest);
1753
+ return json(res, 200, { ok: true });
1754
+ }
1755
+ const idMatch = url.pathname.match(/^\/([^/]+)$/);
1756
+ if (idMatch) {
1757
+ const id = idMatch[1];
1758
+ if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
1759
+ if (method === "PATCH") {
1760
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1761
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1762
+ const body = await readBody(req);
1763
+ const manifest = await readManifest(ctx.manifestPath);
1764
+ const folder = manifest.folders.find((f) => f.id === id);
1765
+ if (!folder) return json(res, 404, { error: "folder not found" });
1766
+ if (body.name !== void 0) {
1767
+ const name = validateName(body.name);
1768
+ if (!name) return json(res, 400, { error: "invalid name" });
1769
+ folder.name = name;
1770
+ }
1771
+ if (body.icon !== void 0) {
1772
+ const icon = validateIcon(body.icon);
1773
+ if (!icon) return json(res, 400, { error: "invalid icon" });
1774
+ folder.icon = icon;
1775
+ }
1776
+ await writeManifest(ctx.manifestPath, manifest);
1777
+ return json(res, 200, folder);
1778
+ }
1779
+ if (method === "DELETE") {
1780
+ const requestCheck = validateMutationRequest(req);
1781
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1782
+ const manifest = await readManifest(ctx.manifestPath);
1783
+ const before = manifest.folders.length;
1784
+ manifest.folders = manifest.folders.filter((f) => f.id !== id);
1785
+ if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
1786
+ for (const [docId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[docId];
1787
+ await writeManifest(ctx.manifestPath, manifest);
1788
+ return json(res, 200, { ok: true });
1789
+ }
1790
+ }
1791
+ next();
1792
+ } catch (err) {
1793
+ json(res, 500, { error: String(err.message ?? err) });
1794
+ }
1795
+ });
1796
+ }
1797
+ //#endregion
1798
+ //#region src/vite/api-plugin.ts
1799
+ function apiPlugin(opts) {
1800
+ return {
1801
+ name: "mosage:api",
1802
+ apply: "serve",
1803
+ configureServer(server) {
1804
+ const ctx = makeContext(opts);
1805
+ registerAssetRoutes(server, ctx);
1806
+ registerFolderRoutes(server, ctx);
1807
+ registerDocRoutes(server, ctx);
1808
+ registerEditRoutes(server, ctx);
1809
+ }
1810
+ };
1811
+ }
1812
+ //#endregion
1813
+ //#region src/vite/current-plugin.ts
1814
+ const TEXT_SNIPPET_MAX = 120;
1815
+ function parseSelection(raw) {
1816
+ if (raw == null || typeof raw !== "object") return null;
1817
+ const sel = raw;
1818
+ if (typeof sel.line !== "number" || !Number.isFinite(sel.line)) return null;
1819
+ if (typeof sel.column !== "number" || !Number.isFinite(sel.column)) return null;
1820
+ const tagName = typeof sel.tagName === "string" ? sel.tagName.toLowerCase().slice(0, 32) : "unknown";
1821
+ const text = typeof sel.text === "string" ? sel.text.replace(/\s+/g, " ").trim().slice(0, TEXT_SNIPPET_MAX) : "";
1822
+ return {
1823
+ line: Math.max(1, Math.floor(sel.line)),
1824
+ column: Math.max(0, Math.floor(sel.column)),
1825
+ tagName,
1826
+ text
1827
+ };
1828
+ }
1829
+ /**
1830
+ * Writes `node_modules/.mosage/current.json` whenever the viewer navigates or
1831
+ * the inspector picks an element, so an agent can resolve "this page" without
1832
+ * asking. Dev only — a static build has no cursor to report.
1833
+ */
1834
+ function currentPlugin(opts) {
1835
+ const userCwd = opts.userCwd;
1836
+ const docsDir = opts.docsDir ?? "docs";
1837
+ const outDir = path.join(userCwd, "node_modules", ".mosage");
1838
+ const outFile = path.join(outDir, "current.json");
1839
+ const tmpFile = `${outFile}.tmp`;
1840
+ let cached = null;
1841
+ return {
1842
+ name: "mosage:current",
1843
+ apply: "serve",
1844
+ configureServer(server) {
1845
+ server.ws.on("mosage:current", async (raw) => {
1846
+ const next = cached ? { ...cached } : {
1847
+ docId: "",
1848
+ pageIndex: 0,
1849
+ pageNumber: 1,
1850
+ totalPages: 1,
1851
+ docTitle: "",
1852
+ pagePath: "",
1853
+ selection: null
1854
+ };
1855
+ if (typeof raw?.docId === "string") {
1856
+ if (!DOC_ID_RE.test(raw.docId)) return;
1857
+ const totalPages = typeof raw.totalPages === "number" && Number.isFinite(raw.totalPages) && raw.totalPages > 0 ? Math.floor(raw.totalPages) : 1;
1858
+ const rawIndex = typeof raw.pageIndex === "number" && Number.isFinite(raw.pageIndex) ? Math.floor(raw.pageIndex) : 0;
1859
+ const pageIndex = Math.max(0, Math.min(totalPages - 1, rawIndex));
1860
+ const docTitle = typeof raw.docTitle === "string" ? raw.docTitle : raw.docId;
1861
+ const pagePath = path.join(docsDir, raw.docId, "index.tsx").split(path.sep).join("/");
1862
+ if (cached?.docId !== raw.docId || cached?.pageIndex !== pageIndex) next.selection = null;
1863
+ next.docId = raw.docId;
1864
+ next.pageIndex = pageIndex;
1865
+ next.pageNumber = pageIndex + 1;
1866
+ next.totalPages = totalPages;
1867
+ next.docTitle = docTitle;
1868
+ next.pagePath = pagePath;
1869
+ }
1870
+ if ("selection" in raw) next.selection = parseSelection(raw.selection);
1871
+ if (!next.docId) return;
1872
+ cached = next;
1873
+ const body = {
1874
+ ...next,
1875
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1876
+ };
1877
+ try {
1878
+ await fs.mkdir(outDir, { recursive: true });
1879
+ await fs.writeFile(tmpFile, `${JSON.stringify(body, null, 2)}\n`, "utf8");
1880
+ await fs.rename(tmpFile, outFile);
1881
+ } catch {}
1882
+ });
1883
+ }
1884
+ };
1885
+ }
1886
+ //#endregion
1887
+ //#region src/vite/data-plugin.ts
1888
+ const DATA_RE = /\.(csv|tsv)$/i;
1889
+ /**
1890
+ * Makes `import rows from './data/q3.csv'` an array of objects at build time.
1891
+ *
1892
+ * Fetching data at render time is not an option here: the flow packer measures
1893
+ * the real DOM to decide where pages break, and both exporters serialize what
1894
+ * is on screen. Data that arrives a tick later arrives after the layout is
1895
+ * already decided. Resolving it as a module keeps a table's numbers as
1896
+ * synchronous as the prose around them, in the dev server and in a static build
1897
+ * alike.
1898
+ *
1899
+ * `?raw` and `?url` are left to Vite.
1900
+ */
1901
+ function dataPlugin() {
1902
+ return {
1903
+ name: "mosage:data",
1904
+ enforce: "pre",
1905
+ async load(id) {
1906
+ const [file, query] = id.split("?");
1907
+ if (query !== void 0) return null;
1908
+ if (!DATA_RE.test(file)) return null;
1909
+ let text;
1910
+ try {
1911
+ text = await fs.readFile(file, "utf8");
1912
+ } catch {
1913
+ return null;
1914
+ }
1915
+ const table = parseDelimited(text, { delimiter: /\.tsv$/i.test(file) ? " " : "," });
1916
+ return [
1917
+ `export const columns = ${JSON.stringify(table.columns)};`,
1918
+ `export const rows = ${JSON.stringify(table.rows)};`,
1919
+ "export default rows;"
1920
+ ].join("\n");
1921
+ }
1922
+ };
1923
+ }
1924
+ //#endregion
1925
+ //#region src/vite/design-plugin.ts
1926
+ function parseSource(source) {
1927
+ try {
1928
+ return parse(source, {
1929
+ sourceType: "module",
1930
+ plugins: ["typescript", "jsx"],
1931
+ errorRecovery: true
1932
+ });
1933
+ } catch {
1934
+ return null;
1935
+ }
1936
+ }
1937
+ function programBody(ast) {
1938
+ return ast.program?.body ?? [];
1939
+ }
1940
+ function designObjectOf(node) {
1941
+ let varDecl = null;
1942
+ if (node.type === "VariableDeclaration") varDecl = node;
1943
+ else if (node.type === "ExportNamedDeclaration") {
1944
+ const decl = node.declaration;
1945
+ if (decl?.type === "VariableDeclaration") varDecl = decl;
1946
+ }
1947
+ if (!varDecl) return null;
1948
+ const declarations = varDecl.declarations ?? [];
1949
+ for (const d of declarations) {
1950
+ const id = d.id;
1951
+ if (id?.type !== "Identifier" || id.name !== "design") continue;
1952
+ const init = d.init;
1953
+ if (!init) return "unsupported";
1954
+ let inner = init;
1955
+ if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
1956
+ const expr = inner.expression;
1957
+ if (expr) inner = expr;
1958
+ }
1959
+ if (inner.type !== "ObjectExpression") return "unsupported";
1960
+ return {
1961
+ decl: node,
1962
+ object: inner
1963
+ };
1964
+ }
1965
+ return null;
1966
+ }
1967
+ function findDesign(ast) {
1968
+ for (const node of programBody(ast)) {
1969
+ const hit = designObjectOf(node);
1970
+ if (hit === null) continue;
1971
+ if (hit === "unsupported") return null;
1972
+ return {
1973
+ loc: {
1974
+ declStart: hit.decl.start,
1975
+ declEnd: hit.decl.end,
1976
+ objectStart: hit.object.start,
1977
+ objectEnd: hit.object.end
1978
+ },
1979
+ object: hit.object
1980
+ };
1981
+ }
1982
+ return null;
1983
+ }
1984
+ function literalToValue(node) {
1985
+ switch (node.type) {
1986
+ case "StringLiteral": return node.value;
1987
+ case "NumericLiteral": return node.value;
1988
+ case "BooleanLiteral": return node.value;
1989
+ case "NullLiteral": return null;
1990
+ case "UnaryExpression": {
1991
+ const op = node.operator;
1992
+ const arg = node.argument;
1993
+ const v = literalToValue(arg);
1994
+ if (op === "-" && typeof v === "number") return -v;
1995
+ if (op === "+" && typeof v === "number") return v;
1996
+ throw new Error(`unsupported unary operator ${op}`);
1997
+ }
1998
+ case "TemplateLiteral": {
1999
+ const quasis = node.quasis;
2000
+ if (node.expressions.length > 0) throw new Error("template literal has expressions");
2001
+ const value = quasis[0].value;
2002
+ return value.cooked ?? value.raw;
2003
+ }
2004
+ case "ObjectExpression": {
2005
+ const properties = node.properties;
2006
+ const out = {};
2007
+ for (const prop of properties) {
2008
+ if (prop.type !== "ObjectProperty") throw new Error("object has spread or method");
2009
+ const p = prop;
2010
+ if (p.computed) throw new Error("object has computed key");
2011
+ let key;
2012
+ if (p.key.type === "Identifier" && typeof p.key.name === "string") key = p.key.name;
2013
+ else if (p.key.type === "StringLiteral" && typeof p.key.value === "string") key = p.key.value;
2014
+ else throw new Error("unsupported object key");
2015
+ out[key] = literalToValue(p.value);
2016
+ }
2017
+ return out;
2018
+ }
2019
+ default: throw new Error(`unsupported node type ${node.type}`);
2020
+ }
2021
+ }
2022
+ function isPlainObject(v) {
2023
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2024
+ }
2025
+ function mergeDesign(base, patch) {
2026
+ const out = JSON.parse(JSON.stringify(base));
2027
+ const apply = (target, src) => {
2028
+ for (const [k, v] of Object.entries(src)) if (isPlainObject(v) && isPlainObject(target[k])) apply(target[k], v);
2029
+ else target[k] = v;
2030
+ };
2031
+ if (isPlainObject(patch)) apply(out, patch);
2032
+ return out;
2033
+ }
2034
+ function indent(level) {
2035
+ return " ".repeat(level);
2036
+ }
2037
+ function jsString(s) {
2038
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
2039
+ }
2040
+ function isValidIdentifier(name) {
2041
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
2042
+ }
2043
+ function serializeValue(value, level) {
2044
+ if (value === null) return "null";
2045
+ if (typeof value === "string") return jsString(value);
2046
+ if (typeof value === "number") {
2047
+ if (!Number.isFinite(value)) throw new Error("non-finite number");
2048
+ return String(value);
2049
+ }
2050
+ if (typeof value === "boolean") return value ? "true" : "false";
2051
+ if (isPlainObject(value)) {
2052
+ const entries = Object.entries(value);
2053
+ if (entries.length === 0) return "{}";
2054
+ const childIndent = indent(level + 1);
2055
+ return `{\n${entries.map(([k, v]) => {
2056
+ const key = isValidIdentifier(k) ? k : jsString(k);
2057
+ return `${childIndent}${key}: ${serializeValue(v, level + 1)},`;
2058
+ }).join("\n")}\n${indent(level)}}`;
2059
+ }
2060
+ throw new Error(`unsupported value type ${typeof value}`);
2061
+ }
2062
+ function serializeDesign(design) {
2063
+ return serializeValue(design, 0);
2064
+ }
2065
+ function parseDocDesign(source) {
2066
+ const ast = parseSource(source);
2067
+ if (!ast) return {
2068
+ ok: false,
2069
+ exists: true,
2070
+ error: "could not parse document source"
2071
+ };
2072
+ const found = findDesign(ast);
2073
+ if (!found) {
2074
+ if (programBody(ast).some((node) => designObjectOf(node) !== null)) return {
2075
+ ok: false,
2076
+ exists: true,
2077
+ error: "design has an unsupported initializer"
2078
+ };
2079
+ return {
2080
+ ok: false,
2081
+ exists: false
2082
+ };
2083
+ }
2084
+ let value;
2085
+ try {
2086
+ value = literalToValue(found.object);
2087
+ } catch (err) {
2088
+ return {
2089
+ ok: false,
2090
+ exists: true,
2091
+ error: err.message
2092
+ };
2093
+ }
2094
+ return {
2095
+ ok: true,
2096
+ design: mergeDesign(defaultDesign, value),
2097
+ loc: found.loc
2098
+ };
2099
+ }
2100
+ function findImports(ast) {
2101
+ const out = [];
2102
+ for (const node of programBody(ast)) {
2103
+ if (node.type !== "ImportDeclaration") continue;
2104
+ const src = node.source?.value;
2105
+ if (typeof src !== "string") continue;
2106
+ out.push({
2107
+ node,
2108
+ source: src,
2109
+ specifiers: node.specifiers ?? []
2110
+ });
2111
+ }
2112
+ return out;
2113
+ }
2114
+ function ensureDesignSystemImport(source, ast) {
2115
+ const imports = findImports(ast);
2116
+ const coreImport = imports.find((imp) => imp.source === "mosage");
2117
+ if (coreImport) {
2118
+ if (coreImport.specifiers.some((spec) => {
2119
+ if (spec.type !== "ImportSpecifier") return false;
2120
+ return spec.imported?.name === "DesignSystem";
2121
+ })) return source;
2122
+ const node = coreImport.node;
2123
+ const braceClose = source.slice(node.start, node.end).lastIndexOf("}");
2124
+ if (braceClose === -1) return source;
2125
+ const absoluteBrace = node.start + braceClose;
2126
+ const insertText = coreImport.specifiers.length > 0 ? ", type DesignSystem" : "type DesignSystem";
2127
+ return source.slice(0, absoluteBrace) + insertText + source.slice(absoluteBrace);
2128
+ }
2129
+ const stmt = `import type { DesignSystem } from 'mosage';\n`;
2130
+ if (imports.length > 0) {
2131
+ const insertAt = imports[imports.length - 1].node.end;
2132
+ const trail = source[insertAt] === "\n" ? "" : "\n";
2133
+ return `${source.slice(0, insertAt)}\n${stmt.slice(0, -1)}${trail}${source.slice(insertAt)}`;
2134
+ }
2135
+ return `${stmt}\n${source}`;
2136
+ }
2137
+ function findInsertionPoint(source, ast) {
2138
+ const imports = findImports(ast);
2139
+ if (imports.length === 0) return 0;
2140
+ let off = imports[imports.length - 1].node.end;
2141
+ while (off < source.length && source[off] !== "\n") off++;
2142
+ if (off < source.length) off++;
2143
+ return off;
2144
+ }
2145
+ function applyDesignWrite(source, next) {
2146
+ let body;
2147
+ try {
2148
+ body = serializeDesign(next);
2149
+ } catch (err) {
2150
+ return {
2151
+ ok: false,
2152
+ status: 422,
2153
+ error: `serialize failed: ${err.message}`
2154
+ };
2155
+ }
2156
+ const ast = parseSource(source);
2157
+ if (!ast) return {
2158
+ ok: false,
2159
+ status: 422,
2160
+ error: "could not parse document source"
2161
+ };
2162
+ const found = findDesign(ast);
2163
+ if (found) return {
2164
+ ok: true,
2165
+ source: source.slice(0, found.loc.objectStart) + body + source.slice(found.loc.objectEnd),
2166
+ created: false
2167
+ };
2168
+ const withImport = ensureDesignSystemImport(source, ast);
2169
+ const ast2 = parseSource(withImport);
2170
+ if (!ast2) return {
2171
+ ok: false,
2172
+ status: 422,
2173
+ error: "failed to re-parse after adding import"
2174
+ };
2175
+ const insertAt = findInsertionPoint(withImport, ast2);
2176
+ const block = `\nexport const design: DesignSystem = ${body};\n`;
2177
+ return {
2178
+ ok: true,
2179
+ source: withImport.slice(0, insertAt) + block + withImport.slice(insertAt),
2180
+ created: true
2181
+ };
2182
+ }
2183
+ function designPlugin(opts) {
2184
+ const userCwd = opts.userCwd;
2185
+ const docsDir = opts.docsDir ?? "docs";
2186
+ return {
2187
+ name: "mosage:design",
2188
+ apply: "serve",
2189
+ configureServer(server) {
2190
+ server.middlewares.use("/__design", async (req, res, next) => {
2191
+ const url = new URL(req.url ?? "/", "http://local");
2192
+ const method = req.method ?? "GET";
2193
+ const docId = url.searchParams.get("docId") ?? "";
2194
+ const file = resolveDocPath(userCwd, docsDir, docId);
2195
+ if (!file) return json(res, 400, { error: "invalid docId" });
2196
+ try {
2197
+ if (method === "GET" && url.pathname === "/") {
2198
+ let source;
2199
+ try {
2200
+ source = await fs.readFile(file, "utf8");
2201
+ } catch {
2202
+ return json(res, 404, { error: "document not found" });
2203
+ }
2204
+ const parsed = parseDocDesign(source);
2205
+ if (parsed.ok) return json(res, 200, {
2206
+ design: parsed.design,
2207
+ exists: true,
2208
+ warning: null
2209
+ });
2210
+ if (parsed.exists === false) return json(res, 200, {
2211
+ design: defaultDesign,
2212
+ exists: false,
2213
+ warning: null
2214
+ });
2215
+ return json(res, 200, {
2216
+ design: defaultDesign,
2217
+ exists: true,
2218
+ warning: parsed.error
2219
+ });
2220
+ }
2221
+ if (method === "PUT" && url.pathname === "/") {
2222
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2223
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2224
+ const patch = (await readBody(req)).patch;
2225
+ if (!patch || typeof patch !== "object") return json(res, 400, { error: "missing patch object" });
2226
+ let source;
2227
+ try {
2228
+ source = await fs.readFile(file, "utf8");
2229
+ } catch {
2230
+ return json(res, 404, { error: "document not found" });
2231
+ }
2232
+ const parsed = parseDocDesign(source);
2233
+ if (!parsed.ok && parsed.exists) return json(res, 422, { error: parsed.error });
2234
+ const merged = mergeDesign(parsed.ok ? parsed.design : defaultDesign, patch);
2235
+ const written = applyDesignWrite(source, merged);
2236
+ if (!written.ok) return json(res, written.status, { error: written.error });
2237
+ if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
2238
+ return json(res, 200, {
2239
+ ok: true,
2240
+ design: merged,
2241
+ created: written.created
2242
+ });
2243
+ }
2244
+ if (method === "POST" && url.pathname === "/reset") {
2245
+ const requestCheck = validateMutationRequest(req);
2246
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2247
+ let source;
2248
+ try {
2249
+ source = await fs.readFile(file, "utf8");
2250
+ } catch {
2251
+ return json(res, 404, { error: "document not found" });
2252
+ }
2253
+ const written = applyDesignWrite(source, defaultDesign);
2254
+ if (!written.ok) return json(res, written.status, { error: written.error });
2255
+ if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
2256
+ return json(res, 200, {
2257
+ ok: true,
2258
+ design: defaultDesign,
2259
+ created: written.created
2260
+ });
2261
+ }
2262
+ return next();
2263
+ } catch (err) {
2264
+ json(res, 500, { error: String(err.message ?? err) });
2265
+ }
2266
+ });
2267
+ }
2268
+ };
2269
+ }
2270
+ //#endregion
2271
+ //#region src/vite/diagram-plugin.ts
2272
+ const DIAGRAM_RE = /\.(mmd|mermaid)$/i;
2273
+ /**
2274
+ * Makes `import chart from './architecture.mmd'` a themed SVG at build time.
2275
+ *
2276
+ * The same rule as `data-plugin` applies, and for the same reason: the flow
2277
+ * packer measures the real DOM to decide where pages break, so a drawing that
2278
+ * renders a tick later renders after the layout is decided. Compiling in the
2279
+ * plugin also means no diagram library reaches the browser bundle — the SVG
2280
+ * arrives as text, already sized.
2281
+ *
2282
+ * `?raw` and `?url` are left to Vite.
2283
+ */
2284
+ function diagramPlugin() {
2285
+ return {
2286
+ name: "mosage:diagram",
2287
+ enforce: "pre",
2288
+ async load(id) {
2289
+ const [file, query] = id.split("?");
2290
+ if (query !== void 0) return null;
2291
+ if (!DIAGRAM_RE.test(file)) return null;
2292
+ let text;
2293
+ try {
2294
+ text = await fs.readFile(file, "utf8");
2295
+ } catch {
2296
+ return null;
2297
+ }
2298
+ try {
2299
+ const suffix = path.basename(file).replace(/[^a-zA-Z0-9]/g, "");
2300
+ const compiled = compileDiagram(text, { idSuffix: suffix });
2301
+ return [
2302
+ `export const svg = ${JSON.stringify(compiled.svg)};`,
2303
+ `export const width = ${compiled.width};`,
2304
+ `export const height = ${compiled.height};`,
2305
+ "export default { svg, width, height };"
2306
+ ].join("\n");
2307
+ } catch (err) {
2308
+ if (err instanceof DiagramSyntaxError) this.error(`${path.relative(process.cwd(), file)}: ${err.message}`);
2309
+ throw err;
2310
+ }
2311
+ }
2312
+ };
2313
+ }
2314
+ //#endregion
2315
+ //#region src/vite/loc-tags-plugin.ts
2316
+ const FORWARDING_COMPONENTS = /* @__PURE__ */ new Set(["ImagePlaceholder"]);
2317
+ function taggableName(opening) {
2318
+ const name = opening.name;
2319
+ if (name?.type !== "JSXIdentifier" || typeof name.name !== "string") return null;
2320
+ if (/^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name)) return name.name;
2321
+ return null;
2322
+ }
2323
+ function alreadyTagged(opening) {
2324
+ return (opening.attributes ?? []).some((attr) => {
2325
+ if (attr.type !== "JSXAttribute") return false;
2326
+ return attr.name?.name === "data-od-loc";
2327
+ });
2328
+ }
2329
+ function injectLocTags(code) {
2330
+ const ast = parseSource$1(code);
2331
+ if (!ast) return null;
2332
+ const insertions = [];
2333
+ walkJsx(ast, (node) => {
2334
+ const opening = node.openingElement;
2335
+ if (!opening || !node.loc) return;
2336
+ if (!taggableName(opening) || alreadyTagged(opening)) return;
2337
+ const nameNode = opening.name;
2338
+ insertions.push({
2339
+ offset: nameNode.end,
2340
+ text: ` data-od-loc="${node.loc.start.line}:${node.loc.start.column}"`
2341
+ });
2342
+ });
2343
+ if (insertions.length === 0) return null;
2344
+ insertions.sort((a, b) => b.offset - a.offset);
2345
+ let next = code;
2346
+ for (const insertion of insertions) next = next.slice(0, insertion.offset) + insertion.text + next.slice(insertion.offset);
2347
+ return next;
2348
+ }
2349
+ function isDocSourceFile(id, docsRootPosix) {
2350
+ const filePath = id.split(/[?#]/)[0].replace(/\\/g, "/");
2351
+ if (!filePath.startsWith(`${docsRootPosix}/`)) return false;
2352
+ if (!filePath.endsWith(".tsx")) return false;
2353
+ if (filePath.endsWith(".d.ts") || filePath.endsWith(".test.tsx")) return false;
2354
+ return filePath.slice(docsRootPosix.length + 1).includes("/");
2355
+ }
2356
+ function locTagsPlugin(opts) {
2357
+ const docsRoot = path.resolve(opts.userCwd, opts.docsDir ?? "docs").replace(/\\/g, "/");
2358
+ return {
2359
+ name: "mosage:loc-tags",
2360
+ apply: "serve",
2361
+ enforce: "pre",
2362
+ transform(code, id) {
2363
+ if (!isDocSourceFile(id, docsRoot)) return null;
2364
+ const next = injectLocTags(code);
2365
+ return next === null ? null : {
2366
+ code: next,
2367
+ map: null
2368
+ };
2369
+ }
2370
+ };
2371
+ }
2372
+ //#endregion
2373
+ //#region src/vite/themes-plugin.ts
2374
+ const THEMES_VMOD = "virtual:mosage/themes";
2375
+ function resolved(id) {
2376
+ return `\0${id}`;
2377
+ }
2378
+ const FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
2379
+ function parseFrontmatter(raw, themeId) {
2380
+ const match = raw.match(FM_RE);
2381
+ const fmText = match ? match[1] : "";
2382
+ const body = match ? match[2] : raw;
2383
+ const data = {};
2384
+ for (const line of fmText.split(/\r?\n/)) {
2385
+ const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
2386
+ if (!m) continue;
2387
+ let value = m[2].trim();
2388
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2389
+ data[m[1]] = value;
2390
+ }
2391
+ return {
2392
+ fm: {
2393
+ name: data.name || themeId,
2394
+ description: data.description || "",
2395
+ pageSize: data.pageSize || "",
2396
+ mode: data.mode || ""
2397
+ },
2398
+ body: body.trim()
2399
+ };
2400
+ }
2401
+ async function findThemes(userCwd, themesDir) {
2402
+ const abs = path.resolve(userCwd, themesDir);
2403
+ if (!existsSync(abs)) return [];
2404
+ return (await fg("*.md", {
2405
+ cwd: abs,
2406
+ absolute: true,
2407
+ onlyFiles: true
2408
+ })).sort();
2409
+ }
2410
+ async function readTheme(mdAbs, themesRoot) {
2411
+ const id = path.basename(mdAbs, ".md");
2412
+ const { fm, body } = parseFrontmatter(await fs.readFile(mdAbs, "utf8"), id);
2413
+ let demoAbs = null;
2414
+ for (const cand of [
2415
+ `${id}.demo.tsx`,
2416
+ `${id}.demo.jsx`,
2417
+ `${id}.demo.ts`,
2418
+ `${id}.demo.js`
2419
+ ]) {
2420
+ const p = path.join(themesRoot, cand);
2421
+ if (existsSync(p)) {
2422
+ demoAbs = p;
2423
+ break;
2424
+ }
2425
+ }
2426
+ return {
2427
+ id,
2428
+ frontmatter: fm,
2429
+ body,
2430
+ demoAbs
2431
+ };
2432
+ }
2433
+ function generateThemesModule(themes, isDev) {
2434
+ const meta = themes.map((t) => ({
2435
+ id: t.id,
2436
+ name: t.frontmatter.name,
2437
+ description: t.frontmatter.description,
2438
+ pageSize: t.frontmatter.pageSize,
2439
+ mode: t.frontmatter.mode,
2440
+ body: t.body,
2441
+ hasDemo: t.demoAbs !== null
2442
+ }));
2443
+ const cases = themes.flatMap((t) => {
2444
+ const abs = t.demoAbs;
2445
+ if (!abs) return [];
2446
+ const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
2447
+ const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(importPath)})` : `import(${JSON.stringify(importPath)})`;
2448
+ return [` case ${JSON.stringify(t.id)}: return ${importExpr};`];
2449
+ }).join("\n");
2450
+ return `// virtual:mosage/themes — generated
2451
+ export const themes = ${JSON.stringify(meta)};
2452
+
2453
+ export async function loadThemeDemo(id) {
2454
+ switch (id) {
2455
+ ${cases}
2456
+ default: throw new Error('Theme demo not found: ' + id);
2457
+ }
2458
+ }
2459
+ `;
2460
+ }
2461
+ function themesPlugin(opts) {
2462
+ const { userCwd, config } = opts;
2463
+ const themesDir = config.themesDir ?? "themes";
2464
+ const themesRoot = path.resolve(userCwd, themesDir);
2465
+ let isDev = false;
2466
+ return {
2467
+ name: "mosage:themes",
2468
+ config(_c, env) {
2469
+ isDev = env.command === "serve";
2470
+ },
2471
+ resolveId(id) {
2472
+ if (id === THEMES_VMOD) return resolved(THEMES_VMOD);
2473
+ return null;
2474
+ },
2475
+ async load(id) {
2476
+ if (id !== resolved(THEMES_VMOD)) return null;
2477
+ const files = await findThemes(userCwd, themesDir);
2478
+ return generateThemesModule(await Promise.all(files.map((f) => readTheme(f, themesRoot))), isDev);
2479
+ },
2480
+ configureServer(server) {
2481
+ const isThemeFile = (p) => {
2482
+ const rel = path.relative(themesRoot, p);
2483
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
2484
+ if (rel.includes(path.sep)) return false;
2485
+ return /\.(md|demo\.(tsx|jsx|ts|js))$/.test(rel);
2486
+ };
2487
+ let reloadTimer = null;
2488
+ const reload = () => {
2489
+ if (reloadTimer) clearTimeout(reloadTimer);
2490
+ reloadTimer = setTimeout(() => {
2491
+ reloadTimer = null;
2492
+ const mod = server.moduleGraph.getModuleById(resolved(THEMES_VMOD));
2493
+ if (mod) server.moduleGraph.invalidateModule(mod);
2494
+ server.ws.send({ type: "full-reload" });
2495
+ }, 150);
2496
+ };
2497
+ server.watcher.add(themesRoot);
2498
+ for (const event of [
2499
+ "add",
2500
+ "unlink",
2501
+ "change"
2502
+ ]) server.watcher.on(event, (p) => {
2503
+ if (isThemeFile(p)) reload();
2504
+ });
2505
+ }
2506
+ };
2507
+ }
2508
+ //#endregion
2509
+ //#region src/vite/config.ts
2510
+ function findPackageRoot(fromFile) {
2511
+ let dir = path.dirname(fromFile);
2512
+ while (dir !== path.dirname(dir)) {
2513
+ if (existsSync(path.join(dir, "package.json"))) return dir;
2514
+ dir = path.dirname(dir);
2515
+ }
2516
+ throw new Error(`Could not find package.json walking up from ${fromFile}`);
2517
+ }
2518
+ const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
2519
+ const APP_ROOT = path.join(PKG_ROOT, "src", "app");
2520
+ function readCoreVersion() {
2521
+ try {
2522
+ const raw = readFileSync(path.join(PKG_ROOT, "package.json"), "utf8");
2523
+ return JSON.parse(raw).version ?? "0.0.0";
2524
+ } catch {
2525
+ return "0.0.0";
2526
+ }
2527
+ }
2528
+ const CORE_VERSION = readCoreVersion();
2529
+ async function createViteConfig(opts) {
2530
+ const userCwd = path.resolve(opts.userCwd);
2531
+ const config = opts.config ?? await loadUserConfig(userCwd);
2532
+ const docsDir = config.docsDir ?? "docs";
2533
+ const assetsDir = config.assetsDir ?? "assets";
2534
+ const docsAbs = path.resolve(userCwd, docsDir);
2535
+ const themesAbs = path.resolve(userCwd, config.themesDir ?? "themes");
2536
+ const assetsAbs = path.resolve(userCwd, assetsDir);
2537
+ return {
2538
+ base: config.base ?? "/",
2539
+ root: APP_ROOT,
2540
+ configFile: false,
2541
+ envDir: userCwd,
2542
+ plugins: [
2543
+ dataPlugin(),
2544
+ diagramPlugin(),
2545
+ locTagsPlugin({
2546
+ userCwd,
2547
+ docsDir
2548
+ }),
2549
+ react(),
2550
+ tailwindcss(),
2551
+ mosagePlugin({
2552
+ userCwd,
2553
+ config,
2554
+ coreVersion: CORE_VERSION
2555
+ }),
2556
+ themesPlugin({
2557
+ userCwd,
2558
+ config
2559
+ }),
2560
+ designPlugin({
2561
+ userCwd,
2562
+ docsDir
2563
+ }),
2564
+ apiPlugin({
2565
+ userCwd,
2566
+ docsDir,
2567
+ assetsDir,
2568
+ coreVersion: CORE_VERSION
2569
+ }),
2570
+ ...opts.headless ? [] : [currentPlugin({
2571
+ userCwd,
2572
+ docsDir
2573
+ })]
2574
+ ],
2575
+ resolve: { alias: {
2576
+ "@": APP_ROOT,
2577
+ "@assets": assetsAbs
2578
+ } },
2579
+ optimizeDeps: {
2580
+ entries: [path.join(APP_ROOT, "main.tsx")],
2581
+ include: [
2582
+ "react",
2583
+ "react-dom",
2584
+ "react-dom/client",
2585
+ "react-router-dom",
2586
+ "next-themes",
2587
+ "lucide-react",
2588
+ "clsx",
2589
+ "tailwind-merge"
2590
+ ],
2591
+ esbuildOptions: { plugins: [{
2592
+ name: "mosage:virtual-externals",
2593
+ setup(build) {
2594
+ build.onResolve({ filter: /^virtual:mosage\// }, (args) => ({
2595
+ path: args.path,
2596
+ external: true
2597
+ }));
2598
+ }
2599
+ }] }
2600
+ },
2601
+ server: {
2602
+ port: config.port ?? 5273,
2603
+ ...config.allowedHosts !== void 0 ? { allowedHosts: config.allowedHosts } : {},
2604
+ fs: { allow: [
2605
+ APP_ROOT,
2606
+ userCwd,
2607
+ docsAbs,
2608
+ themesAbs,
2609
+ assetsAbs
2610
+ ] }
2611
+ },
2612
+ build: {
2613
+ outDir: path.resolve(userCwd, "dist"),
2614
+ emptyOutDir: true
2615
+ }
2616
+ };
2617
+ }
2618
+ //#endregion
2619
+ export { resolveEntry as a, DOC_ID_RE as c, listDocIds as i, loadUserConfig as l, OpsError as n, makeContext as o, createDocument as r, validateAssetName as s, createViteConfig as t, mosagePlugin as u };