@opentf/web-cli 1.1.0 → 1.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 +4 -1
- package/package.json +1 -1
- package/src/build.js +39 -13
- package/src/prerender.js +20 -4
- package/src/serve.js +5 -1
- package/src/shared.js +132 -43
package/README.md
CHANGED
|
@@ -39,7 +39,10 @@ otfw serve # build, then server-render each route per request (SSR)
|
|
|
39
39
|
Picks port 3000 (scanning upward), or `--port` for an explicit one. Phase 1: the page
|
|
40
40
|
becomes interactive via the client bundle (CSR mount); hydration is a later phase.
|
|
41
41
|
|
|
42
|
-
`OTFWC_BIN` overrides the compiler binary location
|
|
42
|
+
`OTFWC_BIN` overrides the compiler binary location. Published installs under
|
|
43
|
+
`node_modules` use the packaged compiler, even when the app lives inside a Cargo
|
|
44
|
+
workspace; this repository's own source checkout may build and use
|
|
45
|
+
`target/debug/otfwc` for compiler development.
|
|
43
46
|
|
|
44
47
|
## License
|
|
45
48
|
|
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
readHtmlShell,
|
|
30
30
|
runBlogFeed,
|
|
31
31
|
runDocsSearchIndex,
|
|
32
|
+
runLlmsFiles,
|
|
32
33
|
runLastUpdated,
|
|
33
34
|
stampHydrateSentinel,
|
|
34
35
|
} from "./shared.js";
|
|
@@ -36,17 +37,37 @@ import { fmtMs, step } from "./reporter.js";
|
|
|
36
37
|
|
|
37
38
|
const hash = (s) => Bun.hash(s).toString(16).padStart(16, "0").slice(0, 8);
|
|
38
39
|
|
|
39
|
-
// Site origin for absolute canonical / sitemap URLs. Priority: `--base-url=`
|
|
40
|
-
// then `otfw.config` (`{ site: { url } }`)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const flag = process.argv.find((a) => a.startsWith("--base-url="));
|
|
40
|
+
// Site origin for absolute canonical / sitemap/feed URLs. Priority: `--base-url=`
|
|
41
|
+
// flag, then `otfw.config` (`{ site: { url } }`).
|
|
42
|
+
export function resolveBaseUrl(config, argv = process.argv) {
|
|
43
|
+
const flag = argv.find((a) => a.startsWith("--base-url="));
|
|
44
44
|
if (flag) return flag.slice("--base-url=".length).replace(/\/+$/, "");
|
|
45
45
|
if (config?.site?.url) return String(config.site.url).replace(/\/+$/, "");
|
|
46
46
|
return "";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function buildRequiresBaseUrl(config, argv = process.argv) {
|
|
50
|
+
return argv.includes("--ssg") || !!config?.docs || !!config?.blog;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireBaseUrl(config, argv = process.argv) {
|
|
54
|
+
const baseUrl = resolveBaseUrl(config, argv);
|
|
55
|
+
if (baseUrl || !buildRequiresBaseUrl(config, argv)) return baseUrl;
|
|
56
|
+
console.error(
|
|
57
|
+
`✗ site.url is required for this production build.\n` +
|
|
58
|
+
` Add it to otfw.config.js:\n\n` +
|
|
59
|
+
` export default defineDocsConfig({\n` +
|
|
60
|
+
` site: { url: "https://example.com" }\n` +
|
|
61
|
+
` })\n\n` +
|
|
62
|
+
` Or pass --base-url=https://example.com`,
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
export async function runBuild(options = {}) {
|
|
68
|
+
const projectRoot = process.cwd();
|
|
69
|
+
const config = await loadConfig(projectRoot);
|
|
70
|
+
const baseUrl = requireBaseUrl(config);
|
|
50
71
|
const { root, appDir, webEntry, otfwc, exclude } = loadProject();
|
|
51
72
|
const t0 = performance.now();
|
|
52
73
|
|
|
@@ -63,7 +84,6 @@ export async function runBuild(options = {}) {
|
|
|
63
84
|
|
|
64
85
|
// Docs generator: resolve `@opentf/web-docs/nav` to the build-time nav tree when
|
|
65
86
|
// the project has a `docs` config block.
|
|
66
|
-
const config = await loadConfig(root);
|
|
67
87
|
const docsPlugins = await loadDocsPlugins(root, appDir, config, exclude);
|
|
68
88
|
|
|
69
89
|
const outDir = join(root, "dist");
|
|
@@ -145,7 +165,6 @@ export async function runBuild(options = {}) {
|
|
|
145
165
|
// (so per-route files carry the same bundle + stylesheet links).
|
|
146
166
|
let ssg = null;
|
|
147
167
|
if (process.argv.includes("--ssg")) {
|
|
148
|
-
const baseUrl = resolveBaseUrl(config);
|
|
149
168
|
const { runPrerender } = await import("./prerender.js");
|
|
150
169
|
// Per-page last-updated map (git/frontmatter) for the article:modified_time tag.
|
|
151
170
|
const lastUpdated = await runLastUpdated(root, appDir, config, exclude);
|
|
@@ -185,13 +204,20 @@ export async function runBuild(options = {}) {
|
|
|
185
204
|
searchStep.done(`Search index — ${search?.pages ?? 0} page(s)`);
|
|
186
205
|
}
|
|
187
206
|
|
|
188
|
-
// Blog RSS
|
|
189
|
-
// public/ copy so
|
|
207
|
+
// Blog RSS + Atom feeds (when a `blog` block is configured). Written
|
|
208
|
+
// after the public/ copy so project-supplied feed overrides aren't clobbered.
|
|
190
209
|
if (config?.blog) {
|
|
191
|
-
const feedStep = step("Generating
|
|
192
|
-
const feed = await runBlogFeed(root, appDir, config, outDir,
|
|
193
|
-
if (feed) feedStep.done(`
|
|
194
|
-
else feedStep.done("
|
|
210
|
+
const feedStep = step("Generating blog feeds");
|
|
211
|
+
const feed = await runBlogFeed(root, appDir, config, outDir, baseUrl, exclude);
|
|
212
|
+
if (feed) feedStep.done(`Blog feeds — ${feed.count} post(s) → ${feed.paths.join(", ")}`);
|
|
213
|
+
else feedStep.done("Blog feeds — skipped");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (config?.docs || config?.blog) {
|
|
217
|
+
const llmsStep = step("Generating LLM context");
|
|
218
|
+
const llms = await runLlmsFiles(root, appDir, pages, config, outDir, baseUrl);
|
|
219
|
+
if (llms) llmsStep.done(`LLM context — ${llms.paths.join(", ")}`);
|
|
220
|
+
else llmsStep.done("LLM context — skipped");
|
|
195
221
|
}
|
|
196
222
|
|
|
197
223
|
console.log(`\n → dist/ ready in ${fmtMs(performance.now() - t0)}\n`);
|
package/src/prerender.js
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildServerBundle,
|
|
11
|
+
injectHead,
|
|
12
|
+
injectHydrationData,
|
|
13
|
+
injectMarkup,
|
|
14
|
+
withHtmlLang,
|
|
15
|
+
} from "./shared.js";
|
|
10
16
|
|
|
11
17
|
// "/" → dist/index.html, "/post/1" → dist/post/1/index.html, "/fr/about" → dist/fr/about/index.html.
|
|
12
18
|
function htmlPathFor(outDir, route) {
|
|
@@ -97,7 +103,8 @@ export async function runPrerender({ root, pages, webEntry, otfwc, shellHtml, ou
|
|
|
97
103
|
try {
|
|
98
104
|
// Passing the localized URL pins router.locale (resolveLocale) and still
|
|
99
105
|
// matches the locale-agnostic route table, so `t()` renders in `locale`.
|
|
100
|
-
const { html, metadata
|
|
106
|
+
const { html, metadata, hydration } =
|
|
107
|
+
(await mod.renderRoute(urlPath, params)) ?? { html: "", metadata: {}, hydration: "" };
|
|
101
108
|
const meta = i18nOn
|
|
102
109
|
? { ...metadata, links: [...(metadata.links || []), ...alternatesFor(path, locales, defaultLocale)] }
|
|
103
110
|
: metadata;
|
|
@@ -108,7 +115,13 @@ export async function runPrerender({ root, pages, webEntry, otfwc, shellHtml, ou
|
|
|
108
115
|
if (iso) head += `\n<meta property="article:modified_time" content="${escapeXml(iso)}">`;
|
|
109
116
|
const file = htmlPathFor(outDir, urlPath);
|
|
110
117
|
mkdirSync(dirname(file), { recursive: true });
|
|
111
|
-
writeFileSync(
|
|
118
|
+
writeFileSync(
|
|
119
|
+
file,
|
|
120
|
+
injectHydrationData(
|
|
121
|
+
injectMarkup(injectHead(withHtmlLang(shellHtml, locale), head), html),
|
|
122
|
+
hydration,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
112
125
|
rendered.push(urlPath);
|
|
113
126
|
} catch (e) {
|
|
114
127
|
failed.push(urlPath);
|
|
@@ -123,7 +136,10 @@ export async function runPrerender({ root, pages, webEntry, otfwc, shellHtml, ou
|
|
|
123
136
|
const result = await mod.renderRoute("/__otfw_404__");
|
|
124
137
|
if (result) {
|
|
125
138
|
const head = mod.renderHead({ robots: "noindex", ...result.metadata }, { baseUrl });
|
|
126
|
-
writeFileSync(
|
|
139
|
+
writeFileSync(
|
|
140
|
+
join(outDir, "404.html"),
|
|
141
|
+
injectHydrationData(injectMarkup(injectHead(shellHtml, head), result.html), result.hydration),
|
|
142
|
+
);
|
|
127
143
|
}
|
|
128
144
|
} catch {
|
|
129
145
|
/* no 404 page */
|
package/src/serve.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
buildServerBundle,
|
|
34
34
|
discoverPages,
|
|
35
35
|
injectHead,
|
|
36
|
+
injectHydrationData,
|
|
36
37
|
injectMarkup,
|
|
37
38
|
loadConfig,
|
|
38
39
|
loadDocsPlugins,
|
|
@@ -189,7 +190,10 @@ export async function runServe() {
|
|
|
189
190
|
: result.metadata;
|
|
190
191
|
const head = mod.renderHead(meta, { path: url.pathname, baseUrl });
|
|
191
192
|
const localizedShell = i18nOn ? withHtmlLang(shell, localeOf(url.pathname)) : shell;
|
|
192
|
-
const html =
|
|
193
|
+
const html = injectHydrationData(
|
|
194
|
+
injectMarkup(injectHead(localizedShell, head), result.html),
|
|
195
|
+
result.hydration,
|
|
196
|
+
);
|
|
193
197
|
// 200 for a real route; 404 when the path fell back to the registered 404 page.
|
|
194
198
|
return new Response(html, {
|
|
195
199
|
status: result.status ?? 200,
|
package/src/shared.js
CHANGED
|
@@ -59,6 +59,20 @@ export function injectMarkup(shellHtml, markup) {
|
|
|
59
59
|
return shellHtml.replace(/(<div id="app"[^>]*>)\s*(<\/div>)/, (_m, open, close) => `${open}${markup}${close}`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Embed the island hydration payload (`renderRoute().hydration`) as a
|
|
64
|
+
* `<script type="application/json" id="__otfw_h">` the client reads at upgrade so
|
|
65
|
+
* components resume from rich JS props (compiler-driven data hydration). Placed before
|
|
66
|
+
* `</body>`; a deferred module bundle runs after parse, so the payload is always in the
|
|
67
|
+
* DOM first. `json` is already `<`-escaped by the server collector, so it can't break out
|
|
68
|
+
* of the script. No-op for an empty payload. Uses a function replacer so `$` in the JSON
|
|
69
|
+
* stays literal.
|
|
70
|
+
*/
|
|
71
|
+
export function injectHydrationData(shellHtml, json) {
|
|
72
|
+
if (!json) return shellHtml;
|
|
73
|
+
return injectBeforeBody(shellHtml, `<script type="application/json" id="__otfw_h">${json}</script>`);
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* Stamp the `data-otfw-hydrate` sentinel onto the shell's `#app` container, telling
|
|
64
78
|
* the client to *adopt* the server-rendered DOM on first paint instead of rebuilding
|
|
@@ -105,10 +119,45 @@ export function findUp(name, from) {
|
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
|
|
122
|
+
function hasLocalCompilerWorkspace(workspace) {
|
|
123
|
+
return !!workspace && existsSync(join(workspace, "crates", "otfw_cli", "Cargo.toml"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function resolveCompiler({
|
|
127
|
+
cliDir = dirname(fileURLToPath(import.meta.url)),
|
|
128
|
+
env = process.env,
|
|
129
|
+
resolvePackagedCompiler = otfwcPath,
|
|
130
|
+
findWorkspace = findUp,
|
|
131
|
+
ensure = ensureCompiler,
|
|
132
|
+
} = {}) {
|
|
133
|
+
if (env.OTFWC_BIN) return { otfwc: env.OTFWC_BIN, workspace: null };
|
|
134
|
+
|
|
135
|
+
let packagedError = null;
|
|
136
|
+
try {
|
|
137
|
+
return { otfwc: resolvePackagedCompiler(), workspace: null };
|
|
138
|
+
} catch (e) {
|
|
139
|
+
packagedError = e;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const installedPackage = cliDir.split(/[\\/]/).includes("node_modules");
|
|
143
|
+
const workspace = installedPackage ? null : findWorkspace("Cargo.toml", cliDir);
|
|
144
|
+
if (hasLocalCompilerWorkspace(workspace)) {
|
|
145
|
+
const otfwc = join(workspace, "target", "debug", "otfwc");
|
|
146
|
+
ensure(otfwc, workspace);
|
|
147
|
+
return { otfwc, workspace };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fail(
|
|
151
|
+
`${packagedError?.message ?? "cannot resolve @opentf/web-compiler prebuilt binary"}\n` +
|
|
152
|
+
` No local otfwc compiler workspace was found to build from source.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
108
156
|
/**
|
|
109
157
|
* Resolve the project and toolchain: the app being built (cwd), the runtime
|
|
110
158
|
* package, the `otfwc` compiler, and the excluded routes. Exits with a clear
|
|
111
|
-
* message on any hard failure.
|
|
159
|
+
* message on any hard failure. Source checkouts can build the compiler on demand
|
|
160
|
+
* from this repo's Cargo workspace; published installs use @opentf/web-compiler.
|
|
112
161
|
*/
|
|
113
162
|
export function loadProject() {
|
|
114
163
|
const root = process.cwd();
|
|
@@ -129,25 +178,9 @@ export function loadProject() {
|
|
|
129
178
|
fail(`cannot resolve "@opentf/web" from ${root}\n add it to your dependencies.`);
|
|
130
179
|
}
|
|
131
180
|
|
|
132
|
-
// Locate the `otfwc` compiler.
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
// overrides both.
|
|
136
|
-
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
137
|
-
const workspace = findUp("Cargo.toml", cliDir);
|
|
138
|
-
let otfwc;
|
|
139
|
-
if (process.env.OTFWC_BIN) {
|
|
140
|
-
otfwc = process.env.OTFWC_BIN;
|
|
141
|
-
} else if (workspace) {
|
|
142
|
-
otfwc = join(workspace, "target", "debug", "otfwc");
|
|
143
|
-
ensureCompiler(otfwc, workspace);
|
|
144
|
-
} else {
|
|
145
|
-
try {
|
|
146
|
-
otfwc = otfwcPath();
|
|
147
|
-
} catch (e) {
|
|
148
|
-
fail(e.message);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
181
|
+
// Locate the `otfwc` compiler. Explicit overrides win, then the packaged
|
|
182
|
+
// compiler binary, then this repo's local Rust workspace as a source fallback.
|
|
183
|
+
const { otfwc, workspace } = resolveCompiler();
|
|
151
184
|
|
|
152
185
|
// Route directories to skip during discovery — comma-separated names in
|
|
153
186
|
// EXCLUDE_ROUTES. None are excluded by default.
|
|
@@ -162,6 +195,19 @@ function ensureCompiler(otfwc, workspace) {
|
|
|
162
195
|
if (existsSync(otfwc)) return;
|
|
163
196
|
if (!workspace) fail(`otfwc compiler not found at ${otfwc}`);
|
|
164
197
|
console.log("building compiler (cargo build -p otfw_cli)…");
|
|
198
|
+
const cargo = Bun.spawnSync(["cargo", "--version"], {
|
|
199
|
+
cwd: workspace,
|
|
200
|
+
stdout: "ignore",
|
|
201
|
+
stderr: "pipe",
|
|
202
|
+
});
|
|
203
|
+
if (cargo.error || cargo.exitCode !== 0) {
|
|
204
|
+
fail(
|
|
205
|
+
`cannot build otfwc because cargo is not available.\n` +
|
|
206
|
+
` This source checkout needs Rust/Cargo to build ${otfwc}.\n` +
|
|
207
|
+
` Install Rust in the build environment, set OTFWC_BIN to an existing otfwc binary,\n` +
|
|
208
|
+
` or build with the published @opentf/web-cli package so @opentf/web-compiler can use its prebuilt binary.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
165
211
|
const b = Bun.spawnSync(["cargo", "build", "-p", "otfw_cli"], {
|
|
166
212
|
cwd: workspace,
|
|
167
213
|
stdout: "inherit",
|
|
@@ -271,43 +317,86 @@ export async function runDocsSearchIndex(root, config, siteDir, onProgress) {
|
|
|
271
317
|
}
|
|
272
318
|
|
|
273
319
|
/**
|
|
274
|
-
* Generate
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* / package missing). Resolved from the app's
|
|
320
|
+
* Generate blog feeds when the project has a `blog` config and a base URL. Scans the
|
|
321
|
+
* post folders, renders RSS 2.0 + Atom 1.0, and writes them under
|
|
322
|
+
* `<siteDir>/<blogDir>/`. Project-supplied `public/<blogDir>/{rss,atom}.xml` files
|
|
323
|
+
* take precedence independently. Returns `{ paths, urls, count }` or null (no blog /
|
|
324
|
+
* no base URL / all feeds overridden / package missing). Resolved from the app's
|
|
325
|
+
* `@opentf/web-docs`.
|
|
279
326
|
*/
|
|
280
327
|
export async function runBlogFeed(root, appDir, config, siteDir, baseUrl, exclude = new Set()) {
|
|
281
328
|
const blog = config?.blog;
|
|
282
329
|
if (!blog) return null;
|
|
283
330
|
if (!baseUrl) {
|
|
284
|
-
console.warn("⚠ blog
|
|
331
|
+
console.warn("⚠ blog feeds skipped: no site URL (pass --base-url or set otfw.config)");
|
|
285
332
|
return null;
|
|
286
333
|
}
|
|
287
334
|
const contentDir = blog.dir ?? "blog";
|
|
288
|
-
if (existsSync(join(root, "public", contentDir, "rss.xml"))) return null; // honor override
|
|
289
335
|
try {
|
|
290
336
|
const entry = Bun.resolveSync("@opentf/web-docs/build", root);
|
|
291
|
-
const { loadPosts, renderBlogFeed } = await import(pathToFileURL(entry).href);
|
|
337
|
+
const { loadPosts, renderAtomFeed, renderBlogFeed } = await import(pathToFileURL(entry).href);
|
|
292
338
|
const posts = loadPosts({ appDir, contentDir, exclude });
|
|
293
|
-
const feedPath = `/${contentDir}/rss.xml`;
|
|
294
339
|
const title = blog.title || (config?.docs?.title ? `${config.docs.title} Blog` : "Blog");
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
340
|
+
const channel = {
|
|
341
|
+
title,
|
|
342
|
+
description: blog.description || title,
|
|
343
|
+
link: `/${contentDir}`,
|
|
344
|
+
};
|
|
345
|
+
const feeds = [
|
|
346
|
+
{
|
|
347
|
+
file: "rss.xml",
|
|
348
|
+
path: `/${contentDir}/rss.xml`,
|
|
349
|
+
render: renderBlogFeed,
|
|
303
350
|
},
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
351
|
+
{
|
|
352
|
+
file: "atom.xml",
|
|
353
|
+
path: `/${contentDir}/atom.xml`,
|
|
354
|
+
render: renderAtomFeed,
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
const written = [];
|
|
358
|
+
for (const feed of feeds) {
|
|
359
|
+
if (existsSync(join(root, "public", contentDir, feed.file))) continue; // honor override
|
|
360
|
+
const out = join(siteDir, contentDir, feed.file);
|
|
361
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
362
|
+
writeFileSync(out, feed.render({ posts, baseUrl, feedPath: feed.path, channel }));
|
|
363
|
+
written.push(feed.path);
|
|
364
|
+
}
|
|
365
|
+
if (!written.length) return null;
|
|
366
|
+
const origin = baseUrl.replace(/\/+$/, "");
|
|
367
|
+
return { paths: written, urls: written.map((path) => origin + path), count: posts.length };
|
|
368
|
+
} catch (e) {
|
|
369
|
+
console.warn(`⚠ blog feeds skipped: ${e?.message ?? e}`);
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Generate `/llms.txt` and `/llms-full.txt` for docs/blog sites from the same
|
|
376
|
+
* filesystem route list used by the app build. Project-supplied public files override
|
|
377
|
+
* each output independently.
|
|
378
|
+
*/
|
|
379
|
+
export async function runLlmsFiles(root, appDir, pages, config, siteDir, baseUrl) {
|
|
380
|
+
if (!config?.docs && !config?.blog) return null;
|
|
381
|
+
const outputs = [
|
|
382
|
+
{ file: "llms.txt", render: "renderLlmsTxt" },
|
|
383
|
+
{ file: "llms-full.txt", render: "renderLlmsFullTxt" },
|
|
384
|
+
].filter((out) => !existsSync(join(root, "public", out.file)));
|
|
385
|
+
if (!outputs.length) return null;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const entry = Bun.resolveSync("@opentf/web-docs/build", root);
|
|
389
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
390
|
+
const written = [];
|
|
391
|
+
for (const out of outputs) {
|
|
392
|
+
const render = mod[out.render];
|
|
393
|
+
if (typeof render !== "function") continue;
|
|
394
|
+
writeFileSync(join(siteDir, out.file), render({ appDir, pages, baseUrl, config }));
|
|
395
|
+
written.push(`/${out.file}`);
|
|
396
|
+
}
|
|
397
|
+
return written.length ? { paths: written } : null;
|
|
309
398
|
} catch (e) {
|
|
310
|
-
console.warn(`⚠
|
|
399
|
+
console.warn(`⚠ llms.txt skipped: ${e?.message ?? e}`);
|
|
311
400
|
return null;
|
|
312
401
|
}
|
|
313
402
|
}
|