@autono/open-pages 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-6hM9Ga2d.js → build-CCoxDxL-.js} +11 -10
- package/dist/cli/bin.d.ts +1 -1
- package/dist/cli/bin.js +427 -30
- package/dist/{config-CBbwLHOo.js → config-D_U_aCQz.js} +484 -460
- package/dist/{config-C-sasGou.d.ts → config-p-TWNXbv.d.ts} +4 -4
- package/dist/{design-C13iz9_4.js → design-Cpqx7SJs.js} +1 -2
- package/dist/{dev-BO0cHiYy.js → dev-CTrB263u.js} +40 -20
- package/dist/{export-DbVwSmKW.js → export-enKL9R7x.js} +7 -10
- package/dist/index.d.ts +6 -8
- package/dist/index.js +2 -3
- package/dist/locale/index.d.ts +8 -14
- package/dist/locale/index.js +67 -32
- package/dist/{open-pages-plugin-C0poXLsx.js → open-pages-plugin-C8cFhMB9.js} +23 -38
- package/dist/preview-Dmzyd1cf.js +24 -0
- package/dist/{sync-ui-CDydLpY4.js → sync-ui-BzRiAGCK.js} +3 -5
- package/dist/{types-BzxkFHhd.d.ts → types-DDdUFUIS.d.ts} +19 -6
- package/dist/{update-D1U8TVwd.js → update-CWaCKlnl.js} +4 -7
- package/dist/{update-package-D9zws9nc.js → update-package-GaNsVdr8.js} +44 -22
- package/dist/{version-B6AiiGvT.js → version-DZrXLoAD.js} +19 -3
- package/dist/vite/index.d.ts +3 -7
- package/dist/vite/index.js +2 -7
- package/package.json +22 -21
- package/skills/apply-comments/SKILL.md +1 -0
- package/skills/create-page/SKILL.md +5 -1
- package/skills/create-theme/SKILL.md +2 -0
- package/skills/page-authoring/SKILL.md +3 -0
- package/skills/web-design-guidelines/SKILL.md +41 -0
- package/src/app/app.tsx +19 -14
- package/src/app/assets/autono-social.png +0 -0
- package/src/app/assets/autono.svg +7 -0
- package/src/app/components/asset-view.tsx +510 -26
- package/src/app/components/command/command-menu.tsx +5 -1
- package/src/app/components/command/command.tsx +4 -2
- package/src/app/components/command/home-command-menu.tsx +6 -6
- package/src/app/components/icon-tooltip.tsx +24 -15
- package/src/app/components/sidebar/folder-item.tsx +46 -19
- package/src/app/components/sidebar/icon-picker.tsx +1 -1
- package/src/app/components/sidebar/sidebar-footer.tsx +18 -7
- package/src/app/components/sidebar/sidebar.tsx +91 -64
- package/src/app/components/theme-toggle.tsx +4 -5
- package/src/app/components/themes/theme-detail.tsx +6 -3
- package/src/app/components/themes/themes-gallery.tsx +12 -7
- package/src/app/index.html +33 -2
- package/src/app/lib/assets.ts +33 -0
- package/src/app/lib/last-home-location.ts +18 -0
- package/src/app/main.tsx +10 -1
- package/src/app/routes/home-shell.tsx +82 -61
- package/src/app/routes/home.tsx +42 -39
- package/src/app/routes/page.tsx +30 -10
- package/src/app/routes/themes.tsx +6 -8
- package/src/app/styles.css +40 -4
- package/src/app/virtual.d.ts +1 -0
- package/src/locale/en.ts +17 -6
- package/src/locale/ja.ts +18 -6
- package/src/locale/types.ts +17 -4
- package/src/locale/zh-cn.ts +19 -8
- package/src/locale/zh-tw.ts +17 -6
- package/src/shared/update-package.ts +193 -0
- package/workspace/deps.json +1 -1
- package/dist/export-BsK6OCpc.js +0 -4
- package/dist/preview-sOKhzyWB.js +0 -21
- package/dist/sync-Be_hxSOF.js +0 -139
- package/dist/sync-lKEXyPOh.js +0 -3
- package/dist/version-CeffgdfZ.js +0 -3
- package/src/app/components/language-toggle.tsx +0 -46
- package/src/app/favicon.ico +0 -0
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import fs from "node:fs/promises";
|
|
1
|
+
import { n as PKG_ROOT, r as readCoreVersion, t as APP_ROOT } from "./version-DZrXLoAD.js";
|
|
2
|
+
import { n as defaultDesign } from "./design-Cpqx7SJs.js";
|
|
3
|
+
import { c as localOpenPagesCommand, i as formatCommand, l as runCommand, n as detectPackageManager, o as invalidateLatestCache, r as fetchLatest, s as isOutdated, u as updateCommandFor } from "./update-package-GaNsVdr8.js";
|
|
4
|
+
import { a as openPagesPlugin, c as resolvePageEntry, d as validatePageName, i as loadUserConfig, l as rmPageDir, o as PAGE_ID_RE, r as invalidatePagesModule, s as duplicatePageDir, u as updateMetaTitleInSource } from "./open-pages-plugin-C8cFhMB9.js";
|
|
6
5
|
import path from "node:path";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
9
|
import tailwindcss from "@tailwindcss/vite";
|
|
10
10
|
import react from "@vitejs/plugin-react";
|
|
11
11
|
import { normalizePath, searchForWorkspaceRoot } from "vite";
|
|
12
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
13
|
import * as t from "@babel/types";
|
|
17
14
|
import { isJSXElement, isJSXFragment } from "@babel/types";
|
|
18
15
|
import fg from "fast-glob";
|
|
19
|
-
|
|
20
16
|
//#region src/editing/babel-walk.ts
|
|
21
|
-
const SKIP_KEYS = new Set([
|
|
17
|
+
const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
22
18
|
"loc",
|
|
23
19
|
"start",
|
|
24
20
|
"end",
|
|
@@ -57,19 +53,41 @@ function walkJsx(ast, visit) {
|
|
|
57
53
|
function walkAll(ast, visit) {
|
|
58
54
|
walk(ast, visit, acceptAll);
|
|
59
55
|
}
|
|
60
|
-
|
|
56
|
+
/** Parse with error recovery; recoverable syntax errors are kept in `errors`. */
|
|
57
|
+
function tryParse(source) {
|
|
61
58
|
try {
|
|
62
|
-
|
|
59
|
+
return parse(source, {
|
|
63
60
|
sourceType: "module",
|
|
64
61
|
plugins: ["typescript", "jsx"],
|
|
65
62
|
errorRecovery: true
|
|
66
63
|
});
|
|
67
|
-
return ast.errors && ast.errors.length > 0 ? null : ast;
|
|
68
64
|
} catch {
|
|
69
65
|
return null;
|
|
70
66
|
}
|
|
71
67
|
}
|
|
72
|
-
|
|
68
|
+
/** `tryParse`, but a source with any recovered error is rejected outright. */
|
|
69
|
+
function parseSource$1(source) {
|
|
70
|
+
const ast = tryParse(source);
|
|
71
|
+
if (!ast) return null;
|
|
72
|
+
return ast.errors && ast.errors.length > 0 ? null : ast;
|
|
73
|
+
}
|
|
74
|
+
/** JSX nodes enclosing (line, column), innermost first. */
|
|
75
|
+
function findJsxAncestors$1(ast, line, column) {
|
|
76
|
+
const hits = [];
|
|
77
|
+
walkJsx(ast, (n) => {
|
|
78
|
+
if (!n.loc || !isJSXElement(n) && !isJSXFragment(n)) return;
|
|
79
|
+
const s = n.loc.start;
|
|
80
|
+
const e = n.loc.end;
|
|
81
|
+
const afterStart = line > s.line || line === s.line && column >= s.column;
|
|
82
|
+
const beforeEnd = line < e.line || line === e.line && column < e.column;
|
|
83
|
+
if (afterStart && beforeEnd) hits.push({
|
|
84
|
+
node: n,
|
|
85
|
+
size: (n.end ?? 0) - (n.start ?? 0)
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
hits.sort((a, b) => a.size - b.size);
|
|
89
|
+
return hits.map((h) => h.node);
|
|
90
|
+
}
|
|
73
91
|
//#endregion
|
|
74
92
|
//#region src/editing/edit-ops.ts
|
|
75
93
|
function jsString$1(s) {
|
|
@@ -87,34 +105,32 @@ function formatJsxAttrValue(value) {
|
|
|
87
105
|
return `{${jsString$1(value)}}`;
|
|
88
106
|
}
|
|
89
107
|
function jsxAttrName(attr) {
|
|
90
|
-
return t
|
|
108
|
+
return t.isJSXIdentifier(attr.name) ? attr.name.name : null;
|
|
91
109
|
}
|
|
92
110
|
function findJsxAttr(opening, name) {
|
|
93
|
-
for (const attr of opening.attributes) if (t
|
|
111
|
+
for (const attr of opening.attributes) if (t.isJSXAttribute(attr) && jsxAttrName(attr) === name) return attr;
|
|
94
112
|
return null;
|
|
95
113
|
}
|
|
96
114
|
function readJsxStringAttr(opening, name) {
|
|
97
|
-
const
|
|
98
|
-
const v = attr?.value;
|
|
115
|
+
const v = findJsxAttr(opening, name)?.value;
|
|
99
116
|
if (!v) return null;
|
|
100
|
-
if (t
|
|
101
|
-
if (t
|
|
117
|
+
if (t.isStringLiteral(v)) return v.value;
|
|
118
|
+
if (t.isJSXExpressionContainer(v) && t.isStringLiteral(v.expression)) return v.expression.value;
|
|
102
119
|
return null;
|
|
103
120
|
}
|
|
104
121
|
function readJsxNumberAttr(opening, name) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (!
|
|
108
|
-
if (!t$3.isNumericLiteral(v.expression)) return null;
|
|
122
|
+
const v = findJsxAttr(opening, name)?.value;
|
|
123
|
+
if (!v || !t.isJSXExpressionContainer(v)) return null;
|
|
124
|
+
if (!t.isNumericLiteral(v.expression)) return null;
|
|
109
125
|
const n = v.expression.value;
|
|
110
126
|
return Number.isFinite(n) ? n : null;
|
|
111
127
|
}
|
|
112
128
|
function findImports$1(ast) {
|
|
113
129
|
const out = [];
|
|
114
130
|
for (const node of ast.program.body) {
|
|
115
|
-
if (!t
|
|
131
|
+
if (!t.isImportDeclaration(node)) continue;
|
|
116
132
|
let def = null;
|
|
117
|
-
for (const spec of node.specifiers) if (t
|
|
133
|
+
for (const spec of node.specifiers) if (t.isImportDefaultSpecifier(spec)) {
|
|
118
134
|
def = spec.local.name;
|
|
119
135
|
break;
|
|
120
136
|
}
|
|
@@ -127,10 +143,10 @@ function findImports$1(ast) {
|
|
|
127
143
|
return out;
|
|
128
144
|
}
|
|
129
145
|
function collectTopLevelIdentifiers(ast) {
|
|
130
|
-
const names = new Set();
|
|
146
|
+
const names = /* @__PURE__ */ new Set();
|
|
131
147
|
for (const imp of findImports$1(ast)) {
|
|
132
148
|
if (imp.defaultIdent) names.add(imp.defaultIdent);
|
|
133
|
-
for (const spec of imp.node.specifiers) if (!t
|
|
149
|
+
for (const spec of imp.node.specifiers) if (!t.isImportDefaultSpecifier(spec)) names.add(spec.local.name);
|
|
134
150
|
}
|
|
135
151
|
return names;
|
|
136
152
|
}
|
|
@@ -153,10 +169,10 @@ function safeAssetIdentifier(filename, taken) {
|
|
|
153
169
|
}
|
|
154
170
|
return candidate;
|
|
155
171
|
}
|
|
156
|
-
function findJsxAncestors
|
|
172
|
+
function findJsxAncestors(ast, line, column) {
|
|
157
173
|
const hits = [];
|
|
158
174
|
walkJsx(ast, (n) => {
|
|
159
|
-
if (!n.loc || !t
|
|
175
|
+
if (!n.loc || !t.isJSXElement(n) && !t.isJSXFragment(n)) return;
|
|
160
176
|
const s = n.loc.start;
|
|
161
177
|
const e = n.loc.end;
|
|
162
178
|
const afterStart = line > s.line || line === s.line && column >= s.column;
|
|
@@ -172,7 +188,7 @@ function findJsxAncestors$1(ast, line, column) {
|
|
|
172
188
|
function findJsxByStart(ast, line, column) {
|
|
173
189
|
let hit = null;
|
|
174
190
|
walkJsx(ast, (n) => {
|
|
175
|
-
if (!t
|
|
191
|
+
if (!t.isJSXElement(n) || !n.loc) return;
|
|
176
192
|
const s = n.loc.start;
|
|
177
193
|
if (s.line === line && s.column === column) {
|
|
178
194
|
hit = n;
|
|
@@ -184,13 +200,13 @@ function findJsxByStart(ast, line, column) {
|
|
|
184
200
|
function findInnermostJsxElement(ast, line, column) {
|
|
185
201
|
const exact = findJsxByStart(ast, line, column);
|
|
186
202
|
if (exact) return exact;
|
|
187
|
-
for (const n of findJsxAncestors
|
|
203
|
+
for (const n of findJsxAncestors(ast, line, column)) if (t.isJSXElement(n)) return n;
|
|
188
204
|
return null;
|
|
189
205
|
}
|
|
190
206
|
function findUniqueElementByText(ast, prevText) {
|
|
191
207
|
const hits = [];
|
|
192
208
|
walkJsx(ast, (n) => {
|
|
193
|
-
if (!t
|
|
209
|
+
if (!t.isJSXElement(n)) return;
|
|
194
210
|
const parts = [];
|
|
195
211
|
collectTextRangeParts(n, parts);
|
|
196
212
|
if (textRangeContent(parts) !== prevText) return;
|
|
@@ -204,8 +220,7 @@ function findUniqueElementByText(ast, prevText) {
|
|
|
204
220
|
const best = hits[0];
|
|
205
221
|
const bestStart = best.node.start ?? 0;
|
|
206
222
|
const bestEnd = best.node.end ?? 0;
|
|
207
|
-
|
|
208
|
-
return hasSiblingMatch ? null : best.node;
|
|
223
|
+
return hits.slice(1).some(({ node }) => (node.start ?? 0) > bestStart || (node.end ?? 0) < bestEnd) ? null : best.node;
|
|
209
224
|
}
|
|
210
225
|
function fallbackTextForOps(ops) {
|
|
211
226
|
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;
|
|
@@ -239,19 +254,19 @@ function buildStyleSplice(source, element, ops) {
|
|
|
239
254
|
let hasRawEntry = false;
|
|
240
255
|
if (existing) {
|
|
241
256
|
const value = existing.value;
|
|
242
|
-
if (!value || !t
|
|
257
|
+
if (!value || !t.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
|
|
243
258
|
const expr = value.expression;
|
|
244
|
-
if (!t
|
|
259
|
+
if (!t.isObjectExpression(expr)) {
|
|
245
260
|
if (typeof expr.start !== "number" || typeof expr.end !== "number") return { error: "style value missing source range" };
|
|
246
261
|
entries.push({
|
|
247
262
|
kind: "raw",
|
|
248
263
|
text: `...(${source.slice(expr.start, expr.end)})`
|
|
249
264
|
});
|
|
250
265
|
hasRawEntry = true;
|
|
251
|
-
} else for (const prop of expr.properties) if (t
|
|
266
|
+
} else for (const prop of expr.properties) if (t.isObjectProperty(prop) && !prop.computed) {
|
|
252
267
|
let keyName = null;
|
|
253
|
-
if (t
|
|
254
|
-
else if (t
|
|
268
|
+
if (t.isIdentifier(prop.key)) keyName = prop.key.name;
|
|
269
|
+
else if (t.isStringLiteral(prop.key)) keyName = prop.key.value;
|
|
255
270
|
if (!keyName) return { error: "style has unsupported key" };
|
|
256
271
|
const v = prop.value;
|
|
257
272
|
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" };
|
|
@@ -298,15 +313,14 @@ function buildStyleSplice(source, element, ops) {
|
|
|
298
313
|
text: ""
|
|
299
314
|
};
|
|
300
315
|
}
|
|
301
|
-
const
|
|
302
|
-
const newAttr = `style={{ ${propsText} }}`;
|
|
316
|
+
const newAttr = `style={{ ${entries.map((entry) => entry.kind === "prop" ? `${entry.keyText}: ${entry.valueText}` : entry.text).join(", ")} }}`;
|
|
303
317
|
if (existing) {
|
|
304
|
-
const lastAttr
|
|
305
|
-
if (lastAttr
|
|
306
|
-
const attrsAfterStyle = source.slice(existing.end ?? 0, lastAttr
|
|
318
|
+
const lastAttr = opening.attributes[opening.attributes.length - 1];
|
|
319
|
+
if (lastAttr && lastAttr !== existing && typeof lastAttr.end === "number") {
|
|
320
|
+
const attrsAfterStyle = source.slice(existing.end ?? 0, lastAttr.end).replace(/^[ \t]+/, "");
|
|
307
321
|
return {
|
|
308
322
|
from: existing.start ?? 0,
|
|
309
|
-
to: lastAttr
|
|
323
|
+
to: lastAttr.end,
|
|
310
324
|
text: `${attrsAfterStyle} ${newAttr}`
|
|
311
325
|
};
|
|
312
326
|
}
|
|
@@ -316,8 +330,7 @@ function buildStyleSplice(source, element, ops) {
|
|
|
316
330
|
text: newAttr
|
|
317
331
|
};
|
|
318
332
|
}
|
|
319
|
-
const
|
|
320
|
-
const at = lastAttr?.end ?? opening.name.end ?? 0;
|
|
333
|
+
const at = opening.attributes[opening.attributes.length - 1]?.end ?? opening.name.end ?? 0;
|
|
321
334
|
return {
|
|
322
335
|
from: at,
|
|
323
336
|
to: at,
|
|
@@ -330,7 +343,7 @@ function formatJsxText(value) {
|
|
|
330
343
|
}
|
|
331
344
|
function meaningfulChildren(parent) {
|
|
332
345
|
return parent.children.filter((c) => {
|
|
333
|
-
if (t
|
|
346
|
+
if (t.isJSXText(c)) return c.value.trim() !== "";
|
|
334
347
|
return true;
|
|
335
348
|
});
|
|
336
349
|
}
|
|
@@ -397,14 +410,14 @@ function cleanJsxTextWithOffsets(value) {
|
|
|
397
410
|
};
|
|
398
411
|
}
|
|
399
412
|
function isJsxBrElement(node) {
|
|
400
|
-
if (!t
|
|
413
|
+
if (!t.isJSXElement(node)) return false;
|
|
401
414
|
const name = node.openingElement.name;
|
|
402
|
-
return t
|
|
415
|
+
return t.isJSXIdentifier(name) && name.name.toLowerCase() === "br";
|
|
403
416
|
}
|
|
404
417
|
function collectTextCandidates(element, out) {
|
|
405
418
|
const meaningful = meaningfulChildren(element);
|
|
406
419
|
const isSole = meaningful.length === 1;
|
|
407
|
-
for (const child of meaningful) if (t
|
|
420
|
+
for (const child of meaningful) if (t.isJSXText(child)) {
|
|
408
421
|
const current = child.value.trim();
|
|
409
422
|
if (!current) continue;
|
|
410
423
|
out.push({
|
|
@@ -415,9 +428,9 @@ function collectTextCandidates(element, out) {
|
|
|
415
428
|
text: formatJsxText(v)
|
|
416
429
|
}
|
|
417
430
|
});
|
|
418
|
-
} else if (t
|
|
431
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
419
432
|
const expr = child.expression;
|
|
420
|
-
if (t
|
|
433
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr)) {
|
|
421
434
|
const current = String(expr.value);
|
|
422
435
|
out.push({
|
|
423
436
|
current,
|
|
@@ -428,7 +441,7 @@ function collectTextCandidates(element, out) {
|
|
|
428
441
|
}
|
|
429
442
|
});
|
|
430
443
|
}
|
|
431
|
-
} else if (t
|
|
444
|
+
} else if (t.isJSXElement(child) || t.isJSXFragment(child)) collectTextCandidates(child, out);
|
|
432
445
|
}
|
|
433
446
|
function collectTextRangeParts(element, out) {
|
|
434
447
|
const parts = [];
|
|
@@ -436,7 +449,7 @@ function collectTextRangeParts(element, out) {
|
|
|
436
449
|
out.push(...normalizeTextRangeParts(parts));
|
|
437
450
|
}
|
|
438
451
|
function collectTextRangePartsRaw(element, out) {
|
|
439
|
-
for (const child of element.children) if (t
|
|
452
|
+
for (const child of element.children) if (t.isJSXText(child)) {
|
|
440
453
|
const { text: current, offsets } = cleanJsxTextWithOffsets(child.value);
|
|
441
454
|
if (current) out.push({
|
|
442
455
|
node: child,
|
|
@@ -446,9 +459,9 @@ function collectTextRangePartsRaw(element, out) {
|
|
|
446
459
|
text: formatJsxText,
|
|
447
460
|
offsets
|
|
448
461
|
});
|
|
449
|
-
} else if (t
|
|
462
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
450
463
|
const expression = child.expression;
|
|
451
|
-
if (t
|
|
464
|
+
if (t.isStringLiteral(expression) || t.isNumericLiteral(expression)) {
|
|
452
465
|
const raw = String(expression.value);
|
|
453
466
|
const current = raw;
|
|
454
467
|
if (current) out.push({
|
|
@@ -464,7 +477,7 @@ function collectTextRangePartsRaw(element, out) {
|
|
|
464
477
|
node: child,
|
|
465
478
|
current: "\n"
|
|
466
479
|
});
|
|
467
|
-
else if (t
|
|
480
|
+
else if (t.isJSXElement(child) || t.isJSXFragment(child)) collectTextRangePartsRaw(child, out);
|
|
468
481
|
}
|
|
469
482
|
function normalizeTextRangeParts(parts) {
|
|
470
483
|
return parts.flatMap((part, index) => {
|
|
@@ -627,7 +640,7 @@ function buildTextRangeStyleSplices(ast, source, element, start, end, op, prevTe
|
|
|
627
640
|
leafStart = leafEnd;
|
|
628
641
|
continue;
|
|
629
642
|
}
|
|
630
|
-
if (selectedStart === leafStart && selectedEnd === leafEnd && t
|
|
643
|
+
if (selectedStart === leafStart && selectedEnd === leafEnd && t.isJSXElement(leaf.parent) && leaf.parent !== element && isOnlyMeaningfulChild(leaf.parent, leaf.node)) {
|
|
631
644
|
const result = buildStyleSplice(source, leaf.parent, [op]);
|
|
632
645
|
if (result && "error" in result) return result;
|
|
633
646
|
if (result) splices.push(result);
|
|
@@ -643,8 +656,8 @@ function buildTextRangeStyleSplices(ast, source, element, start, end, op, prevTe
|
|
|
643
656
|
const before = raw.slice(0, rawStart);
|
|
644
657
|
const selected = leaf.current.slice(localStart, localEnd);
|
|
645
658
|
const after = raw.slice(rawEnd);
|
|
646
|
-
const beforeText = t
|
|
647
|
-
const afterText = t
|
|
659
|
+
const beforeText = t.isJSXText(leaf.node) ? before : formatOptionalText(before, leaf.text);
|
|
660
|
+
const afterText = t.isJSXText(leaf.node) ? after : formatOptionalText(after, leaf.text);
|
|
648
661
|
splices.push(spliceRange(leaf.node, `${beforeText}${styleSpanForText(selected, op.key, op.value)}${afterText}`));
|
|
649
662
|
leafStart = leafEnd;
|
|
650
663
|
}
|
|
@@ -654,8 +667,8 @@ function propPassthroughName(element) {
|
|
|
654
667
|
const meaningful = meaningfulChildren(element);
|
|
655
668
|
if (meaningful.length !== 1) return null;
|
|
656
669
|
const child = meaningful[0];
|
|
657
|
-
if (!t
|
|
658
|
-
return t
|
|
670
|
+
if (!t.isJSXExpressionContainer(child)) return null;
|
|
671
|
+
return t.isIdentifier(child.expression) ? child.expression.name : null;
|
|
659
672
|
}
|
|
660
673
|
function findEnclosingComponent(ast, target) {
|
|
661
674
|
let best = null;
|
|
@@ -677,17 +690,17 @@ function findEnclosingComponent(ast, target) {
|
|
|
677
690
|
}
|
|
678
691
|
};
|
|
679
692
|
const visitDecl = (decl) => {
|
|
680
|
-
if (t
|
|
681
|
-
else if (t
|
|
682
|
-
if (!t
|
|
683
|
-
if (t
|
|
693
|
+
if (t.isFunctionDeclaration(decl) && decl.id) consider(decl.id.name, decl);
|
|
694
|
+
else if (t.isVariableDeclaration(decl)) for (const d of decl.declarations) {
|
|
695
|
+
if (!t.isVariableDeclarator(d) || !t.isIdentifier(d.id) || !d.init) continue;
|
|
696
|
+
if (t.isArrowFunctionExpression(d.init) || t.isFunctionExpression(d.init)) consider(d.id.name, d.init);
|
|
684
697
|
}
|
|
685
698
|
};
|
|
686
699
|
for (const decl of ast.program.body) {
|
|
687
700
|
visitDecl(decl);
|
|
688
|
-
if (t
|
|
701
|
+
if (t.isExportNamedDeclaration(decl) || t.isExportDefaultDeclaration(decl)) {
|
|
689
702
|
const inner = decl.declaration;
|
|
690
|
-
if (inner && (t
|
|
703
|
+
if (inner && (t.isStatement(inner) || t.isFunctionDeclaration(inner))) visitDecl(inner);
|
|
691
704
|
}
|
|
692
705
|
}
|
|
693
706
|
return best;
|
|
@@ -695,40 +708,40 @@ function findEnclosingComponent(ast, target) {
|
|
|
695
708
|
function componentDestructuresProp(fn, propName) {
|
|
696
709
|
if (fn.params.length === 0) return false;
|
|
697
710
|
let first = fn.params[0];
|
|
698
|
-
if (t
|
|
699
|
-
if (!t
|
|
711
|
+
if (t.isAssignmentPattern(first)) first = first.left;
|
|
712
|
+
if (!t.isObjectPattern(first)) return false;
|
|
700
713
|
for (const prop of first.properties) {
|
|
701
|
-
if (!t
|
|
702
|
-
if (t
|
|
703
|
-
if (t
|
|
714
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
715
|
+
if (t.isIdentifier(prop.key) && prop.key.name === propName) return true;
|
|
716
|
+
if (t.isStringLiteral(prop.key) && prop.key.value === propName) return true;
|
|
704
717
|
}
|
|
705
718
|
return false;
|
|
706
719
|
}
|
|
707
720
|
function collectCallSiteCandidates(ast, componentName) {
|
|
708
721
|
const out = [];
|
|
709
722
|
walkJsx(ast, (n) => {
|
|
710
|
-
if (!t
|
|
723
|
+
if (!t.isJSXElement(n)) return;
|
|
711
724
|
const elName = n.openingElement.name;
|
|
712
|
-
if (t
|
|
725
|
+
if (t.isJSXIdentifier(elName) && elName.name === componentName) collectTextCandidates(n, out);
|
|
713
726
|
});
|
|
714
727
|
return out;
|
|
715
728
|
}
|
|
716
729
|
function collectPropCallSiteCandidates(ast, componentName, propName) {
|
|
717
730
|
const out = [];
|
|
718
731
|
walkJsx(ast, (n) => {
|
|
719
|
-
if (!t
|
|
732
|
+
if (!t.isJSXElement(n)) return;
|
|
720
733
|
const elName = n.openingElement.name;
|
|
721
|
-
if (!t
|
|
734
|
+
if (!t.isJSXIdentifier(elName) || elName.name !== componentName) return;
|
|
722
735
|
const attr = findJsxAttr(n.openingElement, propName);
|
|
723
736
|
if (!attr?.value) return;
|
|
724
737
|
const v = attr.value;
|
|
725
|
-
if (t
|
|
738
|
+
if (t.isStringLiteral(v)) out.push({
|
|
726
739
|
current: v.value,
|
|
727
740
|
splice: (s) => spliceRange(v, formatJsxAttrValue(s))
|
|
728
741
|
});
|
|
729
|
-
else if (t
|
|
742
|
+
else if (t.isJSXExpressionContainer(v)) {
|
|
730
743
|
const expr = v.expression;
|
|
731
|
-
if (t
|
|
744
|
+
if (t.isStringLiteral(expr) || t.isNumericLiteral(expr)) out.push({
|
|
732
745
|
current: String(expr.value),
|
|
733
746
|
splice: (s) => spliceRange(v, formatJsxAttrValue(s))
|
|
734
747
|
});
|
|
@@ -741,17 +754,17 @@ function findEnclosingMapCallback(ast, target) {
|
|
|
741
754
|
const targetStart = target.start ?? 0;
|
|
742
755
|
const targetEnd = target.end ?? 0;
|
|
743
756
|
walkAll(ast, (node) => {
|
|
744
|
-
if (!t
|
|
757
|
+
if (!t.isCallExpression(node)) return;
|
|
745
758
|
const callee = node.callee;
|
|
746
|
-
if (!t
|
|
747
|
-
if (!t
|
|
759
|
+
if (!t.isMemberExpression(callee) || callee.computed) return;
|
|
760
|
+
if (!t.isIdentifier(callee.property)) return;
|
|
748
761
|
if (callee.property.name !== "map" && callee.property.name !== "flatMap") return;
|
|
749
762
|
const fn = node.arguments[0];
|
|
750
|
-
if (!fn || !t
|
|
763
|
+
if (!fn || !t.isArrowFunctionExpression(fn) && !t.isFunctionExpression(fn)) return;
|
|
751
764
|
const fnStart = fn.start ?? 0;
|
|
752
765
|
const fnEnd = fn.end ?? 0;
|
|
753
766
|
if (fnStart > targetStart || fnEnd < targetEnd) return;
|
|
754
|
-
if (!t
|
|
767
|
+
if (!t.isExpression(callee.object)) return;
|
|
755
768
|
const size = fnEnd - fnStart;
|
|
756
769
|
if (!best || size < best.size) best = {
|
|
757
770
|
fn,
|
|
@@ -768,26 +781,26 @@ function findEnclosingMapCallback(ast, target) {
|
|
|
768
781
|
}
|
|
769
782
|
function resolveArrayLiteralElements(ast, expr) {
|
|
770
783
|
const dropHoles = (arr) => arr.elements.filter((e) => e != null);
|
|
771
|
-
if (t
|
|
772
|
-
if (!t
|
|
784
|
+
if (t.isArrayExpression(expr)) return dropHoles(expr);
|
|
785
|
+
if (!t.isIdentifier(expr)) return null;
|
|
773
786
|
const name = expr.name;
|
|
774
787
|
const useStart = expr.start ?? 0;
|
|
775
788
|
let init = null;
|
|
776
789
|
walkAll(ast, (node) => {
|
|
777
|
-
if (!t
|
|
778
|
-
if (!t
|
|
779
|
-
if (!node.init || !t
|
|
790
|
+
if (!t.isVariableDeclarator(node)) return;
|
|
791
|
+
if (!t.isIdentifier(node.id) || node.id.name !== name) return;
|
|
792
|
+
if (!node.init || !t.isArrayExpression(node.init)) return;
|
|
780
793
|
if ((node.init.start ?? 0) > useStart) return;
|
|
781
794
|
init = node.init;
|
|
782
795
|
});
|
|
783
796
|
return init ? dropHoles(init) : null;
|
|
784
797
|
}
|
|
785
798
|
function findObjectProperty(obj, name) {
|
|
786
|
-
if (!t
|
|
799
|
+
if (!t.isObjectExpression(obj)) return null;
|
|
787
800
|
for (const prop of obj.properties) {
|
|
788
|
-
if (!t
|
|
789
|
-
if (t
|
|
790
|
-
if (t
|
|
801
|
+
if (!t.isObjectProperty(prop) || prop.computed) continue;
|
|
802
|
+
if (t.isIdentifier(prop.key) && prop.key.name === name) return prop;
|
|
803
|
+
if (t.isStringLiteral(prop.key) && prop.key.value === name) return prop;
|
|
791
804
|
}
|
|
792
805
|
return null;
|
|
793
806
|
}
|
|
@@ -795,22 +808,22 @@ function decodeMapPassthrough(element, callbackParam) {
|
|
|
795
808
|
const meaningful = meaningfulChildren(element);
|
|
796
809
|
if (meaningful.length !== 1) return null;
|
|
797
810
|
const child = meaningful[0];
|
|
798
|
-
if (!t
|
|
811
|
+
if (!t.isJSXExpressionContainer(child)) return null;
|
|
799
812
|
const expr = child.expression;
|
|
800
|
-
if (t
|
|
813
|
+
if (t.isMemberExpression(expr)) {
|
|
801
814
|
if (expr.computed) return null;
|
|
802
|
-
if (!t
|
|
803
|
-
if (!callbackParam || !t
|
|
815
|
+
if (!t.isIdentifier(expr.object) || !t.isIdentifier(expr.property)) return null;
|
|
816
|
+
if (!callbackParam || !t.isIdentifier(callbackParam)) return null;
|
|
804
817
|
if (callbackParam.name !== expr.object.name) return null;
|
|
805
818
|
return expr.property.name;
|
|
806
819
|
}
|
|
807
|
-
if (t
|
|
820
|
+
if (t.isIdentifier(expr)) {
|
|
808
821
|
const fieldName = expr.name;
|
|
809
|
-
if (!callbackParam || !t
|
|
822
|
+
if (!callbackParam || !t.isObjectPattern(callbackParam)) return null;
|
|
810
823
|
for (const prop of callbackParam.properties) {
|
|
811
|
-
if (!t
|
|
812
|
-
if (!t
|
|
813
|
-
return t
|
|
824
|
+
if (!t.isObjectProperty(prop) || prop.computed) continue;
|
|
825
|
+
if (!t.isIdentifier(prop.key) || prop.key.name !== fieldName) continue;
|
|
826
|
+
return t.isIdentifier(prop.value) && prop.value.name === fieldName ? fieldName : null;
|
|
814
827
|
}
|
|
815
828
|
}
|
|
816
829
|
return null;
|
|
@@ -827,11 +840,11 @@ function collectArrayMapCandidates(ast, element) {
|
|
|
827
840
|
const prop = findObjectProperty(obj, fieldName);
|
|
828
841
|
if (!prop) continue;
|
|
829
842
|
const v = prop.value;
|
|
830
|
-
if (t
|
|
843
|
+
if (t.isStringLiteral(v)) out.push({
|
|
831
844
|
current: v.value,
|
|
832
845
|
splice: (s) => spliceRange(v, jsString$1(s))
|
|
833
846
|
});
|
|
834
|
-
else if (t
|
|
847
|
+
else if (t.isNumericLiteral(v)) out.push({
|
|
835
848
|
current: String(v.value),
|
|
836
849
|
splice: (s) => spliceRange(v, jsString$1(s))
|
|
837
850
|
});
|
|
@@ -871,18 +884,16 @@ function planAssetImport(ast, assetPath) {
|
|
|
871
884
|
identifier: imp.defaultIdent,
|
|
872
885
|
importSplice: null
|
|
873
886
|
};
|
|
874
|
-
const
|
|
875
|
-
const identifier = safeAssetIdentifier(filename, collectTopLevelIdentifiers(ast));
|
|
887
|
+
const identifier = safeAssetIdentifier(assetPath.slice(assetPath.lastIndexOf("/") + 1), collectTopLevelIdentifiers(ast));
|
|
876
888
|
const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
|
|
877
889
|
const last = imports[imports.length - 1];
|
|
878
890
|
const insertAt = last ? last.node.end ?? 0 : 0;
|
|
879
|
-
const prefix = last ? "\n" : "";
|
|
880
891
|
return {
|
|
881
892
|
identifier,
|
|
882
893
|
importSplice: {
|
|
883
894
|
from: insertAt,
|
|
884
895
|
to: insertAt,
|
|
885
|
-
text:
|
|
896
|
+
text: (last ? "\n" : "") + importStmt
|
|
886
897
|
}
|
|
887
898
|
};
|
|
888
899
|
}
|
|
@@ -893,23 +904,22 @@ function planAssetAttr(ast, element, attr, assetPath) {
|
|
|
893
904
|
const opening = element.openingElement;
|
|
894
905
|
const newAttr = `${attr}={${identifier}}`;
|
|
895
906
|
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
907
|
return {
|
|
906
908
|
importSplice,
|
|
907
|
-
attrSplice
|
|
909
|
+
attrSplice: existing ? {
|
|
910
|
+
from: existing.start ?? 0,
|
|
911
|
+
to: existing.end ?? 0,
|
|
912
|
+
text: newAttr
|
|
913
|
+
} : {
|
|
914
|
+
from: opening.name.end ?? 0,
|
|
915
|
+
to: opening.name.end ?? 0,
|
|
916
|
+
text: ` ${newAttr}`
|
|
917
|
+
}
|
|
908
918
|
};
|
|
909
919
|
}
|
|
910
920
|
function planReplacePlaceholder(ast, element, assetPath) {
|
|
911
921
|
const opening = element.openingElement;
|
|
912
|
-
if (!t
|
|
922
|
+
if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
|
|
913
923
|
if (!assetPath.startsWith("./assets/") && !assetPath.startsWith("@assets/")) return { error: "asset path must start with ./assets/ or @assets/" };
|
|
914
924
|
const hint = readJsxStringAttr(opening, "hint") ?? "";
|
|
915
925
|
const width = readJsxNumberAttr(opening, "width");
|
|
@@ -922,10 +932,9 @@ function planReplacePlaceholder(ast, element, assetPath) {
|
|
|
922
932
|
else if (width != null) styleParts.push(`height: '100%'`);
|
|
923
933
|
styleParts.push(`objectFit: 'cover'`);
|
|
924
934
|
styleParts.push(`objectPosition: '50% 50%'`);
|
|
925
|
-
const replacement = `<img src={${identifier}} alt=${jsString$1(hint)} style={{ ${styleParts.join(", ")} }} />`;
|
|
926
935
|
return {
|
|
927
936
|
importSplice,
|
|
928
|
-
elementSplice: spliceRange(element,
|
|
937
|
+
elementSplice: spliceRange(element, `<img src={${identifier}} alt=${jsString$1(hint)} style={{ ${styleParts.join(", ")} }} />`)
|
|
929
938
|
};
|
|
930
939
|
}
|
|
931
940
|
function applyEdit(source, line, column, ops) {
|
|
@@ -1049,20 +1058,19 @@ function applyEdit(source, line, column, ops) {
|
|
|
1049
1058
|
source: next
|
|
1050
1059
|
};
|
|
1051
1060
|
}
|
|
1052
|
-
|
|
1053
1061
|
//#endregion
|
|
1054
1062
|
//#region src/editing/revert-asset.ts
|
|
1055
1063
|
function collectImgSrcUses(ast, identifier) {
|
|
1056
1064
|
const uses = [];
|
|
1057
1065
|
walkJsx(ast, (n) => {
|
|
1058
|
-
if (!t
|
|
1066
|
+
if (!t.isJSXElement(n)) return;
|
|
1059
1067
|
const opening = n.openingElement;
|
|
1060
|
-
if (!t
|
|
1068
|
+
if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "img") return;
|
|
1061
1069
|
const src = findJsxAttr(opening, "src");
|
|
1062
1070
|
if (!src?.value) return;
|
|
1063
|
-
if (!t
|
|
1071
|
+
if (!t.isJSXExpressionContainer(src.value)) return;
|
|
1064
1072
|
const expr = src.value.expression;
|
|
1065
|
-
if (!t
|
|
1073
|
+
if (!t.isIdentifier(expr) || expr.name !== identifier) return;
|
|
1066
1074
|
uses.push({
|
|
1067
1075
|
element: n,
|
|
1068
1076
|
identNode: expr
|
|
@@ -1073,17 +1081,16 @@ function collectImgSrcUses(ast, identifier) {
|
|
|
1073
1081
|
function readStyleNumericDim(opening, key) {
|
|
1074
1082
|
const style = findJsxAttr(opening, "style");
|
|
1075
1083
|
if (!style?.value) return null;
|
|
1076
|
-
if (!t
|
|
1084
|
+
if (!t.isJSXExpressionContainer(style.value)) return null;
|
|
1077
1085
|
const obj = style.value.expression;
|
|
1078
|
-
if (!t
|
|
1086
|
+
if (!t.isObjectExpression(obj)) return null;
|
|
1079
1087
|
for (const prop of obj.properties) {
|
|
1080
|
-
if (!t
|
|
1088
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
1081
1089
|
if (prop.computed) continue;
|
|
1082
1090
|
const k = prop.key;
|
|
1083
|
-
|
|
1084
|
-
if (keyName !== key) continue;
|
|
1091
|
+
if ((t.isIdentifier(k) ? k.name : t.isStringLiteral(k) ? k.value : null) !== key) continue;
|
|
1085
1092
|
const v = prop.value;
|
|
1086
|
-
if (t
|
|
1093
|
+
if (t.isNumericLiteral(v) && Number.isFinite(v.value)) return v.value;
|
|
1087
1094
|
return null;
|
|
1088
1095
|
}
|
|
1089
1096
|
return null;
|
|
@@ -1102,31 +1109,29 @@ function planEnsureImagePlaceholderImport(ast) {
|
|
|
1102
1109
|
if (imp.source !== "@autono/open-pages") continue;
|
|
1103
1110
|
const declIsTypeOnly = readKind(imp.node);
|
|
1104
1111
|
for (const spec of imp.node.specifiers) {
|
|
1105
|
-
if (!t
|
|
1112
|
+
if (!t.isImportSpecifier(spec)) continue;
|
|
1106
1113
|
const imported = spec.imported;
|
|
1107
|
-
|
|
1108
|
-
if (
|
|
1109
|
-
const specIsTypeOnly = readKind(spec) || declIsTypeOnly;
|
|
1110
|
-
if (!specIsTypeOnly) return null;
|
|
1114
|
+
if ((t.isIdentifier(imported) ? imported.name : imported.value) !== "ImagePlaceholder") continue;
|
|
1115
|
+
if (!(readKind(spec) || declIsTypeOnly)) return null;
|
|
1111
1116
|
}
|
|
1112
1117
|
if (!declIsTypeOnly && !valueImport) valueImport = imp;
|
|
1113
1118
|
}
|
|
1114
1119
|
if (valueImport) {
|
|
1115
1120
|
const node = valueImport.node;
|
|
1116
1121
|
const lastSpec = node.specifiers[node.specifiers.length - 1];
|
|
1117
|
-
if (lastSpec && t
|
|
1118
|
-
const insertAt
|
|
1122
|
+
if (lastSpec && t.isImportSpecifier(lastSpec)) {
|
|
1123
|
+
const insertAt = lastSpec.end ?? 0;
|
|
1119
1124
|
return {
|
|
1120
|
-
from: insertAt
|
|
1121
|
-
to: insertAt
|
|
1125
|
+
from: insertAt,
|
|
1126
|
+
to: insertAt,
|
|
1122
1127
|
text: ", ImagePlaceholder"
|
|
1123
1128
|
};
|
|
1124
1129
|
}
|
|
1125
|
-
if (lastSpec && t
|
|
1126
|
-
const insertAt
|
|
1130
|
+
if (lastSpec && t.isImportDefaultSpecifier(lastSpec)) {
|
|
1131
|
+
const insertAt = lastSpec.end ?? 0;
|
|
1127
1132
|
return {
|
|
1128
|
-
from: insertAt
|
|
1129
|
-
to: insertAt
|
|
1133
|
+
from: insertAt,
|
|
1134
|
+
to: insertAt,
|
|
1130
1135
|
text: ", { ImagePlaceholder }"
|
|
1131
1136
|
};
|
|
1132
1137
|
}
|
|
@@ -1146,28 +1151,27 @@ function planEnsureImagePlaceholderImport(ast) {
|
|
|
1146
1151
|
function findAssetUsages(source, assetPath) {
|
|
1147
1152
|
const ast = parseSource$1(source);
|
|
1148
1153
|
if (!ast) return 0;
|
|
1149
|
-
const
|
|
1150
|
-
const target = imports.find((imp) => imp.source === assetPath && imp.defaultIdent);
|
|
1154
|
+
const target = findImports$1(ast).find((imp) => imp.source === assetPath && imp.defaultIdent);
|
|
1151
1155
|
if (!target?.defaultIdent) return 0;
|
|
1152
1156
|
return collectImgSrcUses(ast, target.defaultIdent).length;
|
|
1153
1157
|
}
|
|
1154
1158
|
function findReferencedAssets(source, assetPaths) {
|
|
1155
|
-
const referenced = new Set();
|
|
1159
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1156
1160
|
if (assetPaths.length === 0) return referenced;
|
|
1157
1161
|
const ast = parseSource$1(source);
|
|
1158
1162
|
if (!ast) return referenced;
|
|
1159
1163
|
const wanted = new Set(assetPaths);
|
|
1160
|
-
const identToPath = new Map();
|
|
1161
|
-
const importLocals = new Set();
|
|
1164
|
+
const identToPath = /* @__PURE__ */ new Map();
|
|
1165
|
+
const importLocals = /* @__PURE__ */ new Set();
|
|
1162
1166
|
for (const imp of findImports$1(ast)) {
|
|
1163
1167
|
if (!imp.defaultIdent) continue;
|
|
1164
1168
|
if (!wanted.has(imp.source)) continue;
|
|
1165
1169
|
identToPath.set(imp.defaultIdent, imp.source);
|
|
1166
|
-
for (const spec of imp.node.specifiers) if (t
|
|
1170
|
+
for (const spec of imp.node.specifiers) if (t.isImportDefaultSpecifier(spec) && spec.local.name === imp.defaultIdent) importLocals.add(spec.local);
|
|
1167
1171
|
}
|
|
1168
1172
|
if (identToPath.size === 0) return referenced;
|
|
1169
1173
|
walkAll(ast, (n) => {
|
|
1170
|
-
if (!t
|
|
1174
|
+
if (!t.isIdentifier(n)) return;
|
|
1171
1175
|
const p = identToPath.get(n.name);
|
|
1172
1176
|
if (!p) return;
|
|
1173
1177
|
if (importLocals.has(n)) return;
|
|
@@ -1182,15 +1186,14 @@ function applyRevertAsset(source, assetPath) {
|
|
|
1182
1186
|
status: 422,
|
|
1183
1187
|
error: "could not parse source"
|
|
1184
1188
|
};
|
|
1185
|
-
const
|
|
1186
|
-
const target = imports.find((imp) => imp.source === assetPath && imp.defaultIdent);
|
|
1189
|
+
const target = findImports$1(ast).find((imp) => imp.source === assetPath && imp.defaultIdent);
|
|
1187
1190
|
if (!target?.defaultIdent) return {
|
|
1188
1191
|
ok: true,
|
|
1189
1192
|
source
|
|
1190
1193
|
};
|
|
1191
1194
|
const identifier = target.defaultIdent;
|
|
1192
1195
|
const importLocal = (() => {
|
|
1193
|
-
for (const spec of target.node.specifiers) if (t
|
|
1196
|
+
for (const spec of target.node.specifiers) if (t.isImportDefaultSpecifier(spec) && spec.local.name === identifier) return spec.local;
|
|
1194
1197
|
return null;
|
|
1195
1198
|
})();
|
|
1196
1199
|
const imgUses = collectImgSrcUses(ast, identifier);
|
|
@@ -1198,7 +1201,7 @@ function applyRevertAsset(source, assetPath) {
|
|
|
1198
1201
|
if (importLocal) allowed.add(importLocal);
|
|
1199
1202
|
let foreign = false;
|
|
1200
1203
|
walkAll(ast, (n) => {
|
|
1201
|
-
if (!t
|
|
1204
|
+
if (!t.isIdentifier(n) || n.name !== identifier) return;
|
|
1202
1205
|
if (!allowed.has(n)) foreign = true;
|
|
1203
1206
|
});
|
|
1204
1207
|
if (foreign) return {
|
|
@@ -1242,11 +1245,9 @@ function applyRevertAsset(source, assetPath) {
|
|
|
1242
1245
|
source: next
|
|
1243
1246
|
};
|
|
1244
1247
|
}
|
|
1245
|
-
|
|
1246
1248
|
//#endregion
|
|
1247
1249
|
//#region src/files/assets.ts
|
|
1248
1250
|
const GLOBAL_SCOPE = "@global";
|
|
1249
|
-
const ASSET_MAX_BYTES = 25 * 1024 * 1024;
|
|
1250
1251
|
const ASSET_FORBIDDEN_RE = /[\x00-\x1F\x7F/\\:*?"<>|]/;
|
|
1251
1252
|
const MIME_BY_EXT = {
|
|
1252
1253
|
png: "image/png",
|
|
@@ -1290,42 +1291,27 @@ function validateAssetName(v) {
|
|
|
1290
1291
|
}
|
|
1291
1292
|
function resolveAssetsDir(pagesRoot, pageId) {
|
|
1292
1293
|
if (!PAGE_ID_RE.test(pageId)) return null;
|
|
1293
|
-
const
|
|
1294
|
-
if (!
|
|
1295
|
-
|
|
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;
|
|
1294
|
+
const pageDir = path.resolve(pagesRoot, pageId);
|
|
1295
|
+
if (!pageDir.startsWith(pagesRoot + path.sep)) return null;
|
|
1296
|
+
return path.join(pageDir, "assets");
|
|
1306
1297
|
}
|
|
1307
1298
|
function resolveScopedAssetsDir(pagesRoot, globalAssetsRoot, scope) {
|
|
1308
|
-
if (scope ===
|
|
1299
|
+
if (scope === "@global") return globalAssetsRoot;
|
|
1309
1300
|
return resolveAssetsDir(pagesRoot, scope);
|
|
1310
1301
|
}
|
|
1311
1302
|
function resolveScopedAssetFile(pagesRoot, globalAssetsRoot, scope, filename) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
return resolveAssetFile(pagesRoot, scope, filename);
|
|
1303
|
+
const assetsDir = resolveScopedAssetsDir(pagesRoot, globalAssetsRoot, scope);
|
|
1304
|
+
if (!assetsDir) return null;
|
|
1305
|
+
if (!validateAssetName(filename)) return null;
|
|
1306
|
+
const file = path.resolve(assetsDir, filename);
|
|
1307
|
+
if (!file.startsWith(assetsDir + path.sep)) return null;
|
|
1308
|
+
return file;
|
|
1319
1309
|
}
|
|
1320
|
-
|
|
1321
1310
|
//#endregion
|
|
1322
1311
|
//#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
1312
|
function headerValue(req, name) {
|
|
1328
|
-
|
|
1313
|
+
const raw = req.headers[name.toLowerCase()];
|
|
1314
|
+
return (Array.isArray(raw) ? raw[0] : raw)?.trim() ?? null;
|
|
1329
1315
|
}
|
|
1330
1316
|
function firstCommaToken(value) {
|
|
1331
1317
|
if (!value) return null;
|
|
@@ -1347,15 +1333,13 @@ function normalizedOrigin(origin) {
|
|
|
1347
1333
|
}
|
|
1348
1334
|
function validateMutationRequest(req, opts = {}) {
|
|
1349
1335
|
if (opts.requireJsonBody) {
|
|
1350
|
-
|
|
1351
|
-
if (!contentType || !contentType.startsWith("application/json")) return {
|
|
1336
|
+
if (!(headerValue(req, "content-type")?.toLowerCase())?.startsWith("application/json")) return {
|
|
1352
1337
|
ok: false,
|
|
1353
1338
|
status: 415,
|
|
1354
1339
|
error: "content-type must be application/json"
|
|
1355
1340
|
};
|
|
1356
1341
|
}
|
|
1357
|
-
|
|
1358
|
-
if (fetchSite === "cross-site") return {
|
|
1342
|
+
if (firstCommaToken(headerValue(req, "sec-fetch-site"))?.toLowerCase() === "cross-site") return {
|
|
1359
1343
|
ok: false,
|
|
1360
1344
|
status: 403,
|
|
1361
1345
|
error: "cross-site request blocked"
|
|
@@ -1379,15 +1363,90 @@ function validateMutationRequest(req, opts = {}) {
|
|
|
1379
1363
|
status: 400,
|
|
1380
1364
|
error: "missing host header"
|
|
1381
1365
|
};
|
|
1382
|
-
|
|
1383
|
-
if (actualOrigin !== expectedOrigin) return {
|
|
1366
|
+
if (actualOrigin !== `${requestProto(req)}://${host}`.toLowerCase()) return {
|
|
1384
1367
|
ok: false,
|
|
1385
1368
|
status: 403,
|
|
1386
1369
|
error: "origin mismatch"
|
|
1387
1370
|
};
|
|
1388
1371
|
return { ok: true };
|
|
1389
1372
|
}
|
|
1390
|
-
|
|
1373
|
+
//#endregion
|
|
1374
|
+
//#region src/files/short-id.ts
|
|
1375
|
+
function shortId(prefix) {
|
|
1376
|
+
return `${prefix}-${randomUUID().replace(/-/g, "").slice(0, 8)}`;
|
|
1377
|
+
}
|
|
1378
|
+
//#endregion
|
|
1379
|
+
//#region src/files/folders.ts
|
|
1380
|
+
const FOLDER_ID_RE = /^f-[a-f0-9]{8}$/;
|
|
1381
|
+
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
|
1382
|
+
function foldersManifestPath(pagesRoot) {
|
|
1383
|
+
return path.join(pagesRoot, ".folders.json");
|
|
1384
|
+
}
|
|
1385
|
+
function emptyManifest() {
|
|
1386
|
+
return {
|
|
1387
|
+
folders: [],
|
|
1388
|
+
assignments: {}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
async function readManifest(file) {
|
|
1392
|
+
try {
|
|
1393
|
+
const raw = await fs.readFile(file, "utf8");
|
|
1394
|
+
const parsed = JSON.parse(raw);
|
|
1395
|
+
return {
|
|
1396
|
+
folders: Array.isArray(parsed.folders) ? parsed.folders : [],
|
|
1397
|
+
assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
|
|
1398
|
+
};
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
if (err.code === "ENOENT") return emptyManifest();
|
|
1401
|
+
throw err;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function writeManifest(file, manifest) {
|
|
1405
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
1406
|
+
await fs.writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
1407
|
+
}
|
|
1408
|
+
function newFolderId() {
|
|
1409
|
+
return shortId("f");
|
|
1410
|
+
}
|
|
1411
|
+
function validateName(v) {
|
|
1412
|
+
if (typeof v !== "string") return null;
|
|
1413
|
+
const trimmed = v.trim();
|
|
1414
|
+
if (trimmed.length < 1 || trimmed.length > 40) return null;
|
|
1415
|
+
return trimmed;
|
|
1416
|
+
}
|
|
1417
|
+
function validateReorder(v, current) {
|
|
1418
|
+
if (!Array.isArray(v) || v.length !== current.length) return null;
|
|
1419
|
+
const known = new Set(current.map((f) => f.id));
|
|
1420
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1421
|
+
const out = [];
|
|
1422
|
+
for (const id of v) {
|
|
1423
|
+
if (typeof id !== "string" || !FOLDER_ID_RE.test(id)) return null;
|
|
1424
|
+
if (!known.has(id) || seen.has(id)) return null;
|
|
1425
|
+
seen.add(id);
|
|
1426
|
+
out.push(id);
|
|
1427
|
+
}
|
|
1428
|
+
return out;
|
|
1429
|
+
}
|
|
1430
|
+
function validateIcon(v) {
|
|
1431
|
+
if (!v || typeof v !== "object") return null;
|
|
1432
|
+
const icon = v;
|
|
1433
|
+
if (icon.type === "emoji") {
|
|
1434
|
+
if (typeof icon.value !== "string") return null;
|
|
1435
|
+
if (icon.value.length < 1 || icon.value.length > 8) return null;
|
|
1436
|
+
return {
|
|
1437
|
+
type: "emoji",
|
|
1438
|
+
value: icon.value
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
if (icon.type === "color") {
|
|
1442
|
+
if (typeof icon.value !== "string" || !COLOR_RE.test(icon.value)) return null;
|
|
1443
|
+
return {
|
|
1444
|
+
type: "color",
|
|
1445
|
+
value: icon.value
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1391
1450
|
//#endregion
|
|
1392
1451
|
//#region src/vite/routes/context.ts
|
|
1393
1452
|
function makeContext(opts) {
|
|
@@ -1395,14 +1454,12 @@ function makeContext(opts) {
|
|
|
1395
1454
|
const pagesDir = opts.pagesDir ?? "pages";
|
|
1396
1455
|
const assetsDir = opts.assetsDir ?? "assets";
|
|
1397
1456
|
const pagesRoot = path.resolve(userCwd, pagesDir);
|
|
1398
|
-
const globalAssetsRoot = path.resolve(userCwd, assetsDir);
|
|
1399
|
-
const manifestPath = path.join(pagesRoot, ".folders.json");
|
|
1400
1457
|
return {
|
|
1401
1458
|
userCwd,
|
|
1402
1459
|
pagesDir,
|
|
1403
1460
|
pagesRoot,
|
|
1404
|
-
globalAssetsRoot,
|
|
1405
|
-
manifestPath,
|
|
1461
|
+
globalAssetsRoot: path.resolve(userCwd, assetsDir),
|
|
1462
|
+
manifestPath: foldersManifestPath(pagesRoot),
|
|
1406
1463
|
coreVersion: opts.coreVersion
|
|
1407
1464
|
};
|
|
1408
1465
|
}
|
|
@@ -1437,7 +1494,6 @@ function resolvePagePath(userCwd, pagesDir, pageId) {
|
|
|
1437
1494
|
function resolvePageEntryPath(ctx, pageId) {
|
|
1438
1495
|
return resolvePagePath(ctx.userCwd, ctx.pagesDir, pageId);
|
|
1439
1496
|
}
|
|
1440
|
-
|
|
1441
1497
|
//#endregion
|
|
1442
1498
|
//#region src/vite/routes/assets.ts
|
|
1443
1499
|
function registerAssetRoutes(server, ctx) {
|
|
@@ -1456,8 +1512,7 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1456
1512
|
const assetPath = isGlobal ? `@assets/${filename}` : `./assets/${filename}`;
|
|
1457
1513
|
let pageIds;
|
|
1458
1514
|
if (isGlobal) try {
|
|
1459
|
-
|
|
1460
|
-
pageIds = entries.filter((e) => e.isDirectory() && PAGE_ID_RE.test(e.name)).map((e) => e.name);
|
|
1515
|
+
pageIds = (await fs.readdir(ctx.pagesRoot, { withFileTypes: true })).filter((e) => e.isDirectory() && PAGE_ID_RE.test(e.name)).map((e) => e.name);
|
|
1461
1516
|
} catch {
|
|
1462
1517
|
pageIds = [];
|
|
1463
1518
|
}
|
|
@@ -1521,8 +1576,7 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1521
1576
|
const isGlobal = pageId === GLOBAL_SCOPE;
|
|
1522
1577
|
let scanIds;
|
|
1523
1578
|
if (isGlobal) try {
|
|
1524
|
-
|
|
1525
|
-
scanIds = dirs.filter((e) => e.isDirectory() && PAGE_ID_RE.test(e.name)).map((e) => e.name);
|
|
1579
|
+
scanIds = (await fs.readdir(ctx.pagesRoot, { withFileTypes: true })).filter((e) => e.isDirectory() && PAGE_ID_RE.test(e.name)).map((e) => e.name);
|
|
1526
1580
|
} catch {
|
|
1527
1581
|
scanIds = [];
|
|
1528
1582
|
}
|
|
@@ -1568,7 +1622,7 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1568
1622
|
const overwrite = url.searchParams.get("overwrite") === "1";
|
|
1569
1623
|
const lenHeader = req.headers["content-length"];
|
|
1570
1624
|
const len = typeof lenHeader === "string" ? Number(lenHeader) : NaN;
|
|
1571
|
-
if (Number.isFinite(len) && len >
|
|
1625
|
+
if (Number.isFinite(len) && len > 26214400) return json(res, 413, { error: "file too large" });
|
|
1572
1626
|
if (!overwrite) try {
|
|
1573
1627
|
await fs.access(file);
|
|
1574
1628
|
return json(res, 409, { error: "asset exists" });
|
|
@@ -1582,7 +1636,7 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1582
1636
|
await new Promise((resolve, reject) => {
|
|
1583
1637
|
req.on("data", (c) => {
|
|
1584
1638
|
total += c.length;
|
|
1585
|
-
if (total >
|
|
1639
|
+
if (total > 26214400) {
|
|
1586
1640
|
oversized = true;
|
|
1587
1641
|
req.destroy();
|
|
1588
1642
|
return;
|
|
@@ -1608,8 +1662,7 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1608
1662
|
if (method === "PATCH") {
|
|
1609
1663
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
1610
1664
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
1611
|
-
const
|
|
1612
|
-
const target = validateAssetName(body.name);
|
|
1665
|
+
const target = validateAssetName((await readBody(req)).name);
|
|
1613
1666
|
if (!target) return json(res, 400, { error: "invalid name" });
|
|
1614
1667
|
if (target === filename) return json(res, 200, {
|
|
1615
1668
|
ok: true,
|
|
@@ -1650,10 +1703,9 @@ function registerAssetRoutes(server, ctx) {
|
|
|
1650
1703
|
}
|
|
1651
1704
|
});
|
|
1652
1705
|
}
|
|
1653
|
-
|
|
1654
1706
|
//#endregion
|
|
1655
1707
|
//#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*\*\/\}
|
|
1708
|
+
const MARKER_RE = /\{\/\*\s*@page-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_-]+={0,2})"\s*\*\/\}/;
|
|
1657
1709
|
function b64urlEncode(s) {
|
|
1658
1710
|
return Buffer.from(s, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1659
1711
|
}
|
|
@@ -1665,9 +1717,7 @@ function parseMarkers(source) {
|
|
|
1665
1717
|
const comments = [];
|
|
1666
1718
|
const lines = source.split("\n");
|
|
1667
1719
|
for (let i = 0; i < lines.length; i++) {
|
|
1668
|
-
const
|
|
1669
|
-
MARKER_RE.lastIndex = 0;
|
|
1670
|
-
const m = MARKER_RE.exec(line);
|
|
1720
|
+
const m = lines[i].match(MARKER_RE);
|
|
1671
1721
|
if (!m) continue;
|
|
1672
1722
|
const [, id, ts, textB64] = m;
|
|
1673
1723
|
try {
|
|
@@ -1684,7 +1734,7 @@ function parseMarkers(source) {
|
|
|
1684
1734
|
return comments;
|
|
1685
1735
|
}
|
|
1686
1736
|
function newCommentId() {
|
|
1687
|
-
return
|
|
1737
|
+
return shortId("c");
|
|
1688
1738
|
}
|
|
1689
1739
|
function markerDeleteRegex(id) {
|
|
1690
1740
|
return new RegExp(`\\{\\/\\*\\s*@page-comment\\s+id="${id}"\\s+ts="[^"]+"\\s+text="[A-Za-z0-9_\\-]+={0,2}"\\s*\\*\\/\\}`);
|
|
@@ -1700,50 +1750,19 @@ function lineToOffset(source, line) {
|
|
|
1700
1750
|
}
|
|
1701
1751
|
function lineIndent(source, lineNumber) {
|
|
1702
1752
|
const start = lineToOffset(source, lineNumber);
|
|
1703
|
-
|
|
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);
|
|
1753
|
+
return source.slice(start, start + 200).match(/^[ \t]*/)?.[0] ?? "";
|
|
1721
1754
|
}
|
|
1722
1755
|
function planInsertion(source, target) {
|
|
1723
|
-
if (t
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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;
|
|
1756
|
+
if (t.isJSXElement(target) && target.openingElement.selfClosing) return null;
|
|
1757
|
+
return {
|
|
1758
|
+
offset: (t.isJSXFragment(target) ? target.openingFragment : target.openingElement).end ?? 0,
|
|
1759
|
+
indent: `${lineIndent(source, target.loc?.start.line ?? 1)} `
|
|
1760
|
+
};
|
|
1741
1761
|
}
|
|
1742
1762
|
function findInsertion(source, line, column) {
|
|
1743
1763
|
const ast = parseSource$1(source);
|
|
1744
1764
|
if (!ast) return null;
|
|
1745
|
-
const
|
|
1746
|
-
const ancestors = findJsxAncestors(ast, line, col);
|
|
1765
|
+
const ancestors = findJsxAncestors$1(ast, line, column ?? 0);
|
|
1747
1766
|
for (const node of ancestors) {
|
|
1748
1767
|
const plan = planInsertion(source, node);
|
|
1749
1768
|
if (plan) return plan;
|
|
@@ -1751,11 +1770,8 @@ function findInsertion(source, line, column) {
|
|
|
1751
1770
|
return null;
|
|
1752
1771
|
}
|
|
1753
1772
|
function offsetToLine(source, offset) {
|
|
1754
|
-
|
|
1755
|
-
for (let i = 0; i < offset && i < source.length; i++) if (source[i] === "\n") line++;
|
|
1756
|
-
return line;
|
|
1773
|
+
return source.slice(0, offset).split("\n").length;
|
|
1757
1774
|
}
|
|
1758
|
-
|
|
1759
1775
|
//#endregion
|
|
1760
1776
|
//#region src/vite/routes/comments.ts
|
|
1761
1777
|
function registerCommentRoutes(server, ctx) {
|
|
@@ -1764,8 +1780,7 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1764
1780
|
const method = req.method ?? "GET";
|
|
1765
1781
|
try {
|
|
1766
1782
|
if (method === "GET" && url.pathname === "/") {
|
|
1767
|
-
const
|
|
1768
|
-
const file = resolvePageEntryPath(ctx, pageId);
|
|
1783
|
+
const file = resolvePageEntryPath(ctx, url.searchParams.get("pageId") ?? "");
|
|
1769
1784
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1770
1785
|
let source;
|
|
1771
1786
|
try {
|
|
@@ -1779,8 +1794,7 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1779
1794
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
1780
1795
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
1781
1796
|
const body = await readBody(req);
|
|
1782
|
-
const
|
|
1783
|
-
const file = resolvePageEntryPath(ctx, pageId);
|
|
1797
|
+
const file = resolvePageEntryPath(ctx, body.pageId ?? "");
|
|
1784
1798
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1785
1799
|
if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
|
|
1786
1800
|
if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
|
|
@@ -1793,18 +1807,17 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1793
1807
|
const plan = findInsertion(source, body.line, body.column);
|
|
1794
1808
|
if (!plan) return json(res, 422, { error: `could not find a JSX container around line ${body.line}. Try clicking a different element.` });
|
|
1795
1809
|
const id = newCommentId();
|
|
1796
|
-
const ts = new Date().toISOString();
|
|
1810
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1797
1811
|
const payload = b64urlEncode(JSON.stringify({
|
|
1798
1812
|
note: body.text,
|
|
1799
1813
|
hint: body.hint
|
|
1800
1814
|
}));
|
|
1801
1815
|
const marker = `\n${plan.indent}{/* @page-comment id="${id}" ts="${ts}" text="${payload}" */}`;
|
|
1802
|
-
const next
|
|
1803
|
-
await fs.writeFile(file, next
|
|
1804
|
-
const markerLine = offsetToLine(next$1, plan.offset + 1);
|
|
1816
|
+
const next = source.slice(0, plan.offset) + marker + source.slice(plan.offset);
|
|
1817
|
+
await fs.writeFile(file, next, "utf8");
|
|
1805
1818
|
return json(res, 200, {
|
|
1806
1819
|
id,
|
|
1807
|
-
line:
|
|
1820
|
+
line: offsetToLine(next, plan.offset + 1)
|
|
1808
1821
|
});
|
|
1809
1822
|
}
|
|
1810
1823
|
if (method === "DELETE" && url.pathname.startsWith("/")) {
|
|
@@ -1812,8 +1825,7 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1812
1825
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
1813
1826
|
const id = url.pathname.slice(1);
|
|
1814
1827
|
if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
|
|
1815
|
-
const
|
|
1816
|
-
const file = resolvePageEntryPath(ctx, pageId);
|
|
1828
|
+
const file = resolvePageEntryPath(ctx, url.searchParams.get("pageId") ?? "");
|
|
1817
1829
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1818
1830
|
let source;
|
|
1819
1831
|
try {
|
|
@@ -1835,21 +1847,18 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1835
1847
|
}
|
|
1836
1848
|
});
|
|
1837
1849
|
}
|
|
1838
|
-
|
|
1839
1850
|
//#endregion
|
|
1840
1851
|
//#region src/vite/routes/edit.ts
|
|
1841
1852
|
function registerEditRoutes(server, ctx) {
|
|
1842
1853
|
server.middlewares.use("/__edit", async (req, res, next) => {
|
|
1843
1854
|
const url = new URL(req.url ?? "/", "http://local");
|
|
1844
|
-
|
|
1845
|
-
if (method !== "POST") return next();
|
|
1855
|
+
if ((req.method ?? "GET") !== "POST") return next();
|
|
1846
1856
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
1847
1857
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
1848
1858
|
try {
|
|
1849
1859
|
if (url.pathname === "/") {
|
|
1850
1860
|
const body = await readBody(req);
|
|
1851
|
-
const
|
|
1852
|
-
const file = resolvePageEntryPath(ctx, pageId);
|
|
1861
|
+
const file = resolvePageEntryPath(ctx, body.pageId ?? "");
|
|
1853
1862
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1854
1863
|
if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
|
|
1855
1864
|
if (!Array.isArray(body.ops)) return json(res, 400, { error: "missing ops" });
|
|
@@ -1893,8 +1902,7 @@ function registerEditRoutes(server, ctx) {
|
|
|
1893
1902
|
}
|
|
1894
1903
|
if (url.pathname === "/batch") {
|
|
1895
1904
|
const body = await readBody(req);
|
|
1896
|
-
const
|
|
1897
|
-
const file = resolvePageEntryPath(ctx, pageId);
|
|
1905
|
+
const file = resolvePageEntryPath(ctx, body.pageId ?? "");
|
|
1898
1906
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1899
1907
|
if (!Array.isArray(body.edits)) return json(res, 400, { error: "missing edits" });
|
|
1900
1908
|
let source;
|
|
@@ -1936,77 +1944,6 @@ function registerEditRoutes(server, ctx) {
|
|
|
1936
1944
|
}
|
|
1937
1945
|
});
|
|
1938
1946
|
}
|
|
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
1947
|
//#endregion
|
|
2011
1948
|
//#region src/vite/routes/folders.ts
|
|
2012
1949
|
function registerFolderRoutes(server, ctx) {
|
|
@@ -2014,10 +1951,7 @@ function registerFolderRoutes(server, ctx) {
|
|
|
2014
1951
|
const url = new URL(req.url ?? "/", "http://local");
|
|
2015
1952
|
const method = req.method ?? "GET";
|
|
2016
1953
|
try {
|
|
2017
|
-
if (method === "GET" && url.pathname === "/")
|
|
2018
|
-
const manifest = await readManifest(ctx.manifestPath);
|
|
2019
|
-
return json(res, 200, manifest);
|
|
2020
|
-
}
|
|
1954
|
+
if (method === "GET" && url.pathname === "/") return json(res, 200, await readManifest(ctx.manifestPath));
|
|
2021
1955
|
if (method === "POST" && url.pathname === "/") {
|
|
2022
1956
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2023
1957
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
@@ -2107,7 +2041,123 @@ function registerFolderRoutes(server, ctx) {
|
|
|
2107
2041
|
}
|
|
2108
2042
|
});
|
|
2109
2043
|
}
|
|
2110
|
-
|
|
2044
|
+
//#endregion
|
|
2045
|
+
//#region src/vite/routes/gfonts.ts
|
|
2046
|
+
const CATALOG_URL = "https://fonts.google.com/metadata/fonts";
|
|
2047
|
+
const CATALOG_TTL = 36e5;
|
|
2048
|
+
const LEGACY_UA = "Mozilla/5.0";
|
|
2049
|
+
const FONT_EXTS = /* @__PURE__ */ new Set([
|
|
2050
|
+
"ttf",
|
|
2051
|
+
"otf",
|
|
2052
|
+
"woff",
|
|
2053
|
+
"woff2"
|
|
2054
|
+
]);
|
|
2055
|
+
const FETCH_TIMEOUT_MS = 15e3;
|
|
2056
|
+
const DEFAULT_LIMIT = 30;
|
|
2057
|
+
const MAX_LIMIT = 100;
|
|
2058
|
+
function isGstaticHost(hostname) {
|
|
2059
|
+
const host = hostname.toLowerCase();
|
|
2060
|
+
return host === "gstatic.com" || host.endsWith(".gstatic.com");
|
|
2061
|
+
}
|
|
2062
|
+
function clampLimit(raw) {
|
|
2063
|
+
const n = Math.floor(Number(raw));
|
|
2064
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
|
|
2065
|
+
return Math.min(n, MAX_LIMIT);
|
|
2066
|
+
}
|
|
2067
|
+
let catalogCache = null;
|
|
2068
|
+
let catalogInflight = null;
|
|
2069
|
+
async function loadCatalog() {
|
|
2070
|
+
if (catalogCache && Date.now() - catalogCache.at < CATALOG_TTL) return catalogCache.entries;
|
|
2071
|
+
if (catalogInflight) return catalogInflight;
|
|
2072
|
+
catalogInflight = (async () => {
|
|
2073
|
+
const res = await fetch(CATALOG_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
2074
|
+
if (!res.ok) throw new Error(`metadata ${res.status}`);
|
|
2075
|
+
const text = await res.text();
|
|
2076
|
+
const start = text.indexOf("{");
|
|
2077
|
+
const entries = (JSON.parse(start > 0 ? text.slice(start) : text).familyMetadataList ?? []).map((f) => ({
|
|
2078
|
+
family: f.family,
|
|
2079
|
+
category: f.category ?? "",
|
|
2080
|
+
variants: Object.keys(f.fonts ?? {}),
|
|
2081
|
+
popularity: typeof f.popularity === "number" ? f.popularity : Number.MAX_SAFE_INTEGER
|
|
2082
|
+
}));
|
|
2083
|
+
catalogCache = {
|
|
2084
|
+
at: Date.now(),
|
|
2085
|
+
entries
|
|
2086
|
+
};
|
|
2087
|
+
return entries;
|
|
2088
|
+
})();
|
|
2089
|
+
try {
|
|
2090
|
+
return await catalogInflight;
|
|
2091
|
+
} finally {
|
|
2092
|
+
catalogInflight = null;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function cssFamilyParam(family) {
|
|
2096
|
+
return encodeURIComponent(family).replace(/%20/g, "+");
|
|
2097
|
+
}
|
|
2098
|
+
async function resolveFontFile(family, variant) {
|
|
2099
|
+
const italic = /i(talic)?$/.test(variant);
|
|
2100
|
+
const weight = variant.replace(/i(talic)?$/, "") || "400";
|
|
2101
|
+
const style = italic ? `${weight}italic` : weight;
|
|
2102
|
+
const cssUrl = `https://fonts.googleapis.com/css?family=${cssFamilyParam(family)}:${style}`;
|
|
2103
|
+
const css = await fetch(cssUrl, {
|
|
2104
|
+
headers: { "user-agent": LEGACY_UA },
|
|
2105
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2106
|
+
});
|
|
2107
|
+
if (!css.ok) return null;
|
|
2108
|
+
const match = (await css.text()).match(/url\((https:\/\/[^)]+)\)/);
|
|
2109
|
+
if (!match) return null;
|
|
2110
|
+
let parsed;
|
|
2111
|
+
try {
|
|
2112
|
+
parsed = new URL(match[1]);
|
|
2113
|
+
} catch {
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
if (!isGstaticHost(parsed.hostname)) return null;
|
|
2117
|
+
const rawExt = parsed.pathname.split(".").pop()?.toLowerCase() ?? "";
|
|
2118
|
+
const ext = FONT_EXTS.has(rawExt) ? rawExt : "ttf";
|
|
2119
|
+
return {
|
|
2120
|
+
url: parsed.toString(),
|
|
2121
|
+
ext
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
function registerGfontsRoutes(server) {
|
|
2125
|
+
server.middlewares.use("/__gfonts", async (req, res, next) => {
|
|
2126
|
+
const reqUrl = new URL(req.url ?? "/", "http://local");
|
|
2127
|
+
if ((req.method ?? "GET") !== "GET") return next();
|
|
2128
|
+
try {
|
|
2129
|
+
if (reqUrl.pathname === "/search") {
|
|
2130
|
+
const q = (reqUrl.searchParams.get("q") ?? "").trim().toLowerCase();
|
|
2131
|
+
const limit = clampLimit(reqUrl.searchParams.get("limit"));
|
|
2132
|
+
let entries = await loadCatalog();
|
|
2133
|
+
if (q) entries = entries.filter((e) => e.family.toLowerCase().includes(q));
|
|
2134
|
+
return json(res, 200, entries.slice().sort((a, b) => a.popularity - b.popularity).slice(0, limit).map((e) => ({
|
|
2135
|
+
family: e.family,
|
|
2136
|
+
category: e.category,
|
|
2137
|
+
variants: e.variants
|
|
2138
|
+
})));
|
|
2139
|
+
}
|
|
2140
|
+
if (reqUrl.pathname === "/download") {
|
|
2141
|
+
const family = reqUrl.searchParams.get("family");
|
|
2142
|
+
const variant = reqUrl.searchParams.get("variant") || "400";
|
|
2143
|
+
if (!family) return json(res, 400, { error: "missing family" });
|
|
2144
|
+
const resolved = await resolveFontFile(family, variant);
|
|
2145
|
+
if (!resolved) return json(res, 404, { error: "font not found" });
|
|
2146
|
+
const upstream = await fetch(resolved.url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
2147
|
+
if (!upstream.ok) return json(res, 502, { error: `gstatic ${upstream.status}` });
|
|
2148
|
+
res.statusCode = 200;
|
|
2149
|
+
res.setHeader("content-type", upstream.headers.get("content-type") ?? "font/ttf");
|
|
2150
|
+
res.setHeader("x-font-ext", resolved.ext);
|
|
2151
|
+
res.setHeader("cache-control", "no-store");
|
|
2152
|
+
res.end(Buffer.from(await upstream.arrayBuffer()));
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
return next();
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
json(res, 502, { error: String(err.message ?? err) });
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2111
2161
|
//#endregion
|
|
2112
2162
|
//#region src/vite/routes/pages.ts
|
|
2113
2163
|
function registerPageRoutes(server, ctx) {
|
|
@@ -2119,14 +2169,14 @@ function registerPageRoutes(server, ctx) {
|
|
|
2119
2169
|
if (duplicateMatch && method === "POST") {
|
|
2120
2170
|
const requestCheck = validateMutationRequest(req);
|
|
2121
2171
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2122
|
-
const pageId
|
|
2123
|
-
if (!PAGE_ID_RE.test(pageId
|
|
2172
|
+
const pageId = duplicateMatch[1];
|
|
2173
|
+
if (!PAGE_ID_RE.test(pageId)) return json(res, 400, { error: "invalid pageId" });
|
|
2124
2174
|
const body = await readBody(req);
|
|
2125
2175
|
if (body.newId !== void 0 && typeof body.newId !== "string") return json(res, 400, { error: "invalid newId" });
|
|
2126
|
-
const duplicated = await duplicatePageDir(ctx.pagesRoot, pageId
|
|
2176
|
+
const duplicated = await duplicatePageDir(ctx.pagesRoot, pageId, body.newId);
|
|
2127
2177
|
if (!duplicated.ok) return json(res, duplicated.status, { error: duplicated.error });
|
|
2128
2178
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2129
|
-
const folderId = manifest.assignments[pageId
|
|
2179
|
+
const folderId = manifest.assignments[pageId];
|
|
2130
2180
|
if (folderId) {
|
|
2131
2181
|
manifest.assignments[duplicated.pageId] = folderId;
|
|
2132
2182
|
await writeManifest(ctx.manifestPath, manifest);
|
|
@@ -2168,8 +2218,7 @@ function registerPageRoutes(server, ctx) {
|
|
|
2168
2218
|
if (method === "DELETE") {
|
|
2169
2219
|
const requestCheck = validateMutationRequest(req);
|
|
2170
2220
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2171
|
-
|
|
2172
|
-
if (!removed) return json(res, 404, { error: "page not found" });
|
|
2221
|
+
if (!await rmPageDir(ctx.pagesRoot, pageId)) return json(res, 404, { error: "page not found" });
|
|
2173
2222
|
const manifest = await readManifest(ctx.manifestPath);
|
|
2174
2223
|
delete manifest.assignments[pageId];
|
|
2175
2224
|
await writeManifest(ctx.manifestPath, manifest);
|
|
@@ -2182,10 +2231,6 @@ function registerPageRoutes(server, ctx) {
|
|
|
2182
2231
|
}
|
|
2183
2232
|
});
|
|
2184
2233
|
}
|
|
2185
|
-
|
|
2186
|
-
//#endregion
|
|
2187
|
-
//#region src/vite/routes/restart.ts
|
|
2188
|
-
const RESTART_EXIT_CODE = 52;
|
|
2189
2234
|
const DEV_SUPERVISED_ENV = "OPEN_PAGES_DEV_SUPERVISED";
|
|
2190
2235
|
const executionId = randomUUID();
|
|
2191
2236
|
function isSupervised() {
|
|
@@ -2205,18 +2250,16 @@ function registerRestartRoutes(server) {
|
|
|
2205
2250
|
const guard = validateMutationRequest(req);
|
|
2206
2251
|
if (!guard.ok) return json(res, guard.status, { error: guard.error });
|
|
2207
2252
|
if (!isSupervised()) return json(res, 409, { error: "dev server was not started by the open-pages CLI" });
|
|
2208
|
-
res.on("finish", () => process.exit(
|
|
2253
|
+
res.on("finish", () => process.exit(52));
|
|
2209
2254
|
json(res, 200, { restarting: true });
|
|
2210
2255
|
});
|
|
2211
2256
|
}
|
|
2212
|
-
|
|
2213
2257
|
//#endregion
|
|
2214
2258
|
//#region src/vite/routes/svgl.ts
|
|
2215
2259
|
function registerSvglRoutes(server) {
|
|
2216
2260
|
server.middlewares.use("/__svgl", async (req, res, next) => {
|
|
2217
2261
|
const reqUrl = new URL(req.url ?? "/", "http://local");
|
|
2218
|
-
|
|
2219
|
-
if (method !== "GET") return next();
|
|
2262
|
+
if ((req.method ?? "GET") !== "GET") return next();
|
|
2220
2263
|
try {
|
|
2221
2264
|
let target = null;
|
|
2222
2265
|
if (reqUrl.pathname === "/search") {
|
|
@@ -2253,7 +2296,6 @@ function registerSvglRoutes(server) {
|
|
|
2253
2296
|
}
|
|
2254
2297
|
});
|
|
2255
2298
|
}
|
|
2256
|
-
|
|
2257
2299
|
//#endregion
|
|
2258
2300
|
//#region src/vite/routes/update.ts
|
|
2259
2301
|
let updateInFlight = null;
|
|
@@ -2292,18 +2334,15 @@ function registerUpdateRoutes(server, ctx) {
|
|
|
2292
2334
|
updateInFlight ??= updatePackage(ctx).finally(() => {
|
|
2293
2335
|
updateInFlight = null;
|
|
2294
2336
|
});
|
|
2295
|
-
|
|
2296
|
-
json(res, 200, result);
|
|
2337
|
+
json(res, 200, await updateInFlight);
|
|
2297
2338
|
} catch (err) {
|
|
2298
2339
|
json(res, 500, { error: err instanceof Error ? err.message : "update failed" });
|
|
2299
2340
|
}
|
|
2300
2341
|
});
|
|
2301
2342
|
}
|
|
2302
|
-
|
|
2303
2343
|
//#endregion
|
|
2304
2344
|
//#region src/vite/routes/watchers.ts
|
|
2305
2345
|
function registerWatchers(server, ctx) {
|
|
2306
|
-
server.watcher.add(ctx.manifestPath);
|
|
2307
2346
|
server.watcher.on("change", (p) => {
|
|
2308
2347
|
if (p === ctx.manifestPath) server.ws.send({
|
|
2309
2348
|
type: "custom",
|
|
@@ -2321,8 +2360,7 @@ function registerWatchers(server, ctx) {
|
|
|
2321
2360
|
return;
|
|
2322
2361
|
}
|
|
2323
2362
|
if (!p.startsWith(ctx.pagesRoot + path.sep)) return;
|
|
2324
|
-
const
|
|
2325
|
-
const parts = rel.split(path.sep);
|
|
2363
|
+
const parts = p.slice(ctx.pagesRoot.length + 1).split(path.sep);
|
|
2326
2364
|
if (parts.length < 3 || parts[1] !== "assets") return;
|
|
2327
2365
|
const pageId = parts[0];
|
|
2328
2366
|
if (!PAGE_ID_RE.test(pageId)) return;
|
|
@@ -2336,7 +2374,6 @@ function registerWatchers(server, ctx) {
|
|
|
2336
2374
|
server.watcher.on("change", onAssetChange);
|
|
2337
2375
|
server.watcher.on("unlink", onAssetChange);
|
|
2338
2376
|
}
|
|
2339
|
-
|
|
2340
2377
|
//#endregion
|
|
2341
2378
|
//#region src/vite/api-plugin.ts
|
|
2342
2379
|
function apiPlugin(opts) {
|
|
@@ -2351,13 +2388,13 @@ function apiPlugin(opts) {
|
|
|
2351
2388
|
registerPageRoutes(server, ctx);
|
|
2352
2389
|
registerAssetRoutes(server, ctx);
|
|
2353
2390
|
registerSvglRoutes(server);
|
|
2391
|
+
registerGfontsRoutes(server);
|
|
2354
2392
|
registerFolderRoutes(server, ctx);
|
|
2355
2393
|
registerUpdateRoutes(server, ctx);
|
|
2356
2394
|
registerRestartRoutes(server);
|
|
2357
2395
|
}
|
|
2358
2396
|
};
|
|
2359
2397
|
}
|
|
2360
|
-
|
|
2361
2398
|
//#endregion
|
|
2362
2399
|
//#region src/vite/current-plugin.ts
|
|
2363
2400
|
const TEXT_SNIPPET_MAX = 120;
|
|
@@ -2411,7 +2448,7 @@ function currentPlugin(opts) {
|
|
|
2411
2448
|
cached = next;
|
|
2412
2449
|
const body = {
|
|
2413
2450
|
...next,
|
|
2414
|
-
updatedAt: new Date().toISOString()
|
|
2451
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2415
2452
|
};
|
|
2416
2453
|
try {
|
|
2417
2454
|
await fs.mkdir(outDir, { recursive: true });
|
|
@@ -2422,7 +2459,6 @@ function currentPlugin(opts) {
|
|
|
2422
2459
|
}
|
|
2423
2460
|
};
|
|
2424
2461
|
}
|
|
2425
|
-
|
|
2426
2462
|
//#endregion
|
|
2427
2463
|
//#region src/vite/design-plugin.ts
|
|
2428
2464
|
function parseSource(source) {
|
|
@@ -2484,17 +2520,13 @@ function literalToValue(node) {
|
|
|
2484
2520
|
}
|
|
2485
2521
|
case "TemplateLiteral": {
|
|
2486
2522
|
const quasis = node.quasis;
|
|
2487
|
-
|
|
2488
|
-
if (expressions.length > 0) throw new Error("template literal has expressions");
|
|
2523
|
+
if (node.expressions.length > 0) throw new Error("template literal has expressions");
|
|
2489
2524
|
return quasis[0].value.cooked ?? quasis[0].value.raw;
|
|
2490
2525
|
}
|
|
2491
|
-
case "ArrayExpression": {
|
|
2492
|
-
|
|
2493
|
-
return
|
|
2494
|
-
|
|
2495
|
-
return literalToValue(el);
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2526
|
+
case "ArrayExpression": return node.elements.map((el) => {
|
|
2527
|
+
if (!el) throw new Error("array has hole");
|
|
2528
|
+
return literalToValue(el);
|
|
2529
|
+
});
|
|
2498
2530
|
case "ObjectExpression": {
|
|
2499
2531
|
const properties = node.properties;
|
|
2500
2532
|
const out = {};
|
|
@@ -2544,18 +2576,16 @@ function serializeValue(value, level) {
|
|
|
2544
2576
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
2545
2577
|
if (Array.isArray(value)) {
|
|
2546
2578
|
if (value.length === 0) return "[]";
|
|
2547
|
-
|
|
2548
|
-
return `[${inner}]`;
|
|
2579
|
+
return `[${value.map((el) => serializeValue(el, level + 1)).join(", ")}]`;
|
|
2549
2580
|
}
|
|
2550
2581
|
if (isPlainObject(value)) {
|
|
2551
2582
|
const entries = Object.entries(value);
|
|
2552
2583
|
if (entries.length === 0) return "{}";
|
|
2553
2584
|
const childIndent = indent(level + 1);
|
|
2554
|
-
|
|
2585
|
+
return `{\n${entries.map(([k, v]) => {
|
|
2555
2586
|
const key = isValidIdentifier(k) ? k : jsString(k);
|
|
2556
2587
|
return `${childIndent}${key}: ${serializeValue(v, level + 1)},`;
|
|
2557
|
-
})
|
|
2558
|
-
return `{\n${lines.join("\n")}\n${indent(level)}}`;
|
|
2588
|
+
}).join("\n")}\n${indent(level)}}`;
|
|
2559
2589
|
}
|
|
2560
2590
|
throw new Error(`unsupported value type ${typeof value}`);
|
|
2561
2591
|
}
|
|
@@ -2590,10 +2620,9 @@ function parseDocDesign(source) {
|
|
|
2590
2620
|
error: err.message
|
|
2591
2621
|
};
|
|
2592
2622
|
}
|
|
2593
|
-
const merged = mergeDesign(defaultDesign, value);
|
|
2594
2623
|
return {
|
|
2595
2624
|
ok: true,
|
|
2596
|
-
design:
|
|
2625
|
+
design: mergeDesign(defaultDesign, value),
|
|
2597
2626
|
loc
|
|
2598
2627
|
};
|
|
2599
2628
|
}
|
|
@@ -2644,52 +2673,44 @@ function ensureDesignSystemImport(source, ast) {
|
|
|
2644
2673
|
const imports = findImports(ast);
|
|
2645
2674
|
const coreImport = imports.find((imp) => imp.source === "@autono/open-pages");
|
|
2646
2675
|
if (coreImport) {
|
|
2647
|
-
|
|
2676
|
+
if (coreImport.specifiers.some((spec) => {
|
|
2648
2677
|
if (spec.type !== "ImportSpecifier") return false;
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
});
|
|
2652
|
-
if (hasDesignSystem) return {
|
|
2678
|
+
return spec.imported?.name === "DesignSystem";
|
|
2679
|
+
})) return {
|
|
2653
2680
|
source,
|
|
2654
2681
|
offsetShift: 0
|
|
2655
2682
|
};
|
|
2656
2683
|
const node = coreImport.node;
|
|
2657
|
-
const
|
|
2658
|
-
const braceClose = importText.lastIndexOf("}");
|
|
2684
|
+
const braceClose = source.slice(node.start, node.end).lastIndexOf("}");
|
|
2659
2685
|
if (braceClose === -1) return {
|
|
2660
2686
|
source,
|
|
2661
2687
|
offsetShift: 0
|
|
2662
2688
|
};
|
|
2663
2689
|
const absoluteBrace = node.start + braceClose;
|
|
2664
2690
|
const insertText = coreImport.specifiers.length > 0 ? ", type DesignSystem" : "type DesignSystem";
|
|
2665
|
-
const next$1 = `${source.slice(0, absoluteBrace)}${insertText}${source.slice(absoluteBrace)}`;
|
|
2666
2691
|
return {
|
|
2667
|
-
source:
|
|
2692
|
+
source: `${source.slice(0, absoluteBrace)}${insertText}${source.slice(absoluteBrace)}`,
|
|
2668
2693
|
offsetShift: insertText.length
|
|
2669
2694
|
};
|
|
2670
2695
|
}
|
|
2671
2696
|
const stmt = `import type { DesignSystem } from '@autono/open-pages';\n`;
|
|
2672
2697
|
if (imports.length > 0) {
|
|
2673
|
-
const
|
|
2674
|
-
const insertAt = last.node.end;
|
|
2698
|
+
const insertAt = imports[imports.length - 1].node.end;
|
|
2675
2699
|
const trail = source[insertAt] === "\n" ? "" : "\n";
|
|
2676
|
-
const next$1 = `${source.slice(0, insertAt)}\n${stmt.slice(0, -1)}${trail}${source.slice(insertAt)}`;
|
|
2677
2700
|
return {
|
|
2678
|
-
source:
|
|
2701
|
+
source: `${source.slice(0, insertAt)}\n${stmt.slice(0, -1)}${trail}${source.slice(insertAt)}`,
|
|
2679
2702
|
offsetShift: 1 + stmt.length - (trail ? 0 : 1)
|
|
2680
2703
|
};
|
|
2681
2704
|
}
|
|
2682
|
-
const next = `${stmt}\n${source}`;
|
|
2683
2705
|
return {
|
|
2684
|
-
source:
|
|
2706
|
+
source: `${stmt}\n${source}`,
|
|
2685
2707
|
offsetShift: stmt.length + 1
|
|
2686
2708
|
};
|
|
2687
2709
|
}
|
|
2688
2710
|
function findInsertionPoint(source, ast) {
|
|
2689
2711
|
const imports = findImports(ast);
|
|
2690
2712
|
if (imports.length === 0) return 0;
|
|
2691
|
-
|
|
2692
|
-
let off = last.node.end;
|
|
2713
|
+
let off = imports[imports.length - 1].node.end;
|
|
2693
2714
|
while (off < source.length && source[off] !== "\n") off++;
|
|
2694
2715
|
if (off < source.length) off++;
|
|
2695
2716
|
return off;
|
|
@@ -2712,14 +2733,11 @@ function applyDesignWrite(source, next) {
|
|
|
2712
2733
|
error: "could not parse page source"
|
|
2713
2734
|
};
|
|
2714
2735
|
const loc = findDesignDecl(ast);
|
|
2715
|
-
if (loc) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
created: false
|
|
2721
|
-
};
|
|
2722
|
-
}
|
|
2736
|
+
if (loc) return {
|
|
2737
|
+
ok: true,
|
|
2738
|
+
source: source.slice(0, loc.objectStart) + body + source.slice(loc.objectEnd),
|
|
2739
|
+
created: false
|
|
2740
|
+
};
|
|
2723
2741
|
const withImport = ensureDesignSystemImport(source, ast);
|
|
2724
2742
|
const ast2 = parseSource(withImport.source);
|
|
2725
2743
|
if (!ast2) return {
|
|
@@ -2729,10 +2747,9 @@ function applyDesignWrite(source, next) {
|
|
|
2729
2747
|
};
|
|
2730
2748
|
const insertAt = findInsertionPoint(withImport.source, ast2);
|
|
2731
2749
|
const block = `\nconst design: DesignSystem = ${body};\n`;
|
|
2732
|
-
const out = withImport.source.slice(0, insertAt) + block + withImport.source.slice(insertAt);
|
|
2733
2750
|
return {
|
|
2734
2751
|
ok: true,
|
|
2735
|
-
source:
|
|
2752
|
+
source: withImport.source.slice(0, insertAt) + block + withImport.source.slice(insertAt),
|
|
2736
2753
|
created: true
|
|
2737
2754
|
};
|
|
2738
2755
|
}
|
|
@@ -2777,8 +2794,7 @@ function designPlugin(opts) {
|
|
|
2777
2794
|
if (method === "PUT" && url.pathname === "/") {
|
|
2778
2795
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2779
2796
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2780
|
-
const
|
|
2781
|
-
const patch = body.patch;
|
|
2797
|
+
const patch = (await readBody(req)).patch;
|
|
2782
2798
|
if (!patch || typeof patch !== "object") return json(res, 400, { error: "missing patch object" });
|
|
2783
2799
|
let source;
|
|
2784
2800
|
try {
|
|
@@ -2825,10 +2841,9 @@ function designPlugin(opts) {
|
|
|
2825
2841
|
}
|
|
2826
2842
|
};
|
|
2827
2843
|
}
|
|
2828
|
-
|
|
2829
2844
|
//#endregion
|
|
2830
2845
|
//#region src/vite/loc-tags-plugin.ts
|
|
2831
|
-
const UNTAGGABLE_COMPONENTS = new Set([
|
|
2846
|
+
const UNTAGGABLE_COMPONENTS = /* @__PURE__ */ new Set([
|
|
2832
2847
|
"Fragment",
|
|
2833
2848
|
"StrictMode",
|
|
2834
2849
|
"Suspense"
|
|
@@ -2873,8 +2888,7 @@ function isPageSourceFile(id, docsRootPosix) {
|
|
|
2873
2888
|
if (!filePath.startsWith(`${docsRootPosix}/`)) return false;
|
|
2874
2889
|
if (!filePath.endsWith(".tsx")) return false;
|
|
2875
2890
|
if (filePath.endsWith(".d.ts") || filePath.endsWith(".test.tsx")) return false;
|
|
2876
|
-
|
|
2877
|
-
return rel.includes("/");
|
|
2891
|
+
return filePath.slice(docsRootPosix.length + 1).includes("/");
|
|
2878
2892
|
}
|
|
2879
2893
|
function locTagsPlugin(opts) {
|
|
2880
2894
|
const pagesRoot = path.resolve(opts.userCwd, opts.pagesDir ?? "pages").replace(/\\/g, "/");
|
|
@@ -2893,7 +2907,6 @@ function locTagsPlugin(opts) {
|
|
|
2893
2907
|
}
|
|
2894
2908
|
};
|
|
2895
2909
|
}
|
|
2896
|
-
|
|
2897
2910
|
//#endregion
|
|
2898
2911
|
//#region src/vite/themes-plugin.ts
|
|
2899
2912
|
const THEMES_VMOD = "virtual:open-pages/themes";
|
|
@@ -2924,17 +2937,15 @@ function parseFrontmatter(raw, themeId) {
|
|
|
2924
2937
|
async function findThemes(userCwd, themesDir) {
|
|
2925
2938
|
const abs = path.resolve(userCwd, themesDir);
|
|
2926
2939
|
if (!existsSync(abs)) return [];
|
|
2927
|
-
|
|
2940
|
+
return (await fg("*.md", {
|
|
2928
2941
|
cwd: abs,
|
|
2929
2942
|
absolute: true,
|
|
2930
2943
|
onlyFiles: true
|
|
2931
|
-
});
|
|
2932
|
-
return hits.sort();
|
|
2944
|
+
})).sort();
|
|
2933
2945
|
}
|
|
2934
2946
|
async function readTheme(mdAbs, themesRoot) {
|
|
2935
2947
|
const id = path.basename(mdAbs, ".md");
|
|
2936
|
-
const
|
|
2937
|
-
const { fm, body } = parseFrontmatter(raw, id);
|
|
2948
|
+
const { fm, body } = parseFrontmatter(await fs.readFile(mdAbs, "utf8"), id);
|
|
2938
2949
|
const demoCandidates = [
|
|
2939
2950
|
`${id}.demo.tsx`,
|
|
2940
2951
|
`${id}.demo.jsx`,
|
|
@@ -2960,30 +2971,33 @@ async function readTheme(mdAbs, themesRoot) {
|
|
|
2960
2971
|
};
|
|
2961
2972
|
}
|
|
2962
2973
|
function generateThemesModule(themes, isDev) {
|
|
2963
|
-
const meta = themes.map((t
|
|
2964
|
-
id: t
|
|
2965
|
-
name: t
|
|
2966
|
-
description: t
|
|
2967
|
-
body: t
|
|
2968
|
-
hasDemo: t
|
|
2969
|
-
hasCss: t
|
|
2974
|
+
const meta = themes.map((t) => ({
|
|
2975
|
+
id: t.id,
|
|
2976
|
+
name: t.frontmatter.name,
|
|
2977
|
+
description: t.frontmatter.description,
|
|
2978
|
+
body: t.body,
|
|
2979
|
+
hasDemo: t.demoAbs !== null,
|
|
2980
|
+
hasCss: t.cssAbs !== null
|
|
2970
2981
|
}));
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
const importPath = isDev ? `@fs/${normalizePath(abs).replace(/^\/+/, "")}` : abs;
|
|
2982
|
+
const withDemo = themes.filter((t) => t.demoAbs !== null);
|
|
2983
|
+
const cases = withDemo.map((t) => {
|
|
2984
|
+
const importPath = isDev ? `@fs/${normalizePath(t.demoAbs).replace(/^\/+/, "")}` : t.demoAbs;
|
|
2975
2985
|
const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(importPath)})` : `import(${JSON.stringify(importPath)})`;
|
|
2976
|
-
return
|
|
2986
|
+
return ` case ${JSON.stringify(t.id)}: return ${importExpr};`;
|
|
2977
2987
|
}).join("\n");
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
|
|
2988
|
+
const urlCases = withDemo.map((t) => {
|
|
2989
|
+
const urlExpr = isDev ? `import.meta.env.BASE_URL + ${JSON.stringify(`@fs/${normalizePath(t.demoAbs).replace(/^\/+/, "")}`)}` : "''";
|
|
2990
|
+
return ` case ${JSON.stringify(t.id)}: return ${urlExpr};`;
|
|
2991
|
+
}).join("\n");
|
|
2992
|
+
const withCss = themes.filter((t) => t.cssAbs !== null);
|
|
2993
|
+
const cssCases = withCss.map((t) => {
|
|
2994
|
+
const importPath = isDev ? `@fs/${normalizePath(t.cssAbs).replace(/^\/+/, "")}` : t.cssAbs;
|
|
2981
2995
|
const importExpr = isDev ? `import(/* @vite-ignore */ import.meta.env.BASE_URL + ${JSON.stringify(importPath)})` : `import(${JSON.stringify(importPath)})`;
|
|
2982
|
-
return ` case ${JSON.stringify(t
|
|
2996
|
+
return ` case ${JSON.stringify(t.id)}: return ${importExpr};`;
|
|
2983
2997
|
}).join("\n");
|
|
2984
2998
|
return `// virtual:open-pages/themes — generated
|
|
2985
2999
|
export const themes = ${JSON.stringify(meta)};
|
|
2986
|
-
export const themeCssIds = ${JSON.stringify(withCss.map((t
|
|
3000
|
+
export const themeCssIds = ${JSON.stringify(withCss.map((t) => t.id))};
|
|
2987
3001
|
|
|
2988
3002
|
// Importing a stylesheet through Vite injects it into the document; the
|
|
2989
3003
|
// frame calls this for a page's meta.theme and for theme demos.
|
|
@@ -3000,6 +3014,15 @@ ${cases}
|
|
|
3000
3014
|
default: throw new Error('Theme demo not found: ' + id);
|
|
3001
3015
|
}
|
|
3002
3016
|
}
|
|
3017
|
+
|
|
3018
|
+
// Dev-server module URL so the render worker can import the same demo the
|
|
3019
|
+
// main thread just loaded. A static build imports through loadThemeDemo.
|
|
3020
|
+
export function demoImportUrl(id) {
|
|
3021
|
+
switch (id) {
|
|
3022
|
+
${urlCases}
|
|
3023
|
+
default: throw new Error('Theme demo not found: ' + id);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3003
3026
|
`;
|
|
3004
3027
|
}
|
|
3005
3028
|
function themesPlugin(opts) {
|
|
@@ -3019,8 +3042,7 @@ function themesPlugin(opts) {
|
|
|
3019
3042
|
async load(id) {
|
|
3020
3043
|
if (id !== resolved(THEMES_VMOD)) return null;
|
|
3021
3044
|
const files = await findThemes(userCwd, themesDir);
|
|
3022
|
-
|
|
3023
|
-
return generateThemesModule(themes, isDev);
|
|
3045
|
+
return generateThemesModule(await Promise.all(files.map((f) => readTheme(f, themesRoot))), isDev);
|
|
3024
3046
|
},
|
|
3025
3047
|
configureServer(server) {
|
|
3026
3048
|
const isThemeFile = (p) => {
|
|
@@ -3052,7 +3074,6 @@ function themesPlugin(opts) {
|
|
|
3052
3074
|
}
|
|
3053
3075
|
};
|
|
3054
3076
|
}
|
|
3055
|
-
|
|
3056
3077
|
//#endregion
|
|
3057
3078
|
//#region src/vite/config.ts
|
|
3058
3079
|
const CORE_VERSION = readCoreVersion();
|
|
@@ -3104,7 +3125,11 @@ async function createViteConfig(opts) {
|
|
|
3104
3125
|
"@assets": assetsAbs
|
|
3105
3126
|
} },
|
|
3106
3127
|
optimizeDeps: {
|
|
3107
|
-
entries: [
|
|
3128
|
+
entries: [
|
|
3129
|
+
path.join(APP_ROOT, "main.tsx"),
|
|
3130
|
+
path.join(APP_ROOT, "frame", "main.tsx"),
|
|
3131
|
+
path.join(pagesAbs, "*", "index.{tsx,jsx,ts,js}")
|
|
3132
|
+
],
|
|
3108
3133
|
include: [
|
|
3109
3134
|
"react",
|
|
3110
3135
|
"react-dom",
|
|
@@ -3120,15 +3145,15 @@ async function createViteConfig(opts) {
|
|
|
3120
3145
|
"class-variance-authority",
|
|
3121
3146
|
"emoji-picker-react"
|
|
3122
3147
|
],
|
|
3123
|
-
|
|
3124
|
-
target: "es2022",
|
|
3148
|
+
rolldownOptions: {
|
|
3149
|
+
transform: { target: "es2022" },
|
|
3125
3150
|
plugins: [{
|
|
3126
3151
|
name: "open-pages:virtual-externals",
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3152
|
+
resolveId(id) {
|
|
3153
|
+
return id.startsWith("virtual:open-pages/") ? {
|
|
3154
|
+
id,
|
|
3130
3155
|
external: true
|
|
3131
|
-
}
|
|
3156
|
+
} : null;
|
|
3132
3157
|
}
|
|
3133
3158
|
}]
|
|
3134
3159
|
}
|
|
@@ -3157,6 +3182,5 @@ async function createViteConfig(opts) {
|
|
|
3157
3182
|
}
|
|
3158
3183
|
};
|
|
3159
3184
|
}
|
|
3160
|
-
|
|
3161
3185
|
//#endregion
|
|
3162
|
-
export { DEV_SUPERVISED_ENV
|
|
3186
|
+
export { DEV_SUPERVISED_ENV as n, createViteConfig as t };
|