@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,632 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { loadConfigFromFile, normalizePath } from "vite";
5
+ import { parse } from "@babel/parser";
6
+ import { createRequire } from "node:module";
7
+ import fg from "fast-glob";
8
+
9
+ //#region src/editing/page-ops.ts
10
+ const PAGE_ID_RE = /^[a-z0-9_-]+$/i;
11
+ function validatePageName(v) {
12
+ if (typeof v !== "string") return null;
13
+ const trimmed = v.trim();
14
+ if (trimmed.length < 1 || trimmed.length > 80) return null;
15
+ return trimmed;
16
+ }
17
+ function unwrapExpression(node) {
18
+ let current = node;
19
+ while (current && (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression")) current = current.expression;
20
+ return current;
21
+ }
22
+ function readMetaTitleInSource(source) {
23
+ let ast;
24
+ try {
25
+ ast = parse(source, {
26
+ sourceType: "module",
27
+ plugins: ["typescript", "jsx"],
28
+ errorRecovery: true
29
+ });
30
+ } catch {
31
+ return { kind: "unsupported" };
32
+ }
33
+ const body = ast.program?.body ?? [];
34
+ for (const stmt of body) {
35
+ if (stmt.type !== "ExportNamedDeclaration") continue;
36
+ const decl = stmt.declaration;
37
+ if (!decl || decl.type !== "VariableDeclaration") continue;
38
+ const declarations = decl.declarations ?? [];
39
+ for (const d of declarations) {
40
+ const id = d.id;
41
+ if (!id || id.type !== "Identifier" || id.name !== "meta") continue;
42
+ const init = unwrapExpression(d.init);
43
+ if (!init || init.type !== "ObjectExpression") return { kind: "unsupported" };
44
+ const properties = init.properties ?? [];
45
+ for (const property of properties) {
46
+ if (property.type !== "ObjectProperty" || property.computed) continue;
47
+ const key = property.key;
48
+ const keyName = key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? key.value : void 0;
49
+ if (keyName !== "title") continue;
50
+ const value = property.value;
51
+ if (value?.type === "StringLiteral" && typeof value.value === "string") return {
52
+ kind: "found",
53
+ title: value.value
54
+ };
55
+ if (value?.type === "TemplateLiteral") {
56
+ const expressions = value.expressions ?? [];
57
+ const quasis = value.quasis ?? [];
58
+ const firstValue = quasis[0]?.value;
59
+ const cooked = firstValue?.cooked;
60
+ const raw = firstValue?.raw;
61
+ if (expressions.length === 0 && typeof (cooked ?? raw) === "string") return {
62
+ kind: "found",
63
+ title: cooked ?? raw
64
+ };
65
+ }
66
+ return { kind: "unsupported" };
67
+ }
68
+ return { kind: "missing" };
69
+ }
70
+ }
71
+ return { kind: "missing" };
72
+ }
73
+ async function rmPageDir(pagesRoot, pageId) {
74
+ if (!PAGE_ID_RE.test(pageId)) return false;
75
+ const dir = path.resolve(pagesRoot, pageId);
76
+ if (!dir.startsWith(pagesRoot + path.sep)) return false;
77
+ try {
78
+ await fs.rm(dir, {
79
+ recursive: true,
80
+ force: true
81
+ });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ async function duplicatePageDir(pagesRoot, pageId, desiredId) {
88
+ if (!PAGE_ID_RE.test(pageId)) return {
89
+ ok: false,
90
+ status: 400,
91
+ error: "invalid pageId"
92
+ };
93
+ const root = path.resolve(pagesRoot);
94
+ const srcDir = path.resolve(root, pageId);
95
+ if (!srcDir.startsWith(root + path.sep)) return {
96
+ ok: false,
97
+ status: 400,
98
+ error: "invalid pageId"
99
+ };
100
+ try {
101
+ await fs.access(path.join(srcDir, "index.tsx"));
102
+ } catch {
103
+ return {
104
+ ok: false,
105
+ status: 404,
106
+ error: "page not found"
107
+ };
108
+ }
109
+ let newId;
110
+ if (desiredId !== void 0) {
111
+ if (!PAGE_ID_RE.test(desiredId)) return {
112
+ ok: false,
113
+ status: 400,
114
+ error: "invalid newId"
115
+ };
116
+ newId = desiredId;
117
+ const dstDir$1 = path.resolve(root, newId);
118
+ if (!dstDir$1.startsWith(root + path.sep)) return {
119
+ ok: false,
120
+ status: 400,
121
+ error: "invalid newId"
122
+ };
123
+ try {
124
+ await fs.access(dstDir$1);
125
+ return {
126
+ ok: false,
127
+ status: 409,
128
+ error: "page already exists"
129
+ };
130
+ } catch {}
131
+ } else {
132
+ let suffix = 1;
133
+ while (true) {
134
+ newId = suffix === 1 ? `${pageId}-copy` : `${pageId}-copy-${suffix}`;
135
+ try {
136
+ await fs.access(path.resolve(root, newId));
137
+ suffix++;
138
+ } catch {
139
+ break;
140
+ }
141
+ }
142
+ }
143
+ const dstDir = path.resolve(root, newId);
144
+ if (!dstDir.startsWith(root + path.sep)) return {
145
+ ok: false,
146
+ status: 400,
147
+ error: "invalid newId"
148
+ };
149
+ const srcEntry = path.join(srcDir, "index.tsx");
150
+ let copiedEntrySource;
151
+ try {
152
+ const source = await fs.readFile(srcEntry, "utf8");
153
+ const metaTitle = readMetaTitleInSource(source);
154
+ if (metaTitle.kind === "unsupported") return {
155
+ ok: false,
156
+ status: 422,
157
+ error: "could not update copied page title"
158
+ };
159
+ const title = metaTitle.kind === "found" ? metaTitle.title : pageId;
160
+ const updated = updateMetaTitleInSource(source, `${title} (copy)`);
161
+ if (updated === null) return {
162
+ ok: false,
163
+ status: 422,
164
+ error: "could not update copied page title"
165
+ };
166
+ copiedEntrySource = updated;
167
+ } catch {
168
+ return {
169
+ ok: false,
170
+ status: 404,
171
+ error: "page not found"
172
+ };
173
+ }
174
+ try {
175
+ await fs.cp(srcDir, dstDir, {
176
+ recursive: true,
177
+ errorOnExist: true,
178
+ force: false
179
+ });
180
+ await fs.writeFile(path.join(dstDir, "index.tsx"), copiedEntrySource, "utf8");
181
+ return {
182
+ ok: true,
183
+ pageId: newId
184
+ };
185
+ } catch (err) {
186
+ if (err.code === "EEXIST") return {
187
+ ok: false,
188
+ status: 409,
189
+ error: "page already exists"
190
+ };
191
+ return {
192
+ ok: false,
193
+ status: 500,
194
+ error: String(err.message ?? err)
195
+ };
196
+ }
197
+ }
198
+ function resolvePageEntry(pagesRoot, pageId) {
199
+ if (!PAGE_ID_RE.test(pageId)) return null;
200
+ const dir = path.resolve(pagesRoot, pageId);
201
+ if (!dir.startsWith(pagesRoot + path.sep)) return null;
202
+ return path.join(dir, "index.tsx");
203
+ }
204
+ function escapeSingleQuoted(s) {
205
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
206
+ }
207
+ /**
208
+ * Rewrite (or insert) the `title` field in the page module's `export const meta`.
209
+ *
210
+ * Strategy:
211
+ * 1. Find `export const meta` and brace-match its object literal.
212
+ * 2. If the object already has a `title: '...'` entry, replace the literal.
213
+ * 3. If the object exists but has no title, inject a new `title: '...'` line
214
+ * as the first property (preserving the author's surrounding indentation).
215
+ * 4. If there is no `meta` export at all, insert a fresh one right before
216
+ * `export default`.
217
+ *
218
+ * Returns the rewritten source, or `null` if the file shape was too surprising
219
+ * to touch safely (e.g. `export default` missing when we'd need to inject meta).
220
+ */
221
+ function updateMetaTitleInSource(source, title) {
222
+ const newLiteral = `'${escapeSingleQuoted(title)}'`;
223
+ const metaStart = source.search(/export\s+const\s+meta\b/);
224
+ if (metaStart !== -1) {
225
+ const eqIdx = source.indexOf("=", metaStart);
226
+ if (eqIdx === -1) return null;
227
+ const openBrace = source.indexOf("{", eqIdx);
228
+ if (openBrace === -1) return null;
229
+ let depth = 0;
230
+ let closeBrace = -1;
231
+ for (let i = openBrace; i < source.length; i++) {
232
+ const ch = source[i];
233
+ if (ch === "{") depth++;
234
+ else if (ch === "}") {
235
+ depth--;
236
+ if (depth === 0) {
237
+ closeBrace = i;
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ if (closeBrace === -1) return null;
243
+ const body = source.slice(openBrace + 1, closeBrace);
244
+ const titleRe = /(^|[\s,{])(title\s*:\s*)(['"`])((?:\\.|(?!\3).)*)\3/;
245
+ const match = body.match(titleRe);
246
+ if (match) {
247
+ const newBody = body.replace(titleRe, `${match[1]}${match[2]}${newLiteral}`);
248
+ return source.slice(0, openBrace + 1) + newBody + source.slice(closeBrace);
249
+ }
250
+ const firstIndentMatch = body.match(/\n([ \t]+)\S/);
251
+ const indent = firstIndentMatch ? firstIndentMatch[1] : " ";
252
+ const trimmedBody = body.replace(/^\s*\n?/, "");
253
+ const needsSeparator = trimmedBody.trim().length > 0;
254
+ const insertion$1 = `\n${indent}title: ${newLiteral}${needsSeparator ? "," : ""}`;
255
+ return source.slice(0, openBrace + 1) + insertion$1 + body + source.slice(closeBrace);
256
+ }
257
+ const exportDefaultIdx = source.search(/export\s+default\b/);
258
+ if (exportDefaultIdx === -1) return null;
259
+ const insertion = `export const meta: PageMeta = { title: ${newLiteral} };\n\n`;
260
+ return source.slice(0, exportDefaultIdx) + insertion + source.slice(exportDefaultIdx);
261
+ }
262
+
263
+ //#endregion
264
+ //#region src/vite/open-pages-plugin.ts
265
+ const CONFIG_FILE = "open-pages.config.ts";
266
+ const PAGES_VMOD = "virtual:open-pages/pages";
267
+ const CONFIG_VMOD = "virtual:open-pages/config";
268
+ const FOLDERS_VMOD = "virtual:open-pages/folders";
269
+ const PAGES_CSS_VMOD = "virtual:open-pages/pages.css";
270
+ const ENTRY_GLOB = "*/index.{tsx,jsx,ts,js,html}";
271
+ const REACT_ENTRY_RE = /^index\.(tsx|jsx|ts|js)$/;
272
+ async function readFoldersManifest(file) {
273
+ try {
274
+ const raw = await fs.readFile(file, "utf8");
275
+ const parsed = JSON.parse(raw);
276
+ return {
277
+ folders: Array.isArray(parsed.folders) ? parsed.folders : [],
278
+ assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
279
+ };
280
+ } catch (err) {
281
+ if (err.code === "ENOENT") return {
282
+ folders: [],
283
+ assignments: {}
284
+ };
285
+ throw err;
286
+ }
287
+ }
288
+ function resolved(id) {
289
+ return `\0${id}`;
290
+ }
291
+ /**
292
+ * Every `pages/<id>/index.*` entry. A folder with both a React entry and an
293
+ * `index.html` is a React page — the HTML is then a plain asset of the page.
294
+ */
295
+ async function findPages(userCwd, pagesDir) {
296
+ const abs = path.resolve(userCwd, pagesDir);
297
+ if (!existsSync(abs)) return [];
298
+ const hits = await fg(ENTRY_GLOB, {
299
+ cwd: abs,
300
+ absolute: true,
301
+ onlyFiles: true
302
+ });
303
+ const byId = new Map();
304
+ for (const file of hits.sort()) {
305
+ const rel = path.relative(abs, file);
306
+ const [id, name] = rel.split(path.sep);
307
+ const kind = REACT_ENTRY_RE.test(name) ? "react" : "html";
308
+ const existing = byId.get(id);
309
+ if (!existing || existing.kind === "html" && kind === "react") byId.set(id, {
310
+ id,
311
+ kind,
312
+ file
313
+ });
314
+ }
315
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
316
+ }
317
+ const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
318
+ const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
319
+ const META_TITLE_RE = /(?:^|[\s,{])title\s*:\s*['"]([^'"]+)['"]/;
320
+ const HTML_TITLE_RE = /<title[^>]*>([^<]*)<\/title>/i;
321
+ const EMPTY_META = {
322
+ title: null,
323
+ theme: null,
324
+ createdAt: null
325
+ };
326
+ function extractMeta(src) {
327
+ const metaStart = src.search(/export\s+const\s+meta\b/);
328
+ if (metaStart === -1) return EMPTY_META;
329
+ const eqIdx = src.indexOf("=", metaStart);
330
+ if (eqIdx === -1) return EMPTY_META;
331
+ const openBrace = src.indexOf("{", eqIdx);
332
+ if (openBrace === -1) return EMPTY_META;
333
+ let depth = 0;
334
+ let closeBrace = -1;
335
+ for (let i = openBrace; i < src.length; i++) {
336
+ const ch = src[i];
337
+ if (ch === "{") depth++;
338
+ else if (ch === "}") {
339
+ depth--;
340
+ if (depth === 0) {
341
+ closeBrace = i;
342
+ break;
343
+ }
344
+ }
345
+ }
346
+ if (closeBrace === -1) return EMPTY_META;
347
+ const body = src.slice(openBrace + 1, closeBrace);
348
+ return {
349
+ title: body.match(META_TITLE_RE)?.[1] ?? null,
350
+ theme: body.match(META_THEME_RE)?.[1] ?? null,
351
+ createdAt: body.match(META_CREATED_AT_RE)?.[1] ?? null
352
+ };
353
+ }
354
+ function extractHtmlMeta(src) {
355
+ const title = src.match(HTML_TITLE_RE)?.[1]?.trim() ?? null;
356
+ return {
357
+ title: title || null,
358
+ theme: null,
359
+ createdAt: null
360
+ };
361
+ }
362
+ async function readPageMeta(entry) {
363
+ try {
364
+ const src = await fs.readFile(entry.file, "utf8");
365
+ return entry.kind === "html" ? extractHtmlMeta(src) : extractMeta(src);
366
+ } catch {
367
+ return EMPTY_META;
368
+ }
369
+ }
370
+ function parseCreatedAtMs(iso) {
371
+ if (!iso) return null;
372
+ const ms = Date.parse(iso);
373
+ return Number.isFinite(ms) ? ms : null;
374
+ }
375
+ const warnedInvalidPageIds = new Set();
376
+ async function generatePagesModule(entries, isDev) {
377
+ const scanned = await Promise.all(entries.map(async (entry) => {
378
+ const importPath = isDev ? `@fs/${normalizePath(entry.file).replace(/^\/+/, "")}` : entry.file;
379
+ const meta = await readPageMeta(entry);
380
+ return {
381
+ ...entry,
382
+ importPath,
383
+ meta,
384
+ createdAt: parseCreatedAtMs(meta.createdAt)
385
+ };
386
+ }));
387
+ const valid = scanned.filter((e) => PAGE_ID_RE.test(e.id));
388
+ const ignored = scanned.filter((e) => !PAGE_ID_RE.test(e.id)).map((e) => e.id);
389
+ const kinds = {};
390
+ const themesMap = {};
391
+ const createdAtMap = {};
392
+ for (const e of valid) {
393
+ kinds[e.id] = e.kind;
394
+ if (e.meta.theme) themesMap[e.id] = e.meta.theme;
395
+ if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
396
+ }
397
+ const importTokens = JSON.stringify(Object.fromEntries(valid.filter((e) => e.kind === "react").map((e) => [e.id, 0])));
398
+ const devRuntime = isDev ? `
399
+ const pageImportTokens = ${importTokens};
400
+ if (import.meta.hot) {
401
+ import.meta.hot.on('open-pages:page-changed', (data) => {
402
+ const ids = Array.isArray(data?.pageIds) ? data.pageIds : data?.pageId ? [data.pageId] : [];
403
+ const token = Date.now();
404
+ for (const id of ids) {
405
+ if (Object.prototype.hasOwnProperty.call(pageImportTokens, id)) pageImportTokens[id] = token;
406
+ }
407
+ });
408
+ }
409
+ ` : "";
410
+ const cases = valid.map((e) => {
411
+ if (e.kind === "html") {
412
+ const mod = { meta: { title: e.meta.title ?? e.id } };
413
+ return ` case ${JSON.stringify(e.id)}: return Promise.resolve(${JSON.stringify(mod)});`;
414
+ }
415
+ const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(`${e.importPath}?t=`)} + pageImportTokens[${JSON.stringify(e.id)}])` : `import(${JSON.stringify(e.importPath)})`;
416
+ return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
417
+ }).join("\n");
418
+ const code = `// virtual:open-pages/pages — generated
419
+ export const pageIds = ${JSON.stringify(valid.map((e) => e.id))};
420
+ export const pageKinds = ${JSON.stringify(kinds)};
421
+ export const pageThemes = ${JSON.stringify(themesMap)};
422
+ export const pageCreatedAt = ${JSON.stringify(createdAtMap)};
423
+ ${devRuntime}
424
+
425
+ export async function loadPage(id) {
426
+ switch (id) {
427
+ ${cases}
428
+ default: throw new Error('Page not found: ' + id);
429
+ }
430
+ }
431
+ `;
432
+ return {
433
+ code,
434
+ ignored
435
+ };
436
+ }
437
+ /**
438
+ * Tailwind for page source. The workspace UI has its own stylesheet rooted
439
+ * at the core app; pages live in the user's project, so they get a generated
440
+ * stylesheet whose `@source` points at their directories. Written under
441
+ * node_modules so Tailwind resolves `tailwindcss` from the workspace.
442
+ */
443
+ function pagesCssFile(userCwd) {
444
+ return path.join(userCwd, "node_modules", ".open-pages", "pages.css");
445
+ }
446
+ const TAILWIND_CSS = normalizePath(createRequire(import.meta.url).resolve("tailwindcss/index.css"));
447
+ function writePagesCss(userCwd, sources) {
448
+ const file = pagesCssFile(userCwd);
449
+ const lines = [`@import ${JSON.stringify(TAILWIND_CSS)} source(none);`];
450
+ for (const dir of sources) lines.push(`@source ${JSON.stringify(normalizePath(dir))};`);
451
+ mkdirSync(path.dirname(file), { recursive: true });
452
+ writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
453
+ return file;
454
+ }
455
+ function rewriteHtmlUrls(html, pageDir) {
456
+ const prefix = `/@fs/${normalizePath(pageDir).replace(/^\/+/, "")}/`;
457
+ return html.replace(/(\s(?:src|href)=)(["'])(?!\/|[a-z][a-z0-9+.-]*:|#|\2)(?:\.\/)?([^"']+)\2/gi, (_m, attr, quote, rel) => `${attr}${quote}${prefix}${rel}${quote}`);
458
+ }
459
+ function openPagesPlugin(opts) {
460
+ const { userCwd, config, coreVersion } = opts;
461
+ const pagesDir = config.pagesDir ?? "pages";
462
+ const themesDir = config.themesDir ?? "themes";
463
+ const pagesRoot = path.resolve(userCwd, pagesDir);
464
+ const themesRoot = path.resolve(userCwd, themesDir);
465
+ const foldersManifestPath = path.join(pagesRoot, ".folders.json");
466
+ let isDev = false;
467
+ let cssFile = "";
468
+ const pageIdForEntry = (p) => {
469
+ const rel = path.relative(pagesRoot, p);
470
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
471
+ const parts = rel.split(path.sep);
472
+ if (parts.length !== 2) return null;
473
+ if (REACT_ENTRY_RE.test(parts[1])) return {
474
+ id: parts[0],
475
+ kind: "react"
476
+ };
477
+ if (parts[1] === "index.html") return {
478
+ id: parts[0],
479
+ kind: "html"
480
+ };
481
+ return null;
482
+ };
483
+ let pageChangeTimer = null;
484
+ const pendingPageChanges = new Set();
485
+ const queuePageChanged = (server, id) => {
486
+ pendingPageChanges.add(id);
487
+ if (pageChangeTimer) clearTimeout(pageChangeTimer);
488
+ pageChangeTimer = setTimeout(() => {
489
+ pageChangeTimer = null;
490
+ const mod = server.moduleGraph.getModuleById(resolved(PAGES_VMOD));
491
+ if (mod) server.moduleGraph.invalidateModule(mod);
492
+ const pageIds = Array.from(pendingPageChanges);
493
+ pendingPageChanges.clear();
494
+ server.ws.send({
495
+ type: "custom",
496
+ event: "open-pages:page-changed",
497
+ data: { pageIds }
498
+ });
499
+ }, 100);
500
+ };
501
+ return {
502
+ name: "open-pages",
503
+ config(_c, env) {
504
+ isDev = env.command === "serve";
505
+ cssFile = writePagesCss(userCwd, [pagesRoot, themesRoot]);
506
+ return { server: { fs: { allow: [userCwd] } } };
507
+ },
508
+ resolveId(id) {
509
+ if (id === PAGES_VMOD) return resolved(PAGES_VMOD);
510
+ if (id === CONFIG_VMOD) return resolved(CONFIG_VMOD);
511
+ if (id === FOLDERS_VMOD) return resolved(FOLDERS_VMOD);
512
+ if (id === PAGES_CSS_VMOD) return cssFile;
513
+ return null;
514
+ },
515
+ async load(id) {
516
+ if (id === resolved(PAGES_VMOD)) {
517
+ const entries = await findPages(userCwd, pagesDir);
518
+ const { code, ignored } = await generatePagesModule(entries, isDev);
519
+ for (const pageId of ignored) {
520
+ if (warnedInvalidPageIds.has(pageId)) continue;
521
+ warnedInvalidPageIds.add(pageId);
522
+ this.warn(`Ignoring page folder "${pageId}": page ids must match ${PAGE_ID_RE} (lowercase/uppercase letters, digits, "-", "_"). Rename the folder under "${pagesDir}/" to a kebab-case id so it appears in the browser and can be moved into folders.`);
523
+ }
524
+ return code;
525
+ }
526
+ if (id === resolved(CONFIG_VMOD)) {
527
+ const userBuild = config.build ?? {};
528
+ const buildResolved = isDev ? {
529
+ showPageBrowser: true,
530
+ showPageUi: true
531
+ } : {
532
+ showPageBrowser: userBuild.showPageBrowser ?? true,
533
+ showPageUi: userBuild.showPageUi ?? true
534
+ };
535
+ const resolvedConfig = {
536
+ ...config,
537
+ build: buildResolved,
538
+ version: coreVersion
539
+ };
540
+ return `export default ${JSON.stringify(resolvedConfig)};\n`;
541
+ }
542
+ if (id === resolved(FOLDERS_VMOD)) {
543
+ const manifest = await readFoldersManifest(foldersManifestPath);
544
+ return `export default ${JSON.stringify(manifest)};\n`;
545
+ }
546
+ return null;
547
+ },
548
+ handleHotUpdate(ctx) {
549
+ const entry = pageIdForEntry(ctx.file);
550
+ if (!entry) return;
551
+ queuePageChanged(ctx.server, entry.id);
552
+ return [];
553
+ },
554
+ configureServer(server) {
555
+ let reloadTimer = null;
556
+ const reload = () => {
557
+ if (reloadTimer) clearTimeout(reloadTimer);
558
+ reloadTimer = setTimeout(() => {
559
+ reloadTimer = null;
560
+ const mod = server.moduleGraph.getModuleById(resolved(PAGES_VMOD));
561
+ if (mod) server.moduleGraph.invalidateModule(mod);
562
+ server.ws.send({ type: "full-reload" });
563
+ }, 150);
564
+ };
565
+ if (existsSync(pagesRoot)) server.watcher.add(pagesRoot);
566
+ server.watcher.on("add", (p) => {
567
+ if (pageIdForEntry(p)) reload();
568
+ });
569
+ server.watcher.on("unlink", (p) => {
570
+ if (pageIdForEntry(p)) reload();
571
+ });
572
+ server.watcher.on("change", (p) => {
573
+ const entry = pageIdForEntry(p);
574
+ if (entry?.kind === "html") queuePageChanged(server, entry.id);
575
+ });
576
+ let foldersTimer = null;
577
+ const invalidateFolders = () => {
578
+ if (foldersTimer) clearTimeout(foldersTimer);
579
+ foldersTimer = setTimeout(() => {
580
+ foldersTimer = null;
581
+ const mod = server.moduleGraph.getModuleById(resolved(FOLDERS_VMOD));
582
+ if (mod) server.moduleGraph.invalidateModule(mod);
583
+ }, 100);
584
+ };
585
+ server.watcher.add(foldersManifestPath);
586
+ server.watcher.on("change", (p) => {
587
+ if (p === foldersManifestPath) invalidateFolders();
588
+ });
589
+ server.watcher.on("add", (p) => {
590
+ if (p === foldersManifestPath) invalidateFolders();
591
+ });
592
+ server.watcher.on("unlink", (p) => {
593
+ if (p === foldersManifestPath) invalidateFolders();
594
+ });
595
+ server.middlewares.use("/__page", async (req, res, next) => {
596
+ const url = new URL(req.url ?? "/", "http://local");
597
+ const m = url.pathname.match(/^\/([^/]+)\/(?:index\.html)?$/);
598
+ if (!m || (req.method ?? "GET") !== "GET") return next();
599
+ const pageId = decodeURIComponent(m[1]);
600
+ if (!PAGE_ID_RE.test(pageId)) return next();
601
+ const pageDir = path.join(pagesRoot, pageId);
602
+ const file = path.join(pageDir, "index.html");
603
+ let raw;
604
+ try {
605
+ raw = await fs.readFile(file, "utf8");
606
+ } catch {
607
+ return next();
608
+ }
609
+ try {
610
+ const html = await server.transformIndexHtml(`/__page/${pageId}/index.html`, rewriteHtmlUrls(raw, pageDir), req.originalUrl);
611
+ res.statusCode = 200;
612
+ res.setHeader("content-type", "text/html");
613
+ res.end(html);
614
+ } catch (err) {
615
+ next(err);
616
+ }
617
+ });
618
+ }
619
+ };
620
+ }
621
+ async function loadUserConfig(userCwd) {
622
+ const file = path.join(userCwd, CONFIG_FILE);
623
+ if (!existsSync(file)) return {};
624
+ const loaded = await loadConfigFromFile({
625
+ command: "serve",
626
+ mode: "development"
627
+ }, file, userCwd, "silent");
628
+ return loaded?.config ?? {};
629
+ }
630
+
631
+ //#endregion
632
+ export { PAGE_ID_RE, duplicatePageDir, extractMeta, findPages, loadUserConfig, openPagesPlugin, resolvePageEntry, rmPageDir, updateMetaTitleInSource, validatePageName };
@@ -0,0 +1,21 @@
1
+ import "./design-C13iz9_4.js";
2
+ import "./open-pages-plugin-D_DN_zjh.js";
3
+ import { createViteConfig } from "./config-L0IbktJ0.js";
4
+ import "./update-package-Cxt57pz7.js";
5
+ import "./version-B6AiiGvT.js";
6
+ import { mergeConfig, preview as preview$1 } from "vite";
7
+
8
+ //#region src/cli/preview.ts
9
+ async function preview(opts = {}) {
10
+ const base = await createViteConfig({ userCwd: process.cwd() });
11
+ const config = mergeConfig(base, { preview: {
12
+ ...opts.port !== void 0 ? { port: opts.port } : {},
13
+ ...opts.host !== void 0 ? { host: opts.host } : {},
14
+ ...opts.open !== void 0 ? { open: opts.open } : {}
15
+ } });
16
+ const server = await preview$1(config);
17
+ server.printUrls();
18
+ }
19
+
20
+ //#endregion
21
+ export { preview };