pagegraph 0.10.1 → 0.12.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.
Files changed (65) hide show
  1. package/README.md +163 -124
  2. package/dist/audit.d.ts +1 -1
  3. package/dist/audit.js +1 -1
  4. package/dist/build-only/audit.d.ts +1 -0
  5. package/dist/build-only/audit.js +6 -0
  6. package/dist/build-only/audit.js.map +1 -0
  7. package/dist/build-only/config.d.ts +1 -0
  8. package/dist/build-only/config.js +6 -0
  9. package/dist/build-only/config.js.map +1 -0
  10. package/dist/build-only/tanstack-start.d.ts +1 -0
  11. package/dist/build-only/tanstack-start.js +6 -0
  12. package/dist/build-only/tanstack-start.js.map +1 -0
  13. package/dist/build-only/vite.d.ts +1 -0
  14. package/dist/build-only/vite.js +6 -0
  15. package/dist/build-only/vite.js.map +1 -0
  16. package/dist/checks-uZk5Em9A.js +352 -0
  17. package/dist/checks-uZk5Em9A.js.map +1 -0
  18. package/dist/cli.js +3814 -3017
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts +10 -45
  21. package/dist/config.js +10 -98
  22. package/dist/config.js.map +1 -1
  23. package/dist/{content-DVL2iyGU.d.ts → content-B1cPtL8b.d.ts} +4 -128
  24. package/dist/{content-BE9KCTgb.js → content-kcEnQdfi.js} +4 -489
  25. package/dist/content-kcEnQdfi.js.map +1 -0
  26. package/dist/declare-D5o7hwmv.js +9 -0
  27. package/dist/declare-D5o7hwmv.js.map +1 -0
  28. package/dist/declare-DVgdEdOL.d.ts +12127 -0
  29. package/dist/graph-Bcsourlv.js +162 -0
  30. package/dist/graph-Bcsourlv.js.map +1 -0
  31. package/dist/graph-BxCY90vq.d.ts +104 -0
  32. package/dist/graph-CBIY71w6.d.ts +57 -0
  33. package/dist/index.d.ts +37 -73
  34. package/dist/index.js +23 -133
  35. package/dist/index.js.map +1 -1
  36. package/dist/message-DT_CjUdt.js +12 -0
  37. package/dist/message-DT_CjUdt.js.map +1 -0
  38. package/dist/page-heads-6y-CARRQ.js +25 -0
  39. package/dist/page-heads-6y-CARRQ.js.map +1 -0
  40. package/dist/page-heads-DNkjMUEN.d.ts +21 -0
  41. package/dist/projections-D0oOFKiY.js +142 -0
  42. package/dist/projections-D0oOFKiY.js.map +1 -0
  43. package/dist/projections-DeEFe7XT.d.ts +78 -0
  44. package/dist/react.d.ts +26 -12051
  45. package/dist/react.js +12 -3
  46. package/dist/react.js.map +1 -1
  47. package/dist/route-config-35ZEzkNC.js +308 -0
  48. package/dist/route-config-35ZEzkNC.js.map +1 -0
  49. package/dist/route-config-wneg6ktn.d.ts +29 -0
  50. package/dist/tanstack-start/index.d.ts +40 -0
  51. package/dist/tanstack-start/index.js +368 -0
  52. package/dist/tanstack-start/index.js.map +1 -0
  53. package/dist/tanstack-start/server.d.ts +14 -0
  54. package/dist/tanstack-start/server.js +53 -0
  55. package/dist/tanstack-start/server.js.map +1 -0
  56. package/dist/vite-graph-loader-01p1WxEq.js +104 -0
  57. package/dist/vite-graph-loader-01p1WxEq.js.map +1 -0
  58. package/dist/vite-graph-loader-Btgjz5RH.d.ts +56 -0
  59. package/dist/vite.d.ts +2 -28
  60. package/dist/vite.js +1 -307
  61. package/dist/wire-B4RQMc4e.js +26 -0
  62. package/dist/wire-B4RQMc4e.js.map +1 -0
  63. package/package.json +35 -13
  64. package/dist/content-BE9KCTgb.js.map +0 -1
  65. package/dist/vite.js.map +0 -1
@@ -0,0 +1,368 @@
1
+ import { t as buildSeoGraph } from "../graph-Bcsourlv.js";
2
+ import { n as graphToJson } from "../wire-B4RQMc4e.js";
3
+ import { a as globToRegExp } from "../checks-uZk5Em9A.js";
4
+ import { i as seoRouteConfig, n as inMemoryWriteFs, r as resolveOptions, t as deriveRouteConfig } from "../route-config-35ZEzkNC.js";
5
+ import { n as withCleanStdout } from "../vite-graph-loader-01p1WxEq.js";
6
+ import { join, relative, resolve } from "node:path";
7
+ import { Generator, getConfig } from "@tanstack/router-generator";
8
+ import { createRunnableDevEnvironment, resolveConfig } from "vite";
9
+ import { pathToFileURL } from "node:url";
10
+ //#region src/tanstack-start/routes.ts
11
+ /**
12
+ * Route discovery for the graph: the router's own generator parses the routes
13
+ * directory (the same parse `routeTree.gen.ts` comes from), into memory only.
14
+ */
15
+ async function scanRoutes(root, routesDirectory) {
16
+ const cacheDirectory = resolve(root, "node_modules/.cache/pagegraph");
17
+ let scanned;
18
+ const config = getConfig({
19
+ routesDirectory: resolve(root, routesDirectory),
20
+ generatedRouteTree: resolve(cacheDirectory, "route-tree.gen.ts"),
21
+ tmpDir: resolve(cacheDirectory, "tmp"),
22
+ disableTypes: true,
23
+ enableRouteTreeFormatting: false,
24
+ disableLogging: true,
25
+ plugins: [{
26
+ name: "pagegraph:scan",
27
+ onRouteTreeChanged({ routeTree, routeNodes, rootRouteNode }) {
28
+ scanned = {
29
+ root: rootRouteNode,
30
+ tree: routeTree,
31
+ nodes: routeNodes
32
+ };
33
+ }
34
+ }]
35
+ }, root);
36
+ const memory = inMemoryWriteFs(config.generatedRouteTree);
37
+ await new Generator({
38
+ config,
39
+ root,
40
+ fs: memory.fs
41
+ }).run();
42
+ if (scanned === void 0) throw new Error(`pagegraph found no route tree in ${resolve(root, routesDirectory)}`);
43
+ return scanned;
44
+ }
45
+ //#endregion
46
+ //#region src/tanstack-start/graph.ts
47
+ /**
48
+ * Build the graph from the app's own modules, evaluated in the `pagegraph`
49
+ * Vite environment: one module per route file that declares `staticData`, plus
50
+ * the app's collections module. Never the route tree, which reaches every
51
+ * route — including app routes that only resolve inside the server runtime.
52
+ */
53
+ const GRAPH_ROUTE_TYPES = /* @__PURE__ */ new Set([
54
+ "__root",
55
+ "static",
56
+ "layout",
57
+ "pathless_layout"
58
+ ]);
59
+ const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
60
+ const routeStaticData = (module, file) => {
61
+ const route = module["Route"];
62
+ if (route?.options === void 0) throw new Error(`${file} has no \`Route\` export`);
63
+ return route.options.staticData;
64
+ };
65
+ const loadCollections = async (runner, root, fullPath) => {
66
+ const path = relative(root, fullPath);
67
+ const exported = (await runner.import(fullPath).catch((cause) => {
68
+ throw new Error(`pagegraph could not evaluate ${path}: ${messageOf(cause)}`, { cause });
69
+ }))["collections"];
70
+ if (exported === void 0) throw new Error(`${path} has no \`collections\` export`);
71
+ const collections = typeof exported === "function" ? await exported() : exported;
72
+ if (!Array.isArray(collections)) throw new Error(`${path} \`collections\` must be an array or return one`);
73
+ return collections;
74
+ };
75
+ /** Scan the routes directory and pick the modules an evaluation loads. */
76
+ async function planGraph(root, options) {
77
+ const scan = await scanRoutes(root, options.routesDirectory ?? "src/routes");
78
+ const excluded = (options.exclude ?? []).map(globToRegExp);
79
+ const participates = (node) => GRAPH_ROUTE_TYPES.has(node._fsRouteType) && (node._fsRouteType === "__root" || node.createFileRouteProps?.has("staticData") === true) && !excluded.some((pattern) => pattern.test(node.filePath));
80
+ return {
81
+ root,
82
+ options,
83
+ scan,
84
+ routeFiles: [scan.root, ...scan.nodes].filter(participates).map((node) => node.fullPath),
85
+ collectionsFile: options.collections === void 0 ? void 0 : resolve(root, options.collections)
86
+ };
87
+ }
88
+ /**
89
+ * A route node's path relative to its parent, as the router matches it: route
90
+ * groups, pathless layouts, and trailing underscores contribute no segment.
91
+ */
92
+ const publicSegment = (node) => node.isNonPath === true || node.cleanedPath === void 0 || node.cleanedPath === "" ? void 0 : node.cleanedPath;
93
+ /** Evaluate a plan's modules through `runner`, then build the graph and the site runtime. */
94
+ async function evaluateGraph(runner, plan) {
95
+ const { root, options, scan } = plan;
96
+ const staticData = new Map(await Promise.all(plan.routeFiles.map(async (fullPath) => {
97
+ const file = relative(root, fullPath);
98
+ const routeFile = relative(resolve(root, options.routesDirectory ?? "src/routes"), fullPath);
99
+ const module = await runner.import(fullPath).catch((cause) => {
100
+ throw new Error(`pagegraph could not evaluate ${file} in the graph environment: ${messageOf(cause)}\nIf it is an app surface rather than a public page, add "${routeFile}" to pagegraph({ exclude }).`, { cause });
101
+ });
102
+ return [fullPath, routeStaticData(module, file)];
103
+ })));
104
+ const walkable = (node) => ({
105
+ options: {
106
+ path: publicSegment(node),
107
+ staticData: staticData.get(node.fullPath)
108
+ },
109
+ children: (node.children ?? []).map(walkable)
110
+ });
111
+ const routeTree = {
112
+ options: { staticData: staticData.get(scan.root.fullPath) },
113
+ children: scan.tree.map(walkable)
114
+ };
115
+ const collections = plan.collectionsFile === void 0 ? [] : await loadCollections(runner, root, plan.collectionsFile);
116
+ const graph = buildSeoGraph({
117
+ routeTree,
118
+ collections
119
+ });
120
+ const exclusions = options.routeConfig === void 0 ? [] : deriveRouteConfig(scan.nodes, resolveOptions({
121
+ ...options.routeConfig,
122
+ routesDirectory: options.routesDirectory
123
+ })).robotsExclusions;
124
+ return {
125
+ graph,
126
+ site: {
127
+ origin: options.origin,
128
+ indexable: options.indexable ?? true,
129
+ robots: {
130
+ disallow: [.../* @__PURE__ */ new Set([...exclusions, ...options.robots?.disallow ?? []])].sort(),
131
+ contentSignal: options.robots?.contentSignal,
132
+ directives: options.robots?.directives
133
+ }
134
+ }
135
+ };
136
+ }
137
+ //#endregion
138
+ //#region src/tanstack-start/plugin.ts
139
+ /**
140
+ * The `pagegraph()` Vite plugin for TanStack Start.
141
+ *
142
+ * The graph is built once per build (and on change in dev) inside the app's own
143
+ * Vite pipeline, then shipped to the server runtime as `virtual:pagegraph/runtime`.
144
+ * The Worker never rebuilds it and never imports build code.
145
+ *
146
+ * Route modules and content collections are evaluated in a dedicated Node
147
+ * environment, `pagegraph`, that shares the app's plugins, aliases, and
148
+ * `define`s. Two settings make that safe:
149
+ * - its `resolve.noExternal` is cleared, so TanStack Start's packages load from
150
+ * node_modules, where their virtual router entry is an inert stub. Bundled (as
151
+ * the app's server bundles them), they would pull in the whole route tree.
152
+ * - only route files that declare `staticData` are evaluated, one by one, never
153
+ * `routeTree.gen.ts`.
154
+ *
155
+ * Dev imports those modules through the environment's module runner and
156
+ * re-evaluates after a change reaches it. A build builds the environment first
157
+ * (a `buildApp` hook that runs before the framework's) with the build's own
158
+ * config and plugins, then imports the output. The CLI and `loadPageHeads` use
159
+ * {@link evaluateAppGraph}, which runs the dev path without a server.
160
+ */
161
+ const GRAPH_ENVIRONMENT$1 = "pagegraph";
162
+ const PLUGIN_NAME = "pagegraph";
163
+ const RESOLVED_RUNTIME_ID = `\0virtual:pagegraph/runtime`;
164
+ const ENTRY_ID = "virtual:pagegraph/entry";
165
+ const RESOLVED_ENTRY_ID = `\0${ENTRY_ID}`;
166
+ /** The runtime entry imports the virtual module, so dep pre-bundling must leave it to this plugin. */
167
+ const RUNTIME_ENTRY = "pagegraph/tanstack-start/server";
168
+ /** Where a build writes the graph environment's output, relative to the Vite root. */
169
+ const BUILD_DIRECTORY = "node_modules/.cache/pagegraph/build";
170
+ const runtimeModulesPlugin = {
171
+ name: "pagegraph:runtime-modules",
172
+ enforce: "post",
173
+ applyToEnvironment: (environment) => environment.name === GRAPH_ENVIRONMENT$1,
174
+ resolveId: {
175
+ filter: { id: /^(?!node:|data:|file:|virtual:)[a-z][a-z0-9+.-]*:/ },
176
+ handler: (id) => ({
177
+ id,
178
+ external: true
179
+ })
180
+ }
181
+ };
182
+ const runtimeModule = ({ graph, site }) => `export const graph = ${JSON.stringify(graphToJson(graph))};\nexport const site = ${JSON.stringify(site)};\n`;
183
+ /** The graph environment's build entry: a loader per module the plan evaluates. */
184
+ const entryModule = (plan) => {
185
+ return `export const modules = {\n${[...plan.routeFiles, ...plan.collectionsFile === void 0 ? [] : [plan.collectionsFile]].map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(file)}),`).join("\n")}\n};\n`;
186
+ };
187
+ /** Build the graph environment with the build's config, import its output, and evaluate. */
188
+ const buildGraph = async (builder, environment, plan) => {
189
+ await builder.build(environment);
190
+ const entry = join(resolve(environment.config.root, environment.config.build.outDir), "entry.mjs");
191
+ const { modules } = await import(pathToFileURL(entry).href);
192
+ return evaluateGraph({ import: (file) => {
193
+ const load = modules[file];
194
+ if (load === void 0) throw new Error(`${file} is not in the graph build`);
195
+ return load();
196
+ } }, plan);
197
+ };
198
+ function pagegraph(options) {
199
+ const api = { options };
200
+ let root = "";
201
+ let server;
202
+ let devEnvironment;
203
+ /** The plan the graph environment's build entry is generated from. */
204
+ let buildPlan;
205
+ let current;
206
+ /** Bumped by every change that reaches the graph; an evaluation that spans one is discarded. */
207
+ let generation = 0;
208
+ const evaluateInDev = async (environment) => {
209
+ for (;;) {
210
+ const started = generation;
211
+ environment.runner.clearCache();
212
+ const result = await evaluateGraph(environment.runner, await planGraph(root, options));
213
+ if (started === generation) return result;
214
+ }
215
+ };
216
+ return [
217
+ {
218
+ name: PLUGIN_NAME,
219
+ api,
220
+ sharedDuringBuild: true,
221
+ config() {
222
+ return { environments: { [GRAPH_ENVIRONMENT$1]: {
223
+ consumer: "server",
224
+ dev: { createEnvironment: (name, environmentConfig) => createRunnableDevEnvironment(name, environmentConfig) },
225
+ build: {
226
+ outDir: BUILD_DIRECTORY,
227
+ emptyOutDir: true,
228
+ copyPublicDir: false,
229
+ minify: false,
230
+ rolldownOptions: {
231
+ input: { entry: ENTRY_ID },
232
+ output: {
233
+ entryFileNames: "[name].mjs",
234
+ chunkFileNames: "chunks/[name]-[hash].mjs"
235
+ }
236
+ }
237
+ }
238
+ } } };
239
+ },
240
+ configEnvironment(name, environmentConfig) {
241
+ if (name === "pagegraph") {
242
+ environmentConfig.resolve ??= {};
243
+ environmentConfig.resolve.noExternal = [];
244
+ return;
245
+ }
246
+ environmentConfig.optimizeDeps ??= {};
247
+ environmentConfig.optimizeDeps.exclude = [...environmentConfig.optimizeDeps.exclude ?? [], RUNTIME_ENTRY];
248
+ },
249
+ configResolved(resolved) {
250
+ root = resolved.root;
251
+ },
252
+ configureServer(devServer) {
253
+ server = devServer;
254
+ devEnvironment = devServer.environments[GRAPH_ENVIRONMENT$1];
255
+ },
256
+ hotUpdate({ file, modules }) {
257
+ if (this.environment.name !== "pagegraph") return;
258
+ const routesDirectory = resolve(root, options.routesDirectory ?? "src/routes");
259
+ if (modules.length === 0 && !file.startsWith(`${routesDirectory}/`)) return;
260
+ generation += 1;
261
+ current = void 0;
262
+ for (const other of Object.values(server?.environments ?? {})) {
263
+ if (other.name === "pagegraph") continue;
264
+ const runtime = other.moduleGraph.getModuleById(RESOLVED_RUNTIME_ID);
265
+ if (runtime !== void 0) other.reloadModule(runtime);
266
+ }
267
+ },
268
+ buildApp: {
269
+ order: "pre",
270
+ async handler(builder) {
271
+ const environment = builder.environments[GRAPH_ENVIRONMENT$1];
272
+ if (environment === void 0) return;
273
+ buildPlan = await planGraph(environment.config.root, options);
274
+ const built = buildGraph(builder, environment, buildPlan);
275
+ current = built;
276
+ await built;
277
+ environment.isBuilt = false;
278
+ }
279
+ },
280
+ resolveId: {
281
+ filter: { id: /^virtual:pagegraph\/(runtime|entry)$/ },
282
+ handler: (id) => `\0${id}`
283
+ },
284
+ load: {
285
+ filter: { id: /^\0virtual:pagegraph\/(runtime|entry)$/ },
286
+ async handler(id) {
287
+ if (id === RESOLVED_ENTRY_ID) {
288
+ if (buildPlan === void 0) throw new Error(`[${PLUGIN_NAME}] ${ENTRY_ID} is only built by the pagegraph buildApp hook`);
289
+ return entryModule(buildPlan);
290
+ }
291
+ if (this.environment.name === "pagegraph") return "export const graph = null;\nexport const site = null;\n";
292
+ if (current === void 0) {
293
+ if (devEnvironment === void 0) throw new Error(`[${PLUGIN_NAME}] the graph was not built before ${this.environment.name} imported it; build with Vite's app builder (\`vite build\`).`);
294
+ current = evaluateInDev(devEnvironment);
295
+ }
296
+ return runtimeModule(await current);
297
+ }
298
+ }
299
+ },
300
+ runtimeModulesPlugin,
301
+ ...options.routeConfig === void 0 ? [] : [seoRouteConfig({
302
+ ...options.routeConfig,
303
+ routesDirectory: options.routesDirectory
304
+ })]
305
+ ];
306
+ }
307
+ //#endregion
308
+ //#region src/tanstack-start/load.ts
309
+ /**
310
+ * Evaluate an app's graph outside a running dev server — for a build, the
311
+ * CLI (`seo.config.ts`), and build-time head loading — through the same
312
+ * `pagegraph` environment the dev server uses.
313
+ */
314
+ const GRAPH_ENVIRONMENT = "pagegraph";
315
+ const pluginApi = (plugins, configFile) => {
316
+ const plugin = plugins.find((candidate) => candidate.name === "pagegraph");
317
+ if (plugin?.api === void 0) throw new Error(`${configFile ?? "The Vite config"} does not register pagegraph() from "pagegraph/tanstack-start".`);
318
+ return plugin.api;
319
+ };
320
+ /**
321
+ * Resolve the app's Vite config in serve mode, evaluate the graph in its
322
+ * `pagegraph` environment, and release it — the dev server's evaluation path
323
+ * without a server. A config that branches on `command` sees `serve`; pass
324
+ * `mode` for the deployment the graph should describe.
325
+ */
326
+ async function evaluateAppGraph(options) {
327
+ const config = await resolveConfig({
328
+ root: options.root,
329
+ configFile: options.configFile,
330
+ mode: options.mode,
331
+ logLevel: "error"
332
+ }, "serve");
333
+ const api = pluginApi(config.plugins, config.configFile);
334
+ const environment = createRunnableDevEnvironment(GRAPH_ENVIRONMENT, config, { hot: false });
335
+ await environment.init();
336
+ try {
337
+ await environment.pluginContainer.buildStart({});
338
+ return await evaluateGraph(environment.runner, await planGraph(config.root, api.options));
339
+ } finally {
340
+ await environment.close();
341
+ }
342
+ }
343
+ /**
344
+ * A `seo.config.ts` graph loader for a TanStack Start app: the same graph the
345
+ * `pagegraph()` plugin ships to the runtime, with its robots policy.
346
+ *
347
+ * ```ts
348
+ * export default defineSeoConfig({
349
+ * origin: "https://example.com",
350
+ * loadGraph: tanstackStartGraph({ root: import.meta.dirname }),
351
+ * });
352
+ * ```
353
+ */
354
+ const tanstackStartGraph = (options) => async () => {
355
+ const { graph, site } = await withCleanStdout(() => evaluateAppGraph({
356
+ ...options,
357
+ mode: options.mode ?? "production"
358
+ }));
359
+ return {
360
+ graph,
361
+ robots: site.robots,
362
+ dispose: async () => {}
363
+ };
364
+ };
365
+ //#endregion
366
+ export { evaluateAppGraph, pagegraph, tanstackStartGraph };
367
+
368
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["GRAPH_ENVIRONMENT"],"sources":["../../src/tanstack-start/routes.ts","../../src/tanstack-start/graph.ts","../../src/tanstack-start/plugin.ts","../../src/tanstack-start/load.ts"],"sourcesContent":["/**\n * Route discovery for the graph: the router's own generator parses the routes\n * directory (the same parse `routeTree.gen.ts` comes from), into memory only.\n */\n\nimport { resolve } from \"node:path\";\n\nimport { Generator, getConfig, type RouteNode } from \"@tanstack/router-generator\";\n\nimport { inMemoryWriteFs } from \"../vite/route-config\";\n\nexport interface ScannedRoutes {\n /** `__root.tsx`. */\n readonly root: RouteNode;\n /** Top-level route nodes, each with its `children`. */\n readonly tree: ReadonlyArray<RouteNode>;\n /** Every route node, flat. */\n readonly nodes: ReadonlyArray<RouteNode>;\n}\n\nexport async function scanRoutes(root: string, routesDirectory: string): Promise<ScannedRoutes> {\n const cacheDirectory = resolve(root, \"node_modules/.cache/pagegraph\");\n let scanned: ScannedRoutes | undefined;\n const config = getConfig(\n {\n routesDirectory: resolve(root, routesDirectory),\n generatedRouteTree: resolve(cacheDirectory, \"route-tree.gen.ts\"),\n tmpDir: resolve(cacheDirectory, \"tmp\"),\n disableTypes: true,\n enableRouteTreeFormatting: false,\n disableLogging: true,\n plugins: [\n {\n name: \"pagegraph:scan\",\n onRouteTreeChanged({ routeTree, routeNodes, rootRouteNode }) {\n scanned = { root: rootRouteNode, tree: routeTree, nodes: routeNodes };\n },\n },\n ],\n },\n root,\n );\n const memory = inMemoryWriteFs(config.generatedRouteTree);\n await new Generator({ config, root, fs: memory.fs }).run();\n if (scanned === undefined) {\n throw new Error(`pagegraph found no route tree in ${resolve(root, routesDirectory)}`);\n }\n return scanned;\n}\n","/**\n * Build the graph from the app's own modules, evaluated in the `pagegraph`\n * Vite environment: one module per route file that declares `staticData`, plus\n * the app's collections module. Never the route tree, which reaches every\n * route — including app routes that only resolve inside the server runtime.\n */\n\nimport { relative, resolve } from \"node:path\";\n\nimport type { RouteNode } from \"@tanstack/router-generator\";\n\nimport { globToRegExp } from \"../core/checks\";\nimport type { RouteSeo } from \"../core/declare\";\nimport { buildSeoGraph, type SeoCollection, type SeoGraph, type SeoRouteNode } from \"../core/graph\";\nimport type { RobotsConfig } from \"../core/projections\";\nimport { deriveRouteConfig, resolveOptions, type SeoRouteConfigOptions } from \"../vite/route-config\";\nimport { scanRoutes, type ScannedRoutes } from \"./routes\";\n\n/** The robots.txt policy a build serves; `origin` and indexability come from {@link SiteRuntime}. */\nexport type RobotsPolicy = Pick<RobotsConfig, \"disallow\" | \"contentSignal\" | \"directives\">;\n\n/** The graph and the site runtime one evaluation produces. */\nexport interface AppGraph {\n readonly graph: SeoGraph;\n readonly site: SiteRuntime;\n}\n\n/** Site identity and robots policy, baked into the runtime module per build. */\nexport interface SiteRuntime {\n readonly origin: string;\n readonly indexable: boolean;\n readonly robots: RobotsPolicy;\n}\n\nexport interface PagegraphOptions {\n /** This build's canonical origin, no trailing slash, e.g. `https://example.com`. */\n readonly origin: string;\n /**\n * Whether this build's host may be indexed. A non-indexable build serves a\n * disallow-all robots.txt with no Sitemap line. Defaults to `true`.\n */\n readonly indexable?: boolean | undefined;\n readonly robots?:\n | {\n /** Path prefixes disallowed on an indexable host, beyond the route-config exclusions. */\n readonly disallow?: ReadonlyArray<string> | undefined;\n /** `Content-Signal` value for an indexable host, e.g. `\"search=yes, ai-input=yes, ai-train=yes\"`. */\n readonly contentSignal?: string | undefined;\n /** Extra lines in the `User-agent: *` group on an indexable host. */\n readonly directives?: ReadonlyArray<string> | undefined;\n }\n | undefined;\n /**\n * Root-relative module whose `collections` export is a `SeoCollection[]`, or a\n * function returning one (sync or async). It runs in the app's Vite pipeline,\n * so content virtual modules (Fumadocs, MDX) and path aliases resolve.\n */\n readonly collections?: string | undefined;\n /**\n * Route files (globs relative to the routes directory) never evaluated for the\n * graph: app surfaces that declare `staticData` but import server-runtime-only\n * modules. Their paths stay out of the graph.\n */\n readonly exclude?: ReadonlyArray<string> | undefined;\n /** Relative to the Vite root. Defaults to `src/routes`. */\n readonly routesDirectory?: string | undefined;\n /**\n * Generate the route-config module and enforce per-group SEO coverage (the\n * `seoRouteConfig` gate). Its robots exclusions join `robots.disallow`.\n */\n readonly routeConfig?: Omit<SeoRouteConfigOptions, \"routesDirectory\"> | undefined;\n}\n\n/** Imports a module through the `pagegraph` environment's module runner. */\nexport interface GraphModuleRunner {\n import(url: string): Promise<Record<string, unknown>>;\n}\n\n\nconst GRAPH_ROUTE_TYPES = new Set([\"__root\", \"static\", \"layout\", \"pathless_layout\"]);\n\nconst messageOf = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));\n\nconst routeStaticData = (module: Record<string, unknown>, file: string): SeoRouteNode[\"options\"][\"staticData\"] => {\n const route = module[\"Route\"] as { options?: { staticData?: { seo?: RouteSeo } } } | undefined;\n if (route?.options === undefined) throw new Error(`${file} has no \\`Route\\` export`);\n return route.options.staticData;\n};\n\nconst loadCollections = async (\n runner: GraphModuleRunner,\n root: string,\n fullPath: string,\n): Promise<ReadonlyArray<SeoCollection>> => {\n const path = relative(root, fullPath);\n const module = await runner.import(fullPath).catch((cause: unknown) => {\n throw new Error(`pagegraph could not evaluate ${path}: ${messageOf(cause)}`, { cause });\n });\n const exported = module[\"collections\"];\n if (exported === undefined) throw new Error(`${path} has no \\`collections\\` export`);\n const collections: unknown = typeof exported === \"function\" ? await (exported as () => unknown)() : exported;\n if (!Array.isArray(collections)) throw new Error(`${path} \\`collections\\` must be an array or return one`);\n return collections as ReadonlyArray<SeoCollection>;\n};\n\n/** What one evaluation loads: the scanned route tree and the modules that feed the graph. */\nexport interface GraphPlan {\n readonly root: string;\n readonly options: PagegraphOptions;\n readonly scan: ScannedRoutes;\n /** Absolute paths of the route files that declare `staticData`, plus `__root`. */\n readonly routeFiles: ReadonlyArray<string>;\n /** Absolute path of the collections module, when configured. */\n readonly collectionsFile: string | undefined;\n}\n\n/** Scan the routes directory and pick the modules an evaluation loads. */\nexport async function planGraph(root: string, options: PagegraphOptions): Promise<GraphPlan> {\n const scan = await scanRoutes(root, options.routesDirectory ?? \"src/routes\");\n const excluded = (options.exclude ?? []).map(globToRegExp);\n const participates = (node: RouteNode): boolean =>\n GRAPH_ROUTE_TYPES.has(node._fsRouteType) &&\n (node._fsRouteType === \"__root\" || node.createFileRouteProps?.has(\"staticData\") === true) &&\n !excluded.some((pattern) => pattern.test(node.filePath));\n return {\n root,\n options,\n scan,\n routeFiles: [scan.root, ...scan.nodes].filter(participates).map((node) => node.fullPath),\n collectionsFile: options.collections === undefined ? undefined : resolve(root, options.collections),\n };\n}\n\n/**\n * A route node's path relative to its parent, as the router matches it: route\n * groups, pathless layouts, and trailing underscores contribute no segment.\n */\nconst publicSegment = (node: RouteNode): string | undefined =>\n node.isNonPath === true || node.cleanedPath === undefined || node.cleanedPath === \"\" ? undefined : node.cleanedPath;\n\n/** Evaluate a plan's modules through `runner`, then build the graph and the site runtime. */\nexport async function evaluateGraph(runner: GraphModuleRunner, plan: GraphPlan): Promise<AppGraph> {\n const { root, options, scan } = plan;\n const staticData = new Map(\n await Promise.all(\n plan.routeFiles.map(async (fullPath) => {\n const file = relative(root, fullPath);\n const routeFile = relative(resolve(root, options.routesDirectory ?? \"src/routes\"), fullPath);\n const module = await runner.import(fullPath).catch((cause: unknown) => {\n throw new Error(\n `pagegraph could not evaluate ${file} in the graph environment: ${messageOf(cause)}\\n` +\n `If it is an app surface rather than a public page, add \"${routeFile}\" to pagegraph({ exclude }).`,\n { cause },\n );\n });\n return [fullPath, routeStaticData(module, file)] as const;\n }),\n ),\n );\n\n const walkable = (node: RouteNode): SeoRouteNode => ({\n options: { path: publicSegment(node), staticData: staticData.get(node.fullPath) },\n children: (node.children ?? []).map(walkable),\n });\n const routeTree: SeoRouteNode = {\n options: { staticData: staticData.get(scan.root.fullPath) },\n children: scan.tree.map(walkable),\n };\n\n const collections =\n plan.collectionsFile === undefined ? [] : await loadCollections(runner, root, plan.collectionsFile);\n const graph = buildSeoGraph({ routeTree, collections });\n\n const exclusions =\n options.routeConfig === undefined\n ? []\n : deriveRouteConfig(\n scan.nodes,\n resolveOptions({ ...options.routeConfig, routesDirectory: options.routesDirectory }),\n ).robotsExclusions;\n const site: SiteRuntime = {\n origin: options.origin,\n indexable: options.indexable ?? true,\n robots: {\n disallow: [...new Set([...exclusions, ...(options.robots?.disallow ?? [])])].sort(),\n contentSignal: options.robots?.contentSignal,\n directives: options.robots?.directives,\n },\n };\n return { graph, site };\n}\n","/**\n * The `pagegraph()` Vite plugin for TanStack Start.\n *\n * The graph is built once per build (and on change in dev) inside the app's own\n * Vite pipeline, then shipped to the server runtime as `virtual:pagegraph/runtime`.\n * The Worker never rebuilds it and never imports build code.\n *\n * Route modules and content collections are evaluated in a dedicated Node\n * environment, `pagegraph`, that shares the app's plugins, aliases, and\n * `define`s. Two settings make that safe:\n * - its `resolve.noExternal` is cleared, so TanStack Start's packages load from\n * node_modules, where their virtual router entry is an inert stub. Bundled (as\n * the app's server bundles them), they would pull in the whole route tree.\n * - only route files that declare `staticData` are evaluated, one by one, never\n * `routeTree.gen.ts`.\n *\n * Dev imports those modules through the environment's module runner and\n * re-evaluates after a change reaches it. A build builds the environment first\n * (a `buildApp` hook that runs before the framework's) with the build's own\n * config and plugins, then imports the output. The CLI and `loadPageHeads` use\n * {@link evaluateAppGraph}, which runs the dev path without a server.\n */\n\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport {\n createRunnableDevEnvironment,\n type BuildEnvironment,\n type Plugin,\n type RunnableDevEnvironment,\n type ViteBuilder,\n type ViteDevServer,\n} from \"vite\";\n\nimport { graphToJson } from \"../core/wire\";\nimport { seoRouteConfig } from \"../vite/route-config\";\nimport { evaluateGraph, planGraph, type AppGraph, type GraphModuleRunner, type GraphPlan, type PagegraphOptions } from \"./graph\";\n\nexport const GRAPH_ENVIRONMENT = \"pagegraph\";\nexport const PLUGIN_NAME = \"pagegraph\";\nconst RUNTIME_ID = \"virtual:pagegraph/runtime\";\nconst RESOLVED_RUNTIME_ID = `\\0${RUNTIME_ID}`;\nconst ENTRY_ID = \"virtual:pagegraph/entry\";\nconst RESOLVED_ENTRY_ID = `\\0${ENTRY_ID}`;\n/** The runtime entry imports the virtual module, so dep pre-bundling must leave it to this plugin. */\nconst RUNTIME_ENTRY = \"pagegraph/tanstack-start/server\";\n/** Where a build writes the graph environment's output, relative to the Vite root. */\nconst BUILD_DIRECTORY = \"node_modules/.cache/pagegraph/build\";\n/**\n * Runtime-provided modules (`cloudflare:workers`, `bun:sqlite`) that no plugin\n * resolves stay external in the graph environment, so an unexcluded route that\n * imports one fails at evaluation with its file name rather than as an\n * unresolved import. Plugin-owned schemes (`fumadocs-mdx:`) resolve first.\n */\nconst RUNTIME_SCHEME = /^(?!node:|data:|file:|virtual:)[a-z][a-z0-9+.-]*:/;\n\nconst runtimeModulesPlugin: Plugin = {\n name: \"pagegraph:runtime-modules\",\n enforce: \"post\",\n applyToEnvironment: (environment) => environment.name === GRAPH_ENVIRONMENT,\n resolveId: {\n filter: { id: RUNTIME_SCHEME },\n handler: (id) => ({ id, external: true }),\n },\n};\n\n/** What the plugin exposes to {@link evaluateAppGraph} through `plugin.api`. */\nexport interface PagegraphPluginApi {\n readonly options: PagegraphOptions;\n}\n\nconst runtimeModule = ({ graph, site }: AppGraph): string =>\n `export const graph = ${JSON.stringify(graphToJson(graph))};\\nexport const site = ${JSON.stringify(site)};\\n`;\n\n/** The graph environment's build entry: a loader per module the plan evaluates. */\nconst entryModule = (plan: GraphPlan): string => {\n const files = [...plan.routeFiles, ...(plan.collectionsFile === undefined ? [] : [plan.collectionsFile])];\n const loaders = files.map((file) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(file)}),`);\n return `export const modules = {\\n${loaders.join(\"\\n\")}\\n};\\n`;\n};\n\n/** Build the graph environment with the build's config, import its output, and evaluate. */\nconst buildGraph = async (builder: ViteBuilder, environment: BuildEnvironment, plan: GraphPlan): Promise<AppGraph> => {\n await builder.build(environment);\n const entry = join(resolve(environment.config.root, environment.config.build.outDir), \"entry.mjs\");\n const { modules } = (await import(pathToFileURL(entry).href)) as {\n modules: Record<string, () => Promise<Record<string, unknown>>>;\n };\n const runner: GraphModuleRunner = {\n import: (file) => {\n const load = modules[file];\n if (load === undefined) throw new Error(`${file} is not in the graph build`);\n return load();\n },\n };\n return evaluateGraph(runner, plan);\n};\n\nexport function pagegraph(options: PagegraphOptions): Array<Plugin> {\n const api: PagegraphPluginApi = { options };\n let root = \"\";\n let server: ViteDevServer | undefined;\n let devEnvironment: RunnableDevEnvironment | undefined;\n /** The plan the graph environment's build entry is generated from. */\n let buildPlan: GraphPlan | undefined;\n let current: Promise<AppGraph> | undefined;\n /** Bumped by every change that reaches the graph; an evaluation that spans one is discarded. */\n let generation = 0;\n\n const evaluateInDev = async (environment: RunnableDevEnvironment): Promise<AppGraph> => {\n for (;;) {\n const started = generation;\n // Re-evaluate from scratch: content and route modules may have changed.\n environment.runner.clearCache();\n const result = await evaluateGraph(environment.runner, await planGraph(root, options));\n if (started === generation) return result;\n }\n };\n\n const graphPlugin: Plugin & { api: PagegraphPluginApi } = {\n name: PLUGIN_NAME,\n api,\n // One instance across a build's environments: the graph built in buildApp\n // is the one every environment's runtime module serves.\n sharedDuringBuild: true,\n config() {\n return {\n environments: {\n [GRAPH_ENVIRONMENT]: {\n consumer: \"server\",\n dev: {\n createEnvironment: (name, environmentConfig) => createRunnableDevEnvironment(name, environmentConfig),\n },\n build: {\n outDir: BUILD_DIRECTORY,\n emptyOutDir: true,\n copyPublicDir: false,\n minify: false,\n rolldownOptions: {\n input: { entry: ENTRY_ID },\n output: { entryFileNames: \"[name].mjs\", chunkFileNames: \"chunks/[name]-[hash].mjs\" },\n },\n },\n },\n },\n };\n },\n configEnvironment(name, environmentConfig) {\n if (name === GRAPH_ENVIRONMENT) {\n environmentConfig.resolve ??= {};\n environmentConfig.resolve.noExternal = [];\n return;\n }\n environmentConfig.optimizeDeps ??= {};\n environmentConfig.optimizeDeps.exclude = [...(environmentConfig.optimizeDeps.exclude ?? []), RUNTIME_ENTRY];\n },\n configResolved(resolved) {\n root = resolved.root;\n },\n configureServer(devServer) {\n server = devServer;\n devEnvironment = devServer.environments[GRAPH_ENVIRONMENT] as RunnableDevEnvironment | undefined;\n },\n // Runs after Vite invalidates the changed file in each environment, so the\n // next evaluation reads fresh modules.\n hotUpdate({ file, modules }) {\n if (this.environment.name !== GRAPH_ENVIRONMENT) return;\n const routesDirectory = resolve(root, options.routesDirectory ?? \"src/routes\");\n if (modules.length === 0 && !file.startsWith(`${routesDirectory}/`)) return;\n generation += 1;\n current = undefined;\n for (const other of Object.values(server?.environments ?? {})) {\n if (other.name === GRAPH_ENVIRONMENT) continue;\n const runtime = other.moduleGraph.getModuleById(RESOLVED_RUNTIME_ID);\n if (runtime !== undefined) void other.reloadModule(runtime);\n }\n },\n buildApp: {\n order: \"pre\",\n async handler(builder) {\n const environment = builder.environments[GRAPH_ENVIRONMENT];\n if (environment === undefined) return;\n buildPlan = await planGraph(environment.config.root, options);\n const built = buildGraph(builder, environment, buildPlan);\n current = built;\n await built;\n // Vite builds every environment when a build's app builder builds none;\n // the graph environment must not count, or a plain app would skip the rest.\n environment.isBuilt = false;\n },\n },\n resolveId: {\n filter: { id: /^virtual:pagegraph\\/(runtime|entry)$/ },\n handler: (id) => `\\0${id}`,\n },\n load: {\n filter: { id: /^\\0virtual:pagegraph\\/(runtime|entry)$/ },\n async handler(id) {\n if (id === RESOLVED_ENTRY_ID) {\n if (buildPlan === undefined) throw new Error(`[${PLUGIN_NAME}] ${ENTRY_ID} is only built by the pagegraph buildApp hook`);\n return entryModule(buildPlan);\n }\n // A page evaluated for the graph may import the runtime; it never serves from it.\n if (this.environment.name === GRAPH_ENVIRONMENT) return \"export const graph = null;\\nexport const site = null;\\n\";\n if (current === undefined) {\n if (devEnvironment === undefined) {\n throw new Error(\n `[${PLUGIN_NAME}] the graph was not built before ${this.environment.name} imported it; build with Vite's app builder (\\`vite build\\`).`,\n );\n }\n current = evaluateInDev(devEnvironment);\n }\n return runtimeModule(await current);\n },\n },\n };\n\n return [\n graphPlugin,\n runtimeModulesPlugin,\n ...(options.routeConfig === undefined\n ? []\n : [seoRouteConfig({ ...options.routeConfig, routesDirectory: options.routesDirectory })]),\n ];\n}\n","/**\n * Evaluate an app's graph outside a running dev server — for a build, the\n * CLI (`seo.config.ts`), and build-time head loading — through the same\n * `pagegraph` environment the dev server uses.\n */\n\nimport { createRunnableDevEnvironment, resolveConfig, type Plugin } from \"vite\";\n\nimport type { LoadedSeoGraph, SeoGraphLoader } from \"../config/vite-graph-loader\";\nimport { withCleanStdout } from \"../config/vite-graph-loader\";\nimport { evaluateGraph, planGraph, type AppGraph } from \"./graph\";\nimport type { PagegraphPluginApi } from \"./plugin\";\n\nconst GRAPH_ENVIRONMENT = \"pagegraph\";\n\nexport interface EvaluateAppGraphOptions {\n /** The app's Vite root. */\n readonly root: string;\n /** The app's Vite config file. Defaults to Vite's lookup from `root`. */\n readonly configFile?: string | undefined;\n /**\n * Vite mode the config is evaluated in. Origins and other build constants come\n * from it, so the CLI defaults to `production` to describe the deployed site.\n */\n readonly mode?: string | undefined;\n}\n\nconst pluginApi = (plugins: ReadonlyArray<Plugin>, configFile: string | undefined): PagegraphPluginApi => {\n const plugin = plugins.find((candidate) => candidate.name === \"pagegraph\") as (Plugin & { api?: PagegraphPluginApi }) | undefined;\n if (plugin?.api === undefined) {\n throw new Error(`${configFile ?? \"The Vite config\"} does not register pagegraph() from \"pagegraph/tanstack-start\".`);\n }\n return plugin.api;\n};\n\n/**\n * Resolve the app's Vite config in serve mode, evaluate the graph in its\n * `pagegraph` environment, and release it — the dev server's evaluation path\n * without a server. A config that branches on `command` sees `serve`; pass\n * `mode` for the deployment the graph should describe.\n */\nexport async function evaluateAppGraph(options: EvaluateAppGraphOptions): Promise<AppGraph> {\n const config = await resolveConfig(\n { root: options.root, configFile: options.configFile, mode: options.mode, logLevel: \"error\" },\n \"serve\",\n );\n const api = pluginApi(config.plugins, config.configFile);\n const environment = createRunnableDevEnvironment(GRAPH_ENVIRONMENT, config, { hot: false });\n await environment.init();\n try {\n // Content plugins (Fumadocs, MDX) generate their virtual sources at buildStart.\n await environment.pluginContainer.buildStart({});\n return await evaluateGraph(environment.runner, await planGraph(config.root, api.options));\n } finally {\n await environment.close();\n }\n}\n\n/**\n * A `seo.config.ts` graph loader for a TanStack Start app: the same graph the\n * `pagegraph()` plugin ships to the runtime, with its robots policy.\n *\n * ```ts\n * export default defineSeoConfig({\n * origin: \"https://example.com\",\n * loadGraph: tanstackStartGraph({ root: import.meta.dirname }),\n * });\n * ```\n */\nexport const tanstackStartGraph =\n (options: EvaluateAppGraphOptions): SeoGraphLoader =>\n async (): Promise<LoadedSeoGraph> => {\n const { graph, site } = await withCleanStdout(() =>\n evaluateAppGraph({ ...options, mode: options.mode ?? \"production\" }),\n );\n return { graph, robots: site.robots, dispose: async () => {} };\n };\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { a as SeoGraph } from "../graph-BxCY90vq.js";
2
+ import { i as SiteRuntime } from "../graph-CBIY71w6.js";
3
+ //#region src/tanstack-start/server.d.ts
4
+ /** The deployment's SEO graph. */
5
+ declare function seoGraph(): SeoGraph;
6
+ /** The deployment's origin, indexability, and robots policy. */
7
+ declare function seoSite(): SiteRuntime;
8
+ /** `GET /robots.txt`: the robots policy on an indexable host, disallow-all otherwise. */
9
+ declare function robotsTxt(): Response;
10
+ /** `GET /sitemap.xml`. */
11
+ declare function sitemapXml(): Response;
12
+ //#endregion
13
+ export { robotsTxt, seoGraph, seoSite, sitemapXml };
14
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { t as graphFromJson } from "../wire-B4RQMc4e.js";
2
+ import { o as renderRobots, s as renderSitemap } from "../projections-D0oOFKiY.js";
3
+ import { graph, site } from "virtual:pagegraph/runtime";
4
+ //#region src/tanstack-start/server.ts
5
+ /**
6
+ * `pagegraph/tanstack-start/server` — runtime. Serves the graph the
7
+ * `pagegraph()` plugin built for this deployment:
8
+ *
9
+ * ```ts
10
+ * // src/routes/robots[.]txt.ts
11
+ * export const Route = createFileRoute("/robots.txt")({ server: { handlers: { GET: robotsTxt } } });
12
+ * ```
13
+ */
14
+ let decoded;
15
+ const unavailable = () => /* @__PURE__ */ new Error("pagegraph/tanstack-start/server has no graph: register pagegraph() from \"pagegraph/tanstack-start\" in vite.config.ts. (Inside the graph environment itself, the graph is still being built.)");
16
+ /** The deployment's SEO graph. */
17
+ function seoGraph() {
18
+ if (graph === null) throw unavailable();
19
+ decoded ??= graphFromJson(graph);
20
+ return decoded;
21
+ }
22
+ /** The deployment's origin, indexability, and robots policy. */
23
+ function seoSite() {
24
+ if (site === null) throw unavailable();
25
+ return site;
26
+ }
27
+ /** `GET /robots.txt`: the robots policy on an indexable host, disallow-all otherwise. */
28
+ function robotsTxt() {
29
+ const site = seoSite();
30
+ return new Response(renderRobots(seoGraph(), {
31
+ origin: site.origin,
32
+ indexable: site.indexable,
33
+ ...site.robots
34
+ }), { headers: {
35
+ "Content-Type": "text/plain; charset=utf-8",
36
+ "Cache-Control": site.indexable ? "public, max-age=86400" : "public, max-age=3600"
37
+ } });
38
+ }
39
+ /** `GET /sitemap.xml`. */
40
+ function sitemapXml() {
41
+ const site = seoSite();
42
+ return new Response(renderSitemap(seoGraph(), {
43
+ origin: site.origin,
44
+ indexable: site.indexable
45
+ }), { headers: {
46
+ "Content-Type": "application/xml; charset=utf-8",
47
+ "Cache-Control": "public, max-age=3600"
48
+ } });
49
+ }
50
+ //#endregion
51
+ export { robotsTxt, seoGraph, seoSite, sitemapXml };
52
+
53
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":["graphJson","siteRuntime"],"sources":["../../src/tanstack-start/server.ts"],"sourcesContent":["/**\n * `pagegraph/tanstack-start/server` — runtime. Serves the graph the\n * `pagegraph()` plugin built for this deployment:\n *\n * ```ts\n * // src/routes/robots[.]txt.ts\n * export const Route = createFileRoute(\"/robots.txt\")({ server: { handlers: { GET: robotsTxt } } });\n * ```\n */\n\nimport { graph as graphJson, site as siteRuntime } from \"virtual:pagegraph/runtime\";\n\nimport type { SeoGraph } from \"../core/graph\";\nimport { renderRobots, renderSitemap } from \"../core/projections\";\nimport { graphFromJson } from \"../core/wire\";\nimport type { SiteRuntime } from \"./graph\";\n\nlet decoded: SeoGraph | undefined;\n\nconst unavailable = (): Error =>\n new Error(\n \"pagegraph/tanstack-start/server has no graph: register pagegraph() from \\\"pagegraph/tanstack-start\\\" in vite.config.ts. \" +\n \"(Inside the graph environment itself, the graph is still being built.)\",\n );\n\n/** The deployment's SEO graph. */\nexport function seoGraph(): SeoGraph {\n if (graphJson === null) throw unavailable();\n decoded ??= graphFromJson(graphJson);\n return decoded;\n}\n\n/** The deployment's origin, indexability, and robots policy. */\nexport function seoSite(): SiteRuntime {\n if (siteRuntime === null) throw unavailable();\n return siteRuntime;\n}\n\n/** `GET /robots.txt`: the robots policy on an indexable host, disallow-all otherwise. */\nexport function robotsTxt(): Response {\n const site = seoSite();\n return new Response(renderRobots(seoGraph(), { origin: site.origin, indexable: site.indexable, ...site.robots }), {\n headers: {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n \"Cache-Control\": site.indexable ? \"public, max-age=86400\" : \"public, max-age=3600\",\n },\n });\n}\n\n/** `GET /sitemap.xml`. */\nexport function sitemapXml(): Response {\n const site = seoSite();\n return new Response(renderSitemap(seoGraph(), { origin: site.origin, indexable: site.indexable }), {\n headers: {\n \"Content-Type\": \"application/xml; charset=utf-8\",\n \"Cache-Control\": \"public, max-age=3600\",\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,IAAI;AAEJ,MAAM,oCACJ,IAAI,MACF,gMAEF;;AAGF,SAAgB,WAAqB;CACnC,IAAIA,UAAc,MAAM,MAAM,YAAY;CAC1C,YAAY,cAAcA,KAAS;CACnC,OAAO;AACT;;AAGA,SAAgB,UAAuB;CACrC,IAAIC,SAAgB,MAAM,MAAM,YAAY;CAC5C,OAAOA;AACT;;AAGA,SAAgB,YAAsB;CACpC,MAAM,OAAO,QAAQ;CACrB,OAAO,IAAI,SAAS,aAAa,SAAS,GAAG;EAAE,QAAQ,KAAK;EAAQ,WAAW,KAAK;EAAW,GAAG,KAAK;CAAO,CAAC,GAAG,EAChH,SAAS;EACP,gBAAgB;EAChB,iBAAiB,KAAK,YAAY,0BAA0B;CAC9D,EACF,CAAC;AACH;;AAGA,SAAgB,aAAuB;CACrC,MAAM,OAAO,QAAQ;CACrB,OAAO,IAAI,SAAS,cAAc,SAAS,GAAG;EAAE,QAAQ,KAAK;EAAQ,WAAW,KAAK;CAAU,CAAC,GAAG,EACjG,SAAS;EACP,gBAAgB;EAChB,iBAAiB;CACnB,EACF,CAAC;AACH"}
@@ -0,0 +1,104 @@
1
+ import { createServer } from "vite";
2
+ //#region src/config/vite-graph-loader.ts
3
+ /**
4
+ * A graph loader that evaluates the host app's own graph module inside a
5
+ * headless Vite server.
6
+ *
7
+ * The SEO graph is built *by the app*: its module reads the generated route
8
+ * tree and whatever content collections it owns, so it pulls in the app's real
9
+ * module graph — path aliases, virtual modules from content plugins, bare
10
+ * imports that only exist inside a server runtime. A CLI cannot just `import()`
11
+ * that from Node or Bun.
12
+ *
13
+ * So this boots a scoped, in-process Vite server (middleware mode, no HMR, no
14
+ * watcher, no HTTP listener), `ssrLoadModule`s the app's graph module, and calls
15
+ * its exported loader. The graph comes back as a live in-process object — there
16
+ * is no serialization boundary for the commands to cross — and the server is
17
+ * closed by {@link LoadedSeoGraph.dispose}.
18
+ *
19
+ * Everything host-shaped is an option: the app supplies its own {@link
20
+ * ViteGraphLoaderOptions.plugins} (a content plugin, an MDX loader), {@link
21
+ * ViteGraphLoaderOptions.stubs} for bare modules that only resolve inside its
22
+ * server runtime, and {@link ViteGraphLoaderOptions.env} for variables its
23
+ * modules parse at import. This file knows about none of them.
24
+ */
25
+ const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
26
+ /**
27
+ * Vite and its plugins write progress to stdout. The CLI reserves stdout for the
28
+ * data plane, so redirect it to stderr for the duration of the load.
29
+ *
30
+ * Both the stream method and `console.log`/`info` are swapped: under Bun,
31
+ * `console.log` writes to fd 1 natively rather than going through
32
+ * `process.stdout.write`, so the stream swap alone would miss a plugin that
33
+ * logs through the console.
34
+ */
35
+ const withCleanStdout = async (run) => {
36
+ const originalWrite = process.stdout.write.bind(process.stdout);
37
+ const originalLog = console.log;
38
+ const originalInfo = console.info;
39
+ process.stdout.write = process.stderr.write.bind(process.stderr);
40
+ console.log = (...args) => console.error(...args);
41
+ console.info = (...args) => console.error(...args);
42
+ try {
43
+ return await run();
44
+ } finally {
45
+ process.stdout.write = originalWrite;
46
+ console.log = originalLog;
47
+ console.info = originalInfo;
48
+ }
49
+ };
50
+ /** Turn `{ id: source }` into a Vite plugin that resolves and loads each id. */
51
+ const stubPlugin = (stubs) => {
52
+ const virtualIdOf = (id) => `\0seo-cli-stub:${id}`;
53
+ const sourceByVirtualId = new Map(Object.entries(stubs).map(([id, source]) => [virtualIdOf(id), source]));
54
+ return {
55
+ name: "seo:stubs",
56
+ resolveId: (id) => id in stubs ? virtualIdOf(id) : null,
57
+ load: (id) => sourceByVirtualId.get(id) ?? null
58
+ };
59
+ };
60
+ const seedEnv = (env) => {
61
+ for (const [key, value] of Object.entries(env)) process.env[key] ??= value;
62
+ };
63
+ const inlineConfigFor = (options) => ({
64
+ configFile: false,
65
+ root: options.root,
66
+ mode: "production",
67
+ logLevel: "error",
68
+ appType: "custom",
69
+ clearScreen: false,
70
+ server: {
71
+ middlewareMode: true,
72
+ hmr: false,
73
+ watch: null
74
+ },
75
+ resolve: { tsconfigPaths: true },
76
+ plugins: [...options.stubs === void 0 ? [] : [stubPlugin(options.stubs)], ...options.plugins ?? []]
77
+ });
78
+ /**
79
+ * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite
80
+ * server rooted at `root`, and returns what its `exportName` export resolves to.
81
+ */
82
+ const viteGraphLoader = (options) => async (input) => {
83
+ const exportName = options.exportName ?? "loadSeoGraph";
84
+ if (options.env) seedEnv(options.env);
85
+ const server = await withCleanStdout(() => createServer(inlineConfigFor(options))).catch((cause) => {
86
+ throw new Error(`Could not start the Vite loader: ${messageOf(cause)}`);
87
+ });
88
+ return {
89
+ graph: await withCleanStdout(async () => {
90
+ const module = await server.ssrLoadModule(options.entry);
91
+ const load = module[exportName];
92
+ if (typeof load !== "function") throw new Error(`${options.entry} has no \`${exportName}\` export (found: ${Object.keys(module).join(", ") || "nothing"}).`);
93
+ return await load(input);
94
+ }).catch(async (cause) => {
95
+ await withCleanStdout(() => server.close()).catch(() => {});
96
+ throw new Error(`Could not build the SEO graph: ${messageOf(cause)}`);
97
+ }),
98
+ dispose: () => withCleanStdout(() => server.close())
99
+ };
100
+ };
101
+ //#endregion
102
+ export { withCleanStdout as n, viteGraphLoader as t };
103
+
104
+ //# sourceMappingURL=vite-graph-loader-01p1WxEq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-graph-loader-01p1WxEq.js","names":[],"sources":["../src/config/vite-graph-loader.ts"],"sourcesContent":["/**\n * A graph loader that evaluates the host app's own graph module inside a\n * headless Vite server.\n *\n * The SEO graph is built *by the app*: its module reads the generated route\n * tree and whatever content collections it owns, so it pulls in the app's real\n * module graph — path aliases, virtual modules from content plugins, bare\n * imports that only exist inside a server runtime. A CLI cannot just `import()`\n * that from Node or Bun.\n *\n * So this boots a scoped, in-process Vite server (middleware mode, no HMR, no\n * watcher, no HTTP listener), `ssrLoadModule`s the app's graph module, and calls\n * its exported loader. The graph comes back as a live in-process object — there\n * is no serialization boundary for the commands to cross — and the server is\n * closed by {@link LoadedSeoGraph.dispose}.\n *\n * Everything host-shaped is an option: the app supplies its own {@link\n * ViteGraphLoaderOptions.plugins} (a content plugin, an MDX loader), {@link\n * ViteGraphLoaderOptions.stubs} for bare modules that only resolve inside its\n * server runtime, and {@link ViteGraphLoaderOptions.env} for variables its\n * modules parse at import. This file knows about none of them.\n */\n\nimport { createServer, type InlineConfig, type PluginOption } from \"vite\";\n\nimport type { SeoGraph } from \"../core/graph\";\nimport type { RobotsConfig } from \"../core/projections\";\n\n/** A graph, plus the release of whatever producing it acquired. */\nexport interface LoadedSeoGraph {\n readonly graph: SeoGraph;\n /**\n * The robots policy the app serves, when the loader knows it (the TanStack\n * Start loader reads it from the `pagegraph()` plugin). `pagegraph robots`\n * uses it instead of `seo.config.ts` fields.\n */\n readonly robots?: Pick<RobotsConfig, \"disallow\" | \"contentSignal\" | \"directives\"> | undefined;\n /** Called once the command is done with the graph, on success or failure. */\n readonly dispose: () => Promise<void>;\n}\n\n/**\n * Produces the SEO graph for the `pagegraph` CLI. A plain promise on purpose: a\n * config file must be writable without Effect, which the CLI keeps behind its\n * `bin` (an optional peer dependency).\n */\n/** JSON input that can be supplied by config files to an app graph export. */\nexport type GraphLoaderInput = null | boolean | number | string | ReadonlyArray<GraphLoaderInput> | { readonly [key: string]: GraphLoaderInput };\n\nexport type SeoGraphLoader<Input extends GraphLoaderInput | void = void> = (input: Input) => Promise<LoadedSeoGraph>;\n\nexport interface ViteGraphLoaderOptions {\n /** The app's Vite root — the directory its aliases and plugins resolve against. */\n readonly root: string;\n /** Module exporting the graph loader, root-relative (e.g. `/lib/seo/graph.ts`). */\n readonly entry: string;\n /** Named export on `entry` returning `Promise<SeoGraph>`. Defaults to `loadSeoGraph`. */\n readonly exportName?: string | undefined;\n /** Vite plugins the app's module graph needs — a content/MDX plugin, say. */\n readonly plugins?: ReadonlyArray<PluginOption> | undefined;\n /**\n * Bare or virtual module ids to replace with inert source, so a module that\n * only resolves inside the app's server runtime does not break the load.\n */\n readonly stubs?: Readonly<Record<string, string>> | undefined;\n /**\n * Variables to seed on `process.env` before the app's modules parse it. Vite\n * exposes prefixed values through `import.meta.env`, so this is how a module\n * that demands an env var at import time is satisfied. Existing values win —\n * a real `.env` is never clobbered.\n */\n readonly env?: Readonly<Record<string, string>> | undefined;\n}\n\nconst messageOf = (cause: unknown): string =>\n cause instanceof Error ? cause.message : String(cause);\n\n/**\n * Vite and its plugins write progress to stdout. The CLI reserves stdout for the\n * data plane, so redirect it to stderr for the duration of the load.\n *\n * Both the stream method and `console.log`/`info` are swapped: under Bun,\n * `console.log` writes to fd 1 natively rather than going through\n * `process.stdout.write`, so the stream swap alone would miss a plugin that\n * logs through the console.\n */\nexport const withCleanStdout = async <A>(run: () => Promise<A>): Promise<A> => {\n const originalWrite = process.stdout.write.bind(process.stdout);\n const originalLog = console.log;\n const originalInfo = console.info;\n process.stdout.write = process.stderr.write.bind(process.stderr) as typeof process.stdout.write;\n console.log = (...args: Array<unknown>) => console.error(...args);\n console.info = (...args: Array<unknown>) => console.error(...args);\n try {\n return await run();\n } finally {\n process.stdout.write = originalWrite;\n console.log = originalLog;\n console.info = originalInfo;\n }\n};\n\n/** Turn `{ id: source }` into a Vite plugin that resolves and loads each id. */\nconst stubPlugin = (stubs: Readonly<Record<string, string>>): PluginOption => {\n const virtualIdOf = (id: string) => `\\0seo-cli-stub:${id}`;\n const sourceByVirtualId = new Map(\n Object.entries(stubs).map(([id, source]) => [virtualIdOf(id), source]),\n );\n return {\n name: \"seo:stubs\",\n resolveId: (id) => (id in stubs ? virtualIdOf(id) : null),\n load: (id) => sourceByVirtualId.get(id) ?? null,\n };\n};\n\nconst seedEnv = (env: Readonly<Record<string, string>>): void => {\n for (const [key, value] of Object.entries(env)) {\n // oxlint-disable-next-line node/no-process-env -- CLI boundary: seeds the raw env BEFORE the app's own env module parses it; that module is the consumer here, not an option\n process.env[key] ??= value;\n }\n};\n\nconst inlineConfigFor = (options: ViteGraphLoaderOptions): InlineConfig => ({\n configFile: false,\n root: options.root,\n mode: \"production\",\n logLevel: \"error\",\n appType: \"custom\",\n clearScreen: false,\n server: { middlewareMode: true, hmr: false, watch: null },\n resolve: { tsconfigPaths: true },\n plugins: [\n ...(options.stubs === undefined ? [] : [stubPlugin(options.stubs)]),\n ...(options.plugins ?? []),\n ],\n});\n\n/**\n * Build a {@link SeoGraphLoader} that evaluates `entry` in a headless Vite\n * server rooted at `root`, and returns what its `exportName` export resolves to.\n */\nexport const viteGraphLoader =\n <Input extends GraphLoaderInput | void = void>(options: ViteGraphLoaderOptions): SeoGraphLoader<Input> =>\n async (input) => {\n const exportName = options.exportName ?? \"loadSeoGraph\";\n if (options.env) seedEnv(options.env);\n\n const server = await withCleanStdout(() => createServer(inlineConfigFor(options))).catch(\n (cause: unknown) => {\n throw new Error(`Could not start the Vite loader: ${messageOf(cause)}`);\n },\n );\n\n // The server is a resource from here on: a failed load must still close it,\n // and a successful one hands the close to the caller as `dispose`.\n const graph = await withCleanStdout(async () => {\n const module = (await server.ssrLoadModule(options.entry)) as Record<string, unknown>;\n const load = module[exportName];\n if (typeof load !== \"function\") {\n throw new Error(\n `${options.entry} has no \\`${exportName}\\` export (found: ${Object.keys(module).join(\", \") || \"nothing\"}).`,\n );\n }\n return (await load(input)) as SeoGraph;\n }).catch(async (cause: unknown) => {\n // Close on the way out, but never let a close failure bury the real\n // error: the reason the graph did not build is the useful one.\n await withCleanStdout(() => server.close()).catch(() => {});\n throw new Error(`Could not build the SEO graph: ${messageOf(cause)}`);\n });\n\n return { graph, dispose: () => withCleanStdout(() => server.close()) };\n };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0EA,MAAM,aAAa,UACjB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;;;;AAWvD,MAAa,kBAAkB,OAAU,QAAsC;CAC7E,MAAM,gBAAgB,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC9D,MAAM,cAAc,QAAQ;CAC5B,MAAM,eAAe,QAAQ;CAC7B,QAAQ,OAAO,QAAQ,QAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM;CAC/D,QAAQ,OAAO,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CAChE,QAAQ,QAAQ,GAAG,SAAyB,QAAQ,MAAM,GAAG,IAAI;CACjE,IAAI;EACF,OAAO,MAAM,IAAI;CACnB,UAAU;EACR,QAAQ,OAAO,QAAQ;EACvB,QAAQ,MAAM;EACd,QAAQ,OAAO;CACjB;AACF;;AAGA,MAAM,cAAc,UAA0D;CAC5E,MAAM,eAAe,OAAe,kBAAkB;CACtD,MAAM,oBAAoB,IAAI,IAC5B,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,YAAY,EAAE,GAAG,MAAM,CAAC,CACvE;CACA,OAAO;EACL,MAAM;EACN,YAAY,OAAQ,MAAM,QAAQ,YAAY,EAAE,IAAI;EACpD,OAAO,OAAO,kBAAkB,IAAI,EAAE,KAAK;CAC7C;AACF;AAEA,MAAM,WAAW,QAAgD;CAC/D,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAE3C,QAAQ,IAAI,SAAS;AAEzB;AAEA,MAAM,mBAAmB,aAAmD;CAC1E,YAAY;CACZ,MAAM,QAAQ;CACd,MAAM;CACN,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;EAAE,gBAAgB;EAAM,KAAK;EAAO,OAAO;CAAK;CACxD,SAAS,EAAE,eAAe,KAAK;CAC/B,SAAS,CACP,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,CAAC,WAAW,QAAQ,KAAK,CAAC,GACjE,GAAI,QAAQ,WAAW,CAAC,CAC1B;AACF;;;;;AAMA,MAAa,mBACoC,YAC/C,OAAO,UAAU;CACf,MAAM,aAAa,QAAQ,cAAc;CACzC,IAAI,QAAQ,KAAK,QAAQ,QAAQ,GAAG;CAEpC,MAAM,SAAS,MAAM,sBAAsB,aAAa,gBAAgB,OAAO,CAAC,CAAC,CAAC,CAAC,OAChF,UAAmB;EAClB,MAAM,IAAI,MAAM,oCAAoC,UAAU,KAAK,GAAG;CACxE,CACF;CAoBA,OAAO;EAAE,OAAA,MAhBW,gBAAgB,YAAY;GAC9C,MAAM,SAAU,MAAM,OAAO,cAAc,QAAQ,KAAK;GACxD,MAAM,OAAO,OAAO;GACpB,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MACR,GAAG,QAAQ,MAAM,YAAY,WAAW,oBAAoB,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,KAAK,UAAU,GAC1G;GAEF,OAAQ,MAAM,KAAK,KAAK;EAC1B,CAAC,CAAC,CAAC,MAAM,OAAO,UAAmB;GAGjC,MAAM,sBAAsB,OAAO,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;GAC1D,MAAM,IAAI,MAAM,kCAAkC,UAAU,KAAK,GAAG;EACtE,CAAC;EAEe,eAAe,sBAAsB,OAAO,MAAM,CAAC;CAAE;AACvE"}