@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.
- package/LICENSE +23 -0
- package/README.md +108 -0
- package/bin.js +2 -0
- package/dist/build-TP72kiw7.js +35 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +100 -0
- package/dist/config-C9or4m4J.d.ts +23 -0
- package/dist/config-L0IbktJ0.js +3135 -0
- package/dist/design-C13iz9_4.js +35 -0
- package/dist/dev-I2IyEh5W.js +97 -0
- package/dist/export-B-kBRWB1.js +4 -0
- package/dist/export-ic28osQP.js +131 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +3 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +881 -0
- package/dist/open-pages-plugin-D_DN_zjh.js +632 -0
- package/dist/preview-BrDuvgAN.js +21 -0
- package/dist/sync-Be_hxSOF.js +139 -0
- package/dist/sync-lKEXyPOh.js +3 -0
- package/dist/types-C5NCdDT-.d.ts +219 -0
- package/dist/update-CV62NhBT.js +37 -0
- package/dist/update-package-Cxt57pz7.js +152 -0
- package/dist/version-B6AiiGvT.js +26 -0
- package/dist/version-CeffgdfZ.js +3 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +7 -0
- package/env.d.ts +59 -0
- package/package.json +106 -0
- package/skills/apply-comments/SKILL.md +86 -0
- package/skills/create-page/SKILL.md +92 -0
- package/skills/create-theme/SKILL.md +223 -0
- package/skills/current-page/SKILL.md +106 -0
- package/skills/page-authoring/SKILL.md +148 -0
- package/skills/page-authoring/references/assets-and-fonts.md +85 -0
- package/skills/page-authoring/references/html-pages.md +65 -0
- package/skills/page-authoring/references/interactivity.md +77 -0
- package/skills/page-authoring/references/layout-and-responsive.md +71 -0
- package/skills/page-authoring/references/typography-and-color.md +60 -0
- package/src/app/app.tsx +45 -0
- package/src/app/components/asset-view.tsx +1665 -0
- package/src/app/components/command/command-menu.tsx +227 -0
- package/src/app/components/command/command.tsx +142 -0
- package/src/app/components/command/home-command-menu.tsx +96 -0
- package/src/app/components/icon-tooltip.tsx +34 -0
- package/src/app/components/language-toggle.tsx +46 -0
- package/src/app/components/sidebar/folder-item.tsx +269 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +139 -0
- package/src/app/components/sidebar/sidebar.tsx +302 -0
- package/src/app/components/theme-toggle.tsx +66 -0
- package/src/app/components/themes/theme-detail.tsx +226 -0
- package/src/app/components/themes/themes-gallery.tsx +104 -0
- package/src/app/components/ui/badge.tsx +48 -0
- package/src/app/components/ui/button.tsx +93 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +247 -0
- package/src/app/components/ui/dialog.tsx +155 -0
- package/src/app/components/ui/dropdown-menu.tsx +267 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +21 -0
- package/src/app/components/ui/popover.tsx +82 -0
- package/src/app/components/ui/progress.tsx +33 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +195 -0
- package/src/app/components/ui/separator.tsx +26 -0
- package/src/app/components/ui/slider.tsx +68 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +84 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +75 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/frame/inspect.ts +140 -0
- package/src/app/frame/main.tsx +91 -0
- package/src/app/frame.html +12 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/asset-filter.test.ts +176 -0
- package/src/app/lib/asset-filter.ts +80 -0
- package/src/app/lib/assets.ts +257 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/frame.ts +38 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-thumb.tsx +46 -0
- package/src/app/lib/pages.ts +28 -0
- package/src/app/lib/sdk.ts +43 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-page-module.ts +48 -0
- package/src/app/lib/use-page-titles.ts +29 -0
- package/src/app/lib/use-restart-server.ts +78 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +252 -0
- package/src/app/routes/home.tsx +846 -0
- package/src/app/routes/page.tsx +329 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +405 -0
- package/src/app/virtual.d.ts +53 -0
- package/src/locale/en.ts +218 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +221 -0
- package/src/locale/types.ts +215 -0
- package/src/locale/zh-cn.ts +218 -0
- 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 };
|