@autono/open-pages 0.7.0 → 0.9.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-CCoxDxL-.js → build-Cpod-K9R.js} +9 -4
- package/dist/cli/bin.js +7 -7
- package/dist/{config-D_U_aCQz.js → config-CYLBzMMb.js} +1077 -18
- package/dist/{config-p-TWNXbv.d.ts → config-IkwICvOe.d.ts} +2 -1
- package/dist/{dev-CTrB263u.js → dev-B7x-G-wk.js} +1 -1
- package/dist/{export-enKL9R7x.js → export-BxMksWEc.js} +74 -12
- package/dist/index.d.ts +23 -3
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +96 -0
- package/dist/{preview-Dmzyd1cf.js → preview-CMv1lGOC.js} +1 -1
- package/dist/{types-DDdUFUIS.d.ts → types-B-9WXL93.d.ts} +24 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/apply-comments/SKILL.md +6 -3
- package/skills/create-email/SKILL.md +166 -0
- package/skills/create-page/SKILL.md +5 -1
- package/skills/create-theme/SKILL.md +33 -7
- package/skills/create-theme/references/email-theme-from-css.mjs +185 -0
- package/skills/create-theme/references/email-theme.md +48 -0
- package/skills/current-page/SKILL.md +3 -3
- package/skills/page-authoring/SKILL.md +3 -0
- package/skills/web-design-guidelines/SKILL.md +41 -0
- package/src/app/app.tsx +4 -0
- package/src/app/components/command/home-command-menu.tsx +19 -2
- package/src/app/components/selection-note.tsx +71 -0
- package/src/app/components/sidebar/folder-item.tsx +9 -2
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/frame/email-inspect.ts +17 -0
- package/src/app/lib/emails.ts +17 -0
- package/src/app/lib/frame.ts +15 -4
- package/src/app/lib/page-thumb.tsx +11 -3
- package/src/app/lib/sdk.ts +22 -0
- package/src/app/routes/email.tsx +424 -0
- package/src/app/routes/emails.tsx +94 -0
- package/src/app/routes/home-shell.tsx +21 -2
- package/src/app/routes/page.tsx +21 -57
- package/src/app/virtual.d.ts +10 -0
- package/src/locale/en.ts +25 -0
- package/src/locale/ja.ts +25 -0
- package/src/locale/types.ts +24 -0
- package/src/locale/zh-cn.ts +25 -0
- package/src/locale/zh-tw.ts +25 -0
- package/workspace/components.json +3 -1
- package/workspace/styles/globals.css +8 -2
- package/dist/open-pages-plugin-C8cFhMB9.js +0 -662
- package/dist/version-DZrXLoAD.js +0 -42
|
@@ -1,18 +1,278 @@
|
|
|
1
|
-
import { n as PKG_ROOT, r as readCoreVersion, t as APP_ROOT } from "./version-DZrXLoAD.js";
|
|
2
1
|
import { n as defaultDesign } from "./design-Cpqx7SJs.js";
|
|
3
2
|
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 {
|
|
3
|
+
import { createRequire } from "node:module";
|
|
5
4
|
import path from "node:path";
|
|
6
|
-
import {
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import fs from "node:fs/promises";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import tailwindcss from "@tailwindcss/vite";
|
|
10
10
|
import react from "@vitejs/plugin-react";
|
|
11
|
-
import { normalizePath, searchForWorkspaceRoot } from "vite";
|
|
11
|
+
import { loadConfigFromFile, normalizePath, searchForWorkspaceRoot } from "vite";
|
|
12
12
|
import { parse } from "@babel/parser";
|
|
13
13
|
import * as t from "@babel/types";
|
|
14
14
|
import { isJSXElement, isJSXFragment } from "@babel/types";
|
|
15
15
|
import fg from "fast-glob";
|
|
16
|
+
//#region \0rolldown/runtime.js
|
|
17
|
+
var __defProp = Object.defineProperty;
|
|
18
|
+
var __exportAll = (all, no_symbols) => {
|
|
19
|
+
let target = {};
|
|
20
|
+
for (var name in all) __defProp(target, name, {
|
|
21
|
+
get: all[name],
|
|
22
|
+
enumerable: true
|
|
23
|
+
});
|
|
24
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
return target;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/editing/page-ops.ts
|
|
29
|
+
const PAGE_ID_RE = /^[a-z0-9_-]+$/i;
|
|
30
|
+
function validatePageName(v) {
|
|
31
|
+
if (typeof v !== "string") return null;
|
|
32
|
+
const trimmed = v.trim();
|
|
33
|
+
if (trimmed.length < 1 || trimmed.length > 80) return null;
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
function unwrapExpression(node) {
|
|
37
|
+
let current = node;
|
|
38
|
+
while (current && (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression")) current = current.expression;
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
function readMetaTitleInSource(source) {
|
|
42
|
+
let ast;
|
|
43
|
+
try {
|
|
44
|
+
ast = parse(source, {
|
|
45
|
+
sourceType: "module",
|
|
46
|
+
plugins: ["typescript", "jsx"],
|
|
47
|
+
errorRecovery: true
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return { kind: "unsupported" };
|
|
51
|
+
}
|
|
52
|
+
const body = ast.program?.body ?? [];
|
|
53
|
+
for (const stmt of body) {
|
|
54
|
+
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
55
|
+
const decl = stmt.declaration;
|
|
56
|
+
if (!decl || decl.type !== "VariableDeclaration") continue;
|
|
57
|
+
const declarations = decl.declarations ?? [];
|
|
58
|
+
for (const d of declarations) {
|
|
59
|
+
const id = d.id;
|
|
60
|
+
if (!id || id.type !== "Identifier" || id.name !== "meta") continue;
|
|
61
|
+
const init = unwrapExpression(d.init);
|
|
62
|
+
if (!init || init.type !== "ObjectExpression") return { kind: "unsupported" };
|
|
63
|
+
const properties = init.properties ?? [];
|
|
64
|
+
for (const property of properties) {
|
|
65
|
+
if (property.type !== "ObjectProperty" || property.computed) continue;
|
|
66
|
+
const key = property.key;
|
|
67
|
+
if ((key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" ? key.value : void 0) !== "title") continue;
|
|
68
|
+
const value = property.value;
|
|
69
|
+
if (value?.type === "StringLiteral" && typeof value.value === "string") return {
|
|
70
|
+
kind: "found",
|
|
71
|
+
title: value.value
|
|
72
|
+
};
|
|
73
|
+
if (value?.type === "TemplateLiteral") {
|
|
74
|
+
const expressions = value.expressions ?? [];
|
|
75
|
+
const firstValue = (value.quasis ?? [])[0]?.value;
|
|
76
|
+
const cooked = firstValue?.cooked;
|
|
77
|
+
const raw = firstValue?.raw;
|
|
78
|
+
if (expressions.length === 0 && typeof (cooked ?? raw) === "string") return {
|
|
79
|
+
kind: "found",
|
|
80
|
+
title: cooked ?? raw
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { kind: "unsupported" };
|
|
84
|
+
}
|
|
85
|
+
return { kind: "missing" };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { kind: "missing" };
|
|
89
|
+
}
|
|
90
|
+
async function rmPageDir(pagesRoot, pageId) {
|
|
91
|
+
if (!PAGE_ID_RE.test(pageId)) return false;
|
|
92
|
+
const dir = path.resolve(pagesRoot, pageId);
|
|
93
|
+
if (!dir.startsWith(pagesRoot + path.sep)) return false;
|
|
94
|
+
try {
|
|
95
|
+
await fs.rm(dir, {
|
|
96
|
+
recursive: true,
|
|
97
|
+
force: true
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function duplicatePageDir(pagesRoot, pageId, desiredId) {
|
|
105
|
+
if (!PAGE_ID_RE.test(pageId)) return {
|
|
106
|
+
ok: false,
|
|
107
|
+
status: 400,
|
|
108
|
+
error: "invalid pageId"
|
|
109
|
+
};
|
|
110
|
+
const root = path.resolve(pagesRoot);
|
|
111
|
+
const srcDir = path.resolve(root, pageId);
|
|
112
|
+
if (!srcDir.startsWith(root + path.sep)) return {
|
|
113
|
+
ok: false,
|
|
114
|
+
status: 400,
|
|
115
|
+
error: "invalid pageId"
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
await fs.access(path.join(srcDir, "index.tsx"));
|
|
119
|
+
} catch {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
status: 404,
|
|
123
|
+
error: "page not found"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let newId;
|
|
127
|
+
if (desiredId !== void 0) {
|
|
128
|
+
if (!PAGE_ID_RE.test(desiredId)) return {
|
|
129
|
+
ok: false,
|
|
130
|
+
status: 400,
|
|
131
|
+
error: "invalid newId"
|
|
132
|
+
};
|
|
133
|
+
newId = desiredId;
|
|
134
|
+
const dstDir = path.resolve(root, newId);
|
|
135
|
+
if (!dstDir.startsWith(root + path.sep)) return {
|
|
136
|
+
ok: false,
|
|
137
|
+
status: 400,
|
|
138
|
+
error: "invalid newId"
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
await fs.access(dstDir);
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
status: 409,
|
|
145
|
+
error: "page already exists"
|
|
146
|
+
};
|
|
147
|
+
} catch {}
|
|
148
|
+
} else {
|
|
149
|
+
let suffix = 1;
|
|
150
|
+
while (true) {
|
|
151
|
+
newId = suffix === 1 ? `${pageId}-copy` : `${pageId}-copy-${suffix}`;
|
|
152
|
+
try {
|
|
153
|
+
await fs.access(path.resolve(root, newId));
|
|
154
|
+
suffix++;
|
|
155
|
+
} catch {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const dstDir = path.resolve(root, newId);
|
|
161
|
+
if (!dstDir.startsWith(root + path.sep)) return {
|
|
162
|
+
ok: false,
|
|
163
|
+
status: 400,
|
|
164
|
+
error: "invalid newId"
|
|
165
|
+
};
|
|
166
|
+
const srcEntry = path.join(srcDir, "index.tsx");
|
|
167
|
+
let copiedEntrySource;
|
|
168
|
+
try {
|
|
169
|
+
const source = await fs.readFile(srcEntry, "utf8");
|
|
170
|
+
const metaTitle = readMetaTitleInSource(source);
|
|
171
|
+
if (metaTitle.kind === "unsupported") return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: 422,
|
|
174
|
+
error: "could not update copied page title"
|
|
175
|
+
};
|
|
176
|
+
const updated = updateMetaTitleInSource(source, `${metaTitle.kind === "found" ? metaTitle.title : pageId} (copy)`);
|
|
177
|
+
if (updated === null) return {
|
|
178
|
+
ok: false,
|
|
179
|
+
status: 422,
|
|
180
|
+
error: "could not update copied page title"
|
|
181
|
+
};
|
|
182
|
+
copiedEntrySource = updated;
|
|
183
|
+
} catch {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
status: 404,
|
|
187
|
+
error: "page not found"
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await fs.cp(srcDir, dstDir, {
|
|
192
|
+
recursive: true,
|
|
193
|
+
errorOnExist: true,
|
|
194
|
+
force: false
|
|
195
|
+
});
|
|
196
|
+
await fs.writeFile(path.join(dstDir, "index.tsx"), copiedEntrySource, "utf8");
|
|
197
|
+
return {
|
|
198
|
+
ok: true,
|
|
199
|
+
pageId: newId
|
|
200
|
+
};
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err.code === "EEXIST") return {
|
|
203
|
+
ok: false,
|
|
204
|
+
status: 409,
|
|
205
|
+
error: "page already exists"
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
status: 500,
|
|
210
|
+
error: String(err.message ?? err)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function resolvePageEntry(pagesRoot, pageId) {
|
|
215
|
+
if (!PAGE_ID_RE.test(pageId)) return null;
|
|
216
|
+
const dir = path.resolve(pagesRoot, pageId);
|
|
217
|
+
if (!dir.startsWith(pagesRoot + path.sep)) return null;
|
|
218
|
+
return path.join(dir, "index.tsx");
|
|
219
|
+
}
|
|
220
|
+
function escapeSingleQuoted(s) {
|
|
221
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Rewrite (or insert) the `title` field in the page module's `export const meta`.
|
|
225
|
+
*
|
|
226
|
+
* Strategy:
|
|
227
|
+
* 1. Find `export const meta` and brace-match its object literal.
|
|
228
|
+
* 2. If the object already has a `title: '...'` entry, replace the literal.
|
|
229
|
+
* 3. If the object exists but has no title, inject a new `title: '...'` line
|
|
230
|
+
* as the first property (preserving the author's surrounding indentation).
|
|
231
|
+
* 4. If there is no `meta` export at all, insert a fresh one right before
|
|
232
|
+
* `export default`.
|
|
233
|
+
*
|
|
234
|
+
* Returns the rewritten source, or `null` if the file shape was too surprising
|
|
235
|
+
* to touch safely (e.g. `export default` missing when we'd need to inject meta).
|
|
236
|
+
*/
|
|
237
|
+
function updateMetaTitleInSource(source, title) {
|
|
238
|
+
const newLiteral = `'${escapeSingleQuoted(title)}'`;
|
|
239
|
+
const metaStart = source.search(/export\s+const\s+meta\b/);
|
|
240
|
+
if (metaStart !== -1) {
|
|
241
|
+
const eqIdx = source.indexOf("=", metaStart);
|
|
242
|
+
if (eqIdx === -1) return null;
|
|
243
|
+
const openBrace = source.indexOf("{", eqIdx);
|
|
244
|
+
if (openBrace === -1) return null;
|
|
245
|
+
let depth = 0;
|
|
246
|
+
let closeBrace = -1;
|
|
247
|
+
for (let i = openBrace; i < source.length; i++) {
|
|
248
|
+
const ch = source[i];
|
|
249
|
+
if (ch === "{") depth++;
|
|
250
|
+
else if (ch === "}") {
|
|
251
|
+
depth--;
|
|
252
|
+
if (depth === 0) {
|
|
253
|
+
closeBrace = i;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (closeBrace === -1) return null;
|
|
259
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
260
|
+
const titleRe = /(^|[\s,{])(title\s*:\s*)(['"`])((?:\\.|(?!\3).)*)\3/;
|
|
261
|
+
const match = body.match(titleRe);
|
|
262
|
+
if (match) {
|
|
263
|
+
const newBody = body.replace(titleRe, `${match[1]}${match[2]}${newLiteral}`);
|
|
264
|
+
return source.slice(0, openBrace + 1) + newBody + source.slice(closeBrace);
|
|
265
|
+
}
|
|
266
|
+
const firstIndentMatch = body.match(/\n([ \t]+)\S/);
|
|
267
|
+
const insertion = `\n${firstIndentMatch ? firstIndentMatch[1] : " "}title: ${newLiteral}${body.replace(/^\s*\n?/, "").trim().length > 0 ? "," : ""}`;
|
|
268
|
+
return source.slice(0, openBrace + 1) + insertion + body + source.slice(closeBrace);
|
|
269
|
+
}
|
|
270
|
+
const exportDefaultIdx = source.search(/export\s+default\b/);
|
|
271
|
+
if (exportDefaultIdx === -1) return null;
|
|
272
|
+
const insertion = `export const meta: PageMeta = { title: ${newLiteral} };\n\n`;
|
|
273
|
+
return source.slice(0, exportDefaultIdx) + insertion + source.slice(exportDefaultIdx);
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
16
276
|
//#region src/editing/babel-walk.ts
|
|
17
277
|
const SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
18
278
|
"loc",
|
|
@@ -1452,12 +1712,15 @@ function validateIcon(v) {
|
|
|
1452
1712
|
function makeContext(opts) {
|
|
1453
1713
|
const userCwd = opts.userCwd;
|
|
1454
1714
|
const pagesDir = opts.pagesDir ?? "pages";
|
|
1715
|
+
const emailsDir = opts.emailsDir ?? "emails";
|
|
1455
1716
|
const assetsDir = opts.assetsDir ?? "assets";
|
|
1456
1717
|
const pagesRoot = path.resolve(userCwd, pagesDir);
|
|
1457
1718
|
return {
|
|
1458
1719
|
userCwd,
|
|
1459
1720
|
pagesDir,
|
|
1460
1721
|
pagesRoot,
|
|
1722
|
+
emailsDir,
|
|
1723
|
+
emailsRoot: path.resolve(userCwd, emailsDir),
|
|
1461
1724
|
globalAssetsRoot: path.resolve(userCwd, assetsDir),
|
|
1462
1725
|
manifestPath: foldersManifestPath(pagesRoot),
|
|
1463
1726
|
coreVersion: opts.coreVersion
|
|
@@ -1494,6 +1757,10 @@ function resolvePagePath(userCwd, pagesDir, pageId) {
|
|
|
1494
1757
|
function resolvePageEntryPath(ctx, pageId) {
|
|
1495
1758
|
return resolvePagePath(ctx.userCwd, ctx.pagesDir, pageId);
|
|
1496
1759
|
}
|
|
1760
|
+
/** The `index.tsx` a comment marker lands in: a page entry, or an email entry. */
|
|
1761
|
+
function resolveCommentEntryPath(ctx, id, target) {
|
|
1762
|
+
return resolvePagePath(ctx.userCwd, target === "email" ? ctx.emailsDir : ctx.pagesDir, id);
|
|
1763
|
+
}
|
|
1497
1764
|
//#endregion
|
|
1498
1765
|
//#region src/vite/routes/assets.ts
|
|
1499
1766
|
function registerAssetRoutes(server, ctx) {
|
|
@@ -1774,13 +2041,16 @@ function offsetToLine(source, offset) {
|
|
|
1774
2041
|
}
|
|
1775
2042
|
//#endregion
|
|
1776
2043
|
//#region src/vite/routes/comments.ts
|
|
2044
|
+
function parseTarget(raw) {
|
|
2045
|
+
return raw === "email" ? "email" : "page";
|
|
2046
|
+
}
|
|
1777
2047
|
function registerCommentRoutes(server, ctx) {
|
|
1778
2048
|
server.middlewares.use("/__comments", async (req, res, next) => {
|
|
1779
2049
|
const url = new URL(req.url ?? "/", "http://local");
|
|
1780
2050
|
const method = req.method ?? "GET";
|
|
1781
2051
|
try {
|
|
1782
2052
|
if (method === "GET" && url.pathname === "/") {
|
|
1783
|
-
const file =
|
|
2053
|
+
const file = resolveCommentEntryPath(ctx, url.searchParams.get("pageId") ?? "", parseTarget(url.searchParams.get("target")));
|
|
1784
2054
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1785
2055
|
let source;
|
|
1786
2056
|
try {
|
|
@@ -1794,7 +2064,7 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1794
2064
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
1795
2065
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
1796
2066
|
const body = await readBody(req);
|
|
1797
|
-
const file =
|
|
2067
|
+
const file = resolveCommentEntryPath(ctx, body.pageId ?? "", parseTarget(body.target));
|
|
1798
2068
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1799
2069
|
if (!body.line || body.line < 1) return json(res, 400, { error: "invalid line" });
|
|
1800
2070
|
if (!body.text || typeof body.text !== "string") return json(res, 400, { error: "missing text" });
|
|
@@ -1825,7 +2095,7 @@ function registerCommentRoutes(server, ctx) {
|
|
|
1825
2095
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
1826
2096
|
const id = url.pathname.slice(1);
|
|
1827
2097
|
if (!/^c-[a-f0-9]+$/.test(id)) return json(res, 400, { error: "invalid id" });
|
|
1828
|
-
const file =
|
|
2098
|
+
const file = resolveCommentEntryPath(ctx, url.searchParams.get("pageId") ?? "", parseTarget(url.searchParams.get("target")));
|
|
1829
2099
|
if (!file) return json(res, 400, { error: "invalid pageId" });
|
|
1830
2100
|
let source;
|
|
1831
2101
|
try {
|
|
@@ -2159,6 +2429,417 @@ function registerGfontsRoutes(server) {
|
|
|
2159
2429
|
});
|
|
2160
2430
|
}
|
|
2161
2431
|
//#endregion
|
|
2432
|
+
//#region src/vite/open-pages-plugin.ts
|
|
2433
|
+
const CONFIG_FILE = "open-pages.config.ts";
|
|
2434
|
+
const PAGES_VMOD = "virtual:open-pages/pages";
|
|
2435
|
+
const CONFIG_VMOD = "virtual:open-pages/config";
|
|
2436
|
+
const FOLDERS_VMOD = "virtual:open-pages/folders";
|
|
2437
|
+
const PAGES_CSS_VMOD = "virtual:open-pages/pages.css";
|
|
2438
|
+
const WORKSPACE_SOURCE_DIRS = [
|
|
2439
|
+
"ui",
|
|
2440
|
+
"lib",
|
|
2441
|
+
"hooks"
|
|
2442
|
+
];
|
|
2443
|
+
const ENTRY_GLOB$1 = "*/index.{tsx,jsx,ts,js,html}";
|
|
2444
|
+
const REACT_ENTRY_RE = /^index\.(tsx|jsx|ts|js)$/;
|
|
2445
|
+
async function readFoldersManifest(file) {
|
|
2446
|
+
try {
|
|
2447
|
+
const raw = await fs.readFile(file, "utf8");
|
|
2448
|
+
const parsed = JSON.parse(raw);
|
|
2449
|
+
return {
|
|
2450
|
+
folders: Array.isArray(parsed.folders) ? parsed.folders : [],
|
|
2451
|
+
assignments: parsed.assignments && typeof parsed.assignments === "object" ? parsed.assignments : {}
|
|
2452
|
+
};
|
|
2453
|
+
} catch (err) {
|
|
2454
|
+
if (err.code === "ENOENT") return {
|
|
2455
|
+
folders: [],
|
|
2456
|
+
assignments: {}
|
|
2457
|
+
};
|
|
2458
|
+
throw err;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function resolved$2(id) {
|
|
2462
|
+
return `\0${id}`;
|
|
2463
|
+
}
|
|
2464
|
+
function invalidatePagesModule(server) {
|
|
2465
|
+
const mod = server.moduleGraph.getModuleById(resolved$2(PAGES_VMOD));
|
|
2466
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2467
|
+
server.ws.send({ type: "full-reload" });
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Every `pages/<id>/index.*` entry. A folder with both a React entry and an
|
|
2471
|
+
* `index.html` is a React page — the HTML is then a plain asset of the page.
|
|
2472
|
+
*/
|
|
2473
|
+
async function findPages(userCwd, pagesDir) {
|
|
2474
|
+
const abs = path.resolve(userCwd, pagesDir);
|
|
2475
|
+
if (!existsSync(abs)) return [];
|
|
2476
|
+
const hits = await fg(ENTRY_GLOB$1, {
|
|
2477
|
+
cwd: abs,
|
|
2478
|
+
absolute: true,
|
|
2479
|
+
onlyFiles: true
|
|
2480
|
+
});
|
|
2481
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2482
|
+
for (const file of hits.sort()) {
|
|
2483
|
+
const [id, name] = path.relative(abs, file).split(path.sep);
|
|
2484
|
+
const kind = REACT_ENTRY_RE.test(name) ? "react" : "html";
|
|
2485
|
+
const existing = byId.get(id);
|
|
2486
|
+
if (!existing || existing.kind === "html" && kind === "react") byId.set(id, {
|
|
2487
|
+
id,
|
|
2488
|
+
kind,
|
|
2489
|
+
file
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
2493
|
+
}
|
|
2494
|
+
const META_THEME_RE = /(?:^|[\s,{])theme\s*:\s*['"]([^'"]+)['"]/;
|
|
2495
|
+
const META_CREATED_AT_RE = /(?:^|[\s,{])createdAt\s*:\s*['"]([^'"]+)['"]/;
|
|
2496
|
+
const META_TITLE_RE = /(?:^|[\s,{])title\s*:\s*['"]([^'"]+)['"]/;
|
|
2497
|
+
const HTML_TITLE_RE = /<title[^>]*>([^<]*)<\/title>/i;
|
|
2498
|
+
const EMPTY_META$1 = {
|
|
2499
|
+
title: null,
|
|
2500
|
+
theme: null,
|
|
2501
|
+
createdAt: null
|
|
2502
|
+
};
|
|
2503
|
+
/** The text between the braces of `export const meta = { … }`, or null. */
|
|
2504
|
+
function metaObjectBody(src) {
|
|
2505
|
+
const metaStart = src.search(/export\s+const\s+meta\b/);
|
|
2506
|
+
if (metaStart === -1) return null;
|
|
2507
|
+
const eqIdx = src.indexOf("=", metaStart);
|
|
2508
|
+
if (eqIdx === -1) return null;
|
|
2509
|
+
const openBrace = src.indexOf("{", eqIdx);
|
|
2510
|
+
if (openBrace === -1) return null;
|
|
2511
|
+
let depth = 0;
|
|
2512
|
+
let closeBrace = -1;
|
|
2513
|
+
for (let i = openBrace; i < src.length; i++) {
|
|
2514
|
+
const ch = src[i];
|
|
2515
|
+
if (ch === "{") depth++;
|
|
2516
|
+
else if (ch === "}") {
|
|
2517
|
+
depth--;
|
|
2518
|
+
if (depth === 0) {
|
|
2519
|
+
closeBrace = i;
|
|
2520
|
+
break;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
if (closeBrace === -1) return null;
|
|
2525
|
+
return src.slice(openBrace + 1, closeBrace);
|
|
2526
|
+
}
|
|
2527
|
+
function extractMeta(src) {
|
|
2528
|
+
const body = metaObjectBody(src);
|
|
2529
|
+
if (body === null) return EMPTY_META$1;
|
|
2530
|
+
return {
|
|
2531
|
+
title: body.match(META_TITLE_RE)?.[1] ?? null,
|
|
2532
|
+
theme: body.match(META_THEME_RE)?.[1] ?? null,
|
|
2533
|
+
createdAt: body.match(META_CREATED_AT_RE)?.[1] ?? null
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
function extractHtmlMeta(src) {
|
|
2537
|
+
return {
|
|
2538
|
+
title: (src.match(HTML_TITLE_RE)?.[1]?.trim() ?? null) || null,
|
|
2539
|
+
theme: null,
|
|
2540
|
+
createdAt: null
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
async function readPageMeta(entry) {
|
|
2544
|
+
try {
|
|
2545
|
+
const src = await fs.readFile(entry.file, "utf8");
|
|
2546
|
+
return entry.kind === "html" ? extractHtmlMeta(src) : extractMeta(src);
|
|
2547
|
+
} catch {
|
|
2548
|
+
return EMPTY_META$1;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
function parseCreatedAtMs(iso) {
|
|
2552
|
+
if (!iso) return null;
|
|
2553
|
+
const ms = Date.parse(iso);
|
|
2554
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2555
|
+
}
|
|
2556
|
+
const warnedInvalidPageIds = /* @__PURE__ */ new Set();
|
|
2557
|
+
async function generatePagesModule(entries, isDev) {
|
|
2558
|
+
const scanned = await Promise.all(entries.map(async (entry) => {
|
|
2559
|
+
const importPath = isDev ? `@fs/${normalizePath(entry.file).replace(/^\/+/, "")}` : entry.file;
|
|
2560
|
+
const meta = await readPageMeta(entry);
|
|
2561
|
+
return {
|
|
2562
|
+
...entry,
|
|
2563
|
+
importPath,
|
|
2564
|
+
meta,
|
|
2565
|
+
createdAt: parseCreatedAtMs(meta.createdAt)
|
|
2566
|
+
};
|
|
2567
|
+
}));
|
|
2568
|
+
const valid = scanned.filter((e) => PAGE_ID_RE.test(e.id));
|
|
2569
|
+
const ignored = scanned.filter((e) => !PAGE_ID_RE.test(e.id)).map((e) => e.id);
|
|
2570
|
+
const kinds = {};
|
|
2571
|
+
const themesMap = {};
|
|
2572
|
+
const createdAtMap = {};
|
|
2573
|
+
for (const e of valid) {
|
|
2574
|
+
kinds[e.id] = e.kind;
|
|
2575
|
+
if (e.meta.theme) themesMap[e.id] = e.meta.theme;
|
|
2576
|
+
if (e.createdAt !== null) createdAtMap[e.id] = e.createdAt;
|
|
2577
|
+
}
|
|
2578
|
+
const importTokens = JSON.stringify(Object.fromEntries(valid.filter((e) => e.kind === "react").map((e) => [e.id, 0])));
|
|
2579
|
+
const devRuntime = isDev ? `
|
|
2580
|
+
const pageImportTokens = ${importTokens};
|
|
2581
|
+
if (import.meta.hot) {
|
|
2582
|
+
import.meta.hot.on('open-pages:page-changed', (data) => {
|
|
2583
|
+
const ids = Array.isArray(data?.pageIds) ? data.pageIds : data?.pageId ? [data.pageId] : [];
|
|
2584
|
+
const token = Date.now();
|
|
2585
|
+
for (const id of ids) {
|
|
2586
|
+
if (Object.prototype.hasOwnProperty.call(pageImportTokens, id)) pageImportTokens[id] = token;
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
` : "";
|
|
2591
|
+
const cases = valid.map((e) => {
|
|
2592
|
+
if (e.kind === "html") {
|
|
2593
|
+
const mod = { meta: { title: e.meta.title ?? e.id } };
|
|
2594
|
+
return ` case ${JSON.stringify(e.id)}: return Promise.resolve(${JSON.stringify(mod)});`;
|
|
2595
|
+
}
|
|
2596
|
+
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)})`;
|
|
2597
|
+
return ` case ${JSON.stringify(e.id)}: return ${importExpr};`;
|
|
2598
|
+
}).join("\n");
|
|
2599
|
+
return {
|
|
2600
|
+
code: `// virtual:open-pages/pages — generated
|
|
2601
|
+
export const pageIds = ${JSON.stringify(valid.map((e) => e.id))};
|
|
2602
|
+
export const pageKinds = ${JSON.stringify(kinds)};
|
|
2603
|
+
export const pageThemes = ${JSON.stringify(themesMap)};
|
|
2604
|
+
export const pageCreatedAt = ${JSON.stringify(createdAtMap)};
|
|
2605
|
+
${devRuntime}
|
|
2606
|
+
|
|
2607
|
+
export async function loadPage(id) {
|
|
2608
|
+
switch (id) {
|
|
2609
|
+
${cases}
|
|
2610
|
+
default: throw new Error('Page not found: ' + id);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
`,
|
|
2614
|
+
ignored
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Tailwind for page source. The workspace UI has its own stylesheet rooted
|
|
2619
|
+
* at the core app; pages live in the user's project, so they get a generated
|
|
2620
|
+
* stylesheet whose `@source` points at their directories. Written under
|
|
2621
|
+
* node_modules so Tailwind resolves `tailwindcss` from the workspace.
|
|
2622
|
+
*/
|
|
2623
|
+
function pagesCssFile(userCwd) {
|
|
2624
|
+
return path.join(userCwd, "node_modules", ".open-pages", "pages.css");
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* The workspace's own stylesheet when it has one (the shadcn `tailwind.css`
|
|
2628
|
+
* entry from components.json, default `styles/globals.css`) — it carries the
|
|
2629
|
+
* theme tokens every installed component reads. Falls back to the generated
|
|
2630
|
+
* file for workspaces scaffolded without one.
|
|
2631
|
+
*/
|
|
2632
|
+
function workspaceCssFile(userCwd) {
|
|
2633
|
+
let rel = "styles/globals.css";
|
|
2634
|
+
try {
|
|
2635
|
+
const cfg = JSON.parse(readFileSync(path.join(userCwd, "components.json"), "utf8"));
|
|
2636
|
+
if (typeof cfg.tailwind?.css === "string" && cfg.tailwind.css) rel = cfg.tailwind.css;
|
|
2637
|
+
} catch {}
|
|
2638
|
+
const abs = path.resolve(userCwd, rel);
|
|
2639
|
+
return existsSync(abs) ? abs : null;
|
|
2640
|
+
}
|
|
2641
|
+
const TAILWIND_CSS = normalizePath(createRequire(import.meta.url).resolve("tailwindcss/index.css"));
|
|
2642
|
+
function writePagesCss(userCwd, sources) {
|
|
2643
|
+
const file = pagesCssFile(userCwd);
|
|
2644
|
+
const lines = [`@import ${JSON.stringify(TAILWIND_CSS)} source(none);`];
|
|
2645
|
+
for (const dir of sources) lines.push(`@source ${JSON.stringify(normalizePath(dir))};`);
|
|
2646
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
2647
|
+
writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
|
|
2648
|
+
return file;
|
|
2649
|
+
}
|
|
2650
|
+
function rewriteHtmlUrls(html, pageDir) {
|
|
2651
|
+
const prefix = `/@fs/${normalizePath(pageDir).replace(/^\/+/, "")}/`;
|
|
2652
|
+
return html.replace(/(\s(?:src|href)=)(["'])(?!\/|[a-z][a-z0-9+.-]*:|#|\2)(?:\.\/)?([^"']+)\2/gi, (_m, attr, quote, rel) => `${attr}${quote}${prefix}${rel}${quote}`);
|
|
2653
|
+
}
|
|
2654
|
+
function pagesSignature(entries) {
|
|
2655
|
+
return entries.map((e) => `${e.id}:${e.kind}:${e.file}`).join(",");
|
|
2656
|
+
}
|
|
2657
|
+
function openPagesPlugin(opts) {
|
|
2658
|
+
const { userCwd, config, coreVersion } = opts;
|
|
2659
|
+
const pagesDir = config.pagesDir ?? "pages";
|
|
2660
|
+
const themesDir = config.themesDir ?? "themes";
|
|
2661
|
+
const pagesRoot = path.resolve(userCwd, pagesDir);
|
|
2662
|
+
const themesRoot = path.resolve(userCwd, themesDir);
|
|
2663
|
+
const foldersManifestPath = path.join(pagesRoot, ".folders.json");
|
|
2664
|
+
let isDev = false;
|
|
2665
|
+
let cssFile = "";
|
|
2666
|
+
let generatedSignature = "";
|
|
2667
|
+
const pageIdForEntry = (p) => {
|
|
2668
|
+
const rel = path.relative(pagesRoot, p);
|
|
2669
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
2670
|
+
const parts = rel.split(path.sep);
|
|
2671
|
+
if (parts.length !== 2) return null;
|
|
2672
|
+
if (REACT_ENTRY_RE.test(parts[1])) return {
|
|
2673
|
+
id: parts[0],
|
|
2674
|
+
kind: "react"
|
|
2675
|
+
};
|
|
2676
|
+
if (parts[1] === "index.html") return {
|
|
2677
|
+
id: parts[0],
|
|
2678
|
+
kind: "html"
|
|
2679
|
+
};
|
|
2680
|
+
return null;
|
|
2681
|
+
};
|
|
2682
|
+
let pageChangeTimer = null;
|
|
2683
|
+
const pendingPageChanges = /* @__PURE__ */ new Set();
|
|
2684
|
+
const queuePageChanged = (server, id) => {
|
|
2685
|
+
pendingPageChanges.add(id);
|
|
2686
|
+
if (pageChangeTimer) clearTimeout(pageChangeTimer);
|
|
2687
|
+
pageChangeTimer = setTimeout(() => {
|
|
2688
|
+
pageChangeTimer = null;
|
|
2689
|
+
const mod = server.moduleGraph.getModuleById(resolved$2(PAGES_VMOD));
|
|
2690
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2691
|
+
const pageIds = Array.from(pendingPageChanges);
|
|
2692
|
+
pendingPageChanges.clear();
|
|
2693
|
+
server.ws.send({
|
|
2694
|
+
type: "custom",
|
|
2695
|
+
event: "open-pages:page-changed",
|
|
2696
|
+
data: { pageIds }
|
|
2697
|
+
});
|
|
2698
|
+
}, 100);
|
|
2699
|
+
};
|
|
2700
|
+
return {
|
|
2701
|
+
name: "open-pages",
|
|
2702
|
+
config(_c, env) {
|
|
2703
|
+
isDev = env.command === "serve";
|
|
2704
|
+
cssFile = workspaceCssFile(userCwd) ?? writePagesCss(userCwd, [
|
|
2705
|
+
pagesRoot,
|
|
2706
|
+
themesRoot,
|
|
2707
|
+
...WORKSPACE_SOURCE_DIRS.map((d) => path.join(userCwd, d))
|
|
2708
|
+
]);
|
|
2709
|
+
return { server: { fs: { allow: [userCwd] } } };
|
|
2710
|
+
},
|
|
2711
|
+
resolveId(id) {
|
|
2712
|
+
if (id === PAGES_VMOD) return resolved$2(PAGES_VMOD);
|
|
2713
|
+
if (id === CONFIG_VMOD) return resolved$2(CONFIG_VMOD);
|
|
2714
|
+
if (id === FOLDERS_VMOD) return resolved$2(FOLDERS_VMOD);
|
|
2715
|
+
if (id === PAGES_CSS_VMOD) return cssFile;
|
|
2716
|
+
return null;
|
|
2717
|
+
},
|
|
2718
|
+
async load(id) {
|
|
2719
|
+
if (id === resolved$2(PAGES_VMOD)) {
|
|
2720
|
+
const entries = await findPages(userCwd, pagesDir);
|
|
2721
|
+
generatedSignature = pagesSignature(entries);
|
|
2722
|
+
const { code, ignored } = await generatePagesModule(entries, isDev);
|
|
2723
|
+
for (const pageId of ignored) {
|
|
2724
|
+
if (warnedInvalidPageIds.has(pageId)) continue;
|
|
2725
|
+
warnedInvalidPageIds.add(pageId);
|
|
2726
|
+
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.`);
|
|
2727
|
+
}
|
|
2728
|
+
return code;
|
|
2729
|
+
}
|
|
2730
|
+
if (id === resolved$2(CONFIG_VMOD)) {
|
|
2731
|
+
const userBuild = config.build ?? {};
|
|
2732
|
+
const buildResolved = isDev ? {
|
|
2733
|
+
showPageBrowser: true,
|
|
2734
|
+
showPageUi: true
|
|
2735
|
+
} : {
|
|
2736
|
+
showPageBrowser: userBuild.showPageBrowser ?? true,
|
|
2737
|
+
showPageUi: userBuild.showPageUi ?? true
|
|
2738
|
+
};
|
|
2739
|
+
const resolvedConfig = {
|
|
2740
|
+
...config,
|
|
2741
|
+
build: buildResolved,
|
|
2742
|
+
version: coreVersion
|
|
2743
|
+
};
|
|
2744
|
+
return `export default ${JSON.stringify(resolvedConfig)};\n`;
|
|
2745
|
+
}
|
|
2746
|
+
if (id === resolved$2(FOLDERS_VMOD)) {
|
|
2747
|
+
const manifest = await readFoldersManifest(foldersManifestPath);
|
|
2748
|
+
return `export default ${JSON.stringify(manifest)};\n`;
|
|
2749
|
+
}
|
|
2750
|
+
return null;
|
|
2751
|
+
},
|
|
2752
|
+
handleHotUpdate(ctx) {
|
|
2753
|
+
const entry = pageIdForEntry(ctx.file);
|
|
2754
|
+
if (!entry) return;
|
|
2755
|
+
queuePageChanged(ctx.server, entry.id);
|
|
2756
|
+
return [];
|
|
2757
|
+
},
|
|
2758
|
+
configureServer(server) {
|
|
2759
|
+
let reloadTimer = null;
|
|
2760
|
+
const reload = () => {
|
|
2761
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
2762
|
+
reloadTimer = setTimeout(async () => {
|
|
2763
|
+
reloadTimer = null;
|
|
2764
|
+
try {
|
|
2765
|
+
if (pagesSignature(await findPages(userCwd, pagesDir)) === generatedSignature) return;
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
server.config.logger.error(`[open-pages] failed to rescan pages: ${err}`, { error: err instanceof Error ? err : void 0 });
|
|
2768
|
+
}
|
|
2769
|
+
invalidatePagesModule(server);
|
|
2770
|
+
}, 150);
|
|
2771
|
+
};
|
|
2772
|
+
const isPageDir = (p) => path.dirname(p) === pagesRoot;
|
|
2773
|
+
if (existsSync(pagesRoot)) server.watcher.add(pagesRoot);
|
|
2774
|
+
server.watcher.on("add", (p) => {
|
|
2775
|
+
if (pageIdForEntry(p)) reload();
|
|
2776
|
+
});
|
|
2777
|
+
server.watcher.on("unlink", (p) => {
|
|
2778
|
+
if (pageIdForEntry(p)) reload();
|
|
2779
|
+
});
|
|
2780
|
+
server.watcher.on("addDir", (p) => {
|
|
2781
|
+
if (isPageDir(p)) reload();
|
|
2782
|
+
});
|
|
2783
|
+
server.watcher.on("unlinkDir", (p) => {
|
|
2784
|
+
if (isPageDir(p)) reload();
|
|
2785
|
+
});
|
|
2786
|
+
server.watcher.on("change", (p) => {
|
|
2787
|
+
const entry = pageIdForEntry(p);
|
|
2788
|
+
if (entry?.kind === "html") queuePageChanged(server, entry.id);
|
|
2789
|
+
});
|
|
2790
|
+
let foldersTimer = null;
|
|
2791
|
+
const invalidateFolders = () => {
|
|
2792
|
+
if (foldersTimer) clearTimeout(foldersTimer);
|
|
2793
|
+
foldersTimer = setTimeout(() => {
|
|
2794
|
+
foldersTimer = null;
|
|
2795
|
+
const mod = server.moduleGraph.getModuleById(resolved$2(FOLDERS_VMOD));
|
|
2796
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
2797
|
+
}, 100);
|
|
2798
|
+
};
|
|
2799
|
+
server.watcher.add(foldersManifestPath);
|
|
2800
|
+
server.watcher.on("change", (p) => {
|
|
2801
|
+
if (p === foldersManifestPath) invalidateFolders();
|
|
2802
|
+
});
|
|
2803
|
+
server.watcher.on("add", (p) => {
|
|
2804
|
+
if (p === foldersManifestPath) invalidateFolders();
|
|
2805
|
+
});
|
|
2806
|
+
server.watcher.on("unlink", (p) => {
|
|
2807
|
+
if (p === foldersManifestPath) invalidateFolders();
|
|
2808
|
+
});
|
|
2809
|
+
server.middlewares.use("/__page", async (req, res, next) => {
|
|
2810
|
+
const m = new URL(req.url ?? "/", "http://local").pathname.match(/^\/([^/]+)\/(?:index\.html)?$/);
|
|
2811
|
+
if (!m || (req.method ?? "GET") !== "GET") return next();
|
|
2812
|
+
const pageId = decodeURIComponent(m[1]);
|
|
2813
|
+
if (!PAGE_ID_RE.test(pageId)) return next();
|
|
2814
|
+
const pageDir = path.join(pagesRoot, pageId);
|
|
2815
|
+
const file = path.join(pageDir, "index.html");
|
|
2816
|
+
let raw;
|
|
2817
|
+
try {
|
|
2818
|
+
raw = await fs.readFile(file, "utf8");
|
|
2819
|
+
} catch {
|
|
2820
|
+
return next();
|
|
2821
|
+
}
|
|
2822
|
+
try {
|
|
2823
|
+
const html = await server.transformIndexHtml(`/__page/${pageId}/index.html`, rewriteHtmlUrls(raw, pageDir), req.originalUrl);
|
|
2824
|
+
res.statusCode = 200;
|
|
2825
|
+
res.setHeader("content-type", "text/html");
|
|
2826
|
+
res.end(html);
|
|
2827
|
+
} catch (err) {
|
|
2828
|
+
next(err);
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
async function loadUserConfig(userCwd) {
|
|
2835
|
+
const file = path.join(userCwd, CONFIG_FILE);
|
|
2836
|
+
if (!existsSync(file)) return {};
|
|
2837
|
+
return (await loadConfigFromFile({
|
|
2838
|
+
command: "serve",
|
|
2839
|
+
mode: "development"
|
|
2840
|
+
}, file, userCwd, "silent"))?.config ?? {};
|
|
2841
|
+
}
|
|
2842
|
+
//#endregion
|
|
2162
2843
|
//#region src/vite/routes/pages.ts
|
|
2163
2844
|
function registerPageRoutes(server, ctx) {
|
|
2164
2845
|
server.middlewares.use("/__pages", async (req, res, next) => {
|
|
@@ -2194,8 +2875,7 @@ function registerPageRoutes(server, ctx) {
|
|
|
2194
2875
|
if (method === "PATCH") {
|
|
2195
2876
|
const requestCheck = validateMutationRequest(req, { requireJsonBody: true });
|
|
2196
2877
|
if (!requestCheck.ok) return json(res, requestCheck.status, { error: requestCheck.error });
|
|
2197
|
-
const
|
|
2198
|
-
const name = validatePageName(body.name);
|
|
2878
|
+
const name = validatePageName((await readBody(req)).name);
|
|
2199
2879
|
if (!name) return json(res, 400, { error: "invalid name" });
|
|
2200
2880
|
const entry = resolvePageEntry(ctx.pagesRoot, pageId);
|
|
2201
2881
|
if (!entry) return json(res, 400, { error: "invalid pageId" });
|
|
@@ -2415,6 +3095,7 @@ function parseSelection(raw) {
|
|
|
2415
3095
|
function currentPlugin(opts) {
|
|
2416
3096
|
const userCwd = opts.userCwd;
|
|
2417
3097
|
const pagesDir = opts.pagesDir ?? "pages";
|
|
3098
|
+
const emailsDir = opts.emailsDir ?? "emails";
|
|
2418
3099
|
const outDir = path.join(userCwd, "node_modules", ".open-pages");
|
|
2419
3100
|
const outFile = path.join(outDir, "current.json");
|
|
2420
3101
|
const tmpFile = `${outFile}.tmp`;
|
|
@@ -2434,10 +3115,11 @@ function currentPlugin(opts) {
|
|
|
2434
3115
|
if (typeof raw?.pageId === "string") {
|
|
2435
3116
|
if (!PAGE_ID_RE.test(raw.pageId)) return;
|
|
2436
3117
|
const pageTitle = typeof raw.pageTitle === "string" ? raw.pageTitle : raw.pageId;
|
|
2437
|
-
const view = raw.view === "assets" ? "assets" : "pages";
|
|
2438
|
-
const
|
|
2439
|
-
const
|
|
2440
|
-
|
|
3118
|
+
const view = raw.view === "assets" ? "assets" : raw.view === "emails" ? "emails" : "pages";
|
|
3119
|
+
const dir = view === "emails" ? emailsDir : pagesDir;
|
|
3120
|
+
const entry = view === "emails" || existsSync(path.join(userCwd, pagesDir, raw.pageId, "index.tsx")) ? "index.tsx" : "index.html";
|
|
3121
|
+
const pagePath = path.join(dir, raw.pageId, entry).split(path.sep).join("/");
|
|
3122
|
+
if (cached?.pageId !== raw.pageId || cached?.view !== view) next.selection = null;
|
|
2441
3123
|
next.pageId = raw.pageId;
|
|
2442
3124
|
next.pageTitle = pageTitle;
|
|
2443
3125
|
next.view = view;
|
|
@@ -2842,6 +3524,350 @@ function designPlugin(opts) {
|
|
|
2842
3524
|
};
|
|
2843
3525
|
}
|
|
2844
3526
|
//#endregion
|
|
3527
|
+
//#region src/vite/emails-plugin.ts
|
|
3528
|
+
const EMAILS_VMOD = "virtual:open-pages/emails";
|
|
3529
|
+
const ENTRY_GLOB = "*/index.{tsx,jsx}";
|
|
3530
|
+
const ENTRY_RE = /^index\.(tsx|jsx)$/;
|
|
3531
|
+
const EMAIL_LOC_ATTR_RE = / data-op-loc="\d+:\d+"/g;
|
|
3532
|
+
function resolved$1(id) {
|
|
3533
|
+
return `\0${id}`;
|
|
3534
|
+
}
|
|
3535
|
+
function invalidateEmailsModule(server) {
|
|
3536
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(EMAILS_VMOD));
|
|
3537
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
3538
|
+
server.ws.send({ type: "full-reload" });
|
|
3539
|
+
}
|
|
3540
|
+
async function findEmails(userCwd, emailsDir) {
|
|
3541
|
+
const abs = path.resolve(userCwd, emailsDir);
|
|
3542
|
+
if (!existsSync(abs)) return [];
|
|
3543
|
+
const hits = await fg(ENTRY_GLOB, {
|
|
3544
|
+
cwd: abs,
|
|
3545
|
+
absolute: true,
|
|
3546
|
+
onlyFiles: true
|
|
3547
|
+
});
|
|
3548
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3549
|
+
for (const file of hits.sort()) {
|
|
3550
|
+
const id = path.relative(abs, file).split(path.sep)[0];
|
|
3551
|
+
if (!byId.has(id) && PAGE_ID_RE.test(id)) byId.set(id, {
|
|
3552
|
+
id,
|
|
3553
|
+
file
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3557
|
+
}
|
|
3558
|
+
const EMPTY_META = {
|
|
3559
|
+
title: null,
|
|
3560
|
+
subject: null,
|
|
3561
|
+
description: null,
|
|
3562
|
+
createdAt: null
|
|
3563
|
+
};
|
|
3564
|
+
function metaField(body, key) {
|
|
3565
|
+
return body.match(new RegExp(`(?:^|[\\s,{])${key}\\s*:\\s*['"]([^'"]+)['"]`))?.[1] ?? null;
|
|
3566
|
+
}
|
|
3567
|
+
function extractEmailMeta(src) {
|
|
3568
|
+
const body = metaObjectBody(src);
|
|
3569
|
+
if (body === null) return EMPTY_META;
|
|
3570
|
+
return {
|
|
3571
|
+
title: metaField(body, "title"),
|
|
3572
|
+
subject: metaField(body, "subject"),
|
|
3573
|
+
description: metaField(body, "description"),
|
|
3574
|
+
createdAt: metaField(body, "createdAt")
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
async function generateEmailsModule(entries) {
|
|
3578
|
+
const meta = {};
|
|
3579
|
+
const createdAt = {};
|
|
3580
|
+
for (const entry of entries) {
|
|
3581
|
+
let extracted = EMPTY_META;
|
|
3582
|
+
try {
|
|
3583
|
+
extracted = extractEmailMeta(await fs.readFile(entry.file, "utf8"));
|
|
3584
|
+
} catch {}
|
|
3585
|
+
meta[entry.id] = {
|
|
3586
|
+
title: extracted.title ?? entry.id,
|
|
3587
|
+
subject: extracted.subject,
|
|
3588
|
+
description: extracted.description
|
|
3589
|
+
};
|
|
3590
|
+
const ms = extracted.createdAt ? Date.parse(extracted.createdAt) : NaN;
|
|
3591
|
+
if (Number.isFinite(ms)) createdAt[entry.id] = ms;
|
|
3592
|
+
}
|
|
3593
|
+
return `// virtual:open-pages/emails — generated
|
|
3594
|
+
export const emailIds = ${JSON.stringify(entries.map((e) => e.id))};
|
|
3595
|
+
export const emailMeta = ${JSON.stringify(meta)};
|
|
3596
|
+
export const emailCreatedAt = ${JSON.stringify(createdAt)};
|
|
3597
|
+
`;
|
|
3598
|
+
}
|
|
3599
|
+
function emailsSignature(entries) {
|
|
3600
|
+
return entries.map((e) => `${e.id}:${e.file}`).join(",");
|
|
3601
|
+
}
|
|
3602
|
+
function unresolvedClasses(html) {
|
|
3603
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3604
|
+
for (const match of html.matchAll(/\sclass="([^"]*)"/g)) for (const name of match[1].split(/\s+/)) if (name && !name.includes(":")) seen.add(name);
|
|
3605
|
+
return [...seen].sort();
|
|
3606
|
+
}
|
|
3607
|
+
const runtimes = /* @__PURE__ */ new Map();
|
|
3608
|
+
function workspaceRuntime(userCwd) {
|
|
3609
|
+
let cached = runtimes.get(userCwd);
|
|
3610
|
+
if (!cached) {
|
|
3611
|
+
cached = (async () => {
|
|
3612
|
+
const require = createRequire(path.join(userCwd, "package.json"));
|
|
3613
|
+
let reactPath;
|
|
3614
|
+
let reactEmailPath;
|
|
3615
|
+
try {
|
|
3616
|
+
reactPath = require.resolve("react");
|
|
3617
|
+
reactEmailPath = require.resolve("react-email");
|
|
3618
|
+
} catch {
|
|
3619
|
+
throw new Error("Emails need the `react-email` package in this workspace. Install it with your package manager (for example `npm install react-email`), then reload.");
|
|
3620
|
+
}
|
|
3621
|
+
const react = await import(pathToFileURL(reactPath).href);
|
|
3622
|
+
const reactEmail = await import(pathToFileURL(reactEmailPath).href);
|
|
3623
|
+
const createElement = react.createElement ?? react.default?.createElement;
|
|
3624
|
+
const render = reactEmail.render ?? reactEmail.default?.render;
|
|
3625
|
+
if (!createElement || !render) throw new Error("Could not load `react` and `react-email` from this workspace.");
|
|
3626
|
+
return {
|
|
3627
|
+
createElement,
|
|
3628
|
+
render
|
|
3629
|
+
};
|
|
3630
|
+
})();
|
|
3631
|
+
runtimes.set(userCwd, cached);
|
|
3632
|
+
cached.catch(() => runtimes.delete(userCwd));
|
|
3633
|
+
}
|
|
3634
|
+
return cached;
|
|
3635
|
+
}
|
|
3636
|
+
async function renderEmail(server, userCwd, entry) {
|
|
3637
|
+
const runtime = await workspaceRuntime(userCwd);
|
|
3638
|
+
const mod = await server.ssrLoadModule(entry.file);
|
|
3639
|
+
if (typeof mod.default !== "function") throw new Error(`emails/${entry.id}/index.tsx must default-export a component. Got: ${typeof mod.default}`);
|
|
3640
|
+
const element = runtime.createElement(mod.default);
|
|
3641
|
+
const [html, text] = await Promise.all([runtime.render(element, { pretty: true }), runtime.render(element, { plainText: true })]);
|
|
3642
|
+
return {
|
|
3643
|
+
html,
|
|
3644
|
+
text,
|
|
3645
|
+
title: mod.meta?.title ?? entry.id,
|
|
3646
|
+
subject: mod.meta?.subject ?? null,
|
|
3647
|
+
unresolved: unresolvedClasses(html)
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
function stripLocTags(html) {
|
|
3651
|
+
return html.replace(EMAIL_LOC_ATTR_RE, "");
|
|
3652
|
+
}
|
|
3653
|
+
const EMAIL_THEME_FILE_RE = /(^|\/)components\/email\/email-theme\.(ts|tsx|js|jsx|mjs)$/;
|
|
3654
|
+
const CREATE_CONFIG_RE = /export\s+const\s+createEmailTailwindConfig\s*=/;
|
|
3655
|
+
const EMAIL_THEME_ALIASES = `
|
|
3656
|
+
export const createEmailTailwindConfig = (theme) =>
|
|
3657
|
+
__openPagesAliasEmailTheme(__openPagesCreateEmailTailwindConfig(theme), theme);
|
|
3658
|
+
|
|
3659
|
+
function __openPagesAliasEmailTheme(config, theme) {
|
|
3660
|
+
if (!config || typeof config !== 'object' || !theme || typeof theme !== 'object') return config;
|
|
3661
|
+
const extend = (config.theme ??= {}).extend ??= {};
|
|
3662
|
+
const colors = (extend.colors ??= {});
|
|
3663
|
+
const aliases = {
|
|
3664
|
+
background: theme.colorBackground,
|
|
3665
|
+
'background-muted': theme.colorBackgroundMuted,
|
|
3666
|
+
foreground: theme.colorText,
|
|
3667
|
+
'foreground-muted': theme.colorTextMuted,
|
|
3668
|
+
primary: theme.colorPrimary,
|
|
3669
|
+
'primary-fg': theme.colorPrimaryForeground,
|
|
3670
|
+
border: theme.colorBorder,
|
|
3671
|
+
};
|
|
3672
|
+
for (const [name, value] of Object.entries(aliases)) {
|
|
3673
|
+
if (!(name in colors) && typeof value === 'string') colors[name] = value;
|
|
3674
|
+
}
|
|
3675
|
+
const maxWidth = (extend.maxWidth ??= {});
|
|
3676
|
+
if (!('container' in maxWidth) && typeof theme.containerWidth === 'string') {
|
|
3677
|
+
maxWidth.container = theme.containerWidth;
|
|
3678
|
+
}
|
|
3679
|
+
return config;
|
|
3680
|
+
}
|
|
3681
|
+
`;
|
|
3682
|
+
function isEmailThemeModule(id, userCwd) {
|
|
3683
|
+
const file = id.split(/[?#]/)[0].replace(/\\/g, "/");
|
|
3684
|
+
const root = userCwd.replace(/\\/g, "/");
|
|
3685
|
+
return file.startsWith(`${root}/`) && EMAIL_THEME_FILE_RE.test(file.slice(root.length + 1));
|
|
3686
|
+
}
|
|
3687
|
+
function aliasEmailThemeModule(code) {
|
|
3688
|
+
if (!CREATE_CONFIG_RE.test(code)) return null;
|
|
3689
|
+
return code.replace(CREATE_CONFIG_RE, "const __openPagesCreateEmailTailwindConfig =") + EMAIL_THEME_ALIASES;
|
|
3690
|
+
}
|
|
3691
|
+
function injectBeforeBodyEnd(html, snippet) {
|
|
3692
|
+
const idx = html.lastIndexOf("</body>");
|
|
3693
|
+
return idx === -1 ? html + snippet : html.slice(0, idx) + snippet + html.slice(idx);
|
|
3694
|
+
}
|
|
3695
|
+
function errorDocument(message) {
|
|
3696
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Email error</title></head><body style="margin:0"><pre style="margin:0;padding:16px 20px;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#b91c1c;background:#fef2f2;white-space:pre-wrap">${message.replace(/&/g, "&").replace(/</g, "<")}</pre></body></html>`;
|
|
3697
|
+
}
|
|
3698
|
+
function emailsPlugin(opts) {
|
|
3699
|
+
const { userCwd, config } = opts;
|
|
3700
|
+
const emailsDir = config.emailsDir ?? "emails";
|
|
3701
|
+
const emailsRoot = path.resolve(userCwd, emailsDir);
|
|
3702
|
+
let generatedSignature = "";
|
|
3703
|
+
const emailIdForFile = (p) => {
|
|
3704
|
+
const rel = path.relative(emailsRoot, p);
|
|
3705
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
3706
|
+
const [id] = rel.split(path.sep);
|
|
3707
|
+
return id && PAGE_ID_RE.test(id) ? id : null;
|
|
3708
|
+
};
|
|
3709
|
+
const isEntryFile = (p) => {
|
|
3710
|
+
const parts = path.relative(emailsRoot, p).split(path.sep);
|
|
3711
|
+
return parts.length === 2 && ENTRY_RE.test(parts[1]);
|
|
3712
|
+
};
|
|
3713
|
+
const emailsImporting = (modules) => {
|
|
3714
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3715
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3716
|
+
const queue = [...modules];
|
|
3717
|
+
while (queue.length) {
|
|
3718
|
+
const mod = queue.pop();
|
|
3719
|
+
if (!mod || seen.has(mod)) continue;
|
|
3720
|
+
seen.add(mod);
|
|
3721
|
+
if (mod.file) {
|
|
3722
|
+
const id = emailIdForFile(mod.file);
|
|
3723
|
+
if (id && isEntryFile(mod.file)) ids.add(id);
|
|
3724
|
+
}
|
|
3725
|
+
for (const importer of mod.importers) queue.push(importer);
|
|
3726
|
+
}
|
|
3727
|
+
return ids;
|
|
3728
|
+
};
|
|
3729
|
+
let changeTimer = null;
|
|
3730
|
+
const pendingChanges = /* @__PURE__ */ new Set();
|
|
3731
|
+
const queueEmailChanged = (server, ids) => {
|
|
3732
|
+
for (const id of ids) pendingChanges.add(id);
|
|
3733
|
+
if (changeTimer) clearTimeout(changeTimer);
|
|
3734
|
+
changeTimer = setTimeout(() => {
|
|
3735
|
+
changeTimer = null;
|
|
3736
|
+
const emailIds = Array.from(pendingChanges);
|
|
3737
|
+
pendingChanges.clear();
|
|
3738
|
+
if (emailIds.length === 0) return;
|
|
3739
|
+
server.ws.send({
|
|
3740
|
+
type: "custom",
|
|
3741
|
+
event: "open-pages:email-changed",
|
|
3742
|
+
data: { emailIds }
|
|
3743
|
+
});
|
|
3744
|
+
}, 100);
|
|
3745
|
+
};
|
|
3746
|
+
return {
|
|
3747
|
+
name: "open-pages:emails",
|
|
3748
|
+
resolveId(id) {
|
|
3749
|
+
return id === "virtual:open-pages/emails" ? resolved$1(EMAILS_VMOD) : null;
|
|
3750
|
+
},
|
|
3751
|
+
async load(id) {
|
|
3752
|
+
if (id !== resolved$1("virtual:open-pages/emails")) return null;
|
|
3753
|
+
const entries = await findEmails(userCwd, emailsDir);
|
|
3754
|
+
generatedSignature = emailsSignature(entries);
|
|
3755
|
+
return generateEmailsModule(entries);
|
|
3756
|
+
},
|
|
3757
|
+
transform(code, id) {
|
|
3758
|
+
if (!isEmailThemeModule(id, userCwd)) return null;
|
|
3759
|
+
const next = aliasEmailThemeModule(code);
|
|
3760
|
+
return next === null ? null : {
|
|
3761
|
+
code: next,
|
|
3762
|
+
map: null
|
|
3763
|
+
};
|
|
3764
|
+
},
|
|
3765
|
+
hotUpdate({ file, modules, server }) {
|
|
3766
|
+
if (this.environment.name !== "ssr") return;
|
|
3767
|
+
const ids = emailsImporting(modules);
|
|
3768
|
+
const direct = emailIdForFile(file);
|
|
3769
|
+
if (direct) ids.add(direct);
|
|
3770
|
+
if (ids.size === 0) return;
|
|
3771
|
+
queueEmailChanged(server, ids);
|
|
3772
|
+
return [];
|
|
3773
|
+
},
|
|
3774
|
+
configureServer(server) {
|
|
3775
|
+
let reloadTimer = null;
|
|
3776
|
+
const rescan = () => {
|
|
3777
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
3778
|
+
reloadTimer = setTimeout(async () => {
|
|
3779
|
+
reloadTimer = null;
|
|
3780
|
+
try {
|
|
3781
|
+
if (emailsSignature(await findEmails(userCwd, emailsDir)) === generatedSignature) return;
|
|
3782
|
+
} catch (err) {
|
|
3783
|
+
server.config.logger.error(`[open-pages] failed to rescan emails: ${err}`, { error: err instanceof Error ? err : void 0 });
|
|
3784
|
+
}
|
|
3785
|
+
invalidateEmailsModule(server);
|
|
3786
|
+
}, 150);
|
|
3787
|
+
};
|
|
3788
|
+
if (existsSync(emailsRoot)) server.watcher.add(emailsRoot);
|
|
3789
|
+
const onEntryEvent = (p) => {
|
|
3790
|
+
if (emailIdForFile(p) && isEntryFile(p)) rescan();
|
|
3791
|
+
};
|
|
3792
|
+
server.watcher.on("add", onEntryEvent);
|
|
3793
|
+
server.watcher.on("unlink", onEntryEvent);
|
|
3794
|
+
server.watcher.on("addDir", (p) => {
|
|
3795
|
+
if (path.dirname(p) === emailsRoot) rescan();
|
|
3796
|
+
});
|
|
3797
|
+
server.watcher.on("unlinkDir", (p) => {
|
|
3798
|
+
if (path.dirname(p) === emailsRoot) rescan();
|
|
3799
|
+
});
|
|
3800
|
+
server.watcher.on("change", (p) => {
|
|
3801
|
+
if (!emailIdForFile(p) || !isEntryFile(p)) return;
|
|
3802
|
+
const mod = server.moduleGraph.getModuleById(resolved$1(EMAILS_VMOD));
|
|
3803
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
3804
|
+
});
|
|
3805
|
+
server.middlewares.use("/__email", async (req, res, next) => {
|
|
3806
|
+
const url = new URL(req.url ?? "/", "http://local");
|
|
3807
|
+
const m = url.pathname.match(/^\/([^/]+)\/(index\.html|index\.txt|index\.json)$/);
|
|
3808
|
+
if (!m || (req.method ?? "GET") !== "GET") return next();
|
|
3809
|
+
const emailId = decodeURIComponent(m[1]);
|
|
3810
|
+
if (!PAGE_ID_RE.test(emailId)) return next();
|
|
3811
|
+
const entry = (await findEmails(userCwd, emailsDir)).find((e) => e.id === emailId);
|
|
3812
|
+
if (!entry) {
|
|
3813
|
+
res.statusCode = 404;
|
|
3814
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
3815
|
+
res.end(`Email not found: ${emailId}`);
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
const wantText = m[2] === "index.txt";
|
|
3819
|
+
const wantJson = m[2] === "index.json";
|
|
3820
|
+
const raw = url.searchParams.has("raw");
|
|
3821
|
+
try {
|
|
3822
|
+
const rendered = await renderEmail(server, userCwd, entry);
|
|
3823
|
+
if (rendered.unresolved.length > 0) server.config.logger.warn(`[open-pages] email "${emailId}": ${rendered.unresolved.length} Tailwind ${rendered.unresolved.length === 1 ? "class" : "classes"} did not compile to inline styles: ${rendered.unresolved.join(" ")}`);
|
|
3824
|
+
res.statusCode = 200;
|
|
3825
|
+
res.setHeader("cache-control", "no-store");
|
|
3826
|
+
if (wantJson) {
|
|
3827
|
+
res.setHeader("content-type", "application/json");
|
|
3828
|
+
res.end(JSON.stringify({
|
|
3829
|
+
title: rendered.title,
|
|
3830
|
+
subject: rendered.subject,
|
|
3831
|
+
unresolved: rendered.unresolved
|
|
3832
|
+
}));
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
if (wantText) {
|
|
3836
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
3837
|
+
res.end(rendered.text);
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
3841
|
+
if (raw) {
|
|
3842
|
+
res.end(stripLocTags(rendered.html));
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
const script = `<script type="module" src="${server.config.base}frame/email-inspect.ts"><\/script>`;
|
|
3846
|
+
res.end(injectBeforeBodyEnd(rendered.html, script));
|
|
3847
|
+
} catch (err) {
|
|
3848
|
+
const message = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
3849
|
+
server.config.logger.error(`[open-pages] email "${emailId}" failed to render:\n${message}`);
|
|
3850
|
+
res.statusCode = 500;
|
|
3851
|
+
res.setHeader("cache-control", "no-store");
|
|
3852
|
+
if (wantJson) {
|
|
3853
|
+
res.setHeader("content-type", "application/json");
|
|
3854
|
+
res.end(JSON.stringify({ error: message }));
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
if (wantText) {
|
|
3858
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
3859
|
+
res.end(message);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
const script = `<script type="module" src="${server.config.base}frame/email-inspect.ts"><\/script>`;
|
|
3863
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
3864
|
+
res.end(injectBeforeBodyEnd(errorDocument(message), script));
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
//#endregion
|
|
2845
3871
|
//#region src/vite/loc-tags-plugin.ts
|
|
2846
3872
|
const UNTAGGABLE_COMPONENTS = /* @__PURE__ */ new Set([
|
|
2847
3873
|
"Fragment",
|
|
@@ -2891,13 +3917,13 @@ function isPageSourceFile(id, docsRootPosix) {
|
|
|
2891
3917
|
return filePath.slice(docsRootPosix.length + 1).includes("/");
|
|
2892
3918
|
}
|
|
2893
3919
|
function locTagsPlugin(opts) {
|
|
2894
|
-
const
|
|
3920
|
+
const roots = [opts.pagesDir ?? "pages", opts.emailsDir ?? "emails"].map((dir) => path.resolve(opts.userCwd, dir).replace(/\\/g, "/"));
|
|
2895
3921
|
return {
|
|
2896
3922
|
name: "open-pages:loc-tags",
|
|
2897
3923
|
apply: opts.apply ?? "serve",
|
|
2898
3924
|
enforce: "pre",
|
|
2899
3925
|
transform(code, id) {
|
|
2900
|
-
if (!isPageSourceFile(id,
|
|
3926
|
+
if (!roots.some((root) => isPageSourceFile(id, root))) return null;
|
|
2901
3927
|
const next = injectLocTags(code);
|
|
2902
3928
|
if (next === null) return null;
|
|
2903
3929
|
return {
|
|
@@ -3075,12 +4101,38 @@ function themesPlugin(opts) {
|
|
|
3075
4101
|
};
|
|
3076
4102
|
}
|
|
3077
4103
|
//#endregion
|
|
4104
|
+
//#region src/vite/version.ts
|
|
4105
|
+
var version_exports = /* @__PURE__ */ __exportAll({
|
|
4106
|
+
APP_ROOT: () => APP_ROOT,
|
|
4107
|
+
PKG_ROOT: () => PKG_ROOT,
|
|
4108
|
+
readCoreVersion: () => readCoreVersion
|
|
4109
|
+
});
|
|
4110
|
+
function findPackageRoot(fromFile) {
|
|
4111
|
+
let dir = path.dirname(fromFile);
|
|
4112
|
+
while (dir !== path.dirname(dir)) {
|
|
4113
|
+
if (existsSync(path.join(dir, "package.json"))) return dir;
|
|
4114
|
+
dir = path.dirname(dir);
|
|
4115
|
+
}
|
|
4116
|
+
throw new Error(`Could not find package.json walking up from ${fromFile}`);
|
|
4117
|
+
}
|
|
4118
|
+
const PKG_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
4119
|
+
const APP_ROOT = path.join(PKG_ROOT, "src", "app");
|
|
4120
|
+
function readCoreVersion() {
|
|
4121
|
+
try {
|
|
4122
|
+
const raw = readFileSync(path.join(PKG_ROOT, "package.json"), "utf8");
|
|
4123
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
4124
|
+
} catch {
|
|
4125
|
+
return "0.0.0";
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
//#endregion
|
|
3078
4129
|
//#region src/vite/config.ts
|
|
3079
4130
|
const CORE_VERSION = readCoreVersion();
|
|
3080
4131
|
async function createViteConfig(opts) {
|
|
3081
4132
|
const userCwd = path.resolve(opts.userCwd);
|
|
3082
4133
|
const config = opts.config ?? await loadUserConfig(userCwd);
|
|
3083
4134
|
const pagesDir = config.pagesDir ?? "pages";
|
|
4135
|
+
const emailsDir = config.emailsDir ?? "emails";
|
|
3084
4136
|
const themesDir = config.themesDir ?? "themes";
|
|
3085
4137
|
const assetsDir = config.assetsDir ?? "assets";
|
|
3086
4138
|
const pagesAbs = path.resolve(userCwd, pagesDir);
|
|
@@ -3094,7 +4146,8 @@ async function createViteConfig(opts) {
|
|
|
3094
4146
|
plugins: [
|
|
3095
4147
|
locTagsPlugin({
|
|
3096
4148
|
userCwd,
|
|
3097
|
-
pagesDir
|
|
4149
|
+
pagesDir,
|
|
4150
|
+
emailsDir
|
|
3098
4151
|
}),
|
|
3099
4152
|
react(),
|
|
3100
4153
|
tailwindcss(),
|
|
@@ -3107,16 +4160,22 @@ async function createViteConfig(opts) {
|
|
|
3107
4160
|
userCwd,
|
|
3108
4161
|
config
|
|
3109
4162
|
}),
|
|
4163
|
+
emailsPlugin({
|
|
4164
|
+
userCwd,
|
|
4165
|
+
config
|
|
4166
|
+
}),
|
|
3110
4167
|
designPlugin({ userCwd }),
|
|
3111
4168
|
apiPlugin({
|
|
3112
4169
|
userCwd,
|
|
3113
4170
|
pagesDir,
|
|
4171
|
+
emailsDir,
|
|
3114
4172
|
assetsDir,
|
|
3115
4173
|
coreVersion: CORE_VERSION
|
|
3116
4174
|
}),
|
|
3117
4175
|
currentPlugin({
|
|
3118
4176
|
userCwd,
|
|
3119
|
-
pagesDir
|
|
4177
|
+
pagesDir,
|
|
4178
|
+
emailsDir
|
|
3120
4179
|
})
|
|
3121
4180
|
],
|
|
3122
4181
|
resolve: { alias: {
|
|
@@ -3183,4 +4242,4 @@ async function createViteConfig(opts) {
|
|
|
3183
4242
|
};
|
|
3184
4243
|
}
|
|
3185
4244
|
//#endregion
|
|
3186
|
-
export {
|
|
4245
|
+
export { renderEmail as a, extractMeta as c, openPagesPlugin as d, findEmails as i, findPages as l, readCoreVersion as n, stripLocTags as o, version_exports as r, DEV_SUPERVISED_ENV as s, createViteConfig as t, loadUserConfig as u };
|