vite-plugin-htjs-pages 0.2.0
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/README.md +629 -0
- package/dist/chunk-XFMAP2PF.js +23177 -0
- package/dist/chunk-XFMAP2PF.js.map +1 -0
- package/dist/index.cjs +23499 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +447 -0
- package/dist/index.js.map +1 -0
- package/dist/watch-KN535AIO.js +6933 -0
- package/dist/watch-KN535AIO.js.map +1 -0
- package/package.json +35 -0
- package/src/dev-server.ts +34 -0
- package/src/discover.ts +38 -0
- package/src/errors.ts +19 -0
- package/src/index.ts +8 -0
- package/src/manifest.ts +25 -0
- package/src/page-index.ts +44 -0
- package/src/path-utils.ts +20 -0
- package/src/plugin.ts +154 -0
- package/src/render-bundle.ts +53 -0
- package/src/render-runtime.ts +27 -0
- package/src/route-utils.ts +114 -0
- package/src/types.ts +41 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-htjs-pages",
|
|
3
|
+
"author": "Paul Browne",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Small HT.js pages plugin for Vite with dynamic routes, catch-all routes, and static params",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"vite": "^5 || ^6"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"fast-glob": "^3.3.3",
|
|
26
|
+
"p-limit": "^4.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.17.0",
|
|
30
|
+
"rollup": "^4.34.6",
|
|
31
|
+
"tsup": "^8.3.6",
|
|
32
|
+
"typescript": "^5.7.3",
|
|
33
|
+
"vite": "^6.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
import { renderPage } from './render-runtime';
|
|
3
|
+
import { routeMatch } from './route-utils';
|
|
4
|
+
import type { HtPageInfo, HtPageModule } from './types';
|
|
5
|
+
|
|
6
|
+
export function installDevServer(args: {
|
|
7
|
+
server: ViteDevServer;
|
|
8
|
+
getPages: () => HtPageInfo[];
|
|
9
|
+
}): void {
|
|
10
|
+
const { server, getPages } = args;
|
|
11
|
+
|
|
12
|
+
server.middlewares.use(async (req, res, next) => {
|
|
13
|
+
if (!req.url || req.method !== 'GET') return next();
|
|
14
|
+
|
|
15
|
+
const pathname = req.url.split('?')[0];
|
|
16
|
+
const pages = getPages();
|
|
17
|
+
|
|
18
|
+
for (const page of pages) {
|
|
19
|
+
const params = routeMatch(page.routePattern, pathname);
|
|
20
|
+
if (!params) continue;
|
|
21
|
+
|
|
22
|
+
const mod = (await server.ssrLoadModule(page.entryPath)) as HtPageModule;
|
|
23
|
+
const resolvedPage = { ...page, routePath: pathname || '/', params };
|
|
24
|
+
const html = await renderPage(resolvedPage, mod, true);
|
|
25
|
+
|
|
26
|
+
res.statusCode = 200;
|
|
27
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
28
|
+
res.end(html);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
}
|
package/src/discover.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fg from 'fast-glob';
|
|
3
|
+
import { normalizeFsPath, toPosix } from './path-utils';
|
|
4
|
+
import { getParamNames, isDynamicPage, toRoutePattern } from './route-utils';
|
|
5
|
+
import type { HtPageInfo, HtPagesPluginOptions } from './types';
|
|
6
|
+
|
|
7
|
+
export async function discoverEntryPages(root: string, options: HtPagesPluginOptions): Promise<HtPageInfo[]> {
|
|
8
|
+
const include = Array.isArray(options.include) ? options.include : [options.include ?? 'src/**/*.ht.js'];
|
|
9
|
+
const exclude = Array.isArray(options.exclude) ? options.exclude : options.exclude ? [options.exclude] : [];
|
|
10
|
+
const pagesDir = options.pagesDir ?? 'src';
|
|
11
|
+
|
|
12
|
+
const files = await fg(include, {
|
|
13
|
+
cwd: root,
|
|
14
|
+
ignore: exclude,
|
|
15
|
+
absolute: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return files.sort().map((absolutePath) => {
|
|
19
|
+
const entryPath = normalizeFsPath(absolutePath);
|
|
20
|
+
const relativePath = toPosix(path.relative(root, entryPath));
|
|
21
|
+
const relativeFromPagesDir = toPosix(path.relative(path.join(root, pagesDir), entryPath));
|
|
22
|
+
const dynamic = isDynamicPage(relativeFromPagesDir);
|
|
23
|
+
const routePattern = toRoutePattern(relativeFromPagesDir);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
id: entryPath,
|
|
27
|
+
entryPath,
|
|
28
|
+
absolutePath: entryPath,
|
|
29
|
+
relativePath,
|
|
30
|
+
routePattern,
|
|
31
|
+
routePath: routePattern,
|
|
32
|
+
fileName: '',
|
|
33
|
+
dynamic,
|
|
34
|
+
paramNames: getParamNames(relativeFromPagesDir),
|
|
35
|
+
params: {},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HtPageInfo } from './types';
|
|
2
|
+
|
|
3
|
+
export function invalidHtmlReturn(page: HtPageInfo, value: unknown): Error {
|
|
4
|
+
return new Error(
|
|
5
|
+
`[vite-plugin-ht-pages] Page "${page.relativePath}" must resolve to an HTML string, got ${typeof value}`,
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function pageError(page: HtPageInfo, cause: unknown): Error {
|
|
10
|
+
const message = `[vite-plugin-ht-pages] Failed to render ${page.relativePath} (${page.routePath})`;
|
|
11
|
+
if (cause instanceof Error && cause.stack) {
|
|
12
|
+
const err = new Error(message);
|
|
13
|
+
err.stack = `${err.stack}
|
|
14
|
+
Caused by:
|
|
15
|
+
${cause.stack}`;
|
|
16
|
+
return err;
|
|
17
|
+
}
|
|
18
|
+
return new Error(message);
|
|
19
|
+
}
|
package/src/index.ts
ADDED
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HtPageInfo } from './types';
|
|
2
|
+
|
|
3
|
+
function js(value: unknown): string {
|
|
4
|
+
return JSON.stringify(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createManifestModule(entries: HtPageInfo[]): string {
|
|
8
|
+
const imports = entries
|
|
9
|
+
.map((page, i) => `import * as page${i} from ${js(page.entryPath)};`)
|
|
10
|
+
.join('');
|
|
11
|
+
|
|
12
|
+
const records = entries
|
|
13
|
+
.map((page, i) => `{
|
|
14
|
+
page: ${js(page)},
|
|
15
|
+
mod: page${i}
|
|
16
|
+
}`)
|
|
17
|
+
.join(',');
|
|
18
|
+
|
|
19
|
+
return `${imports}
|
|
20
|
+
|
|
21
|
+
export const manifest = [
|
|
22
|
+
${records}
|
|
23
|
+
];
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { compareRoutePriority, expandStaticPaths, fileNameFromRoute } from './route-utils';
|
|
2
|
+
import type { HtPageInfo, HtPageModule, StaticParamRecord } from './types';
|
|
3
|
+
|
|
4
|
+
export async function buildPageIndex(args: {
|
|
5
|
+
entries: HtPageInfo[];
|
|
6
|
+
modulesByEntry: Map<string, HtPageModule>;
|
|
7
|
+
cleanUrls: boolean;
|
|
8
|
+
}): Promise<HtPageInfo[]> {
|
|
9
|
+
const { entries, modulesByEntry, cleanUrls } = args;
|
|
10
|
+
const pages: HtPageInfo[] = [];
|
|
11
|
+
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const mod = modulesByEntry.get(entry.entryPath) ?? {};
|
|
14
|
+
|
|
15
|
+
if (entry.dynamic) {
|
|
16
|
+
const rows = mod.generateStaticParams ? await mod.generateStaticParams() : [];
|
|
17
|
+
pages.push(
|
|
18
|
+
...expandStaticPaths(
|
|
19
|
+
{
|
|
20
|
+
id: entry.id,
|
|
21
|
+
entryPath: entry.entryPath,
|
|
22
|
+
absolutePath: entry.absolutePath,
|
|
23
|
+
relativePath: entry.relativePath,
|
|
24
|
+
routePattern: entry.routePattern,
|
|
25
|
+
dynamic: entry.dynamic,
|
|
26
|
+
paramNames: entry.paramNames,
|
|
27
|
+
} as Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,
|
|
28
|
+
rows as StaticParamRecord[],
|
|
29
|
+
cleanUrls,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
pages.push({
|
|
34
|
+
...entry,
|
|
35
|
+
routePath: entry.routePattern,
|
|
36
|
+
fileName: fileNameFromRoute(entry.routePattern, cleanUrls),
|
|
37
|
+
params: {},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pages.sort((a, b) => compareRoutePriority(a.routePattern, b.routePattern));
|
|
43
|
+
return pages;
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function toPosix(p: string): string {
|
|
4
|
+
return p.split(path.sep).join('/');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function stripHtSuffix(file: string): string {
|
|
8
|
+
return file.replace(/\.ht\.js$/i, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeRoutePath(p: string): string {
|
|
12
|
+
let out = p.startsWith('/') ? p : `/${p}`;
|
|
13
|
+
out = out.replace(/\/+/g, '/');
|
|
14
|
+
if (out !== '/' && out.endsWith('/')) out = out.slice(0, -1);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeFsPath(p: string): string {
|
|
19
|
+
return toPosix(path.resolve(p));
|
|
20
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import pLimit from 'p-limit';
|
|
5
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
|
|
6
|
+
import { discoverEntryPages } from './discover';
|
|
7
|
+
import { installDevServer } from './dev-server';
|
|
8
|
+
import { buildPageIndex } from './page-index';
|
|
9
|
+
import { buildRenderBundle } from './render-bundle';
|
|
10
|
+
import { renderPage } from './render-runtime';
|
|
11
|
+
import type { HtPageInfo, HtPageModule, HtPagesPluginOptions } from './types';
|
|
12
|
+
|
|
13
|
+
function chunkArray<T>(items: T[], size: number): T[][] {
|
|
14
|
+
const out: T[][] = [];
|
|
15
|
+
for (let i = 0; i < items.length; i += size) {
|
|
16
|
+
out.push(items.slice(i, i + size));
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createEntriesKey(entries: HtPageInfo[]): string {
|
|
22
|
+
const raw = entries
|
|
23
|
+
.map((e) => `${e.entryPath}|${e.routePattern}|${e.dynamic}`)
|
|
24
|
+
.join('\n');
|
|
25
|
+
|
|
26
|
+
return createHash('sha256').update(raw).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function importManifest(bundlePath: string): Promise<Array<{ page: HtPageInfo; mod: HtPageModule }>> {
|
|
30
|
+
const mod = await import(pathToFileURL(bundlePath).href + `?t=${Date.now()}`);
|
|
31
|
+
return mod.manifest as Array<{ page: HtPageInfo; mod: HtPageModule }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function htPages(options: HtPagesPluginOptions = {}): Plugin {
|
|
35
|
+
let root = process.cwd();
|
|
36
|
+
let server: ViteDevServer | null = null;
|
|
37
|
+
let config: ResolvedConfig;
|
|
38
|
+
let devPages: HtPageInfo[] = [];
|
|
39
|
+
let cachedManifestKey: string | null = null;
|
|
40
|
+
let cachedBundlePath: string | null = null;
|
|
41
|
+
const cleanUrls = options.cleanUrls ?? true;
|
|
42
|
+
|
|
43
|
+
async function loadDevPages(): Promise<HtPageInfo[]> {
|
|
44
|
+
const entries = await discoverEntryPages(root, options);
|
|
45
|
+
const modulesByEntry = new Map<string, HtPageModule>();
|
|
46
|
+
|
|
47
|
+
if (!server) return [];
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const mod = (await server.ssrLoadModule(entry.entryPath)) as HtPageModule;
|
|
51
|
+
modulesByEntry.set(entry.entryPath, mod);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
devPages = await buildPageIndex({
|
|
55
|
+
entries,
|
|
56
|
+
modulesByEntry,
|
|
57
|
+
cleanUrls,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return devPages;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name: 'vite-plugin-ht-pages',
|
|
65
|
+
|
|
66
|
+
configResolved(resolved) {
|
|
67
|
+
config = resolved;
|
|
68
|
+
root = resolved.root;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async buildStart() {
|
|
72
|
+
const entries = await discoverEntryPages(root, options);
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
this.addWatchFile(entry.entryPath);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
configureServer(_server) {
|
|
79
|
+
server = _server;
|
|
80
|
+
|
|
81
|
+
installDevServer({
|
|
82
|
+
server,
|
|
83
|
+
getPages: () => devPages,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
loadDevPages().catch((error) => {
|
|
87
|
+
server?.config.logger.error(String(error));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
server = null;
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async handleHotUpdate() {
|
|
96
|
+
if (server) {
|
|
97
|
+
await loadDevPages();
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async generateBundle() {
|
|
103
|
+
const entries = await discoverEntryPages(root, options);
|
|
104
|
+
const cacheDir = path.join(root, 'node_modules/.cache/vite-plugin-ht-pages');
|
|
105
|
+
|
|
106
|
+
const entriesKey = createEntriesKey(entries);
|
|
107
|
+
|
|
108
|
+
let bundlePath: string;
|
|
109
|
+
if (cachedBundlePath && cachedManifestKey === entriesKey) {
|
|
110
|
+
bundlePath = cachedBundlePath;
|
|
111
|
+
} else {
|
|
112
|
+
bundlePath = await buildRenderBundle({
|
|
113
|
+
entries,
|
|
114
|
+
cacheDir,
|
|
115
|
+
ssrPlugins: options.ssrPlugins,
|
|
116
|
+
});
|
|
117
|
+
cachedManifestKey = entriesKey;
|
|
118
|
+
cachedBundlePath = bundlePath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const manifest = await importManifest(bundlePath);
|
|
122
|
+
const modulesByEntry = new Map<string, HtPageModule>();
|
|
123
|
+
for (const rec of manifest) {
|
|
124
|
+
modulesByEntry.set(rec.page.entryPath, rec.mod);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pages = await buildPageIndex({
|
|
128
|
+
entries,
|
|
129
|
+
modulesByEntry,
|
|
130
|
+
cleanUrls,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const limit = pLimit(options.renderConcurrency ?? 8);
|
|
134
|
+
const batchSize = options.renderBatchSize ?? Math.max(options.renderConcurrency ?? 8, 32);
|
|
135
|
+
|
|
136
|
+
for (const batch of chunkArray(pages, batchSize)) {
|
|
137
|
+
await Promise.all(
|
|
138
|
+
batch.map((page) =>
|
|
139
|
+
limit(async () => {
|
|
140
|
+
const mod = modulesByEntry.get(page.entryPath);
|
|
141
|
+
if (!mod) throw new Error(`Missing module for page entry: ${page.entryPath}`);
|
|
142
|
+
const html = await renderPage(page, mod, false);
|
|
143
|
+
this.emitFile({
|
|
144
|
+
type: 'asset',
|
|
145
|
+
fileName: options.mapOutputPath?.(page) ?? page.fileName,
|
|
146
|
+
source: html,
|
|
147
|
+
});
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { rollup, type Plugin as RollupPlugin } from 'rollup';
|
|
5
|
+
import { createManifestModule } from './manifest';
|
|
6
|
+
import type { HtPageInfo } from './types';
|
|
7
|
+
|
|
8
|
+
const VIRTUAL_MANIFEST_ID = '\0virtual:ht-pages-manifest';
|
|
9
|
+
|
|
10
|
+
export async function buildRenderBundle(args: {
|
|
11
|
+
entries: HtPageInfo[];
|
|
12
|
+
cacheDir: string;
|
|
13
|
+
ssrPlugins?: RollupPlugin[];
|
|
14
|
+
}): Promise<string> {
|
|
15
|
+
const { entries, cacheDir, ssrPlugins = [] } = args;
|
|
16
|
+
const source = createManifestModule(entries);
|
|
17
|
+
const hash = createHash('sha256').update(source).digest('hex').slice(0, 12);
|
|
18
|
+
const bundlePath = path.join(cacheDir, `render-${hash}.mjs`);
|
|
19
|
+
|
|
20
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
const bundle = await rollup({
|
|
23
|
+
input: VIRTUAL_MANIFEST_ID,
|
|
24
|
+
plugins: [
|
|
25
|
+
{
|
|
26
|
+
name: 'ht-pages:virtual-manifest',
|
|
27
|
+
resolveId(id) {
|
|
28
|
+
return id === VIRTUAL_MANIFEST_ID ? id : null;
|
|
29
|
+
},
|
|
30
|
+
load(id) {
|
|
31
|
+
return id === VIRTUAL_MANIFEST_ID ? source : null;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
...ssrPlugins,
|
|
35
|
+
],
|
|
36
|
+
treeshake: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const { output } = await bundle.generate({
|
|
40
|
+
format: 'esm',
|
|
41
|
+
exports: 'named',
|
|
42
|
+
inlineDynamicImports: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const chunk = output.find((item) => item.type === 'chunk');
|
|
46
|
+
if (!chunk || chunk.type !== 'chunk') {
|
|
47
|
+
throw new Error('Failed to generate HT pages render bundle.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await fs.writeFile(bundlePath, chunk.code, 'utf8');
|
|
51
|
+
await bundle.close();
|
|
52
|
+
return bundlePath;
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { invalidHtmlReturn, pageError } from './errors';
|
|
2
|
+
import type { HtPageInfo, HtPageModule, HtPageRenderContext } from './types';
|
|
3
|
+
|
|
4
|
+
export async function renderPage(page: HtPageInfo, mod: HtPageModule, dev = false): Promise<string> {
|
|
5
|
+
const ctx: HtPageRenderContext = {
|
|
6
|
+
page,
|
|
7
|
+
params: page.params,
|
|
8
|
+
dev,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
if (typeof mod.data === 'function') {
|
|
13
|
+
ctx.data = await mod.data(ctx);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const entry = mod.default;
|
|
17
|
+
const html = typeof entry === 'function' ? await entry(ctx) : entry;
|
|
18
|
+
|
|
19
|
+
if (typeof html !== 'string') {
|
|
20
|
+
throw invalidHtmlReturn(page, html);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return html;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw pageError(page, error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { normalizeRoutePath, stripHtSuffix, toPosix } from './path-utils';
|
|
2
|
+
import type { HtPageInfo, StaticParamRecord } from './types';
|
|
3
|
+
|
|
4
|
+
const DYNAMIC_SEGMENT_RE = /\[([A-Za-z0-9_]+)\]/g;
|
|
5
|
+
const CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]/g;
|
|
6
|
+
const ANY_PARAM_RE = /\[(?:\.\.\.)?([A-Za-z0-9_]+)\]/g;
|
|
7
|
+
|
|
8
|
+
export function getParamNames(relativeFromPagesDir: string): string[] {
|
|
9
|
+
return [...relativeFromPagesDir.matchAll(ANY_PARAM_RE)].map((m) => m[1]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isDynamicPage(relativeFromPagesDir: string): boolean {
|
|
13
|
+
return ANY_PARAM_RE.test(relativeFromPagesDir);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toRoutePattern(relativeFromPagesDir: string): string {
|
|
17
|
+
const noExt = stripHtSuffix(toPosix(relativeFromPagesDir));
|
|
18
|
+
const raw = noExt
|
|
19
|
+
.replace(/(^|\/)index$/i, '$1')
|
|
20
|
+
.replace(CATCH_ALL_SEGMENT_RE, '*:$1')
|
|
21
|
+
.replace(DYNAMIC_SEGMENT_RE, ':$1');
|
|
22
|
+
return normalizeRoutePath(raw || '/');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fillParams(pattern: string, params: Record<string, string>): string {
|
|
26
|
+
return pattern
|
|
27
|
+
.replace(/\*:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
28
|
+
if (!(key in params)) throw new Error(`Missing catch-all route param "${key}"`);
|
|
29
|
+
return String(params[key])
|
|
30
|
+
.split('/')
|
|
31
|
+
.map((part) => encodeURIComponent(part))
|
|
32
|
+
.join('/');
|
|
33
|
+
})
|
|
34
|
+
.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
35
|
+
if (!(key in params)) throw new Error(`Missing route param "${key}"`);
|
|
36
|
+
return encodeURIComponent(params[key]);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function fileNameFromRoute(routePath: string, cleanUrls: boolean): string {
|
|
41
|
+
const normalized = normalizeRoutePath(routePath);
|
|
42
|
+
if (normalized === '/') return 'index.html';
|
|
43
|
+
const base = normalized.slice(1);
|
|
44
|
+
return cleanUrls ? `${base}/index.html` : `${base}.html`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function expandStaticPaths(
|
|
48
|
+
basePage: Omit<HtPageInfo, 'routePath' | 'fileName' | 'params'>,
|
|
49
|
+
rows: StaticParamRecord[],
|
|
50
|
+
cleanUrls: boolean,
|
|
51
|
+
): HtPageInfo[] {
|
|
52
|
+
return rows.map((row) => {
|
|
53
|
+
const params = Object.fromEntries(
|
|
54
|
+
Object.entries(row).map(([k, v]) => [k, String(v)]),
|
|
55
|
+
);
|
|
56
|
+
const routePath = fillParams(basePage.routePattern, params);
|
|
57
|
+
return {
|
|
58
|
+
...basePage,
|
|
59
|
+
routePath,
|
|
60
|
+
fileName: fileNameFromRoute(routePath, cleanUrls),
|
|
61
|
+
params,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function routeMatch(pattern: string, urlPath: string): Record<string, string> | null {
|
|
67
|
+
const a = normalizeRoutePath(pattern).split('/').filter(Boolean);
|
|
68
|
+
const b = normalizeRoutePath(urlPath).split('/').filter(Boolean);
|
|
69
|
+
const params: Record<string, string> = {};
|
|
70
|
+
|
|
71
|
+
for (let i = 0, j = 0; i < a.length; i++, j++) {
|
|
72
|
+
const seg = a[i];
|
|
73
|
+
|
|
74
|
+
if (seg.startsWith('*:')) {
|
|
75
|
+
params[seg.slice(2)] = b.slice(j).map(decodeURIComponent).join('/');
|
|
76
|
+
return params;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (j >= b.length) return null;
|
|
80
|
+
|
|
81
|
+
if (seg.startsWith(':')) {
|
|
82
|
+
params[seg.slice(1)] = decodeURIComponent(b[j]);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (seg !== b[j]) return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return a.length === b.length ? params : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function compareRoutePriority(a: string, b: string): number {
|
|
93
|
+
const aSegs = normalizeRoutePath(a).split('/').filter(Boolean);
|
|
94
|
+
const bSegs = normalizeRoutePath(b).split('/').filter(Boolean);
|
|
95
|
+
const len = Math.max(aSegs.length, bSegs.length);
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < len; i++) {
|
|
98
|
+
const aa = aSegs[i];
|
|
99
|
+
const bb = bSegs[i];
|
|
100
|
+
|
|
101
|
+
if (aa == null) return -1;
|
|
102
|
+
if (bb == null) return 1;
|
|
103
|
+
|
|
104
|
+
const aCatchAll = aa.startsWith('*:');
|
|
105
|
+
const bCatchAll = bb.startsWith('*:');
|
|
106
|
+
if (aCatchAll !== bCatchAll) return aCatchAll ? 1 : -1;
|
|
107
|
+
|
|
108
|
+
const aDynamic = aa.startsWith(':');
|
|
109
|
+
const bDynamic = bb.startsWith(':');
|
|
110
|
+
if (aDynamic !== bDynamic) return aDynamic ? 1 : -1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return aSegs.length - bSegs.length;
|
|
114
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface StaticParamRecord {
|
|
2
|
+
[key: string]: string | number | boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface HtPageInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
entryPath: string;
|
|
8
|
+
absolutePath: string;
|
|
9
|
+
relativePath: string;
|
|
10
|
+
routePattern: string;
|
|
11
|
+
routePath: string;
|
|
12
|
+
fileName: string;
|
|
13
|
+
dynamic: boolean;
|
|
14
|
+
paramNames: string[];
|
|
15
|
+
params: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface HtPageRenderContext {
|
|
19
|
+
page: HtPageInfo;
|
|
20
|
+
params: Record<string, string>;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
dev?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface HtPageModule {
|
|
26
|
+
default?: string | ((ctx: HtPageRenderContext) => string | Promise<string>);
|
|
27
|
+
data?: (ctx: HtPageRenderContext) => unknown | Promise<unknown>;
|
|
28
|
+
generateStaticParams?: () => StaticParamRecord[] | Promise<StaticParamRecord[]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HtPagesPluginOptions {
|
|
32
|
+
root?: string;
|
|
33
|
+
include?: string | string[];
|
|
34
|
+
exclude?: string | string[];
|
|
35
|
+
pagesDir?: string;
|
|
36
|
+
renderConcurrency?: number;
|
|
37
|
+
renderBatchSize?: number;
|
|
38
|
+
cleanUrls?: boolean;
|
|
39
|
+
ssrPlugins?: import('rollup').Plugin[];
|
|
40
|
+
mapOutputPath?: (page: HtPageInfo) => string;
|
|
41
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|