@autono/open-pages 0.8.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.
Files changed (44) hide show
  1. package/dist/{build-CCoxDxL-.js → build-Cpod-K9R.js} +9 -4
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-D_U_aCQz.js → config-CYLBzMMb.js} +1077 -18
  4. package/dist/{config-p-TWNXbv.d.ts → config-IkwICvOe.d.ts} +2 -1
  5. package/dist/{dev-CTrB263u.js → dev-B7x-G-wk.js} +1 -1
  6. package/dist/{export-enKL9R7x.js → export-BxMksWEc.js} +74 -12
  7. package/dist/index.d.ts +23 -3
  8. package/dist/locale/index.d.ts +1 -1
  9. package/dist/locale/index.js +96 -0
  10. package/dist/{preview-Dmzyd1cf.js → preview-CMv1lGOC.js} +1 -1
  11. package/dist/{types-DDdUFUIS.d.ts → types-B-9WXL93.d.ts} +24 -0
  12. package/dist/vite/index.d.ts +1 -1
  13. package/dist/vite/index.js +1 -1
  14. package/package.json +1 -1
  15. package/skills/apply-comments/SKILL.md +6 -4
  16. package/skills/create-email/SKILL.md +166 -0
  17. package/skills/create-theme/SKILL.md +31 -7
  18. package/skills/create-theme/references/email-theme-from-css.mjs +185 -0
  19. package/skills/create-theme/references/email-theme.md +48 -0
  20. package/skills/current-page/SKILL.md +3 -3
  21. package/src/app/app.tsx +4 -0
  22. package/src/app/components/command/home-command-menu.tsx +19 -2
  23. package/src/app/components/selection-note.tsx +71 -0
  24. package/src/app/components/sidebar/folder-item.tsx +9 -2
  25. package/src/app/components/sidebar/sidebar.tsx +10 -0
  26. package/src/app/frame/email-inspect.ts +17 -0
  27. package/src/app/lib/emails.ts +17 -0
  28. package/src/app/lib/frame.ts +15 -4
  29. package/src/app/lib/page-thumb.tsx +11 -3
  30. package/src/app/lib/sdk.ts +22 -0
  31. package/src/app/routes/email.tsx +424 -0
  32. package/src/app/routes/emails.tsx +94 -0
  33. package/src/app/routes/home-shell.tsx +21 -2
  34. package/src/app/routes/page.tsx +9 -50
  35. package/src/app/virtual.d.ts +10 -0
  36. package/src/locale/en.ts +25 -0
  37. package/src/locale/ja.ts +25 -0
  38. package/src/locale/types.ts +24 -0
  39. package/src/locale/zh-cn.ts +25 -0
  40. package/src/locale/zh-tw.ts +25 -0
  41. package/workspace/components.json +3 -1
  42. package/workspace/styles/globals.css +8 -2
  43. package/dist/open-pages-plugin-C8cFhMB9.js +0 -662
  44. 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 { 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";
3
+ import { createRequire } from "node:module";
5
4
  import path from "node:path";
6
- import { existsSync } from "node:fs";
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 = resolvePageEntryPath(ctx, url.searchParams.get("pageId") ?? "");
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 = resolvePageEntryPath(ctx, body.pageId ?? "");
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 = resolvePageEntryPath(ctx, url.searchParams.get("pageId") ?? "");
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 body = await readBody(req);
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 entry = existsSync(path.join(userCwd, pagesDir, raw.pageId, "index.tsx")) ? "index.tsx" : "index.html";
2439
- const pagePath = path.join(pagesDir, raw.pageId, entry).split(path.sep).join("/");
2440
- if (cached?.pageId !== raw.pageId) next.selection = null;
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, "&amp;").replace(/</g, "&lt;")}</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 pagesRoot = path.resolve(opts.userCwd, opts.pagesDir ?? "pages").replace(/\\/g, "/");
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, pagesRoot)) return null;
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 { DEV_SUPERVISED_ENV as n, createViteConfig as t };
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 };