vite-plugin-html-pages 1.0.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/LICENSE +21 -0
- package/README.md +532 -0
- package/TODO +0 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +640 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/constants.ts +4 -0
- package/src/dev-server.ts +71 -0
- package/src/discover.ts +65 -0
- package/src/errors.ts +32 -0
- package/src/fetch-cache.ts +141 -0
- package/src/index.ts +12 -0
- package/src/manifest.ts +27 -0
- package/src/page-index.ts +67 -0
- package/src/path-utils.ts +20 -0
- package/src/plugin.ts +272 -0
- package/src/render-bundle.ts +92 -0
- package/src/render-runtime.ts +36 -0
- package/src/route-utils.ts +182 -0
- package/src/types.ts +57 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import path3 from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import pLimit from "p-limit";
|
|
6
|
+
|
|
7
|
+
// src/discover.ts
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
|
|
10
|
+
// src/path-utils.ts
|
|
11
|
+
import path from "path";
|
|
12
|
+
function toPosix(p) {
|
|
13
|
+
return p.split(path.sep).join("/");
|
|
14
|
+
}
|
|
15
|
+
function stripHtSuffix(file) {
|
|
16
|
+
return file.replace(/\.ht\.js$/i, "");
|
|
17
|
+
}
|
|
18
|
+
function normalizeRoutePath(p) {
|
|
19
|
+
let out = p.startsWith("/") ? p : `/${p}`;
|
|
20
|
+
out = out.replace(/\/+/g, "/");
|
|
21
|
+
if (out !== "/" && out.endsWith("/")) out = out.slice(0, -1);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function normalizeFsPath(p) {
|
|
25
|
+
return toPosix(path.resolve(p));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/route-utils.ts
|
|
29
|
+
function safeDecodeURIComponent(str) {
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(str);
|
|
32
|
+
} catch {
|
|
33
|
+
return str;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
var DYNAMIC_SEGMENT_RE = /\[([A-Za-z0-9_]+)\]/g;
|
|
37
|
+
var CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]/g;
|
|
38
|
+
var OPTIONAL_CATCH_ALL_SEGMENT_RE = /\[\.\.\.([A-Za-z0-9_]+)\]\?/g;
|
|
39
|
+
var ANY_PARAM_RE = /\[(?:\.\.\.)?([A-Za-z0-9_]+)\]\??/g;
|
|
40
|
+
var ROUTE_GROUP_RE = /(^|\/)\(([^)]+)\)(?=\/|$)/g;
|
|
41
|
+
function getParamNames(relativeFromPagesDir) {
|
|
42
|
+
return [...relativeFromPagesDir.matchAll(ANY_PARAM_RE)].map((m) => m[1]);
|
|
43
|
+
}
|
|
44
|
+
function isDynamicPage(relativeFromPagesDir) {
|
|
45
|
+
return /\[(?:\.\.\.)?[A-Za-z0-9_]+\]\??/.test(relativeFromPagesDir);
|
|
46
|
+
}
|
|
47
|
+
function toRoutePattern(relativeFromPagesDir) {
|
|
48
|
+
const noExt = stripHtSuffix(toPosix(relativeFromPagesDir));
|
|
49
|
+
const withoutGroups = noExt.replace(ROUTE_GROUP_RE, "$1");
|
|
50
|
+
const withoutIndex = withoutGroups.replace(/\/index$/i, "").replace(/^index$/i, "");
|
|
51
|
+
const raw = withoutIndex.replace(OPTIONAL_CATCH_ALL_SEGMENT_RE, "*?:$1").replace(CATCH_ALL_SEGMENT_RE, "*:$1").replace(DYNAMIC_SEGMENT_RE, ":$1");
|
|
52
|
+
return normalizeRoutePath(raw || "/");
|
|
53
|
+
}
|
|
54
|
+
function fillParams(pattern, params) {
|
|
55
|
+
const result = pattern.replace(/\*\?:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
56
|
+
const value = params[key];
|
|
57
|
+
if (value == null || value === "") {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
return String(value).split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
61
|
+
}).replace(/\*:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
62
|
+
if (!(key in params)) {
|
|
63
|
+
throw new Error(`Missing catch-all route param "${key}"`);
|
|
64
|
+
}
|
|
65
|
+
return String(params[key]).split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
66
|
+
}).replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
67
|
+
if (!(key in params)) {
|
|
68
|
+
throw new Error(`Missing route param "${key}"`);
|
|
69
|
+
}
|
|
70
|
+
return encodeURIComponent(params[key]);
|
|
71
|
+
});
|
|
72
|
+
return normalizeRoutePath(result || "/");
|
|
73
|
+
}
|
|
74
|
+
function fileNameFromRoute(routePath, cleanUrls) {
|
|
75
|
+
const normalized = normalizeRoutePath(routePath);
|
|
76
|
+
if (normalized === "/") return "index.html";
|
|
77
|
+
const base = normalized.slice(1);
|
|
78
|
+
return cleanUrls ? `${base}/index.html` : `${base}.html`;
|
|
79
|
+
}
|
|
80
|
+
function expandStaticPaths(basePage, rows, cleanUrls) {
|
|
81
|
+
return rows.map((row) => {
|
|
82
|
+
const params = Object.fromEntries(
|
|
83
|
+
Object.entries(row).map(([k, v]) => [k, String(v)])
|
|
84
|
+
);
|
|
85
|
+
const routePath = fillParams(basePage.routePattern, params);
|
|
86
|
+
return {
|
|
87
|
+
...basePage,
|
|
88
|
+
routePath,
|
|
89
|
+
fileName: fileNameFromRoute(routePath, cleanUrls),
|
|
90
|
+
params
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function routeMatch(pattern, urlPath) {
|
|
95
|
+
const a = normalizeRoutePath(pattern).split("/").filter(Boolean);
|
|
96
|
+
const b = normalizeRoutePath(urlPath).split("/").filter(Boolean);
|
|
97
|
+
const params = {};
|
|
98
|
+
for (let i = 0; i < a.length; i++) {
|
|
99
|
+
const patternSeg = a[i];
|
|
100
|
+
const urlSeg = b[i];
|
|
101
|
+
if (patternSeg.startsWith("*?:")) {
|
|
102
|
+
params[patternSeg.slice(3)] = i < b.length ? b.slice(i).map(safeDecodeURIComponent).join("/") : "";
|
|
103
|
+
return params;
|
|
104
|
+
}
|
|
105
|
+
if (patternSeg.startsWith("*:")) {
|
|
106
|
+
const rest = b.slice(i);
|
|
107
|
+
if (rest.length === 0) return null;
|
|
108
|
+
params[patternSeg.slice(2)] = rest.map(safeDecodeURIComponent).join("/");
|
|
109
|
+
return params;
|
|
110
|
+
}
|
|
111
|
+
if (!urlSeg) return null;
|
|
112
|
+
if (patternSeg.startsWith(":")) {
|
|
113
|
+
params[patternSeg.slice(1)] = safeDecodeURIComponent(urlSeg);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (patternSeg !== urlSeg) return null;
|
|
117
|
+
}
|
|
118
|
+
return a.length === b.length ? params : null;
|
|
119
|
+
}
|
|
120
|
+
function compareRoutePriority(a, b) {
|
|
121
|
+
const aSegs = normalizeRoutePath(a).split("/").filter(Boolean);
|
|
122
|
+
const bSegs = normalizeRoutePath(b).split("/").filter(Boolean);
|
|
123
|
+
const len = Math.max(aSegs.length, bSegs.length);
|
|
124
|
+
for (let i = 0; i < len; i++) {
|
|
125
|
+
const aa = aSegs[i];
|
|
126
|
+
const bb = bSegs[i];
|
|
127
|
+
if (aa == null) return 1;
|
|
128
|
+
if (bb == null) return -1;
|
|
129
|
+
const aOptionalCatchAll = aa.startsWith("*?:");
|
|
130
|
+
const bOptionalCatchAll = bb.startsWith("*?:");
|
|
131
|
+
if (aOptionalCatchAll !== bOptionalCatchAll) {
|
|
132
|
+
return aOptionalCatchAll ? 1 : -1;
|
|
133
|
+
}
|
|
134
|
+
const aCatchAll = aa.startsWith("*:");
|
|
135
|
+
const bCatchAll = bb.startsWith("*:");
|
|
136
|
+
if (aCatchAll !== bCatchAll) {
|
|
137
|
+
return aCatchAll ? 1 : -1;
|
|
138
|
+
}
|
|
139
|
+
const aDynamic = aa.startsWith(":");
|
|
140
|
+
const bDynamic = bb.startsWith(":");
|
|
141
|
+
if (aDynamic !== bDynamic) {
|
|
142
|
+
return aDynamic ? 1 : -1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return bSegs.length - aSegs.length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/constants.ts
|
|
149
|
+
var PLUGIN_NAME = "vite-plugin-html-pages";
|
|
150
|
+
var VIRTUAL_BUILD_ENTRY_ID = `\0${PLUGIN_NAME}:build-entry`;
|
|
151
|
+
var VIRTUAL_MANIFEST_ID = `\0virtual:${PLUGIN_NAME}-manifest`;
|
|
152
|
+
var CACHE_DIR_NAME = `node_modules/.cache/${PLUGIN_NAME}`;
|
|
153
|
+
|
|
154
|
+
// src/discover.ts
|
|
155
|
+
async function discoverEntryPages(root, options) {
|
|
156
|
+
const fgModule = await import("fast-glob");
|
|
157
|
+
const fg = fgModule.default ?? fgModule;
|
|
158
|
+
const include = Array.isArray(options.include) ? options.include : [options.include ?? "src/**/*.ht.js"];
|
|
159
|
+
const exclude = Array.isArray(options.exclude) ? options.exclude : options.exclude ? [options.exclude] : [];
|
|
160
|
+
const pagesDir = options.pagesDir ?? "src";
|
|
161
|
+
const pagesRoot = normalizeFsPath(path2.join(root, pagesDir));
|
|
162
|
+
const files = await fg.glob(include, {
|
|
163
|
+
cwd: root,
|
|
164
|
+
ignore: exclude,
|
|
165
|
+
absolute: true
|
|
166
|
+
});
|
|
167
|
+
return files.sort().map((absolutePath) => {
|
|
168
|
+
const entryPath = normalizeFsPath(absolutePath);
|
|
169
|
+
const relativePath = toPosix(path2.relative(root, entryPath));
|
|
170
|
+
const relativeFromPagesDir = toPosix(path2.relative(pagesRoot, entryPath));
|
|
171
|
+
if (relativeFromPagesDir.startsWith("../") || relativeFromPagesDir === "..") {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`[${PLUGIN_NAME}] Page is outside pagesDir: ${entryPath} (pagesDir: ${pagesDir})`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const dynamic = isDynamicPage(relativeFromPagesDir);
|
|
177
|
+
const routePattern = toRoutePattern(relativeFromPagesDir);
|
|
178
|
+
return {
|
|
179
|
+
id: entryPath,
|
|
180
|
+
entryPath,
|
|
181
|
+
absolutePath: entryPath,
|
|
182
|
+
relativePath,
|
|
183
|
+
routePattern,
|
|
184
|
+
routePath: routePattern,
|
|
185
|
+
fileName: "",
|
|
186
|
+
dynamic,
|
|
187
|
+
paramNames: getParamNames(relativeFromPagesDir),
|
|
188
|
+
params: {}
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/errors.ts
|
|
194
|
+
function invalidHtmlReturn(page, value) {
|
|
195
|
+
return new Error(
|
|
196
|
+
`[${PLUGIN_NAME}] Page "${page.relativePath}" must resolve to an HTML string, got ${typeof value}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
function missingDefaultExport(page) {
|
|
200
|
+
return new Error(
|
|
201
|
+
`[${PLUGIN_NAME}] Page "${page.relativePath}" does not export a default renderer`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
function pageError(page, cause) {
|
|
205
|
+
const message = `[${PLUGIN_NAME}] Failed to render "${page.relativePath}" at route "${page.routePath}"`;
|
|
206
|
+
if (cause instanceof Error) {
|
|
207
|
+
const err = new Error(`${message}: ${cause.message}`);
|
|
208
|
+
if (cause.stack) {
|
|
209
|
+
err.stack = `${err.stack}
|
|
210
|
+
Caused by:
|
|
211
|
+
${cause.stack}`;
|
|
212
|
+
}
|
|
213
|
+
return err;
|
|
214
|
+
}
|
|
215
|
+
return new Error(`${message}: ${String(cause)}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/render-runtime.ts
|
|
219
|
+
async function renderPage(page, mod, dev = false) {
|
|
220
|
+
const ctx = {
|
|
221
|
+
page,
|
|
222
|
+
params: page.params,
|
|
223
|
+
dev
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
if (typeof mod.data === "function") {
|
|
227
|
+
ctx.data = await mod.data(ctx);
|
|
228
|
+
}
|
|
229
|
+
const entry = mod.default;
|
|
230
|
+
if (entry == null) {
|
|
231
|
+
throw missingDefaultExport(page);
|
|
232
|
+
}
|
|
233
|
+
const html = typeof entry === "function" ? await entry(ctx) : entry;
|
|
234
|
+
if (typeof html !== "string") {
|
|
235
|
+
throw invalidHtmlReturn(page, html);
|
|
236
|
+
}
|
|
237
|
+
return html;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw pageError(page, error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/dev-server.ts
|
|
244
|
+
function isDynamicOnly(mod) {
|
|
245
|
+
return mod.dynamic === true || mod.prerender === false;
|
|
246
|
+
}
|
|
247
|
+
function installDevServer(args) {
|
|
248
|
+
const { server, getPages, getEntries } = args;
|
|
249
|
+
server.middlewares.use(async (req, res, next) => {
|
|
250
|
+
try {
|
|
251
|
+
if (!req.url || req.method !== "GET") return next();
|
|
252
|
+
const pathname = req.url.split("?")[0];
|
|
253
|
+
const pages = await getPages();
|
|
254
|
+
const staticPage = pages.find((p) => p.routePath === pathname);
|
|
255
|
+
if (staticPage) {
|
|
256
|
+
const mod = await server.ssrLoadModule(
|
|
257
|
+
`/${staticPage.relativePath}`
|
|
258
|
+
);
|
|
259
|
+
const html = await renderPage(staticPage, mod, true);
|
|
260
|
+
res.statusCode = 200;
|
|
261
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
262
|
+
res.end(html);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const entries = await getEntries();
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const mod = await server.ssrLoadModule(
|
|
268
|
+
`/${entry.relativePath}`
|
|
269
|
+
);
|
|
270
|
+
if (!isDynamicOnly(mod)) continue;
|
|
271
|
+
const params = routeMatch(entry.routePattern, pathname);
|
|
272
|
+
if (!params) continue;
|
|
273
|
+
const page = {
|
|
274
|
+
...entry,
|
|
275
|
+
routePath: pathname,
|
|
276
|
+
fileName: "",
|
|
277
|
+
params
|
|
278
|
+
};
|
|
279
|
+
const html = await renderPage(page, mod, true);
|
|
280
|
+
res.statusCode = 200;
|
|
281
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
282
|
+
res.end(html);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
next();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
next(error);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/page-index.ts
|
|
293
|
+
async function buildPageIndex(args) {
|
|
294
|
+
const { entries, modulesByEntry, cleanUrls } = args;
|
|
295
|
+
const pages = [];
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
const mod = modulesByEntry.get(entry.entryPath) ?? {};
|
|
298
|
+
if (entry.dynamic) {
|
|
299
|
+
const rows = (mod.generateStaticParams ? await mod.generateStaticParams() : []) ?? [];
|
|
300
|
+
pages.push(
|
|
301
|
+
...expandStaticPaths(
|
|
302
|
+
{
|
|
303
|
+
id: entry.id,
|
|
304
|
+
entryPath: entry.entryPath,
|
|
305
|
+
absolutePath: entry.absolutePath,
|
|
306
|
+
relativePath: entry.relativePath,
|
|
307
|
+
routePattern: entry.routePattern,
|
|
308
|
+
dynamic: entry.dynamic,
|
|
309
|
+
paramNames: entry.paramNames
|
|
310
|
+
},
|
|
311
|
+
Array.isArray(rows) ? rows : [],
|
|
312
|
+
cleanUrls
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
pages.push({
|
|
317
|
+
...entry,
|
|
318
|
+
routePath: entry.routePattern,
|
|
319
|
+
fileName: fileNameFromRoute(entry.routePattern, cleanUrls),
|
|
320
|
+
params: {}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
pages.sort((a, b) => compareRoutePriority(a.routePattern, b.routePattern));
|
|
325
|
+
const seenRoutes = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const page of pages) {
|
|
327
|
+
const existing = seenRoutes.get(page.routePath);
|
|
328
|
+
if (existing) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`[${PLUGIN_NAME}] Duplicate route generated: "${page.routePath}" from "${existing.relativePath}" and "${page.relativePath}"`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
seenRoutes.set(page.routePath, page);
|
|
334
|
+
}
|
|
335
|
+
return pages;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/plugin.ts
|
|
339
|
+
function chunkArray(items, size) {
|
|
340
|
+
const out = [];
|
|
341
|
+
for (let i = 0; i < items.length; i += size) {
|
|
342
|
+
out.push(items.slice(i, i + size));
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
async function importPageModule(entryPath) {
|
|
347
|
+
const mod = await import(pathToFileURL(entryPath).href + `?t=${Date.now()}`);
|
|
348
|
+
return mod;
|
|
349
|
+
}
|
|
350
|
+
async function warnIfNotEsm(root) {
|
|
351
|
+
try {
|
|
352
|
+
const pkgPath = path3.join(root, "package.json");
|
|
353
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
354
|
+
if (pkg.type !== "module") {
|
|
355
|
+
console.warn(
|
|
356
|
+
`[${PLUGIN_NAME}] Tip: add "type": "module" to package.json to avoid Node ESM warnings.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function htPages(options = {}) {
|
|
363
|
+
let root = process.cwd();
|
|
364
|
+
let server = null;
|
|
365
|
+
let devPages = [];
|
|
366
|
+
const cleanUrls = options.cleanUrls ?? true;
|
|
367
|
+
function logDebug(enabled, ...args) {
|
|
368
|
+
if (!enabled) return;
|
|
369
|
+
console.log(`[${PLUGIN_NAME}]`, ...args);
|
|
370
|
+
}
|
|
371
|
+
async function loadDevPages() {
|
|
372
|
+
const entries = await discoverEntryPages(root, options);
|
|
373
|
+
const modulesByEntry = /* @__PURE__ */ new Map();
|
|
374
|
+
logDebug(
|
|
375
|
+
options.debug,
|
|
376
|
+
"discovered entries",
|
|
377
|
+
entries.map((e) => e.relativePath)
|
|
378
|
+
);
|
|
379
|
+
if (!server) return [];
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
const mod = await server.ssrLoadModule(
|
|
382
|
+
`/${entry.relativePath}`
|
|
383
|
+
);
|
|
384
|
+
modulesByEntry.set(entry.entryPath, mod);
|
|
385
|
+
}
|
|
386
|
+
devPages = await buildPageIndex({
|
|
387
|
+
entries,
|
|
388
|
+
modulesByEntry,
|
|
389
|
+
cleanUrls
|
|
390
|
+
});
|
|
391
|
+
logDebug(
|
|
392
|
+
options.debug,
|
|
393
|
+
"dev pages",
|
|
394
|
+
devPages.map((p) => `${p.routePath} -> ${p.relativePath}`)
|
|
395
|
+
);
|
|
396
|
+
return devPages;
|
|
397
|
+
}
|
|
398
|
+
async function buildPagesPipeline() {
|
|
399
|
+
const entries = await discoverEntryPages(root, options);
|
|
400
|
+
const modulesByEntry = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const entry of entries) {
|
|
402
|
+
const mod = await importPageModule(entry.entryPath);
|
|
403
|
+
modulesByEntry.set(entry.entryPath, mod);
|
|
404
|
+
}
|
|
405
|
+
const pages = await buildPageIndex({
|
|
406
|
+
entries,
|
|
407
|
+
modulesByEntry,
|
|
408
|
+
cleanUrls
|
|
409
|
+
});
|
|
410
|
+
return { entries, modulesByEntry, pages };
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
name: PLUGIN_NAME,
|
|
414
|
+
config(userConfig, env) {
|
|
415
|
+
if (env.command !== "build") return;
|
|
416
|
+
const hasExplicitInput = userConfig.build?.rollupOptions?.input != null;
|
|
417
|
+
if (hasExplicitInput) return;
|
|
418
|
+
return {
|
|
419
|
+
build: {
|
|
420
|
+
rollupOptions: {
|
|
421
|
+
input: VIRTUAL_BUILD_ENTRY_ID
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
},
|
|
426
|
+
resolveId(id) {
|
|
427
|
+
if (id === VIRTUAL_BUILD_ENTRY_ID) return id;
|
|
428
|
+
return null;
|
|
429
|
+
},
|
|
430
|
+
load(id) {
|
|
431
|
+
if (id === VIRTUAL_BUILD_ENTRY_ID) {
|
|
432
|
+
return "export default {};";
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
},
|
|
436
|
+
configResolved(resolved) {
|
|
437
|
+
root = resolved.root;
|
|
438
|
+
void warnIfNotEsm(root);
|
|
439
|
+
},
|
|
440
|
+
async buildStart() {
|
|
441
|
+
const entries = await discoverEntryPages(root, options);
|
|
442
|
+
for (const entry of entries) {
|
|
443
|
+
this.addWatchFile(entry.entryPath);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
configureServer(_server) {
|
|
447
|
+
server = _server;
|
|
448
|
+
installDevServer({
|
|
449
|
+
server,
|
|
450
|
+
getPages: async () => {
|
|
451
|
+
if (devPages.length > 0) return devPages;
|
|
452
|
+
return loadDevPages();
|
|
453
|
+
},
|
|
454
|
+
getEntries: async () => discoverEntryPages(root, options)
|
|
455
|
+
});
|
|
456
|
+
loadDevPages().catch((error) => {
|
|
457
|
+
server?.config.logger.error(
|
|
458
|
+
`[${PLUGIN_NAME}] loadDevPages failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
async handleHotUpdate(ctx) {
|
|
463
|
+
if (!server) return;
|
|
464
|
+
if (!ctx.file.endsWith(".ht.js")) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
logDebug(options.debug, "page updated", ctx.file);
|
|
468
|
+
await loadDevPages();
|
|
469
|
+
return void 0;
|
|
470
|
+
},
|
|
471
|
+
async generateBundle(_, bundle) {
|
|
472
|
+
const { modulesByEntry, pages } = await buildPagesPipeline();
|
|
473
|
+
logDebug(
|
|
474
|
+
options.debug,
|
|
475
|
+
"emitting pages",
|
|
476
|
+
pages.map((p) => p.fileName)
|
|
477
|
+
);
|
|
478
|
+
const limit = pLimit(options.renderConcurrency ?? 8);
|
|
479
|
+
const batchSize = options.renderBatchSize ?? Math.max(options.renderConcurrency ?? 8, 32);
|
|
480
|
+
for (const batch of chunkArray(pages, batchSize)) {
|
|
481
|
+
await Promise.all(
|
|
482
|
+
batch.map(
|
|
483
|
+
(page) => limit(async () => {
|
|
484
|
+
const mod = modulesByEntry.get(page.entryPath);
|
|
485
|
+
if (!mod) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`[${PLUGIN_NAME}] Missing module for page entry: ${page.entryPath}`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const html = await renderPage(page, mod, false);
|
|
491
|
+
this.emitFile({
|
|
492
|
+
type: "asset",
|
|
493
|
+
fileName: options.mapOutputPath?.(page) ?? page.fileName,
|
|
494
|
+
source: html
|
|
495
|
+
});
|
|
496
|
+
})
|
|
497
|
+
)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const sitemapBase = options.site ?? "";
|
|
501
|
+
const sitemapRoutes = [...new Set(pages.map((p) => p.routePath))].filter(
|
|
502
|
+
(route) => !route.includes(":") && !route.includes("*")
|
|
503
|
+
);
|
|
504
|
+
if (sitemapRoutes.length > 0) {
|
|
505
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
506
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
507
|
+
${sitemapRoutes.map((route) => ` <url><loc>${sitemapBase}${route}</loc></url>`).join("\n")}
|
|
508
|
+
</urlset>
|
|
509
|
+
`;
|
|
510
|
+
this.emitFile({
|
|
511
|
+
type: "asset",
|
|
512
|
+
fileName: "sitemap.xml",
|
|
513
|
+
source: sitemap
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (options.rss?.site) {
|
|
517
|
+
const routePrefix = options.rss.routePrefix ?? "/blog";
|
|
518
|
+
const rssItems = pages.filter((page) => page.routePath.startsWith(routePrefix)).map((page) => {
|
|
519
|
+
const url = `${options.rss.site}${page.routePath}`;
|
|
520
|
+
return ` <item>
|
|
521
|
+
<title>${page.routePath}</title>
|
|
522
|
+
<link>${url}</link>
|
|
523
|
+
<guid>${url}</guid>
|
|
524
|
+
</item>`;
|
|
525
|
+
}).join("\n");
|
|
526
|
+
const rss = `<?xml version="1.0" encoding="UTF-8"?>
|
|
527
|
+
<rss version="2.0">
|
|
528
|
+
<channel>
|
|
529
|
+
<title>${options.rss.title ?? PLUGIN_NAME}</title>
|
|
530
|
+
<link>${options.rss.site}</link>
|
|
531
|
+
<description>${options.rss.description ?? "RSS feed"}</description>
|
|
532
|
+
${rssItems}
|
|
533
|
+
</channel>
|
|
534
|
+
</rss>
|
|
535
|
+
`;
|
|
536
|
+
this.emitFile({
|
|
537
|
+
type: "asset",
|
|
538
|
+
fileName: "rss.xml",
|
|
539
|
+
source: rss
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
543
|
+
if (output.type === "chunk" && output.facadeModuleId === VIRTUAL_BUILD_ENTRY_ID) {
|
|
544
|
+
delete bundle[fileName];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/fetch-cache.ts
|
|
552
|
+
import fs2 from "fs/promises";
|
|
553
|
+
import path4 from "path";
|
|
554
|
+
import { createHash } from "crypto";
|
|
555
|
+
var memoryCache = /* @__PURE__ */ new Map();
|
|
556
|
+
function createDefaultCacheKey(input, init) {
|
|
557
|
+
const raw = JSON.stringify({
|
|
558
|
+
url: String(input),
|
|
559
|
+
method: init?.method ?? "GET",
|
|
560
|
+
headers: init?.headers ?? {},
|
|
561
|
+
body: init?.body ?? null
|
|
562
|
+
});
|
|
563
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
564
|
+
}
|
|
565
|
+
function getCacheFilePath(cacheKey) {
|
|
566
|
+
return path4.join(process.cwd(), CACHE_DIR_NAME, "fetch", `${cacheKey}.json`);
|
|
567
|
+
}
|
|
568
|
+
function getEffectiveCacheMode(mode) {
|
|
569
|
+
if (mode === "memory" || mode === "fs" || mode === "none") {
|
|
570
|
+
return mode;
|
|
571
|
+
}
|
|
572
|
+
return process.env.NODE_ENV === "production" ? "fs" : "memory";
|
|
573
|
+
}
|
|
574
|
+
function toResponse(cached) {
|
|
575
|
+
return new Response(cached.body, {
|
|
576
|
+
status: cached.status,
|
|
577
|
+
statusText: cached.statusText,
|
|
578
|
+
headers: cached.headers
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function isFresh(cached, maxAgeSeconds) {
|
|
582
|
+
const ageSeconds = (Date.now() - cached.timestamp) / 1e3;
|
|
583
|
+
return ageSeconds <= maxAgeSeconds;
|
|
584
|
+
}
|
|
585
|
+
async function fetchAndCache(input, init, options = {}) {
|
|
586
|
+
const maxAge = options.maxAge ?? 60 * 60;
|
|
587
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
588
|
+
if (method !== "GET" && !options.cacheKey) {
|
|
589
|
+
return fetch(input, init);
|
|
590
|
+
}
|
|
591
|
+
const cacheMode = getEffectiveCacheMode(options.cache);
|
|
592
|
+
const cacheKey = options.cacheKey ?? createDefaultCacheKey(input, init);
|
|
593
|
+
if (cacheMode === "none") {
|
|
594
|
+
return fetch(input, init);
|
|
595
|
+
}
|
|
596
|
+
if (cacheMode === "memory" && !options.forceRefresh) {
|
|
597
|
+
const cached = memoryCache.get(cacheKey);
|
|
598
|
+
if (cached && isFresh(cached, maxAge)) {
|
|
599
|
+
return toResponse(cached);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const filePath = getCacheFilePath(cacheKey);
|
|
603
|
+
if (cacheMode === "fs") {
|
|
604
|
+
await fs2.mkdir(path4.dirname(filePath), { recursive: true });
|
|
605
|
+
if (!options.forceRefresh) {
|
|
606
|
+
try {
|
|
607
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
608
|
+
const cached = JSON.parse(raw);
|
|
609
|
+
if (isFresh(cached, maxAge)) {
|
|
610
|
+
return toResponse(cached);
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const res = await fetch(input, init);
|
|
617
|
+
const body = await res.text();
|
|
618
|
+
const record = {
|
|
619
|
+
timestamp: Date.now(),
|
|
620
|
+
status: res.status,
|
|
621
|
+
statusText: res.statusText,
|
|
622
|
+
headers: [...res.headers.entries()],
|
|
623
|
+
body
|
|
624
|
+
};
|
|
625
|
+
if (cacheMode === "memory") {
|
|
626
|
+
memoryCache.set(cacheKey, record);
|
|
627
|
+
} else if (cacheMode === "fs") {
|
|
628
|
+
await fs2.writeFile(filePath, JSON.stringify(record), "utf8");
|
|
629
|
+
}
|
|
630
|
+
return new Response(body, {
|
|
631
|
+
status: res.status,
|
|
632
|
+
statusText: res.statusText,
|
|
633
|
+
headers: res.headers
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
export {
|
|
637
|
+
fetchAndCache,
|
|
638
|
+
htPages
|
|
639
|
+
};
|
|
640
|
+
//# sourceMappingURL=index.js.map
|