pressy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,757 @@
1
+ // ../../node_modules/.pnpm/tsup@8.5.1_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ var getFilename = () => fileURLToPath(import.meta.url);
5
+ var getDirname = () => path.dirname(getFilename());
6
+ var __dirname = /* @__PURE__ */ getDirname();
7
+
8
+ // src/config.ts
9
+ function defineConfig(config) {
10
+ return {
11
+ pagination: {
12
+ defaultMode: "scroll"
13
+ },
14
+ pwa: {
15
+ enabled: true,
16
+ themeColor: "#ffffff",
17
+ backgroundColor: "#ffffff",
18
+ display: "standalone"
19
+ },
20
+ outDir: "dist",
21
+ contentDir: "content",
22
+ ...config
23
+ };
24
+ }
25
+
26
+ // src/vite/plugin.ts
27
+ import { resolve, join, relative, dirname } from "path";
28
+ import { existsSync, readFileSync } from "fs";
29
+ import { createHash } from "crypto";
30
+ import { createRequire } from "module";
31
+ import fastGlob from "fast-glob";
32
+ import yaml from "yaml";
33
+ import matter2 from "gray-matter";
34
+
35
+ // src/mdx/processor.ts
36
+ import { compile } from "@mdx-js/mdx";
37
+ import remarkGfm from "remark-gfm";
38
+ import rehypeSlug from "rehype-slug";
39
+ import matter from "gray-matter";
40
+ async function compileMDX(source, filePath) {
41
+ const { content, data: frontmatter } = matter(source);
42
+ const result = await compile(content, {
43
+ jsxImportSource: "preact",
44
+ remarkPlugins: [remarkGfm],
45
+ rehypePlugins: [rehypeSlug],
46
+ development: false
47
+ });
48
+ const compiled = String(result);
49
+ const code = `${compiled}
50
+ export const frontmatter = ${JSON.stringify(frontmatter)};
51
+ `;
52
+ return {
53
+ code,
54
+ map: null,
55
+ frontmatter
56
+ };
57
+ }
58
+
59
+ // src/runtime/offline-page.ts
60
+ function generateOfflinePage(siteTitle) {
61
+ return `<!DOCTYPE html>
62
+ <html lang="en">
63
+ <head>
64
+ <meta charset="UTF-8">
65
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
66
+ <title>Offline | ${siteTitle}</title>
67
+ <style>
68
+ * { box-sizing: border-box; margin: 0; padding: 0; }
69
+
70
+ body {
71
+ font-family: Georgia, 'Times New Roman', serif;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ min-height: 100vh;
76
+ padding: 2rem;
77
+ background: #fafafa;
78
+ color: #1a1a1a;
79
+ }
80
+
81
+ .offline-container {
82
+ text-align: center;
83
+ max-width: 420px;
84
+ }
85
+
86
+ .offline-icon {
87
+ width: 64px;
88
+ height: 64px;
89
+ margin: 0 auto 1.5rem;
90
+ color: #999;
91
+ }
92
+
93
+ h1 {
94
+ font-size: 1.5rem;
95
+ margin-bottom: 0.75rem;
96
+ }
97
+
98
+ p {
99
+ color: #666;
100
+ line-height: 1.6;
101
+ margin-bottom: 1.5rem;
102
+ }
103
+
104
+ button {
105
+ font-family: inherit;
106
+ font-size: 1rem;
107
+ padding: 0.75rem 1.5rem;
108
+ background: #1a1a1a;
109
+ color: #fff;
110
+ border: none;
111
+ border-radius: 0.5rem;
112
+ cursor: pointer;
113
+ transition: background 0.15s;
114
+ }
115
+
116
+ button:hover { background: #333; }
117
+
118
+ @media (prefers-color-scheme: dark) {
119
+ body { background: #1a1a1a; color: #e5e5e5; }
120
+ p { color: #999; }
121
+ .offline-icon { color: #666; }
122
+ button { background: #e5e5e5; color: #1a1a1a; }
123
+ button:hover { background: #ccc; }
124
+ }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div class="offline-container">
129
+ <svg class="offline-icon" viewBox="0 0 24 24" fill="currentColor">
130
+ <path d="M23.64 7c-.45-.34-4.93-4-11.64-4-1.5 0-2.89.19-4.15.48L18.18 13.8 23.64 7zM3.41 1.31L2 2.72l2.05 2.05C1.91 5.76.59 6.82.36 7L12 21.5l3.91-4.87 3.32 3.32 1.41-1.41L3.41 1.31z"/>
131
+ </svg>
132
+ <h1>You\u2019re offline</h1>
133
+ <p>This page isn\u2019t available right now. If you\u2019ve previously downloaded this book for offline reading, try navigating to it directly.</p>
134
+ <button onclick="window.location.reload()">Try again</button>
135
+ </div>
136
+ </body>
137
+ </html>`;
138
+ }
139
+
140
+ // src/vite/plugin.ts
141
+ var _require = createRequire(import.meta.url);
142
+ var VIRTUAL_MODULE_ID = "virtual:pressy";
143
+ var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
144
+ var VIRTUAL_ENTRY_ID = "virtual:pressy-entry";
145
+ var RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID;
146
+ var VIRTUAL_ROUTE_PREFIX = "virtual:pressy-route:";
147
+ var RESOLVED_ROUTE_PREFIX = "\0" + VIRTUAL_ROUTE_PREFIX;
148
+ var VIRTUAL_CHAPTER_MAP_PREFIX = "virtual:pressy-chapter-map:";
149
+ var RESOLVED_CHAPTER_MAP_PREFIX = "\0" + VIRTUAL_CHAPTER_MAP_PREFIX;
150
+ function pressyPlugin(config) {
151
+ let root;
152
+ let contentDir;
153
+ let manifest = { books: [], articles: [] };
154
+ let routes = [];
155
+ const pwaEnabled = config.pwa?.enabled !== false;
156
+ const pwaConfig = {
157
+ themeColor: config.pwa?.themeColor || "#ffffff",
158
+ backgroundColor: config.pwa?.backgroundColor || "#ffffff",
159
+ display: config.pwa?.display || "standalone",
160
+ shortName: config.pwa?.shortName || config.site.title
161
+ };
162
+ const contentDiscovery = {
163
+ async discoverContent() {
164
+ const books = await this.discoverBooks();
165
+ const articles = await this.discoverArticles();
166
+ return { books, articles };
167
+ },
168
+ async discoverBooks() {
169
+ const booksDir = join(contentDir, "books");
170
+ if (!existsSync(booksDir)) return [];
171
+ const bookDirs = await fastGlob("*", {
172
+ cwd: booksDir,
173
+ onlyDirectories: true
174
+ });
175
+ const books = [];
176
+ for (const bookSlug of bookDirs) {
177
+ const bookPath = join(booksDir, bookSlug);
178
+ const metadataPath = join(bookPath, "_book.yaml");
179
+ if (!existsSync(metadataPath)) continue;
180
+ const metadataContent = readFileSync(metadataPath, "utf-8");
181
+ const metadata = yaml.parse(metadataContent);
182
+ const chapterFiles = await fastGlob("*.mdx", {
183
+ cwd: bookPath,
184
+ ignore: ["_*.mdx"]
185
+ });
186
+ const chapters = chapterFiles.map((file) => {
187
+ const match = file.match(/^(\d+)-(.+)\.mdx$/);
188
+ if (!match) return null;
189
+ const order = parseInt(match[1], 10);
190
+ const slug = match[2];
191
+ const filePath = join(bookPath, file);
192
+ const content = readFileSync(filePath, "utf-8");
193
+ const { data } = matter2(content);
194
+ return {
195
+ slug,
196
+ title: data.title || slug.replace(/-/g, " "),
197
+ order,
198
+ filePath,
199
+ wordCount: content.split(/\s+/).length,
200
+ readingTime: Math.ceil(content.split(/\s+/).length / 200)
201
+ };
202
+ }).filter((c) => c !== null).sort((a, b) => a.order - b.order);
203
+ const coverPath = existsSync(join(bookPath, "cover.jpg")) ? join(bookPath, "cover.jpg") : existsSync(join(bookPath, "cover.png")) ? join(bookPath, "cover.png") : void 0;
204
+ books.push({
205
+ slug: bookSlug,
206
+ metadata,
207
+ chapters,
208
+ basePath: bookPath,
209
+ coverPath
210
+ });
211
+ }
212
+ return books;
213
+ },
214
+ async discoverArticles() {
215
+ const articlesDir = join(contentDir, "articles");
216
+ if (!existsSync(articlesDir)) return [];
217
+ const articleDirs = await fastGlob("*", {
218
+ cwd: articlesDir,
219
+ onlyDirectories: true
220
+ });
221
+ const articles = [];
222
+ for (const articleSlug of articleDirs) {
223
+ const articlePath = join(articlesDir, articleSlug);
224
+ const metadataPath = join(articlePath, "_article.yaml");
225
+ const indexPath = join(articlePath, "index.mdx");
226
+ if (!existsSync(indexPath)) continue;
227
+ let metadata;
228
+ if (existsSync(metadataPath)) {
229
+ metadata = yaml.parse(readFileSync(metadataPath, "utf-8"));
230
+ } else {
231
+ const content2 = readFileSync(indexPath, "utf-8");
232
+ const { data } = matter2(content2);
233
+ metadata = data;
234
+ }
235
+ const content = readFileSync(indexPath, "utf-8");
236
+ articles.push({
237
+ slug: articleSlug,
238
+ metadata,
239
+ filePath: indexPath,
240
+ basePath: articlePath,
241
+ wordCount: content.split(/\s+/).length,
242
+ readingTime: Math.ceil(content.split(/\s+/).length / 200)
243
+ });
244
+ }
245
+ return articles;
246
+ }
247
+ };
248
+ function generateRoutes(manifest2) {
249
+ const routes2 = [
250
+ { path: "/", type: "home" },
251
+ { path: "/books", type: "books" },
252
+ { path: "/articles", type: "articles" }
253
+ ];
254
+ for (const book of manifest2.books) {
255
+ routes2.push({
256
+ path: `/books/${book.slug}`,
257
+ type: "book",
258
+ content: book
259
+ });
260
+ for (const chapter of book.chapters) {
261
+ routes2.push({
262
+ path: `/books/${book.slug}/${chapter.slug}`,
263
+ type: "chapter",
264
+ content: chapter,
265
+ book
266
+ });
267
+ }
268
+ }
269
+ for (const article of manifest2.articles) {
270
+ routes2.push({
271
+ path: `/articles/${article.slug}`,
272
+ type: "article",
273
+ content: article
274
+ });
275
+ }
276
+ return routes2;
277
+ }
278
+ function escapeHtmlAttr(str) {
279
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
280
+ }
281
+ function generateHTML(route) {
282
+ const title = escapeHtmlAttr(getRouteTitle(route, config));
283
+ const description = escapeHtmlAttr(getRouteDescription(route, config));
284
+ let contentImport = "";
285
+ let contentArg = "";
286
+ let chapterMapImport = "";
287
+ let chapterMapArg = "";
288
+ if (route.type === "chapter") {
289
+ const chapter = route.content;
290
+ const importPath = "/" + relative(root, chapter.filePath).split("\\").join("/");
291
+ contentImport = `import Content from '${importPath}';
292
+ `;
293
+ contentArg = ", Content";
294
+ if (route.book) {
295
+ chapterMapImport = `import { chapterMap, chapterOrder } from '${VIRTUAL_CHAPTER_MAP_PREFIX}${route.book.slug}';
296
+ `;
297
+ chapterMapArg = ", { chapterMap, chapterOrder }";
298
+ }
299
+ } else if (route.type === "article") {
300
+ const article = route.content;
301
+ const importPath = "/" + relative(root, article.filePath).split("\\").join("/");
302
+ contentImport = `import Content from '${importPath}';
303
+ `;
304
+ contentArg = ", Content";
305
+ }
306
+ const dataJson = JSON.stringify({
307
+ route: route.path,
308
+ routeType: route.type,
309
+ manifest,
310
+ pagination: config.pagination
311
+ });
312
+ const pwaTags = pwaEnabled ? `
313
+ <link rel="manifest" href="/manifest.webmanifest">
314
+ <meta name="theme-color" content="${pwaConfig.themeColor}">
315
+ <meta name="apple-mobile-web-app-capable" content="yes">
316
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
317
+ <meta name="apple-mobile-web-app-title" content="${pwaConfig.shortName}">
318
+ <link rel="apple-touch-icon" href="/icon-192.png">` : `
319
+ <meta name="theme-color" content="${pwaConfig.themeColor}">`;
320
+ return `<!DOCTYPE html>
321
+ <html lang="${config.site.language || "en"}">
322
+ <head>
323
+ <meta charset="UTF-8">
324
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
325
+ <title>${title}</title>
326
+ <meta name="description" content="${description}">
327
+ <link rel="stylesheet" href="/@pressy-pub/typography/prose.css">
328
+ <link rel="stylesheet" href="/@pressy-pub/typography/fluid.css">
329
+ <link rel="stylesheet" href="/@pressy-pub/typography/themes/light.css">
330
+ <link rel="stylesheet" href="/@pressy-pub/typography/themes/dark.css">
331
+ <link rel="stylesheet" href="/@pressy-pub/typography/themes/sepia.css">${pwaTags}
332
+ </head>
333
+ <body>
334
+ <div id="app"></div>
335
+ <script type="module">
336
+ import { hydrate } from '/@pressy-pub/client';
337
+ ${contentImport}${chapterMapImport}const data = ${dataJson};
338
+ hydrate(data${contentArg}${chapterMapArg});
339
+ </script>
340
+ </body>
341
+ </html>`;
342
+ }
343
+ function getRouteTitle(route, config2) {
344
+ switch (route.type) {
345
+ case "home":
346
+ return config2.site.title;
347
+ case "books":
348
+ return `Books | ${config2.site.title}`;
349
+ case "articles":
350
+ return `Articles | ${config2.site.title}`;
351
+ case "book":
352
+ return `${route.content.metadata.title} | ${config2.site.title}`;
353
+ case "chapter":
354
+ return `${route.content.title} | ${route.book?.metadata.title} | ${config2.site.title}`;
355
+ case "article":
356
+ return `${route.content.metadata.title} | ${config2.site.title}`;
357
+ default:
358
+ return config2.site.title;
359
+ }
360
+ }
361
+ function getRouteDescription(route, config2) {
362
+ switch (route.type) {
363
+ case "book":
364
+ return route.content.metadata.description || "";
365
+ case "article":
366
+ return route.content.metadata.description || "";
367
+ default:
368
+ return config2.site.description || "";
369
+ }
370
+ }
371
+ function generatePlaceholderIcon(size) {
372
+ const initial = config.site.title.charAt(0).toUpperCase();
373
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
374
+ <rect width="${size}" height="${size}" fill="${pwaConfig.themeColor === "#ffffff" ? "#1a1a1a" : pwaConfig.themeColor}" rx="${Math.round(size * 0.15)}"/>
375
+ <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-family="Georgia, serif" font-size="${Math.round(size * 0.45)}" fill="#ffffff">${initial}</text>
376
+ </svg>`;
377
+ }
378
+ return [
379
+ {
380
+ name: "pressy:config",
381
+ enforce: "pre",
382
+ async config(_, { command }) {
383
+ if (command === "build") {
384
+ const tempContentDir = resolve(config.contentDir || "content");
385
+ contentDir = tempContentDir;
386
+ manifest = await contentDiscovery.discoverContent();
387
+ routes = generateRoutes(manifest);
388
+ const input = {
389
+ "pressy-entry": VIRTUAL_ENTRY_ID
390
+ };
391
+ for (const route of routes) {
392
+ const name = route.path === "/" ? "index" : route.path.slice(1).replace(/\//g, "-");
393
+ input[name] = VIRTUAL_ROUTE_PREFIX + route.path;
394
+ }
395
+ return {
396
+ appType: "custom",
397
+ resolve: {
398
+ alias: {
399
+ "preact": dirname(_require.resolve("preact/package.json")),
400
+ "preact/jsx-runtime": dirname(_require.resolve("preact/package.json")) + "/jsx-runtime"
401
+ }
402
+ },
403
+ build: {
404
+ rollupOptions: {
405
+ input,
406
+ preserveEntrySignatures: "exports-only",
407
+ external: ["@pressy-pub/shopify"]
408
+ }
409
+ }
410
+ };
411
+ }
412
+ return { appType: "custom" };
413
+ },
414
+ configResolved(resolvedConfig) {
415
+ root = resolvedConfig.root;
416
+ contentDir = resolve(root, config.contentDir || "content");
417
+ }
418
+ },
419
+ {
420
+ name: "pressy:content",
421
+ async buildStart() {
422
+ manifest = await contentDiscovery.discoverContent();
423
+ routes = generateRoutes(manifest);
424
+ },
425
+ options(opts) {
426
+ return opts;
427
+ },
428
+ resolveId(id) {
429
+ if (id === VIRTUAL_MODULE_ID) {
430
+ return RESOLVED_VIRTUAL_MODULE_ID;
431
+ }
432
+ if (id === VIRTUAL_ENTRY_ID) {
433
+ return RESOLVED_VIRTUAL_ENTRY_ID;
434
+ }
435
+ if (id.startsWith(VIRTUAL_ROUTE_PREFIX)) {
436
+ return "\0" + id;
437
+ }
438
+ if (id.startsWith(VIRTUAL_CHAPTER_MAP_PREFIX)) {
439
+ return "\0" + id;
440
+ }
441
+ if (id === "@pressy-pub/shopify") {
442
+ return { id, external: true };
443
+ }
444
+ if (id === "/@pressy-pub/client") {
445
+ return resolve(__dirname, "../runtime/client.js");
446
+ }
447
+ if (id.startsWith("/@pressy-pub/typography/")) {
448
+ const cssFile = id.replace("/@pressy-pub/typography/", "");
449
+ return { id: `@pressy-pub/typography/${cssFile}`, external: false };
450
+ }
451
+ },
452
+ load(id) {
453
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) {
454
+ return `export const manifest = ${JSON.stringify(manifest)};
455
+ export const routes = ${JSON.stringify(routes)};
456
+ export const config = ${JSON.stringify(config)};`;
457
+ }
458
+ if (id === RESOLVED_VIRTUAL_ENTRY_ID) {
459
+ return [
460
+ `import '@pressy-pub/typography/prose.css'`,
461
+ `import '@pressy-pub/typography/fluid.css'`,
462
+ `import '@pressy-pub/typography/themes/light.css'`,
463
+ `import '@pressy-pub/typography/themes/dark.css'`,
464
+ `import '@pressy-pub/typography/themes/sepia.css'`,
465
+ `export { hydrate } from '/@pressy-pub/client'`
466
+ ].join("\n");
467
+ }
468
+ if (id.startsWith(RESOLVED_ROUTE_PREFIX)) {
469
+ const routePath = id.slice(RESOLVED_ROUTE_PREFIX.length);
470
+ const route = routes.find((r) => r.path === routePath);
471
+ if (!route) return null;
472
+ const dataJson = JSON.stringify({
473
+ route: route.path,
474
+ routeType: route.type,
475
+ manifest,
476
+ pagination: config.pagination
477
+ });
478
+ let contentImport = "";
479
+ let contentArg = "";
480
+ let chapterMapImport = "";
481
+ let chapterMapArg = "";
482
+ if (route.type === "chapter") {
483
+ const chapter = route.content;
484
+ const importPath = chapter.filePath;
485
+ contentImport = `import Content from '${importPath}';
486
+ `;
487
+ contentArg = ", Content";
488
+ if (route.book) {
489
+ chapterMapImport = `import { chapterMap, chapterOrder } from '${VIRTUAL_CHAPTER_MAP_PREFIX}${route.book.slug}';
490
+ `;
491
+ chapterMapArg = ", { chapterMap, chapterOrder }";
492
+ }
493
+ } else if (route.type === "article") {
494
+ const article = route.content;
495
+ const importPath = article.filePath;
496
+ contentImport = `import Content from '${importPath}';
497
+ `;
498
+ contentArg = ", Content";
499
+ }
500
+ return [
501
+ `import { hydrate } from '/@pressy-pub/client';`,
502
+ contentImport,
503
+ chapterMapImport,
504
+ `const data = ${dataJson};`,
505
+ `hydrate(data${contentArg}${chapterMapArg});`
506
+ ].join("\n");
507
+ }
508
+ if (id.startsWith(RESOLVED_CHAPTER_MAP_PREFIX)) {
509
+ const bookSlug = id.slice(RESOLVED_CHAPTER_MAP_PREFIX.length);
510
+ const book = manifest.books.find((b) => b.slug === bookSlug);
511
+ if (!book) return null;
512
+ const entries = book.chapters.map((ch) => {
513
+ return ` ${JSON.stringify(ch.slug)}: () => import(${JSON.stringify(ch.filePath)})`;
514
+ });
515
+ return [
516
+ `export const chapterMap = {`,
517
+ entries.join(",\n"),
518
+ `};`,
519
+ `export const chapterOrder = ${JSON.stringify(book.chapters.map((ch) => ch.slug))};`
520
+ ].join("\n");
521
+ }
522
+ }
523
+ },
524
+ {
525
+ name: "pressy:mdx",
526
+ async transform(code, id) {
527
+ if (!id.endsWith(".mdx")) return null;
528
+ const result = await compileMDX(code, id);
529
+ return {
530
+ code: result.code,
531
+ map: result.map
532
+ };
533
+ }
534
+ },
535
+ {
536
+ name: "pressy:html",
537
+ configureServer(server) {
538
+ server.middlewares.use(async (req, res, next) => {
539
+ const url = req.url || "/";
540
+ if (url === "/favicon.ico") {
541
+ res.statusCode = 204;
542
+ res.end();
543
+ return;
544
+ }
545
+ if (url === "/sw.js") {
546
+ res.setHeader("Content-Type", "application/javascript");
547
+ res.setHeader("Service-Worker-Allowed", "/");
548
+ res.end([
549
+ "// Dev-mode service worker \u2014 no caching, passes everything through",
550
+ 'self.addEventListener("install", () => self.skipWaiting());',
551
+ 'self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));',
552
+ 'self.addEventListener("message", (e) => {',
553
+ ' if (e.data?.type === "SKIP_WAITING") self.skipWaiting();',
554
+ "});"
555
+ ].join("\n"));
556
+ return;
557
+ }
558
+ if (url === "/manifest.webmanifest") {
559
+ res.setHeader("Content-Type", "application/manifest+json");
560
+ res.end(JSON.stringify({
561
+ name: config.site.title,
562
+ short_name: pwaConfig.shortName,
563
+ description: config.site.description,
564
+ start_url: "./",
565
+ display: pwaConfig.display,
566
+ background_color: pwaConfig.backgroundColor,
567
+ theme_color: pwaConfig.themeColor,
568
+ icons: [
569
+ { src: "./icon-192.png", sizes: "192x192", type: "image/png" },
570
+ { src: "./icon-512.png", sizes: "512x512", type: "image/png" }
571
+ ]
572
+ }, null, 2));
573
+ return;
574
+ }
575
+ if (url === "/icon-192.png" || url === "/icon-512.png") {
576
+ const size = url.includes("192") ? 192 : 512;
577
+ res.setHeader("Content-Type", "image/svg+xml");
578
+ res.end(generatePlaceholderIcon(size));
579
+ return;
580
+ }
581
+ if (url === "/offline.html") {
582
+ res.setHeader("Content-Type", "text/html");
583
+ res.end(generateOfflinePage(config.site.title));
584
+ return;
585
+ }
586
+ if (url.startsWith("/@pressy-pub/")) {
587
+ const bareImport = url.slice(1);
588
+ try {
589
+ const result = await server.pluginContainer.resolveId(bareImport);
590
+ if (result && !result.external) {
591
+ req.url = "/@fs" + result.id;
592
+ }
593
+ } catch {
594
+ }
595
+ next();
596
+ return;
597
+ }
598
+ const pathname = url.split("?")[0];
599
+ const route = routes.find((r) => r.path === pathname);
600
+ if (route) {
601
+ const html = generateHTML(route);
602
+ const transformed = await server.transformIndexHtml(url, html);
603
+ res.setHeader("Content-Type", "text/html");
604
+ res.end(transformed);
605
+ return;
606
+ }
607
+ next();
608
+ });
609
+ }
610
+ },
611
+ {
612
+ name: "pressy:build",
613
+ apply: "build",
614
+ async generateBundle(_, bundle) {
615
+ const cssFiles = [];
616
+ const routeChunks = /* @__PURE__ */ new Map();
617
+ for (const [fileName, chunk] of Object.entries(bundle)) {
618
+ if (fileName.endsWith(".css")) {
619
+ cssFiles.push(fileName);
620
+ }
621
+ if (chunk.type === "chunk" && chunk.isEntry && chunk.name) {
622
+ routeChunks.set(chunk.name, fileName);
623
+ }
624
+ }
625
+ for (const route of routes) {
626
+ const title = escapeHtmlAttr(getRouteTitle(route, config));
627
+ const description = escapeHtmlAttr(getRouteDescription(route, config));
628
+ const routeName = route.path === "/" ? "index" : route.path.slice(1).replace(/\//g, "-");
629
+ const routeJsFile = routeChunks.get(routeName);
630
+ const depth = route.path === "/" ? 0 : route.path.slice(1).split("/").length;
631
+ const assetPrefix = depth === 0 ? "./" : "../".repeat(depth);
632
+ const cssLinks = cssFiles.map((f) => ` <link rel="stylesheet" href="${assetPrefix}${f}">`).join("\n");
633
+ const buildPwaTags = pwaEnabled ? `
634
+ <link rel="manifest" href="${assetPrefix}manifest.webmanifest">
635
+ <meta name="theme-color" content="${pwaConfig.themeColor}">
636
+ <meta name="apple-mobile-web-app-capable" content="yes">
637
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
638
+ <meta name="apple-mobile-web-app-title" content="${pwaConfig.shortName}">
639
+ <link rel="apple-touch-icon" href="${assetPrefix}icon-192.png">` : `
640
+ <meta name="theme-color" content="${pwaConfig.themeColor}">`;
641
+ const html = `<!DOCTYPE html>
642
+ <html lang="${config.site.language || "en"}">
643
+ <head>
644
+ <meta charset="UTF-8">
645
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
646
+ <title>${title}</title>
647
+ <meta name="description" content="${description}">
648
+ ${cssLinks}${buildPwaTags}
649
+ </head>
650
+ <body>
651
+ <div id="app"></div>
652
+ <script type="module" src="${assetPrefix}${routeJsFile}"></script>
653
+ </body>
654
+ </html>`;
655
+ const fileName = route.path === "/" ? "index.html" : `${route.path.slice(1)}/index.html`;
656
+ this.emitFile({
657
+ type: "asset",
658
+ fileName,
659
+ source: html
660
+ });
661
+ }
662
+ const hasCustomIcons = config.pwa?.icon192 && config.pwa?.icon512;
663
+ const webManifest = {
664
+ name: config.site.title,
665
+ short_name: pwaConfig.shortName,
666
+ description: config.site.description,
667
+ start_url: "./",
668
+ display: pwaConfig.display,
669
+ background_color: pwaConfig.backgroundColor,
670
+ theme_color: pwaConfig.themeColor,
671
+ icons: [
672
+ {
673
+ src: "./icon-192.png",
674
+ sizes: "192x192",
675
+ type: "image/png"
676
+ },
677
+ {
678
+ src: "./icon-512.png",
679
+ sizes: "512x512",
680
+ type: "image/png",
681
+ purpose: "any maskable"
682
+ }
683
+ ]
684
+ };
685
+ this.emitFile({
686
+ type: "asset",
687
+ fileName: "manifest.webmanifest",
688
+ source: JSON.stringify(webManifest, null, 2)
689
+ });
690
+ this.emitFile({
691
+ type: "asset",
692
+ fileName: "offline.html",
693
+ source: generateOfflinePage(config.site.title)
694
+ });
695
+ if (!hasCustomIcons) {
696
+ this.emitFile({
697
+ type: "asset",
698
+ fileName: "icon-192.png",
699
+ source: generatePlaceholderIcon(192)
700
+ });
701
+ this.emitFile({
702
+ type: "asset",
703
+ fileName: "icon-512.png",
704
+ source: generatePlaceholderIcon(512)
705
+ });
706
+ }
707
+ if (pwaEnabled) {
708
+ const precacheEntries = [];
709
+ const contentRevision = (source) => createHash("md5").update(source).digest("hex").slice(0, 8);
710
+ for (const route of routes) {
711
+ const fileName = route.path === "/" ? "index.html" : `${route.path.slice(1)}/index.html`;
712
+ const htmlAsset = bundle[fileName];
713
+ const source = htmlAsset && htmlAsset.type === "asset" ? String(htmlAsset.source) : "";
714
+ precacheEntries.push({
715
+ url: `./${fileName}`,
716
+ revision: contentRevision(source)
717
+ });
718
+ }
719
+ const offlineSource = generateOfflinePage(config.site.title);
720
+ precacheEntries.push({ url: "./offline.html", revision: contentRevision(offlineSource) });
721
+ for (const [fileName] of Object.entries(bundle)) {
722
+ if (fileName.endsWith(".js") || fileName.endsWith(".css")) {
723
+ precacheEntries.push({
724
+ url: `./${fileName}`,
725
+ revision: null
726
+ });
727
+ }
728
+ }
729
+ const swSourcePath = resolve(__dirname, "../runtime/sw.js");
730
+ const manifestJson = JSON.stringify(precacheEntries);
731
+ const { build: esbuildBuild } = await import("esbuild");
732
+ const esbuildResult = await esbuildBuild({
733
+ entryPoints: [swSourcePath],
734
+ bundle: true,
735
+ format: "iife",
736
+ write: false,
737
+ platform: "browser",
738
+ define: {
739
+ "self.__WB_MANIFEST": manifestJson
740
+ }
741
+ });
742
+ const swCode = esbuildResult.outputFiles[0].text;
743
+ this.emitFile({
744
+ type: "asset",
745
+ fileName: "sw.js",
746
+ source: swCode
747
+ });
748
+ }
749
+ }
750
+ }
751
+ ];
752
+ }
753
+ export {
754
+ defineConfig,
755
+ pressyPlugin
756
+ };
757
+ //# sourceMappingURL=index.js.map