@evertondgn/solid-tooling 0.5.0 → 0.6.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 +193 -634
- package/app-env.d.ts +7 -0
- package/dist/architecture-plugin.mjs +1 -1
- package/dist/backend-plugin.mjs +139 -4
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +42 -0
- package/dist/{constants-Jqvtbvxu.mjs → constants-BZJm7ouk.mjs} +1 -1
- package/dist/css-filename-CEHQd2gz.mjs +40 -0
- package/dist/css-filename.mjs +1 -40
- package/dist/fmt-9dlUYOMk.mjs +34 -0
- package/dist/fmt.mjs +1 -34
- package/dist/i18n-generate.d.mts +1 -0
- package/dist/i18n-generate.mjs +23 -0
- package/dist/i18nPlugin-CS7Gob6R.mjs +109 -0
- package/dist/knip.d.mts +2 -1
- package/dist/knip.mjs +7 -2
- package/dist/{lint-UTfyg7AR.mjs → lint-ehdtTKfw.mjs} +33 -86
- package/dist/lint.mjs +1 -1
- package/dist/playwright-teardown.mjs +1 -1
- package/dist/playwright.d.mts +2 -1
- package/dist/playwright.mjs +4 -3
- package/dist/prepareMessages-BXmY6Ko2.mjs +222 -0
- package/dist/project-plugin.mjs +2 -2
- package/dist/protocol-DSNNUYja.mjs +18 -0
- package/dist/publication/read-llms-index.d.mts +12 -0
- package/dist/publication/read-llms-index.mjs +1 -0
- package/dist/publication-CRUTOB0B.mjs +885 -0
- package/dist/readLlmsIndex-Gj4iTxAk.mjs +27 -0
- package/dist/stylelint.mjs +2 -2
- package/dist/test-locale--tuH0Np5.mjs +3 -0
- package/dist/types-DHu2Z_ot.d.mts +49 -0
- package/dist/vite.d.mts +33 -7
- package/dist/vite.mjs +415 -88
- package/dist/vitest-BjLr7iLf.mjs +362 -0
- package/dist/vitest.d.mts +3 -7
- package/dist/vitest.mjs +1 -129
- package/lefthook.yml +3 -1
- package/node-env.d.ts +1 -0
- package/package.json +32 -39
- package/tsconfig.app.json +4 -9
- package/virtual-modules.d.ts +90 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import { a as LLMS_PATH, d as PUBLICATION_PHASE_ENV, f as PUBLICATION_PRERENDER_PHASE, g as SITEMAP_MANIFEST_MAX_BYTES, h as SITEMAP_MANIFEST_FILE, i as LLMS_MAX_BYTES, m as SITEMAP_IDENTITIES_MAX_BYTES, n as LLMS_FULL_MAX_BYTES, o as MARKDOWN_PRERENDER_PATH, p as SITEMAP_IDENTITIES_FILE, r as LLMS_FULL_PATH, t as LLMS_DESCRIBEDBY_LINK, u as PUBLICATION_ARTIFACT_DIR } from "./protocol-DSNNUYja.mjs";
|
|
2
|
+
import { t as readLlmsIndex } from "./readLlmsIndex-Gj4iTxAk.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { env } from "node:process";
|
|
5
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import * as v from "valibot";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
+
// src/publication/assertDisabledArtifacts/index.ts
|
|
10
|
+
async function assertDisabledArtifacts({ publicDir, hasLlms, hasLlmsFull }) {
|
|
11
|
+
const forbidden = [...hasLlms ? [] : [LLMS_PATH], ...hasLlmsFull ? [] : [LLMS_FULL_PATH]];
|
|
12
|
+
for (const name of forbidden) {
|
|
13
|
+
try {
|
|
14
|
+
await lstat(path.join(publicDir, name));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Disabled publication artifact exists: ${name}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// src/publication/publicationArtifacts/constants.ts
|
|
23
|
+
const SITEMAP_INVENTORY_PATH = "/sitemaps/static.json";
|
|
24
|
+
// src/publication/markdownPrerender/helpers/artifactDigest/index.ts
|
|
25
|
+
const CHUNK_BYTES$1 = 65536;
|
|
26
|
+
const HASH = "sha256";
|
|
27
|
+
const READ_FLAGS$1 = constants.O_RDONLY | constants.O_NOFOLLOW;
|
|
28
|
+
function readRouteBytes(route) {
|
|
29
|
+
if (route.data) return new Uint8Array(route.data);
|
|
30
|
+
return new TextEncoder().encode(route.contents ?? "");
|
|
31
|
+
}
|
|
32
|
+
function digestBytes(bytes) {
|
|
33
|
+
return {
|
|
34
|
+
size: bytes.byteLength,
|
|
35
|
+
hash: createHash(HASH).update(bytes).digest("hex")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function digestFile(file) {
|
|
39
|
+
const handle = await open(file, READ_FLAGS$1);
|
|
40
|
+
try {
|
|
41
|
+
const hash = createHash(HASH);
|
|
42
|
+
const buffer = new Uint8Array(CHUNK_BYTES$1);
|
|
43
|
+
let size = 0;
|
|
44
|
+
let read = await handle.read(buffer, 0, CHUNK_BYTES$1, null);
|
|
45
|
+
while (read.bytesRead > 0) {
|
|
46
|
+
size += read.bytesRead;
|
|
47
|
+
hash.update(buffer.subarray(0, read.bytesRead));
|
|
48
|
+
read = await handle.read(buffer, 0, CHUNK_BYTES$1, null);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
size,
|
|
52
|
+
hash: hash.digest("hex")
|
|
53
|
+
};
|
|
54
|
+
} finally {
|
|
55
|
+
await handle.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// src/publication/publicationArtifacts/helpers/assertPublishedArtifacts/index.ts
|
|
59
|
+
const SITEMAP_DIR = "sitemaps";
|
|
60
|
+
const SITEMAP_ROOT$1 = "/sitemap.xml";
|
|
61
|
+
function isMissing$1(error) {
|
|
62
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
63
|
+
}
|
|
64
|
+
async function scanSitemapFiles(publicDir) {
|
|
65
|
+
const scan = {
|
|
66
|
+
files: [],
|
|
67
|
+
irregular: []
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
const entries = await readdir(path.join(publicDir, SITEMAP_DIR), {
|
|
71
|
+
recursive: true,
|
|
72
|
+
withFileTypes: true
|
|
73
|
+
});
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const path$4 = `/${path.relative(publicDir, path.join(entry.parentPath, entry.name)).split(path.sep).join("/")}`;
|
|
76
|
+
if (entry.isDirectory()) continue;
|
|
77
|
+
(entry.isFile() ? scan.files : scan.irregular).push(path$4);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!isMissing$1(error)) throw error;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
((await lstat(path.join(publicDir, SITEMAP_ROOT$1))).isFile() ? scan.files : scan.irregular).push(SITEMAP_ROOT$1);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!isMissing$1(error)) throw error;
|
|
86
|
+
}
|
|
87
|
+
return scan;
|
|
88
|
+
}
|
|
89
|
+
async function findChangedFiles$1({ publicDir, approved }) {
|
|
90
|
+
const problems = [];
|
|
91
|
+
for (const [path$5, digest] of approved) try {
|
|
92
|
+
const actual = await digestFile(path.join(publicDir, path$5));
|
|
93
|
+
if (actual.size !== digest.size || actual.hash !== digest.hash) problems.push(`${path$5} (content differs from the approved artifact)`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!isMissing$1(error)) throw error;
|
|
96
|
+
problems.push(`${path$5} (missing from the static output)`);
|
|
97
|
+
}
|
|
98
|
+
return problems;
|
|
99
|
+
}
|
|
100
|
+
async function assertPublishedArtifacts({ publicDir, prerenderedRoutes, xmlPaths, approved }) {
|
|
101
|
+
const written = new Map(prerenderedRoutes.map((route) => [route.route, route]));
|
|
102
|
+
const { files, irregular } = await scanSitemapFiles(publicDir);
|
|
103
|
+
const found = new Set(files);
|
|
104
|
+
const problems = [
|
|
105
|
+
...irregular.map((path) => `${path} (not a regular file)`),
|
|
106
|
+
...files.filter((path) => !xmlPaths.has(path)).map((path) => `${path} (would shadow a dynamic sitemap route)`),
|
|
107
|
+
...[...xmlPaths].filter((path) => !found.has(path) || written.get(path)?.fileName !== path).map((path) => `${path} (not generated by the prerender)`),
|
|
108
|
+
...await findChangedFiles$1({
|
|
109
|
+
publicDir,
|
|
110
|
+
approved
|
|
111
|
+
})
|
|
112
|
+
];
|
|
113
|
+
if (problems.length > 0) throw new Error(`Invalid publication output: ${problems.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
// src/publication/publicationArtifacts/helpers/writeServerArtifacts/index.ts
|
|
116
|
+
const encoder$2 = new TextEncoder();
|
|
117
|
+
function buildManifest({ inventory, identities, partDigests }) {
|
|
118
|
+
const { buildId, siteUrl, byteBudget, hasArraySources, hasPartitionSources } = inventory;
|
|
119
|
+
const identityDigest = identities && digestBytes(identities);
|
|
120
|
+
const generation = createHash("sha256").update(JSON.stringify({
|
|
121
|
+
version: 1,
|
|
122
|
+
buildId,
|
|
123
|
+
siteUrl,
|
|
124
|
+
byteBudget,
|
|
125
|
+
hasArraySources,
|
|
126
|
+
hasPartitionSources,
|
|
127
|
+
parts: partDigests.map(({ hash }) => hash),
|
|
128
|
+
identities: identityDigest?.hash
|
|
129
|
+
})).digest("hex");
|
|
130
|
+
return JSON.stringify({
|
|
131
|
+
version: 1,
|
|
132
|
+
buildId,
|
|
133
|
+
generation,
|
|
134
|
+
siteUrl,
|
|
135
|
+
byteBudget,
|
|
136
|
+
pagePartCount: partDigests.length,
|
|
137
|
+
partBytes: partDigests.map(({ size }) => size),
|
|
138
|
+
isRootStatic: !hasArraySources && !hasPartitionSources && partDigests.length > 0,
|
|
139
|
+
hasArraySources,
|
|
140
|
+
hasPartitionSources,
|
|
141
|
+
identities: identityDigest ? {
|
|
142
|
+
file: SITEMAP_IDENTITIES_FILE,
|
|
143
|
+
bytes: identityDigest.size,
|
|
144
|
+
sha256: identityDigest.hash
|
|
145
|
+
} : null
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function publishFile({ directory, name, bytes }) {
|
|
149
|
+
const workDir = await mkdtemp(path.join(directory, ".artifact-"));
|
|
150
|
+
try {
|
|
151
|
+
const temporary = path.join(workDir, name);
|
|
152
|
+
await writeFile(temporary, bytes, { flag: "wx" });
|
|
153
|
+
await rename(temporary, path.join(directory, name));
|
|
154
|
+
} finally {
|
|
155
|
+
await rm(workDir, {
|
|
156
|
+
recursive: true,
|
|
157
|
+
force: true
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const expected = digestBytes(bytes);
|
|
161
|
+
const actual = await digestFile(path.join(directory, name));
|
|
162
|
+
if (actual.size !== expected.size || actual.hash !== expected.hash) throw new Error(`Server artifact differs after writing: ${name}`);
|
|
163
|
+
}
|
|
164
|
+
async function writeServerArtifacts({ serverDir, ...options }) {
|
|
165
|
+
const manifest = encoder$2.encode(buildManifest(options));
|
|
166
|
+
if (manifest.byteLength > 1048576) throw new Error(`Sitemap manifest has ${manifest.byteLength} bytes, above the ${SITEMAP_MANIFEST_MAX_BYTES} byte budget`);
|
|
167
|
+
const directory = path.join(serverDir, PUBLICATION_ARTIFACT_DIR);
|
|
168
|
+
await mkdir(directory, { recursive: true });
|
|
169
|
+
const { identities } = options;
|
|
170
|
+
if (identities) await publishFile({
|
|
171
|
+
directory,
|
|
172
|
+
name: SITEMAP_IDENTITIES_FILE,
|
|
173
|
+
bytes: identities
|
|
174
|
+
});
|
|
175
|
+
await publishFile({
|
|
176
|
+
directory,
|
|
177
|
+
name: SITEMAP_MANIFEST_FILE,
|
|
178
|
+
bytes: manifest
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// src/publication/publicationArtifacts/index.ts
|
|
182
|
+
const SITEMAP_ROOT = "/sitemap.xml";
|
|
183
|
+
const XML_TYPE = "application/xml";
|
|
184
|
+
const XML_START = "<?xml";
|
|
185
|
+
const encoder$1 = new TextEncoder();
|
|
186
|
+
const CountSchema = v.pipe(v.number(), v.integer(), v.minValue(0));
|
|
187
|
+
const EntrySchema = v.object({
|
|
188
|
+
path: v.string(),
|
|
189
|
+
lastmod: v.optional(v.string())
|
|
190
|
+
});
|
|
191
|
+
const IdentityListSchema = v.object({
|
|
192
|
+
urls: v.array(v.string()),
|
|
193
|
+
entries: v.array(EntrySchema)
|
|
194
|
+
});
|
|
195
|
+
const InventorySchema = v.object({
|
|
196
|
+
version: v.literal(1),
|
|
197
|
+
buildId: v.pipe(v.string(), v.nonEmpty()),
|
|
198
|
+
siteUrl: v.pipe(v.string(), v.url()),
|
|
199
|
+
byteBudget: v.pipe(CountSchema, v.minValue(1)),
|
|
200
|
+
pagePartCount: CountSchema,
|
|
201
|
+
hasArraySources: v.boolean(),
|
|
202
|
+
hasPartitionSources: v.boolean(),
|
|
203
|
+
identities: v.nullable(IdentityListSchema)
|
|
204
|
+
});
|
|
205
|
+
function partPath(index) {
|
|
206
|
+
return `/sitemaps/pages/${index}.xml`;
|
|
207
|
+
}
|
|
208
|
+
function readInventory$1(contents) {
|
|
209
|
+
try {
|
|
210
|
+
const inventory = v.parse(InventorySchema, JSON.parse(contents ?? ""));
|
|
211
|
+
return inventory.hasArraySources === (inventory.identities !== null) ? inventory : void 0;
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function findXmlProblem({ route, byteBudget }) {
|
|
217
|
+
const bytes = readRouteBytes(route);
|
|
218
|
+
if (route.fileName !== route.route) return `written as ${route.fileName}`;
|
|
219
|
+
if (!route.contentType?.startsWith(XML_TYPE)) return `content type ${route.contentType}`;
|
|
220
|
+
if (bytes.byteLength > byteBudget) return `${bytes.byteLength} bytes, above the ${byteBudget} byte budget`;
|
|
221
|
+
return route.contents?.startsWith(XML_START) ? void 0 : "not an XML document";
|
|
222
|
+
}
|
|
223
|
+
function publicationArtifacts({ buildId, hasSitemap = true }) {
|
|
224
|
+
const approved = /* @__PURE__ */ new Map();
|
|
225
|
+
const xmlPaths = /* @__PURE__ */ new Set();
|
|
226
|
+
let inventory;
|
|
227
|
+
let identities;
|
|
228
|
+
let isVerified = false;
|
|
229
|
+
function readSitemapInventory({ route, queue }) {
|
|
230
|
+
route.skip = true;
|
|
231
|
+
if (route.error) return;
|
|
232
|
+
inventory = readInventory$1(route.contents);
|
|
233
|
+
if (!queue || !inventory) {
|
|
234
|
+
route.error = /* @__PURE__ */ new Error("Invalid sitemap inventory");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (inventory.buildId !== buildId) {
|
|
238
|
+
route.error = /* @__PURE__ */ new Error("Sitemap inventory belongs to another build");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (inventory.identities) {
|
|
242
|
+
identities = encoder$1.encode(JSON.stringify(inventory.identities));
|
|
243
|
+
if (identities.byteLength > 33554432) {
|
|
244
|
+
route.error = /* @__PURE__ */ new Error(`Sitemap identities have ${identities.byteLength} bytes, above the ${SITEMAP_IDENTITIES_MAX_BYTES} byte budget`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const { pagePartCount, hasArraySources, hasPartitionSources } = inventory;
|
|
249
|
+
for (let index = 1; index <= pagePartCount; index += 1) xmlPaths.add(partPath(index));
|
|
250
|
+
if (pagePartCount > 0 && !hasArraySources && !hasPartitionSources) xmlPaths.add(SITEMAP_ROOT);
|
|
251
|
+
for (const path of xmlPaths) queue.add(path);
|
|
252
|
+
}
|
|
253
|
+
function approveXml(options) {
|
|
254
|
+
const { route } = options;
|
|
255
|
+
if (route.error) return;
|
|
256
|
+
const problem = findXmlProblem(options);
|
|
257
|
+
if (problem) {
|
|
258
|
+
route.error = /* @__PURE__ */ new Error(`Invalid sitemap ${route.route}: ${problem}`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
approved.set(route.route, digestBytes(readRouteBytes(route)));
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
inspect(options) {
|
|
265
|
+
const { route } = options;
|
|
266
|
+
if (hasSitemap && route.route === "/sitemaps/static.json") {
|
|
267
|
+
readSitemapInventory(options);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (!inventory || !xmlPaths.has(route.route)) return false;
|
|
271
|
+
approveXml({
|
|
272
|
+
route,
|
|
273
|
+
byteBudget: inventory.byteBudget
|
|
274
|
+
});
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
approve({ route, maxBytes }) {
|
|
278
|
+
const digest = digestBytes(readRouteBytes(route));
|
|
279
|
+
if (maxBytes !== void 0 && digest.size > maxBytes) {
|
|
280
|
+
route.error = /* @__PURE__ */ new Error(`${route.route} has ${digest.size} bytes, above the ${maxBytes} byte budget`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
approved.set(route.route, digest);
|
|
284
|
+
},
|
|
285
|
+
async assertPublished(options) {
|
|
286
|
+
if (hasSitemap && !inventory) throw new Error("Sitemap inventory was not prerendered");
|
|
287
|
+
await assertPublishedArtifacts({
|
|
288
|
+
...options,
|
|
289
|
+
xmlPaths,
|
|
290
|
+
approved
|
|
291
|
+
});
|
|
292
|
+
isVerified = true;
|
|
293
|
+
},
|
|
294
|
+
async writeServer(serverDir) {
|
|
295
|
+
if (!inventory || !isVerified) return;
|
|
296
|
+
const partDigests = Array.from({ length: inventory.pagePartCount }, (_, index) => {
|
|
297
|
+
const digest = approved.get(partPath(index + 1));
|
|
298
|
+
if (!digest) throw new Error(`Unverified sitemap part ${index + 1}`);
|
|
299
|
+
return digest;
|
|
300
|
+
});
|
|
301
|
+
await writeServerArtifacts({
|
|
302
|
+
serverDir,
|
|
303
|
+
inventory,
|
|
304
|
+
identities,
|
|
305
|
+
partDigests
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// src/publication/markdownPrerender/helpers/findMarkdownFiles/index.ts
|
|
311
|
+
const MARKDOWN_EXTENSION = ".md";
|
|
312
|
+
const HTML_ALIAS_EXTENSION = ".md.html";
|
|
313
|
+
async function findMarkdownFiles(publicDir) {
|
|
314
|
+
const result = {
|
|
315
|
+
files: [],
|
|
316
|
+
htmlAliases: [],
|
|
317
|
+
irregular: []
|
|
318
|
+
};
|
|
319
|
+
const entries = await readdir(publicDir, {
|
|
320
|
+
recursive: true,
|
|
321
|
+
withFileTypes: true
|
|
322
|
+
});
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
const isAlias = entry.name.endsWith(HTML_ALIAS_EXTENSION);
|
|
325
|
+
if (!isAlias && !entry.name.endsWith(MARKDOWN_EXTENSION)) continue;
|
|
326
|
+
const path$3 = `/${path.relative(publicDir, path.join(entry.parentPath, entry.name)).split(path.sep).join("/")}`;
|
|
327
|
+
if (isAlias) {
|
|
328
|
+
result.htmlAliases.push(path$3);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (entry.isFile() && !entry.isSymbolicLink()) result.files.push(path$3);
|
|
332
|
+
else result.irregular.push(path$3);
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
// src/publication/markdownPrerender/helpers/assertStaticOutput/index.ts
|
|
337
|
+
async function findChangedFiles({ publicDir, expected, approved }) {
|
|
338
|
+
const problems = [];
|
|
339
|
+
for (const path$2 of expected.keys()) {
|
|
340
|
+
const digest = approved.get(path$2);
|
|
341
|
+
if (!digest) {
|
|
342
|
+
problems.push(`${path$2} (not validated by the prerender)`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const actual = await digestFile(path.join(publicDir, path$2));
|
|
346
|
+
if (actual.size !== digest.size || actual.hash !== digest.hash) problems.push(`${path$2} (content differs from the approved artifact)`);
|
|
347
|
+
}
|
|
348
|
+
return problems;
|
|
349
|
+
}
|
|
350
|
+
async function assertStaticOutput(options) {
|
|
351
|
+
const { publicDir, expected } = options;
|
|
352
|
+
const { files, htmlAliases, irregular } = await findMarkdownFiles(publicDir);
|
|
353
|
+
const found = new Set(files);
|
|
354
|
+
const problems = [
|
|
355
|
+
...htmlAliases.map((path) => `${path} (HTML written for a .md URL)`),
|
|
356
|
+
...irregular.map((path) => `${path} (not a regular file)`),
|
|
357
|
+
...files.filter((path) => !expected.has(path)).map((path) => `${path} (not in the static Markdown list)`),
|
|
358
|
+
...[...expected.keys()].filter((path) => !found.has(path)).map((path) => `${path} (missing from the static output)`)
|
|
359
|
+
];
|
|
360
|
+
if (problems.length === 0) problems.push(...await findChangedFiles(options));
|
|
361
|
+
if (problems.length > 0) throw new Error(`Invalid static Markdown output: ${problems.join(", ")}`);
|
|
362
|
+
}
|
|
363
|
+
// src/publication/markdownPrerender/helpers/inspectMarkdownArtifact/index.ts
|
|
364
|
+
const HTML_DOCUMENT_START = /^\s*(?:<!doctype\s+html\b|<html[\s>])/iu;
|
|
365
|
+
const HTML_DOCUMENT_END = /<\/html>\s*$/iu;
|
|
366
|
+
const MARKDOWN_TYPE = "text/markdown";
|
|
367
|
+
const UTF8_CHARSET = "charset=utf-8";
|
|
368
|
+
function isMarkdownUtf8(contentType) {
|
|
369
|
+
const [type, ...parameters] = (contentType ?? "").split(";").map((part) => part.trim().toLowerCase().replaceAll("\"", ""));
|
|
370
|
+
return type === MARKDOWN_TYPE && parameters.includes(UTF8_CHARSET);
|
|
371
|
+
}
|
|
372
|
+
function inspectMarkdownArtifact(route) {
|
|
373
|
+
if (route.fileName !== route.route) return `written as ${route.fileName ?? "no file"}`;
|
|
374
|
+
if (!route.contentType) return "served as no content type";
|
|
375
|
+
if (!isMarkdownUtf8(route.contentType)) return `served as ${route.contentType}`;
|
|
376
|
+
let text;
|
|
377
|
+
try {
|
|
378
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(readRouteBytes(route));
|
|
379
|
+
} catch {
|
|
380
|
+
return "invalid UTF-8 body";
|
|
381
|
+
}
|
|
382
|
+
if (!text.trim()) return "empty body";
|
|
383
|
+
if (HTML_DOCUMENT_START.test(text) && HTML_DOCUMENT_END.test(text)) return "HTML document body";
|
|
384
|
+
}
|
|
385
|
+
// src/publication/markdownPrerender/helpers/selectLlmsFullDocuments/index.ts
|
|
386
|
+
function classify({ href, origin, inventory, baseLocalePaths, ownPaths }) {
|
|
387
|
+
let url;
|
|
388
|
+
let path;
|
|
389
|
+
try {
|
|
390
|
+
url = new URL(href);
|
|
391
|
+
path = decodeURI(url.pathname);
|
|
392
|
+
} catch {
|
|
393
|
+
return { reason: "invalid URL" };
|
|
394
|
+
}
|
|
395
|
+
if (url.origin !== origin) return { reason: "external link" };
|
|
396
|
+
if (ownPaths.has(path)) return void 0;
|
|
397
|
+
if (url.search) return { reason: "URL with query, served on request" };
|
|
398
|
+
if (!path.endsWith(".md")) return { reason: "HTML page without Markdown" };
|
|
399
|
+
if (!inventory.has(path)) return { reason: "Markdown generated on request" };
|
|
400
|
+
if (!baseLocalePaths.has(path)) return { reason: "page in another language" };
|
|
401
|
+
url.hash = "";
|
|
402
|
+
return { document: {
|
|
403
|
+
url: url.href,
|
|
404
|
+
path
|
|
405
|
+
} };
|
|
406
|
+
}
|
|
407
|
+
function selectLlmsFullDocuments({ links, siteUrl, inventory, baseLocalePaths, ownPaths }) {
|
|
408
|
+
const { origin } = new URL(siteUrl);
|
|
409
|
+
const documents = /* @__PURE__ */ new Map();
|
|
410
|
+
const omitted = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const { href } of links) {
|
|
412
|
+
const result = classify({
|
|
413
|
+
href,
|
|
414
|
+
origin,
|
|
415
|
+
inventory,
|
|
416
|
+
baseLocalePaths,
|
|
417
|
+
ownPaths
|
|
418
|
+
});
|
|
419
|
+
if (!result) continue;
|
|
420
|
+
if ("document" in result) {
|
|
421
|
+
const { document } = result;
|
|
422
|
+
if (!documents.has(document.path)) documents.set(document.path, document);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!omitted.has(href)) omitted.set(href, {
|
|
426
|
+
url: href,
|
|
427
|
+
reason: result.reason
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
documents: [...documents.values()],
|
|
432
|
+
omitted: [...omitted.values()]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// src/publication/markdownPrerender/helpers/writeAll/index.ts
|
|
436
|
+
async function writeAll({ output, bytes, signal }) {
|
|
437
|
+
let offset = 0;
|
|
438
|
+
while (offset < bytes.byteLength) {
|
|
439
|
+
signal?.throwIfAborted();
|
|
440
|
+
const { bytesWritten } = await output.write(bytes.subarray(offset));
|
|
441
|
+
if (bytesWritten <= 0) throw new Error("File write made no progress");
|
|
442
|
+
offset += bytesWritten;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// src/publication/markdownPrerender/helpers/writeLlmsFull/index.ts
|
|
446
|
+
const CHUNK_BYTES = 65536;
|
|
447
|
+
const FILE_NAME = LLMS_FULL_PATH.slice(1);
|
|
448
|
+
const READ_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW;
|
|
449
|
+
const encoder = new TextEncoder();
|
|
450
|
+
const SEPARATOR = encoder.encode("\n");
|
|
451
|
+
function isMissing(error) {
|
|
452
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
453
|
+
}
|
|
454
|
+
async function exists(file) {
|
|
455
|
+
try {
|
|
456
|
+
await lstat(file);
|
|
457
|
+
return true;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (isMissing(error)) return false;
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function resolveArtifact({ root, path: path$1 }) {
|
|
464
|
+
const segments = path$1.slice(1).split("/");
|
|
465
|
+
if (!path$1.startsWith("/") || path$1.includes("\0") || segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment.includes("\\"))) throw new Error(`Static Markdown path escapes the output: ${path$1}`);
|
|
466
|
+
const file = path.join(root, ...segments);
|
|
467
|
+
let real;
|
|
468
|
+
try {
|
|
469
|
+
real = await realpath(file);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (isMissing(error)) throw new Error(`Static Markdown missing: ${path$1}`, { cause: error });
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
const stats = await lstat(file);
|
|
475
|
+
if (real !== file || !stats.isFile()) throw new Error(`Static Markdown is not a regular file: ${path$1}`);
|
|
476
|
+
return {
|
|
477
|
+
file,
|
|
478
|
+
size: stats.size
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function formatHeader({ index, baseLocale, documents, omitted }) {
|
|
482
|
+
const noun = documents.length === 1 ? "page" : "pages";
|
|
483
|
+
const lines = [`# ${index.title}`, ""];
|
|
484
|
+
if (index.description) lines.push(`> ${index.description}`, "");
|
|
485
|
+
lines.push(`Complete Markdown of ${documents.length} static ${noun} linked from ${index.url}, in the base language (${baseLocale}) and in index order. Each document starts with a Source line, the URL of its Markdown file, and a Bytes line with the exact UTF-8 length of the body that follows.`, "", "## Documents", "", ...documents.map(({ url }) => `- ${url}`), "");
|
|
486
|
+
if (omitted.length > 0) lines.push("## Not included", "", "Linked from the index, but outside this file:", "", ...omitted.map(({ url, reason }) => `- ${url}: ${reason}`), "");
|
|
487
|
+
return lines.join("\n");
|
|
488
|
+
}
|
|
489
|
+
function formatSource({ url, size }) {
|
|
490
|
+
return `\nSource: ${url}\nBytes: ${size}\n\n`;
|
|
491
|
+
}
|
|
492
|
+
async function copyArtifact({ artifact, output, signal }) {
|
|
493
|
+
const changed = /* @__PURE__ */ new Error(`Static Markdown changed while reading: ${artifact.path}`);
|
|
494
|
+
const source = await open(artifact.file, READ_FLAGS);
|
|
495
|
+
try {
|
|
496
|
+
const stats = await source.stat();
|
|
497
|
+
if (!stats.isFile() || stats.size !== artifact.size) throw changed;
|
|
498
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
499
|
+
const buffer = new Uint8Array(CHUNK_BYTES);
|
|
500
|
+
let total = 0;
|
|
501
|
+
let read = await source.read(buffer, 0, CHUNK_BYTES, null);
|
|
502
|
+
while (read.bytesRead > 0) {
|
|
503
|
+
total += read.bytesRead;
|
|
504
|
+
if (total > artifact.size) throw changed;
|
|
505
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
506
|
+
decoder.decode(chunk, { stream: true });
|
|
507
|
+
await writeAll({
|
|
508
|
+
output,
|
|
509
|
+
bytes: chunk,
|
|
510
|
+
signal
|
|
511
|
+
});
|
|
512
|
+
read = await source.read(buffer, 0, CHUNK_BYTES, null);
|
|
513
|
+
}
|
|
514
|
+
decoder.decode();
|
|
515
|
+
if (total !== artifact.size) throw changed;
|
|
516
|
+
} finally {
|
|
517
|
+
await source.close();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function assemble({ output, header, artifacts, signal }) {
|
|
521
|
+
await writeAll({
|
|
522
|
+
output,
|
|
523
|
+
bytes: header,
|
|
524
|
+
signal
|
|
525
|
+
});
|
|
526
|
+
for (const artifact of artifacts) {
|
|
527
|
+
await writeAll({
|
|
528
|
+
output,
|
|
529
|
+
bytes: encoder.encode(formatSource(artifact)),
|
|
530
|
+
signal
|
|
531
|
+
});
|
|
532
|
+
await copyArtifact({
|
|
533
|
+
artifact,
|
|
534
|
+
output,
|
|
535
|
+
signal
|
|
536
|
+
});
|
|
537
|
+
await writeAll({
|
|
538
|
+
output,
|
|
539
|
+
bytes: SEPARATOR,
|
|
540
|
+
signal
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function writeLlmsFull(options) {
|
|
545
|
+
const { outputDir, publicDir, maxBytes, documents, signal } = options;
|
|
546
|
+
signal?.throwIfAborted();
|
|
547
|
+
const root = await realpath(publicDir);
|
|
548
|
+
const target = path.join(root, FILE_NAME);
|
|
549
|
+
if (await exists(target)) throw new Error(`${LLMS_FULL_PATH} already exists in the static output`);
|
|
550
|
+
if (documents.length === 0) return;
|
|
551
|
+
const artifacts = [];
|
|
552
|
+
for (const document of documents) artifacts.push({
|
|
553
|
+
...document,
|
|
554
|
+
...await resolveArtifact({
|
|
555
|
+
root,
|
|
556
|
+
path: document.path
|
|
557
|
+
})
|
|
558
|
+
});
|
|
559
|
+
const header = encoder.encode(formatHeader(options));
|
|
560
|
+
const total = artifacts.reduce((sum, artifact) => sum + encoder.encode(formatSource(artifact)).byteLength + artifact.size + SEPARATOR.byteLength, header.byteLength);
|
|
561
|
+
if (total > maxBytes) throw new Error(`${LLMS_FULL_PATH} would have ${total} bytes, above the ${maxBytes} byte budget`);
|
|
562
|
+
const workDir = await mkdtemp(path.join(outputDir, ".llms-full-"));
|
|
563
|
+
try {
|
|
564
|
+
const temporary = path.join(workDir, FILE_NAME);
|
|
565
|
+
const output = await open(temporary, "wx");
|
|
566
|
+
try {
|
|
567
|
+
await assemble({
|
|
568
|
+
output,
|
|
569
|
+
header,
|
|
570
|
+
artifacts,
|
|
571
|
+
signal
|
|
572
|
+
});
|
|
573
|
+
await output.sync();
|
|
574
|
+
const { size } = await output.stat();
|
|
575
|
+
if (size !== total) throw new Error(`${LLMS_FULL_PATH} has ${size} bytes instead of the expected ${total}`);
|
|
576
|
+
} finally {
|
|
577
|
+
await output.close();
|
|
578
|
+
}
|
|
579
|
+
signal?.throwIfAborted();
|
|
580
|
+
await rename(temporary, target);
|
|
581
|
+
} finally {
|
|
582
|
+
await rm(workDir, {
|
|
583
|
+
recursive: true,
|
|
584
|
+
force: true
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// src/publication/markdownPrerender/helpers/writeVercelRoutes/index.ts
|
|
589
|
+
const INDEX_FILE = "/index.md";
|
|
590
|
+
const NOT_FOUND = -1;
|
|
591
|
+
const VERCEL_CONFIG_VERSION = 3;
|
|
592
|
+
const JSON_INDENT = 2;
|
|
593
|
+
const LLMS_TYPE = "text/markdown; charset=utf-8";
|
|
594
|
+
const REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/gu;
|
|
595
|
+
const RouteSchema = v.looseObject({
|
|
596
|
+
src: v.optional(v.string()),
|
|
597
|
+
handle: v.optional(v.string())
|
|
598
|
+
});
|
|
599
|
+
const OverridesSchema = v.record(v.string(), v.looseObject({}));
|
|
600
|
+
const BuildConfigSchema = v.looseObject({
|
|
601
|
+
version: v.literal(VERCEL_CONFIG_VERSION),
|
|
602
|
+
routes: v.array(RouteSchema),
|
|
603
|
+
overrides: v.optional(OverridesSchema)
|
|
604
|
+
});
|
|
605
|
+
function escapeRegex(text) {
|
|
606
|
+
return text.replaceAll(REGEX_SPECIALS, String.raw`\$&`);
|
|
607
|
+
}
|
|
608
|
+
function assertInitialRoutes({ routes, directories }) {
|
|
609
|
+
const pages = directories.flatMap((directory) => directory ? [directory, `${directory}/`] : ["/"]);
|
|
610
|
+
for (const route of routes) {
|
|
611
|
+
if (route.src === void 0 || route.continue === true || route.dest !== void 0 || route.status !== void 0) continue;
|
|
612
|
+
const pattern = new RegExp(`^(?:${route.src})$`, route.caseSensitive === true ? "u" : "iu");
|
|
613
|
+
if (pages.some((page) => pattern.test(page))) throw new Error(`Vercel route ends before Markdown page protection: ${route.src}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function guardDirectoryPages({ routes, directories }) {
|
|
617
|
+
if (directories.length === 0) return [];
|
|
618
|
+
const prefix = `^(?=(?:${directories.map((directory) => directory ? `${escapeRegex(directory)}/?` : "/").join("|")})$)`;
|
|
619
|
+
let phase;
|
|
620
|
+
const guards = routes.flatMap((route) => {
|
|
621
|
+
if (route.handle !== void 0) {
|
|
622
|
+
phase = route.handle;
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
if (phase !== "filesystem" || route.src === void 0) return [];
|
|
626
|
+
return [{
|
|
627
|
+
...route,
|
|
628
|
+
src: `${prefix}(?:${route.src})$`
|
|
629
|
+
}];
|
|
630
|
+
});
|
|
631
|
+
const boundary = routes.findIndex((route) => "handle" in route);
|
|
632
|
+
if (boundary === NOT_FOUND || guards.length === 0) throw new Error("Vercel output has no filesystem routes for Markdown pages");
|
|
633
|
+
assertInitialRoutes({
|
|
634
|
+
routes: routes.slice(0, boundary),
|
|
635
|
+
directories
|
|
636
|
+
});
|
|
637
|
+
return guards;
|
|
638
|
+
}
|
|
639
|
+
function describedbyRoute(hasLlmsFull) {
|
|
640
|
+
return {
|
|
641
|
+
src: `^/(?:${hasLlmsFull ? `.+\\.md|${escapeRegex(LLMS_FULL_PATH.slice(1))}` : String.raw`.+\.md`})$`,
|
|
642
|
+
headers: { link: LLMS_DESCRIBEDBY_LINK },
|
|
643
|
+
continue: true
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function markdownRoutes({ routes, paths, hasLlmsFull, hasLlms }) {
|
|
647
|
+
const guards = guardDirectoryPages({
|
|
648
|
+
routes,
|
|
649
|
+
directories: paths.filter((path) => path.endsWith(INDEX_FILE)).map((path) => path.slice(0, -9))
|
|
650
|
+
});
|
|
651
|
+
if (!routes.some((route) => route.handle === "filesystem")) throw new Error("Vercel output has no filesystem phase for static Markdown");
|
|
652
|
+
return [...hasLlms ? [describedbyRoute(hasLlmsFull)] : [], ...guards];
|
|
653
|
+
}
|
|
654
|
+
async function writeVercelRoutes({ outputDir, paths, hasLlmsFull, hasLlmsIndex, hasLlms }) {
|
|
655
|
+
const hasMarkdown = paths.length > 0 || hasLlmsFull;
|
|
656
|
+
if (!hasMarkdown && !hasLlmsIndex) return;
|
|
657
|
+
const file = path.join(outputDir, "config.json");
|
|
658
|
+
const config = v.parse(BuildConfigSchema, JSON.parse(await readFile(file, "utf8")));
|
|
659
|
+
if (hasMarkdown) {
|
|
660
|
+
const { routes } = config;
|
|
661
|
+
const initial = routes.findIndex((route) => "handle" in route);
|
|
662
|
+
routes.splice(initial, 0, ...markdownRoutes({
|
|
663
|
+
routes,
|
|
664
|
+
paths,
|
|
665
|
+
hasLlmsFull,
|
|
666
|
+
hasLlmsIndex,
|
|
667
|
+
hasLlms
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
const textFiles = [...hasLlmsIndex ? [LLMS_PATH] : [], ...hasLlmsFull ? [LLMS_FULL_PATH] : []];
|
|
671
|
+
for (const textFile of textFiles) {
|
|
672
|
+
const name = textFile.slice(1);
|
|
673
|
+
config.overrides = {
|
|
674
|
+
...config.overrides,
|
|
675
|
+
[name]: {
|
|
676
|
+
...config.overrides?.[name],
|
|
677
|
+
contentType: LLMS_TYPE
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
await writeFile(file, `${JSON.stringify(config, null, JSON_INDENT)}\n`);
|
|
682
|
+
}
|
|
683
|
+
// src/publication/markdownPrerender/index.ts
|
|
684
|
+
const MarkdownPathSchema = v.pipe(v.string(), v.regex(/^\/[^?#]+\.md$/u));
|
|
685
|
+
const ManifestSchema = v.object({
|
|
686
|
+
buildId: v.optional(v.string()),
|
|
687
|
+
siteUrl: v.pipe(v.string(), v.url()),
|
|
688
|
+
baseLocale: v.pipe(v.string(), v.nonEmpty()),
|
|
689
|
+
paths: v.array(MarkdownPathSchema),
|
|
690
|
+
baseLocalePaths: v.array(MarkdownPathSchema),
|
|
691
|
+
mismatches: v.array(v.string())
|
|
692
|
+
});
|
|
693
|
+
function readInventory(contents) {
|
|
694
|
+
try {
|
|
695
|
+
const manifest = v.parse(ManifestSchema, JSON.parse(contents ?? ""));
|
|
696
|
+
const expected = new Map(manifest.paths.map((path) => [decodeURI(path), path]));
|
|
697
|
+
const baseLocalePaths = new Set(manifest.baseLocalePaths.map((path) => decodeURI(path)));
|
|
698
|
+
return expected.size === manifest.paths.length && [...baseLocalePaths].every((path) => expected.has(path)) ? {
|
|
699
|
+
manifest,
|
|
700
|
+
expected,
|
|
701
|
+
baseLocalePaths
|
|
702
|
+
} : void 0;
|
|
703
|
+
} catch {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function isMarkdownArtifact(route) {
|
|
708
|
+
return route.route.endsWith(".md") || route.fileName?.endsWith(".md") === true;
|
|
709
|
+
}
|
|
710
|
+
function assertRegistered({ prerenderedRoutes, expected }) {
|
|
711
|
+
const written = new Map(prerenderedRoutes.map((route) => [route.route, route]));
|
|
712
|
+
const missing = [...expected].filter(([path]) => written.get(path)?.fileName !== path).map(([, published]) => published);
|
|
713
|
+
if (missing.length > 0) throw new Error(`Static Markdown not generated: ${missing.join(", ")}`);
|
|
714
|
+
const extra = prerenderedRoutes.filter((route) => isMarkdownArtifact(route) && !expected.has(route.route)).map((route) => route.route);
|
|
715
|
+
if (extra.length > 0) throw new Error(`Static Markdown outside the list: ${extra.join(", ")}`);
|
|
716
|
+
}
|
|
717
|
+
async function publishLlmsFull({ inventory, llmsText, outputDir, publicDir, maxBytes }) {
|
|
718
|
+
const { manifest, expected, baseLocalePaths } = inventory;
|
|
719
|
+
const index = readLlmsIndex(llmsText);
|
|
720
|
+
const { documents, omitted } = selectLlmsFullDocuments({
|
|
721
|
+
links: index.links,
|
|
722
|
+
siteUrl: manifest.siteUrl,
|
|
723
|
+
inventory: new Set(expected.keys()),
|
|
724
|
+
baseLocalePaths,
|
|
725
|
+
ownPaths: /* @__PURE__ */ new Set([LLMS_PATH, LLMS_FULL_PATH])
|
|
726
|
+
});
|
|
727
|
+
const fullUrl = new URL(LLMS_FULL_PATH, manifest.siteUrl).href;
|
|
728
|
+
const isAnnounced = index.links.some(({ href }) => href === fullUrl);
|
|
729
|
+
if (isAnnounced !== documents.length > 0) throw new Error(isAnnounced ? `llms.txt announces ${LLMS_FULL_PATH} without static Markdown pages` : `llms.txt does not announce ${LLMS_FULL_PATH} for its static Markdown pages`);
|
|
730
|
+
await writeLlmsFull({
|
|
731
|
+
outputDir,
|
|
732
|
+
publicDir,
|
|
733
|
+
maxBytes,
|
|
734
|
+
index: {
|
|
735
|
+
title: index.title,
|
|
736
|
+
description: index.description,
|
|
737
|
+
url: new URL(LLMS_PATH, manifest.siteUrl).href
|
|
738
|
+
},
|
|
739
|
+
baseLocale: manifest.baseLocale,
|
|
740
|
+
documents,
|
|
741
|
+
omitted
|
|
742
|
+
});
|
|
743
|
+
return documents.length > 0;
|
|
744
|
+
}
|
|
745
|
+
function markdownPrerender({ buildId, hasMarkdown = true, hasSitemap = true, hasLlms = true, hasLlmsFull: shouldBuildLlmsFull = true, llmsMaxBytes = LLMS_MAX_BYTES, llmsFullMaxBytes = LLMS_FULL_MAX_BYTES }) {
|
|
746
|
+
return (nitro) => {
|
|
747
|
+
let queue;
|
|
748
|
+
let inventory;
|
|
749
|
+
let llmsText;
|
|
750
|
+
let hasLlmsFull = false;
|
|
751
|
+
const approved = /* @__PURE__ */ new Map();
|
|
752
|
+
const publication = publicationArtifacts({
|
|
753
|
+
buildId,
|
|
754
|
+
hasSitemap
|
|
755
|
+
});
|
|
756
|
+
nitro.options.prerender.failOnError = true;
|
|
757
|
+
nitro.options.prerender.routes = [.../* @__PURE__ */ new Set([
|
|
758
|
+
...nitro.options.prerender.routes ?? [],
|
|
759
|
+
...hasMarkdown ? [MARKDOWN_PRERENDER_PATH] : [],
|
|
760
|
+
...hasSitemap ? [SITEMAP_INVENTORY_PATH] : [],
|
|
761
|
+
...hasLlms ? [LLMS_PATH] : []
|
|
762
|
+
])];
|
|
763
|
+
nitro.hooks.hook("prerender:init", () => {
|
|
764
|
+
env[PUBLICATION_PHASE_ENV] = PUBLICATION_PRERENDER_PHASE;
|
|
765
|
+
});
|
|
766
|
+
nitro.hooks.hook("prerender:routes", (initial) => {
|
|
767
|
+
queue = initial;
|
|
768
|
+
});
|
|
769
|
+
nitro.hooks.hook("prerender:generate", (route) => {
|
|
770
|
+
if (publication.inspect({
|
|
771
|
+
route,
|
|
772
|
+
queue
|
|
773
|
+
})) return;
|
|
774
|
+
if (hasMarkdown && route.route === "/_markdown/static.json") {
|
|
775
|
+
if (route.error) return;
|
|
776
|
+
inventory = readInventory(route.contents);
|
|
777
|
+
if (!queue || !inventory) {
|
|
778
|
+
route.error = /* @__PURE__ */ new Error("Invalid static Markdown list");
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (!inventory.manifest.buildId || inventory.manifest.buildId !== buildId) {
|
|
782
|
+
route.error = /* @__PURE__ */ new Error("Static Markdown build identity does not match SSR");
|
|
783
|
+
inventory = void 0;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const { mismatches, paths } = inventory.manifest;
|
|
787
|
+
if (mismatches.length > 0) {
|
|
788
|
+
route.error = /* @__PURE__ */ new Error(`Static Markdown pages resolve to another route: ${mismatches.join(", ")}`);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
for (const path of paths) queue.add(path);
|
|
792
|
+
publication.approve({ route });
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (hasLlms && route.route === "/llms.txt") {
|
|
796
|
+
if (route.error) return;
|
|
797
|
+
llmsText = route.contents;
|
|
798
|
+
publication.approve({
|
|
799
|
+
route,
|
|
800
|
+
maxBytes: llmsMaxBytes
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (!isMarkdownArtifact(route) || route.error) return;
|
|
805
|
+
const problem = inspectMarkdownArtifact(route);
|
|
806
|
+
if (problem) {
|
|
807
|
+
route.error = /* @__PURE__ */ new Error(`Invalid static Markdown ${route.route}: ${problem}`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
approved.set(route.route, digestBytes(readRouteBytes(route)));
|
|
811
|
+
});
|
|
812
|
+
nitro.hooks.hook("prerender:done", async ({ prerenderedRoutes, failedRoutes }) => {
|
|
813
|
+
Reflect.deleteProperty(env, PUBLICATION_PHASE_ENV);
|
|
814
|
+
if (failedRoutes.length > 0 && nitro.options.prerender.failOnError) return;
|
|
815
|
+
if (hasMarkdown && !inventory) throw new Error("Static Markdown list was not prerendered");
|
|
816
|
+
const expected = inventory?.expected ?? /* @__PURE__ */ new Map();
|
|
817
|
+
assertRegistered({
|
|
818
|
+
prerenderedRoutes,
|
|
819
|
+
expected
|
|
820
|
+
});
|
|
821
|
+
const { dir: outputDir, publicDir } = nitro.options.output;
|
|
822
|
+
await assertStaticOutput({
|
|
823
|
+
publicDir,
|
|
824
|
+
expected,
|
|
825
|
+
approved
|
|
826
|
+
});
|
|
827
|
+
if (hasLlms && llmsText === void 0) throw new Error("llms.txt was not prerendered");
|
|
828
|
+
await publication.assertPublished({
|
|
829
|
+
publicDir,
|
|
830
|
+
prerenderedRoutes
|
|
831
|
+
});
|
|
832
|
+
await assertDisabledArtifacts({
|
|
833
|
+
publicDir,
|
|
834
|
+
hasLlms,
|
|
835
|
+
hasLlmsFull: shouldBuildLlmsFull
|
|
836
|
+
});
|
|
837
|
+
if (shouldBuildLlmsFull && inventory && llmsText !== void 0) hasLlmsFull = await publishLlmsFull({
|
|
838
|
+
inventory,
|
|
839
|
+
llmsText,
|
|
840
|
+
outputDir,
|
|
841
|
+
publicDir,
|
|
842
|
+
maxBytes: llmsFullMaxBytes
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
nitro.hooks.hook("compiled", async () => {
|
|
846
|
+
await writeVercelRoutes({
|
|
847
|
+
outputDir: nitro.options.output.dir,
|
|
848
|
+
paths: inventory?.manifest.paths ?? [],
|
|
849
|
+
hasLlmsFull,
|
|
850
|
+
hasLlms,
|
|
851
|
+
hasLlmsIndex: llmsText !== void 0
|
|
852
|
+
});
|
|
853
|
+
await publication.writeServer(nitro.options.output.serverDir);
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
// src/publication/publicationArtifacts/helpers/createPublicationBuild/index.ts
|
|
858
|
+
const BUILD_ID_DEFINE = "import.meta.env.PUBLICATION_BUILD_ID";
|
|
859
|
+
function createPublicationBuild() {
|
|
860
|
+
const buildId = randomUUID();
|
|
861
|
+
return {
|
|
862
|
+
buildId,
|
|
863
|
+
plugin: {
|
|
864
|
+
name: "project:publication-build",
|
|
865
|
+
config: () => ({ define: { [BUILD_ID_DEFINE]: JSON.stringify(buildId) } })
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
// src/publication/index.ts
|
|
870
|
+
function publicationBuild(features) {
|
|
871
|
+
const publication = createPublicationBuild();
|
|
872
|
+
return {
|
|
873
|
+
plugin: publication.plugin,
|
|
874
|
+
module: markdownPrerender({
|
|
875
|
+
buildId: publication.buildId,
|
|
876
|
+
hasMarkdown: typeof features.markdown === "object" && features.markdown.static === true,
|
|
877
|
+
hasSitemap: typeof features.sitemap === "object",
|
|
878
|
+
hasLlms: Boolean(features.llms),
|
|
879
|
+
hasLlmsFull: Boolean(features.llmsFull),
|
|
880
|
+
llmsMaxBytes: typeof features.llms === "object" ? features.llms.maxBytes : void 0,
|
|
881
|
+
llmsFullMaxBytes: typeof features.llmsFull === "object" ? features.llmsFull.maxBytes : void 0
|
|
882
|
+
})
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
export { publicationBuild };
|