vite-plugin-html-pages 1.2.2 → 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/dist/index.js +228 -320
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dev-server.ts +92 -42
- package/src/plugin.ts +118 -134
- package/src/static-assets.ts +97 -0
- package/src/assets.ts +0 -231
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
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
209
|
-
|
|
210
|
-
kind:
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
223
|
+
|
|
224
|
+
const staticAssets = await collectStaticAssets({
|
|
225
|
+
root,
|
|
226
|
+
pagesDir,
|
|
227
|
+
pageExtensions,
|
|
255
228
|
});
|
|
256
|
-
|
|
229
|
+
|
|
257
230
|
logDebug(
|
|
258
231
|
options.debug,
|
|
259
|
-
'
|
|
260
|
-
|
|
232
|
+
'emitting pages',
|
|
233
|
+
pages.map((p) => p.fileName),
|
|
261
234
|
);
|
|
262
|
-
|
|
235
|
+
|
|
263
236
|
logDebug(
|
|
264
237
|
options.debug,
|
|
265
|
-
'emitting
|
|
266
|
-
|
|
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.
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
+
}
|