@autono/open-pages 0.1.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 (110) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +108 -0
  3. package/bin.js +2 -0
  4. package/dist/build-TP72kiw7.js +35 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +100 -0
  7. package/dist/config-C9or4m4J.d.ts +23 -0
  8. package/dist/config-L0IbktJ0.js +3135 -0
  9. package/dist/design-C13iz9_4.js +35 -0
  10. package/dist/dev-I2IyEh5W.js +97 -0
  11. package/dist/export-B-kBRWB1.js +4 -0
  12. package/dist/export-ic28osQP.js +131 -0
  13. package/dist/index.d.ts +56 -0
  14. package/dist/index.js +3 -0
  15. package/dist/locale/index.d.ts +24 -0
  16. package/dist/locale/index.js +881 -0
  17. package/dist/open-pages-plugin-D_DN_zjh.js +632 -0
  18. package/dist/preview-BrDuvgAN.js +21 -0
  19. package/dist/sync-Be_hxSOF.js +139 -0
  20. package/dist/sync-lKEXyPOh.js +3 -0
  21. package/dist/types-C5NCdDT-.d.ts +219 -0
  22. package/dist/update-CV62NhBT.js +37 -0
  23. package/dist/update-package-Cxt57pz7.js +152 -0
  24. package/dist/version-B6AiiGvT.js +26 -0
  25. package/dist/version-CeffgdfZ.js +3 -0
  26. package/dist/vite/index.d.ts +14 -0
  27. package/dist/vite/index.js +7 -0
  28. package/env.d.ts +59 -0
  29. package/package.json +106 -0
  30. package/skills/apply-comments/SKILL.md +86 -0
  31. package/skills/create-page/SKILL.md +92 -0
  32. package/skills/create-theme/SKILL.md +223 -0
  33. package/skills/current-page/SKILL.md +106 -0
  34. package/skills/page-authoring/SKILL.md +148 -0
  35. package/skills/page-authoring/references/assets-and-fonts.md +85 -0
  36. package/skills/page-authoring/references/html-pages.md +65 -0
  37. package/skills/page-authoring/references/interactivity.md +77 -0
  38. package/skills/page-authoring/references/layout-and-responsive.md +71 -0
  39. package/skills/page-authoring/references/typography-and-color.md +60 -0
  40. package/src/app/app.tsx +45 -0
  41. package/src/app/components/asset-view.tsx +1665 -0
  42. package/src/app/components/command/command-menu.tsx +227 -0
  43. package/src/app/components/command/command.tsx +142 -0
  44. package/src/app/components/command/home-command-menu.tsx +96 -0
  45. package/src/app/components/icon-tooltip.tsx +34 -0
  46. package/src/app/components/language-toggle.tsx +46 -0
  47. package/src/app/components/sidebar/folder-item.tsx +269 -0
  48. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  49. package/src/app/components/sidebar/sidebar-footer.tsx +139 -0
  50. package/src/app/components/sidebar/sidebar.tsx +302 -0
  51. package/src/app/components/theme-toggle.tsx +66 -0
  52. package/src/app/components/themes/theme-detail.tsx +226 -0
  53. package/src/app/components/themes/themes-gallery.tsx +104 -0
  54. package/src/app/components/ui/badge.tsx +48 -0
  55. package/src/app/components/ui/button.tsx +93 -0
  56. package/src/app/components/ui/card.tsx +92 -0
  57. package/src/app/components/ui/context-menu.tsx +247 -0
  58. package/src/app/components/ui/dialog.tsx +155 -0
  59. package/src/app/components/ui/dropdown-menu.tsx +267 -0
  60. package/src/app/components/ui/input.tsx +25 -0
  61. package/src/app/components/ui/label.tsx +21 -0
  62. package/src/app/components/ui/popover.tsx +82 -0
  63. package/src/app/components/ui/progress.tsx +33 -0
  64. package/src/app/components/ui/scroll-area.tsx +53 -0
  65. package/src/app/components/ui/select.tsx +195 -0
  66. package/src/app/components/ui/separator.tsx +26 -0
  67. package/src/app/components/ui/slider.tsx +68 -0
  68. package/src/app/components/ui/sonner.tsx +48 -0
  69. package/src/app/components/ui/tabs.tsx +79 -0
  70. package/src/app/components/ui/textarea.tsx +22 -0
  71. package/src/app/components/ui/toggle-group.tsx +84 -0
  72. package/src/app/components/ui/toggle.tsx +45 -0
  73. package/src/app/components/ui/tooltip.tsx +75 -0
  74. package/src/app/favicon.ico +0 -0
  75. package/src/app/frame/inspect.ts +140 -0
  76. package/src/app/frame/main.tsx +91 -0
  77. package/src/app/frame.html +12 -0
  78. package/src/app/index.html +13 -0
  79. package/src/app/lib/asset-filter.test.ts +176 -0
  80. package/src/app/lib/asset-filter.ts +80 -0
  81. package/src/app/lib/assets.ts +257 -0
  82. package/src/app/lib/design.ts +58 -0
  83. package/src/app/lib/folders.ts +239 -0
  84. package/src/app/lib/frame.ts +38 -0
  85. package/src/app/lib/locale-store.ts +67 -0
  86. package/src/app/lib/page-thumb.tsx +46 -0
  87. package/src/app/lib/pages.ts +28 -0
  88. package/src/app/lib/sdk.ts +43 -0
  89. package/src/app/lib/themes.ts +22 -0
  90. package/src/app/lib/use-locale.ts +8 -0
  91. package/src/app/lib/use-page-module.ts +48 -0
  92. package/src/app/lib/use-page-titles.ts +29 -0
  93. package/src/app/lib/use-restart-server.ts +78 -0
  94. package/src/app/lib/utils.test.ts +25 -0
  95. package/src/app/lib/utils.ts +6 -0
  96. package/src/app/main.tsx +14 -0
  97. package/src/app/routes/assets.tsx +9 -0
  98. package/src/app/routes/home-shell.tsx +252 -0
  99. package/src/app/routes/home.tsx +846 -0
  100. package/src/app/routes/page.tsx +329 -0
  101. package/src/app/routes/themes.tsx +34 -0
  102. package/src/app/styles.css +405 -0
  103. package/src/app/virtual.d.ts +53 -0
  104. package/src/locale/en.ts +218 -0
  105. package/src/locale/format.ts +12 -0
  106. package/src/locale/index.ts +6 -0
  107. package/src/locale/ja.ts +221 -0
  108. package/src/locale/types.ts +215 -0
  109. package/src/locale/zh-cn.ts +218 -0
  110. package/src/locale/zh-tw.ts +218 -0
@@ -0,0 +1,3135 @@
1
+ import { defaultDesign } from "./design-C13iz9_4.js";
2
+ import { PAGE_ID_RE, duplicatePageDir, loadUserConfig, openPagesPlugin, resolvePageEntry, rmPageDir, updateMetaTitleInSource, validatePageName } from "./open-pages-plugin-D_DN_zjh.js";
3
+ import { detectPackageManager, fetchLatest, formatCommand, invalidateLatestCache, isOutdated, localOpenPagesCommand, runCommand, updateCommandFor } from "./update-package-Cxt57pz7.js";
4
+ import { APP_ROOT, PKG_ROOT, readCoreVersion } from "./version-B6AiiGvT.js";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+ import { existsSync } from "node:fs";
9
+ import tailwindcss from "@tailwindcss/vite";
10
+ import react from "@vitejs/plugin-react";
11
+ import { normalizePath, searchForWorkspaceRoot } from "vite";
12
+ import { parse } from "@babel/parser";
13
+ import * as t$3 from "@babel/types";
14
+ import * as t$2 from "@babel/types";
15
+ import * as t$1 from "@babel/types";
16
+ import * as t from "@babel/types";
17
+ import { isJSXElement, isJSXFragment } from "@babel/types";
18
+ import fg from "fast-glob";
19
+
20
+ //#region src/editing/babel-walk.ts
21
+ const SKIP_KEYS = new Set([
22
+ "loc",
23
+ "start",
24
+ "end",
25
+ "type",
26
+ "extra",
27
+ "leadingComments",
28
+ "trailingComments",
29
+ "innerComments"
30
+ ]);
31
+ function walk(ast, visit, accept) {
32
+ let stopped = false;
33
+ const recurse = (node) => {
34
+ if (stopped || !node || typeof node !== "object") return;
35
+ if (Array.isArray(node)) {
36
+ for (const c of node) recurse(c);
37
+ return;
38
+ }
39
+ const n = node;
40
+ if (typeof n.type !== "string") return;
41
+ if (accept(n) && visit(n) === "stop") {
42
+ stopped = true;
43
+ return;
44
+ }
45
+ for (const key of Object.keys(n)) {
46
+ if (SKIP_KEYS.has(key)) continue;
47
+ recurse(n[key]);
48
+ }
49
+ };
50
+ recurse(ast);
51
+ }
52
+ const isJsx = (n) => isJSXElement(n) || isJSXFragment(n);
53
+ const acceptAll = () => true;
54
+ function walkJsx(ast, visit) {
55
+ walk(ast, visit, isJsx);
56
+ }
57
+ function walkAll(ast, visit) {
58
+ walk(ast, visit, acceptAll);
59
+ }
60
+ function parseSource$1(source) {
61
+ try {
62
+ const ast = parse(source, {
63
+ sourceType: "module",
64
+ plugins: ["typescript", "jsx"],
65
+ errorRecovery: true
66
+ });
67
+ return ast.errors && ast.errors.length > 0 ? null : ast;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ //#endregion
74
+ //#region src/editing/edit-ops.ts
75
+ function jsString$1(s) {
76
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
77
+ }
78
+ function spliceRange(node, text) {
79
+ return {
80
+ from: node.start ?? 0,
81
+ to: node.end ?? 0,
82
+ text
83
+ };
84
+ }
85
+ function formatJsxAttrValue(value) {
86
+ if (/^[^"\\<>&{}\n\r]*$/.test(value)) return `"${value}"`;
87
+ return `{${jsString$1(value)}}`;
88
+ }
89
+ function jsxAttrName(attr) {
90
+ return t$3.isJSXIdentifier(attr.name) ? attr.name.name : null;
91
+ }
92
+ function findJsxAttr(opening, name) {
93
+ for (const attr of opening.attributes) if (t$3.isJSXAttribute(attr) && jsxAttrName(attr) === name) return attr;
94
+ return null;
95
+ }
96
+ function readJsxStringAttr(opening, name) {
97
+ const attr = findJsxAttr(opening, name);
98
+ const v = attr?.value;
99
+ if (!v) return null;
100
+ if (t$3.isStringLiteral(v)) return v.value;
101
+ if (t$3.isJSXExpressionContainer(v) && t$3.isStringLiteral(v.expression)) return v.expression.value;
102
+ return null;
103
+ }
104
+ function readJsxNumberAttr(opening, name) {
105
+ const attr = findJsxAttr(opening, name);
106
+ const v = attr?.value;
107
+ if (!v || !t$3.isJSXExpressionContainer(v)) return null;
108
+ if (!t$3.isNumericLiteral(v.expression)) return null;
109
+ const n = v.expression.value;
110
+ return Number.isFinite(n) ? n : null;
111
+ }
112
+ function findImports$1(ast) {
113
+ const out = [];
114
+ for (const node of ast.program.body) {
115
+ if (!t$3.isImportDeclaration(node)) continue;
116
+ let def = null;
117
+ for (const spec of node.specifiers) if (t$3.isImportDefaultSpecifier(spec)) {
118
+ def = spec.local.name;
119
+ break;
120
+ }
121
+ out.push({
122
+ node,
123
+ source: node.source.value,
124
+ defaultIdent: def
125
+ });
126
+ }
127
+ return out;
128
+ }
129
+ function collectTopLevelIdentifiers(ast) {
130
+ const names = new Set();
131
+ for (const imp of findImports$1(ast)) {
132
+ if (imp.defaultIdent) names.add(imp.defaultIdent);
133
+ for (const spec of imp.node.specifiers) if (!t$3.isImportDefaultSpecifier(spec)) names.add(spec.local.name);
134
+ }
135
+ return names;
136
+ }
137
+ function safeAssetIdentifier(filename, taken) {
138
+ const stem = filename.replace(/\.[^.]+$/, "");
139
+ let camel = "";
140
+ let upper = false;
141
+ for (const ch of stem) if (/[A-Za-z0-9]/.test(ch)) {
142
+ camel += upper ? ch.toUpperCase() : ch;
143
+ upper = false;
144
+ } else upper = camel.length > 0;
145
+ let base = camel;
146
+ if (!base || !/^[A-Za-z_$]/.test(base)) base = `asset${base.charAt(0).toUpperCase()}${base.slice(1)}` || "asset";
147
+ base = base.charAt(0).toLowerCase() + base.slice(1);
148
+ let candidate = base;
149
+ let i = 2;
150
+ while (taken.has(candidate)) {
151
+ candidate = `${base}${i}`;
152
+ i += 1;
153
+ }
154
+ return candidate;
155
+ }
156
+ function findJsxAncestors$1(ast, line, column) {
157
+ const hits = [];
158
+ walkJsx(ast, (n) => {
159
+ if (!n.loc || !t$3.isJSXElement(n) && !t$3.isJSXFragment(n)) return;
160
+ const s = n.loc.start;
161
+ const e = n.loc.end;
162
+ const afterStart = line > s.line || line === s.line && column >= s.column;
163
+ const beforeEnd = line < e.line || line === e.line && column < e.column;
164
+ if (afterStart && beforeEnd) hits.push({
165
+ node: n,
166
+ size: (n.end ?? 0) - (n.start ?? 0)
167
+ });
168
+ });
169
+ hits.sort((a, b) => a.size - b.size);
170
+ return hits.map((h) => h.node);
171
+ }
172
+ function findJsxByStart(ast, line, column) {
173
+ let hit = null;
174
+ walkJsx(ast, (n) => {
175
+ if (!t$3.isJSXElement(n) || !n.loc) return;
176
+ const s = n.loc.start;
177
+ if (s.line === line && s.column === column) {
178
+ hit = n;
179
+ return "stop";
180
+ }
181
+ });
182
+ return hit;
183
+ }
184
+ function findInnermostJsxElement(ast, line, column) {
185
+ const exact = findJsxByStart(ast, line, column);
186
+ if (exact) return exact;
187
+ for (const n of findJsxAncestors$1(ast, line, column)) if (t$3.isJSXElement(n)) return n;
188
+ return null;
189
+ }
190
+ function findUniqueElementByText(ast, prevText) {
191
+ const hits = [];
192
+ walkJsx(ast, (n) => {
193
+ if (!t$3.isJSXElement(n)) return;
194
+ const parts = [];
195
+ collectTextRangeParts(n, parts);
196
+ if (textRangeContent(parts) !== prevText) return;
197
+ hits.push({
198
+ node: n,
199
+ size: (n.end ?? 0) - (n.start ?? 0)
200
+ });
201
+ });
202
+ if (hits.length === 0) return null;
203
+ hits.sort((a, b) => a.size - b.size);
204
+ const best = hits[0];
205
+ const bestStart = best.node.start ?? 0;
206
+ const bestEnd = best.node.end ?? 0;
207
+ const hasSiblingMatch = hits.slice(1).some(({ node }) => (node.start ?? 0) > bestStart || (node.end ?? 0) < bestEnd);
208
+ return hasSiblingMatch ? null : best.node;
209
+ }
210
+ function fallbackTextForOps(ops) {
211
+ for (const op of ops) if ((op.kind === "set-style" || op.kind === "set-text" || op.kind === "set-text-range-style") && op.prevText !== void 0) return op.prevText;
212
+ return null;
213
+ }
214
+ function hasOnlyTextOps(ops) {
215
+ return ops.length > 0 && ops.every((op) => op.kind === "set-text");
216
+ }
217
+ function elementTextMatches(element, prevText) {
218
+ const parts = [];
219
+ collectTextRangeParts(element, parts);
220
+ return textRangeContent(parts) === prevText;
221
+ }
222
+ function elementHasTextCandidate(ast, element, prevText) {
223
+ const norm = prevText.trim();
224
+ return collectElementTextCandidates(ast, element).some((candidate) => candidate.current === norm);
225
+ }
226
+ function findElementForEdit(ast, line, column, ops) {
227
+ const element = findInnermostJsxElement(ast, line, column);
228
+ const prevText = fallbackTextForOps(ops);
229
+ if (prevText === null) return element;
230
+ if (hasOnlyTextOps(ops) && element && (elementTextMatches(element, prevText) || elementHasTextCandidate(ast, element, prevText))) return element;
231
+ const textMatch = findUniqueElementByText(ast, prevText);
232
+ if (element && elementTextMatches(element, prevText)) return textMatch ?? element;
233
+ return textMatch ?? element;
234
+ }
235
+ function buildStyleSplice(source, element, ops) {
236
+ const opening = element.openingElement;
237
+ const existing = findJsxAttr(opening, "style");
238
+ const entries = [];
239
+ let hasRawEntry = false;
240
+ if (existing) {
241
+ const value = existing.value;
242
+ if (!value || !t$3.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
243
+ const expr = value.expression;
244
+ if (!t$3.isObjectExpression(expr)) {
245
+ if (typeof expr.start !== "number" || typeof expr.end !== "number") return { error: "style value missing source range" };
246
+ entries.push({
247
+ kind: "raw",
248
+ text: `...(${source.slice(expr.start, expr.end)})`
249
+ });
250
+ hasRawEntry = true;
251
+ } else for (const prop of expr.properties) if (t$3.isObjectProperty(prop) && !prop.computed) {
252
+ let keyName = null;
253
+ if (t$3.isIdentifier(prop.key)) keyName = prop.key.name;
254
+ else if (t$3.isStringLiteral(prop.key)) keyName = prop.key.value;
255
+ if (!keyName) return { error: "style has unsupported key" };
256
+ const v = prop.value;
257
+ if (typeof prop.key.start !== "number" || typeof prop.key.end !== "number" || typeof v.start !== "number" || typeof v.end !== "number") return { error: "style value missing source range" };
258
+ entries.push({
259
+ kind: "prop",
260
+ key: keyName,
261
+ keyText: source.slice(prop.key.start, prop.key.end),
262
+ valueText: source.slice(v.start, v.end)
263
+ });
264
+ } else {
265
+ if (typeof prop.start !== "number" || typeof prop.end !== "number") return { error: "style value missing source range" };
266
+ entries.push({
267
+ kind: "raw",
268
+ text: source.slice(prop.start, prop.end)
269
+ });
270
+ hasRawEntry = true;
271
+ }
272
+ }
273
+ for (const op of ops) {
274
+ const matching = entries.filter((entry) => entry.kind === "prop" && entry.key === op.key);
275
+ if (op.value === null) {
276
+ for (const entry of matching) entries.splice(entries.indexOf(entry), 1);
277
+ if (hasRawEntry) entries.push({
278
+ kind: "prop",
279
+ key: op.key,
280
+ keyText: op.key,
281
+ valueText: "undefined"
282
+ });
283
+ } else if (matching.length > 0) matching[matching.length - 1].valueText = jsString$1(op.value);
284
+ else entries.push({
285
+ kind: "prop",
286
+ key: op.key,
287
+ keyText: op.key,
288
+ valueText: jsString$1(op.value)
289
+ });
290
+ }
291
+ if (entries.length === 0) {
292
+ if (!existing) return null;
293
+ let from = existing.start ?? 0;
294
+ if (from > 0 && source[from - 1] === " ") from -= 1;
295
+ return {
296
+ from,
297
+ to: existing.end ?? 0,
298
+ text: ""
299
+ };
300
+ }
301
+ const propsText = entries.map((entry) => entry.kind === "prop" ? `${entry.keyText}: ${entry.valueText}` : entry.text).join(", ");
302
+ const newAttr = `style={{ ${propsText} }}`;
303
+ if (existing) {
304
+ const lastAttr$1 = opening.attributes[opening.attributes.length - 1];
305
+ if (lastAttr$1 && lastAttr$1 !== existing && typeof lastAttr$1.end === "number") {
306
+ const attrsAfterStyle = source.slice(existing.end ?? 0, lastAttr$1.end).replace(/^[ \t]+/, "");
307
+ return {
308
+ from: existing.start ?? 0,
309
+ to: lastAttr$1.end,
310
+ text: `${attrsAfterStyle} ${newAttr}`
311
+ };
312
+ }
313
+ return {
314
+ from: existing.start ?? 0,
315
+ to: existing.end ?? 0,
316
+ text: newAttr
317
+ };
318
+ }
319
+ const lastAttr = opening.attributes[opening.attributes.length - 1];
320
+ const at = lastAttr?.end ?? opening.name.end ?? 0;
321
+ return {
322
+ from: at,
323
+ to: at,
324
+ text: ` ${newAttr}`
325
+ };
326
+ }
327
+ function formatJsxText(value) {
328
+ if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString$1(value)}}`;
329
+ return value;
330
+ }
331
+ function meaningfulChildren(parent) {
332
+ return parent.children.filter((c) => {
333
+ if (t$3.isJSXText(c)) return c.value.trim() !== "";
334
+ return true;
335
+ });
336
+ }
337
+ function isOnlyMeaningfulChild(parent, child) {
338
+ const meaningful = meaningfulChildren(parent);
339
+ return meaningful.length === 1 && meaningful[0] === child;
340
+ }
341
+ function wrapSplice(parent, text) {
342
+ const first = parent.children[0];
343
+ const last = parent.children[parent.children.length - 1];
344
+ return {
345
+ from: first.start ?? 0,
346
+ to: last.end ?? 0,
347
+ text
348
+ };
349
+ }
350
+ function splitLinesWithOffsets(value) {
351
+ const lines = [];
352
+ let start = 0;
353
+ for (let i = 0; i < value.length; i++) {
354
+ const ch = value[i];
355
+ if (ch !== "\n" && ch !== "\r") continue;
356
+ lines.push({
357
+ text: value.slice(start, i),
358
+ start
359
+ });
360
+ if (ch === "\r" && value[i + 1] === "\n") i += 1;
361
+ start = i + 1;
362
+ }
363
+ lines.push({
364
+ text: value.slice(start),
365
+ start
366
+ });
367
+ return lines;
368
+ }
369
+ function cleanJsxTextWithOffsets(value) {
370
+ const lines = splitLinesWithOffsets(value);
371
+ let lastNonEmptyLine = 0;
372
+ for (let i = 0; i < lines.length; i++) if (lines[i].text.trim()) lastNonEmptyLine = i;
373
+ let text = "";
374
+ const offsets = [];
375
+ for (let i = 0; i < lines.length; i++) {
376
+ const chars = Array.from(lines[i].text, (ch, j) => ({
377
+ ch: ch === " " ? " " : ch,
378
+ offset: lines[i].start + j
379
+ }));
380
+ let from = 0;
381
+ let to = chars.length;
382
+ if (i !== 0) while (from < to && chars[from].ch === " ") from += 1;
383
+ if (i !== lines.length - 1) while (to > from && chars[to - 1].ch === " ") to -= 1;
384
+ if (from >= to) continue;
385
+ for (const item of chars.slice(from, to)) {
386
+ text += item.ch;
387
+ offsets.push(item.offset);
388
+ }
389
+ if (i !== lastNonEmptyLine) {
390
+ text += " ";
391
+ offsets.push(null);
392
+ }
393
+ }
394
+ return {
395
+ text,
396
+ offsets
397
+ };
398
+ }
399
+ function isJsxBrElement(node) {
400
+ if (!t$3.isJSXElement(node)) return false;
401
+ const name = node.openingElement.name;
402
+ return t$3.isJSXIdentifier(name) && name.name.toLowerCase() === "br";
403
+ }
404
+ function collectTextCandidates(element, out) {
405
+ const meaningful = meaningfulChildren(element);
406
+ const isSole = meaningful.length === 1;
407
+ for (const child of meaningful) if (t$3.isJSXText(child)) {
408
+ const current = child.value.trim();
409
+ if (!current) continue;
410
+ out.push({
411
+ current,
412
+ splice: (v) => isSole ? wrapSplice(element, formatJsxText(v)) : {
413
+ from: child.start ?? 0,
414
+ to: child.end ?? 0,
415
+ text: formatJsxText(v)
416
+ }
417
+ });
418
+ } else if (t$3.isJSXExpressionContainer(child)) {
419
+ const expr = child.expression;
420
+ if (t$3.isStringLiteral(expr) || t$3.isNumericLiteral(expr)) {
421
+ const current = String(expr.value);
422
+ out.push({
423
+ current,
424
+ splice: (v) => isSole ? wrapSplice(element, `{${jsString$1(v)}}`) : {
425
+ from: child.start ?? 0,
426
+ to: child.end ?? 0,
427
+ text: `{${jsString$1(v)}}`
428
+ }
429
+ });
430
+ }
431
+ } else if (t$3.isJSXElement(child) || t$3.isJSXFragment(child)) collectTextCandidates(child, out);
432
+ }
433
+ function collectTextRangeParts(element, out) {
434
+ const parts = [];
435
+ collectTextRangePartsRaw(element, parts);
436
+ out.push(...normalizeTextRangeParts(parts));
437
+ }
438
+ function collectTextRangePartsRaw(element, out) {
439
+ for (const child of element.children) if (t$3.isJSXText(child)) {
440
+ const { text: current, offsets } = cleanJsxTextWithOffsets(child.value);
441
+ if (current) out.push({
442
+ node: child,
443
+ parent: element,
444
+ current,
445
+ raw: child.value,
446
+ text: formatJsxText,
447
+ offsets
448
+ });
449
+ } else if (t$3.isJSXExpressionContainer(child)) {
450
+ const expression = child.expression;
451
+ if (t$3.isStringLiteral(expression) || t$3.isNumericLiteral(expression)) {
452
+ const raw = String(expression.value);
453
+ const current = raw;
454
+ if (current) out.push({
455
+ node: child,
456
+ parent: element,
457
+ current,
458
+ raw,
459
+ text: (value) => `{${jsString$1(value)}}`,
460
+ offsets: Array.from({ length: current.length }, (_, i) => i)
461
+ });
462
+ }
463
+ } else if (isJsxBrElement(child)) out.push({
464
+ node: child,
465
+ current: "\n"
466
+ });
467
+ else if (t$3.isJSXElement(child) || t$3.isJSXFragment(child)) collectTextRangePartsRaw(child, out);
468
+ }
469
+ function normalizeTextRangeParts(parts) {
470
+ return parts.flatMap((part, index) => {
471
+ if (!("raw" in part)) return [part];
472
+ let start = 0;
473
+ let end = part.current.length;
474
+ if (parts[index - 1]?.current === "\n") while (start < end && /\s/.test(part.current[start] ?? "")) start++;
475
+ if (parts[index + 1]?.current === "\n") while (end > start && /\s/.test(part.current[end - 1] ?? "")) end--;
476
+ if (start === 0 && end === part.current.length) return [part];
477
+ if (start >= end) return [];
478
+ return [{
479
+ ...part,
480
+ current: part.current.slice(start, end),
481
+ offsets: part.offsets.slice(start, end)
482
+ }];
483
+ });
484
+ }
485
+ function resetValueForRangeStyle(key) {
486
+ if (key === "fontWeight") return "400";
487
+ if (key === "fontStyle") return "normal";
488
+ return null;
489
+ }
490
+ function styleSpanForText(text, key, value) {
491
+ const styleValue = value ?? resetValueForRangeStyle(key);
492
+ if (styleValue === null) return formatJsxText(text);
493
+ return `<span style={{ ${key}: ${jsString$1(styleValue)} }}>${formatJsxText(text)}</span>`;
494
+ }
495
+ function textRangeContent(parts) {
496
+ return parts.map((part) => part.current).join("");
497
+ }
498
+ function compactText(value) {
499
+ return value.replace(/\s+/g, "");
500
+ }
501
+ function textMatchesExpected(current, expected) {
502
+ return current === expected || compactText(current) === compactText(expected);
503
+ }
504
+ function formatRichText(value, formatText = formatJsxText) {
505
+ return value.split("\n").map((part) => formatText(part)).join("<br />");
506
+ }
507
+ function formatOptionalText(value, formatText = formatJsxText) {
508
+ return value ? formatText(value) : "";
509
+ }
510
+ function textDiff(prevText, nextText) {
511
+ let start = 0;
512
+ while (start < prevText.length && start < nextText.length && prevText[start] === nextText[start]) start += 1;
513
+ let prevEnd = prevText.length;
514
+ let nextEnd = nextText.length;
515
+ while (prevEnd > start && nextEnd > start && prevText[prevEnd - 1] === nextText[nextEnd - 1]) {
516
+ prevEnd -= 1;
517
+ nextEnd -= 1;
518
+ }
519
+ return {
520
+ start,
521
+ end: prevEnd,
522
+ value: nextText.slice(start, nextEnd)
523
+ };
524
+ }
525
+ function textLeafSplice(part, value) {
526
+ const rawRange = textLeafRawRange(part, 0, part.current.length);
527
+ if (!rawRange) return spliceRange(part.node, part.text(value));
528
+ const { rawStart, rawEnd } = rawRange;
529
+ return {
530
+ from: part.node.start ?? 0,
531
+ to: part.node.end ?? 0,
532
+ text: `${part.raw.slice(0, rawStart)}${formatRichText(value, part.text)}${part.raw.slice(rawEnd)}`
533
+ };
534
+ }
535
+ function textLeafRawRange(part, start, end) {
536
+ if (start >= end) return null;
537
+ let first = null;
538
+ let last = null;
539
+ for (let i = start; i < end; i++) {
540
+ const offset = part.offsets[i];
541
+ if (offset === void 0) return null;
542
+ if (offset === null) continue;
543
+ first ??= offset;
544
+ last = offset;
545
+ }
546
+ if (first === null || last === null) return null;
547
+ return {
548
+ rawStart: first,
549
+ rawEnd: last + 1
550
+ };
551
+ }
552
+ function buildTextRangeReplaceSplices(parts, start, end, value) {
553
+ const splices = [];
554
+ let offset = 0;
555
+ let inserted = false;
556
+ for (const part of parts) {
557
+ const partStart = offset;
558
+ const partEnd = partStart + part.current.length;
559
+ offset = partEnd;
560
+ const overlaps = start < partEnd && end > partStart;
561
+ const insertsHere = start === end && !inserted && start >= partStart && start <= partEnd;
562
+ if (!overlaps && !insertsHere) continue;
563
+ if ("raw" in part) {
564
+ const localStart = Math.max(start, partStart) - partStart;
565
+ const localEnd = overlaps ? Math.min(end, partEnd) - partStart : localStart;
566
+ const nextText = `${part.current.slice(0, localStart)}${inserted ? "" : value}${part.current.slice(localEnd)}`;
567
+ splices.push(textLeafSplice(part, nextText));
568
+ } else if (overlaps) splices.push(spliceRange(part.node, inserted ? "" : formatRichText(value)));
569
+ else if (insertsHere) {
570
+ const at = start === partStart ? part.node.start ?? 0 : part.node.end ?? 0;
571
+ splices.push({
572
+ from: at,
573
+ to: at,
574
+ text: formatRichText(value)
575
+ });
576
+ }
577
+ inserted = true;
578
+ }
579
+ if (!inserted && start === end && start === offset) {
580
+ const last = parts[parts.length - 1];
581
+ if (!last) return { error: "element has no editable text" };
582
+ if ("raw" in last) splices.push(textLeafSplice(last, `${last.current}${value}`));
583
+ else splices.push({
584
+ from: last.node.end ?? 0,
585
+ to: last.node.end ?? 0,
586
+ text: formatRichText(value)
587
+ });
588
+ }
589
+ return splices;
590
+ }
591
+ function buildTextContentSplices(element, value, prevText) {
592
+ const parts = [];
593
+ collectTextRangeParts(element, parts);
594
+ const current = textRangeContent(parts);
595
+ if (!textMatchesExpected(current, prevText)) return { error: "no text candidate matches the current value" };
596
+ const diff = textDiff(current, value);
597
+ if (diff.start === diff.end && diff.value === "") return [];
598
+ return buildTextRangeReplaceSplices(parts, diff.start, diff.end, diff.value);
599
+ }
600
+ function buildTextRangeStyleSplices(ast, source, element, start, end, op, prevText) {
601
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start) return { error: "invalid text range" };
602
+ const parts = [];
603
+ collectTextRangeParts(element, parts);
604
+ const current = prevText ?? textRangeContent(parts);
605
+ if (!current) return { error: "element has no editable text" };
606
+ if (end > current.length) return { error: "text range is out of bounds" };
607
+ const renderedText = textRangeContent(parts);
608
+ if (prevText !== void 0 && renderedText !== prevText) {
609
+ if (elementTextCandidateMatches(ast, element, prevText)) {
610
+ const result = buildStyleSplice(source, element, [op]);
611
+ if (result && "error" in result) return result;
612
+ return result ? [result] : [];
613
+ }
614
+ return { error: "no text candidate matches the current value" };
615
+ }
616
+ const splices = [];
617
+ let leafStart = 0;
618
+ for (const leaf of parts) {
619
+ const leafEnd = leafStart + leaf.current.length;
620
+ if (!("raw" in leaf)) {
621
+ leafStart = leafEnd;
622
+ continue;
623
+ }
624
+ const selectedStart = Math.max(start, leafStart);
625
+ const selectedEnd = Math.min(end, leafEnd);
626
+ if (selectedStart >= selectedEnd) {
627
+ leafStart = leafEnd;
628
+ continue;
629
+ }
630
+ if (selectedStart === leafStart && selectedEnd === leafEnd && t$3.isJSXElement(leaf.parent) && leaf.parent !== element && isOnlyMeaningfulChild(leaf.parent, leaf.node)) {
631
+ const result = buildStyleSplice(source, leaf.parent, [op]);
632
+ if (result && "error" in result) return result;
633
+ if (result) splices.push(result);
634
+ leafStart = leafEnd;
635
+ continue;
636
+ }
637
+ const localStart = selectedStart - leafStart;
638
+ const localEnd = selectedEnd - leafStart;
639
+ const rawRange = textLeafRawRange(leaf, localStart, localEnd);
640
+ if (!rawRange) return { error: "text range source mismatch" };
641
+ const raw = leaf.raw;
642
+ const { rawStart, rawEnd } = rawRange;
643
+ const before = raw.slice(0, rawStart);
644
+ const selected = leaf.current.slice(localStart, localEnd);
645
+ const after = raw.slice(rawEnd);
646
+ const beforeText = t$3.isJSXText(leaf.node) ? before : formatOptionalText(before, leaf.text);
647
+ const afterText = t$3.isJSXText(leaf.node) ? after : formatOptionalText(after, leaf.text);
648
+ splices.push(spliceRange(leaf.node, `${beforeText}${styleSpanForText(selected, op.key, op.value)}${afterText}`));
649
+ leafStart = leafEnd;
650
+ }
651
+ return splices.length > 0 ? splices : null;
652
+ }
653
+ function propPassthroughName(element) {
654
+ const meaningful = meaningfulChildren(element);
655
+ if (meaningful.length !== 1) return null;
656
+ const child = meaningful[0];
657
+ if (!t$3.isJSXExpressionContainer(child)) return null;
658
+ return t$3.isIdentifier(child.expression) ? child.expression.name : null;
659
+ }
660
+ function findEnclosingComponent(ast, target) {
661
+ let best = null;
662
+ let bestSize = Number.POSITIVE_INFINITY;
663
+ const targetStart = target.start ?? 0;
664
+ const targetEnd = target.end ?? 0;
665
+ const consider = (name, fn) => {
666
+ if (!/^[A-Z]/.test(name)) return;
667
+ const fnStart = fn.start ?? 0;
668
+ const fnEnd = fn.end ?? 0;
669
+ if (fnStart > targetStart || fnEnd < targetEnd) return;
670
+ const size = fnEnd - fnStart;
671
+ if (size < bestSize) {
672
+ best = {
673
+ name,
674
+ fn
675
+ };
676
+ bestSize = size;
677
+ }
678
+ };
679
+ const visitDecl = (decl) => {
680
+ if (t$3.isFunctionDeclaration(decl) && decl.id) consider(decl.id.name, decl);
681
+ else if (t$3.isVariableDeclaration(decl)) for (const d of decl.declarations) {
682
+ if (!t$3.isVariableDeclarator(d) || !t$3.isIdentifier(d.id) || !d.init) continue;
683
+ if (t$3.isArrowFunctionExpression(d.init) || t$3.isFunctionExpression(d.init)) consider(d.id.name, d.init);
684
+ }
685
+ };
686
+ for (const decl of ast.program.body) {
687
+ visitDecl(decl);
688
+ if (t$3.isExportNamedDeclaration(decl) || t$3.isExportDefaultDeclaration(decl)) {
689
+ const inner = decl.declaration;
690
+ if (inner && (t$3.isStatement(inner) || t$3.isFunctionDeclaration(inner))) visitDecl(inner);
691
+ }
692
+ }
693
+ return best;
694
+ }
695
+ function componentDestructuresProp(fn, propName) {
696
+ if (fn.params.length === 0) return false;
697
+ let first = fn.params[0];
698
+ if (t$3.isAssignmentPattern(first)) first = first.left;
699
+ if (!t$3.isObjectPattern(first)) return false;
700
+ for (const prop of first.properties) {
701
+ if (!t$3.isObjectProperty(prop)) continue;
702
+ if (t$3.isIdentifier(prop.key) && prop.key.name === propName) return true;
703
+ if (t$3.isStringLiteral(prop.key) && prop.key.value === propName) return true;
704
+ }
705
+ return false;
706
+ }
707
+ function collectCallSiteCandidates(ast, componentName) {
708
+ const out = [];
709
+ walkJsx(ast, (n) => {
710
+ if (!t$3.isJSXElement(n)) return;
711
+ const elName = n.openingElement.name;
712
+ if (t$3.isJSXIdentifier(elName) && elName.name === componentName) collectTextCandidates(n, out);
713
+ });
714
+ return out;
715
+ }
716
+ function collectPropCallSiteCandidates(ast, componentName, propName) {
717
+ const out = [];
718
+ walkJsx(ast, (n) => {
719
+ if (!t$3.isJSXElement(n)) return;
720
+ const elName = n.openingElement.name;
721
+ if (!t$3.isJSXIdentifier(elName) || elName.name !== componentName) return;
722
+ const attr = findJsxAttr(n.openingElement, propName);
723
+ if (!attr?.value) return;
724
+ const v = attr.value;
725
+ if (t$3.isStringLiteral(v)) out.push({
726
+ current: v.value,
727
+ splice: (s) => spliceRange(v, formatJsxAttrValue(s))
728
+ });
729
+ else if (t$3.isJSXExpressionContainer(v)) {
730
+ const expr = v.expression;
731
+ if (t$3.isStringLiteral(expr) || t$3.isNumericLiteral(expr)) out.push({
732
+ current: String(expr.value),
733
+ splice: (s) => spliceRange(v, formatJsxAttrValue(s))
734
+ });
735
+ }
736
+ });
737
+ return out;
738
+ }
739
+ function findEnclosingMapCallback(ast, target) {
740
+ let best = null;
741
+ const targetStart = target.start ?? 0;
742
+ const targetEnd = target.end ?? 0;
743
+ walkAll(ast, (node) => {
744
+ if (!t$3.isCallExpression(node)) return;
745
+ const callee = node.callee;
746
+ if (!t$3.isMemberExpression(callee) || callee.computed) return;
747
+ if (!t$3.isIdentifier(callee.property)) return;
748
+ if (callee.property.name !== "map" && callee.property.name !== "flatMap") return;
749
+ const fn = node.arguments[0];
750
+ if (!fn || !t$3.isArrowFunctionExpression(fn) && !t$3.isFunctionExpression(fn)) return;
751
+ const fnStart = fn.start ?? 0;
752
+ const fnEnd = fn.end ?? 0;
753
+ if (fnStart > targetStart || fnEnd < targetEnd) return;
754
+ if (!t$3.isExpression(callee.object)) return;
755
+ const size = fnEnd - fnStart;
756
+ if (!best || size < best.size) best = {
757
+ fn,
758
+ arrayArg: callee.object,
759
+ size
760
+ };
761
+ });
762
+ if (!best) return null;
763
+ const found = best;
764
+ return {
765
+ fn: found.fn,
766
+ arrayArg: found.arrayArg
767
+ };
768
+ }
769
+ function resolveArrayLiteralElements(ast, expr) {
770
+ const dropHoles = (arr) => arr.elements.filter((e) => e != null);
771
+ if (t$3.isArrayExpression(expr)) return dropHoles(expr);
772
+ if (!t$3.isIdentifier(expr)) return null;
773
+ const name = expr.name;
774
+ const useStart = expr.start ?? 0;
775
+ let init = null;
776
+ walkAll(ast, (node) => {
777
+ if (!t$3.isVariableDeclarator(node)) return;
778
+ if (!t$3.isIdentifier(node.id) || node.id.name !== name) return;
779
+ if (!node.init || !t$3.isArrayExpression(node.init)) return;
780
+ if ((node.init.start ?? 0) > useStart) return;
781
+ init = node.init;
782
+ });
783
+ return init ? dropHoles(init) : null;
784
+ }
785
+ function findObjectProperty(obj, name) {
786
+ if (!t$3.isObjectExpression(obj)) return null;
787
+ for (const prop of obj.properties) {
788
+ if (!t$3.isObjectProperty(prop) || prop.computed) continue;
789
+ if (t$3.isIdentifier(prop.key) && prop.key.name === name) return prop;
790
+ if (t$3.isStringLiteral(prop.key) && prop.key.value === name) return prop;
791
+ }
792
+ return null;
793
+ }
794
+ function decodeMapPassthrough(element, callbackParam) {
795
+ const meaningful = meaningfulChildren(element);
796
+ if (meaningful.length !== 1) return null;
797
+ const child = meaningful[0];
798
+ if (!t$3.isJSXExpressionContainer(child)) return null;
799
+ const expr = child.expression;
800
+ if (t$3.isMemberExpression(expr)) {
801
+ if (expr.computed) return null;
802
+ if (!t$3.isIdentifier(expr.object) || !t$3.isIdentifier(expr.property)) return null;
803
+ if (!callbackParam || !t$3.isIdentifier(callbackParam)) return null;
804
+ if (callbackParam.name !== expr.object.name) return null;
805
+ return expr.property.name;
806
+ }
807
+ if (t$3.isIdentifier(expr)) {
808
+ const fieldName = expr.name;
809
+ if (!callbackParam || !t$3.isObjectPattern(callbackParam)) return null;
810
+ for (const prop of callbackParam.properties) {
811
+ if (!t$3.isObjectProperty(prop) || prop.computed) continue;
812
+ if (!t$3.isIdentifier(prop.key) || prop.key.name !== fieldName) continue;
813
+ return t$3.isIdentifier(prop.value) && prop.value.name === fieldName ? fieldName : null;
814
+ }
815
+ }
816
+ return null;
817
+ }
818
+ function collectArrayMapCandidates(ast, element) {
819
+ const ctx = findEnclosingMapCallback(ast, element);
820
+ if (!ctx) return [];
821
+ const fieldName = decodeMapPassthrough(element, ctx.fn.params[0]);
822
+ if (!fieldName) return [];
823
+ const elements = resolveArrayLiteralElements(ast, ctx.arrayArg);
824
+ if (!elements) return [];
825
+ const out = [];
826
+ for (const obj of elements) {
827
+ const prop = findObjectProperty(obj, fieldName);
828
+ if (!prop) continue;
829
+ const v = prop.value;
830
+ if (t$3.isStringLiteral(v)) out.push({
831
+ current: v.value,
832
+ splice: (s) => spliceRange(v, jsString$1(s))
833
+ });
834
+ else if (t$3.isNumericLiteral(v)) out.push({
835
+ current: String(v.value),
836
+ splice: (s) => spliceRange(v, jsString$1(s))
837
+ });
838
+ }
839
+ return out;
840
+ }
841
+ function collectElementTextCandidates(ast, element) {
842
+ const candidates = [];
843
+ collectTextCandidates(element, candidates);
844
+ if (candidates.length === 0) {
845
+ const passthrough = propPassthroughName(element);
846
+ const enclosing = passthrough ? findEnclosingComponent(ast, element) : null;
847
+ if (passthrough === "children" && enclosing) candidates.push(...collectCallSiteCandidates(ast, enclosing.name));
848
+ else if (passthrough && enclosing && componentDestructuresProp(enclosing.fn, passthrough)) candidates.push(...collectPropCallSiteCandidates(ast, enclosing.name, passthrough));
849
+ }
850
+ if (candidates.length === 0) candidates.push(...collectArrayMapCandidates(ast, element));
851
+ return candidates;
852
+ }
853
+ function elementTextCandidateMatches(ast, element, prevText) {
854
+ const norm = prevText.trim();
855
+ return collectElementTextCandidates(ast, element).some((candidate) => candidate.current === norm);
856
+ }
857
+ function buildTextSplice(ast, element, value, prevText) {
858
+ const candidates = collectElementTextCandidates(ast, element);
859
+ if (candidates.length === 0) return { error: "element has no editable text" };
860
+ if (candidates.length === 1) return candidates[0].splice(value);
861
+ if (prevText === void 0) return { error: "element has multiple text candidates; missing prevText" };
862
+ const norm = prevText.trim();
863
+ const matches = candidates.filter((c) => c.current === norm);
864
+ if (matches.length === 0) return { error: "no text candidate matches the current value" };
865
+ if (matches.length > 1) return { error: "multiple text candidates share the same value; cannot disambiguate" };
866
+ return matches[0].splice(value);
867
+ }
868
+ function planAssetImport(ast, assetPath) {
869
+ const imports = findImports$1(ast);
870
+ for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) return {
871
+ identifier: imp.defaultIdent,
872
+ importSplice: null
873
+ };
874
+ const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
875
+ const identifier = safeAssetIdentifier(filename, collectTopLevelIdentifiers(ast));
876
+ const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
877
+ const last = imports[imports.length - 1];
878
+ const insertAt = last ? last.node.end ?? 0 : 0;
879
+ const prefix = last ? "\n" : "";
880
+ return {
881
+ identifier,
882
+ importSplice: {
883
+ from: insertAt,
884
+ to: insertAt,
885
+ text: prefix + importStmt
886
+ }
887
+ };
888
+ }
889
+ function planAssetAttr(ast, element, attr, assetPath) {
890
+ if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
891
+ if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return { error: "asset path must start with ./assets/ or @assets/" };
892
+ const { identifier, importSplice } = planAssetImport(ast, assetPath);
893
+ const opening = element.openingElement;
894
+ const newAttr = `${attr}={${identifier}}`;
895
+ const existing = findJsxAttr(opening, attr);
896
+ const attrSplice = existing ? {
897
+ from: existing.start ?? 0,
898
+ to: existing.end ?? 0,
899
+ text: newAttr
900
+ } : {
901
+ from: opening.name.end ?? 0,
902
+ to: opening.name.end ?? 0,
903
+ text: ` ${newAttr}`
904
+ };
905
+ return {
906
+ importSplice,
907
+ attrSplice
908
+ };
909
+ }
910
+ function planReplacePlaceholder(ast, element, assetPath) {
911
+ const opening = element.openingElement;
912
+ if (!t$3.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
913
+ if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return { error: "asset path must start with ./assets/ or @assets/" };
914
+ const hint = readJsxStringAttr(opening, "hint") ?? "";
915
+ const width = readJsxNumberAttr(opening, "width");
916
+ const height = readJsxNumberAttr(opening, "height");
917
+ const { identifier, importSplice } = planAssetImport(ast, assetPath);
918
+ const styleParts = [];
919
+ if (width != null) styleParts.push(`width: ${width}`);
920
+ else if (height != null) styleParts.push(`width: '100%'`);
921
+ if (height != null) styleParts.push(`height: ${height}`);
922
+ else if (width != null) styleParts.push(`height: '100%'`);
923
+ styleParts.push(`objectFit: 'cover'`);
924
+ styleParts.push(`objectPosition: '50% 50%'`);
925
+ const replacement = `<img src={${identifier}} alt=${jsString$1(hint)} style={{ ${styleParts.join(", ")} }} />`;
926
+ return {
927
+ importSplice,
928
+ elementSplice: spliceRange(element, replacement)
929
+ };
930
+ }
931
+ function applyEdit(source, line, column, ops) {
932
+ if (ops.length === 0) return {
933
+ ok: true,
934
+ source
935
+ };
936
+ const ast = parseSource$1(source);
937
+ if (!ast) return {
938
+ ok: false,
939
+ status: 422,
940
+ error: "could not parse source"
941
+ };
942
+ const element = findElementForEdit(ast, line, column, ops);
943
+ if (!element) return {
944
+ ok: false,
945
+ status: 422,
946
+ error: "no JSX element at location"
947
+ };
948
+ const splices = [];
949
+ const styleOps = ops.flatMap((op) => op.kind === "set-style" ? [{
950
+ key: op.key,
951
+ value: op.value
952
+ }] : []);
953
+ if (styleOps.length > 0) {
954
+ const result = buildStyleSplice(source, element, styleOps);
955
+ if (result && "error" in result) return {
956
+ ok: false,
957
+ status: 422,
958
+ error: result.error
959
+ };
960
+ if (result) splices.push(result);
961
+ }
962
+ for (const op of ops) {
963
+ if (op.kind !== "set-text-range-style") continue;
964
+ const result = buildTextRangeStyleSplices(ast, source, element, op.start, op.end, {
965
+ key: op.key,
966
+ value: op.value
967
+ }, op.prevText);
968
+ if (result && "error" in result) return {
969
+ ok: false,
970
+ status: 422,
971
+ error: result.error
972
+ };
973
+ if (result) splices.push(...result);
974
+ }
975
+ for (const op of ops) {
976
+ if (op.kind !== "set-text") continue;
977
+ if (op.prevText !== void 0 && (op.value.includes("\n") || op.prevText.includes("\n"))) {
978
+ const richResult = buildTextContentSplices(element, op.value, op.prevText);
979
+ if (!("error" in richResult)) {
980
+ splices.push(...richResult);
981
+ continue;
982
+ }
983
+ }
984
+ const result = buildTextSplice(ast, element, op.value, op.prevText);
985
+ if ("error" in result) {
986
+ if (op.prevText === void 0) return {
987
+ ok: false,
988
+ status: 422,
989
+ error: result.error
990
+ };
991
+ const richResult = buildTextContentSplices(element, op.value, op.prevText);
992
+ if ("error" in richResult) return {
993
+ ok: false,
994
+ status: 422,
995
+ error: result.error
996
+ };
997
+ splices.push(...richResult);
998
+ } else splices.push(result);
999
+ }
1000
+ const assetOps = ops.flatMap((op) => op.kind === "set-attr-asset" ? [op] : []);
1001
+ const placeholderOps = ops.flatMap((op) => op.kind === "replace-placeholder-with-image" ? [op] : []);
1002
+ if (assetOps.length > 0 || placeholderOps.length > 0) {
1003
+ const importSplices = [];
1004
+ for (const op of assetOps) {
1005
+ const plan = planAssetAttr(ast, element, op.attr, op.assetPath);
1006
+ if ("error" in plan) return {
1007
+ ok: false,
1008
+ status: 422,
1009
+ error: plan.error
1010
+ };
1011
+ splices.push(plan.attrSplice);
1012
+ if (plan.importSplice) importSplices.push(plan.importSplice);
1013
+ }
1014
+ for (const op of placeholderOps) {
1015
+ const plan = planReplacePlaceholder(ast, element, op.assetPath);
1016
+ if ("error" in plan) return {
1017
+ ok: false,
1018
+ status: 422,
1019
+ error: plan.error
1020
+ };
1021
+ splices.push(plan.elementSplice);
1022
+ if (plan.importSplice) importSplices.push(plan.importSplice);
1023
+ }
1024
+ if (importSplices.length > 0) {
1025
+ const from = importSplices[0].from;
1026
+ const to = importSplices[0].to;
1027
+ const text = importSplices.map((s) => s.text).join("");
1028
+ splices.push({
1029
+ from,
1030
+ to,
1031
+ text
1032
+ });
1033
+ }
1034
+ }
1035
+ if (splices.length === 0) return {
1036
+ ok: true,
1037
+ source
1038
+ };
1039
+ splices.sort((a, b) => b.from - a.from);
1040
+ let next = source;
1041
+ for (const sp of splices) next = next.slice(0, sp.from) + sp.text + next.slice(sp.to);
1042
+ if (!parseSource$1(next)) return {
1043
+ ok: false,
1044
+ status: 422,
1045
+ error: "edit would produce invalid source"
1046
+ };
1047
+ return {
1048
+ ok: true,
1049
+ source: next
1050
+ };
1051
+ }
1052
+
1053
+ //#endregion
1054
+ //#region src/editing/revert-asset.ts
1055
+ function collectImgSrcUses(ast, identifier) {
1056
+ const uses = [];
1057
+ walkJsx(ast, (n) => {
1058
+ if (!t$2.isJSXElement(n)) return;
1059
+ const opening = n.openingElement;
1060
+ if (!t$2.isJSXIdentifier(opening.name) || opening.name.name !== "img") return;
1061
+ const src = findJsxAttr(opening, "src");
1062
+ if (!src?.value) return;
1063
+ if (!t$2.isJSXExpressionContainer(src.value)) return;
1064
+ const expr = src.value.expression;
1065
+ if (!t$2.isIdentifier(expr) || expr.name !== identifier) return;
1066
+ uses.push({
1067
+ element: n,
1068
+ identNode: expr
1069
+ });
1070
+ });
1071
+ return uses;
1072
+ }
1073
+ function readStyleNumericDim(opening, key) {
1074
+ const style = findJsxAttr(opening, "style");
1075
+ if (!style?.value) return null;
1076
+ if (!t$2.isJSXExpressionContainer(style.value)) return null;
1077
+ const obj = style.value.expression;
1078
+ if (!t$2.isObjectExpression(obj)) return null;
1079
+ for (const prop of obj.properties) {
1080
+ if (!t$2.isObjectProperty(prop)) continue;
1081
+ if (prop.computed) continue;
1082
+ const k = prop.key;
1083
+ const keyName = t$2.isIdentifier(k) ? k.name : t$2.isStringLiteral(k) ? k.value : null;
1084
+ if (keyName !== key) continue;
1085
+ const v = prop.value;
1086
+ if (t$2.isNumericLiteral(v) && Number.isFinite(v.value)) return v.value;
1087
+ return null;
1088
+ }
1089
+ return null;
1090
+ }
1091
+ function buildPlaceholderReplacement(hint, width, height) {
1092
+ const parts = [`hint=${formatJsxAttrValue(hint)}`];
1093
+ if (width != null) parts.push(`width={${width}}`);
1094
+ if (height != null) parts.push(`height={${height}}`);
1095
+ return `<ImagePlaceholder ${parts.join(" ")} />`;
1096
+ }
1097
+ function planEnsureImagePlaceholderImport(ast) {
1098
+ const readKind = (n) => n.importKind === "type";
1099
+ const imports = findImports$1(ast);
1100
+ let valueImport = null;
1101
+ for (const imp of imports) {
1102
+ if (imp.source !== "@autono/open-pages") continue;
1103
+ const declIsTypeOnly = readKind(imp.node);
1104
+ for (const spec of imp.node.specifiers) {
1105
+ if (!t$2.isImportSpecifier(spec)) continue;
1106
+ const imported = spec.imported;
1107
+ const name = t$2.isIdentifier(imported) ? imported.name : imported.value;
1108
+ if (name !== "ImagePlaceholder") continue;
1109
+ const specIsTypeOnly = readKind(spec) || declIsTypeOnly;
1110
+ if (!specIsTypeOnly) return null;
1111
+ }
1112
+ if (!declIsTypeOnly && !valueImport) valueImport = imp;
1113
+ }
1114
+ if (valueImport) {
1115
+ const node = valueImport.node;
1116
+ const lastSpec = node.specifiers[node.specifiers.length - 1];
1117
+ if (lastSpec && t$2.isImportSpecifier(lastSpec)) {
1118
+ const insertAt$1 = lastSpec.end ?? 0;
1119
+ return {
1120
+ from: insertAt$1,
1121
+ to: insertAt$1,
1122
+ text: ", ImagePlaceholder"
1123
+ };
1124
+ }
1125
+ if (lastSpec && t$2.isImportDefaultSpecifier(lastSpec)) {
1126
+ const insertAt$1 = lastSpec.end ?? 0;
1127
+ return {
1128
+ from: insertAt$1,
1129
+ to: insertAt$1,
1130
+ text: ", { ImagePlaceholder }"
1131
+ };
1132
+ }
1133
+ const insertAt = (node.source.start ?? 0) - 5;
1134
+ return {
1135
+ from: insertAt,
1136
+ to: insertAt,
1137
+ text: "{ ImagePlaceholder } "
1138
+ };
1139
+ }
1140
+ return {
1141
+ from: 0,
1142
+ to: 0,
1143
+ text: "import { ImagePlaceholder } from '@autono/open-pages';\n"
1144
+ };
1145
+ }
1146
+ function findAssetUsages(source, assetPath) {
1147
+ const ast = parseSource$1(source);
1148
+ if (!ast) return 0;
1149
+ const imports = findImports$1(ast);
1150
+ const target = imports.find((imp) => imp.source === assetPath && imp.defaultIdent);
1151
+ if (!target?.defaultIdent) return 0;
1152
+ return collectImgSrcUses(ast, target.defaultIdent).length;
1153
+ }
1154
+ function findReferencedAssets(source, assetPaths) {
1155
+ const referenced = new Set();
1156
+ if (assetPaths.length === 0) return referenced;
1157
+ const ast = parseSource$1(source);
1158
+ if (!ast) return referenced;
1159
+ const wanted = new Set(assetPaths);
1160
+ const identToPath = new Map();
1161
+ const importLocals = new Set();
1162
+ for (const imp of findImports$1(ast)) {
1163
+ if (!imp.defaultIdent) continue;
1164
+ if (!wanted.has(imp.source)) continue;
1165
+ identToPath.set(imp.defaultIdent, imp.source);
1166
+ for (const spec of imp.node.specifiers) if (t$2.isImportDefaultSpecifier(spec) && spec.local.name === imp.defaultIdent) importLocals.add(spec.local);
1167
+ }
1168
+ if (identToPath.size === 0) return referenced;
1169
+ walkAll(ast, (n) => {
1170
+ if (!t$2.isIdentifier(n)) return;
1171
+ const p = identToPath.get(n.name);
1172
+ if (!p) return;
1173
+ if (importLocals.has(n)) return;
1174
+ referenced.add(p);
1175
+ });
1176
+ return referenced;
1177
+ }
1178
+ function applyRevertAsset(source, assetPath) {
1179
+ const ast = parseSource$1(source);
1180
+ if (!ast) return {
1181
+ ok: false,
1182
+ status: 422,
1183
+ error: "could not parse source"
1184
+ };
1185
+ const imports = findImports$1(ast);
1186
+ const target = imports.find((imp) => imp.source === assetPath && imp.defaultIdent);
1187
+ if (!target?.defaultIdent) return {
1188
+ ok: true,
1189
+ source
1190
+ };
1191
+ const identifier = target.defaultIdent;
1192
+ const importLocal = (() => {
1193
+ for (const spec of target.node.specifiers) if (t$2.isImportDefaultSpecifier(spec) && spec.local.name === identifier) return spec.local;
1194
+ return null;
1195
+ })();
1196
+ const imgUses = collectImgSrcUses(ast, identifier);
1197
+ const allowed = new Set(imgUses.map((u) => u.identNode));
1198
+ if (importLocal) allowed.add(importLocal);
1199
+ let foreign = false;
1200
+ walkAll(ast, (n) => {
1201
+ if (!t$2.isIdentifier(n) || n.name !== identifier) return;
1202
+ if (!allowed.has(n)) foreign = true;
1203
+ });
1204
+ if (foreign) return {
1205
+ ok: false,
1206
+ status: 422,
1207
+ error: `cannot revert: '${identifier}' is referenced outside <img src={${identifier}}>`
1208
+ };
1209
+ const splices = [];
1210
+ for (const use of imgUses) {
1211
+ const opening = use.element.openingElement;
1212
+ const hint = readJsxStringAttr(opening, "alt") ?? "";
1213
+ const width = readStyleNumericDim(opening, "width");
1214
+ const height = readStyleNumericDim(opening, "height");
1215
+ splices.push(spliceRange(use.element, buildPlaceholderReplacement(hint, width, height)));
1216
+ }
1217
+ const importNode = target.node;
1218
+ const importFrom = importNode.start ?? 0;
1219
+ let importTo = importNode.end ?? 0;
1220
+ if (source[importTo] === "\n") importTo += 1;
1221
+ splices.push({
1222
+ from: importFrom,
1223
+ to: importTo,
1224
+ text: ""
1225
+ });
1226
+ const ensureSplice = planEnsureImagePlaceholderImport(ast);
1227
+ if (ensureSplice) splices.push(ensureSplice);
1228
+ if (splices.length === 0) return {
1229
+ ok: true,
1230
+ source
1231
+ };
1232
+ splices.sort((a, b) => b.from - a.from);
1233
+ let next = source;
1234
+ for (const sp of splices) next = next.slice(0, sp.from) + sp.text + next.slice(sp.to);
1235
+ if (!parseSource$1(next)) return {
1236
+ ok: false,
1237
+ status: 422,
1238
+ error: "edit would produce invalid source"
1239
+ };
1240
+ return {
1241
+ ok: true,
1242
+ source: next
1243
+ };
1244
+ }
1245
+
1246
+ //#endregion
1247
+ //#region src/files/assets.ts
1248
+ const GLOBAL_SCOPE = "@global";
1249
+ const ASSET_MAX_BYTES = 25 * 1024 * 1024;
1250
+ const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
1251
+ const MIME_BY_EXT = {
1252
+ png: "image/png",
1253
+ jpg: "image/jpeg",
1254
+ jpeg: "image/jpeg",
1255
+ gif: "image/gif",
1256
+ svg: "image/svg+xml",
1257
+ webp: "image/webp",
1258
+ avif: "image/avif",
1259
+ ico: "image/x-icon",
1260
+ mp4: "video/mp4",
1261
+ webm: "video/webm",
1262
+ mov: "video/quicktime",
1263
+ woff: "font/woff",
1264
+ woff2: "font/woff2",
1265
+ ttf: "font/ttf",
1266
+ otf: "font/otf",
1267
+ json: "application/json",
1268
+ txt: "text/plain; charset=utf-8",
1269
+ md: "text/markdown; charset=utf-8"
1270
+ };
1271
+ function mimeForFilename(name) {
1272
+ const dot = name.lastIndexOf(".");
1273
+ if (dot < 0) return "application/octet-stream";
1274
+ const ext = name.slice(dot + 1).toLowerCase();
1275
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
1276
+ }
1277
+ function assetCreatedAt(birthtimeMs, mtimeMs) {
1278
+ return Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : mtimeMs;
1279
+ }
1280
+ function validateAssetName(v) {
1281
+ if (typeof v !== "string") return null;
1282
+ const trimmed = v.trim();
1283
+ if (trimmed.length < 1 || trimmed.length > 120) return null;
1284
+ if (ASSET_FORBIDDEN_RE.test(trimmed)) return null;
1285
+ if (trimmed.startsWith(".") || trimmed.startsWith("~")) return null;
1286
+ if (trimmed === ".." || trimmed.split(/[/\\]/).includes("..")) return null;
1287
+ const dot = trimmed.lastIndexOf(".");
1288
+ if (dot <= 0 || dot === trimmed.length - 1) return null;
1289
+ return trimmed;
1290
+ }
1291
+ function resolveAssetsDir(pagesRoot, pageId) {
1292
+ if (!PAGE_ID_RE.test(pageId)) return null;
1293
+ const docDir = path.resolve(pagesRoot, pageId);
1294
+ if (!docDir.startsWith(pagesRoot + path.sep)) return null;
1295
+ const assetsDir = path.resolve(docDir, "assets");
1296
+ if (assetsDir !== path.join(docDir, "assets")) return null;
1297
+ return assetsDir;
1298
+ }
1299
+ function resolveAssetFile(pagesRoot, pageId, filename) {
1300
+ const assetsDir = resolveAssetsDir(pagesRoot, pageId);
1301
+ if (!assetsDir) return null;
1302
+ if (!validateAssetName(filename)) return null;
1303
+ const file = path.resolve(assetsDir, filename);
1304
+ if (!file.startsWith(assetsDir + path.sep)) return null;
1305
+ return file;
1306
+ }
1307
+ function resolveScopedAssetsDir(pagesRoot, globalAssetsRoot, scope) {
1308
+ if (scope === GLOBAL_SCOPE) return globalAssetsRoot;
1309
+ return resolveAssetsDir(pagesRoot, scope);
1310
+ }
1311
+ function resolveScopedAssetFile(pagesRoot, globalAssetsRoot, scope, filename) {
1312
+ if (scope === GLOBAL_SCOPE) {
1313
+ if (!validateAssetName(filename)) return null;
1314
+ const file = path.resolve(globalAssetsRoot, filename);
1315
+ if (!file.startsWith(globalAssetsRoot + path.sep)) return null;
1316
+ return file;
1317
+ }
1318
+ return resolveAssetFile(pagesRoot, scope, filename);
1319
+ }
1320
+
1321
+ //#endregion
1322
+ //#region src/http/request-guard.ts
1323
+ function firstHeaderValue(value) {
1324
+ if (Array.isArray(value)) return value[0] ?? null;
1325
+ return value ?? null;
1326
+ }
1327
+ function headerValue(req, name) {
1328
+ return firstHeaderValue(req.headers[name.toLowerCase()])?.trim() ?? null;
1329
+ }
1330
+ function firstCommaToken(value) {
1331
+ if (!value) return null;
1332
+ const [first] = value.split(",", 1);
1333
+ return first?.trim() || null;
1334
+ }
1335
+ function requestProto(req) {
1336
+ const forwarded = firstCommaToken(headerValue(req, "x-forwarded-proto"))?.toLowerCase();
1337
+ if (forwarded === "http" || forwarded === "https") return forwarded;
1338
+ return "encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
1339
+ }
1340
+ function normalizedOrigin(origin) {
1341
+ try {
1342
+ const url = new URL(origin);
1343
+ return `${url.protocol}//${url.host}`.toLowerCase();
1344
+ } catch {
1345
+ return null;
1346
+ }
1347
+ }
1348
+ function validateMutationRequest(req, opts = {}) {
1349
+ if (opts.requireJsonBody) {
1350
+ const contentType = headerValue(req, "content-type")?.toLowerCase();
1351
+ if (!contentType || !contentType.startsWith("application/json")) return {
1352
+ ok: false,
1353
+ status: 415,
1354
+ error: "content-type must be application/json"
1355
+ };
1356
+ }
1357
+ const fetchSite = firstCommaToken(headerValue(req, "sec-fetch-site"))?.toLowerCase();
1358
+ if (fetchSite === "cross-site") return {
1359
+ ok: false,
1360
+ status: 403,
1361
+ error: "cross-site request blocked"
1362
+ };
1363
+ const originRaw = headerValue(req, "origin");
1364
+ if (!originRaw) return { ok: true };
1365
+ if (originRaw.toLowerCase() === "null") return {
1366
+ ok: false,
1367
+ status: 403,
1368
+ error: "opaque origin is not allowed"
1369
+ };
1370
+ const actualOrigin = normalizedOrigin(originRaw);
1371
+ if (!actualOrigin) return {
1372
+ ok: false,
1373
+ status: 403,
1374
+ error: "invalid origin header"
1375
+ };
1376
+ const host = firstCommaToken(headerValue(req, "x-forwarded-host")) ?? headerValue(req, "host");
1377
+ if (!host) return {
1378
+ ok: false,
1379
+ status: 400,
1380
+ error: "missing host header"
1381
+ };
1382
+ const expectedOrigin = `${requestProto(req)}://${host}`.toLowerCase();
1383
+ if (actualOrigin !== expectedOrigin) return {
1384
+ ok: false,
1385
+ status: 403,
1386
+ error: "origin mismatch"
1387
+ };
1388
+ return { ok: true };
1389
+ }
1390
+
1391
+ //#endregion
1392
+ //#region src/vite/routes/context.ts
1393
+ function makeContext(opts) {
1394
+ const userCwd = opts.userCwd;
1395
+ const pagesDir = opts.pagesDir ?? "pages";
1396
+ const assetsDir = opts.assetsDir ?? "assets";
1397
+ const pagesRoot = path.resolve(userCwd, pagesDir);
1398
+ const globalAssetsRoot = path.resolve(userCwd, assetsDir);
1399
+ const manifestPath = path.join(pagesRoot, ".folders.json");
1400
+ return {
1401
+ userCwd,
1402
+ pagesDir,
1403
+ pagesRoot,
1404
+ globalAssetsRoot,
1405
+ manifestPath,
1406
+ coreVersion: opts.coreVersion
1407
+ };
1408
+ }
1409
+ async function readBody(req) {
1410
+ return await new Promise((resolve, reject) => {
1411
+ const chunks = [];
1412
+ req.on("data", (c) => chunks.push(c));
1413
+ req.on("end", () => {
1414
+ const raw = Buffer.concat(chunks).toString("utf8");
1415
+ if (!raw) return resolve({});
1416
+ try {
1417
+ resolve(JSON.parse(raw));
1418
+ } catch (e) {
1419
+ reject(e);
1420
+ }
1421
+ });
1422
+ req.on("error", reject);
1423
+ });
1424
+ }
1425
+ function json(res, status, body) {
1426
+ res.statusCode = status;
1427
+ res.setHeader("content-type", "application/json");
1428
+ res.end(JSON.stringify(body));
1429
+ }
1430
+ function resolvePagePath(userCwd, pagesDir, pageId) {
1431
+ if (!PAGE_ID_RE.test(pageId)) return null;
1432
+ const pagesRoot = path.resolve(userCwd, pagesDir);
1433
+ const full = path.resolve(pagesRoot, pageId, "index.tsx");
1434
+ if (!full.startsWith(pagesRoot + path.sep)) return null;
1435
+ return full;
1436
+ }
1437
+ function resolvePageEntryPath(ctx, pageId) {
1438
+ return resolvePagePath(ctx.userCwd, ctx.pagesDir, pageId);
1439
+ }
1440
+
1441
+ //#endregion
1442
+ //#region src/vite/routes/assets.ts
1443
+ function registerAssetRoutes(server, ctx) {
1444
+ server.middlewares.use("/__assets", async (req, res, next) => {
1445
+ const url = new URL(req.url ?? "/", "http://local");
1446
+ const method = req.method ?? "GET";
1447
+ try {
1448
+ const listMatch = url.pathname.match(/^\/([^/]+)\/?$/);
1449
+ const fileMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)$/);
1450
+ const usagesMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)\/usages$/);
1451
+ if (usagesMatch && method === "GET") {
1452
+ const scope = usagesMatch[1];
1453
+ const filename = decodeURIComponent(usagesMatch[2]);
1454
+ if (!validateAssetName(filename)) return json(res, 400, { error: "invalid path" });
1455
+ const isGlobal = scope === GLOBAL_SCOPE;
1456
+ const assetPath = isGlobal ? `@assets/${filename}` : `./assets/${filename}`;
1457
+ let pageIds;
1458
+ if (isGlobal) try {
1459
+ const entries = await fs.readdir(ctx.pagesRoot, { withFileTypes: true });
1460
+ pageIds = entries.filter((e) => e.isDirectory() && PAGE_ID_RE.test(e.name)).map((e) => e.name);
1461
+ } catch {
1462
+ pageIds = [];
1463
+ }
1464
+ else {
1465
+ if (!PAGE_ID_RE.test(scope)) return json(res, 400, { error: "invalid pageId" });
1466
+ pageIds = [scope];
1467
+ }
1468
+ const usages = [];
1469
+ let totalCount = 0;
1470
+ for (const sid of pageIds) {
1471
+ const entry = resolvePageEntry(ctx.pagesRoot, sid);
1472
+ if (!entry) continue;
1473
+ let source;
1474
+ try {
1475
+ source = await fs.readFile(entry, "utf8");
1476
+ } catch {
1477
+ continue;
1478
+ }
1479
+ const count = findAssetUsages(source, assetPath);
1480
+ if (count > 0) {
1481
+ usages.push({
1482
+ pageId: sid,
1483
+ count
1484
+ });
1485
+ totalCount += count;
1486
+ }
1487
+ }
1488
+ return json(res, 200, {
1489
+ usages,
1490
+ totalCount
1491
+ });
1492
+ }
1493
+ if (listMatch && method === "GET") {
1494
+ const pageId = listMatch[1];
1495
+ const scopedDir = resolveScopedAssetsDir(ctx.pagesRoot, ctx.globalAssetsRoot, pageId);
1496
+ if (!scopedDir) return json(res, 400, { error: "invalid pageId" });
1497
+ let entries;
1498
+ try {
1499
+ entries = await fs.readdir(scopedDir);
1500
+ } catch (err) {
1501
+ if (err.code === "ENOENT") return json(res, 200, { assets: [] });
1502
+ throw err;
1503
+ }
1504
+ const assets = [];
1505
+ for (const name of entries) {
1506
+ if (!validateAssetName(name)) continue;
1507
+ const stat = await fs.stat(path.join(scopedDir, name));
1508
+ if (!stat.isFile()) continue;
1509
+ assets.push({
1510
+ name,
1511
+ size: stat.size,
1512
+ createdAt: assetCreatedAt(stat.birthtimeMs, stat.mtimeMs),
1513
+ mtime: stat.mtimeMs,
1514
+ mime: mimeForFilename(name),
1515
+ url: `/__assets/${pageId}/${encodeURIComponent(name)}`,
1516
+ unused: true
1517
+ });
1518
+ }
1519
+ assets.sort((a, b) => a.name.localeCompare(b.name));
1520
+ if (assets.length > 0) {
1521
+ const isGlobal = pageId === GLOBAL_SCOPE;
1522
+ let scanIds;
1523
+ if (isGlobal) try {
1524
+ const dirs = await fs.readdir(ctx.pagesRoot, { withFileTypes: true });
1525
+ scanIds = dirs.filter((e) => e.isDirectory() && PAGE_ID_RE.test(e.name)).map((e) => e.name);
1526
+ } catch {
1527
+ scanIds = [];
1528
+ }
1529
+ else scanIds = PAGE_ID_RE.test(pageId) ? [pageId] : [];
1530
+ const paths = assets.map((a) => isGlobal ? `@assets/${a.name}` : `./assets/${a.name}`);
1531
+ const pathToAsset = new Map(paths.map((p, i) => [p, assets[i]]));
1532
+ for (const sid of scanIds) {
1533
+ const entry = resolvePageEntry(ctx.pagesRoot, sid);
1534
+ if (!entry) continue;
1535
+ let source;
1536
+ try {
1537
+ source = await fs.readFile(entry, "utf8");
1538
+ } catch {
1539
+ continue;
1540
+ }
1541
+ for (const p of findReferencedAssets(source, paths)) {
1542
+ const a = pathToAsset.get(p);
1543
+ if (a) a.unused = false;
1544
+ }
1545
+ }
1546
+ }
1547
+ return json(res, 200, { assets });
1548
+ }
1549
+ if (fileMatch) {
1550
+ const pageId = fileMatch[1];
1551
+ const filename = decodeURIComponent(fileMatch[2]);
1552
+ const file = resolveScopedAssetFile(ctx.pagesRoot, ctx.globalAssetsRoot, pageId, filename);
1553
+ if (!file) return json(res, 400, { error: "invalid path" });
1554
+ if (method === "GET") try {
1555
+ const buf = await fs.readFile(file);
1556
+ res.statusCode = 200;
1557
+ res.setHeader("content-type", mimeForFilename(filename));
1558
+ res.setHeader("cache-control", "no-store");
1559
+ res.end(buf);
1560
+ return;
1561
+ } catch (err) {
1562
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
1563
+ throw err;
1564
+ }
1565
+ if (method === "POST") {
1566
+ const requestCheck = validateMutationRequest(req);
1567
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1568
+ const overwrite = url.searchParams.get("overwrite") === "1";
1569
+ const lenHeader = req.headers["content-length"];
1570
+ const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
1571
+ if (Number.isFinite(len) && len > ASSET_MAX_BYTES) return json(res, 413, { error: "file too large" });
1572
+ if (!overwrite) try {
1573
+ await fs.access(file);
1574
+ return json(res, 409, { error: "asset exists" });
1575
+ } catch {}
1576
+ const scopedDir = resolveScopedAssetsDir(ctx.pagesRoot, ctx.globalAssetsRoot, pageId);
1577
+ if (!scopedDir) return json(res, 400, { error: "invalid pageId" });
1578
+ await fs.mkdir(scopedDir, { recursive: true });
1579
+ const chunks = [];
1580
+ let total = 0;
1581
+ let oversized = false;
1582
+ await new Promise((resolve, reject) => {
1583
+ req.on("data", (c) => {
1584
+ total += c.length;
1585
+ if (total > ASSET_MAX_BYTES) {
1586
+ oversized = true;
1587
+ req.destroy();
1588
+ return;
1589
+ }
1590
+ chunks.push(c);
1591
+ });
1592
+ req.on("end", () => resolve());
1593
+ req.on("error", reject);
1594
+ });
1595
+ if (oversized) return json(res, 413, { error: "file too large" });
1596
+ await fs.writeFile(file, Buffer.concat(chunks));
1597
+ const stat = await fs.stat(file);
1598
+ return json(res, 200, {
1599
+ ok: true,
1600
+ name: filename,
1601
+ size: stat.size,
1602
+ createdAt: assetCreatedAt(stat.birthtimeMs, stat.mtimeMs),
1603
+ mtime: stat.mtimeMs,
1604
+ mime: mimeForFilename(filename),
1605
+ url: `/__assets/${pageId}/${encodeURIComponent(filename)}`
1606
+ });
1607
+ }
1608
+ if (method === "PATCH") {
1609
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1610
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1611
+ const body = await readBody(req);
1612
+ const target = validateAssetName(body.name);
1613
+ if (!target) return json(res, 400, { error: "invalid name" });
1614
+ if (target === filename) return json(res, 200, {
1615
+ ok: true,
1616
+ name: filename
1617
+ });
1618
+ const dest = resolveScopedAssetFile(ctx.pagesRoot, ctx.globalAssetsRoot, pageId, target);
1619
+ if (!dest) return json(res, 400, { error: "invalid name" });
1620
+ try {
1621
+ await fs.access(dest);
1622
+ return json(res, 409, { error: "target exists" });
1623
+ } catch {}
1624
+ try {
1625
+ await fs.rename(file, dest);
1626
+ } catch (err) {
1627
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
1628
+ throw err;
1629
+ }
1630
+ return json(res, 200, {
1631
+ ok: true,
1632
+ name: target
1633
+ });
1634
+ }
1635
+ if (method === "DELETE") {
1636
+ const requestCheck = validateMutationRequest(req);
1637
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1638
+ try {
1639
+ await fs.unlink(file);
1640
+ } catch (err) {
1641
+ if (err.code === "ENOENT") return json(res, 404, { error: "asset not found" });
1642
+ throw err;
1643
+ }
1644
+ return json(res, 200, { ok: true });
1645
+ }
1646
+ }
1647
+ return next();
1648
+ } catch (err) {
1649
+ json(res, 500, { error: String(err.message ?? err) });
1650
+ }
1651
+ });
1652
+ }
1653
+
1654
+ //#endregion
1655
+ //#region src/editing/comments.ts
1656
+ const MARKER_RE = /\{\/\*\s*@page-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/g;
1657
+ function b64urlEncode(s) {
1658
+ return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1659
+ }
1660
+ function b64urlDecode(s) {
1661
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
1662
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64").toString("utf8");
1663
+ }
1664
+ function parseMarkers(source) {
1665
+ const comments = [];
1666
+ const lines = source.split("\n");
1667
+ for (let i = 0; i < lines.length; i++) {
1668
+ const line = lines[i];
1669
+ MARKER_RE.lastIndex = 0;
1670
+ const m = MARKER_RE.exec(line);
1671
+ if (!m) continue;
1672
+ const [, id, ts, textB64] = m;
1673
+ try {
1674
+ const payload = JSON.parse(b64urlDecode(textB64));
1675
+ comments.push({
1676
+ id,
1677
+ line: i + 1,
1678
+ ts,
1679
+ note: payload.note,
1680
+ hint: payload.hint
1681
+ });
1682
+ } catch {}
1683
+ }
1684
+ return comments;
1685
+ }
1686
+ function newCommentId() {
1687
+ return `c-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
1688
+ }
1689
+ function markerDeleteRegex(id) {
1690
+ return new RegExp(`\\{\\/\\*\\s*@page-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
1691
+ }
1692
+ function lineToOffset(source, line) {
1693
+ let off = 0;
1694
+ for (let l = 1; l < line; l++) {
1695
+ const nl = source.indexOf("\n", off);
1696
+ if (nl === -1) return source.length;
1697
+ off = nl + 1;
1698
+ }
1699
+ return off;
1700
+ }
1701
+ function lineIndent(source, lineNumber) {
1702
+ const start = lineToOffset(source, lineNumber);
1703
+ const m = source.slice(start, start + 200).match(/^[ \t]*/);
1704
+ return m?.[0] ?? "";
1705
+ }
1706
+ function findJsxAncestors(ast, line, column) {
1707
+ const hits = [];
1708
+ walkJsx(ast, (n) => {
1709
+ if (!n.loc || !t$1.isJSXElement(n) && !t$1.isJSXFragment(n)) return;
1710
+ const s = n.loc.start;
1711
+ const e = n.loc.end;
1712
+ const afterStart = line > s.line || line === s.line && column >= s.column;
1713
+ const beforeEnd = line < e.line || line === e.line && column < e.column;
1714
+ if (afterStart && beforeEnd) hits.push({
1715
+ node: n,
1716
+ size: (n.end ?? 0) - (n.start ?? 0)
1717
+ });
1718
+ });
1719
+ hits.sort((a, b) => a.size - b.size);
1720
+ return hits.map((h) => h.node);
1721
+ }
1722
+ function planInsertion(source, target) {
1723
+ if (t$1.isJSXFragment(target)) {
1724
+ const opening = target.openingFragment;
1725
+ const startLine = target.loc?.start.line ?? 1;
1726
+ return {
1727
+ offset: opening.end ?? 0,
1728
+ indent: `${lineIndent(source, startLine)} `
1729
+ };
1730
+ }
1731
+ if (t$1.isJSXElement(target)) {
1732
+ const opening = target.openingElement;
1733
+ if (opening.selfClosing) return null;
1734
+ const startLine = target.loc?.start.line ?? 1;
1735
+ return {
1736
+ offset: opening.end ?? 0,
1737
+ indent: `${lineIndent(source, startLine)} `
1738
+ };
1739
+ }
1740
+ return null;
1741
+ }
1742
+ function findInsertion(source, line, column) {
1743
+ const ast = parseSource$1(source);
1744
+ if (!ast) return null;
1745
+ const col = column ?? 0;
1746
+ const ancestors = findJsxAncestors(ast, line, col);
1747
+ for (const node of ancestors) {
1748
+ const plan = planInsertion(source, node);
1749
+ if (plan) return plan;
1750
+ }
1751
+ return null;
1752
+ }
1753
+ function offsetToLine(source, offset) {
1754
+ let line = 1;
1755
+ for (let i = 0; i < offset && i < source.length; i++) if (source[i] === "\n") line++;
1756
+ return line;
1757
+ }
1758
+
1759
+ //#endregion
1760
+ //#region src/vite/routes/comments.ts
1761
+ function registerCommentRoutes(server, ctx) {
1762
+ server.middlewares.use("/__comments", async (req, res, next) => {
1763
+ const url = new URL(req.url ?? "/", "http://local");
1764
+ const method = req.method ?? "GET";
1765
+ try {
1766
+ if (method === "GET" && url.pathname === "/") {
1767
+ const pageId = url.searchParams.get("pageId") ?? "";
1768
+ const file = resolvePageEntryPath(ctx, pageId);
1769
+ if (!file) return json(res, 400, { error: "invalid pageId" });
1770
+ let source;
1771
+ try {
1772
+ source = await fs.readFile(file, "utf8");
1773
+ } catch {
1774
+ return json(res, 404, { error: "page not found" });
1775
+ }
1776
+ return json(res, 200, { comments: parseMarkers(source) });
1777
+ }
1778
+ if (method === "POST" && url.pathname === "/add") {
1779
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1780
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1781
+ const body = await readBody(req);
1782
+ const pageId = body.pageId ?? "";
1783
+ const file = resolvePageEntryPath(ctx, pageId);
1784
+ if (!file) return json(res, 400, { error: "invalid pageId" });
1785
+ if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
1786
+ if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
1787
+ let source;
1788
+ try {
1789
+ source = await fs.readFile(file, "utf8");
1790
+ } catch {
1791
+ return json(res, 404, { error: "page not found" });
1792
+ }
1793
+ const plan = findInsertion(source, body.line, body.column);
1794
+ if (!plan) return json(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
1795
+ const id = newCommentId();
1796
+ const ts = new Date().toISOString();
1797
+ const payload = b64urlEncode(JSON.stringify({
1798
+ note: body.text,
1799
+ hint: body.hint
1800
+ }));
1801
+ const marker = `\n${plan.indent}{/* @page-comment id="${id}" ts="${ts}" text="${payload}" */}`;
1802
+ const next$1 = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
1803
+ await fs.writeFile(file, next$1, "utf8");
1804
+ const markerLine = offsetToLine(next$1, plan.offset + 1);
1805
+ return json(res, 200, {
1806
+ id,
1807
+ line: markerLine
1808
+ });
1809
+ }
1810
+ if (method === "DELETE" && url.pathname.startsWith("/")) {
1811
+ const requestCheck = validateMutationRequest(req);
1812
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1813
+ const id = url.pathname.slice(1);
1814
+ if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
1815
+ const pageId = url.searchParams.get("pageId") ?? "";
1816
+ const file = resolvePageEntryPath(ctx, pageId);
1817
+ if (!file) return json(res, 400, { error: "invalid pageId" });
1818
+ let source;
1819
+ try {
1820
+ source = await fs.readFile(file, "utf8");
1821
+ } catch {
1822
+ return json(res, 404, { error: "page not found" });
1823
+ }
1824
+ const lines = source.split("\n");
1825
+ const idRe = markerDeleteRegex(id);
1826
+ const hit = lines.findIndex((l) => idRe.test(l));
1827
+ if (hit === -1) return json(res, 404, { error: "marker not found" });
1828
+ lines.splice(hit, 1);
1829
+ await fs.writeFile(file, lines.join("\n"), "utf8");
1830
+ return json(res, 200, { ok: true });
1831
+ }
1832
+ next();
1833
+ } catch (err) {
1834
+ json(res, 500, { error: String(err.message ?? err) });
1835
+ }
1836
+ });
1837
+ }
1838
+
1839
+ //#endregion
1840
+ //#region src/vite/routes/edit.ts
1841
+ function registerEditRoutes(server, ctx) {
1842
+ server.middlewares.use("/__edit", async (req, res, next) => {
1843
+ const url = new URL(req.url ?? "/", "http://local");
1844
+ const method = req.method ?? "GET";
1845
+ if (method !== "POST") return next();
1846
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
1847
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
1848
+ try {
1849
+ if (url.pathname === "/") {
1850
+ const body = await readBody(req);
1851
+ const pageId = body.pageId ?? "";
1852
+ const file = resolvePageEntryPath(ctx, pageId);
1853
+ if (!file) return json(res, 400, { error: "invalid pageId" });
1854
+ if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
1855
+ if (!Array.isArray(body.ops)) return json(res, 400, { error: "missing ops" });
1856
+ let source;
1857
+ try {
1858
+ source = await fs.readFile(file, "utf8");
1859
+ } catch {
1860
+ return json(res, 404, { error: "page not found" });
1861
+ }
1862
+ const result = applyEdit(source, body.line, body.column ?? 0, body.ops);
1863
+ if (!result.ok) return json(res, result.status, { error: result.error });
1864
+ const changed = result.source !== source;
1865
+ if (changed) await fs.writeFile(file, result.source, "utf8");
1866
+ return json(res, 200, {
1867
+ ok: true,
1868
+ changed
1869
+ });
1870
+ }
1871
+ if (url.pathname === "/revert-asset") {
1872
+ const body = await readBody(req);
1873
+ const pageId = body.pageId ?? "";
1874
+ const assetPath = body.assetPath;
1875
+ const file = resolvePageEntryPath(ctx, pageId);
1876
+ if (!file) return json(res, 400, { error: "invalid pageId" });
1877
+ if (typeof assetPath !== "string" || !assetPath) return json(res, 400, { error: "missing assetPath" });
1878
+ if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return json(res, 400, { error: "asset path must start with ./assets/ or @assets/" });
1879
+ let source;
1880
+ try {
1881
+ source = await fs.readFile(file, "utf8");
1882
+ } catch {
1883
+ return json(res, 404, { error: "page not found" });
1884
+ }
1885
+ const result = applyRevertAsset(source, assetPath);
1886
+ if (!result.ok) return json(res, result.status, { error: result.error });
1887
+ const changed = result.source !== source;
1888
+ if (changed) await fs.writeFile(file, result.source, "utf8");
1889
+ return json(res, 200, {
1890
+ ok: true,
1891
+ changed
1892
+ });
1893
+ }
1894
+ if (url.pathname === "/batch") {
1895
+ const body = await readBody(req);
1896
+ const pageId = body.pageId ?? "";
1897
+ const file = resolvePageEntryPath(ctx, pageId);
1898
+ if (!file) return json(res, 400, { error: "invalid pageId" });
1899
+ if (!Array.isArray(body.edits)) return json(res, 400, { error: "missing edits" });
1900
+ let source;
1901
+ try {
1902
+ source = await fs.readFile(file, "utf8");
1903
+ } catch {
1904
+ return json(res, 404, { error: "page not found" });
1905
+ }
1906
+ const original = source;
1907
+ const results = [];
1908
+ for (const edit of body.edits) {
1909
+ if (!edit.line || edit.line < 1 || !Array.isArray(edit.ops)) {
1910
+ results.push({
1911
+ ok: false,
1912
+ error: "invalid edit"
1913
+ });
1914
+ continue;
1915
+ }
1916
+ const r = applyEdit(source, edit.line, edit.column ?? 0, edit.ops);
1917
+ if (r.ok) {
1918
+ source = r.source;
1919
+ results.push({ ok: true });
1920
+ } else results.push({
1921
+ ok: false,
1922
+ error: r.error
1923
+ });
1924
+ }
1925
+ const changed = source !== original;
1926
+ if (changed) await fs.writeFile(file, source, "utf8");
1927
+ return json(res, 200, {
1928
+ ok: true,
1929
+ changed,
1930
+ results
1931
+ });
1932
+ }
1933
+ return next();
1934
+ } catch (err) {
1935
+ json(res, 500, { error: String(err.message ?? err) });
1936
+ }
1937
+ });
1938
+ }
1939
+
1940
+ //#endregion
1941
+ //#region src/files/folders.ts
1942
+ const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
1943
+ const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
1944
+ function emptyManifest() {
1945
+ return {
1946
+ folders: [],
1947
+ assignments: {}
1948
+ };
1949
+ }
1950
+ async function readManifest(file) {
1951
+ try {
1952
+ const raw = await fs.readFile(file, "utf8");
1953
+ const parsed = JSON.parse(raw);
1954
+ return {
1955
+ folders: Array.isArray(parsed.folders) ? parsed.folders : [],
1956
+ assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
1957
+ };
1958
+ } catch (err) {
1959
+ if (err.code === "ENOENT") return emptyManifest();
1960
+ throw err;
1961
+ }
1962
+ }
1963
+ async function writeManifest(file, manifest) {
1964
+ await fs.mkdir(path.dirname(file), { recursive: true });
1965
+ await fs.writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
1966
+ }
1967
+ function newFolderId() {
1968
+ return `f-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
1969
+ }
1970
+ function validateName(v) {
1971
+ if (typeof v !== "string") return null;
1972
+ const trimmed = v.trim();
1973
+ if (trimmed.length < 1 || trimmed.length > 40) return null;
1974
+ return trimmed;
1975
+ }
1976
+ function validateReorder(v, current) {
1977
+ if (!Array.isArray(v) || v.length !== current.length) return null;
1978
+ const known = new Set(current.map((f) => f.id));
1979
+ const seen = new Set();
1980
+ const out = [];
1981
+ for (const id of v) {
1982
+ if (typeof id !== "string" || !FOLDER_ID_RE.test(id)) return null;
1983
+ if (!known.has(id) || seen.has(id)) return null;
1984
+ seen.add(id);
1985
+ out.push(id);
1986
+ }
1987
+ return out;
1988
+ }
1989
+ function validateIcon(v) {
1990
+ if (!v || typeof v !== "object") return null;
1991
+ const icon = v;
1992
+ if (icon.type === "emoji") {
1993
+ if (typeof icon.value !== "string") return null;
1994
+ if (icon.value.length < 1 || icon.value.length > 8) return null;
1995
+ return {
1996
+ type: "emoji",
1997
+ value: icon.value
1998
+ };
1999
+ }
2000
+ if (icon.type === "color") {
2001
+ if (typeof icon.value !== "string" || !COLOR_RE.test(icon.value)) return null;
2002
+ return {
2003
+ type: "color",
2004
+ value: icon.value
2005
+ };
2006
+ }
2007
+ return null;
2008
+ }
2009
+
2010
+ //#endregion
2011
+ //#region src/vite/routes/folders.ts
2012
+ function registerFolderRoutes(server, ctx) {
2013
+ server.middlewares.use("/__folders", async (req, res, next) => {
2014
+ const url = new URL(req.url ?? "/", "http://local");
2015
+ const method = req.method ?? "GET";
2016
+ try {
2017
+ if (method === "GET" && url.pathname === "/") {
2018
+ const manifest = await readManifest(ctx.manifestPath);
2019
+ return json(res, 200, manifest);
2020
+ }
2021
+ if (method === "POST" && url.pathname === "/") {
2022
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2023
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2024
+ const body = await readBody(req);
2025
+ const name = validateName(body.name);
2026
+ if (!name) return json(res, 400, { error: "invalid name" });
2027
+ const icon = validateIcon(body.icon);
2028
+ if (!icon) return json(res, 400, { error: "invalid icon" });
2029
+ const manifest = await readManifest(ctx.manifestPath);
2030
+ const folder = {
2031
+ id: newFolderId(),
2032
+ name,
2033
+ icon
2034
+ };
2035
+ manifest.folders.push(folder);
2036
+ await writeManifest(ctx.manifestPath, manifest);
2037
+ return json(res, 200, folder);
2038
+ }
2039
+ if (method === "PUT" && url.pathname === "/assign") {
2040
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2041
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2042
+ const body = await readBody(req);
2043
+ if (typeof body.pageId !== "string" || !PAGE_ID_RE.test(body.pageId)) return json(res, 400, { error: "invalid pageId" });
2044
+ const pageId = body.pageId;
2045
+ let folderId;
2046
+ if (body.folderId === null) folderId = null;
2047
+ else if (typeof body.folderId === "string" && FOLDER_ID_RE.test(body.folderId)) folderId = body.folderId;
2048
+ else return json(res, 400, { error: "invalid folderId" });
2049
+ const manifest = await readManifest(ctx.manifestPath);
2050
+ if (folderId && !manifest.folders.some((f) => f.id === folderId)) return json(res, 404, { error: "folder not found" });
2051
+ if (folderId === null) delete manifest.assignments[pageId];
2052
+ else manifest.assignments[pageId] = folderId;
2053
+ await writeManifest(ctx.manifestPath, manifest);
2054
+ return json(res, 200, { ok: true });
2055
+ }
2056
+ if (method === "PUT" && url.pathname === "/reorder") {
2057
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2058
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2059
+ const body = await readBody(req);
2060
+ const manifest = await readManifest(ctx.manifestPath);
2061
+ const ids = validateReorder(body.ids, manifest.folders);
2062
+ if (!ids) return json(res, 400, { error: "invalid ids" });
2063
+ const byId = new Map(manifest.folders.map((f) => [f.id, f]));
2064
+ manifest.folders = ids.map((id) => byId.get(id));
2065
+ await writeManifest(ctx.manifestPath, manifest);
2066
+ return json(res, 200, { ok: true });
2067
+ }
2068
+ const idMatch = url.pathname.match(/^\/([^/]+)$/);
2069
+ if (idMatch) {
2070
+ const id = idMatch[1];
2071
+ if (!FOLDER_ID_RE.test(id)) return json(res, 400, { error: "invalid id" });
2072
+ if (method === "PATCH") {
2073
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2074
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2075
+ const body = await readBody(req);
2076
+ const manifest = await readManifest(ctx.manifestPath);
2077
+ const folder = manifest.folders.find((f) => f.id === id);
2078
+ if (!folder) return json(res, 404, { error: "folder not found" });
2079
+ if (body.name !== void 0) {
2080
+ const name = validateName(body.name);
2081
+ if (!name) return json(res, 400, { error: "invalid name" });
2082
+ folder.name = name;
2083
+ }
2084
+ if (body.icon !== void 0) {
2085
+ const icon = validateIcon(body.icon);
2086
+ if (!icon) return json(res, 400, { error: "invalid icon" });
2087
+ folder.icon = icon;
2088
+ }
2089
+ await writeManifest(ctx.manifestPath, manifest);
2090
+ return json(res, 200, folder);
2091
+ }
2092
+ if (method === "DELETE") {
2093
+ const requestCheck = validateMutationRequest(req);
2094
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2095
+ const manifest = await readManifest(ctx.manifestPath);
2096
+ const before = manifest.folders.length;
2097
+ manifest.folders = manifest.folders.filter((f) => f.id !== id);
2098
+ if (manifest.folders.length === before) return json(res, 404, { error: "folder not found" });
2099
+ for (const [pageId, folderId] of Object.entries(manifest.assignments)) if (folderId === id) delete manifest.assignments[pageId];
2100
+ await writeManifest(ctx.manifestPath, manifest);
2101
+ return json(res, 200, { ok: true });
2102
+ }
2103
+ }
2104
+ next();
2105
+ } catch (err) {
2106
+ json(res, 500, { error: String(err.message ?? err) });
2107
+ }
2108
+ });
2109
+ }
2110
+
2111
+ //#endregion
2112
+ //#region src/vite/routes/pages.ts
2113
+ function registerPageRoutes(server, ctx) {
2114
+ server.middlewares.use("/__pages", async (req, res, next) => {
2115
+ const url = new URL(req.url ?? "/", "http://local");
2116
+ const method = req.method ?? "GET";
2117
+ try {
2118
+ const duplicateMatch = url.pathname.match(/^\/([^/]+)\/duplicate$/);
2119
+ if (duplicateMatch && method === "POST") {
2120
+ const requestCheck = validateMutationRequest(req);
2121
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2122
+ const pageId$1 = duplicateMatch[1];
2123
+ if (!PAGE_ID_RE.test(pageId$1)) return json(res, 400, { error: "invalid pageId" });
2124
+ const body = await readBody(req);
2125
+ if (body.newId !== void 0 && typeof body.newId !== "string") return json(res, 400, { error: "invalid newId" });
2126
+ const duplicated = await duplicatePageDir(ctx.pagesRoot, pageId$1, body.newId);
2127
+ if (!duplicated.ok) return json(res, duplicated.status, { error: duplicated.error });
2128
+ const manifest = await readManifest(ctx.manifestPath);
2129
+ const folderId = manifest.assignments[pageId$1];
2130
+ if (folderId) {
2131
+ manifest.assignments[duplicated.pageId] = folderId;
2132
+ await writeManifest(ctx.manifestPath, manifest);
2133
+ }
2134
+ return json(res, 200, {
2135
+ ok: true,
2136
+ pageId: duplicated.pageId
2137
+ });
2138
+ }
2139
+ const idMatch = url.pathname.match(/^\/([^/]+)$/);
2140
+ if (!idMatch) return next();
2141
+ const pageId = idMatch[1];
2142
+ if (!PAGE_ID_RE.test(pageId)) return json(res, 400, { error: "invalid pageId" });
2143
+ if (method === "PATCH") {
2144
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2145
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2146
+ const body = await readBody(req);
2147
+ const name = validatePageName(body.name);
2148
+ if (!name) return json(res, 400, { error: "invalid name" });
2149
+ const entry = resolvePageEntry(ctx.pagesRoot, pageId);
2150
+ if (!entry) return json(res, 400, { error: "invalid pageId" });
2151
+ let source;
2152
+ try {
2153
+ source = await fs.readFile(entry, "utf8");
2154
+ } catch {
2155
+ return json(res, 404, { error: "page not found" });
2156
+ }
2157
+ const updated = updateMetaTitleInSource(source, name);
2158
+ if (updated === null) return json(res, 422, { error: "could not locate a safe place to write meta.title in index.tsx" });
2159
+ if (updated !== source) await fs.writeFile(entry, updated, "utf8");
2160
+ server.ws.send({ type: "full-reload" });
2161
+ return json(res, 200, {
2162
+ ok: true,
2163
+ pageId,
2164
+ name
2165
+ });
2166
+ }
2167
+ if (method === "DELETE") {
2168
+ const requestCheck = validateMutationRequest(req);
2169
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2170
+ const removed = await rmPageDir(ctx.pagesRoot, pageId);
2171
+ if (!removed) return json(res, 404, { error: "page not found" });
2172
+ const manifest = await readManifest(ctx.manifestPath);
2173
+ delete manifest.assignments[pageId];
2174
+ await writeManifest(ctx.manifestPath, manifest);
2175
+ return json(res, 200, { ok: true });
2176
+ }
2177
+ return next();
2178
+ } catch (err) {
2179
+ json(res, 500, { error: String(err.message ?? err) });
2180
+ }
2181
+ });
2182
+ }
2183
+
2184
+ //#endregion
2185
+ //#region src/vite/routes/restart.ts
2186
+ const RESTART_EXIT_CODE = 52;
2187
+ const DEV_SUPERVISED_ENV = "OPEN_PAGES_DEV_SUPERVISED";
2188
+ const executionId = randomUUID();
2189
+ function isSupervised() {
2190
+ return process.env[DEV_SUPERVISED_ENV] === "1";
2191
+ }
2192
+ function registerRestartRoutes(server) {
2193
+ server.middlewares.use("/__server-status", (req, res, next) => {
2194
+ if ((req.method ?? "GET") !== "GET") return next();
2195
+ res.setHeader("cache-control", "no-store");
2196
+ json(res, 200, {
2197
+ executionId,
2198
+ canRestart: isSupervised()
2199
+ });
2200
+ });
2201
+ server.middlewares.use("/__restart-server", (req, res, next) => {
2202
+ if ((req.method ?? "GET") !== "POST") return next();
2203
+ const guard = validateMutationRequest(req);
2204
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
2205
+ if (!isSupervised()) return json(res, 409, { error: "dev server was not started by the open-pages CLI" });
2206
+ res.on("finish", () => process.exit(RESTART_EXIT_CODE));
2207
+ json(res, 200, { restarting: true });
2208
+ });
2209
+ }
2210
+
2211
+ //#endregion
2212
+ //#region src/vite/routes/svgl.ts
2213
+ function registerSvglRoutes(server) {
2214
+ server.middlewares.use("/__svgl", async (req, res, next) => {
2215
+ const reqUrl = new URL(req.url ?? "/", "http://local");
2216
+ const method = req.method ?? "GET";
2217
+ if (method !== "GET") return next();
2218
+ try {
2219
+ let target = null;
2220
+ if (reqUrl.pathname === "/search") {
2221
+ const params = new URLSearchParams();
2222
+ const q = reqUrl.searchParams.get("q");
2223
+ const limit = reqUrl.searchParams.get("limit");
2224
+ if (q) params.set("search", q);
2225
+ if (limit) params.set("limit", limit);
2226
+ const qs = params.toString();
2227
+ target = `https://api.svgl.app/${qs ? `?${qs}` : ""}`;
2228
+ } else if (reqUrl.pathname === "/svg") {
2229
+ const u = reqUrl.searchParams.get("u");
2230
+ if (!u) return json(res, 400, { error: "missing u" });
2231
+ let parsed;
2232
+ try {
2233
+ parsed = new URL(u);
2234
+ } catch {
2235
+ return json(res, 400, { error: "invalid u" });
2236
+ }
2237
+ if (parsed.protocol !== "https:") return json(res, 400, { error: "https only" });
2238
+ const host = parsed.hostname.toLowerCase();
2239
+ if (host !== "svgl.app" && !host.endsWith(".svgl.app")) return json(res, 400, { error: "host not allowed" });
2240
+ target = parsed.toString();
2241
+ } else return next();
2242
+ const upstream = await fetch(target);
2243
+ const ct = upstream.headers.get("content-type") ?? "application/octet-stream";
2244
+ res.statusCode = upstream.status;
2245
+ res.setHeader("content-type", ct);
2246
+ res.setHeader("cache-control", "no-store");
2247
+ const buf = Buffer.from(await upstream.arrayBuffer());
2248
+ res.end(buf);
2249
+ } catch (err) {
2250
+ json(res, 502, { error: String(err.message ?? err) });
2251
+ }
2252
+ });
2253
+ }
2254
+
2255
+ //#endregion
2256
+ //#region src/vite/routes/update.ts
2257
+ let updateInFlight = null;
2258
+ async function updatePackage(ctx) {
2259
+ const packageManager = await detectPackageManager(ctx.userCwd);
2260
+ const updateCommand = await updateCommandFor(packageManager, ctx.userCwd);
2261
+ const syncCommand = localOpenPagesCommand(ctx.userCwd, ["sync:skills"]);
2262
+ await runCommand(updateCommand, ctx.userCwd);
2263
+ await runCommand(syncCommand, ctx.userCwd);
2264
+ invalidateLatestCache();
2265
+ const latest = await fetchLatest();
2266
+ return {
2267
+ packageManager,
2268
+ command: `${formatCommand(updateCommand)} && open-pages sync:skills`,
2269
+ latest,
2270
+ message: "Updated @autono/open-pages and synced skills."
2271
+ };
2272
+ }
2273
+ function registerUpdateRoutes(server, ctx) {
2274
+ server.middlewares.use("/__update-check", async (req, res, next) => {
2275
+ if ((req.method ?? "GET") !== "GET") return next();
2276
+ const latest = await fetchLatest();
2277
+ const result = {
2278
+ current: ctx.coreVersion,
2279
+ latest,
2280
+ outdated: latest ? isOutdated(ctx.coreVersion, latest) : false
2281
+ };
2282
+ res.setHeader("cache-control", "no-store");
2283
+ json(res, 200, result);
2284
+ });
2285
+ server.middlewares.use("/__update-package", async (req, res, next) => {
2286
+ if ((req.method ?? "GET") !== "POST") return next();
2287
+ const guard = validateMutationRequest(req);
2288
+ if (!guard.ok) return json(res, guard.status, { error: guard.error });
2289
+ try {
2290
+ updateInFlight ??= updatePackage(ctx).finally(() => {
2291
+ updateInFlight = null;
2292
+ });
2293
+ const result = await updateInFlight;
2294
+ json(res, 200, result);
2295
+ } catch (err) {
2296
+ json(res, 500, { error: err instanceof Error ? err.message : "update failed" });
2297
+ }
2298
+ });
2299
+ }
2300
+
2301
+ //#endregion
2302
+ //#region src/vite/routes/watchers.ts
2303
+ function registerWatchers(server, ctx) {
2304
+ server.watcher.add(ctx.manifestPath);
2305
+ server.watcher.on("change", (p) => {
2306
+ if (p === ctx.manifestPath) server.ws.send({
2307
+ type: "custom",
2308
+ event: "open-pages:files-changed"
2309
+ });
2310
+ });
2311
+ server.watcher.add(ctx.globalAssetsRoot);
2312
+ const onAssetChange = (p) => {
2313
+ if (p.startsWith(ctx.globalAssetsRoot + path.sep) || p === ctx.globalAssetsRoot) {
2314
+ server.ws.send({
2315
+ type: "custom",
2316
+ event: "open-pages:assets-changed",
2317
+ data: { pageId: GLOBAL_SCOPE }
2318
+ });
2319
+ return;
2320
+ }
2321
+ if (!p.startsWith(ctx.pagesRoot + path.sep)) return;
2322
+ const rel = p.slice(ctx.pagesRoot.length + 1);
2323
+ const parts = rel.split(path.sep);
2324
+ if (parts.length < 3 || parts[1] !== "assets") return;
2325
+ const pageId = parts[0];
2326
+ if (!PAGE_ID_RE.test(pageId)) return;
2327
+ server.ws.send({
2328
+ type: "custom",
2329
+ event: "open-pages:assets-changed",
2330
+ data: { pageId }
2331
+ });
2332
+ };
2333
+ server.watcher.on("add", onAssetChange);
2334
+ server.watcher.on("change", onAssetChange);
2335
+ server.watcher.on("unlink", onAssetChange);
2336
+ }
2337
+
2338
+ //#endregion
2339
+ //#region src/vite/api-plugin.ts
2340
+ function apiPlugin(opts) {
2341
+ return {
2342
+ name: "open-pages:api",
2343
+ apply: "serve",
2344
+ configureServer(server) {
2345
+ const ctx = makeContext(opts);
2346
+ registerWatchers(server, ctx);
2347
+ registerEditRoutes(server, ctx);
2348
+ registerCommentRoutes(server, ctx);
2349
+ registerPageRoutes(server, ctx);
2350
+ registerAssetRoutes(server, ctx);
2351
+ registerSvglRoutes(server);
2352
+ registerFolderRoutes(server, ctx);
2353
+ registerUpdateRoutes(server, ctx);
2354
+ registerRestartRoutes(server);
2355
+ }
2356
+ };
2357
+ }
2358
+
2359
+ //#endregion
2360
+ //#region src/vite/current-plugin.ts
2361
+ const TEXT_SNIPPET_MAX = 120;
2362
+ function parseSelection(raw) {
2363
+ if (raw == null || typeof raw !== "object") return null;
2364
+ const sel = raw;
2365
+ if (typeof sel.line !== "number" || !Number.isFinite(sel.line)) return null;
2366
+ if (typeof sel.column !== "number" || !Number.isFinite(sel.column)) return null;
2367
+ const tagName = typeof sel.tagName === "string" ? sel.tagName.toLowerCase().slice(0, 32) : "unknown";
2368
+ const text = typeof sel.text === "string" ? sel.text.replace(/\s+/g, " ").trim().slice(0, TEXT_SNIPPET_MAX) : "";
2369
+ return {
2370
+ line: Math.max(1, Math.floor(sel.line)),
2371
+ column: Math.max(0, Math.floor(sel.column)),
2372
+ tagName,
2373
+ text
2374
+ };
2375
+ }
2376
+ function currentPlugin(opts) {
2377
+ const userCwd = opts.userCwd;
2378
+ const pagesDir = opts.pagesDir ?? "pages";
2379
+ const outDir = path.join(userCwd, "node_modules", ".open-pages");
2380
+ const outFile = path.join(outDir, "current.json");
2381
+ const tmpFile = `${outFile}.tmp`;
2382
+ let cached = null;
2383
+ return {
2384
+ name: "open-pages:current",
2385
+ apply: "serve",
2386
+ configureServer(server) {
2387
+ server.ws.on("open-pages:current", async (raw) => {
2388
+ const next = cached ? { ...cached } : {
2389
+ pageId: "",
2390
+ pageTitle: "",
2391
+ view: "pages",
2392
+ pagePath: "",
2393
+ selection: null
2394
+ };
2395
+ if (typeof raw?.pageId === "string") {
2396
+ if (!PAGE_ID_RE.test(raw.pageId)) return;
2397
+ const pageTitle = typeof raw.pageTitle === "string" ? raw.pageTitle : raw.pageId;
2398
+ const view = raw.view === "assets" ? "assets" : "pages";
2399
+ const entry = existsSync(path.join(userCwd, pagesDir, raw.pageId, "index.tsx")) ? "index.tsx" : "index.html";
2400
+ const pagePath = path.join(pagesDir, raw.pageId, entry).split(path.sep).join("/");
2401
+ if (cached?.pageId !== raw.pageId) next.selection = null;
2402
+ next.pageId = raw.pageId;
2403
+ next.pageTitle = pageTitle;
2404
+ next.view = view;
2405
+ next.pagePath = pagePath;
2406
+ }
2407
+ if ("selection" in raw) next.selection = parseSelection(raw.selection);
2408
+ if (!next.pageId) return;
2409
+ cached = next;
2410
+ const body = {
2411
+ ...next,
2412
+ updatedAt: new Date().toISOString()
2413
+ };
2414
+ try {
2415
+ await fs.mkdir(outDir, { recursive: true });
2416
+ await fs.writeFile(tmpFile, `${JSON.stringify(body, null, 2)}\n`, "utf8");
2417
+ await fs.rename(tmpFile, outFile);
2418
+ } catch {}
2419
+ });
2420
+ }
2421
+ };
2422
+ }
2423
+
2424
+ //#endregion
2425
+ //#region src/vite/design-plugin.ts
2426
+ function parseSource(source) {
2427
+ try {
2428
+ return parse(source, {
2429
+ sourceType: "module",
2430
+ plugins: ["typescript", "jsx"],
2431
+ errorRecovery: true
2432
+ });
2433
+ } catch {
2434
+ return null;
2435
+ }
2436
+ }
2437
+ function findDesignDecl(ast) {
2438
+ const body = ast.program?.body ?? [];
2439
+ for (const node of body) {
2440
+ let varDecl = null;
2441
+ if (node.type === "VariableDeclaration") varDecl = node;
2442
+ else if (node.type === "ExportNamedDeclaration") {
2443
+ const decl = node.declaration;
2444
+ if (decl?.type === "VariableDeclaration") varDecl = decl;
2445
+ }
2446
+ if (!varDecl) continue;
2447
+ const declarations = varDecl.declarations ?? [];
2448
+ for (const d of declarations) {
2449
+ const id = d.id;
2450
+ if (!id || id.type !== "Identifier" || id.name !== "design") continue;
2451
+ const init = d.init;
2452
+ if (!init) return null;
2453
+ let inner = init;
2454
+ if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
2455
+ const expr = inner.expression;
2456
+ if (expr) inner = expr;
2457
+ }
2458
+ if (inner.type !== "ObjectExpression") return null;
2459
+ return {
2460
+ declStart: node.start,
2461
+ declEnd: node.end,
2462
+ objectStart: inner.start,
2463
+ objectEnd: inner.end
2464
+ };
2465
+ }
2466
+ }
2467
+ return null;
2468
+ }
2469
+ function literalToValue(node) {
2470
+ switch (node.type) {
2471
+ case "StringLiteral": return node.value;
2472
+ case "NumericLiteral": return node.value;
2473
+ case "BooleanLiteral": return node.value;
2474
+ case "NullLiteral": return null;
2475
+ case "UnaryExpression": {
2476
+ const op = node.operator;
2477
+ const arg = node.argument;
2478
+ const v = literalToValue(arg);
2479
+ if (op === "-" && typeof v === "number") return -v;
2480
+ if (op === "+" && typeof v === "number") return v;
2481
+ throw new Error(`unsupported unary operator ${op}`);
2482
+ }
2483
+ case "TemplateLiteral": {
2484
+ const quasis = node.quasis;
2485
+ const expressions = node.expressions;
2486
+ if (expressions.length > 0) throw new Error("template literal has expressions");
2487
+ return quasis[0].value.cooked ?? quasis[0].value.raw;
2488
+ }
2489
+ case "ArrayExpression": {
2490
+ const elements = node.elements;
2491
+ return elements.map((el) => {
2492
+ if (!el) throw new Error("array has hole");
2493
+ return literalToValue(el);
2494
+ });
2495
+ }
2496
+ case "ObjectExpression": {
2497
+ const properties = node.properties;
2498
+ const out = {};
2499
+ for (const prop of properties) {
2500
+ if (prop.type !== "ObjectProperty") throw new Error("object has spread or method");
2501
+ const p = prop;
2502
+ if (p.computed) throw new Error("object has computed key");
2503
+ let key;
2504
+ if (p.key.type === "Identifier" && typeof p.key.name === "string") key = p.key.name;
2505
+ else if (p.key.type === "StringLiteral" && typeof p.key.value === "string") key = p.key.value;
2506
+ else throw new Error("unsupported object key");
2507
+ out[key] = literalToValue(p.value);
2508
+ }
2509
+ return out;
2510
+ }
2511
+ default: throw new Error(`unsupported node type ${node.type}`);
2512
+ }
2513
+ }
2514
+ function isPlainObject(v) {
2515
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2516
+ }
2517
+ function mergeDesign(base, patch) {
2518
+ const out = JSON.parse(JSON.stringify(base));
2519
+ const apply = (target, src) => {
2520
+ for (const [k, v] of Object.entries(src)) if (isPlainObject(v) && isPlainObject(target[k])) apply(target[k], v);
2521
+ else target[k] = v;
2522
+ };
2523
+ if (isPlainObject(patch)) apply(out, patch);
2524
+ return out;
2525
+ }
2526
+ function indent(level) {
2527
+ return " ".repeat(level);
2528
+ }
2529
+ function jsString(s) {
2530
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
2531
+ }
2532
+ function isValidIdentifier(name) {
2533
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
2534
+ }
2535
+ function serializeValue(value, level) {
2536
+ if (value === null) return "null";
2537
+ if (typeof value === "string") return jsString(value);
2538
+ if (typeof value === "number") {
2539
+ if (!Number.isFinite(value)) throw new Error("non-finite number");
2540
+ return String(value);
2541
+ }
2542
+ if (typeof value === "boolean") return value ? "true" : "false";
2543
+ if (Array.isArray(value)) {
2544
+ if (value.length === 0) return "[]";
2545
+ const inner = value.map((el) => serializeValue(el, level + 1)).join(", ");
2546
+ return `[${inner}]`;
2547
+ }
2548
+ if (isPlainObject(value)) {
2549
+ const entries = Object.entries(value);
2550
+ if (entries.length === 0) return "{}";
2551
+ const childIndent = indent(level + 1);
2552
+ const lines = entries.map(([k, v]) => {
2553
+ const key = isValidIdentifier(k) ? k : jsString(k);
2554
+ return `${childIndent}${key}: ${serializeValue(v, level + 1)},`;
2555
+ });
2556
+ return `{\n${lines.join("\n")}\n${indent(level)}}`;
2557
+ }
2558
+ throw new Error(`unsupported value type ${typeof value}`);
2559
+ }
2560
+ function serializeDesign(design) {
2561
+ return serializeValue(design, 0);
2562
+ }
2563
+ function parseDocDesign(source) {
2564
+ const ast = parseSource(source);
2565
+ if (!ast) return {
2566
+ ok: false,
2567
+ exists: true,
2568
+ error: "could not parse page source"
2569
+ };
2570
+ const loc = findDesignDecl(ast);
2571
+ if (!loc) return {
2572
+ ok: false,
2573
+ exists: false
2574
+ };
2575
+ const objectNode = findDesignObjectNode(ast);
2576
+ if (!objectNode) return {
2577
+ ok: false,
2578
+ exists: true,
2579
+ error: "design has unsupported initializer"
2580
+ };
2581
+ let value;
2582
+ try {
2583
+ value = literalToValue(objectNode);
2584
+ } catch (err) {
2585
+ return {
2586
+ ok: false,
2587
+ exists: true,
2588
+ error: err.message
2589
+ };
2590
+ }
2591
+ const merged = mergeDesign(defaultDesign, value);
2592
+ return {
2593
+ ok: true,
2594
+ design: merged,
2595
+ loc
2596
+ };
2597
+ }
2598
+ function findDesignObjectNode(ast) {
2599
+ const body = ast.program?.body ?? [];
2600
+ for (const node of body) {
2601
+ let varDecl = null;
2602
+ if (node.type === "VariableDeclaration") varDecl = node;
2603
+ else if (node.type === "ExportNamedDeclaration") {
2604
+ const decl = node.declaration;
2605
+ if (decl?.type === "VariableDeclaration") varDecl = decl;
2606
+ }
2607
+ if (!varDecl) continue;
2608
+ const declarations = varDecl.declarations ?? [];
2609
+ for (const d of declarations) {
2610
+ const id = d.id;
2611
+ if (!id || id.type !== "Identifier" || id.name !== "design") continue;
2612
+ const init = d.init;
2613
+ if (!init) return null;
2614
+ let inner = init;
2615
+ if (inner.type === "TSSatisfiesExpression" || inner.type === "TSAsExpression") {
2616
+ const expr = inner.expression;
2617
+ if (expr) inner = expr;
2618
+ }
2619
+ if (inner.type !== "ObjectExpression") return null;
2620
+ return inner;
2621
+ }
2622
+ }
2623
+ return null;
2624
+ }
2625
+ function findImports(ast) {
2626
+ const body = ast.program?.body ?? [];
2627
+ const out = [];
2628
+ for (const node of body) {
2629
+ if (node.type !== "ImportDeclaration") continue;
2630
+ const src = node.source?.value;
2631
+ if (typeof src !== "string") continue;
2632
+ const specs = node.specifiers ?? [];
2633
+ out.push({
2634
+ node,
2635
+ source: src,
2636
+ specifiers: specs
2637
+ });
2638
+ }
2639
+ return out;
2640
+ }
2641
+ function ensureDesignSystemImport(source, ast) {
2642
+ const imports = findImports(ast);
2643
+ const coreImport = imports.find((imp) => imp.source === "@autono/open-pages");
2644
+ if (coreImport) {
2645
+ const hasDesignSystem = coreImport.specifiers.some((spec) => {
2646
+ if (spec.type !== "ImportSpecifier") return false;
2647
+ const imported = spec.imported;
2648
+ return imported?.name === "DesignSystem";
2649
+ });
2650
+ if (hasDesignSystem) return {
2651
+ source,
2652
+ offsetShift: 0
2653
+ };
2654
+ const node = coreImport.node;
2655
+ const importText = source.slice(node.start, node.end);
2656
+ const braceClose = importText.lastIndexOf("}");
2657
+ if (braceClose === -1) return {
2658
+ source,
2659
+ offsetShift: 0
2660
+ };
2661
+ const absoluteBrace = node.start + braceClose;
2662
+ const insertText = coreImport.specifiers.length > 0 ? ", type DesignSystem" : "type DesignSystem";
2663
+ const next$1 = `${source.slice(0, absoluteBrace)}${insertText}${source.slice(absoluteBrace)}`;
2664
+ return {
2665
+ source: next$1,
2666
+ offsetShift: insertText.length
2667
+ };
2668
+ }
2669
+ const stmt = `import type { DesignSystem } from '@autono/open-pages';\n`;
2670
+ if (imports.length > 0) {
2671
+ const last = imports[imports.length - 1];
2672
+ const insertAt = last.node.end;
2673
+ const trail = source[insertAt] === "\n" ? "" : "\n";
2674
+ const next$1 = `${source.slice(0, insertAt)}\n${stmt.slice(0, -1)}${trail}${source.slice(insertAt)}`;
2675
+ return {
2676
+ source: next$1,
2677
+ offsetShift: 1 + stmt.length - (trail ? 0 : 1)
2678
+ };
2679
+ }
2680
+ const next = `${stmt}\n${source}`;
2681
+ return {
2682
+ source: next,
2683
+ offsetShift: stmt.length + 1
2684
+ };
2685
+ }
2686
+ function findInsertionPoint(source, ast) {
2687
+ const imports = findImports(ast);
2688
+ if (imports.length === 0) return 0;
2689
+ const last = imports[imports.length - 1];
2690
+ let off = last.node.end;
2691
+ while (off < source.length && source[off] !== "\n") off++;
2692
+ if (off < source.length) off++;
2693
+ return off;
2694
+ }
2695
+ function applyDesignWrite(source, next) {
2696
+ let body;
2697
+ try {
2698
+ body = serializeDesign(next);
2699
+ } catch (err) {
2700
+ return {
2701
+ ok: false,
2702
+ status: 422,
2703
+ error: `serialize failed: ${err.message}`
2704
+ };
2705
+ }
2706
+ const ast = parseSource(source);
2707
+ if (!ast) return {
2708
+ ok: false,
2709
+ status: 422,
2710
+ error: "could not parse page source"
2711
+ };
2712
+ const loc = findDesignDecl(ast);
2713
+ if (loc) {
2714
+ const out$1 = source.slice(0, loc.objectStart) + body + source.slice(loc.objectEnd);
2715
+ return {
2716
+ ok: true,
2717
+ source: out$1,
2718
+ created: false
2719
+ };
2720
+ }
2721
+ const withImport = ensureDesignSystemImport(source, ast);
2722
+ const ast2 = parseSource(withImport.source);
2723
+ if (!ast2) return {
2724
+ ok: false,
2725
+ status: 422,
2726
+ error: "failed to re-parse after adding import"
2727
+ };
2728
+ const insertAt = findInsertionPoint(withImport.source, ast2);
2729
+ const block = `\nconst design: DesignSystem = ${body};\n`;
2730
+ const out = withImport.source.slice(0, insertAt) + block + withImport.source.slice(insertAt);
2731
+ return {
2732
+ ok: true,
2733
+ source: out,
2734
+ created: true
2735
+ };
2736
+ }
2737
+ function designPlugin(opts) {
2738
+ const userCwd = opts.userCwd;
2739
+ const pagesDir = opts.pagesDir ?? "pages";
2740
+ return {
2741
+ name: "open-pages:design",
2742
+ apply: "serve",
2743
+ configureServer(server) {
2744
+ server.middlewares.use("/__design", async (req, res, next) => {
2745
+ const url = new URL(req.url ?? "/", "http://local");
2746
+ const method = req.method ?? "GET";
2747
+ const pageId = url.searchParams.get("pageId") ?? "";
2748
+ const file = resolvePagePath(userCwd, pagesDir, pageId);
2749
+ if (!file) return json(res, 400, { error: "invalid pageId" });
2750
+ try {
2751
+ if (method === "GET" && url.pathname === "/") {
2752
+ let source;
2753
+ try {
2754
+ source = await fs.readFile(file, "utf8");
2755
+ } catch {
2756
+ return json(res, 404, { error: "page not found" });
2757
+ }
2758
+ const parsed = parseDocDesign(source);
2759
+ if (parsed.ok) return json(res, 200, {
2760
+ design: parsed.design,
2761
+ exists: true,
2762
+ warning: null
2763
+ });
2764
+ if (parsed.exists === false) return json(res, 200, {
2765
+ design: defaultDesign,
2766
+ exists: false,
2767
+ warning: null
2768
+ });
2769
+ return json(res, 200, {
2770
+ design: defaultDesign,
2771
+ exists: true,
2772
+ warning: parsed.error
2773
+ });
2774
+ }
2775
+ if (method === "PUT" && url.pathname === "/") {
2776
+ const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
2777
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2778
+ const body = await readBody(req);
2779
+ const patch = body.patch;
2780
+ if (!patch || typeof patch !== "object") return json(res, 400, { error: "missing patch object" });
2781
+ let source;
2782
+ try {
2783
+ source = await fs.readFile(file, "utf8");
2784
+ } catch {
2785
+ return json(res, 404, { error: "page not found" });
2786
+ }
2787
+ const parsed = parseDocDesign(source);
2788
+ const baseDesign = parsed.ok ? parsed.design : defaultDesign;
2789
+ if (!parsed.ok && parsed.exists) return json(res, 422, { error: parsed.error });
2790
+ const merged = mergeDesign(baseDesign, patch);
2791
+ const written = applyDesignWrite(source, merged);
2792
+ if (!written.ok) return json(res, written.status, { error: written.error });
2793
+ if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
2794
+ return json(res, 200, {
2795
+ ok: true,
2796
+ design: merged,
2797
+ created: written.created
2798
+ });
2799
+ }
2800
+ if (method === "POST" && url.pathname === "/reset") {
2801
+ const requestCheck = validateMutationRequest(req);
2802
+ if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
2803
+ let source;
2804
+ try {
2805
+ source = await fs.readFile(file, "utf8");
2806
+ } catch {
2807
+ return json(res, 404, { error: "page not found" });
2808
+ }
2809
+ const written = applyDesignWrite(source, defaultDesign);
2810
+ if (!written.ok) return json(res, written.status, { error: written.error });
2811
+ if (written.source !== source) await fs.writeFile(file, written.source, "utf8");
2812
+ return json(res, 200, {
2813
+ ok: true,
2814
+ design: defaultDesign,
2815
+ created: written.created
2816
+ });
2817
+ }
2818
+ return next();
2819
+ } catch (err) {
2820
+ json(res, 500, { error: String(err.message ?? err) });
2821
+ }
2822
+ });
2823
+ }
2824
+ };
2825
+ }
2826
+
2827
+ //#endregion
2828
+ //#region src/vite/loc-tags-plugin.ts
2829
+ const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
2830
+ function isTaggableJsxName(name) {
2831
+ if (!t.isJSXIdentifier(name)) return false;
2832
+ return /^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name);
2833
+ }
2834
+ function alreadyTagged(opening) {
2835
+ return opening.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-op-loc");
2836
+ }
2837
+ function injectLocTags(code) {
2838
+ let ast;
2839
+ try {
2840
+ ast = parse(code, {
2841
+ sourceType: "module",
2842
+ plugins: ["typescript", "jsx"],
2843
+ errorRecovery: true
2844
+ });
2845
+ } catch {
2846
+ return null;
2847
+ }
2848
+ const insertions = [];
2849
+ walkJsx(ast, (node) => {
2850
+ if (!t.isJSXElement(node) || !node.loc) return;
2851
+ const opening = node.openingElement;
2852
+ const name = opening.name;
2853
+ if (!isTaggableJsxName(name) || alreadyTagged(opening)) return;
2854
+ insertions.push({
2855
+ offset: name.end ?? 0,
2856
+ text: ` data-op-loc="${node.loc.start.line}:${node.loc.start.column}"`
2857
+ });
2858
+ });
2859
+ if (insertions.length === 0) return null;
2860
+ insertions.sort((a, b) => b.offset - a.offset);
2861
+ let next = code;
2862
+ for (const ins of insertions) next = next.slice(0, ins.offset) + ins.text + next.slice(ins.offset);
2863
+ return next;
2864
+ }
2865
+ function isPageSourceFile(id, docsRootPosix) {
2866
+ const filePath = id.split(/[?#]/)[0].replace(/\\/g, "/");
2867
+ if (!filePath.startsWith(`${docsRootPosix}/`)) return false;
2868
+ if (!filePath.endsWith(".tsx")) return false;
2869
+ if (filePath.endsWith(".d.ts") || filePath.endsWith(".test.tsx")) return false;
2870
+ const rel = filePath.slice(docsRootPosix.length + 1);
2871
+ return rel.includes("/");
2872
+ }
2873
+ function locTagsPlugin(opts) {
2874
+ const pagesRoot = path.resolve(opts.userCwd, opts.pagesDir ?? "pages").replace(/\\/g, "/");
2875
+ return {
2876
+ name: "open-pages:loc-tags",
2877
+ apply: opts.apply ?? "serve",
2878
+ enforce: "pre",
2879
+ transform(code, id) {
2880
+ if (!isPageSourceFile(id, pagesRoot)) return null;
2881
+ const next = injectLocTags(code);
2882
+ if (next === null) return null;
2883
+ return {
2884
+ code: next,
2885
+ map: null
2886
+ };
2887
+ }
2888
+ };
2889
+ }
2890
+
2891
+ //#endregion
2892
+ //#region src/vite/themes-plugin.ts
2893
+ const THEMES_VMOD = "virtual:open-pages/themes";
2894
+ function resolved(id) {
2895
+ return `\0${id}`;
2896
+ }
2897
+ const FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
2898
+ function parseFrontmatter(raw, themeId) {
2899
+ const match = raw.match(FM_RE);
2900
+ const fmText = match ? match[1] : "";
2901
+ const body = match ? match[2] : raw;
2902
+ const data = {};
2903
+ for (const line of fmText.split(/\r?\n/)) {
2904
+ const m = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
2905
+ if (!m) continue;
2906
+ let value = m[2].trim();
2907
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2908
+ data[m[1]] = value;
2909
+ }
2910
+ return {
2911
+ fm: {
2912
+ name: data.name || themeId,
2913
+ description: data.description || ""
2914
+ },
2915
+ body: body.trim()
2916
+ };
2917
+ }
2918
+ async function findThemes(userCwd, themesDir) {
2919
+ const abs = path.resolve(userCwd, themesDir);
2920
+ if (!existsSync(abs)) return [];
2921
+ const hits = await fg("*.md", {
2922
+ cwd: abs,
2923
+ absolute: true,
2924
+ onlyFiles: true
2925
+ });
2926
+ return hits.sort();
2927
+ }
2928
+ async function readTheme(mdAbs, themesRoot) {
2929
+ const id = path.basename(mdAbs, ".md");
2930
+ const raw = await fs.readFile(mdAbs, "utf8");
2931
+ const { fm, body } = parseFrontmatter(raw, id);
2932
+ const demoCandidates = [
2933
+ `${id}.demo.tsx`,
2934
+ `${id}.demo.jsx`,
2935
+ `${id}.demo.ts`,
2936
+ `${id}.demo.js`
2937
+ ];
2938
+ let demoAbs = null;
2939
+ for (const cand of demoCandidates) {
2940
+ const p = path.join(themesRoot, cand);
2941
+ if (existsSync(p)) {
2942
+ demoAbs = p;
2943
+ break;
2944
+ }
2945
+ }
2946
+ return {
2947
+ id,
2948
+ frontmatter: fm,
2949
+ body,
2950
+ demoAbs
2951
+ };
2952
+ }
2953
+ function generateThemesModule(themes, isDev) {
2954
+ const meta = themes.map((t$4) => ({
2955
+ id: t$4.id,
2956
+ name: t$4.frontmatter.name,
2957
+ description: t$4.frontmatter.description,
2958
+ body: t$4.body,
2959
+ hasDemo: t$4.demoAbs !== null
2960
+ }));
2961
+ const cases = themes.flatMap((t$4) => {
2962
+ const abs = t$4.demoAbs;
2963
+ if (!abs) return [];
2964
+ const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
2965
+ const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(importPath)})` : `import(${JSON.stringify(importPath)})`;
2966
+ return [` case ${JSON.stringify(t$4.id)}: return ${importExpr};`];
2967
+ }).join("\n");
2968
+ return `// virtual:open-pages/themes — generated
2969
+ export const themes = ${JSON.stringify(meta)};
2970
+
2971
+ export async function loadThemeDemo(id) {
2972
+ switch (id) {
2973
+ ${cases}
2974
+ default: throw new Error('Theme demo not found: ' + id);
2975
+ }
2976
+ }
2977
+ `;
2978
+ }
2979
+ function themesPlugin(opts) {
2980
+ const { userCwd, config } = opts;
2981
+ const themesDir = config.themesDir ?? "themes";
2982
+ const themesRoot = path.resolve(userCwd, themesDir);
2983
+ let isDev = false;
2984
+ return {
2985
+ name: "open-pages:themes",
2986
+ config(_c, env) {
2987
+ isDev = env.command === "serve";
2988
+ },
2989
+ resolveId(id) {
2990
+ if (id === THEMES_VMOD) return resolved(THEMES_VMOD);
2991
+ return null;
2992
+ },
2993
+ async load(id) {
2994
+ if (id !== resolved(THEMES_VMOD)) return null;
2995
+ const files = await findThemes(userCwd, themesDir);
2996
+ const themes = await Promise.all(files.map((f) => readTheme(f, themesRoot)));
2997
+ return generateThemesModule(themes, isDev);
2998
+ },
2999
+ configureServer(server) {
3000
+ const isThemeFile = (p) => {
3001
+ const rel = path.relative(themesRoot, p);
3002
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
3003
+ if (rel.includes(path.sep)) return false;
3004
+ return /\.(md|demo\.(tsx|jsx|ts|js))$/.test(rel);
3005
+ };
3006
+ let reloadTimer = null;
3007
+ const reload = () => {
3008
+ if (reloadTimer) clearTimeout(reloadTimer);
3009
+ reloadTimer = setTimeout(() => {
3010
+ reloadTimer = null;
3011
+ const mod = server.moduleGraph.getModuleById(resolved(THEMES_VMOD));
3012
+ if (mod) server.moduleGraph.invalidateModule(mod);
3013
+ server.ws.send({ type: "full-reload" });
3014
+ }, 150);
3015
+ };
3016
+ if (existsSync(themesRoot)) server.watcher.add(themesRoot);
3017
+ server.watcher.on("add", (p) => {
3018
+ if (isThemeFile(p)) reload();
3019
+ });
3020
+ server.watcher.on("unlink", (p) => {
3021
+ if (isThemeFile(p)) reload();
3022
+ });
3023
+ server.watcher.on("change", (p) => {
3024
+ if (isThemeFile(p)) reload();
3025
+ });
3026
+ }
3027
+ };
3028
+ }
3029
+
3030
+ //#endregion
3031
+ //#region src/vite/config.ts
3032
+ const CORE_VERSION = readCoreVersion();
3033
+ async function createViteConfig(opts) {
3034
+ const userCwd = path.resolve(opts.userCwd);
3035
+ const config = opts.config ?? await loadUserConfig(userCwd);
3036
+ const pagesDir = config.pagesDir ?? "pages";
3037
+ const themesDir = config.themesDir ?? "themes";
3038
+ const assetsDir = config.assetsDir ?? "assets";
3039
+ const pagesAbs = path.resolve(userCwd, pagesDir);
3040
+ const themesAbs = path.resolve(userCwd, themesDir);
3041
+ const assetsAbs = path.resolve(userCwd, assetsDir);
3042
+ return {
3043
+ base: config.base ?? "/",
3044
+ root: APP_ROOT,
3045
+ configFile: false,
3046
+ envDir: userCwd,
3047
+ plugins: [
3048
+ locTagsPlugin({
3049
+ userCwd,
3050
+ pagesDir
3051
+ }),
3052
+ react(),
3053
+ tailwindcss(),
3054
+ openPagesPlugin({
3055
+ userCwd,
3056
+ config,
3057
+ coreVersion: CORE_VERSION
3058
+ }),
3059
+ themesPlugin({
3060
+ userCwd,
3061
+ config
3062
+ }),
3063
+ designPlugin({ userCwd }),
3064
+ apiPlugin({
3065
+ userCwd,
3066
+ pagesDir,
3067
+ assetsDir,
3068
+ coreVersion: CORE_VERSION
3069
+ }),
3070
+ currentPlugin({
3071
+ userCwd,
3072
+ pagesDir
3073
+ })
3074
+ ],
3075
+ resolve: { alias: {
3076
+ "@": APP_ROOT,
3077
+ "@assets": assetsAbs
3078
+ } },
3079
+ optimizeDeps: {
3080
+ entries: [path.join(APP_ROOT, "main.tsx"), path.join(APP_ROOT, "frame", "main.tsx")],
3081
+ include: [
3082
+ "react",
3083
+ "react-dom",
3084
+ "react-dom/client",
3085
+ "next-themes",
3086
+ "react-router-dom",
3087
+ "@base-ui/react",
3088
+ "use-sync-external-store/shim",
3089
+ "use-sync-external-store/shim/with-selector",
3090
+ "lucide-react",
3091
+ "clsx",
3092
+ "tailwind-merge",
3093
+ "class-variance-authority",
3094
+ "emoji-picker-react"
3095
+ ],
3096
+ esbuildOptions: {
3097
+ target: "es2022",
3098
+ plugins: [{
3099
+ name: "open-pages:virtual-externals",
3100
+ setup(build$1) {
3101
+ build$1.onResolve({ filter: /^virtual:open-pages\// }, (args) => ({
3102
+ path: args.path,
3103
+ external: true
3104
+ }));
3105
+ }
3106
+ }]
3107
+ }
3108
+ },
3109
+ server: {
3110
+ port: config.port ?? 5173,
3111
+ ...config.allowedHosts !== void 0 ? { allowedHosts: config.allowedHosts } : {},
3112
+ fs: { allow: [
3113
+ APP_ROOT,
3114
+ PKG_ROOT,
3115
+ searchForWorkspaceRoot(userCwd),
3116
+ userCwd,
3117
+ pagesAbs,
3118
+ themesAbs,
3119
+ assetsAbs
3120
+ ] }
3121
+ },
3122
+ build: {
3123
+ outDir: path.resolve(userCwd, "dist"),
3124
+ emptyOutDir: true,
3125
+ target: "es2022",
3126
+ rollupOptions: { input: {
3127
+ index: path.join(APP_ROOT, "index.html"),
3128
+ frame: path.join(APP_ROOT, "frame.html")
3129
+ } }
3130
+ }
3131
+ };
3132
+ }
3133
+
3134
+ //#endregion
3135
+ export { DEV_SUPERVISED_ENV, RESTART_EXIT_CODE, createViteConfig };