vite-plugin-htjs-pages 1.1.2 → 1.2.1

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/README.md CHANGED
@@ -1,23 +1,18 @@
1
1
  # vite-plugin-htjs-pages
2
2
 
3
- [![npm
4
- version](https://img.shields.io/npm/v/vite-plugin-htjs-pages.svg)](https://www.npmjs.com/package/vite-plugin-htjs-pages)
5
- [![npm
6
- downloads](https://img.shields.io/npm/dm/vite-plugin-htjs-pages.svg)](https://www.npmjs.com/package/vite-plugin-htjs-pages)
3
+ [![npm version](https://img.shields.io/npm/v/vite-plugin-htjs-pages.svg)](https://www.npmjs.com/package/vite-plugin-htjs-pages)
4
+ [![npm downloads](https://img.shields.io/npm/dm/vite-plugin-htjs-pages.svg)](https://www.npmjs.com/package/vite-plugin-htjs-pages)
7
5
  [![license](https://img.shields.io/npm/l/vite-plugin-htjs-pages.svg)](LICENSE)
8
6
  [![vite](https://img.shields.io/badge/vite-plugin-646CFF?logo=vite&logoColor=white)](https://vitejs.dev)
9
7
 
10
- Minimal **static site generation (SSG) for Vite** using JavaScript
11
- functions that return HTML.
8
+ Minimal **static site generation for Vite** using JavaScript files that return HTML.
12
9
 
13
10
  Generate static HTML pages from `*.ht.js` modules using Vite and
14
11
  [`javascript-to-html`](https://www.npmjs.com/package/javascript-to-html).
15
12
 
16
13
  ⭐ If this project helps you, please consider starring it.
17
14
 
18
- ------------------------------------------------------------------------
19
-
20
- ## Built for the Vite ecosystem
15
+ # Built for the Vite ecosystem
21
16
 
22
17
  Works seamlessly with:
23
18
 
@@ -28,25 +23,9 @@ Works seamlessly with:
28
23
  A minimal **static site generator for Vite** that keeps pages as simple
29
24
  JavaScript functions returning HTML.
30
25
 
31
- ------------------------------------------------------------------------
32
-
33
- # Features
34
-
35
- - File‑based routing
36
- - Dynamic routes (`[slug].ht.js`)
37
- - Catch‑all routes (`[...slug].ht.js`)
38
- - Static params generation (`generateStaticParams()`)
39
- - Dev server SSR rendering
40
- - Clean URL support
41
- - Parallel batched page rendering
42
- - Automatic `404.html`
43
- - Automatic `sitemap.xml`
44
- - Optional `rss.xml` generation
45
- - Debug mode for easier troubleshooting
46
-
47
- ------------------------------------------------------------------------
26
+ ---
48
27
 
49
- ## TL;DR
28
+ # TL;DR
50
29
 
51
30
  Write:
52
31
 
@@ -90,7 +69,34 @@ Get:
90
69
  </html>
91
70
  ```
92
71
 
93
- ------------------------------------------------------------------------
72
+ ---
73
+
74
+ # Features
75
+
76
+ - File-based routing
77
+ - Dynamic routes `[slug]`
78
+ - Multiple parameters `[year]/[slug]`
79
+ - Catch-all routes `[...slug]`
80
+ - Optional catch-all routes `[...slug]?`
81
+ - Route groups `(admin)/users.ht.js`
82
+ - Index routes `blog/[slug]/index.ht.js`
83
+ - Static params generation
84
+ - Dev server SSR rendering
85
+ - Parallel static generation
86
+ - Automatic `404.html`
87
+ - Automatic `sitemap.xml`
88
+ - Optional RSS feed generation
89
+ - Debug logging
90
+
91
+ ---
92
+
93
+ # Installation
94
+
95
+ ```bash
96
+ npm install vite-plugin-htjs-pages javascript-to-html
97
+ ```
98
+
99
+ ---
94
100
 
95
101
  # Why this exists
96
102
 
@@ -118,146 +124,240 @@ It gives you:
118
124
 
119
125
  while keeping pages as **simple JavaScript functions that return HTML**.
120
126
 
121
- ------------------------------------------------------------------------
127
+ ---
122
128
 
123
- # How it works
129
+ # Quick Start
124
130
 
125
- src/index.ht.js
126
- src/blog/[slug].ht.js
127
-
128
-
129
- vite-plugin-htjs-pages
130
-
131
-
132
- dist/index.html
133
- dist/blog/hello-world/index.html
131
+ ### vite.config.js
134
132
 
135
- Pages are just **JavaScript functions that return HTML**.
133
+ ```js
134
+ import { defineConfig } from "vite"
135
+ import { htPages } from "vite-plugin-htjs-pages"
136
136
 
137
- ------------------------------------------------------------------------
137
+ export default defineConfig({
138
+ plugins: [htPages()]
139
+ })
140
+ ```
138
141
 
139
- # Installation
142
+ ---
143
+
144
+ # Example Project Structure
140
145
 
141
- ``` bash
142
- npm install vite-plugin-htjs-pages javascript-to-html
143
146
  ```
147
+ src/
144
148
 
145
- ------------------------------------------------------------------------
149
+ index.ht.js
150
+ about.ht.js
146
151
 
147
- # Quick Start
152
+ blog/
153
+ index.ht.js
154
+ [slug].ht.js
155
+ [year]/[slug].ht.js
156
+
157
+ docs/
158
+ [...slug]?.ht.js
159
+
160
+ (admin)/
161
+ users.ht.js
162
+ ```
163
+
164
+ ---
165
+
166
+ # Routing
167
+
168
+ Routes are generated directly from the filesystem.
148
169
 
149
- ### 1. Configure Vite
170
+
171
+ | Feature | File | URL |
172
+ |-----|-----|-----|
173
+ | static routes | `index.ht.js` | `/` |
174
+ | dynamic routes | `blog/[slug].ht.js` | `/blog/my-post` |
175
+ | multiple params | `blog/[year]/[slug].ht.js` | `/blog/2026/my-post` |
176
+ | catch-all | `docs/[...slug].ht.js` | `/docs/api/auth/login` |
177
+ | optional catch-all | `docs/[...slug]?.ht.js` | `/docs` or `/docs/getting-started` |
178
+ | index routes | `products/[product]/index.ht.js` | `/products/iphone-18` |
179
+ | route groups | `(admin)/users.ht.js` | `/users` |
180
+
181
+ ---
182
+
183
+ # Dynamic Routes
184
+
185
+ ```
186
+ src/blog/[slug].ht.js
187
+ ```
188
+
189
+ Matches:
190
+
191
+ ```
192
+ /blog/hello-world
193
+ /blog/my-first-post
194
+ ```
195
+
196
+ Example:
150
197
 
151
198
  ``` js
152
- import { defineConfig } from 'vite'
153
- import { htPages } from 'vite-plugin-htjs-pages'
199
+ import { fragment, html, body, h1 } from 'javascript-to-html'
200
+
201
+ export function generateStaticParams() {
202
+ return [
203
+ { slug: 'hello-world' },
204
+ { slug: 'my-first-post' }
205
+ ]
206
+ }
207
+
208
+ export default ({ params }) => fragment(
209
+ '<!doctype html>',
210
+ html(
211
+ body(
212
+ h1(params.slug)
213
+ )
214
+ )
215
+ )
216
+ ```
217
+
218
+ ---
219
+
220
+ # Multiple Parameters
154
221
 
155
- export default defineConfig({
156
- plugins: [htPages()]
157
- })
222
+ ```
223
+ src/blog/[year]/[slug].ht.js
158
224
  ```
159
225
 
160
- ### 2. Create a page
226
+ Matches:
161
227
 
162
- src/index.ht.js
228
+ ```
229
+ /blog/2026/vite-routing
230
+ /blog/2025/my-first-post
231
+ ```
232
+
233
+ Example:
163
234
 
164
235
  ``` js
165
- import { fragment, html, head, body, title, h1 } from 'javascript-to-html'
236
+ import { fragment, html, body, h1 } from 'javascript-to-html'
166
237
 
167
- export default () => fragment(
238
+ export function generateStaticParams() {
239
+ return [
240
+ {
241
+ year: 2026,
242
+ slug: "vite-routing"
243
+ },
244
+ {
245
+ year: 2025,
246
+ slug: 'my-first-post'
247
+ }
248
+ ]
249
+ }
250
+
251
+ export default ({ params }) => fragment(
168
252
  '<!doctype html>',
169
253
  html(
170
- head(
171
- title('Hello world')
172
- ),
173
254
  body(
174
- h1('Hello world')
255
+ h1(params.slug)
175
256
  )
176
257
  )
177
258
  )
178
259
  ```
179
260
 
180
- ### 3. Run dev server
261
+ ---
181
262
 
182
- ``` bash
183
- vite
263
+ # Catch-All Routes
264
+
265
+ ```
266
+ src/docs/[...slug].ht.js
184
267
  ```
185
268
 
186
- ### 4. Build
269
+ Matches:
187
270
 
188
- ``` bash
189
- vite build
271
+ ```
272
+ /docs/api/auth/login
273
+ /docs/guides/rendering/static
190
274
  ```
191
275
 
192
- Output:
276
+ Params:
193
277
 
194
- dist/index.html
278
+ ```
279
+ params.slug === "api/auth/login"
280
+ ```
195
281
 
196
- ------------------------------------------------------------------------
282
+ ---
197
283
 
198
- # File‑Based Routing
284
+ # Optional Catch-All Routes
199
285
 
200
- Routes are derived from the filesystem.
286
+ ```
287
+ src/docs/[...slug]?.ht.js
288
+ ```
201
289
 
202
- src/
290
+ Matches both:
203
291
 
204
- index.ht.js
205
- about.ht.js
292
+ ```
293
+ /docs
294
+ /docs/getting-started
295
+ /docs/api/auth/login
296
+ ```
206
297
 
207
- blog/
208
- [slug].ht.js
298
+ Params:
209
299
 
210
- docs/
211
- [...slug].ht.js
300
+ | URL | params.slug |
301
+ |-----|-------------|
302
+ | `/docs` | "" |
303
+ | `/docs/api` | "api" |
304
+ | `/docs/api/auth` | "api/auth" |
212
305
 
213
- Produces:
306
+ ---
214
307
 
215
- /index.html
216
- /about/index.html
217
- /blog/hello-world/index.html
218
- /docs/getting-started/index.html
308
+ # Route Groups
219
309
 
220
- ------------------------------------------------------------------------
310
+ Folders wrapped in parentheses are ignored in URLs.
221
311
 
222
- # Dynamic Routes
312
+ ```
313
+ src/(admin)/users.ht.js
314
+ ```
223
315
 
224
- src/blog/[slug].ht.js
316
+ URL:
225
317
 
226
- ``` js
227
- import { fragment, html, body, h1 } from 'javascript-to-html'
318
+ ```
319
+ /users
320
+ ```
321
+
322
+ ---
323
+
324
+ # Index Routes
325
+
326
+ Files named `index.ht.js` map to the parent route.
327
+
328
+ ```
329
+ src/blog/index.ht.js -> /blog
330
+ src/blog/[slug]/index.ht.js -> /blog/my-post
331
+ ```
332
+
333
+ ---
334
+
335
+ # Static Params
228
336
 
337
+ Dynamic routes can export `generateStaticParams`.
338
+
339
+ ```js
229
340
  export function generateStaticParams() {
230
341
  return [
231
- { slug: 'hello-world' },
232
- { slug: 'deep-dive' }
342
+ { slug: "hello-world" },
343
+ { slug: "vite-routing" }
233
344
  ]
234
345
  }
235
-
236
- export default ({ params }) => fragment(
237
- '<!doctype html>',
238
- html(
239
- body(
240
- h1(params.slug)
241
- )
242
- )
243
- )
244
346
  ```
245
347
 
246
- ------------------------------------------------------------------------
348
+ ---
247
349
 
248
350
  # Data Loading
249
351
 
250
352
  Pages can export a `data()` function.
251
353
 
252
- ``` js
354
+ ```js
253
355
  export async function data({ params }) {
254
- return {
255
- title: params.slug
256
- }
356
+ return { title: params.slug }
257
357
  }
258
358
  ```
259
359
 
260
- ------------------------------------------------------------------------
360
+ ---
261
361
 
262
362
  # Layouts
263
363
 
@@ -277,37 +377,29 @@ export default (...content) => fragment(
277
377
  )
278
378
  ```
279
379
 
280
- ------------------------------------------------------------------------
380
+ ---
281
381
 
282
382
  # Plugin Options
283
383
 
284
- ``` js
285
- htPages({
286
- cleanUrls: true,
287
- renderConcurrency: 8,
288
- renderBatchSize: 64
289
- })
290
- ```
291
-
292
384
  | Option | Description |
293
385
  |------|------|
294
386
  | `pagesDir` | root directory for pages |
295
- | `include` | page glob pattern |
296
- | `exclude` | excluded patterns |
387
+ | `include` | page glob |
388
+ | `exclude` | excluded files |
297
389
  | `cleanUrls` | `/page/index.html` instead of `/page.html` |
298
- | `renderConcurrency` | concurrent page renders |
299
- | `renderBatchSize` | render batch size |
390
+ | `renderConcurrency` | parallel rendering |
391
+ | `renderBatchSize` | batch size |
300
392
  | `debug` | enable debug logging |
301
- | `site` | base URL used for sitemap |
302
- | `rss` | configuration for RSS feed |
393
+ | `site` | base URL for sitemap |
394
+ | `rss` | RSS configuration |
303
395
 
304
- ------------------------------------------------------------------------
396
+ ---
305
397
 
306
398
  # Debug Mode
307
399
 
308
- Enable debug logs when troubleshooting builds or routing.
400
+ Enable debug logging when troubleshooting.
309
401
 
310
- ``` js
402
+ ```js
311
403
  htPages({
312
404
  debug: true
313
405
  })
@@ -322,38 +414,21 @@ Example output:
322
414
  This helps diagnose routing or build issues without modifying plugin
323
415
  code.
324
416
 
325
- ------------------------------------------------------------------------
417
+ ---
326
418
 
327
419
  # Automatic Sitemap
328
420
 
329
- The plugin automatically generates:
330
-
331
- dist/sitemap.xml
332
-
333
- Example:
421
+ A `sitemap.xml` is generated automatically.
334
422
 
335
- ``` xml
336
- <urlset>
337
- <url><loc>/</loc></url>
338
- <url><loc>/blog/hello-world</loc></url>
339
- </urlset>
340
423
  ```
341
-
342
- If you configure a `site` option, URLs become absolute:
343
-
344
- ``` js
345
- htPages({
346
- site: "https://example.com"
347
- })
424
+ dist/sitemap.xml
348
425
  ```
349
426
 
350
- ------------------------------------------------------------------------
427
+ ---
351
428
 
352
- # Automatic RSS Feed
429
+ # Optional RSS Feed
353
430
 
354
- Optional RSS feed generation for blogs.
355
-
356
- ``` js
431
+ ```js
357
432
  htPages({
358
433
  rss: {
359
434
  site: "https://example.com",
@@ -364,35 +439,36 @@ htPages({
364
439
  })
365
440
  ```
366
441
 
367
- This generates:
368
-
369
- dist/rss.xml
370
-
371
- Example item:
442
+ Produces:
372
443
 
373
- ``` xml
374
- <item>
375
- <title>/blog/hello-world</title>
376
- <link>https://example.com/blog/hello-world</link>
377
- </item>
444
+ ```
445
+ dist/rss.xml
378
446
  ```
379
447
 
380
- ------------------------------------------------------------------------
448
+ ---
381
449
 
382
- # Performance Tips
450
+ # Performance
383
451
 
384
- Large sites (500+ pages):
452
+ Large sites can increase concurrency:
385
453
 
386
- ``` js
454
+ ```js
387
455
  htPages({
388
456
  renderConcurrency: 16,
389
457
  renderBatchSize: 128
390
458
  })
391
459
  ```
392
460
 
393
- This keeps memory usage stable during large builds.
461
+ ---
462
+
463
+ # Comparison
394
464
 
395
- ------------------------------------------------------------------------
465
+ | Tool | Focus |
466
+ |-----|-----|
467
+ | Astro | component‑based SSG |
468
+ | Next.js | React SSR framework |
469
+ | vite-plugin-htjs-pages | minimal HTML SSG for Vite |
470
+
471
+ ---
396
472
 
397
473
  # Use Cases
398
474
 
@@ -406,17 +482,7 @@ This keeps memory usage stable during large builds.
406
482
  - **Minimal static site generators**
407
483
  - **HTML‑first Vite projects**
408
484
 
409
- ------------------------------------------------------------------------
410
-
411
- # Comparison
412
-
413
- | Tool | Focus |
414
- |-----|-----|
415
- | Astro | component framework |
416
- | Next.js | SSR framework |
417
- | vite-plugin-htjs-pages | minimal static HTML generation |
418
-
419
- ------------------------------------------------------------------------
485
+ ---
420
486
 
421
487
  # License
422
488
 
package/TODO ADDED
@@ -0,0 +1,2 @@
1
+ - caching for API fetching inside generateStaticParams() and data() like getCachedData.js
2
+ - `export const dynamic = true` to disable static generation for specific routes
package/dist/index.js CHANGED
@@ -27,10 +27,18 @@ function normalizeFsPath(p) {
27
27
  }
28
28
 
29
29
  // src/route-utils.ts
30
+ function safeDecodeURIComponent(str) {
31
+ try {
32
+ return decodeURIComponent(str);
33
+ } catch {
34
+ return str;
35
+ }
36
+ }
30
37
  var DYNAMIC_SEGMENT_RE = /\[([A-Za-z0-9_]+)\]/g;
31
38
  var CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]/g;
32
39
  var OPTIONAL_CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]\?/g;
33
40
  var ANY_PARAM_RE = /\[(?:\.\.\.)?([A-Za-z0-9_]+)\]\??/g;
41
+ var ROUTE_GROUP_RE = /(^|\/)\(([^)]+)\)(?=\/|$)/g;
34
42
  function getParamNames(relativeFromPagesDir) {
35
43
  return [...relativeFromPagesDir.matchAll(ANY_PARAM_RE)].map((m) => m[1]);
36
44
  }
@@ -39,11 +47,13 @@ function isDynamicPage(relativeFromPagesDir) {
39
47
  }
40
48
  function toRoutePattern(relativeFromPagesDir) {
41
49
  const noExt = stripHtSuffix(toPosix(relativeFromPagesDir));
42
- const raw = noExt.replace(/(^|\/)index$/i, "$1").replace(OPTIONAL_CATCH_ALL_SEGMENT_RE, "*?:$1").replace(CATCH_ALL_SEGMENT_RE, "*:$1").replace(DYNAMIC_SEGMENT_RE, ":$1");
50
+ const withoutGroups = noExt.replace(ROUTE_GROUP_RE, "$1");
51
+ const withoutIndex = withoutGroups.replace(/\/index$/i, "").replace(/^index$/i, "");
52
+ const raw = withoutIndex.replace(OPTIONAL_CATCH_ALL_SEGMENT_RE, "*?:$1").replace(CATCH_ALL_SEGMENT_RE, "*:$1").replace(DYNAMIC_SEGMENT_RE, ":$1");
43
53
  return normalizeRoutePath(raw || "/");
44
54
  }
45
55
  function fillParams(pattern, params) {
46
- return pattern.replace(/\*\?:([A-Za-z0-9_]+)/g, (_, key) => {
56
+ const result = pattern.replace(/\*\?:([A-Za-z0-9_]+)/g, (_, key) => {
47
57
  const value = params[key];
48
58
  if (value == null || value === "") {
49
59
  return "";
@@ -59,7 +69,8 @@ function fillParams(pattern, params) {
59
69
  throw new Error(`Missing route param "${key}"`);
60
70
  }
61
71
  return encodeURIComponent(params[key]);
62
- }).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
72
+ });
73
+ return normalizeRoutePath(result || "/");
63
74
  }
64
75
  function fileNameFromRoute(routePath, cleanUrls) {
65
76
  const normalized = normalizeRoutePath(routePath);
@@ -72,9 +83,7 @@ function expandStaticPaths(basePage, rows, cleanUrls) {
72
83
  const params = Object.fromEntries(
73
84
  Object.entries(row).map(([k, v]) => [k, String(v)])
74
85
  );
75
- const routePath = normalizeRoutePath(
76
- fillParams(basePage.routePattern, params)
77
- );
86
+ const routePath = fillParams(basePage.routePattern, params);
78
87
  return {
79
88
  ...basePage,
80
89
  routePath,
@@ -91,20 +100,18 @@ function routeMatch(pattern, urlPath) {
91
100
  const patternSeg = a[i];
92
101
  const urlSeg = b[i];
93
102
  if (patternSeg.startsWith("*?:")) {
94
- params[patternSeg.slice(3)] = i < b.length ? b.slice(i).map(decodeURIComponent).join("/") : "";
103
+ params[patternSeg.slice(3)] = i < b.length ? b.slice(i).map(safeDecodeURIComponent).join("/") : "";
95
104
  return params;
96
105
  }
97
106
  if (patternSeg.startsWith("*:")) {
98
107
  const rest = b.slice(i);
99
- if (rest.length === 0) {
100
- return null;
101
- }
102
- params[patternSeg.slice(2)] = rest.map(decodeURIComponent).join("/");
108
+ if (rest.length === 0) return null;
109
+ params[patternSeg.slice(2)] = rest.map(safeDecodeURIComponent).join("/");
103
110
  return params;
104
111
  }
105
112
  if (!urlSeg) return null;
106
113
  if (patternSeg.startsWith(":")) {
107
- params[patternSeg.slice(1)] = decodeURIComponent(urlSeg);
114
+ params[patternSeg.slice(1)] = safeDecodeURIComponent(urlSeg);
108
115
  continue;
109
116
  }
110
117
  if (patternSeg !== urlSeg) return null;
@@ -136,7 +143,7 @@ function compareRoutePriority(a, b) {
136
143
  return aDynamic ? 1 : -1;
137
144
  }
138
145
  }
139
- return 0;
146
+ return bSegs.length - aSegs.length;
140
147
  }
141
148
 
142
149
  // src/constants.ts
@@ -147,7 +154,11 @@ var CACHE_DIR_NAME = `node_modules/.cache/${PLUGIN_NAME}`;
147
154
 
148
155
  // src/discover.ts
149
156
  async function discoverEntryPages(root, options) {
150
- const include = Array.isArray(options.include) ? options.include : [options.include ?? "src/**/*.ht.js"];
157
+ const rawInclude = Array.isArray(options.include) ? options.include : [options.include ?? "src/**/*.ht.js"];
158
+ let include = rawInclude.filter((p) => typeof p === "string" && p.length > 0);
159
+ if (include.length === 0) {
160
+ include = ["src/**/*.ht.js"];
161
+ }
151
162
  const exclude = Array.isArray(options.exclude) ? options.exclude : options.exclude ? [options.exclude] : [];
152
163
  const pagesDir = options.pagesDir ?? "src";
153
164
  const pagesRoot = normalizeFsPath(path2.join(root, pagesDir));
@@ -271,7 +282,7 @@ async function buildPageIndex(args) {
271
282
  for (const entry of entries) {
272
283
  const mod = modulesByEntry.get(entry.entryPath) ?? {};
273
284
  if (entry.dynamic) {
274
- const rows = mod.generateStaticParams ? await mod.generateStaticParams() : [];
285
+ const rows = (mod.generateStaticParams ? await mod.generateStaticParams() : []) ?? [];
275
286
  pages.push(
276
287
  ...expandStaticPaths(
277
288
  {
@@ -283,7 +294,7 @@ async function buildPageIndex(args) {
283
294
  dynamic: entry.dynamic,
284
295
  paramNames: entry.paramNames
285
296
  },
286
- rows,
297
+ Array.isArray(rows) ? rows : [],
287
298
  cleanUrls
288
299
  )
289
300
  );
@@ -388,12 +399,16 @@ async function buildRenderBundle(args) {
388
399
 
389
400
  // src/plugin.ts
390
401
  function chunkArray(items, size) {
402
+ const safeSize = Math.max(1, Math.floor(size));
391
403
  const out = [];
392
- for (let i = 0; i < items.length; i += size) {
393
- out.push(items.slice(i, i + size));
404
+ for (let i = 0; i < items.length; i += safeSize) {
405
+ out.push(items.slice(i, i + safeSize));
394
406
  }
395
407
  return out;
396
408
  }
409
+ function escapeXml(text) {
410
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
411
+ }
397
412
  function createEntriesKey(entries) {
398
413
  const raw = entries.map((e) => `${e.entryPath}|${e.routePattern}|${e.dynamic}`).join("\n");
399
414
  return createHash2("sha256").update(raw).digest("hex");
@@ -408,12 +423,22 @@ function htPages(options = {}) {
408
423
  let devPages = [];
409
424
  let cachedManifestKey = null;
410
425
  let cachedBundlePath = null;
426
+ let loadDevPagesInFlight = null;
411
427
  const cleanUrls = options.cleanUrls ?? true;
412
428
  function logDebug(enabled, ...args) {
413
429
  if (!enabled) return;
414
430
  console.log(`[${PLUGIN_NAME}]`, ...args);
415
431
  }
416
432
  async function loadDevPages() {
433
+ if (loadDevPagesInFlight) return loadDevPagesInFlight;
434
+ loadDevPagesInFlight = doLoadDevPages();
435
+ try {
436
+ return await loadDevPagesInFlight;
437
+ } finally {
438
+ loadDevPagesInFlight = null;
439
+ }
440
+ }
441
+ async function doLoadDevPages() {
417
442
  const entries = await discoverEntryPages(root, options);
418
443
  const modulesByEntry = /* @__PURE__ */ new Map();
419
444
  logDebug(options.debug, "discovered entries", entries.map((e) => e.relativePath));
@@ -531,8 +556,12 @@ function htPages(options = {}) {
531
556
  async generateBundle(_, bundle) {
532
557
  const { modulesByEntry, pages } = await buildPagesPipeline();
533
558
  logDebug(options.debug, "emitting pages", pages.map((p) => p.fileName));
534
- const limit = pLimit(options.renderConcurrency ?? 8);
535
- const batchSize = options.renderBatchSize ?? Math.max(options.renderConcurrency ?? 8, 32);
559
+ const concurrency = Math.max(1, options.renderConcurrency ?? 8);
560
+ const limit = pLimit(concurrency);
561
+ const batchSize = Math.max(
562
+ 1,
563
+ options.renderBatchSize ?? Math.max(concurrency, 32)
564
+ );
536
565
  for (const batch of chunkArray(pages, batchSize)) {
537
566
  await Promise.all(
538
567
  batch.map(
@@ -558,7 +587,9 @@ function htPages(options = {}) {
558
587
  if (sitemapRoutes.length > 0) {
559
588
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
560
589
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
561
- ${sitemapRoutes.map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`).join("\n")}
590
+ ${sitemapRoutes.map(
591
+ (route) => ` <url><loc>${escapeXml(sitemapBase)}${escapeXml(route)}</loc></url>`
592
+ ).join("\n")}
562
593
  </urlset>
563
594
  `;
564
595
  this.emitFile({
@@ -572,17 +603,17 @@ function htPages(options = {}) {
572
603
  const rssItems = pages.filter((page) => page.routePath.startsWith(routePrefix)).map((page) => {
573
604
  const url = `${options.rss.site}${page.routePath}`;
574
605
  return ` <item>
575
- <title>${page.routePath}</title>
576
- <link>${url}</link>
577
- <guid>${url}</guid>
606
+ <title>${escapeXml(page.routePath)}</title>
607
+ <link>${escapeXml(url)}</link>
608
+ <guid>${escapeXml(url)}</guid>
578
609
  </item>`;
579
610
  }).join("\n");
580
611
  const rss = `<?xml version="1.0" encoding="UTF-8"?>
581
612
  <rss version="2.0">
582
613
  <channel>
583
- <title>${options.rss.title ?? PLUGIN_NAME}</title>
584
- <link>${options.rss.site}</link>
585
- <description>${options.rss.description ?? "RSS feed"}</description>
614
+ <title>${escapeXml(options.rss.title ?? PLUGIN_NAME)}</title>
615
+ <link>${escapeXml(options.rss.site)}</link>
616
+ <description>${escapeXml(options.rss.description ?? "RSS feed")}</description>
586
617
  ${rssItems}
587
618
  </channel>
588
619
  </rss>
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts","../src/discover.ts","../src/path-utils.ts","../src/route-utils.ts","../src/constants.ts","../src/errors.ts","../src/render-runtime.ts","../src/dev-server.ts","../src/page-index.ts","../src/render-bundle.ts","../src/manifest.ts"],"sourcesContent":["import path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { createHash } from 'node:crypto';\nimport pLimit from 'p-limit';\nimport type { Plugin, ViteDevServer } from 'vite';\n\nimport { discoverEntryPages } from './discover';\nimport { installDevServer } from './dev-server';\nimport { buildPageIndex } from './page-index';\nimport { buildRenderBundle } from './render-bundle';\nimport { renderPage } from './render-runtime';\n\nimport type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';\nimport { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID, CACHE_DIR_NAME } from './constants';\n\nfunction chunkArray<T>(items: T[], size: number): T[][] {\n const out: T[][] = [];\n for (let i = 0; i < items.length; i += size) {\n out.push(items.slice(i, i + size));\n }\n return out;\n}\n\nfunction createEntriesKey(entries: HtPageInfo[]): string {\n const raw = entries\n .map((e) => `${e.entryPath}|${e.routePattern}|${e.dynamic}`)\n .join('\\n');\n\n return createHash('sha256').update(raw).digest('hex');\n}\n\nasync function importManifest(\n bundlePath: string,\n): Promise<Array<{ page: HtPageInfo; mod: HtPageModule }>> {\n const mod = await import(pathToFileURL(bundlePath).href + `?t=${Date.now()}`);\n return mod.manifest as Array<{ page: HtPageInfo; mod: HtPageModule }>;\n}\n\nexport function htPages(options: HtPagesPluginOptions = {}): Plugin {\n let root = process.cwd();\n let server: ViteDevServer | null = null;\n let devPages: HtPageInfo[] = [];\n\n let cachedManifestKey: string | null = null;\n let cachedBundlePath: string | null = null;\n\n const cleanUrls = options.cleanUrls ?? true;\n\n function logDebug(enabled: boolean | undefined, ...args: unknown[]) {\n if (!enabled) return;\n console.log(`[${PLUGIN_NAME}]`, ...args);\n }\n\n async function loadDevPages(): Promise<HtPageInfo[]> {\n const entries = await discoverEntryPages(root, options);\n const modulesByEntry = new Map<string, HtPageModule>();\n\n logDebug(options.debug, 'discovered entries', entries.map((e) => e.relativePath));\n\n if (!server) return [];\n\n for (const entry of entries) {\n const mod = (await server.ssrLoadModule(\n `/${entry.relativePath}`,\n )) as HtPageModule;\n\n modulesByEntry.set(entry.entryPath, mod);\n }\n\n devPages = await buildPageIndex({\n entries,\n modulesByEntry,\n cleanUrls,\n });\n\n logDebug(\n options.debug,\n 'dev pages',\n devPages.map((p) => `${p.routePath} -> ${p.relativePath}`),\n );\n\n return devPages;\n }\n\n async function buildPagesPipeline() {\n const entries = await discoverEntryPages(root, options);\n const cacheDir = path.join(root, CACHE_DIR_NAME);\n\n const entriesKey = createEntriesKey(entries);\n\n let bundlePath: string;\n if (cachedBundlePath && cachedManifestKey === entriesKey) {\n bundlePath = cachedBundlePath;\n } else {\n bundlePath = await buildRenderBundle({\n entries,\n cacheDir,\n ssrPlugins: options.ssrPlugins,\n });\n cachedManifestKey = entriesKey;\n cachedBundlePath = bundlePath;\n }\n\n logDebug(options.debug, 'render bundle', bundlePath);\n\n const manifest = await importManifest(bundlePath);\n const modulesByEntry = new Map<string, HtPageModule>();\n\n for (const rec of manifest) {\n modulesByEntry.set(rec.page.entryPath, rec.mod);\n }\n\n const pages = await buildPageIndex({\n entries,\n modulesByEntry,\n cleanUrls,\n });\n\n // Ensure static hosts get a 404.html\n const notFoundPage = pages.find((p) => p.routePath === '/404');\n\n if (notFoundPage && !pages.some((p) => p.fileName === '404.html')) {\n pages.push({\n ...notFoundPage,\n fileName: '404.html',\n });\n } \n\n return { entries, bundlePath, modulesByEntry, pages };\n }\n\n return {\n name: PLUGIN_NAME,\n\n config(userConfig, env) {\n if (env.command !== 'build') return;\n\n const hasExplicitInput = userConfig.build?.rollupOptions?.input != null;\n if (hasExplicitInput) return;\n\n return {\n build: {\n rollupOptions: {\n input: VIRTUAL_BUILD_ENTRY_ID,\n },\n },\n };\n },\n\n resolveId(id) {\n if (id === VIRTUAL_BUILD_ENTRY_ID) return id;\n return null;\n },\n\n load(id) {\n if (id === VIRTUAL_BUILD_ENTRY_ID) {\n return 'export default {};';\n }\n return null;\n },\n\n configResolved(resolved) {\n root = resolved.root;\n },\n\n async buildStart() {\n const entries = await discoverEntryPages(root, options);\n\n for (const entry of entries) {\n this.addWatchFile(entry.entryPath);\n }\n },\n\n configureServer(_server) {\n server = _server;\n\n installDevServer({\n server,\n getPages: async () => {\n if (devPages.length > 0) return devPages;\n return loadDevPages();\n },\n });\n\n loadDevPages().catch((error) => {\n server?.config.logger.error(\n `[${PLUGIN_NAME}] loadDevPages failed: ${\n error instanceof Error ? error.stack ?? error.message : String(error)\n }`,\n );\n });\n },\n\n async handleHotUpdate(ctx) {\n if (!server) return;\n \n const file = ctx.file;\n \n if (\n file.endsWith('.ht.js') ||\n file.includes('/templates/')\n ) {\n logDebug(options.debug, 'reindex triggered by', file);\n await loadDevPages();\n }\n },\n\n async generateBundle(_, bundle) {\n const { modulesByEntry, pages } = await buildPagesPipeline();\n \n logDebug(options.debug, 'emitting pages', pages.map((p) => p.fileName));\n \n const limit = pLimit(options.renderConcurrency ?? 8);\n const batchSize =\n options.renderBatchSize ??\n Math.max(options.renderConcurrency ?? 8, 32);\n \n for (const batch of chunkArray(pages, batchSize)) {\n await Promise.all(\n batch.map((page) =>\n limit(async () => {\n const mod = modulesByEntry.get(page.entryPath);\n \n if (!mod) {\n throw new Error(\n `[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,\n );\n }\n \n const html = await renderPage(page, mod, false);\n \n this.emitFile({\n type: 'asset',\n fileName: options.mapOutputPath?.(page) ?? page.fileName,\n source: html,\n });\n }),\n ),\n );\n }\n \n // Generate sitemap.xml\n const sitemapBase = options.site ?? '';\n const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))]\n .filter((route) => !route.includes(':') && !route.includes('*'));\n \n if (sitemapRoutes.length > 0) {\n const sitemap = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n ${sitemapRoutes\n .map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`)\n .join('\\n')}\n </urlset>\n `;\n \n this.emitFile({\n type: 'asset',\n fileName: 'sitemap.xml',\n source: sitemap,\n });\n }\n \n // Generate rss.xml\n if (options.rss?.site) {\n const routePrefix = options.rss.routePrefix ?? '/blog';\n \n const rssItems = pages\n .filter((page) => page.routePath.startsWith(routePrefix))\n .map((page) => {\n const url = `${options.rss!.site}${page.routePath}`;\n return ` <item>\n <title>${page.routePath}</title>\n <link>${url}</link>\n <guid>${url}</guid>\n </item>`;\n })\n .join('\\n');\n \n const rss = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <rss version=\"2.0\">\n <channel>\n <title>${options.rss.title ?? PLUGIN_NAME}</title>\n <link>${options.rss.site}</link>\n <description>${options.rss.description ?? 'RSS feed'}</description>\n ${rssItems}\n </channel>\n </rss>\n `;\n \n this.emitFile({\n type: 'asset',\n fileName: 'rss.xml',\n source: rss,\n });\n }\n \n // Remove the dummy virtual build entry chunk\n for (const [fileName, output] of Object.entries(bundle)) {\n if (\n output.type === 'chunk' &&\n output.facadeModuleId === VIRTUAL_BUILD_ENTRY_ID\n ) {\n delete bundle[fileName];\n }\n }\n } \n\n };\n}\n","import path from 'node:path';\nimport fg from 'fast-glob';\nimport { normalizeFsPath, toPosix } from './path-utils';\nimport { getParamNames, isDynamicPage, toRoutePattern } from './route-utils';\nimport type { HtPageInfo, HtPagesPluginOptions } from './types';\nimport { PLUGIN_NAME } from './constants';\n\nexport async function discoverEntryPages(\n root: string,\n options: HtPagesPluginOptions,\n): Promise<HtPageInfo[]> {\n const include = Array.isArray(options.include)\n ? options.include\n : [options.include ?? 'src/**/*.ht.js'];\n\n const exclude = Array.isArray(options.exclude)\n ? options.exclude\n : options.exclude\n ? [options.exclude]\n : [];\n\n const pagesDir = options.pagesDir ?? 'src';\n const pagesRoot = normalizeFsPath(path.join(root, pagesDir));\n\n const files = await fg(include, {\n cwd: root,\n ignore: exclude,\n absolute: true,\n });\n\n return files\n .sort()\n .map((absolutePath) => {\n const entryPath = normalizeFsPath(absolutePath);\n const relativePath = toPosix(path.relative(root, entryPath));\n const relativeFromPagesDir = toPosix(path.relative(pagesRoot, entryPath));\n\n if (\n relativeFromPagesDir.startsWith('../') ||\n relativeFromPagesDir === '..'\n ) {\n throw new Error(\n `[${PLUGIN_NAME}] Page is outside pagesDir: ${entryPath} (pagesDir: ${pagesDir})`,\n );\n }\n\n const dynamic = isDynamicPage(relativeFromPagesDir);\n const routePattern = toRoutePattern(relativeFromPagesDir);\n\n return {\n id: entryPath,\n entryPath,\n absolutePath: entryPath,\n relativePath,\n routePattern,\n routePath: routePattern,\n fileName: '',\n dynamic,\n paramNames: getParamNames(relativeFromPagesDir),\n params: {},\n } satisfies HtPageInfo;\n });\n}","import path from 'node:path';\n\nexport function toPosix(p: string): string {\n return p.split(path.sep).join('/');\n}\n\nexport function stripHtSuffix(file: string): string {\n return file.replace(/\\.ht\\.js$/i, '');\n}\n\nexport function normalizeRoutePath(p: string): string {\n let out = p.startsWith('/') ? p : `/${p}`;\n out = out.replace(/\\/+/g, '/');\n if (out !== '/' && out.endsWith('/')) out = out.slice(0, -1);\n return out;\n}\n\nexport function normalizeFsPath(p: string): string {\n return toPosix(path.resolve(p));\n}","import { normalizeRoutePath, stripHtSuffix, toPosix } from './path-utils';\nimport type { HtPageInfo, StaticParamRecord } from './types';\n\nconst DYNAMIC_SEGMENT_RE = /\\[([A-Za-z0-9_]+)\\]/g;\nconst CATCH_ALL_SEGMENT_RE = /\\[\\.\\.\\.([A-Za-z0-9_]+)\\]/g;\nconst OPTIONAL_CATCH_ALL_SEGMENT_RE = /\\[\\.\\.\\.([A-Za-z0-9_]+)\\]\\?/g;\nconst ANY_PARAM_RE = /\\[(?:\\.\\.\\.)?([A-Za-z0-9_]+)\\]\\??/g;\n\nexport function getParamNames(relativeFromPagesDir: string): string[] {\n return [...relativeFromPagesDir.matchAll(ANY_PARAM_RE)].map((m) => m[1]);\n}\n\nexport function isDynamicPage(relativeFromPagesDir: string): boolean {\n return /\\[(?:\\.\\.\\.)?[A-Za-z0-9_]+\\]\\??/.test(relativeFromPagesDir);\n}\n\nexport function toRoutePattern(relativeFromPagesDir: string): string {\n const noExt = stripHtSuffix(toPosix(relativeFromPagesDir));\n\n const raw = noExt\n .replace(/(^|\\/)index$/i, '$1')\n .replace(OPTIONAL_CATCH_ALL_SEGMENT_RE, '*?:$1')\n .replace(CATCH_ALL_SEGMENT_RE, '*:$1')\n .replace(DYNAMIC_SEGMENT_RE, ':$1');\n\n return normalizeRoutePath(raw || '/');\n}\n\nexport function fillParams(\n pattern: string,\n params: Record<string, string>,\n): string {\n return pattern\n .replace(/\\*\\?:([A-Za-z0-9_]+)/g, (_, key) => {\n const value = params[key];\n\n if (value == null || value === '') {\n return '';\n }\n\n return String(value)\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/');\n })\n .replace(/\\*:([A-Za-z0-9_]+)/g, (_, key) => {\n if (!(key in params)) {\n throw new Error(`Missing catch-all route param \"${key}\"`);\n }\n\n return String(params[key])\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/');\n })\n .replace(/:([A-Za-z0-9_]+)/g, (_, key) => {\n if (!(key in params)) {\n throw new Error(`Missing route param \"${key}\"`);\n }\n\n return encodeURIComponent(params[key]);\n })\n .replace(/\\/+/g, '/')\n .replace(/\\/$/, '') || '/';\n}\n\nexport function fileNameFromRoute(\n routePath: string,\n cleanUrls: boolean,\n): string {\n const normalized = normalizeRoutePath(routePath);\n\n if (normalized === '/') return 'index.html';\n\n const base = normalized.slice(1);\n return cleanUrls ? `${base}/index.html` : `${base}.html`;\n}\n\nexport function expandStaticPaths(\n basePage: Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,\n rows: StaticParamRecord[],\n cleanUrls: boolean,\n): HtPageInfo[] {\n return rows.map((row) => {\n const params = Object.fromEntries(\n Object.entries(row).map(([k, v]) => [k, String(v)]),\n );\n\n const routePath = normalizeRoutePath(\n fillParams(basePage.routePattern, params),\n );\n\n return {\n ...basePage,\n routePath,\n fileName: fileNameFromRoute(routePath, cleanUrls),\n params,\n };\n });\n}\n\nexport function routeMatch(\n pattern: string,\n urlPath: string,\n): Record<string, string> | null {\n const a = normalizeRoutePath(pattern).split('/').filter(Boolean);\n const b = normalizeRoutePath(urlPath).split('/').filter(Boolean);\n const params: Record<string, string> = {};\n\n for (let i = 0; i < a.length; i++) {\n const patternSeg = a[i];\n const urlSeg = b[i];\n\n if (patternSeg.startsWith('*?:')) {\n params[patternSeg.slice(3)] =\n i < b.length ? b.slice(i).map(decodeURIComponent).join('/') : '';\n return params;\n }\n\n if (patternSeg.startsWith('*:')) {\n const rest = b.slice(i);\n\n if (rest.length === 0) {\n return null;\n }\n\n params[patternSeg.slice(2)] = rest.map(decodeURIComponent).join('/');\n return params;\n }\n\n if (!urlSeg) return null;\n\n if (patternSeg.startsWith(':')) {\n params[patternSeg.slice(1)] = decodeURIComponent(urlSeg);\n continue;\n }\n\n if (patternSeg !== urlSeg) return null;\n }\n\n return a.length === b.length ? params : null;\n}\n\nexport function compareRoutePriority(a: string, b: string): number {\n const aSegs = normalizeRoutePath(a).split('/').filter(Boolean);\n const bSegs = normalizeRoutePath(b).split('/').filter(Boolean);\n const len = Math.max(aSegs.length, bSegs.length);\n\n for (let i = 0; i < len; i++) {\n const aa = aSegs[i];\n const bb = bSegs[i];\n\n if (aa == null) return 1;\n if (bb == null) return -1;\n\n const aOptionalCatchAll = aa.startsWith('*?:');\n const bOptionalCatchAll = bb.startsWith('*?:');\n if (aOptionalCatchAll !== bOptionalCatchAll) {\n return aOptionalCatchAll ? 1 : -1;\n }\n\n const aCatchAll = aa.startsWith('*:');\n const bCatchAll = bb.startsWith('*:');\n if (aCatchAll !== bCatchAll) {\n return aCatchAll ? 1 : -1;\n }\n\n const aDynamic = aa.startsWith(':');\n const bDynamic = bb.startsWith(':');\n if (aDynamic !== bDynamic) {\n return aDynamic ? 1 : -1;\n }\n }\n\n return 0;\n}","export const PLUGIN_NAME = 'vite-plugin-htjs-pages';\nexport const VIRTUAL_BUILD_ENTRY_ID = `\\0${PLUGIN_NAME}:build-entry`;\nexport const VIRTUAL_MANIFEST_ID = `\\0virtual:${PLUGIN_NAME}-manifest`;\nexport const CACHE_DIR_NAME = `node_modules/.cache/${PLUGIN_NAME}`;","import type { HtPageInfo } from './types';\nimport { PLUGIN_NAME } from './constants';\nexport function invalidHtmlReturn(\n page: HtPageInfo,\n value: unknown,\n): Error {\n return new Error(\n `[${PLUGIN_NAME}] Page \"${page.relativePath}\" must resolve to an HTML string, got ${typeof value}`,\n );\n}\n\nexport function missingDefaultExport(page: HtPageInfo): Error {\n return new Error(\n `[${PLUGIN_NAME}] Page \"${page.relativePath}\" does not export a default renderer`,\n );\n}\n\nexport function pageError(page: HtPageInfo, cause: unknown): Error {\n const message = `[${PLUGIN_NAME}] Failed to render \"${page.relativePath}\" at route \"${page.routePath}\"`;\n\n if (cause instanceof Error) {\n const err = new Error(`${message}: ${cause.message}`);\n\n if (cause.stack) {\n err.stack = `${err.stack}\\nCaused by:\\n${cause.stack}`;\n }\n\n return err;\n }\n\n return new Error(`${message}: ${String(cause)}`);\n}","import { invalidHtmlReturn, pageError, missingDefaultExport } from './errors';\nimport type { HtPageInfo, HtPageModule, HtPageRenderContext } from './types';\n\nexport async function renderPage(\n page: HtPageInfo,\n mod: HtPageModule,\n dev = false,\n): Promise<string> {\n const ctx: HtPageRenderContext = {\n page,\n params: page.params,\n dev,\n };\n\n try {\n if (typeof mod.data === 'function') {\n ctx.data = await mod.data(ctx);\n }\n\n const entry = mod.default;\n\n if (entry == null) {\n throw missingDefaultExport(page);\n }\n\n const html = typeof entry === 'function' ? await entry(ctx) : entry;\n\n if (typeof html !== 'string') {\n throw invalidHtmlReturn(page, html);\n }\n\n return html;\n } catch (error) {\n throw pageError(page, error);\n }\n}","import type { ViteDevServer } from 'vite';\nimport { renderPage } from './render-runtime';\nimport { routeMatch } from './route-utils';\nimport type { HtPageInfo, HtPageModule } from './types';\n\nexport function installDevServer(args: {\n server: ViteDevServer;\n getPages: () => Promise<HtPageInfo[]>;\n}): void {\n const { server, getPages } = args;\n\n server.middlewares.use(async (req, res, next) => {\n try {\n if (!req.url || req.method !== 'GET') return next();\n\n const pathname = req.url.split('?')[0];\n const pages = await getPages();\n\n for (const page of pages) {\n const params = routeMatch(page.routePattern, pathname);\n if (!params) continue;\n\n const mod = (await server.ssrLoadModule(\n `/${page.relativePath}`,\n )) as HtPageModule;\n\n const resolvedPage = {\n ...page,\n routePath: pathname || '/',\n params,\n };\n\n const html = await renderPage(resolvedPage, mod, true);\n\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.end(html);\n return;\n }\n\n next();\n } catch (error) {\n next(error);\n }\n });\n}","import {\n compareRoutePriority,\n expandStaticPaths,\n fileNameFromRoute,\n} from './route-utils';\nimport type { HtPageInfo, HtPageModule, StaticParamRecord } from './types';\nimport { PLUGIN_NAME } from './constants';\nexport async function buildPageIndex(args: {\n entries: HtPageInfo[];\n modulesByEntry: Map<string, HtPageModule>;\n cleanUrls: boolean;\n}): Promise<HtPageInfo[]> {\n const { entries, modulesByEntry, cleanUrls } = args;\n const pages: HtPageInfo[] = [];\n\n for (const entry of entries) {\n const mod = modulesByEntry.get(entry.entryPath) ?? {};\n\n if (entry.dynamic) {\n const rows = mod.generateStaticParams\n ? await mod.generateStaticParams()\n : [];\n\n pages.push(\n ...expandStaticPaths(\n {\n id: entry.id,\n entryPath: entry.entryPath,\n absolutePath: entry.absolutePath,\n relativePath: entry.relativePath,\n routePattern: entry.routePattern,\n dynamic: entry.dynamic,\n paramNames: entry.paramNames,\n } as Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,\n rows as StaticParamRecord[],\n cleanUrls,\n ),\n );\n } else {\n pages.push({\n ...entry,\n routePath: entry.routePattern,\n fileName: fileNameFromRoute(entry.routePattern, cleanUrls),\n params: {},\n });\n }\n }\n\n pages.sort((a, b) => compareRoutePriority(a.routePattern, b.routePattern));\n\n const seenRoutes = new Map<string, HtPageInfo>();\n\n for (const page of pages) {\n const existing = seenRoutes.get(page.routePath);\n\n if (existing) {\n throw new Error(\n `[${PLUGIN_NAME}] Duplicate route generated: \"${page.routePath}\" from \"${existing.relativePath}\" and \"${page.relativePath}\"`,\n );\n }\n\n seenRoutes.set(page.routePath, page);\n }\n\n return pages;\n}","import path from 'node:path';\nimport fs from 'node:fs/promises';\nimport { createHash } from 'node:crypto';\nimport { rollup, type Plugin as RollupPlugin } from 'rollup';\nimport { nodeResolve } from '@rollup/plugin-node-resolve';\nimport { createManifestModule } from './manifest';\nimport type { HtPageInfo } from './types';\nimport { PLUGIN_NAME, VIRTUAL_MANIFEST_ID } from './constants';\n\n\nexport async function buildRenderBundle(args: {\n entries: HtPageInfo[];\n cacheDir: string;\n ssrPlugins?: RollupPlugin[];\n}): Promise<string> {\n const { entries, cacheDir, ssrPlugins = [] } = args;\n\n const source = createManifestModule(entries);\n const hash = createHash('sha256').update(source).digest('hex').slice(0, 12);\n const bundlePath = path.join(cacheDir, `render-${hash}.mjs`);\n\n await fs.mkdir(cacheDir, { recursive: true });\n\n try {\n await fs.access(bundlePath);\n return bundlePath;\n } catch {\n // cache miss, continue\n }\n\n const bundle = await rollup({\n input: VIRTUAL_MANIFEST_ID,\n plugins: [\n {\n name: `${PLUGIN_NAME}:virtual-manifest`,\n resolveId(id) {\n return id === VIRTUAL_MANIFEST_ID ? id : null;\n },\n load(id) {\n return id === VIRTUAL_MANIFEST_ID ? source : null;\n },\n },\n nodeResolve({\n preferBuiltins: true,\n exportConditions: ['node'],\n }),\n ...ssrPlugins,\n ],\n treeshake: true,\n });\n\n try {\n const { output } = await bundle.generate({\n format: 'esm',\n exports: 'named',\n inlineDynamicImports: true,\n });\n\n const chunk = output.find((item) => item.type === 'chunk');\n\n if (!chunk || chunk.type !== 'chunk') {\n throw new Error(`[${PLUGIN_NAME}] Failed to generate HT.js pages render bundle.`);\n }\n\n await fs.writeFile(bundlePath, chunk.code, 'utf8');\n return bundlePath;\n } finally {\n await bundle.close();\n }\n}","import type { HtPageInfo } from './types';\n\nfunction js(value: unknown): string {\n return JSON.stringify(value);\n}\n\nexport function createManifestModule(entries: HtPageInfo[]): string {\n const imports = entries\n .map((page, i) => `import * as page${i} from ${js(page.entryPath)};`)\n .join('\\n');\n\n const records = entries\n .map(\n (page, i) => `{\n page: ${js(page)},\n mod: page${i}\n}`,\n )\n .join(',\\n');\n\n return `${imports}\n\nexport const manifest = [\n${records}\n];\n`;\n}"],"mappings":"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","names":["path","createHash","path","path","path","path","createHash","path"]}
1
+ {"version":3,"sources":["../src/plugin.ts","../src/discover.ts","../src/path-utils.ts","../src/route-utils.ts","../src/constants.ts","../src/errors.ts","../src/render-runtime.ts","../src/dev-server.ts","../src/page-index.ts","../src/render-bundle.ts","../src/manifest.ts"],"sourcesContent":["import path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { createHash } from 'node:crypto';\nimport pLimit from 'p-limit';\nimport type { Plugin, ViteDevServer } from 'vite';\n\nimport { discoverEntryPages } from './discover';\nimport { installDevServer } from './dev-server';\nimport { buildPageIndex } from './page-index';\nimport { buildRenderBundle } from './render-bundle';\nimport { renderPage } from './render-runtime';\n\nimport type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';\nimport { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID, CACHE_DIR_NAME } from './constants';\n\nfunction chunkArray<T>(items: T[], size: number): T[][] {\n const safeSize = Math.max(1, Math.floor(size));\n const out: T[][] = [];\n for (let i = 0; i < items.length; i += safeSize) {\n out.push(items.slice(i, i + safeSize));\n }\n return out;\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\nfunction createEntriesKey(entries: HtPageInfo[]): string {\n const raw = entries\n .map((e) => `${e.entryPath}|${e.routePattern}|${e.dynamic}`)\n .join('\\n');\n\n return createHash('sha256').update(raw).digest('hex');\n}\n\nasync function importManifest(\n bundlePath: string,\n): Promise<Array<{ page: HtPageInfo; mod: HtPageModule }>> {\n const mod = await import(pathToFileURL(bundlePath).href + `?t=${Date.now()}`);\n return mod.manifest as Array<{ page: HtPageInfo; mod: HtPageModule }>;\n}\n\nexport function htPages(options: HtPagesPluginOptions = {}): Plugin {\n let root = process.cwd();\n let server: ViteDevServer | null = null;\n let devPages: HtPageInfo[] = [];\n\n let cachedManifestKey: string | null = null;\n let cachedBundlePath: string | null = null;\n let loadDevPagesInFlight: Promise<HtPageInfo[]> | null = null;\n\n const cleanUrls = options.cleanUrls ?? true;\n\n function logDebug(enabled: boolean | undefined, ...args: unknown[]) {\n if (!enabled) return;\n console.log(`[${PLUGIN_NAME}]`, ...args);\n }\n\n async function loadDevPages(): Promise<HtPageInfo[]> {\n if (loadDevPagesInFlight) return loadDevPagesInFlight;\n loadDevPagesInFlight = doLoadDevPages();\n try {\n return await loadDevPagesInFlight;\n } finally {\n loadDevPagesInFlight = null;\n }\n }\n\n async function doLoadDevPages(): Promise<HtPageInfo[]> {\n const entries = await discoverEntryPages(root, options);\n const modulesByEntry = new Map<string, HtPageModule>();\n\n logDebug(options.debug, 'discovered entries', entries.map((e) => e.relativePath));\n\n if (!server) return [];\n\n for (const entry of entries) {\n const mod = (await server.ssrLoadModule(\n `/${entry.relativePath}`,\n )) as HtPageModule;\n\n modulesByEntry.set(entry.entryPath, mod);\n }\n\n devPages = await buildPageIndex({\n entries,\n modulesByEntry,\n cleanUrls,\n });\n\n logDebug(\n options.debug,\n 'dev pages',\n devPages.map((p) => `${p.routePath} -> ${p.relativePath}`),\n );\n\n return devPages;\n }\n\n async function buildPagesPipeline() {\n const entries = await discoverEntryPages(root, options);\n const cacheDir = path.join(root, CACHE_DIR_NAME);\n\n const entriesKey = createEntriesKey(entries);\n\n let bundlePath: string;\n if (cachedBundlePath && cachedManifestKey === entriesKey) {\n bundlePath = cachedBundlePath;\n } else {\n bundlePath = await buildRenderBundle({\n entries,\n cacheDir,\n ssrPlugins: options.ssrPlugins,\n });\n cachedManifestKey = entriesKey;\n cachedBundlePath = bundlePath;\n }\n\n logDebug(options.debug, 'render bundle', bundlePath);\n\n const manifest = await importManifest(bundlePath);\n const modulesByEntry = new Map<string, HtPageModule>();\n\n for (const rec of manifest) {\n modulesByEntry.set(rec.page.entryPath, rec.mod);\n }\n\n const pages = await buildPageIndex({\n entries,\n modulesByEntry,\n cleanUrls,\n });\n\n // Ensure static hosts get a 404.html\n const notFoundPage = pages.find((p) => p.routePath === '/404');\n\n if (notFoundPage && !pages.some((p) => p.fileName === '404.html')) {\n pages.push({\n ...notFoundPage,\n fileName: '404.html',\n });\n } \n\n return { entries, bundlePath, modulesByEntry, pages };\n }\n\n return {\n name: PLUGIN_NAME,\n\n config(userConfig, env) {\n if (env.command !== 'build') return;\n\n const hasExplicitInput = userConfig.build?.rollupOptions?.input != null;\n if (hasExplicitInput) return;\n\n return {\n build: {\n rollupOptions: {\n input: VIRTUAL_BUILD_ENTRY_ID,\n },\n },\n };\n },\n\n resolveId(id) {\n if (id === VIRTUAL_BUILD_ENTRY_ID) return id;\n return null;\n },\n\n load(id) {\n if (id === VIRTUAL_BUILD_ENTRY_ID) {\n return 'export default {};';\n }\n return null;\n },\n\n configResolved(resolved) {\n root = resolved.root;\n },\n\n async buildStart() {\n const entries = await discoverEntryPages(root, options);\n\n for (const entry of entries) {\n this.addWatchFile(entry.entryPath);\n }\n },\n\n configureServer(_server) {\n server = _server;\n\n installDevServer({\n server,\n getPages: async () => {\n if (devPages.length > 0) return devPages;\n return loadDevPages();\n },\n });\n\n loadDevPages().catch((error) => {\n server?.config.logger.error(\n `[${PLUGIN_NAME}] loadDevPages failed: ${\n error instanceof Error ? error.stack ?? error.message : String(error)\n }`,\n );\n });\n },\n\n async handleHotUpdate(ctx) {\n if (!server) return;\n \n const file = ctx.file;\n \n if (\n file.endsWith('.ht.js') ||\n file.includes('/templates/')\n ) {\n logDebug(options.debug, 'reindex triggered by', file);\n await loadDevPages();\n }\n },\n\n async generateBundle(_, bundle) {\n const { modulesByEntry, pages } = await buildPagesPipeline();\n \n logDebug(options.debug, 'emitting pages', pages.map((p) => p.fileName));\n \n const concurrency = Math.max(1, options.renderConcurrency ?? 8);\n const limit = pLimit(concurrency);\n const batchSize = Math.max(\n 1,\n options.renderBatchSize ?? Math.max(concurrency, 32),\n );\n \n for (const batch of chunkArray(pages, batchSize)) {\n await Promise.all(\n batch.map((page) =>\n limit(async () => {\n const mod = modulesByEntry.get(page.entryPath);\n \n if (!mod) {\n throw new Error(\n `[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,\n );\n }\n \n const html = await renderPage(page, mod, false);\n \n this.emitFile({\n type: 'asset',\n fileName: options.mapOutputPath?.(page) ?? page.fileName,\n source: html,\n });\n }),\n ),\n );\n }\n \n // Generate sitemap.xml\n const sitemapBase = options.site ?? '';\n const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))]\n .filter((route) => !route.includes(':') && !route.includes('*'));\n \n if (sitemapRoutes.length > 0) {\n const sitemap = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n ${sitemapRoutes\n .map(\n (route) =>\n ` <url><loc>${escapeXml(sitemapBase)}${escapeXml(route)}</loc></url>`,\n )\n .join('\\n')}\n </urlset>\n `;\n \n this.emitFile({\n type: 'asset',\n fileName: 'sitemap.xml',\n source: sitemap,\n });\n }\n \n // Generate rss.xml\n if (options.rss?.site) {\n const routePrefix = options.rss.routePrefix ?? '/blog';\n \n const rssItems = pages\n .filter((page) => page.routePath.startsWith(routePrefix))\n .map((page) => {\n const url = `${options.rss!.site}${page.routePath}`;\n return ` <item>\n <title>${escapeXml(page.routePath)}</title>\n <link>${escapeXml(url)}</link>\n <guid>${escapeXml(url)}</guid>\n </item>`;\n })\n .join('\\n');\n \n const rss = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <rss version=\"2.0\">\n <channel>\n <title>${escapeXml(options.rss.title ?? PLUGIN_NAME)}</title>\n <link>${escapeXml(options.rss.site)}</link>\n <description>${escapeXml(options.rss.description ?? 'RSS feed')}</description>\n ${rssItems}\n </channel>\n </rss>\n `;\n \n this.emitFile({\n type: 'asset',\n fileName: 'rss.xml',\n source: rss,\n });\n }\n \n // Remove the dummy virtual build entry chunk\n for (const [fileName, output] of Object.entries(bundle)) {\n if (\n output.type === 'chunk' &&\n output.facadeModuleId === VIRTUAL_BUILD_ENTRY_ID\n ) {\n delete bundle[fileName];\n }\n }\n } \n\n };\n}\n","import path from 'node:path';\nimport fg from 'fast-glob';\nimport { normalizeFsPath, toPosix } from './path-utils';\nimport { getParamNames, isDynamicPage, toRoutePattern } from './route-utils';\nimport type { HtPageInfo, HtPagesPluginOptions } from './types';\nimport { PLUGIN_NAME } from './constants';\n\nexport async function discoverEntryPages(\n root: string,\n options: HtPagesPluginOptions,\n): Promise<HtPageInfo[]> {\n const rawInclude = Array.isArray(options.include)\n ? options.include\n : [options.include ?? 'src/**/*.ht.js'];\n let include = rawInclude.filter((p): p is string => typeof p === 'string' && p.length > 0);\n if (include.length === 0) {\n include = ['src/**/*.ht.js'];\n }\n\n const exclude = Array.isArray(options.exclude)\n ? options.exclude\n : options.exclude\n ? [options.exclude]\n : [];\n\n const pagesDir = options.pagesDir ?? 'src';\n const pagesRoot = normalizeFsPath(path.join(root, pagesDir));\n\n const files = await fg(include, {\n cwd: root,\n ignore: exclude,\n absolute: true,\n });\n\n return files\n .sort()\n .map((absolutePath) => {\n const entryPath = normalizeFsPath(absolutePath);\n const relativePath = toPosix(path.relative(root, entryPath));\n const relativeFromPagesDir = toPosix(path.relative(pagesRoot, entryPath));\n\n if (\n relativeFromPagesDir.startsWith('../') ||\n relativeFromPagesDir === '..'\n ) {\n throw new Error(\n `[${PLUGIN_NAME}] Page is outside pagesDir: ${entryPath} (pagesDir: ${pagesDir})`,\n );\n }\n\n const dynamic = isDynamicPage(relativeFromPagesDir);\n const routePattern = toRoutePattern(relativeFromPagesDir);\n\n return {\n id: entryPath,\n entryPath,\n absolutePath: entryPath,\n relativePath,\n routePattern,\n routePath: routePattern,\n fileName: '',\n dynamic,\n paramNames: getParamNames(relativeFromPagesDir),\n params: {},\n } satisfies HtPageInfo;\n });\n}","import path from 'node:path';\n\nexport function toPosix(p: string): string {\n return p.split(path.sep).join('/');\n}\n\nexport function stripHtSuffix(file: string): string {\n return file.replace(/\\.ht\\.js$/i, '');\n}\n\nexport function normalizeRoutePath(p: string): string {\n let out = p.startsWith('/') ? p : `/${p}`;\n out = out.replace(/\\/+/g, '/');\n if (out !== '/' && out.endsWith('/')) out = out.slice(0, -1);\n return out;\n}\n\nexport function normalizeFsPath(p: string): string {\n return toPosix(path.resolve(p));\n}","import { normalizeRoutePath, stripHtSuffix, toPosix } from './path-utils';\nimport type { HtPageInfo, StaticParamRecord } from './types';\n\nfunction safeDecodeURIComponent(str: string): string {\n try {\n return decodeURIComponent(str);\n } catch {\n return str;\n }\n}\n\nconst DYNAMIC_SEGMENT_RE = /\\[([A-Za-z0-9_]+)\\]/g;\nconst CATCH_ALL_SEGMENT_RE = /\\[\\.\\.\\.([A-Za-z0-9_]+)\\]/g;\nconst OPTIONAL_CATCH_ALL_SEGMENT_RE = /\\[\\.\\.\\.([A-Za-z0-9_]+)\\]\\?/g;\nconst ANY_PARAM_RE = /\\[(?:\\.\\.\\.)?([A-Za-z0-9_]+)\\]\\??/g;\nconst ROUTE_GROUP_RE = /(^|\\/)\\(([^)]+)\\)(?=\\/|$)/g;\n\nexport function getParamNames(relativeFromPagesDir: string): string[] {\n return [...relativeFromPagesDir.matchAll(ANY_PARAM_RE)].map((m) => m[1]);\n}\n\nexport function isDynamicPage(relativeFromPagesDir: string): boolean {\n return /\\[(?:\\.\\.\\.)?[A-Za-z0-9_]+\\]\\??/.test(relativeFromPagesDir);\n}\n\nexport function toRoutePattern(relativeFromPagesDir: string): string {\n const noExt = stripHtSuffix(toPosix(relativeFromPagesDir));\n\n const withoutGroups = noExt.replace(ROUTE_GROUP_RE, '$1');\n const withoutIndex = withoutGroups.replace(/\\/index$/i, '').replace(/^index$/i, '');\n\n const raw = withoutIndex\n .replace(OPTIONAL_CATCH_ALL_SEGMENT_RE, '*?:$1')\n .replace(CATCH_ALL_SEGMENT_RE, '*:$1')\n .replace(DYNAMIC_SEGMENT_RE, ':$1');\n\n return normalizeRoutePath(raw || '/');\n}\n\nexport function fillParams(\n pattern: string,\n params: Record<string, string>,\n): string {\n const result = pattern\n .replace(/\\*\\?:([A-Za-z0-9_]+)/g, (_, key) => {\n const value = params[key];\n if (value == null || value === '') {\n return '';\n }\n\n return String(value)\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/');\n })\n .replace(/\\*:([A-Za-z0-9_]+)/g, (_, key) => {\n if (!(key in params)) {\n throw new Error(`Missing catch-all route param \"${key}\"`);\n }\n\n return String(params[key])\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/');\n })\n .replace(/:([A-Za-z0-9_]+)/g, (_, key) => {\n if (!(key in params)) {\n throw new Error(`Missing route param \"${key}\"`);\n }\n\n return encodeURIComponent(params[key]);\n });\n\n return normalizeRoutePath(result || '/');\n}\n\nexport function fileNameFromRoute(\n routePath: string,\n cleanUrls: boolean,\n): string {\n const normalized = normalizeRoutePath(routePath);\n\n if (normalized === '/') return 'index.html';\n\n const base = normalized.slice(1);\n return cleanUrls ? `${base}/index.html` : `${base}.html`;\n}\n\nexport function expandStaticPaths(\n basePage: Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,\n rows: StaticParamRecord[],\n cleanUrls: boolean,\n): HtPageInfo[] {\n return rows.map((row) => {\n const params = Object.fromEntries(\n Object.entries(row).map(([k, v]) => [k, String(v)]),\n );\n\n const routePath = fillParams(basePage.routePattern, params);\n\n return {\n ...basePage,\n routePath,\n fileName: fileNameFromRoute(routePath, cleanUrls),\n params,\n };\n });\n}\n\nexport function routeMatch(\n pattern: string,\n urlPath: string,\n): Record<string, string> | null {\n const a = normalizeRoutePath(pattern).split('/').filter(Boolean);\n const b = normalizeRoutePath(urlPath).split('/').filter(Boolean);\n const params: Record<string, string> = {};\n\n for (let i = 0; i < a.length; i++) {\n const patternSeg = a[i];\n const urlSeg = b[i];\n\n if (patternSeg.startsWith('*?:')) {\n params[patternSeg.slice(3)] =\n i < b.length ? b.slice(i).map(safeDecodeURIComponent).join('/') : '';\n return params;\n }\n\n if (patternSeg.startsWith('*:')) {\n const rest = b.slice(i);\n if (rest.length === 0) return null;\n\n params[patternSeg.slice(2)] = rest.map(safeDecodeURIComponent).join('/');\n return params;\n }\n\n if (!urlSeg) return null;\n\n if (patternSeg.startsWith(':')) {\n params[patternSeg.slice(1)] = safeDecodeURIComponent(urlSeg);\n continue;\n }\n\n if (patternSeg !== urlSeg) return null;\n }\n\n return a.length === b.length ? params : null;\n}\n\nexport function compareRoutePriority(a: string, b: string): number {\n const aSegs = normalizeRoutePath(a).split('/').filter(Boolean);\n const bSegs = normalizeRoutePath(b).split('/').filter(Boolean);\n const len = Math.max(aSegs.length, bSegs.length);\n\n for (let i = 0; i < len; i++) {\n const aa = aSegs[i];\n const bb = bSegs[i];\n\n if (aa == null) return 1;\n if (bb == null) return -1;\n\n const aOptionalCatchAll = aa.startsWith('*?:');\n const bOptionalCatchAll = bb.startsWith('*?:');\n if (aOptionalCatchAll !== bOptionalCatchAll) {\n return aOptionalCatchAll ? 1 : -1;\n }\n\n const aCatchAll = aa.startsWith('*:');\n const bCatchAll = bb.startsWith('*:');\n if (aCatchAll !== bCatchAll) {\n return aCatchAll ? 1 : -1;\n }\n\n const aDynamic = aa.startsWith(':');\n const bDynamic = bb.startsWith(':');\n if (aDynamic !== bDynamic) {\n return aDynamic ? 1 : -1;\n }\n }\n\n // More specific / longer routes first when otherwise equal\n return bSegs.length - aSegs.length;\n}","export const PLUGIN_NAME = 'vite-plugin-htjs-pages';\nexport const VIRTUAL_BUILD_ENTRY_ID = `\\0${PLUGIN_NAME}:build-entry`;\nexport const VIRTUAL_MANIFEST_ID = `\\0virtual:${PLUGIN_NAME}-manifest`;\nexport const CACHE_DIR_NAME = `node_modules/.cache/${PLUGIN_NAME}`;","import type { HtPageInfo } from './types';\nimport { PLUGIN_NAME } from './constants';\nexport function invalidHtmlReturn(\n page: HtPageInfo,\n value: unknown,\n): Error {\n return new Error(\n `[${PLUGIN_NAME}] Page \"${page.relativePath}\" must resolve to an HTML string, got ${typeof value}`,\n );\n}\n\nexport function missingDefaultExport(page: HtPageInfo): Error {\n return new Error(\n `[${PLUGIN_NAME}] Page \"${page.relativePath}\" does not export a default renderer`,\n );\n}\n\nexport function pageError(page: HtPageInfo, cause: unknown): Error {\n const message = `[${PLUGIN_NAME}] Failed to render \"${page.relativePath}\" at route \"${page.routePath}\"`;\n\n if (cause instanceof Error) {\n const err = new Error(`${message}: ${cause.message}`);\n\n if (cause.stack) {\n err.stack = `${err.stack}\\nCaused by:\\n${cause.stack}`;\n }\n\n return err;\n }\n\n return new Error(`${message}: ${String(cause)}`);\n}","import { invalidHtmlReturn, pageError, missingDefaultExport } from './errors';\nimport type { HtPageInfo, HtPageModule, HtPageRenderContext } from './types';\n\nexport async function renderPage(\n page: HtPageInfo,\n mod: HtPageModule,\n dev = false,\n): Promise<string> {\n const ctx: HtPageRenderContext = {\n page,\n params: page.params,\n dev,\n };\n\n try {\n if (typeof mod.data === 'function') {\n ctx.data = await mod.data(ctx);\n }\n\n const entry = mod.default;\n\n if (entry == null) {\n throw missingDefaultExport(page);\n }\n\n const html = typeof entry === 'function' ? await entry(ctx) : entry;\n\n if (typeof html !== 'string') {\n throw invalidHtmlReturn(page, html);\n }\n\n return html;\n } catch (error) {\n throw pageError(page, error);\n }\n}","import type { ViteDevServer } from 'vite';\nimport { renderPage } from './render-runtime';\nimport { routeMatch } from './route-utils';\nimport type { HtPageInfo, HtPageModule } from './types';\n\nexport function installDevServer(args: {\n server: ViteDevServer;\n getPages: () => Promise<HtPageInfo[]>;\n}): void {\n const { server, getPages } = args;\n\n server.middlewares.use(async (req, res, next) => {\n try {\n if (!req.url || req.method !== 'GET') return next();\n\n const pathname = req.url.split('?')[0];\n const pages = await getPages();\n\n for (const page of pages) {\n const params = routeMatch(page.routePattern, pathname);\n if (!params) continue;\n\n const mod = (await server.ssrLoadModule(\n `/${page.relativePath}`,\n )) as HtPageModule;\n\n const resolvedPage = {\n ...page,\n routePath: pathname || '/',\n params,\n };\n\n const html = await renderPage(resolvedPage, mod, true);\n\n res.statusCode = 200;\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.end(html);\n return;\n }\n\n next();\n } catch (error) {\n next(error);\n }\n });\n}","import {\n compareRoutePriority,\n expandStaticPaths,\n fileNameFromRoute,\n} from './route-utils';\nimport type { HtPageInfo, HtPageModule, StaticParamRecord } from './types';\nimport { PLUGIN_NAME } from './constants';\nexport async function buildPageIndex(args: {\n entries: HtPageInfo[];\n modulesByEntry: Map<string, HtPageModule>;\n cleanUrls: boolean;\n}): Promise<HtPageInfo[]> {\n const { entries, modulesByEntry, cleanUrls } = args;\n const pages: HtPageInfo[] = [];\n\n for (const entry of entries) {\n const mod = modulesByEntry.get(entry.entryPath) ?? {};\n\n if (entry.dynamic) {\n const rows =\n (mod.generateStaticParams\n ? await mod.generateStaticParams()\n : []) ?? [];\n\n pages.push(\n ...expandStaticPaths(\n {\n id: entry.id,\n entryPath: entry.entryPath,\n absolutePath: entry.absolutePath,\n relativePath: entry.relativePath,\n routePattern: entry.routePattern,\n dynamic: entry.dynamic,\n paramNames: entry.paramNames,\n } as Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,\n Array.isArray(rows) ? rows : [],\n cleanUrls,\n ),\n );\n } else {\n pages.push({\n ...entry,\n routePath: entry.routePattern,\n fileName: fileNameFromRoute(entry.routePattern, cleanUrls),\n params: {},\n });\n }\n }\n\n pages.sort((a, b) => compareRoutePriority(a.routePattern, b.routePattern));\n\n const seenRoutes = new Map<string, HtPageInfo>();\n\n for (const page of pages) {\n const existing = seenRoutes.get(page.routePath);\n\n if (existing) {\n throw new Error(\n `[${PLUGIN_NAME}] Duplicate route generated: \"${page.routePath}\" from \"${existing.relativePath}\" and \"${page.relativePath}\"`,\n );\n }\n\n seenRoutes.set(page.routePath, page);\n }\n\n return pages;\n}","import path from 'node:path';\nimport fs from 'node:fs/promises';\nimport { createHash } from 'node:crypto';\nimport { rollup, type Plugin as RollupPlugin } from 'rollup';\nimport { nodeResolve } from '@rollup/plugin-node-resolve';\nimport { createManifestModule } from './manifest';\nimport type { HtPageInfo } from './types';\nimport { PLUGIN_NAME, VIRTUAL_MANIFEST_ID } from './constants';\n\n\nexport async function buildRenderBundle(args: {\n entries: HtPageInfo[];\n cacheDir: string;\n ssrPlugins?: RollupPlugin[];\n}): Promise<string> {\n const { entries, cacheDir, ssrPlugins = [] } = args;\n\n const source = createManifestModule(entries);\n const hash = createHash('sha256').update(source).digest('hex').slice(0, 12);\n const bundlePath = path.join(cacheDir, `render-${hash}.mjs`);\n\n await fs.mkdir(cacheDir, { recursive: true });\n\n try {\n await fs.access(bundlePath);\n return bundlePath;\n } catch {\n // cache miss, continue\n }\n\n const bundle = await rollup({\n input: VIRTUAL_MANIFEST_ID,\n plugins: [\n {\n name: `${PLUGIN_NAME}:virtual-manifest`,\n resolveId(id) {\n return id === VIRTUAL_MANIFEST_ID ? id : null;\n },\n load(id) {\n return id === VIRTUAL_MANIFEST_ID ? source : null;\n },\n },\n nodeResolve({\n preferBuiltins: true,\n exportConditions: ['node'],\n }),\n ...ssrPlugins,\n ],\n treeshake: true,\n });\n\n try {\n const { output } = await bundle.generate({\n format: 'esm',\n exports: 'named',\n inlineDynamicImports: true,\n });\n\n const chunk = output.find((item) => item.type === 'chunk');\n\n if (!chunk || chunk.type !== 'chunk') {\n throw new Error(`[${PLUGIN_NAME}] Failed to generate HT.js pages render bundle.`);\n }\n\n await fs.writeFile(bundlePath, chunk.code, 'utf8');\n return bundlePath;\n } finally {\n await bundle.close();\n }\n}","import type { HtPageInfo } from './types';\n\nfunction js(value: unknown): string {\n return JSON.stringify(value);\n}\n\nexport function createManifestModule(entries: HtPageInfo[]): string {\n const imports = entries\n .map((page, i) => `import * as page${i} from ${js(page.entryPath)};`)\n .join('\\n');\n\n const records = entries\n .map(\n (page, i) => `{\n page: ${js(page)},\n mod: page${i}\n}`,\n )\n .join(',\\n');\n\n return `${imports}\n\nexport const manifest = [\n${records}\n];\n`;\n}"],"mappings":"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","names":["path","createHash","path","path","path","path","createHash","path"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-htjs-pages",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "author": "Paul Browne",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/discover.ts CHANGED
@@ -9,9 +9,13 @@ export async function discoverEntryPages(
9
9
  root: string,
10
10
  options: HtPagesPluginOptions,
11
11
  ): Promise<HtPageInfo[]> {
12
- const include = Array.isArray(options.include)
12
+ const rawInclude = Array.isArray(options.include)
13
13
  ? options.include
14
14
  : [options.include ?? 'src/**/*.ht.js'];
15
+ let include = rawInclude.filter((p): p is string => typeof p === 'string' && p.length > 0);
16
+ if (include.length === 0) {
17
+ include = ['src/**/*.ht.js'];
18
+ }
15
19
 
16
20
  const exclude = Array.isArray(options.exclude)
17
21
  ? options.exclude
package/src/page-index.ts CHANGED
@@ -17,9 +17,10 @@ export async function buildPageIndex(args: {
17
17
  const mod = modulesByEntry.get(entry.entryPath) ?? {};
18
18
 
19
19
  if (entry.dynamic) {
20
- const rows = mod.generateStaticParams
21
- ? await mod.generateStaticParams()
22
- : [];
20
+ const rows =
21
+ (mod.generateStaticParams
22
+ ? await mod.generateStaticParams()
23
+ : []) ?? [];
23
24
 
24
25
  pages.push(
25
26
  ...expandStaticPaths(
@@ -32,7 +33,7 @@ export async function buildPageIndex(args: {
32
33
  dynamic: entry.dynamic,
33
34
  paramNames: entry.paramNames,
34
35
  } as Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,
35
- rows as StaticParamRecord[],
36
+ Array.isArray(rows) ? rows : [],
36
37
  cleanUrls,
37
38
  ),
38
39
  );
package/src/plugin.ts CHANGED
@@ -14,13 +14,23 @@ import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
14
14
  import { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID, CACHE_DIR_NAME } from './constants';
15
15
 
16
16
  function chunkArray<T>(items: T[], size: number): T[][] {
17
+ const safeSize = Math.max(1, Math.floor(size));
17
18
  const out: T[][] = [];
18
- for (let i = 0; i < items.length; i += size) {
19
- out.push(items.slice(i, i + size));
19
+ for (let i = 0; i < items.length; i += safeSize) {
20
+ out.push(items.slice(i, i + safeSize));
20
21
  }
21
22
  return out;
22
23
  }
23
24
 
25
+ function escapeXml(text: string): string {
26
+ return text
27
+ .replace(/&/g, '&amp;')
28
+ .replace(/</g, '&lt;')
29
+ .replace(/>/g, '&gt;')
30
+ .replace(/"/g, '&quot;')
31
+ .replace(/'/g, '&apos;');
32
+ }
33
+
24
34
  function createEntriesKey(entries: HtPageInfo[]): string {
25
35
  const raw = entries
26
36
  .map((e) => `${e.entryPath}|${e.routePattern}|${e.dynamic}`)
@@ -43,6 +53,7 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
43
53
 
44
54
  let cachedManifestKey: string | null = null;
45
55
  let cachedBundlePath: string | null = null;
56
+ let loadDevPagesInFlight: Promise<HtPageInfo[]> | null = null;
46
57
 
47
58
  const cleanUrls = options.cleanUrls ?? true;
48
59
 
@@ -52,6 +63,16 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
52
63
  }
53
64
 
54
65
  async function loadDevPages(): Promise<HtPageInfo[]> {
66
+ if (loadDevPagesInFlight) return loadDevPagesInFlight;
67
+ loadDevPagesInFlight = doLoadDevPages();
68
+ try {
69
+ return await loadDevPagesInFlight;
70
+ } finally {
71
+ loadDevPagesInFlight = null;
72
+ }
73
+ }
74
+
75
+ async function doLoadDevPages(): Promise<HtPageInfo[]> {
55
76
  const entries = await discoverEntryPages(root, options);
56
77
  const modulesByEntry = new Map<string, HtPageModule>();
57
78
 
@@ -210,10 +231,12 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
210
231
 
211
232
  logDebug(options.debug, 'emitting pages', pages.map((p) => p.fileName));
212
233
 
213
- const limit = pLimit(options.renderConcurrency ?? 8);
214
- const batchSize =
215
- options.renderBatchSize ??
216
- Math.max(options.renderConcurrency ?? 8, 32);
234
+ const concurrency = Math.max(1, options.renderConcurrency ?? 8);
235
+ const limit = pLimit(concurrency);
236
+ const batchSize = Math.max(
237
+ 1,
238
+ options.renderBatchSize ?? Math.max(concurrency, 32),
239
+ );
217
240
 
218
241
  for (const batch of chunkArray(pages, batchSize)) {
219
242
  await Promise.all(
@@ -248,7 +271,10 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
248
271
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
249
272
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
250
273
  ${sitemapRoutes
251
- .map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`)
274
+ .map(
275
+ (route) =>
276
+ ` <url><loc>${escapeXml(sitemapBase)}${escapeXml(route)}</loc></url>`,
277
+ )
252
278
  .join('\n')}
253
279
  </urlset>
254
280
  `;
@@ -269,9 +295,9 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
269
295
  .map((page) => {
270
296
  const url = `${options.rss!.site}${page.routePath}`;
271
297
  return ` <item>
272
- <title>${page.routePath}</title>
273
- <link>${url}</link>
274
- <guid>${url}</guid>
298
+ <title>${escapeXml(page.routePath)}</title>
299
+ <link>${escapeXml(url)}</link>
300
+ <guid>${escapeXml(url)}</guid>
275
301
  </item>`;
276
302
  })
277
303
  .join('\n');
@@ -279,9 +305,9 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
279
305
  const rss = `<?xml version="1.0" encoding="UTF-8"?>
280
306
  <rss version="2.0">
281
307
  <channel>
282
- <title>${options.rss.title ?? PLUGIN_NAME}</title>
283
- <link>${options.rss.site}</link>
284
- <description>${options.rss.description ?? 'RSS feed'}</description>
308
+ <title>${escapeXml(options.rss.title ?? PLUGIN_NAME)}</title>
309
+ <link>${escapeXml(options.rss.site)}</link>
310
+ <description>${escapeXml(options.rss.description ?? 'RSS feed')}</description>
285
311
  ${rssItems}
286
312
  </channel>
287
313
  </rss>
@@ -1,10 +1,19 @@
1
1
  import { normalizeRoutePath, stripHtSuffix, toPosix } from './path-utils';
2
2
  import type { HtPageInfo, StaticParamRecord } from './types';
3
3
 
4
+ function safeDecodeURIComponent(str: string): string {
5
+ try {
6
+ return decodeURIComponent(str);
7
+ } catch {
8
+ return str;
9
+ }
10
+ }
11
+
4
12
  const DYNAMIC_SEGMENT_RE = /\[([A-Za-z0-9_]+)\]/g;
5
13
  const CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]/g;
6
14
  const OPTIONAL_CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]\?/g;
7
15
  const ANY_PARAM_RE = /\[(?:\.\.\.)?([A-Za-z0-9_]+)\]\??/g;
16
+ const ROUTE_GROUP_RE = /(^|\/)\(([^)]+)\)(?=\/|$)/g;
8
17
 
9
18
  export function getParamNames(relativeFromPagesDir: string): string[] {
10
19
  return [...relativeFromPagesDir.matchAll(ANY_PARAM_RE)].map((m) => m[1]);
@@ -17,8 +26,10 @@ export function isDynamicPage(relativeFromPagesDir: string): boolean {
17
26
  export function toRoutePattern(relativeFromPagesDir: string): string {
18
27
  const noExt = stripHtSuffix(toPosix(relativeFromPagesDir));
19
28
 
20
- const raw = noExt
21
- .replace(/(^|\/)index$/i, '$1')
29
+ const withoutGroups = noExt.replace(ROUTE_GROUP_RE, '$1');
30
+ const withoutIndex = withoutGroups.replace(/\/index$/i, '').replace(/^index$/i, '');
31
+
32
+ const raw = withoutIndex
22
33
  .replace(OPTIONAL_CATCH_ALL_SEGMENT_RE, '*?:$1')
23
34
  .replace(CATCH_ALL_SEGMENT_RE, '*:$1')
24
35
  .replace(DYNAMIC_SEGMENT_RE, ':$1');
@@ -30,10 +41,9 @@ export function fillParams(
30
41
  pattern: string,
31
42
  params: Record<string, string>,
32
43
  ): string {
33
- return pattern
44
+ const result = pattern
34
45
  .replace(/\*\?:([A-Za-z0-9_]+)/g, (_, key) => {
35
46
  const value = params[key];
36
-
37
47
  if (value == null || value === '') {
38
48
  return '';
39
49
  }
@@ -59,9 +69,9 @@ export function fillParams(
59
69
  }
60
70
 
61
71
  return encodeURIComponent(params[key]);
62
- })
63
- .replace(/\/+/g, '/')
64
- .replace(/\/$/, '') || '/';
72
+ });
73
+
74
+ return normalizeRoutePath(result || '/');
65
75
  }
66
76
 
67
77
  export function fileNameFromRoute(
@@ -86,9 +96,7 @@ export function expandStaticPaths(
86
96
  Object.entries(row).map(([k, v]) => [k, String(v)]),
87
97
  );
88
98
 
89
- const routePath = normalizeRoutePath(
90
- fillParams(basePage.routePattern, params),
91
- );
99
+ const routePath = fillParams(basePage.routePattern, params);
92
100
 
93
101
  return {
94
102
  ...basePage,
@@ -113,25 +121,22 @@ export function routeMatch(
113
121
 
114
122
  if (patternSeg.startsWith('*?:')) {
115
123
  params[patternSeg.slice(3)] =
116
- i < b.length ? b.slice(i).map(decodeURIComponent).join('/') : '';
124
+ i < b.length ? b.slice(i).map(safeDecodeURIComponent).join('/') : '';
117
125
  return params;
118
126
  }
119
127
 
120
128
  if (patternSeg.startsWith('*:')) {
121
129
  const rest = b.slice(i);
130
+ if (rest.length === 0) return null;
122
131
 
123
- if (rest.length === 0) {
124
- return null;
125
- }
126
-
127
- params[patternSeg.slice(2)] = rest.map(decodeURIComponent).join('/');
132
+ params[patternSeg.slice(2)] = rest.map(safeDecodeURIComponent).join('/');
128
133
  return params;
129
134
  }
130
135
 
131
136
  if (!urlSeg) return null;
132
137
 
133
138
  if (patternSeg.startsWith(':')) {
134
- params[patternSeg.slice(1)] = decodeURIComponent(urlSeg);
139
+ params[patternSeg.slice(1)] = safeDecodeURIComponent(urlSeg);
135
140
  continue;
136
141
  }
137
142
 
@@ -172,5 +177,6 @@ export function compareRoutePriority(a: string, b: string): number {
172
177
  }
173
178
  }
174
179
 
175
- return 0;
180
+ // More specific / longer routes first when otherwise equal
181
+ return bSegs.length - aSegs.length;
176
182
  }