vite-plugin-html-pages 1.2.3 → 1.2.4

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/src/plugin.ts CHANGED
@@ -1,19 +1,15 @@
1
1
  import pLimit from 'p-limit';
2
2
  import type { Plugin, ViteDevServer } from 'vite';
3
-
3
+ import {
4
+ buildStaticAssetSource,
5
+ collectStaticAssets,
6
+ } from './static-assets';
4
7
  import { discoverEntryPages } from './discover';
5
8
  import { installDevServer } from './dev-server';
6
9
  import { createPageModuleLoader, closePageModuleLoader } from './module-loader';
7
10
  import { buildPageIndex } from './page-index';
8
11
  import { renderPage } from './render-runtime';
9
- import {
10
- buildHtmlAssetReplacementMap,
11
- collectHtmlAssetRefs,
12
- rewriteHtmlAssetUrls,
13
- } from './assets';
14
-
15
12
  import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
16
- import type { HtmlAssetRef } from './assets';
17
13
  import { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID } from './constants';
18
14
 
19
15
  import fs from 'node:fs';
@@ -51,10 +47,12 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
51
47
  let root = process.cwd();
52
48
  let server: ViteDevServer | null = null;
53
49
  let devPages: HtPageInfo[] = [];
54
- let htmlAssetRefs = new Map<string, HtmlAssetRef>();
55
50
 
56
51
  const cleanUrls = options.cleanUrls ?? true;
57
52
  const pagesDir = options.pagesDir ?? 'src';
53
+ const pageExtensions = options.pageExtensions?.length
54
+ ? options.pageExtensions
55
+ : ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
58
56
 
59
57
  function logDebug(enabled: boolean | undefined, ...args: unknown[]) {
60
58
  if (!enabled) return;
@@ -168,48 +166,23 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
168
166
  this.addWatchFile(entry.entryPath);
169
167
  }
170
168
 
171
- // emitFile() is build-only
172
- if (server) {
173
- return;
174
- }
175
-
176
- htmlAssetRefs.clear();
177
-
178
- const { modulesByEntry, pages } = await buildPagesPipeline();
179
-
180
- const htmlByPageKey = new Map<string, { html: string; pageDir?: string }>();
181
-
182
- for (const page of pages) {
183
- const mod = modulesByEntry.get(page.entryPath);
184
-
185
- if (!mod) {
186
- throw new Error(
187
- `[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,
188
- );
189
- }
190
-
191
- const html = await renderPage(page, mod, false);
192
-
193
- htmlByPageKey.set(page.entryPath, {
194
- html,
195
- pageDir: path.dirname(page.absolutePath),
196
- });
197
- }
198
-
199
- htmlAssetRefs = await collectHtmlAssetRefs({
200
- ctx: this,
169
+ const staticAssets = await collectStaticAssets({
201
170
  root,
202
171
  pagesDir,
203
- htmlByPageKey,
172
+ pageExtensions,
204
173
  });
205
174
 
175
+ for (const asset of staticAssets) {
176
+ this.addWatchFile(asset.absolutePath);
177
+ }
178
+
206
179
  logDebug(
207
180
  options.debug,
208
- 'collected html assets',
209
- [...htmlAssetRefs.values()].map((ref) => ({
210
- kind: ref.kind,
211
- originalUrl: ref.originalUrl,
212
- absolutePath: ref.absolutePath,
181
+ 'static assets',
182
+ staticAssets.map((asset) => ({
183
+ kind: asset.kind,
184
+ input: asset.relativePathFromSrc,
185
+ output: asset.outputFileName,
213
186
  })),
214
187
  );
215
188
  },
@@ -247,48 +220,60 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
247
220
  async generateBundle(_, bundle) {
248
221
  try {
249
222
  const { modulesByEntry, pages } = await buildPagesPipeline();
250
-
251
- const assetReplacements = buildHtmlAssetReplacementMap({
252
- ctx: this,
253
- refs: htmlAssetRefs,
254
- bundle,
223
+
224
+ const staticAssets = await collectStaticAssets({
225
+ root,
226
+ pagesDir,
227
+ pageExtensions,
255
228
  });
256
-
229
+
257
230
  logDebug(
258
231
  options.debug,
259
- 'asset replacements',
260
- [...assetReplacements.entries()],
232
+ 'emitting pages',
233
+ pages.map((p) => p.fileName),
261
234
  );
262
-
235
+
263
236
  logDebug(
264
237
  options.debug,
265
- 'emitting pages',
266
- pages.map((p) => p.fileName),
238
+ 'emitting static assets',
239
+ staticAssets.map((asset) => asset.outputFileName),
267
240
  );
268
-
241
+
269
242
  const limit = pLimit(options.renderConcurrency ?? 8);
270
243
  const batchSize =
271
244
  options.renderBatchSize ??
272
245
  Math.max(options.renderConcurrency ?? 8, 32);
273
-
246
+
274
247
  // ---------------------------
275
- // 1. Render all pages
248
+ // 1. Emit static assets
249
+ // ---------------------------
250
+ for (const asset of staticAssets) {
251
+ const source = await buildStaticAssetSource(asset);
252
+
253
+ this.emitFile({
254
+ type: 'asset',
255
+ fileName: asset.outputFileName,
256
+ source,
257
+ });
258
+ }
259
+
260
+ // ---------------------------
261
+ // 2. Render all pages
276
262
  // ---------------------------
277
263
  for (const batch of chunkArray(pages, batchSize)) {
278
264
  await Promise.all(
279
265
  batch.map((page) =>
280
266
  limit(async () => {
281
267
  const mod = modulesByEntry.get(page.entryPath);
282
-
268
+
283
269
  if (!mod) {
284
270
  throw new Error(
285
271
  `[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,
286
272
  );
287
273
  }
288
-
289
- let html = await renderPage(page, mod, false);
290
- html = rewriteHtmlAssetUrls(html, assetReplacements);
291
-
274
+
275
+ const html = await renderPage(page, mod, false);
276
+
292
277
  this.emitFile({
293
278
  type: 'asset',
294
279
  fileName: options.mapOutputPath?.(page) ?? page.fileName,
@@ -298,137 +283,136 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
298
283
  ),
299
284
  );
300
285
  }
301
-
286
+
302
287
  // ---------------------------
303
- // 2. 404.html
288
+ // 3. 404.html
304
289
  // ---------------------------
305
290
  const notFoundPage = pages.find((p) => p.routePath === '/404');
306
-
291
+
307
292
  if (notFoundPage) {
308
293
  const mod = modulesByEntry.get(notFoundPage.entryPath);
309
-
294
+
310
295
  if (!mod) {
311
296
  throw new Error(
312
297
  `[${PLUGIN_NAME}] Missing module for 404 page entry: ${notFoundPage.entryPath}`,
313
298
  );
314
299
  }
315
-
316
- let html = await renderPage(notFoundPage, mod, false);
317
- html = rewriteHtmlAssetUrls(html, assetReplacements);
318
-
300
+
301
+ const html = await renderPage(notFoundPage, mod, false);
302
+
319
303
  this.emitFile({
320
304
  type: 'asset',
321
305
  fileName: '404.html',
322
306
  source: html,
323
307
  });
324
-
308
+
325
309
  logDebug(options.debug, 'generated 404.html from user page');
326
310
  } else {
327
311
  const default404 = `<!doctype html>
328
- <html lang="en">
329
- <head>
330
- <meta charset="UTF-8" />
331
- <meta name="viewport" content="width=device-width, initial-scale=1" />
332
- <title>404 - Page Not Found</title>
333
- <style>
334
- :root {
335
- color-scheme: light dark;
336
- }
337
- body {
338
- margin: 0;
339
- font-family: system-ui, sans-serif;
340
- min-height: 100vh;
341
- display: grid;
342
- place-items: center;
343
- padding: 2rem;
344
- }
345
- main {
346
- max-width: 40rem;
347
- text-align: center;
348
- }
349
- h1 {
350
- font-size: 3rem;
351
- margin: 0 0 1rem;
352
- }
353
- p {
354
- margin: 0.5rem 0;
355
- line-height: 1.5;
356
- }
357
- a {
358
- color: inherit;
359
- }
360
- </style>
361
- </head>
362
- <body>
363
- <main>
364
- <h1>404</h1>
365
- <p>Page not found.</p>
366
- <p><a href="/">Go back home</a></p>
367
- </main>
368
- </body>
369
- </html>
370
- `;
371
-
312
+ <html lang="en">
313
+ <head>
314
+ <meta charset="UTF-8" />
315
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
316
+ <title>404 - Page Not Found</title>
317
+ <style>
318
+ :root {
319
+ color-scheme: light dark;
320
+ }
321
+ body {
322
+ margin: 0;
323
+ font-family: system-ui, sans-serif;
324
+ min-height: 100vh;
325
+ display: grid;
326
+ place-items: center;
327
+ padding: 2rem;
328
+ }
329
+ main {
330
+ max-width: 40rem;
331
+ text-align: center;
332
+ }
333
+ h1 {
334
+ font-size: 3rem;
335
+ margin: 0 0 1rem;
336
+ }
337
+ p {
338
+ margin: 0.5rem 0;
339
+ line-height: 1.5;
340
+ }
341
+ a {
342
+ color: inherit;
343
+ }
344
+ </style>
345
+ </head>
346
+ <body>
347
+ <main>
348
+ <h1>404</h1>
349
+ <p>Page not found.</p>
350
+ <p><a href="/">Go back home</a></p>
351
+ </main>
352
+ </body>
353
+ </html>
354
+ `;
355
+
372
356
  this.emitFile({
373
357
  type: 'asset',
374
358
  fileName: '404.html',
375
359
  source: default404,
376
360
  });
377
-
361
+
378
362
  logDebug(options.debug, 'generated default 404.html');
379
363
  }
380
-
364
+
381
365
  // ---------------------------
382
- // 3. Sitemap
366
+ // 4. Sitemap
383
367
  // ---------------------------
384
368
  const sitemapBase = options.site ?? '';
385
-
369
+
386
370
  const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))].filter(
387
371
  (route) => !route.includes(':') && !route.includes('*'),
388
372
  );
389
-
373
+
390
374
  if (sitemapBase && sitemapRoutes.length > 0) {
391
375
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${sitemapRoutes
392
376
  .map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`)
393
377
  .join('\n')}\n</urlset>\n`;
394
-
378
+
395
379
  this.emitFile({
396
380
  type: 'asset',
397
381
  fileName: 'sitemap.xml',
398
382
  source: sitemap,
399
383
  });
400
-
384
+
401
385
  logDebug(options.debug, 'generated sitemap.xml');
402
386
  }
403
-
387
+
404
388
  // ---------------------------
405
- // 4. RSS
389
+ // 5. RSS
406
390
  // ---------------------------
407
391
  if (options.rss?.site) {
408
392
  const routePrefix = options.rss.routePrefix ?? '/blog';
409
-
393
+
410
394
  const rssItems = pages
411
395
  .filter((page) => page.routePath.startsWith(routePrefix))
412
396
  .map((page) => {
413
397
  const url = `${options.rss!.site}${page.routePath}`;
414
-
398
+
415
399
  return ` <item>\n <title>${page.routePath}</title>\n <link>${url}</link>\n <guid>${url}</guid>\n </item>`;
416
400
  })
417
401
  .join('\n');
418
-
402
+
419
403
  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`;
420
-
404
+
421
405
  this.emitFile({
422
406
  type: 'asset',
423
407
  fileName: 'rss.xml',
424
408
  source: rss,
425
409
  });
426
-
410
+
427
411
  logDebug(options.debug, 'generated rss.xml');
428
412
  }
429
-
413
+
430
414
  // ---------------------------
431
- // 5. Remove virtual entry chunk
415
+ // 6. Remove virtual entry chunk
432
416
  // ---------------------------
433
417
  for (const [fileName, output] of Object.entries(bundle)) {
434
418
  if (
@@ -441,7 +425,7 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
441
425
  } finally {
442
426
  await closePageModuleLoader();
443
427
  }
444
- },
428
+ }
445
429
  };
446
430
  }
447
431
 
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { transformWithEsbuild } from 'vite';
5
+
6
+ export interface StaticAssetFile {
7
+ absolutePath: string;
8
+ relativePathFromSrc: string;
9
+ outputFileName: string;
10
+ kind: 'copy' | 'ts';
11
+ }
12
+
13
+ export interface CollectStaticAssetsArgs {
14
+ root: string;
15
+ pagesDir: string;
16
+ pageExtensions: string[];
17
+ }
18
+
19
+ function normalizeSlashes(value: string): string {
20
+ return value.replace(/\\/g, '/');
21
+ }
22
+
23
+ function hasAnySuffix(value: string, suffixes: string[]): boolean {
24
+ return suffixes.some((suffix) => value.endsWith(suffix));
25
+ }
26
+
27
+ function toOutputFileName(relativePathFromSrc: string): string {
28
+ if (relativePathFromSrc.endsWith('.ts')) {
29
+ return relativePathFromSrc.slice(0, -3) + '.js';
30
+ }
31
+ return relativePathFromSrc;
32
+ }
33
+
34
+ export async function collectStaticAssets(
35
+ args: CollectStaticAssetsArgs,
36
+ ): Promise<StaticAssetFile[]> {
37
+ const { root, pagesDir, pageExtensions } = args;
38
+ const srcDir = path.join(root, pagesDir);
39
+
40
+ const entries = await fg('**/*', {
41
+ cwd: srcDir,
42
+ onlyFiles: true,
43
+ dot: false,
44
+ absolute: false,
45
+ });
46
+
47
+ const assets: StaticAssetFile[] = [];
48
+
49
+ for (const entry of entries) {
50
+ const rel = normalizeSlashes(entry);
51
+
52
+ if (hasAnySuffix(rel, pageExtensions)) {
53
+ continue;
54
+ }
55
+
56
+ const absolutePath = path.join(srcDir, rel);
57
+
58
+ if (rel.endsWith('.ts')) {
59
+ assets.push({
60
+ absolutePath,
61
+ relativePathFromSrc: rel,
62
+ outputFileName: normalizeSlashes(toOutputFileName(rel)),
63
+ kind: 'ts',
64
+ });
65
+ continue;
66
+ }
67
+
68
+ assets.push({
69
+ absolutePath,
70
+ relativePathFromSrc: rel,
71
+ outputFileName: normalizeSlashes(rel),
72
+ kind: 'copy',
73
+ });
74
+ }
75
+
76
+ return assets;
77
+ }
78
+
79
+ export async function buildStaticAssetSource(
80
+ asset: StaticAssetFile,
81
+ ): Promise<string | Uint8Array> {
82
+ if (asset.kind === 'copy') {
83
+ return fs.readFile(asset.absolutePath);
84
+ }
85
+
86
+ const source = await fs.readFile(asset.absolutePath, 'utf8');
87
+
88
+ const result = await transformWithEsbuild(source, asset.absolutePath, {
89
+ loader: 'ts',
90
+ format: 'esm',
91
+ target: 'es2020',
92
+ sourcemap: false,
93
+ minify: false,
94
+ });
95
+
96
+ return result.code;
97
+ }