vite-plugin-html-pages 1.2.3 → 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.3",
3
+ "version": "1.2.5",
4
4
  "author": "Paul Browne",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/plugin.ts CHANGED
@@ -7,13 +7,12 @@ import { createPageModuleLoader, closePageModuleLoader } from './module-loader';
7
7
  import { buildPageIndex } from './page-index';
8
8
  import { renderPage } from './render-runtime';
9
9
  import {
10
- buildHtmlAssetReplacementMap,
11
- collectHtmlAssetRefs,
12
- rewriteHtmlAssetUrls,
13
- } from './assets';
10
+ buildProcessedStaticAssets,
11
+ collectStaticAssets,
12
+ copyStaticAssetSource,
13
+ } from './static-assets';
14
14
 
15
15
  import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
16
- import type { HtmlAssetRef } from './assets';
17
16
  import { PLUGIN_NAME, VIRTUAL_BUILD_ENTRY_ID } from './constants';
18
17
 
19
18
  import fs from 'node:fs';
@@ -51,10 +50,12 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
51
50
  let root = process.cwd();
52
51
  let server: ViteDevServer | null = null;
53
52
  let devPages: HtPageInfo[] = [];
54
- let htmlAssetRefs = new Map<string, HtmlAssetRef>();
55
53
 
56
54
  const cleanUrls = options.cleanUrls ?? true;
57
55
  const pagesDir = options.pagesDir ?? 'src';
56
+ const pageExtensions = options.pageExtensions?.length
57
+ ? options.pageExtensions
58
+ : ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
58
59
 
59
60
  function logDebug(enabled: boolean | undefined, ...args: unknown[]) {
60
61
  if (!enabled) return;
@@ -163,53 +164,28 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
163
164
 
164
165
  async buildStart() {
165
166
  const entries = await discoverEntryPages(root, options);
166
-
167
+
167
168
  for (const entry of entries) {
168
169
  this.addWatchFile(entry.entryPath);
169
170
  }
170
-
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,
171
+
172
+ const staticAssets = await collectStaticAssets({
201
173
  root,
202
174
  pagesDir,
203
- htmlByPageKey,
175
+ pageExtensions,
204
176
  });
205
-
177
+
178
+ for (const asset of staticAssets) {
179
+ this.addWatchFile(asset.absolutePath);
180
+ }
181
+
206
182
  logDebug(
207
183
  options.debug,
208
- 'collected html assets',
209
- [...htmlAssetRefs.values()].map((ref) => ({
210
- kind: ref.kind,
211
- originalUrl: ref.originalUrl,
212
- absolutePath: ref.absolutePath,
184
+ 'static assets',
185
+ staticAssets.map((asset) => ({
186
+ kind: asset.kind,
187
+ input: asset.relativePathFromSrc,
188
+ output: asset.outputFileName,
213
189
  })),
214
190
  );
215
191
  },
@@ -248,22 +224,26 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
248
224
  try {
249
225
  const { modulesByEntry, pages } = await buildPagesPipeline();
250
226
 
251
- const assetReplacements = buildHtmlAssetReplacementMap({
252
- ctx: this,
253
- refs: htmlAssetRefs,
254
- bundle,
227
+ const staticAssets = await collectStaticAssets({
228
+ root,
229
+ pagesDir,
230
+ pageExtensions,
255
231
  });
256
232
 
257
233
  logDebug(
258
234
  options.debug,
259
- 'asset replacements',
260
- [...assetReplacements.entries()],
235
+ 'emitting pages',
236
+ pages.map((p) => p.fileName),
261
237
  );
262
238
 
263
239
  logDebug(
264
240
  options.debug,
265
- 'emitting pages',
266
- pages.map((p) => p.fileName),
241
+ 'emitting static assets',
242
+ staticAssets.map((asset) => ({
243
+ kind: asset.kind,
244
+ input: asset.relativePathFromSrc,
245
+ output: asset.outputFileName,
246
+ })),
267
247
  );
268
248
 
269
249
  const limit = pLimit(options.renderConcurrency ?? 8);
@@ -271,9 +251,34 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
271
251
  options.renderBatchSize ??
272
252
  Math.max(options.renderConcurrency ?? 8, 32);
273
253
 
274
- // ---------------------------
275
- // 1. Render all pages
276
- // ---------------------------
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
+
270
+ for (const asset of staticAssets) {
271
+ if (asset.kind !== 'copy') continue;
272
+
273
+ const source = await copyStaticAssetSource(asset);
274
+
275
+ this.emitFile({
276
+ type: 'asset',
277
+ fileName: asset.outputFileName,
278
+ source,
279
+ });
280
+ }
281
+
277
282
  for (const batch of chunkArray(pages, batchSize)) {
278
283
  await Promise.all(
279
284
  batch.map((page) =>
@@ -286,8 +291,7 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
286
291
  );
287
292
  }
288
293
 
289
- let html = await renderPage(page, mod, false);
290
- html = rewriteHtmlAssetUrls(html, assetReplacements);
294
+ const html = await renderPage(page, mod, false);
291
295
 
292
296
  this.emitFile({
293
297
  type: 'asset',
@@ -299,9 +303,6 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
299
303
  );
300
304
  }
301
305
 
302
- // ---------------------------
303
- // 2. 404.html
304
- // ---------------------------
305
306
  const notFoundPage = pages.find((p) => p.routePath === '/404');
306
307
 
307
308
  if (notFoundPage) {
@@ -313,8 +314,7 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
313
314
  );
314
315
  }
315
316
 
316
- let html = await renderPage(notFoundPage, mod, false);
317
- html = rewriteHtmlAssetUrls(html, assetReplacements);
317
+ const html = await renderPage(notFoundPage, mod, false);
318
318
 
319
319
  this.emitFile({
320
320
  type: 'asset',
@@ -378,9 +378,6 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
378
378
  logDebug(options.debug, 'generated default 404.html');
379
379
  }
380
380
 
381
- // ---------------------------
382
- // 3. Sitemap
383
- // ---------------------------
384
381
  const sitemapBase = options.site ?? '';
385
382
 
386
383
  const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))].filter(
@@ -401,35 +398,31 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
401
398
  logDebug(options.debug, 'generated sitemap.xml');
402
399
  }
403
400
 
404
- // ---------------------------
405
- // 4. RSS
406
- // ---------------------------
407
- if (options.rss?.site) {
408
- const routePrefix = options.rss.routePrefix ?? '/blog';
401
+ const rss = options.rss;
409
402
 
403
+ if (rss?.site) {
404
+ const routePrefix = rss.routePrefix ?? '/blog';
405
+
410
406
  const rssItems = pages
411
407
  .filter((page) => page.routePath.startsWith(routePrefix))
412
408
  .map((page) => {
413
- const url = `${options.rss!.site}${page.routePath}`;
414
-
409
+ const url = `${rss.site}${page.routePath}`;
410
+
415
411
  return ` <item>\n <title>${page.routePath}</title>\n <link>${url}</link>\n <guid>${url}</guid>\n </item>`;
416
412
  })
417
413
  .join('\n');
418
-
419
- 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
-
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
+
421
417
  this.emitFile({
422
418
  type: 'asset',
423
419
  fileName: 'rss.xml',
424
- source: rss,
420
+ source: rssXml,
425
421
  });
426
-
422
+
427
423
  logDebug(options.debug, 'generated rss.xml');
428
424
  }
429
425
 
430
- // ---------------------------
431
- // 5. Remove virtual entry chunk
432
- // ---------------------------
433
426
  for (const [fileName, output] of Object.entries(bundle)) {
434
427
  if (
435
428
  output.type === 'chunk' &&
@@ -441,8 +434,8 @@ export function htPages(options: HtPagesPluginOptions = {}): Plugin {
441
434
  } finally {
442
435
  await closePageModuleLoader();
443
436
  }
444
- },
437
+ }
445
438
  };
446
439
  }
447
440
 
448
- export default htPages;
441
+ export default htPages;
@@ -0,0 +1,149 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import * as esbuild from 'esbuild';
5
+
6
+ export interface StaticAssetFile {
7
+ absolutePath: string;
8
+ relativePathFromSrc: string;
9
+ outputFileName: string;
10
+ kind: 'copy' | 'process';
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 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
+
46
+ function toOutputFileName(relativePathFromSrc: string): string {
47
+ if (relativePathFromSrc.endsWith('.ts')) {
48
+ return relativePathFromSrc.slice(0, -3) + '.js';
49
+ }
50
+ return relativePathFromSrc;
51
+ }
52
+
53
+ export async function collectStaticAssets(
54
+ args: CollectStaticAssetsArgs,
55
+ ): Promise<StaticAssetFile[]> {
56
+ const { root, pagesDir, pageExtensions } = args;
57
+ const srcDir = path.join(root, pagesDir);
58
+
59
+ const entries = await fg('**/*', {
60
+ cwd: srcDir,
61
+ onlyFiles: true,
62
+ dot: false,
63
+ absolute: false,
64
+ });
65
+
66
+ const assets: StaticAssetFile[] = [];
67
+
68
+ for (const entry of entries) {
69
+ const rel = normalizeSlashes(entry);
70
+
71
+ if (shouldIgnoreFile(rel)) continue;
72
+ if (hasAnySuffix(rel, pageExtensions)) continue;
73
+
74
+ const absolutePath = path.join(srcDir, rel);
75
+
76
+ assets.push({
77
+ absolutePath,
78
+ relativePathFromSrc: rel,
79
+ outputFileName: normalizeSlashes(toOutputFileName(rel)),
80
+ kind: isProcessableAsset(rel) ? 'process' : 'copy',
81
+ });
82
+ }
83
+
84
+ return assets;
85
+ }
86
+
87
+ export async function copyStaticAssetSource(
88
+ asset: StaticAssetFile,
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;
101
+
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
+ }
108
+
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,
122
+ format: 'esm',
123
+ target: 'es2020',
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
+ },
141
+ });
142
+
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
+ }
package/src/assets.ts DELETED
@@ -1,236 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import type {
4
- OutputBundle,
5
- PluginContext,
6
- } from 'rollup';
7
-
8
- export type HtmlAssetKind = 'css' | 'js';
9
-
10
- export interface HtmlAssetRef {
11
- kind: HtmlAssetKind;
12
- originalUrl: string;
13
- absolutePath: string;
14
- refId: string;
15
- }
16
-
17
- export interface ExtractedHtmlAsset {
18
- kind: HtmlAssetKind;
19
- url: string;
20
- }
21
-
22
- const EXTERNAL_URL_RE = /^(?:[a-z]+:)?\/\//i;
23
-
24
- export function isLocalAssetUrl(url: string): boolean {
25
- return (
26
- !!url &&
27
- !url.startsWith('data:') &&
28
- !url.startsWith('mailto:') &&
29
- !url.startsWith('tel:') &&
30
- !url.startsWith('#') &&
31
- !EXTERNAL_URL_RE.test(url)
32
- );
33
- }
34
-
35
- export function stripQueryAndHash(url: string): string {
36
- return url.split('#')[0].split('?')[0];
37
- }
38
-
39
- export function extractHtmlAssets(html: string): ExtractedHtmlAsset[] {
40
- const assets: ExtractedHtmlAsset[] = [];
41
-
42
- for (const match of html.matchAll(
43
- /<link\b[^>]*\brel=["']stylesheet["'][^>]*\bhref=["']([^"']+)["'][^>]*>/gi,
44
- )) {
45
- assets.push({ kind: 'css', url: match[1] });
46
- }
47
-
48
- for (const match of html.matchAll(
49
- /<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi,
50
- )) {
51
- assets.push({ kind: 'js', url: match[1] });
52
- }
53
-
54
- return dedupeExtractedAssets(assets);
55
- }
56
-
57
- function dedupeExtractedAssets(
58
- assets: ExtractedHtmlAsset[],
59
- ): ExtractedHtmlAsset[] {
60
- const seen = new Set<string>();
61
- const out: ExtractedHtmlAsset[] = [];
62
-
63
- for (const asset of assets) {
64
- const key = `${asset.kind}:${asset.url}`;
65
- if (seen.has(key)) continue;
66
- seen.add(key);
67
- out.push(asset);
68
- }
69
-
70
- return out;
71
- }
72
-
73
- export function resolveLocalAssetPath(args: {
74
- root: string;
75
- pagesDir: string;
76
- pageDir?: string;
77
- url: string;
78
- }): string | null {
79
- const { root, pagesDir, pageDir, url } = args;
80
-
81
- if (!isLocalAssetUrl(url)) return null;
82
-
83
- const cleanUrl = stripQueryAndHash(url);
84
-
85
- let abs: string;
86
-
87
- if (cleanUrl.startsWith('/')) {
88
- abs = path.join(root, pagesDir, cleanUrl.slice(1));
89
- } else if (cleanUrl.startsWith(`${pagesDir}/`)) {
90
- abs = path.join(root, cleanUrl);
91
- } else {
92
- const baseDir = pageDir ?? path.join(root, pagesDir);
93
- abs = path.resolve(baseDir, cleanUrl);
94
- }
95
-
96
- return fs.existsSync(abs) ? abs : null;
97
- }
98
-
99
- export function emitHtmlAsset(args: {
100
- ctx: PluginContext;
101
- kind: HtmlAssetKind;
102
- absolutePath: string;
103
- }): string {
104
- const { ctx, kind, absolutePath } = args;
105
-
106
- if (kind === 'css' || kind === 'js') {
107
- return ctx.emitFile({
108
- type: 'chunk',
109
- id: absolutePath,
110
- name: path.basename(absolutePath, path.extname(absolutePath)),
111
- });
112
- }
113
-
114
- throw new Error(`[vite-plugin-html-pages] Unsupported asset kind: ${kind}`);
115
- }
116
-
117
- function replaceAllLiteral(
118
- input: string,
119
- search: string,
120
- replacement: string,
121
- ): string {
122
- return input.split(search).join(replacement);
123
- }
124
-
125
- export function rewriteHtmlAssetUrls(
126
- html: string,
127
- replacements: Map<string, string>,
128
- ): string {
129
- let out = html;
130
-
131
- for (const [originalUrl, builtUrl] of replacements) {
132
- out = replaceAllLiteral(
133
- out,
134
- `href="${originalUrl}"`,
135
- `href="${builtUrl}"`,
136
- );
137
- out = replaceAllLiteral(
138
- out,
139
- `href='${originalUrl}'`,
140
- `href='${builtUrl}'`,
141
- );
142
- out = replaceAllLiteral(
143
- out,
144
- `src="${originalUrl}"`,
145
- `src="${builtUrl}"`,
146
- );
147
- out = replaceAllLiteral(
148
- out,
149
- `src='${originalUrl}'`,
150
- `src='${builtUrl}'`,
151
- );
152
- }
153
-
154
- return out;
155
- }
156
-
157
- export async function collectHtmlAssetRefs(args: {
158
- ctx: PluginContext;
159
- root: string;
160
- pagesDir: string;
161
- htmlByPageKey: Map<string, { html: string; pageDir?: string }>;
162
- }): Promise<Map<string, HtmlAssetRef>> {
163
- const { ctx, root, pagesDir, htmlByPageKey } = args;
164
- const refs = new Map<string, HtmlAssetRef>();
165
-
166
- for (const { html, pageDir } of htmlByPageKey.values()) {
167
- const assets = extractHtmlAssets(html);
168
-
169
- for (const asset of assets) {
170
- const abs = resolveLocalAssetPath({
171
- root,
172
- pagesDir,
173
- pageDir,
174
- url: asset.url,
175
- });
176
-
177
- if (!abs) continue;
178
-
179
- const key = `${asset.kind}:${asset.url}`;
180
- if (refs.has(key)) continue;
181
-
182
- const refId = emitHtmlAsset({
183
- ctx,
184
- kind: asset.kind,
185
- absolutePath: abs,
186
- });
187
-
188
- refs.set(key, {
189
- kind: asset.kind,
190
- originalUrl: asset.url,
191
- absolutePath: abs,
192
- refId,
193
- });
194
- }
195
- }
196
-
197
- return refs;
198
- }
199
-
200
- export function buildHtmlAssetReplacementMap(args: {
201
- ctx: PluginContext;
202
- refs: Map<string, HtmlAssetRef>;
203
- bundle: OutputBundle;
204
- }): Map<string, string> {
205
- const { ctx, refs, bundle } = args;
206
- const replacements = new Map<string, string>();
207
-
208
- for (const ref of refs.values()) {
209
- if (ref.kind === 'js') {
210
- const fileName = ctx.getFileName(ref.refId);
211
- replacements.set(ref.originalUrl, `/${fileName}`);
212
- continue;
213
- }
214
-
215
- if (ref.kind === 'css') {
216
- const jsEntryFile = ctx.getFileName(ref.refId);
217
- const jsChunk = bundle[jsEntryFile];
218
-
219
- if (
220
- jsChunk &&
221
- jsChunk.type === 'chunk' &&
222
- 'viteMetadata' in jsChunk &&
223
- jsChunk.viteMetadata?.importedCss &&
224
- jsChunk.viteMetadata.importedCss.size > 0
225
- ) {
226
- const cssFile = [...jsChunk.viteMetadata.importedCss][0];
227
- replacements.set(ref.originalUrl, `/${cssFile}`);
228
- continue;
229
- }
230
-
231
- replacements.set(ref.originalUrl, `/${jsEntryFile}`);
232
- }
233
- }
234
-
235
- return replacements;
236
- }