vite-plugin-html-pages 1.2.4 → 1.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-html-pages",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "author": "Paul Browne",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/plugin.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import pLimit from 'p-limit';
2
2
  import type { Plugin, ViteDevServer } from 'vite';
3
- import {
4
- buildStaticAssetSource,
5
- collectStaticAssets,
6
- } from './static-assets';
3
+
7
4
  import { discoverEntryPages } from './discover';
8
5
  import { installDevServer } from './dev-server';
9
6
  import { createPageModuleLoader, closePageModuleLoader } from './module-loader';
10
7
  import { buildPageIndex } from './page-index';
11
8
  import { renderPage } from './render-runtime';
9
+ import {
10
+ buildProcessedStaticAssets,
11
+ collectStaticAssets,
12
+ copyStaticAssetSource,
13
+ } from './static-assets';
14
+
12
15
  import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
13
16
  import { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID } from './constants';
14
17
 
@@ -51,8 +54,8 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
51
54
  const cleanUrls = options.cleanUrls ?? true;
52
55
  const pagesDir = options.pagesDir ?? 'src';
53
56
  const pageExtensions = options.pageExtensions?.length
54
- ? options.pageExtensions
55
- : ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
57
+ ? options.pageExtensions
58
+ : ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
56
59
 
57
60
  function logDebug(enabled: boolean | undefined, ...args: unknown[]) {
58
61
  if (!enabled) return;
@@ -161,21 +164,21 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
161
164
 
162
165
  async buildStart() {
163
166
  const entries = await discoverEntryPages(root, options);
164
-
167
+
165
168
  for (const entry of entries) {
166
169
  this.addWatchFile(entry.entryPath);
167
170
  }
168
-
171
+
169
172
  const staticAssets = await collectStaticAssets({
170
173
  root,
171
174
  pagesDir,
172
175
  pageExtensions,
173
176
  });
174
-
177
+
175
178
  for (const asset of staticAssets) {
176
179
  this.addWatchFile(asset.absolutePath);
177
180
  }
178
-
181
+
179
182
  logDebug(
180
183
  options.debug,
181
184
  'static assets',
@@ -220,60 +223,76 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
220
223
  async generateBundle(_, bundle) {
221
224
  try {
222
225
  const { modulesByEntry, pages } = await buildPagesPipeline();
223
-
226
+
224
227
  const staticAssets = await collectStaticAssets({
225
228
  root,
226
229
  pagesDir,
227
230
  pageExtensions,
228
231
  });
229
-
232
+
230
233
  logDebug(
231
234
  options.debug,
232
235
  'emitting pages',
233
236
  pages.map((p) => p.fileName),
234
237
  );
235
-
238
+
236
239
  logDebug(
237
240
  options.debug,
238
241
  'emitting static assets',
239
- staticAssets.map((asset) => asset.outputFileName),
242
+ staticAssets.map((asset) => ({
243
+ kind: asset.kind,
244
+ input: asset.relativePathFromSrc,
245
+ output: asset.outputFileName,
246
+ })),
240
247
  );
241
-
248
+
242
249
  const limit = pLimit(options.renderConcurrency ?? 8);
243
250
  const batchSize =
244
251
  options.renderBatchSize ??
245
252
  Math.max(options.renderConcurrency ?? 8, 32);
246
-
247
- // ---------------------------
248
- // 1. Emit static assets
249
- // ---------------------------
253
+
254
+ const processedOutputs = await buildProcessedStaticAssets({
255
+ root,
256
+ pagesDir,
257
+ assets: staticAssets,
258
+ minify: true,
259
+ sourcemap: false,
260
+ });
261
+
262
+ for (const [fileName, source] of processedOutputs) {
263
+ this.emitFile({
264
+ type: 'asset',
265
+ fileName,
266
+ source,
267
+ });
268
+ }
269
+
250
270
  for (const asset of staticAssets) {
251
- const source = await buildStaticAssetSource(asset);
252
-
271
+ if (asset.kind !== 'copy') continue;
272
+
273
+ const source = await copyStaticAssetSource(asset);
274
+
253
275
  this.emitFile({
254
276
  type: 'asset',
255
277
  fileName: asset.outputFileName,
256
278
  source,
257
279
  });
258
280
  }
259
-
260
- // ---------------------------
261
- // 2. Render all pages
262
- // ---------------------------
281
+
263
282
  for (const batch of chunkArray(pages, batchSize)) {
264
283
  await Promise.all(
265
284
  batch.map((page) =>
266
285
  limit(async () => {
267
286
  const mod = modulesByEntry.get(page.entryPath);
268
-
287
+
269
288
  if (!mod) {
270
289
  throw new Error(
271
290
  `[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,
272
291
  );
273
292
  }
274
-
293
+
275
294
  const html = await renderPage(page, mod, false);
276
-
295
+
277
296
  this.emitFile({
278
297
  type: 'asset',
279
298
  fileName: options.mapOutputPath?.(page) ?? page.fileName,
@@ -283,137 +302,127 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
283
302
  ),
284
303
  );
285
304
  }
286
-
287
- // ---------------------------
288
- // 3. 404.html
289
- // ---------------------------
305
+
290
306
  const notFoundPage = pages.find((p) => p.routePath === '/404');
291
-
307
+
292
308
  if (notFoundPage) {
293
309
  const mod = modulesByEntry.get(notFoundPage.entryPath);
294
-
310
+
295
311
  if (!mod) {
296
312
  throw new Error(
297
313
  `[${PLUGIN_NAME}] Missing module for 404 page entry: ${notFoundPage.entryPath}`,
298
314
  );
299
315
  }
300
-
316
+
301
317
  const html = await renderPage(notFoundPage, mod, false);
302
-
318
+
303
319
  this.emitFile({
304
320
  type: 'asset',
305
321
  fileName: '404.html',
306
322
  source: html,
307
323
  });
308
-
324
+
309
325
  logDebug(options.debug, 'generated 404.html from user page');
310
326
  } else {
311
327
  const default404 = `<!doctype html>
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
-
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
+
356
372
  this.emitFile({
357
373
  type: 'asset',
358
374
  fileName: '404.html',
359
375
  source: default404,
360
376
  });
361
-
377
+
362
378
  logDebug(options.debug, 'generated default 404.html');
363
379
  }
364
-
365
- // ---------------------------
366
- // 4. Sitemap
367
- // ---------------------------
380
+
368
381
  const sitemapBase = options.site ?? '';
369
-
382
+
370
383
  const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))].filter(
371
384
  (route) => !route.includes(':') && !route.includes('*'),
372
385
  );
373
-
386
+
374
387
  if (sitemapBase && sitemapRoutes.length > 0) {
375
388
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${sitemapRoutes
376
389
  .map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`)
377
390
  .join('\n')}\n</urlset>\n`;
378
-
391
+
379
392
  this.emitFile({
380
393
  type: 'asset',
381
394
  fileName: 'sitemap.xml',
382
395
  source: sitemap,
383
396
  });
384
-
397
+
385
398
  logDebug(options.debug, 'generated sitemap.xml');
386
399
  }
387
-
388
- // ---------------------------
389
- // 5. RSS
390
- // ---------------------------
391
- if (options.rss?.site) {
392
- const routePrefix = options.rss.routePrefix ?? '/blog';
393
-
400
+
401
+ const rss = options.rss;
402
+
403
+ if (rss?.site) {
404
+ const routePrefix = rss.routePrefix ?? '/blog';
405
+
394
406
  const rssItems = pages
395
407
  .filter((page) => page.routePath.startsWith(routePrefix))
396
408
  .map((page) => {
397
- const url = `${options.rss!.site}${page.routePath}`;
398
-
409
+ const url = `${rss.site}${page.routePath}`;
410
+
399
411
  return ` <item>\n <title>${page.routePath}</title>\n <link>${url}</link>\n <guid>${url}</guid>\n </item>`;
400
412
  })
401
413
  .join('\n');
402
-
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`;
404
-
414
+
415
+ const rssXml = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0">\n<channel>\n <title>${rss.title ?? PLUGIN_NAME}</title>\n <link>${rss.site}</link>\n <description>${rss.description ?? 'RSS feed'}</description>\n${rssItems}\n</channel>\n</rss>\n`;
416
+
405
417
  this.emitFile({
406
418
  type: 'asset',
407
419
  fileName: 'rss.xml',
408
- source: rss,
420
+ source: rssXml,
409
421
  });
410
-
422
+
411
423
  logDebug(options.debug, 'generated rss.xml');
412
424
  }
413
-
414
- // ---------------------------
415
- // 6. Remove virtual entry chunk
416
- // ---------------------------
425
+
417
426
  for (const [fileName, output] of Object.entries(bundle)) {
418
427
  if (
419
428
  output.type === 'chunk' &&
@@ -429,4 +438,4 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
429
438
  };
430
439
  }
431
440
 
432
- export default htPages;
441
+ export default htPages;
@@ -1,13 +1,13 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import fg from 'fast-glob';
4
- import { transformWithEsbuild } from 'vite';
4
+ import * as esbuild from 'esbuild';
5
5
 
6
6
  export interface StaticAssetFile {
7
7
  absolutePath: string;
8
8
  relativePathFromSrc: string;
9
9
  outputFileName: string;
10
- kind: 'copy' | 'ts';
10
+ kind: 'copy' | 'process';
11
11
  }
12
12
 
13
13
  export interface CollectStaticAssetsArgs {
@@ -24,6 +24,25 @@ function hasAnySuffix(value: string, suffixes: string[]): boolean {
24
24
  return suffixes.some((suffix) => value.endsWith(suffix));
25
25
  }
26
26
 
27
+ function shouldIgnoreFile(rel: string): boolean {
28
+ return (
29
+ rel.endsWith('.d.ts') ||
30
+ rel.endsWith('.map') ||
31
+ rel.endsWith('.tsbuildinfo') ||
32
+ rel.startsWith('.') ||
33
+ rel.includes('/.')
34
+ );
35
+ }
36
+
37
+ function isProcessableAsset(rel: string): boolean {
38
+ return (
39
+ rel.endsWith('.js') ||
40
+ rel.endsWith('.mjs') ||
41
+ rel.endsWith('.ts') ||
42
+ rel.endsWith('.css')
43
+ );
44
+ }
45
+
27
46
  function toOutputFileName(relativePathFromSrc: string): string {
28
47
  if (relativePathFromSrc.endsWith('.ts')) {
29
48
  return relativePathFromSrc.slice(0, -3) + '.js';
@@ -49,49 +68,82 @@ export async function collectStaticAssets(
49
68
  for (const entry of entries) {
50
69
  const rel = normalizeSlashes(entry);
51
70
 
52
- if (hasAnySuffix(rel, pageExtensions)) {
53
- continue;
54
- }
71
+ if (shouldIgnoreFile(rel)) continue;
72
+ if (hasAnySuffix(rel, pageExtensions)) continue;
55
73
 
56
74
  const absolutePath = path.join(srcDir, rel);
57
75
 
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
76
  assets.push({
69
77
  absolutePath,
70
78
  relativePathFromSrc: rel,
71
- outputFileName: normalizeSlashes(rel),
72
- kind: 'copy',
79
+ outputFileName: normalizeSlashes(toOutputFileName(rel)),
80
+ kind: isProcessableAsset(rel) ? 'process' : 'copy',
73
81
  });
74
82
  }
75
83
 
76
84
  return assets;
77
85
  }
78
86
 
79
- export async function buildStaticAssetSource(
87
+ export async function copyStaticAssetSource(
80
88
  asset: StaticAssetFile,
81
- ): Promise<string | Uint8Array> {
82
- if (asset.kind === 'copy') {
83
- return fs.readFile(asset.absolutePath);
84
- }
89
+ ): Promise<Uint8Array> {
90
+ return fs.readFile(asset.absolutePath);
91
+ }
92
+
93
+ export async function buildProcessedStaticAssets(args: {
94
+ root: string;
95
+ pagesDir: string;
96
+ assets: StaticAssetFile[];
97
+ minify?: boolean;
98
+ sourcemap?: boolean;
99
+ }): Promise<Map<string, string | Uint8Array>> {
100
+ const { root, pagesDir, assets, minify = true, sourcemap = false } = args;
85
101
 
86
- const source = await fs.readFile(asset.absolutePath, 'utf8');
102
+ const processable = assets.filter((a) => a.kind === 'process');
103
+ const out = new Map<string, string | Uint8Array>();
104
+
105
+ if (processable.length === 0) {
106
+ return out;
107
+ }
87
108
 
88
- const result = await transformWithEsbuild(source, asset.absolutePath, {
89
- loader: 'ts',
109
+ const srcDir = path.join(root, pagesDir);
110
+ const distDir = path.join(root, 'dist');
111
+
112
+ const result = await esbuild.build({
113
+ entryPoints: processable.map((a) => a.absolutePath),
114
+ absWorkingDir: root,
115
+ outbase: srcDir,
116
+ outdir: distDir,
117
+ bundle: true,
118
+ splitting: true,
119
+ treeShaking: true,
120
+ minify,
121
+ sourcemap,
90
122
  format: 'esm',
91
123
  target: 'es2020',
92
- sourcemap: false,
93
- minify: false,
124
+ platform: 'browser',
125
+ write: false,
126
+ entryNames: '[dir]/[name]',
127
+ assetNames: '[dir]/[name]',
128
+ loader: {
129
+ '.css': 'css',
130
+ '.png': 'file',
131
+ '.jpg': 'file',
132
+ '.jpeg': 'file',
133
+ '.gif': 'file',
134
+ '.svg': 'file',
135
+ '.webp': 'file',
136
+ '.woff': 'file',
137
+ '.woff2': 'file',
138
+ '.ttf': 'file',
139
+ '.otf': 'file',
140
+ },
94
141
  });
95
142
 
96
- return result.code;
97
- }
143
+ for (const file of result.outputFiles) {
144
+ const rel = normalizeSlashes(path.relative(distDir, file.path));
145
+ out.set(rel, file.text ?? file.contents);
146
+ }
147
+
148
+ return out;
149
+ }