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/dist/index.js +2712 -129
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugin.ts +118 -109
- package/src/static-assets.ts +81 -29
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 = `${
|
|
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
|
|
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:
|
|
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;
|
package/src/static-assets.ts
CHANGED
|
@@ -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
|
|
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' | '
|
|
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 (
|
|
53
|
-
|
|
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
|
|
87
|
+
export async function copyStaticAssetSource(
|
|
80
88
|
asset: StaticAssetFile,
|
|
81
|
-
): Promise<
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
+
}
|