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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-html-pages",
3
- "version": "1.3.7",
3
+ "version": "1.4.1",
4
4
  "author": "Paul Browne",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,7 +16,10 @@
16
16
  "javascript-to-html",
17
17
  "static-html",
18
18
  "blog-generator"
19
- ],
19
+ ],
20
+ "files": [
21
+ "dist/**/*"
22
+ ],
20
23
  "description": "Minimal static site generation (SSG) for Vite using JavaScript functions that return HTML",
21
24
  "homepage": "https://github.com/paul-browne/vite-plugin-html-pages",
22
25
  "repository": {
@@ -29,10 +32,15 @@
29
32
  "main": "./dist/index.js",
30
33
  "module": "./dist/index.js",
31
34
  "types": "./dist/index.d.ts",
35
+ "sideEffects": false,
32
36
  "exports": {
33
37
  ".": {
34
38
  "types": "./dist/index.d.ts",
35
39
  "import": "./dist/index.js"
40
+ },
41
+ "./jsx-runtime": {
42
+ "types": "./dist/jsx-runtime.d.ts",
43
+ "import": "./dist/jsx-runtime.js"
36
44
  }
37
45
  },
38
46
  "scripts": {
@@ -46,9 +54,10 @@
46
54
  "vite": ">=5"
47
55
  },
48
56
  "dependencies": {
57
+ "esbuild": "^0.24.0",
49
58
  "fast-glob": "^3.3.3",
50
- "p-limit": "^4.0.0",
51
- "esbuild": "^0.24.0"
59
+ "javascript-to-html": "^1.1.3",
60
+ "p-limit": "^4.0.0"
52
61
  },
53
62
  "engines": {
54
63
  "node": ">=18"
package/TODO DELETED
@@ -1 +0,0 @@
1
- - Update README
package/src/constants.ts DELETED
@@ -1,6 +0,0 @@
1
- export const PLUGIN_NAME = 'vite-plugin-html-pages';
2
- export const VIRTUAL_BUILD_ENTRY_ID = `\0${PLUGIN_NAME}:build-entry`;
3
- export const VIRTUAL_PAGE_HELPER_ID = `${PLUGIN_NAME}/page`;
4
- export const RESOLVED_VIRTUAL_PAGE_HELPER_PREFIX =`\0${PLUGIN_NAME}/page:`;
5
- export const VIRTUAL_MANIFEST_ID = `\0virtual:${PLUGIN_NAME}-manifest`;
6
- export const CACHE_DIR_NAME = `node_modules/.cache/${PLUGIN_NAME}`;
package/src/dev-server.ts DELETED
@@ -1,131 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import type { ViteDevServer } from 'vite';
4
-
5
- import { renderPage } from './render-runtime';
6
- import type { HtPageInfo, HtPageModule } from './types';
7
- import { PLUGIN_NAME } from './constants';
8
- import { createPageModuleLoader } from './module-loader';
9
-
10
- function isStaticAssetRequest(url: string): boolean {
11
- return (
12
- url.endsWith('.css') ||
13
- url.endsWith('.js') ||
14
- url.endsWith('.mjs') ||
15
- url.endsWith('.ts') ||
16
- url.endsWith('.png') ||
17
- url.endsWith('.jpg') ||
18
- url.endsWith('.jpeg') ||
19
- url.endsWith('.gif') ||
20
- url.endsWith('.svg') ||
21
- url.endsWith('.webp') ||
22
- url.endsWith('.ico') ||
23
- url.endsWith('.woff') ||
24
- url.endsWith('.woff2') ||
25
- url.endsWith('.ttf') ||
26
- url.endsWith('.otf')
27
- );
28
- }
29
-
30
- function shouldSkipHtmlRouting(url: string, pagesDir: string): boolean {
31
- return (
32
- url.startsWith('/@vite') ||
33
- url.startsWith('/@fs/') ||
34
- url.startsWith('/node_modules/') ||
35
- url.startsWith(`/${pagesDir}/`) ||
36
- url === '/favicon.ico' ||
37
- isStaticAssetRequest(url)
38
- );
39
- }
40
-
41
- function tryRewriteRootAssetToSrc(
42
- root: string,
43
- pagesDir: string,
44
- url: string,
45
- ): string | null {
46
- if (!url.startsWith('/')) return null;
47
- if (!isStaticAssetRequest(url)) return null;
48
- if (url.startsWith(`/${pagesDir}/`)) return null;
49
-
50
- const candidate = path.join(root, pagesDir, url.slice(1));
51
-
52
- if (fs.existsSync(candidate)) {
53
- return `/${pagesDir}/${url.slice(1)}`;
54
- }
55
-
56
- return null;
57
- }
58
-
59
- function shouldUseDynamicRendering(mod: HtPageModule): boolean {
60
- return mod.dynamic === true || mod.prerender === false;
61
- }
62
-
63
- export function installDevServer(args: {
64
- server: ViteDevServer;
65
- root: string;
66
- pagesDir: string;
67
- getPages: () => Promise<HtPageInfo[]>;
68
- getEntries?: () => Promise<HtPageInfo[]>;
69
- }) {
70
- const { server, root, pagesDir, getPages } = args;
71
-
72
- server.middlewares.use(async (req, res, next) => {
73
- try {
74
- const originalUrl = req.url ?? '/';
75
- const url = originalUrl.split('?')[0];
76
-
77
- const rewrittenAssetUrl = tryRewriteRootAssetToSrc(root, pagesDir, url);
78
- if (rewrittenAssetUrl) {
79
- req.url = rewrittenAssetUrl + originalUrl.slice(url.length);
80
- return next();
81
- }
82
-
83
- if (shouldSkipHtmlRouting(url, pagesDir)) {
84
- return next();
85
- }
86
-
87
- const pages = await getPages();
88
-
89
- const page = pages.find((p) => p.routePath === url);
90
-
91
- if (!page) {
92
- return next();
93
- }
94
-
95
- const loadModule = await createPageModuleLoader({
96
- mode: 'dev',
97
- root,
98
- server,
99
- });
100
-
101
- const mod = await loadModule(page.entryPath, page.relativePath);
102
-
103
- if (!mod) {
104
- return next();
105
- }
106
-
107
- if (!shouldUseDynamicRendering(mod) && page.dynamic) {
108
- return next();
109
- }
110
-
111
- const html = await renderPage(page, mod, true);
112
- const transformedHtml = await server.transformIndexHtml(
113
- url,
114
- html,
115
- req.originalUrl,
116
- );
117
-
118
- res.statusCode = 200;
119
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
120
- res.end(transformedHtml);
121
- } catch (error) {
122
- server.config.logger.error(
123
- `[${PLUGIN_NAME}] dev server render failed: ${
124
- error instanceof Error ? error.stack ?? error.message : String(error)
125
- }`,
126
- );
127
-
128
- next(error as Error);
129
- }
130
- });
131
- }
package/src/discover.ts DELETED
@@ -1,84 +0,0 @@
1
- import path from 'node:path';
2
- import { normalizeFsPath, toPosix } from './path-utils';
3
- import { isDynamicPage, toRoutePattern } from './route-utils';
4
- import { extractRouteParamDefinitions } from './route-params';
5
- import type { HtPageInfo, HtPagesPluginOptions } from './types';
6
- import { PLUGIN_NAME } from './constants';
7
-
8
- function buildDefaultIncludeGlobs(
9
- pagesDir: string,
10
- pageExtensions: string[],
11
- ): string[] {
12
- return pageExtensions.map((ext) => {
13
- const cleanExt = ext.startsWith('.') ? ext.slice(1) : ext;
14
- return `${pagesDir}/**/*.${cleanExt}`;
15
- });
16
- }
17
-
18
- export async function discoverEntryPages(
19
- root: string,
20
- options: HtPagesPluginOptions,
21
- ): Promise<HtPageInfo[]> {
22
- const fgModule = await import('fast-glob');
23
- const fg = (fgModule.default ?? fgModule) as typeof import('fast-glob');
24
-
25
- const pagesDir = options.pagesDir ?? 'src';
26
- const pageExtensions = options.pageExtensions?.length
27
- ? options.pageExtensions
28
- : ['.ht.js', '.html.js', '.ht.ts', '.html.ts'];
29
-
30
- const include = Array.isArray(options.include)
31
- ? options.include
32
- : options.include
33
- ? [options.include]
34
- : buildDefaultIncludeGlobs(pagesDir, pageExtensions);
35
-
36
- const exclude = Array.isArray(options.exclude)
37
- ? options.exclude
38
- : options.exclude
39
- ? [options.exclude]
40
- : [];
41
-
42
- const pagesRoot = normalizeFsPath(path.join(root, pagesDir));
43
-
44
- const files = await fg.glob(include, {
45
- cwd: root,
46
- ignore: exclude,
47
- absolute: true,
48
- });
49
-
50
- return files
51
- .sort()
52
- .map((absolutePath) => {
53
- const entryPath = normalizeFsPath(absolutePath);
54
- const relativePath = toPosix(path.relative(root, entryPath));
55
- const relativeFromPagesDir = toPosix(path.relative(pagesRoot, entryPath));
56
-
57
- if (
58
- relativeFromPagesDir.startsWith('../') ||
59
- relativeFromPagesDir === '..'
60
- ) {
61
- throw new Error(
62
- `[${PLUGIN_NAME}] Page is outside pagesDir: ${entryPath} (pagesDir: ${pagesDir})`,
63
- );
64
- }
65
-
66
- const dynamic = isDynamicPage(relativeFromPagesDir);
67
- const routePattern = toRoutePattern(relativeFromPagesDir, pageExtensions);
68
- const paramDefinitions = extractRouteParamDefinitions(routePattern);
69
-
70
- return {
71
- id: entryPath,
72
- entryPath,
73
- absolutePath: entryPath,
74
- relativePath,
75
- routePattern,
76
- routePath: routePattern,
77
- fileName: '',
78
- dynamic,
79
- paramNames: paramDefinitions.map((p) => p.name),
80
- paramDefinitions,
81
- params: {},
82
- } satisfies HtPageInfo;
83
- });
84
- }
package/src/env.d.ts DELETED
@@ -1,11 +0,0 @@
1
- declare module 'vite-plugin-html-pages/page' {
2
- export type PageParams = Record<string, string | string[] | undefined>;
3
-
4
- export type PageContext = {
5
- params: PageParams;
6
- data?: unknown;
7
- dev: boolean;
8
- };
9
-
10
- export function definePage<T extends (ctx: PageContext) => any>(fn: T): T;
11
- }
package/src/errors.ts DELETED
@@ -1,32 +0,0 @@
1
- import type { HtPageInfo } from './types';
2
- import { PLUGIN_NAME } from './constants';
3
- export function invalidHtmlReturn(
4
- page: HtPageInfo,
5
- value: unknown,
6
- ): Error {
7
- return new Error(
8
- `[${PLUGIN_NAME}] Page "${page.relativePath}" must resolve to an HTML string, got ${typeof value}`,
9
- );
10
- }
11
-
12
- export function missingDefaultExport(page: HtPageInfo): Error {
13
- return new Error(
14
- `[${PLUGIN_NAME}] Page "${page.relativePath}" does not export a default renderer`,
15
- );
16
- }
17
-
18
- export function pageError(page: HtPageInfo, cause: unknown): Error {
19
- const message = `[${PLUGIN_NAME}] Failed to render "${page.relativePath}" at route "${page.routePath}"`;
20
-
21
- if (cause instanceof Error) {
22
- const err = new Error(`${message}: ${cause.message}`);
23
-
24
- if (cause.stack) {
25
- err.stack = `${err.stack}\nCaused by:\n${cause.stack}`;
26
- }
27
-
28
- return err;
29
- }
30
-
31
- return new Error(`${message}: ${String(cause)}`);
32
- }
@@ -1,141 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { CACHE_DIR_NAME } from './constants';
5
-
6
- export type FetchCacheMode = 'auto' | 'memory' | 'fs' | 'none';
7
- export interface FetchWithCacheOptions {
8
- maxAge?: number;
9
- cacheKey?: string;
10
- forceRefresh?: boolean;
11
- cache?: FetchCacheMode;
12
- }
13
-
14
- type CachedResponseRecord = {
15
- timestamp: number;
16
- status: number;
17
- statusText: string;
18
- headers: [string, string][];
19
- body: string;
20
- };
21
-
22
- const memoryCache = new Map<string, CachedResponseRecord>();
23
-
24
- function createDefaultCacheKey(
25
- input: RequestInfo | URL,
26
- init?: RequestInit,
27
- ): string {
28
- const raw = JSON.stringify({
29
- url: String(input),
30
- method: init?.method ?? 'GET',
31
- headers: init?.headers ?? {},
32
- body: init?.body ?? null,
33
- });
34
-
35
- return createHash('sha256').update(raw).digest('hex');
36
- }
37
-
38
- function getCacheFilePath(cacheKey: string): string {
39
- return path.join(process.cwd(), CACHE_DIR_NAME, 'fetch', `${cacheKey}.json`);
40
- }
41
-
42
- function getEffectiveCacheMode(
43
- mode: FetchCacheMode | undefined,
44
- ): Exclude<FetchCacheMode, 'auto'> {
45
- if (mode === 'memory' || mode === 'fs' || mode === 'none') {
46
- return mode;
47
- }
48
-
49
- return process.env.NODE_ENV === 'production' ? 'fs' : 'memory';
50
- }
51
-
52
- function toResponse(cached: CachedResponseRecord): Response {
53
- return new Response(cached.body, {
54
- status: cached.status,
55
- statusText: cached.statusText,
56
- headers: cached.headers,
57
- });
58
- }
59
-
60
- function isFresh(cached: CachedResponseRecord, maxAgeSeconds: number): boolean {
61
- const ageSeconds = (Date.now() - cached.timestamp) / 1000;
62
- return ageSeconds <= maxAgeSeconds;
63
- }
64
-
65
- export function clearMemoryFetchCache(): void {
66
- memoryCache.clear();
67
- }
68
-
69
- export function deleteMemoryFetchCache(cacheKey: string): void {
70
- memoryCache.delete(cacheKey);
71
- }
72
-
73
- export async function fetchWithCache(
74
- input: RequestInfo | URL,
75
- init?: RequestInit,
76
- options: FetchWithCacheOptions = {},
77
- ): Promise<Response> {
78
- const maxAge = options.maxAge ?? 60 * 60;
79
- const method = (init?.method ?? 'GET').toUpperCase();
80
-
81
- if (method !== 'GET' && !options.cacheKey) {
82
- return fetch(input, init);
83
- }
84
-
85
- const cacheMode = getEffectiveCacheMode(options.cache);
86
- const cacheKey = options.cacheKey ?? createDefaultCacheKey(input, init);
87
-
88
- if (cacheMode === 'none') {
89
- return fetch(input, init);
90
- }
91
-
92
- if (cacheMode === 'memory' && !options.forceRefresh) {
93
- const cached = memoryCache.get(cacheKey);
94
-
95
- if (cached && isFresh(cached, maxAge)) {
96
- return toResponse(cached);
97
- }
98
- }
99
-
100
- const filePath = getCacheFilePath(cacheKey);
101
-
102
- if (cacheMode === 'fs') {
103
- await fs.mkdir(path.dirname(filePath), { recursive: true });
104
-
105
- if (!options.forceRefresh) {
106
- try {
107
- const raw = await fs.readFile(filePath, 'utf8');
108
- const cached = JSON.parse(raw) as CachedResponseRecord;
109
-
110
- if (isFresh(cached, maxAge)) {
111
- return toResponse(cached);
112
- }
113
- } catch {
114
- // cache miss or invalid cache; fetch fresh
115
- }
116
- }
117
- }
118
-
119
- const res = await fetch(input, init);
120
- const body = await res.text();
121
-
122
- const record: CachedResponseRecord = {
123
- timestamp: Date.now(),
124
- status: res.status,
125
- statusText: res.statusText,
126
- headers: [...res.headers.entries()],
127
- body,
128
- };
129
-
130
- if (cacheMode === 'memory') {
131
- memoryCache.set(cacheKey, record);
132
- } else if (cacheMode === 'fs') {
133
- await fs.writeFile(filePath, JSON.stringify(record), 'utf8');
134
- }
135
-
136
- return new Response(body, {
137
- status: res.status,
138
- statusText: res.statusText,
139
- headers: res.headers,
140
- });
141
- }
@@ -1,176 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- export interface HtmlAssetValidationOptions {
5
- root: string;
6
- pagesDir: string;
7
- html: string;
8
- pluginName: string;
9
- pageLabel?: string;
10
- missingAssets?: 'error' | 'warn';
11
- }
12
-
13
- function stripQueryAndHash(url: string): string {
14
- return url.split('#')[0].split('?')[0];
15
- }
16
-
17
- function isLocalRootUrl(url: string): boolean {
18
- return !!url && url.startsWith('/') && !url.startsWith('//');
19
- }
20
-
21
- function fileExistsForPublicUrl(root: string, pagesDir: string, url: string): boolean {
22
- const clean = stripQueryAndHash(url).slice(1);
23
-
24
- const fromSrc = path.join(root, pagesDir, clean);
25
- if (fs.existsSync(fromSrc)) return true;
26
-
27
- const fromPublic = path.join(root, 'public', clean);
28
- if (fs.existsSync(fromPublic)) return true;
29
-
30
- return false;
31
- }
32
-
33
- function collectScriptSrcs(html: string): string[] {
34
- const out: string[] = [];
35
-
36
- for (const match of html.matchAll(
37
- /<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi,
38
- )) {
39
- out.push(match[1]);
40
- }
41
-
42
- return out;
43
- }
44
-
45
- function collectStylesheetHrefs(html: string): string[] {
46
- const out: string[] = [];
47
-
48
- for (const match of html.matchAll(
49
- /<link\b[^>]*\brel=["']stylesheet["'][^>]*\bhref=["']([^"']+)["'][^>]*>/gi,
50
- )) {
51
- out.push(match[1]);
52
- }
53
-
54
- return out;
55
- }
56
-
57
- function collectLiteralDynamicImports(html: string): string[] {
58
- const out: string[] = [];
59
-
60
- for (const match of html.matchAll(
61
- /import\s*\(\s*["']([^"'`]+)["']\s*\)/gi,
62
- )) {
63
- out.push(match[1]);
64
- }
65
-
66
- return out;
67
- }
68
-
69
- function unique(values: string[]): string[] {
70
- return [...new Set(values)];
71
- }
72
-
73
- function formatPageLabel(pageLabel?: string): string {
74
- return pageLabel ? ` (${pageLabel})` : '';
75
- }
76
-
77
- function missingAssetMessage(args: {
78
- pluginName: string;
79
- kind: string;
80
- url: string;
81
- root: string;
82
- pagesDir: string;
83
- pageLabel?: string;
84
- }): string {
85
- const { pluginName, kind, url, root, pagesDir, pageLabel } = args;
86
- const clean = stripQueryAndHash(url).slice(1);
87
- const pageSuffix = formatPageLabel(pageLabel);
88
-
89
- return (
90
- `[${pluginName}] Missing ${kind}${pageSuffix}: ${url}\n` +
91
- `Expected one of:\n` +
92
- `- ${path.join(root, pagesDir, clean)}\n` +
93
- `- ${path.join(root, 'public', clean)}`
94
- );
95
- }
96
-
97
- function reportMissing(args: {
98
- mode: 'error' | 'warn';
99
- pluginName: string;
100
- kind: string;
101
- url: string;
102
- root: string;
103
- pagesDir: string;
104
- pageLabel?: string;
105
- }) {
106
- const message = missingAssetMessage(args);
107
-
108
- if (args.mode === 'warn') {
109
- console.warn(`⚠️ ${message}`);
110
- return;
111
- }
112
-
113
- throw new Error(message);
114
- }
115
-
116
- export function validateHtmlAssetReferences(
117
- options: HtmlAssetValidationOptions,
118
- ): void {
119
- const {
120
- root,
121
- pagesDir,
122
- html,
123
- pluginName,
124
- pageLabel,
125
- missingAssets = 'error',
126
- } = options;
127
-
128
- const scriptSrcs = unique(collectScriptSrcs(html)).filter(isLocalRootUrl);
129
- const stylesheetHrefs = unique(collectStylesheetHrefs(html)).filter(isLocalRootUrl);
130
- const literalDynamicImports = unique(collectLiteralDynamicImports(html)).filter(
131
- isLocalRootUrl,
132
- );
133
-
134
- for (const url of scriptSrcs) {
135
- if (!fileExistsForPublicUrl(root, pagesDir, url)) {
136
- reportMissing({
137
- mode: missingAssets,
138
- pluginName,
139
- kind: 'JavaScript asset',
140
- url,
141
- root,
142
- pagesDir,
143
- pageLabel,
144
- });
145
- }
146
- }
147
-
148
- for (const url of stylesheetHrefs) {
149
- if (!fileExistsForPublicUrl(root, pagesDir, url)) {
150
- reportMissing({
151
- mode: missingAssets,
152
- pluginName,
153
- kind: 'stylesheet asset',
154
- url,
155
- root,
156
- pagesDir,
157
- pageLabel,
158
- });
159
- }
160
- }
161
-
162
- for (const url of literalDynamicImports) {
163
- if (!fileExistsForPublicUrl(root, pagesDir, url)) {
164
- console.warn(
165
- `⚠️ ${missingAssetMessage({
166
- pluginName,
167
- kind: 'literal dynamic import',
168
- url,
169
- root,
170
- pagesDir,
171
- pageLabel,
172
- })}`,
173
- );
174
- }
175
- }
176
- }
package/src/index.ts DELETED
@@ -1,12 +0,0 @@
1
- export { htPages as default } from './plugin';
2
- export { fetchWithCache } from './fetch-cache';
3
-
4
- export type {
5
- HtPageInfo,
6
- HtPageModule,
7
- HtPagesPluginOptions,
8
- HtPageRenderContext,
9
- StaticParamRecord,
10
- } from './types';
11
-
12
- export type { FetchWithCacheOptions } from './fetch-cache';
@@ -1,58 +0,0 @@
1
- import path from 'node:path';
2
- import { createServer, type InlineConfig, type ViteDevServer } from 'vite';
3
- import type { HtPageModule } from './types';
4
-
5
- export type PageModuleLoader = (
6
- entryPath: string,
7
- relativePath: string,
8
- ) => Promise<HtPageModule>;
9
-
10
- let buildServer: ViteDevServer | null = null;
11
-
12
- export async function createPageModuleLoader(args: {
13
- mode: 'dev' | 'build';
14
- root: string;
15
- server?: ViteDevServer | null;
16
- }): Promise<PageModuleLoader> {
17
- const { mode, root, server } = args;
18
-
19
- if (mode === 'dev') {
20
- if (!server) {
21
- throw new Error('[vite-plugin-html-pages] dev server not available');
22
- }
23
-
24
- return async (_entryPath, relativePath) => {
25
- const mod = await server.ssrLoadModule(`/${relativePath}`);
26
- return mod as HtPageModule;
27
- };
28
- }
29
-
30
- if (!buildServer) {
31
- const config: InlineConfig = {
32
- root,
33
- configFile: false,
34
- logLevel: 'error',
35
- appType: 'custom',
36
- server: {
37
- middlewareMode: true,
38
- },
39
- };
40
-
41
- buildServer = await createServer(config);
42
- }
43
-
44
- return async (entryPath) => {
45
- const relativePath =
46
- '/' + path.relative(root, entryPath).replace(/\\/g, '/');
47
-
48
- const mod = await buildServer!.ssrLoadModule(relativePath);
49
- return mod as HtPageModule;
50
- };
51
- }
52
-
53
- export async function closePageModuleLoader(): Promise<void> {
54
- if (buildServer) {
55
- await buildServer.close();
56
- buildServer = null;
57
- }
58
- }