vite-plugin-html-pages 1.3.7 → 1.4.1

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.
@@ -1,43 +0,0 @@
1
- import type { HtPageInfo, RouteParamDefinition } from './types';
2
-
3
- function paramsTypeFromDefinitions(
4
- paramDefinitions: RouteParamDefinition[],
5
- ): string {
6
- if (paramDefinitions.length === 0) {
7
- return '{}';
8
- }
9
-
10
- const fields = paramDefinitions.map((param) => {
11
- if (param.type === 'single') {
12
- return `${JSON.stringify(param.name)}: string`;
13
- }
14
-
15
- if (param.type === 'catch-all') {
16
- return `${JSON.stringify(param.name)}: string[]`;
17
- }
18
-
19
- return `${JSON.stringify(param.name)}?: string[]`;
20
- });
21
-
22
- return `{ ${fields.join('; ')} }`;
23
- }
24
-
25
- export function generateTypedPageHelper(page: HtPageInfo | undefined): string {
26
- const paramsType = page
27
- ? paramsTypeFromDefinitions(page.paramDefinitions ?? [])
28
- : '{}';
29
-
30
- return `
31
- export type PageParams = ${paramsType};
32
-
33
- export type PageContext = {
34
- params: PageParams;
35
- data?: unknown;
36
- dev: boolean;
37
- };
38
-
39
- export function definePage<T extends (ctx: PageContext) => any>(fn: T): T {
40
- return fn;
41
- }
42
- `;
43
- }
package/src/page-index.ts DELETED
@@ -1,67 +0,0 @@
1
- import {
2
- compareRoutePriority,
3
- expandStaticPaths,
4
- fileNameFromRoute,
5
- } from './route-utils';
6
- import type { HtPageInfo, HtPageModule, StaticParamRecord } from './types';
7
- import { PLUGIN_NAME } from './constants';
8
- export async function buildPageIndex(args: {
9
- entries: HtPageInfo[];
10
- modulesByEntry: Map<string, HtPageModule>;
11
- cleanUrls: boolean;
12
- }): Promise<HtPageInfo[]> {
13
- const { entries, modulesByEntry, cleanUrls } = args;
14
- const pages: HtPageInfo[] = [];
15
-
16
- for (const entry of entries) {
17
- const mod = modulesByEntry.get(entry.entryPath) ?? {};
18
-
19
- if (entry.dynamic) {
20
- const rows =
21
- (mod.generateStaticParams
22
- ? await mod.generateStaticParams()
23
- : []) ?? [];
24
-
25
- pages.push(
26
- ...expandStaticPaths(
27
- {
28
- id: entry.id,
29
- entryPath: entry.entryPath,
30
- absolutePath: entry.absolutePath,
31
- relativePath: entry.relativePath,
32
- routePattern: entry.routePattern,
33
- dynamic: entry.dynamic,
34
- paramNames: entry.paramNames,
35
- } as Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,
36
- Array.isArray(rows) ? rows : [],
37
- cleanUrls,
38
- ),
39
- );
40
- } else {
41
- pages.push({
42
- ...entry,
43
- routePath: entry.routePattern,
44
- fileName: fileNameFromRoute(entry.routePattern, cleanUrls),
45
- params: {},
46
- });
47
- }
48
- }
49
-
50
- pages.sort((a, b) => compareRoutePriority(a.routePattern, b.routePattern));
51
-
52
- const seenRoutes = new Map<string, HtPageInfo>();
53
-
54
- for (const page of pages) {
55
- const existing = seenRoutes.get(page.routePath);
56
-
57
- if (existing) {
58
- throw new Error(
59
- `[${PLUGIN_NAME}] Duplicate route generated: "${page.routePath}" from "${existing.relativePath}" and "${page.relativePath}"`,
60
- );
61
- }
62
-
63
- seenRoutes.set(page.routePath, page);
64
- }
65
-
66
- return pages;
67
- }
package/src/path-utils.ts DELETED
@@ -1,30 +0,0 @@
1
- import path from 'node:path';
2
-
3
- export function toPosix(value: string): string {
4
- return value.replace(/\\/g, '/');
5
- }
6
-
7
- export function normalizeFsPath(value: string): string {
8
- return path.normalize(value);
9
- }
10
-
11
- export function normalizeRoutePath(value: string): string {
12
- const normalized = toPosix(value).replace(/\/+/g, '/');
13
- if (!normalized || normalized === '/') return '/';
14
- return normalized.startsWith('/') ? normalized : `/${normalized}`;
15
- }
16
-
17
- export function stripPageSuffix(
18
- filePath: string,
19
- extensions: string[],
20
- ): string {
21
- const normalized = toPosix(filePath);
22
-
23
- const match = [...extensions]
24
- .sort((a, b) => b.length - a.length)
25
- .find((ext) => normalized.endsWith(ext));
26
-
27
- if (!match) return normalized;
28
-
29
- return normalized.slice(0, -match.length);
30
- }
package/src/plugin.ts DELETED
@@ -1,513 +0,0 @@
1
- import pLimit from 'p-limit';
2
- import type { Plugin, ViteDevServer } from 'vite';
3
-
4
- import { discoverEntryPages } from './discover';
5
- import { installDevServer } from './dev-server';
6
- import { createPageModuleLoader, closePageModuleLoader } from './module-loader';
7
- import { buildPageIndex } from './page-index';
8
- import { renderPage } from './render-runtime';
9
- import {
10
- buildProcessedStaticAssets,
11
- collectStaticAssets,
12
- copyStaticAssetSource,
13
- } from './static-assets';
14
- import { validateHtmlAssetReferences } from './html-asset-validator';
15
- import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
16
- import {
17
- PLUGIN_NAME,
18
- VIRTUAL_BUILD_ENTRY_ID,
19
- VIRTUAL_PAGE_HELPER_ID,
20
- RESOLVED_VIRTUAL_PAGE_HELPER_PREFIX,
21
- } from './constants';
22
- import { generateTypedPageHelper } from './page-helper-generator';
23
-
24
- import fs from 'node:fs';
25
- import path from 'node:path';
26
-
27
- let hasWarnedESM = false;
28
-
29
- function warnIfNotESM(root: string) {
30
- try {
31
- const pkgPath = path.join(root, 'package.json');
32
-
33
- if (!fs.existsSync(pkgPath)) return;
34
-
35
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
36
-
37
- if (pkg.type !== 'module') {
38
- console.warn(
39
- `[${PLUGIN_NAME}] ⚠️ It is recommended to add "type": "module" to your package.json for optimal performance and to avoid Node ESM warnings.`,
40
- );
41
- }
42
- } catch {
43
- // silent — never break build
44
- }
45
- }
46
-
47
- function chunkArray<T>(items: T[], size: number): T[][] {
48
- const out: T[][] = [];
49
- for (let i = 0; i < items.length; i += size) {
50
- out.push(items.slice(i, i + size));
51
- }
52
- return out;
53
- }
54
-
55
- export function htPages(options: HtPagesPluginOptions = {}): Plugin {
56
- let root = process.cwd();
57
- let server: ViteDevServer | null = null;
58
- let devPages: HtPageInfo[] = [];
59
- let watcherAttached = false;
60
-
61
- const cleanUrls = options.cleanUrls ?? true;
62
- const pagesDir = options.pagesDir ?? 'src';
63
- const pageExtensions = options.pageExtensions?.length
64
- ? options.pageExtensions
65
- : ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
66
-
67
- function logDebug(enabled: boolean | undefined, ...args: unknown[]) {
68
- if (!enabled) return;
69
- console.log(`[${PLUGIN_NAME}]`, ...args);
70
- }
71
-
72
- async function loadDevPages(): Promise<HtPageInfo[]> {
73
- const entries = await discoverEntryPages(root, options);
74
- const modulesByEntry = new Map<string, HtPageModule>();
75
-
76
- logDebug(
77
- options.debug,
78
- 'discovered entries',
79
- entries.map((e) => e.relativePath),
80
- );
81
-
82
- if (!server) return [];
83
-
84
- const loadModule = await createPageModuleLoader({
85
- mode: 'dev',
86
- root,
87
- server,
88
- });
89
-
90
- for (const entry of entries) {
91
- const mod = await loadModule(entry.entryPath, entry.relativePath);
92
- modulesByEntry.set(entry.entryPath, mod);
93
- }
94
-
95
- devPages = await buildPageIndex({
96
- entries,
97
- modulesByEntry,
98
- cleanUrls,
99
- });
100
-
101
- logDebug(
102
- options.debug,
103
- 'dev pages',
104
- devPages.map((p) => `${p.routePath} -> ${p.relativePath}`),
105
- );
106
-
107
- return devPages;
108
- }
109
-
110
- async function buildPagesPipeline() {
111
- const entries = await discoverEntryPages(root, options);
112
- const modulesByEntry = new Map<string, HtPageModule>();
113
-
114
- const loadModule = await createPageModuleLoader({
115
- mode: 'build',
116
- root,
117
- });
118
-
119
- for (const entry of entries) {
120
- const mod = await loadModule(entry.entryPath, entry.relativePath);
121
- modulesByEntry.set(entry.entryPath, mod);
122
- }
123
-
124
- const pages = await buildPageIndex({
125
- entries,
126
- modulesByEntry,
127
- cleanUrls,
128
- });
129
-
130
- return { entries, modulesByEntry, pages };
131
- }
132
-
133
- return {
134
- name: PLUGIN_NAME,
135
-
136
- config(userConfig, env) {
137
- if (env.command !== 'build') return;
138
-
139
- const hasExplicitInput = userConfig.build?.rollupOptions?.input != null;
140
- if (hasExplicitInput) return;
141
-
142
- return {
143
- build: {
144
- rollupOptions: {
145
- input: VIRTUAL_BUILD_ENTRY_ID,
146
- },
147
- },
148
- };
149
- },
150
-
151
- resolveId(id, importer) {
152
- if (id === VIRTUAL_BUILD_ENTRY_ID) return id;
153
-
154
- if (id === VIRTUAL_PAGE_HELPER_ID && importer) {
155
- return `${RESOLVED_VIRTUAL_PAGE_HELPER_PREFIX}${importer}`;
156
- }
157
-
158
- return null;
159
- },
160
-
161
- async load(id) {
162
- if (id === VIRTUAL_BUILD_ENTRY_ID) {
163
- return 'export default {};';
164
- }
165
-
166
- if (id.startsWith(RESOLVED_VIRTUAL_PAGE_HELPER_PREFIX)) {
167
- const importer = id.slice(RESOLVED_VIRTUAL_PAGE_HELPER_PREFIX.length);
168
- const { pages } = await buildPagesPipeline();
169
-
170
- const normalizedImporter = path.resolve(importer);
171
-
172
- const page = pages.find(
173
- (candidate) => path.resolve(candidate.absolutePath) === normalizedImporter,
174
- );
175
-
176
- return generateTypedPageHelper(page);
177
- }
178
-
179
- return null;
180
- },
181
-
182
- configResolved(resolved) {
183
- root = options.root ? path.resolve(resolved.root, options.root) : resolved.root;
184
-
185
- if (!hasWarnedESM) {
186
- warnIfNotESM(root);
187
- hasWarnedESM = true;
188
- }
189
- },
190
-
191
- async buildStart() {
192
- const entries = await discoverEntryPages(root, options);
193
-
194
- for (const entry of entries) {
195
- this.addWatchFile(entry.entryPath);
196
- }
197
-
198
- const staticAssets = await collectStaticAssets({
199
- root,
200
- pagesDir,
201
- pageExtensions,
202
- });
203
-
204
- for (const asset of staticAssets) {
205
- this.addWatchFile(asset.absolutePath);
206
- }
207
-
208
- logDebug(
209
- options.debug,
210
- 'static assets',
211
- staticAssets.map((asset) => ({
212
- kind: asset.kind,
213
- input: asset.relativePathFromSrc,
214
- output: asset.outputFileName,
215
- })),
216
- );
217
- },
218
-
219
- configureServer(_server) {
220
- server = _server;
221
-
222
- installDevServer({
223
- server,
224
- root,
225
- pagesDir,
226
- getPages: async () => {
227
- if (devPages.length > 0) return devPages;
228
- return loadDevPages();
229
- },
230
- getEntries: async () => discoverEntryPages(root, options),
231
- });
232
-
233
- if (!watcherAttached) {
234
- watcherAttached = true;
235
-
236
- const reload = async (file: string) => {
237
- if (!file.includes(`${path.sep}src${path.sep}`) && !file.includes('/src/')) {
238
- return;
239
- }
240
-
241
- logDebug(options.debug, 'file changed', file);
242
-
243
- await loadDevPages();
244
-
245
- server?.ws.send({ type: 'full-reload' });
246
-
247
- };
248
-
249
- server.watcher.on('add', reload);
250
- server.watcher.on('change', reload);
251
- server.watcher.on('unlink', reload);
252
- }
253
-
254
- loadDevPages().catch((error) => {
255
- server?.config.logger.error(
256
- `[${PLUGIN_NAME}] loadDevPages failed: ${
257
- error instanceof Error ? error.stack ?? error.message : String(error)
258
- }`,
259
- );
260
- });
261
- },
262
-
263
- // async handleHotUpdate(ctx) {
264
- // if (!server) return;
265
-
266
- // logDebug(options.debug, 'file changed', ctx.file);
267
-
268
- // await loadDevPages();
269
-
270
- // server.ws.send({
271
- // type: 'full-reload',
272
- // });
273
-
274
- // return [];
275
- // },
276
-
277
- async generateBundle(_, bundle) {
278
- try {
279
- const { modulesByEntry, pages } = await buildPagesPipeline();
280
-
281
- const staticAssets = await collectStaticAssets({
282
- root,
283
- pagesDir,
284
- pageExtensions,
285
- });
286
-
287
- logDebug(
288
- options.debug,
289
- 'emitting pages',
290
- pages.map((p) => p.fileName),
291
- );
292
-
293
- logDebug(
294
- options.debug,
295
- 'emitting static assets',
296
- staticAssets.map((asset) => ({
297
- kind: asset.kind,
298
- input: asset.relativePathFromSrc,
299
- output: asset.outputFileName,
300
- })),
301
- );
302
-
303
- const limit = pLimit(options.renderConcurrency ?? 8);
304
- const batchSize =
305
- options.renderBatchSize ??
306
- Math.max(options.renderConcurrency ?? 8, 32);
307
-
308
- const processedOutputs = await buildProcessedStaticAssets({
309
- root,
310
- pagesDir,
311
- assets: staticAssets,
312
- minify: true,
313
- sourcemap: false,
314
- });
315
-
316
- for (const [fileName, source] of processedOutputs) {
317
- this.emitFile({
318
- type: 'asset',
319
- fileName,
320
- source,
321
- });
322
- }
323
-
324
- for (const asset of staticAssets) {
325
- if (asset.kind !== 'copy') continue;
326
-
327
- const source = await copyStaticAssetSource(asset);
328
-
329
- this.emitFile({
330
- type: 'asset',
331
- fileName: asset.outputFileName,
332
- source,
333
- });
334
- }
335
-
336
- for (const batch of chunkArray(pages, batchSize)) {
337
- await Promise.all(
338
- batch.map((page) =>
339
- limit(async () => {
340
- const mod = modulesByEntry.get(page.entryPath);
341
-
342
- if (!mod) {
343
- throw new Error(
344
- `[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`,
345
- );
346
- }
347
-
348
- const html = await renderPage(page, mod, false);
349
-
350
- validateHtmlAssetReferences({
351
- root,
352
- pagesDir,
353
- html,
354
- pluginName: PLUGIN_NAME,
355
- pageLabel: page.relativePath,
356
- missingAssets: options.missingAssets ?? 'error',
357
- });
358
-
359
- this.emitFile({
360
- type: 'asset',
361
- fileName: options.mapOutputPath?.(page) ?? page.fileName,
362
- source: html,
363
- });
364
- }),
365
- ),
366
- );
367
- }
368
-
369
- const notFoundPage = pages.find((p) => p.routePath === '/404');
370
-
371
- if (notFoundPage) {
372
- const mod = modulesByEntry.get(notFoundPage.entryPath);
373
-
374
- if (!mod) {
375
- throw new Error(
376
- `[${PLUGIN_NAME}] Missing module for 404 page entry: ${notFoundPage.entryPath}`,
377
- );
378
- }
379
-
380
- const html = await renderPage(notFoundPage, mod, false);
381
-
382
- validateHtmlAssetReferences({
383
- root,
384
- pagesDir,
385
- html,
386
- pluginName: PLUGIN_NAME,
387
- pageLabel: notFoundPage.relativePath,
388
- missingAssets: options.missingAssets ?? 'error',
389
- });
390
-
391
- this.emitFile({
392
- type: 'asset',
393
- fileName: '404.html',
394
- source: html,
395
- });
396
-
397
- logDebug(options.debug, 'generated 404.html from user page');
398
- } else {
399
- const default404 = `<!doctype html>
400
- <html lang="en">
401
- <head>
402
- <meta charset="UTF-8" />
403
- <meta name="viewport" content="width=device-width, initial-scale=1" />
404
- <title>404 - Page Not Found</title>
405
- <style>
406
- :root {
407
- color-scheme: light dark;
408
- }
409
- body {
410
- margin: 0;
411
- font-family: system-ui, sans-serif;
412
- min-height: 100vh;
413
- display: grid;
414
- place-items: center;
415
- padding: 2rem;
416
- }
417
- main {
418
- max-width: 40rem;
419
- text-align: center;
420
- }
421
- h1 {
422
- font-size: 3rem;
423
- margin: 0 0 1rem;
424
- }
425
- p {
426
- margin: 0.5rem 0;
427
- line-height: 1.5;
428
- }
429
- a {
430
- color: inherit;
431
- }
432
- </style>
433
- </head>
434
- <body>
435
- <main>
436
- <h1>404</h1>
437
- <p>Page not found.</p>
438
- <p><a href="/">Go back home</a></p>
439
- </main>
440
- </body>
441
- </html>
442
- `;
443
-
444
- this.emitFile({
445
- type: 'asset',
446
- fileName: '404.html',
447
- source: default404,
448
- });
449
-
450
- logDebug(options.debug, 'generated default 404.html');
451
- }
452
-
453
- const sitemapBase = options.site ?? '';
454
-
455
- const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))].filter(
456
- (route) => !route.includes(':') && !route.includes('*'),
457
- );
458
-
459
- if (sitemapBase && sitemapRoutes.length > 0) {
460
- const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${sitemapRoutes
461
- .map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`)
462
- .join('\n')}\n</urlset>\n`;
463
-
464
- this.emitFile({
465
- type: 'asset',
466
- fileName: 'sitemap.xml',
467
- source: sitemap,
468
- });
469
-
470
- logDebug(options.debug, 'generated sitemap.xml');
471
- }
472
-
473
- const rss = options.rss;
474
-
475
- if (rss?.site) {
476
- const routePrefix = rss.routePrefix ?? '/blog';
477
-
478
- const rssItems = pages
479
- .filter((page) => page.routePath.startsWith(routePrefix))
480
- .map((page) => {
481
- const url = `${rss.site}${page.routePath}`;
482
-
483
- return ` <item>\n <title>${page.routePath}</title>\n <link>${url}</link>\n <guid>${url}</guid>\n </item>`;
484
- })
485
- .join('\n');
486
-
487
- 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`;
488
-
489
- this.emitFile({
490
- type: 'asset',
491
- fileName: 'rss.xml',
492
- source: rssXml,
493
- });
494
-
495
- logDebug(options.debug, 'generated rss.xml');
496
- }
497
-
498
- for (const [fileName, output] of Object.entries(bundle)) {
499
- if (
500
- output.type === 'chunk' &&
501
- output.facadeModuleId === VIRTUAL_BUILD_ENTRY_ID
502
- ) {
503
- delete bundle[fileName];
504
- }
505
- }
506
- } finally {
507
- await closePageModuleLoader();
508
- }
509
- }
510
- };
511
- }
512
-
513
- export default htPages;
@@ -1,36 +0,0 @@
1
- import { invalidHtmlReturn, pageError, missingDefaultExport } from './errors';
2
- import type { HtPageInfo, HtPageModule, HtPageRenderContext } from './types';
3
-
4
- export async function renderPage(
5
- page: HtPageInfo,
6
- mod: HtPageModule,
7
- dev = false,
8
- ): Promise<string> {
9
- const ctx: HtPageRenderContext = {
10
- page,
11
- params: page.params,
12
- dev,
13
- };
14
-
15
- try {
16
- if (typeof mod.data === 'function') {
17
- ctx.data = await mod.data(ctx);
18
- }
19
-
20
- const entry = mod.default;
21
-
22
- if (entry == null) {
23
- throw missingDefaultExport(page);
24
- }
25
-
26
- const html = typeof entry === 'function' ? await entry(ctx) : entry;
27
-
28
- if (typeof html !== 'string') {
29
- throw invalidHtmlReturn(page, html);
30
- }
31
-
32
- return html;
33
- } catch (error) {
34
- throw pageError(page, error);
35
- }
36
- }