zudoku 0.85.0 → 0.86.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/dist/cli/cli.js CHANGED
@@ -53,13 +53,90 @@ var init_joinUrl = __esm({
53
53
  }
54
54
  });
55
55
 
56
+ // src/lib/util/markdown.ts
57
+ var getMarkdownPathname;
58
+ var init_markdown = __esm({
59
+ "src/lib/util/markdown.ts"() {
60
+ getMarkdownPathname = (pathname) => pathname === "/" ? "/index" : pathname;
61
+ }
62
+ });
63
+
64
+ // src/lib/util/markdown-representation.ts
65
+ var encodePathSegment, encodeDocumentationRoutePath, resolveDocumentationRoutePath, getMarkdownRepresentationPath, getMarkdownAlternateLink, appendLinkHeader, getMarkdownNotFound;
66
+ var init_markdown_representation = __esm({
67
+ "src/lib/util/markdown-representation.ts"() {
68
+ init_joinUrl();
69
+ init_markdown();
70
+ encodePathSegment = (segment) => {
71
+ try {
72
+ return encodeURIComponent(decodeURIComponent(segment));
73
+ } catch {
74
+ return encodeURIComponent(segment);
75
+ }
76
+ };
77
+ encodeDocumentationRoutePath = (routePath) => {
78
+ const encodedPath = routePath.split("/").filter(Boolean).map(encodePathSegment).join("/");
79
+ return encodedPath ? `/${encodedPath}` : "/";
80
+ };
81
+ resolveDocumentationRoutePath = (requestUrl, basePath) => {
82
+ const pathname = new URL(requestUrl, "http://localhost").pathname;
83
+ const normalizedBasePath = encodeDocumentationRoutePath(joinUrl(basePath));
84
+ if (normalizedBasePath !== "/" && pathname !== normalizedBasePath && !pathname.startsWith(`${normalizedBasePath}/`)) {
85
+ return;
86
+ }
87
+ const relativePath = normalizedBasePath === "/" ? pathname : pathname.slice(normalizedBasePath.length) || "/";
88
+ return encodeDocumentationRoutePath(relativePath);
89
+ };
90
+ getMarkdownRepresentationPath = (routePath, basePath) => joinUrl(
91
+ encodeDocumentationRoutePath(joinUrl(basePath)),
92
+ `${getMarkdownPathname(encodeDocumentationRoutePath(routePath))}.md`
93
+ );
94
+ getMarkdownAlternateLink = (routePath, basePath) => `<${getMarkdownRepresentationPath(routePath, basePath)}>; rel="alternate"; type="text/markdown"`;
95
+ appendLinkHeader = (currentValue, linkValue) => {
96
+ const normalizedValue = Array.isArray(currentValue) ? currentValue.join(", ") : currentValue?.toString().trim();
97
+ return normalizedValue ? `${normalizedValue}, ${linkValue}` : linkValue;
98
+ };
99
+ getMarkdownNotFound = ({
100
+ basePath,
101
+ includeLlmsTxt,
102
+ markdownRoutePaths,
103
+ sitemapOutDir
104
+ }) => {
105
+ const encodedBasePath = encodeDocumentationRoutePath(joinUrl(basePath));
106
+ const markdownEntryRoute = markdownRoutePaths.includes("/") ? "/" : markdownRoutePaths[0];
107
+ const links = [
108
+ `[Documentation home](${encodedBasePath})`,
109
+ ...markdownEntryRoute ? [
110
+ `[Markdown documentation index](${getMarkdownRepresentationPath(markdownEntryRoute, basePath)})`
111
+ ] : [],
112
+ ...includeLlmsTxt ? [`[Agent documentation index](${joinUrl(encodedBasePath, "llms.txt")})`] : [],
113
+ ...sitemapOutDir !== void 0 ? [
114
+ `[Sitemap](${joinUrl(
115
+ encodedBasePath,
116
+ encodeDocumentationRoutePath(sitemapOutDir),
117
+ "sitemap.xml"
118
+ )})`
119
+ ] : []
120
+ ];
121
+ return [
122
+ "# Page not found",
123
+ "",
124
+ "The requested documentation page does not exist. Try one of these entry points:",
125
+ "",
126
+ ...links.map((link) => `- ${link}`),
127
+ ""
128
+ ].join("\n");
129
+ };
130
+ }
131
+ });
132
+
56
133
  // src/vite/llms.ts
57
134
  var llms_exports = {};
58
135
  __export(llms_exports, {
59
136
  generateLlmsTxtFiles: () => generateLlmsTxtFiles
60
137
  });
61
138
  import { writeFile as writeFile5 } from "node:fs/promises";
62
- import path21 from "node:path";
139
+ import path22 from "node:path";
63
140
  import colors6 from "picocolors";
64
141
  async function generateLlmsTxtFiles({
65
142
  markdownFileInfos,
@@ -69,32 +146,44 @@ async function generateLlmsTxtFiles({
69
146
  siteName,
70
147
  llmsTxt,
71
148
  llmsTxtFull,
149
+ title: configuredTitle,
150
+ description: configuredDescription,
151
+ instructions,
72
152
  redirectUrls
73
153
  }) {
74
154
  const nonRedirectUrls = outputUrls.filter((url) => !redirectUrls.has(url));
75
155
  const baseUrl = basePath ?? "";
76
- const title = siteName ?? "Documentation";
156
+ const title = toSingleLine(configuredTitle ?? siteName ?? DEFAULT_TITLE);
157
+ const description = toSingleLine(
158
+ configuredDescription ?? DEFAULT_DESCRIPTION
159
+ );
160
+ const formattedInstructions = instructions ? formatInstructions(instructions) : void 0;
77
161
  const markdownMap = new Map(
78
162
  markdownFileInfos.map((info) => [info.routePath, info])
79
163
  );
80
164
  if (llmsTxt) {
81
- const llmsTxtParts = [];
82
- llmsTxtParts.push(`# ${title}
83
- `);
84
- llmsTxtParts.push("> Documentation files for Large Language Models\n");
85
- llmsTxtParts.push("## Documentation\n");
165
+ const documentationLinks = [];
86
166
  for (const url of nonRedirectUrls) {
87
167
  if (/(400|404|500)$/.test(url)) continue;
88
168
  const mdInfo = markdownMap.get(url);
89
169
  if (mdInfo) {
90
- const mdUrl = joinUrl(baseUrl, `${url}.md`);
91
- const linkTitle = mdInfo.title ?? url;
92
- const description = mdInfo.description ? `: ${mdInfo.description}` : "";
93
- llmsTxtParts.push(`- [${linkTitle}](${mdUrl})${description}`);
170
+ const mdUrl = getMarkdownRepresentationPath(url, basePath);
171
+ const linkTitle = escapeMarkdownLinkLabel(mdInfo.title ?? url);
172
+ const linkDescription = mdInfo.description ? `: ${toSingleLine(mdInfo.description)}` : "";
173
+ documentationLinks.push(`- [${linkTitle}](${mdUrl})${linkDescription}`);
94
174
  }
95
175
  }
96
- const llmsTxt2 = llmsTxtParts.join("\n");
97
- await writeFile5(path21.join(baseOutputDir, "llms.txt"), llmsTxt2, "utf-8");
176
+ const llmsTxtParts = [
177
+ `# ${title}`,
178
+ `> ${description}`,
179
+ ...formattedInstructions ? [formattedInstructions] : [],
180
+ ...documentationLinks.length > 0 ? ["## Documentation", documentationLinks.join("\n")] : []
181
+ ];
182
+ await writeFile5(
183
+ path22.join(baseOutputDir, "llms.txt"),
184
+ llmsTxtParts.join("\n\n"),
185
+ "utf-8"
186
+ );
98
187
  console.log(colors6.blue("\u2713 generated llms.txt"));
99
188
  }
100
189
  if (llmsTxtFull) {
@@ -112,24 +201,35 @@ async function generateLlmsTxtFiles({
112
201
  llmsFullParts.push(`${info.description}
113
202
  `);
114
203
  }
115
- llmsFullParts.push(`URL: ${joinUrl(baseUrl, info.routePath)}
116
- `);
204
+ llmsFullParts.push(
205
+ `URL: ${joinUrl(baseUrl, encodeDocumentationRoutePath(info.routePath))}
206
+ `
207
+ );
117
208
  llmsFullParts.push(`
118
209
  ${info.content}
119
210
  `);
120
211
  }
121
212
  const llmsFull = llmsFullParts.join("\n");
122
213
  await writeFile5(
123
- path21.join(baseOutputDir, "llms-full.txt"),
214
+ path22.join(baseOutputDir, "llms-full.txt"),
124
215
  llmsFull,
125
216
  "utf-8"
126
217
  );
127
218
  console.log(colors6.blue("\u2713 generated llms-full.txt"));
128
219
  }
129
220
  }
221
+ var DEFAULT_TITLE, DEFAULT_DESCRIPTION, toSingleLine, escapeMarkdownLinkLabel, formatInstructions;
130
222
  var init_llms = __esm({
131
223
  "src/vite/llms.ts"() {
132
224
  init_joinUrl();
225
+ init_markdown_representation();
226
+ DEFAULT_TITLE = "Documentation";
227
+ DEFAULT_DESCRIPTION = "Documentation files for Large Language Models";
228
+ toSingleLine = (value) => value.replaceAll(/\s+/g, " ").trim();
229
+ escapeMarkdownLinkLabel = (value) => toSingleLine(value).replaceAll(/([\\[\]])/g, "\\$1");
230
+ formatInstructions = (value) => value.trim().replaceAll("\r\n", "\n").split("\n").map(
231
+ (line) => line.replace(/^( {0,3})(#{1,6})(?=\s|$)/, "$1\\$2").replace(/^( {0,3})([=-]{2,})\s*$/, "$1\\$2")
232
+ ).join("\n");
133
233
  }
134
234
  });
135
235
 
@@ -138,12 +238,12 @@ import { hideBin } from "yargs/helpers";
138
238
  import yargs from "yargs/yargs";
139
239
 
140
240
  // src/cli/build/handler.ts
141
- import path26 from "node:path";
241
+ import path27 from "node:path";
142
242
 
143
243
  // src/vite/build.ts
144
- import { existsSync as existsSync2 } from "node:fs";
145
- import { mkdir as mkdir6, readFile as readFile5, rename as rename2, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
146
- import path24 from "node:path";
244
+ import { existsSync as existsSync3 } from "node:fs";
245
+ import { mkdir as mkdir6, readFile as readFile5, rename as rename2, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
246
+ import path25 from "node:path";
147
247
  import { build as esbuild } from "esbuild";
148
248
  import { createBuilder } from "vite";
149
249
 
@@ -320,7 +420,7 @@ var ZudokuError = class extends Error {
320
420
 
321
421
  // src/config/file-exists.ts
322
422
  import { stat } from "node:fs/promises";
323
- var fileExists = (path31) => stat(path31).then(() => true).catch(() => false);
423
+ var fileExists = (path32) => stat(path32).then(() => true).catch(() => false);
324
424
 
325
425
  // src/config/plugin-versions.ts
326
426
  import { readFile } from "node:fs/promises";
@@ -3067,8 +3167,8 @@ import { createNanoEvents } from "nanoevents";
3067
3167
 
3068
3168
  // src/lib/util/url.ts
3069
3169
  import { matchPath } from "react-router";
3070
- var matchesProtectedPattern = (pattern, path31) => matchPath({ path: pattern, end: true }, path31) != null;
3071
- var matchesAnyProtectedPattern = (patterns, path31) => patterns.some((p) => matchesProtectedPattern(p, path31));
3170
+ var matchesProtectedPattern = (pattern, path32) => matchPath({ path: pattern, end: true }, path32) != null;
3171
+ var matchesAnyProtectedPattern = (patterns, path32) => patterns.some((p) => matchesProtectedPattern(p, path32));
3072
3172
 
3073
3173
  // src/lib/core/ZudokuContext.ts
3074
3174
  var normalizeProtectedRoutes = (val) => {
@@ -3170,6 +3270,16 @@ var ApiConfigSchema = z7.object({
3170
3270
  categories: z7.array(ApiCatalogCategorySchema),
3171
3271
  options: ApiOptionsSchema
3172
3272
  }).partial();
3273
+ var OpenApiPublicationSchema = z7.object({
3274
+ path: z7.string().regex(
3275
+ /^\/(?!\/)(?:[A-Za-z0-9._~-]+\/)*[A-Za-z0-9._~-]+\.(?:json|ya?ml)$/i,
3276
+ "OpenAPI publication path must be a root-relative .json, .yaml, or .yml path without traversal, query, or fragment components"
3277
+ ).refine(
3278
+ (value) => value.split("/").every((segment) => segment !== "." && segment !== ".."),
3279
+ "OpenAPI publication path must not contain traversal segments"
3280
+ ),
3281
+ agentQuality: z7.boolean().optional()
3282
+ });
3173
3283
  var VersionConfigSchema = z7.object({
3174
3284
  path: z7.string(),
3175
3285
  input: z7.string(),
@@ -3187,6 +3297,7 @@ var ApiSchema = z7.discriminatedUnion("type", [
3187
3297
  z7.string(),
3188
3298
  z7.array(z7.union([z7.string(), VersionConfigSchema]))
3189
3299
  ]),
3300
+ publish: OpenApiPublicationSchema.optional(),
3190
3301
  ...ApiConfigSchema.shape
3191
3302
  }),
3192
3303
  z7.object({
@@ -3301,6 +3412,13 @@ var LlmsConfigSchema = z7.object({
3301
3412
  ),
3302
3413
  includeProtected: z7.boolean().default(false).describe(
3303
3414
  "When enabled, includes content from protected routes in the generated .md files and llms.txt files. By default, protected routes are excluded."
3415
+ ),
3416
+ title: z7.string().trim().min(1).optional().describe(
3417
+ "The project or site name rendered as the llms.txt H1. Defaults to the configured site title."
3418
+ ),
3419
+ description: z7.string().trim().min(1).optional().describe("A short project summary rendered as the llms.txt blockquote."),
3420
+ instructions: z7.string().trim().min(1).optional().describe(
3421
+ "Guidance for agents, such as when to use the documentation and how to interpret it. Rendered as introductory prose before the link sections."
3304
3422
  )
3305
3423
  }).partial().prefault({});
3306
3424
  var DocsConfigSchema = z7.object({
@@ -3308,6 +3426,9 @@ var DocsConfigSchema = z7.object({
3308
3426
  publishMarkdown: z7.boolean().default(true).describe(
3309
3427
  "When enabled, generates .md files for each document during build. Access documents at their URL path with .md extension (e.g., /foo/hello.md). Markdown files are generated without frontmatter."
3310
3428
  ),
3429
+ contentNegotiation: z7.boolean().optional().describe(
3430
+ "When enabled, serves generated Markdown at canonical document URLs when Accept prefers text/markdown and adds Vary: Accept. Defaults to the value of publishMarkdown."
3431
+ ),
3311
3432
  defaultOptions: z7.object({
3312
3433
  toc: z7.boolean(),
3313
3434
  copyPage: z7.boolean().optional(),
@@ -3321,7 +3442,10 @@ var DocsConfigSchema = z7.object({
3321
3442
  }).optional()
3322
3443
  }).partial().optional(),
3323
3444
  llms: LlmsConfigSchema
3324
- }).prefault({});
3445
+ }).transform((docs) => ({
3446
+ ...docs,
3447
+ contentNegotiation: docs.publishMarkdown && (docs.contentNegotiation ?? true)
3448
+ })).prefault({});
3325
3449
  var Redirect = z7.object({
3326
3450
  from: z7.string(),
3327
3451
  to: z7.string()
@@ -3956,7 +4080,7 @@ var getIssuer = async (config) => {
3956
4080
  init_joinUrl();
3957
4081
 
3958
4082
  // src/vite/config.ts
3959
- import path17 from "node:path";
4083
+ import path18 from "node:path";
3960
4084
  import dotenv from "dotenv";
3961
4085
  import colors4 from "picocolors";
3962
4086
  import {
@@ -4175,8 +4299,8 @@ var viteApiKeysPlugin = () => {
4175
4299
  var plugin_api_keys_default = viteApiKeysPlugin;
4176
4300
 
4177
4301
  // src/vite/plugin-api.ts
4178
- import fs2 from "node:fs/promises";
4179
- import path12 from "node:path";
4302
+ import fs3 from "node:fs/promises";
4303
+ import path13 from "node:path";
4180
4304
  import { deepEqual as deepEqual2 } from "fast-equals";
4181
4305
  import { runnerImport as runnerImport3 } from "vite";
4182
4306
  import { parse as parseYaml } from "yaml";
@@ -4328,14 +4452,14 @@ var flattenAllOf = (schema2) => {
4328
4452
  };
4329
4453
 
4330
4454
  // src/lib/util/traverse.ts
4331
- var traverse = (specification, transform, path31 = []) => {
4332
- const transformed = transform(specification, path31);
4455
+ var traverse = (specification, transform, path32 = []) => {
4456
+ const transformed = transform(specification, path32);
4333
4457
  if (typeof transformed !== "object" || transformed === null) {
4334
4458
  return transformed;
4335
4459
  }
4336
4460
  const result = Array.isArray(transformed) ? [] : {};
4337
4461
  for (const [key, value] of Object.entries(transformed)) {
4338
- const currentPath = [...path31, key];
4462
+ const currentPath = [...path32, key];
4339
4463
  if (Array.isArray(value)) {
4340
4464
  result[key] = value.map(
4341
4465
  (item, index) => typeof item === "object" && item != null ? traverse(item, transform, [...currentPath, index.toString()]) : item
@@ -4363,9 +4487,9 @@ var resolveLocalRef = (schema2, ref) => {
4363
4487
  if (schemaCache?.has(ref)) {
4364
4488
  return schemaCache.get(ref);
4365
4489
  }
4366
- const path31 = ref.split("/").slice(1);
4490
+ const path32 = ref.split("/").slice(1);
4367
4491
  let current = schema2;
4368
- for (const segment of path31) {
4492
+ for (const segment of path32) {
4369
4493
  if (!current || typeof current !== "object") {
4370
4494
  current = null;
4371
4495
  }
@@ -4384,7 +4508,7 @@ var dereference = async (schema2, resolvers = []) => {
4384
4508
  }
4385
4509
  const cloned = structuredClone(schema2);
4386
4510
  const visited = /* @__PURE__ */ new Set();
4387
- const resolve = async (current, path31) => {
4511
+ const resolve = async (current, path32) => {
4388
4512
  if (isIndexableObject(current)) {
4389
4513
  if (visited.has(current)) {
4390
4514
  return CIRCULAR_REF;
@@ -4392,7 +4516,7 @@ var dereference = async (schema2, resolvers = []) => {
4392
4516
  visited.add(current);
4393
4517
  if (Array.isArray(current)) {
4394
4518
  for (let index = 0; index < current.length; index++) {
4395
- current[index] = await resolve(current[index], `${path31}/${index}`);
4519
+ current[index] = await resolve(current[index], `${path32}/${index}`);
4396
4520
  }
4397
4521
  } else {
4398
4522
  if ("$ref" in current && typeof current.$ref === "string") {
@@ -4403,13 +4527,13 @@ var dereference = async (schema2, resolvers = []) => {
4403
4527
  for (const resolver of resolvers) {
4404
4528
  const resolved = await resolver($ref);
4405
4529
  if (resolved) {
4406
- result2 = await resolve(resolved, path31);
4530
+ result2 = await resolve(resolved, path32);
4407
4531
  break;
4408
4532
  }
4409
4533
  }
4410
4534
  if (result2 === void 0) {
4411
4535
  const resolved = await resolveLocalRef(cloned, $ref);
4412
- result2 = await resolve(resolved, path31);
4536
+ result2 = await resolve(resolved, path32);
4413
4537
  }
4414
4538
  if (hasSiblings) {
4415
4539
  if (result2 === CIRCULAR_REF) {
@@ -4422,7 +4546,7 @@ var dereference = async (schema2, resolvers = []) => {
4422
4546
  return result2;
4423
4547
  }
4424
4548
  for (const key in current) {
4425
- current[key] = await resolve(current[key], `${path31}/${key}`);
4549
+ current[key] = await resolve(current[key], `${path32}/${key}`);
4426
4550
  }
4427
4551
  }
4428
4552
  visited.delete(current);
@@ -4461,9 +4585,9 @@ var upgradeSchema = (schema2) => {
4461
4585
  }
4462
4586
  return sub;
4463
4587
  });
4464
- schema2 = traverse(schema2, (sub, path31) => {
4588
+ schema2 = traverse(schema2, (sub, path32) => {
4465
4589
  if (sub.example !== void 0) {
4466
- if (isSchemaPath(path31 ?? [])) {
4590
+ if (isSchemaPath(path32 ?? [])) {
4467
4591
  sub.examples = [sub.example];
4468
4592
  } else {
4469
4593
  sub.examples = {
@@ -4476,11 +4600,11 @@ var upgradeSchema = (schema2) => {
4476
4600
  }
4477
4601
  return sub;
4478
4602
  });
4479
- schema2 = traverse(schema2, (schema3, path31) => {
4603
+ schema2 = traverse(schema2, (schema3, path32) => {
4480
4604
  if (schema3.type === "object" && schema3.properties !== void 0) {
4481
- const parentPath = path31?.slice(0, -1);
4605
+ const parentPath = path32?.slice(0, -1);
4482
4606
  const isMultipart = parentPath?.some((segment, index) => {
4483
- return segment === "content" && path31?.[index + 1] === "multipart/form-data";
4607
+ return segment === "content" && path32?.[index + 1] === "multipart/form-data";
4484
4608
  });
4485
4609
  if (isMultipart) {
4486
4610
  const entries = Object.entries(schema3.properties);
@@ -4494,8 +4618,8 @@ var upgradeSchema = (schema2) => {
4494
4618
  }
4495
4619
  return schema3;
4496
4620
  });
4497
- schema2 = traverse(schema2, (schema3, path31) => {
4498
- if (path31?.includes("content") && path31.includes("application/octet-stream")) {
4621
+ schema2 = traverse(schema2, (schema3, path32) => {
4622
+ if (path32?.includes("content") && path32.includes("application/octet-stream")) {
4499
4623
  return {};
4500
4624
  }
4501
4625
  if (schema3.type === "string" && schema3.format === "binary") {
@@ -4521,11 +4645,11 @@ var upgradeSchema = (schema2) => {
4521
4645
  }
4522
4646
  return sub;
4523
4647
  });
4524
- schema2 = traverse(schema2, (schema3, path31) => {
4648
+ schema2 = traverse(schema2, (schema3, path32) => {
4525
4649
  if (schema3.type === "string" && schema3.format === "byte") {
4526
- const parentPath = path31?.slice(0, -1);
4650
+ const parentPath = path32?.slice(0, -1);
4527
4651
  const contentMediaType = parentPath?.find(
4528
- (_, index) => path31?.[index - 1] === "content"
4652
+ (_, index) => path32?.[index - 1] === "content"
4529
4653
  );
4530
4654
  return {
4531
4655
  type: "string",
@@ -4537,7 +4661,7 @@ var upgradeSchema = (schema2) => {
4537
4661
  });
4538
4662
  return schema2;
4539
4663
  };
4540
- function isSchemaPath(path31) {
4664
+ function isSchemaPath(path32) {
4541
4665
  const schemaLocations = [
4542
4666
  ["components", "schemas"],
4543
4667
  "properties",
@@ -4550,10 +4674,10 @@ function isSchemaPath(path31) {
4550
4674
  ];
4551
4675
  return schemaLocations.some((location) => {
4552
4676
  if (Array.isArray(location)) {
4553
- return location.every((segment, index) => path31[index] === segment);
4677
+ return location.every((segment, index) => path32[index] === segment);
4554
4678
  }
4555
- return path31.includes(location);
4556
- }) || path31.includes("schema") || path31.some((segment) => segment.endsWith("Schema"));
4679
+ return path32.includes(location);
4680
+ }) || path32.includes("schema") || path32.some((segment) => segment.endsWith("Schema"));
4557
4681
  }
4558
4682
 
4559
4683
  // src/lib/oas/parser/index.ts
@@ -4635,13 +4759,13 @@ var OPENAPI_PROPS = /* @__PURE__ */ new Set([
4635
4759
  "anyOf",
4636
4760
  "oneOf"
4637
4761
  ]);
4638
- var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs = /* @__PURE__ */ new WeakMap(), path31 = [], currentRefPaths = /* @__PURE__ */ new Set()) => {
4762
+ var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs = /* @__PURE__ */ new WeakMap(), path32 = [], currentRefPaths = /* @__PURE__ */ new Set()) => {
4639
4763
  if (obj === null || typeof obj !== "object") return obj;
4640
4764
  const refPath = obj.__$ref;
4641
4765
  const isCircular = currentPath.has(obj) || typeof refPath === "string" && currentRefPaths.has(refPath);
4642
4766
  if (isCircular) {
4643
4767
  if (typeof refPath === "string") return SCHEMA_REF_PREFIX + refPath;
4644
- const circularProp = path31.find((p) => !OPENAPI_PROPS.has(p)) || path31[0];
4768
+ const circularProp = path32.find((p) => !OPENAPI_PROPS.has(p)) || path32[0];
4645
4769
  return [CIRCULAR_REF, circularProp].filter(Boolean).join(":");
4646
4770
  }
4647
4771
  if (refs.has(obj)) return refs.get(obj);
@@ -4651,7 +4775,7 @@ var handleCircularRefs = (obj, currentPath = /* @__PURE__ */ new WeakSet(), refs
4651
4775
  value,
4652
4776
  currentPath,
4653
4777
  refs,
4654
- [...path31, key],
4778
+ [...path32, key],
4655
4779
  currentRefPaths
4656
4780
  );
4657
4781
  const result = Array.isArray(obj) ? obj.map((item, i) => recurse(item, i.toString())) : Object.fromEntries(
@@ -4689,7 +4813,7 @@ var resolveExtensions = (obj) => Object.fromEntries(
4689
4813
  var getAllTags = (schema2) => {
4690
4814
  const rootTags = schema2.tags ?? [];
4691
4815
  const operations = Object.values(schema2.paths ?? {}).flatMap(
4692
- (path31) => HttpMethods.map((k) => path31?.[k]).filter((op) => op != null)
4816
+ (path32) => HttpMethods.map((k) => path32?.[k]).filter((op) => op != null)
4693
4817
  );
4694
4818
  const operationTags = new Set(operations.flatMap((op) => op.tags ?? []));
4695
4819
  const hasUntaggedOperations = operations.some(
@@ -4744,7 +4868,7 @@ var getAllSlugs = (ops, schemaTags = []) => {
4744
4868
  var getOperationSlugKey = (op) => [op.path, op.method, op.operationId, op.summary].filter(Boolean).join("-");
4745
4869
  var getAllOperations = (paths) => {
4746
4870
  const operations = Object.entries(paths ?? {}).flatMap(
4747
- ([path31, value]) => HttpMethods.flatMap((method) => {
4871
+ ([path32, value]) => HttpMethods.flatMap((method) => {
4748
4872
  if (!value?.[method]) return [];
4749
4873
  const operation = value[method];
4750
4874
  const pathParameters = value.parameters ?? [];
@@ -4764,7 +4888,7 @@ var getAllOperations = (paths) => {
4764
4888
  return {
4765
4889
  ...operation,
4766
4890
  method,
4767
- path: path31,
4891
+ path: path32,
4768
4892
  parameters,
4769
4893
  servers,
4770
4894
  tags: operation.tags ?? []
@@ -5267,8 +5391,8 @@ var Schema = builder.objectRef("Schema").implement({
5267
5391
  }),
5268
5392
  paths: t.field({
5269
5393
  type: [PathItem],
5270
- resolve: (root) => Object.entries(root.paths ?? {}).map(([path31, value]) => ({
5271
- path: path31,
5394
+ resolve: (root) => Object.entries(root.paths ?? {}).map(([path32, value]) => ({
5395
+ path: path32,
5272
5396
  // biome-ignore lint/style/noNonNullAssertion: value is guaranteed to be defined
5273
5397
  methods: Object.keys(value)
5274
5398
  }))
@@ -5398,9 +5522,97 @@ var countMcpServers = (schema2) => operationsOf(schema2).filter(
5398
5522
  // src/lib/util/ensureArray.ts
5399
5523
  var ensureArray = (value) => Array.isArray(value) ? value : [value];
5400
5524
 
5401
- // src/vite/api/SchemaManager.ts
5525
+ // src/vite/api/openapi-publication.ts
5402
5526
  import fs from "node:fs/promises";
5403
5527
  import path8 from "node:path";
5528
+ import { stringify as stringifyYaml } from "yaml";
5529
+ var getExtension = (urlPath) => path8.posix.extname(urlPath).toLowerCase();
5530
+ var getOpenApiMediaType = (urlPath) => getExtension(urlPath) === ".json" ? "application/json" : "application/yaml";
5531
+ var createOpenApiPublication = ({
5532
+ apiPath,
5533
+ urlPath,
5534
+ schema: schema2
5535
+ }) => {
5536
+ const mediaType = getOpenApiMediaType(urlPath);
5537
+ const content = mediaType === "application/json" ? `${JSON.stringify(schema2, null, 2)}
5538
+ ` : stringifyYaml(schema2);
5539
+ return { apiPath, urlPath, content, mediaType };
5540
+ };
5541
+ var getRequestPathname = (requestUrl) => {
5542
+ try {
5543
+ return new URL(requestUrl, "http://zudoku.local").pathname;
5544
+ } catch {
5545
+ return void 0;
5546
+ }
5547
+ };
5548
+ var findOpenApiPublication = (requestUrl, publications) => {
5549
+ const pathname = getRequestPathname(requestUrl);
5550
+ return publications.find((publication) => publication.urlPath === pathname);
5551
+ };
5552
+ var createOpenApiDevMiddleware = ({
5553
+ getPublications,
5554
+ getDownloadPathMap
5555
+ }) => async (req, res, next) => {
5556
+ if (!req.url || req.method !== "GET" && req.method !== "HEAD") {
5557
+ return next();
5558
+ }
5559
+ const publication = findOpenApiPublication(req.url, getPublications());
5560
+ if (publication) {
5561
+ res.setHeader("Content-Type", `${publication.mediaType}; charset=utf-8`);
5562
+ return res.end(req.method === "HEAD" ? void 0 : publication.content);
5563
+ }
5564
+ const requestPathname = getRequestPathname(req.url);
5565
+ if (!requestPathname) return next();
5566
+ if (!requestPathname.toLowerCase().endsWith(".json") && !requestPathname.toLowerCase().endsWith(".yaml") && !requestPathname.toLowerCase().endsWith(".yml")) {
5567
+ return next();
5568
+ }
5569
+ const inputPath = getDownloadPathMap().get(requestPathname);
5570
+ if (!inputPath) return next();
5571
+ const content = await fs.readFile(inputPath, "utf-8");
5572
+ const mediaType = getOpenApiMediaType(inputPath);
5573
+ res.setHeader("Content-Type", `${mediaType}; charset=utf-8`);
5574
+ return res.end(req.method === "HEAD" ? void 0 : content);
5575
+ };
5576
+ var resolveOutputPath = (outputDir, urlPath) => {
5577
+ const outputRoot = path8.resolve(outputDir);
5578
+ const relativePath = urlPath.replace(/^\/+/, "");
5579
+ const outputPath = path8.resolve(outputRoot, relativePath);
5580
+ if (!relativePath || relativePath.includes("\\") || relativePath.split("/").some((segment) => segment === "..") || !outputPath.startsWith(`${outputRoot}${path8.sep}`) && outputPath !== outputRoot) {
5581
+ throw new Error(`Unsafe OpenAPI publication output path: ${urlPath}`);
5582
+ }
5583
+ return outputPath;
5584
+ };
5585
+ var writeOpenApiPublications = async (outputDir, publications) => {
5586
+ const outputs = publications.map((publication) => ({
5587
+ publication,
5588
+ outputPath: resolveOutputPath(outputDir, publication.urlPath)
5589
+ }));
5590
+ await Promise.all(
5591
+ outputs.map(async ({ publication, outputPath }) => {
5592
+ try {
5593
+ await fs.lstat(outputPath);
5594
+ } catch (error) {
5595
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
5596
+ return;
5597
+ }
5598
+ throw error;
5599
+ }
5600
+ throw new Error(
5601
+ `Cannot publish OpenAPI for API "${publication.apiPath}" at "${publication.urlPath}" because a build artifact already exists at "${outputPath}". Choose another publish.path or remove the conflicting public/build artifact.`
5602
+ );
5603
+ })
5604
+ );
5605
+ await Promise.all(
5606
+ outputs.map(async ({ publication, outputPath }) => {
5607
+ await fs.mkdir(path8.dirname(outputPath), { recursive: true });
5608
+ await fs.writeFile(outputPath, publication.content, "utf-8");
5609
+ })
5610
+ );
5611
+ };
5612
+
5613
+ // src/vite/api/SchemaManager.ts
5614
+ import fs2 from "node:fs/promises";
5615
+ import path9 from "node:path";
5404
5616
  import { $RefParser as $RefParser2 } from "@apidevtools/json-schema-ref-parser";
5405
5617
  import { upgrade } from "@scalar/openapi-parser";
5406
5618
  import { deepEqual } from "fast-equals";
@@ -5448,9 +5660,179 @@ var flattenAllOfProcessor = async ({ schema: schema2, file }) => {
5448
5660
  // src/vite/api/SchemaManager.ts
5449
5661
  init_joinUrl();
5450
5662
 
5663
+ // src/vite/api/agent-quality.ts
5664
+ var HTTP_METHODS2 = [
5665
+ "get",
5666
+ "put",
5667
+ "post",
5668
+ "delete",
5669
+ "options",
5670
+ "head",
5671
+ "patch",
5672
+ "trace"
5673
+ ];
5674
+ var REQUEST_BODY_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch"]);
5675
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5676
+ var resolveLocalRef2 = (document, value) => {
5677
+ if (!isObject(value) || typeof value.$ref !== "string") return value;
5678
+ if (!value.$ref.startsWith("#/")) return void 0;
5679
+ return value.$ref.slice(2).split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~")).reduce((current, part) => {
5680
+ if (!isObject(current)) return void 0;
5681
+ return current[part];
5682
+ }, document);
5683
+ };
5684
+ var schemaIsTyped = (document, schema2) => {
5685
+ const resolved = resolveLocalRef2(document, schema2);
5686
+ if (typeof resolved === "boolean") return true;
5687
+ if (!isObject(resolved)) return false;
5688
+ return typeof resolved.type === "string" || Array.isArray(resolved.type) || "enum" in resolved || "const" in resolved || "properties" in resolved || "items" in resolved || "oneOf" in resolved || "anyOf" in resolved || "allOf" in resolved;
5689
+ };
5690
+ var contentHasTypedSchema = (document, content) => isObject(content) && Object.values(content).some(
5691
+ (mediaType) => isObject(mediaType) && schemaIsTyped(document, mediaType.schema)
5692
+ );
5693
+ var parameterIsTyped = (document, parameter) => {
5694
+ const resolved = resolveLocalRef2(document, parameter);
5695
+ return isObject(resolved) && (schemaIsTyped(document, resolved.schema) || contentHasTypedSchema(document, resolved.content));
5696
+ };
5697
+ var responseHasTypedSchema = (document, response) => {
5698
+ const resolved = resolveLocalRef2(document, response);
5699
+ return isObject(resolved) && contentHasTypedSchema(document, resolved.content);
5700
+ };
5701
+ var requestBodyHasTypedSchema = (document, requestBody) => {
5702
+ const resolved = resolveLocalRef2(document, requestBody);
5703
+ return isObject(resolved) && contentHasTypedSchema(document, resolved.content);
5704
+ };
5705
+ var getParameterName = (document, parameter) => {
5706
+ const resolved = resolveLocalRef2(document, parameter);
5707
+ return isObject(resolved) && typeof resolved.name === "string" ? resolved.name : "<unknown>";
5708
+ };
5709
+ var getParameterIdentity = (document, parameter) => {
5710
+ const resolved = resolveLocalRef2(document, parameter);
5711
+ if (!isObject(resolved) || typeof resolved.name !== "string" || typeof resolved.in !== "string") {
5712
+ return void 0;
5713
+ }
5714
+ return JSON.stringify([resolved.name, resolved.in]);
5715
+ };
5716
+ var mergeParameters = (document, pathParameters, operationParameters) => {
5717
+ const merged = [];
5718
+ const indexes = /* @__PURE__ */ new Map();
5719
+ for (const parameter of [
5720
+ ...Array.isArray(pathParameters) ? pathParameters : [],
5721
+ ...Array.isArray(operationParameters) ? operationParameters : []
5722
+ ]) {
5723
+ const identity = getParameterIdentity(document, parameter);
5724
+ if (identity === void 0) {
5725
+ merged.push(parameter);
5726
+ continue;
5727
+ }
5728
+ const existingIndex = indexes.get(identity);
5729
+ if (existingIndex === void 0) {
5730
+ indexes.set(identity, merged.length);
5731
+ merged.push(parameter);
5732
+ } else {
5733
+ merged[existingIndex] = parameter;
5734
+ }
5735
+ }
5736
+ return merged;
5737
+ };
5738
+ var responseCannotHaveContent = (method, status) => method === "head" || /^1(?:\d{2}|xx)$/i.test(status) || ["204", "205", "304"].includes(status);
5739
+ var auditOpenApiAgentQuality = (document) => {
5740
+ const issues = [];
5741
+ const operationIds = /* @__PURE__ */ new Map();
5742
+ if (!isObject(document.paths)) return issues;
5743
+ for (const [route, rawPathItem] of Object.entries(document.paths)) {
5744
+ const pathItem = resolveLocalRef2(document, rawPathItem);
5745
+ if (!isObject(pathItem)) continue;
5746
+ for (const method of HTTP_METHODS2) {
5747
+ const operation = pathItem[method];
5748
+ if (!isObject(operation)) continue;
5749
+ const location = `${method.toUpperCase()} ${route}`;
5750
+ if (typeof operation.operationId !== "string" || operation.operationId.trim().length === 0) {
5751
+ issues.push({
5752
+ code: "missing-operation-id",
5753
+ location,
5754
+ message: "Operation is missing a unique operationId."
5755
+ });
5756
+ } else {
5757
+ const locations = operationIds.get(operation.operationId) ?? [];
5758
+ locations.push(location);
5759
+ operationIds.set(operation.operationId, locations);
5760
+ }
5761
+ if (typeof operation.description !== "string" || operation.description.trim().length === 0) {
5762
+ issues.push({
5763
+ code: "missing-description",
5764
+ location,
5765
+ message: "Operation is missing a description."
5766
+ });
5767
+ }
5768
+ const parameters = mergeParameters(
5769
+ document,
5770
+ pathItem.parameters,
5771
+ operation.parameters
5772
+ );
5773
+ for (const parameter of parameters) {
5774
+ if (parameterIsTyped(document, parameter)) continue;
5775
+ issues.push({
5776
+ code: "untyped-parameter",
5777
+ location,
5778
+ message: `Parameter "${getParameterName(document, parameter)}" is missing a typed schema.`
5779
+ });
5780
+ }
5781
+ if (operation.requestBody === void 0) {
5782
+ if (REQUEST_BODY_METHODS.has(method)) {
5783
+ issues.push({
5784
+ code: "missing-request-body",
5785
+ location,
5786
+ message: "Write operation is missing a requestBody schema."
5787
+ });
5788
+ }
5789
+ } else if (!requestBodyHasTypedSchema(document, operation.requestBody)) {
5790
+ issues.push({
5791
+ code: "untyped-request-body",
5792
+ location,
5793
+ message: "Request body is missing a typed schema."
5794
+ });
5795
+ }
5796
+ const responses = operation.responses;
5797
+ if (!isObject(responses) || Object.keys(responses).length === 0) {
5798
+ issues.push({
5799
+ code: "missing-response-schema",
5800
+ location,
5801
+ message: "Operation has no response schemas."
5802
+ });
5803
+ continue;
5804
+ }
5805
+ for (const [status, response] of Object.entries(responses)) {
5806
+ if (responseCannotHaveContent(method, status)) continue;
5807
+ if (responseHasTypedSchema(document, response)) continue;
5808
+ issues.push({
5809
+ code: "missing-response-schema",
5810
+ location,
5811
+ message: `Response ${status} is missing a typed schema.`
5812
+ });
5813
+ }
5814
+ }
5815
+ }
5816
+ for (const [operationId, locations] of operationIds) {
5817
+ if (locations.length < 2) continue;
5818
+ issues.push({
5819
+ code: "duplicate-operation-id",
5820
+ location: locations.join(", "),
5821
+ message: `operationId "${operationId}" is used by multiple operations.`
5822
+ });
5823
+ }
5824
+ return issues;
5825
+ };
5826
+ var formatAgentQualityReport = (apiPath, issues) => [
5827
+ `Agent-quality audit for API "${apiPath}" found ${issues.length} issue${issues.length === 1 ? "" : "s"}:`,
5828
+ ...issues.map(
5829
+ (issue) => `- [${issue.code}] ${issue.location}: ${issue.message}`
5830
+ )
5831
+ ].join("\n");
5832
+
5451
5833
  // src/vite/api/schema-codegen.ts
5452
5834
  var unescapeJsonPointer = (uri) => decodeURIComponent(uri.replace(/~1/g, "/").replace(/~0/g, "~"));
5453
- var getSegmentsFromPath = (path31) => path31.split("/").slice(1).map(unescapeJsonPointer);
5835
+ var getSegmentsFromPath = (path32) => path32.split("/").slice(1).map(unescapeJsonPointer);
5454
5836
  var createLocalRefMap = (obj) => {
5455
5837
  const refMap = /* @__PURE__ */ new Map();
5456
5838
  const siblingsMap = /* @__PURE__ */ new Map();
@@ -5481,16 +5863,16 @@ var replaceMarkers = (code, mergedRefs) => code.replace(/"__refMap:(.*?)"/g, '__
5481
5863
  /"__refMap\+Siblings:(.*?)"/g,
5482
5864
  (_, key) => mergedRefs.get(key) ?? `__refMap["${key}"]`
5483
5865
  );
5484
- var lookup = (schema2, path31, filePath) => {
5485
- const parts = getSegmentsFromPath(path31);
5866
+ var lookup = (schema2, path32, filePath) => {
5867
+ const parts = getSegmentsFromPath(path32);
5486
5868
  let val = schema2;
5487
5869
  for (const part of parts) {
5488
5870
  while (val.$ref?.startsWith("#/")) {
5489
- val = val.$ref === path31 ? val : lookup(schema2, val.$ref, filePath);
5871
+ val = val.$ref === path32 ? val : lookup(schema2, val.$ref, filePath);
5490
5872
  }
5491
5873
  if (val[part] === void 0) {
5492
5874
  throw new Error(
5493
- `Error in ${filePath ?? "code generation"}: Could not find path segment ${part} in path: ${path31}`
5875
+ `Error in ${filePath ?? "code generation"}: Could not find path segment ${part} in path: ${path32}`
5494
5876
  );
5495
5877
  }
5496
5878
  val = val[part];
@@ -5573,6 +5955,7 @@ var SchemaManager = class {
5573
5955
  processors;
5574
5956
  processedSchemas = {};
5575
5957
  referencedBy = /* @__PURE__ */ new Map();
5958
+ publishedSchemas;
5576
5959
  config;
5577
5960
  constructor({
5578
5961
  storeDir,
@@ -5594,18 +5977,18 @@ var SchemaManager = class {
5594
5977
  ];
5595
5978
  }
5596
5979
  getPathForFile = (input, params) => {
5597
- const filePath = path8.resolve(this.config.__meta.rootDir, input);
5980
+ const filePath = path9.resolve(this.config.__meta.rootDir, input);
5598
5981
  const apis = ensureArray(this.config.apis ?? []);
5599
5982
  for (const apiConfig of apis) {
5600
- if (!apiConfig || apiConfig.type !== "file" || !apiConfig.path) continue;
5983
+ if (apiConfig?.type !== "file" || !apiConfig.path) continue;
5601
5984
  const match = normalizeInputs(apiConfig.input).some(
5602
- (i) => path8.resolve(this.config.__meta.rootDir, i.input) === filePath && deepEqual(i.params, params)
5985
+ (i) => path9.resolve(this.config.__meta.rootDir, i.input) === filePath && deepEqual(i.params, params)
5603
5986
  );
5604
5987
  if (match) return apiConfig.path;
5605
5988
  }
5606
5989
  };
5607
5990
  processSchema = async (input) => {
5608
- const filePath = path8.resolve(this.config.__meta.rootDir, input.input);
5991
+ const filePath = path9.resolve(this.config.__meta.rootDir, input.input);
5609
5992
  const params = input.params;
5610
5993
  const configuredPath = this.getPathForFile(input.input, params);
5611
5994
  if (!configuredPath) {
@@ -5640,15 +6023,15 @@ var SchemaManager = class {
5640
6023
  const processedTime = Date.now();
5641
6024
  const code = generateCode(processedSchema, filePath);
5642
6025
  const prefixPath = slugify(configuredPath);
5643
- const processedFilePath = path8.posix.join(
6026
+ const processedFilePath = path9.posix.join(
5644
6027
  this.storeDir,
5645
- `${prefixPath}-${path8.basename(filePath)}${paramsSuffix(params)}.js`
6028
+ `${prefixPath}-${path9.basename(filePath)}${paramsSuffix(params)}.js`
5646
6029
  );
5647
6030
  const importKey = processedFilePath;
5648
- await fs.writeFile(processedFilePath, code);
6031
+ await fs2.writeFile(processedFilePath, code);
5649
6032
  const processedJsonPath = paramsSuffix(params) ? processedFilePath.replace(/\.js$/, ".json") : "";
5650
6033
  if (processedJsonPath) {
5651
- await fs.writeFile(
6034
+ await fs2.writeFile(
5652
6035
  processedJsonPath,
5653
6036
  JSON.stringify(processedSchema, null, 2)
5654
6037
  );
@@ -5664,7 +6047,7 @@ var SchemaManager = class {
5664
6047
  const schemaVersion = processedSchema.info.version ?? FALLBACK_VERSION;
5665
6048
  const versionPath = existingSchema?.path && existingSchema.path.length > 0 ? existingSchema.path : paramsPath(params) || schemaVersion;
5666
6049
  const config = ensureArray(this.config.apis ?? []).find(
5667
- (c) => c.path === configuredPath
6050
+ (c) => c.type === "file" && c.path === configuredPath
5668
6051
  );
5669
6052
  const processed = {
5670
6053
  schema: processedSchema,
@@ -5686,16 +6069,25 @@ var SchemaManager = class {
5686
6069
  };
5687
6070
  if (index > -1) {
5688
6071
  schemas[index] = processed;
6072
+ this.publishedSchemas = void 0;
5689
6073
  } else {
5690
6074
  throw new Error(
5691
6075
  `Schema with input path ${filePath} was not pre-initialized for ${configuredPath}.`
5692
6076
  );
5693
6077
  }
6078
+ if (index === 0 && config?.type === "file" && config.publish?.agentQuality) {
6079
+ const issues = auditOpenApiAgentQuality(processedSchema);
6080
+ if (issues.length > 0) {
6081
+ console.warn(
6082
+ `[zudoku] ${formatAgentQualityReport(configuredPath, issues)}`
6083
+ );
6084
+ }
6085
+ }
5694
6086
  return processed;
5695
6087
  };
5696
6088
  getAllTrackedFiles = () => Array.from(this.referencedBy.keys());
5697
6089
  getFilesToReprocess = (changedFile) => {
5698
- const resolvedPath = path8.resolve(this.config.__meta.rootDir, changedFile);
6090
+ const resolvedPath = path9.resolve(this.config.__meta.rootDir, changedFile);
5699
6091
  const referencedBy = this.referencedBy.get(resolvedPath);
5700
6092
  if (!referencedBy) return [];
5701
6093
  const filesToProcess = referencedBy.size === 0 ? [resolvedPath] : Array.from(referencedBy);
@@ -5714,6 +6106,7 @@ var SchemaManager = class {
5714
6106
  processAllSchemas = async () => {
5715
6107
  this.referencedBy.clear();
5716
6108
  this.processedSchemas = {};
6109
+ this.publishedSchemas = void 0;
5717
6110
  const apis = ensureArray(this.config.apis ?? []);
5718
6111
  for (const apiConfig of apis) {
5719
6112
  if (apiConfig.type !== "file" || !apiConfig.path) continue;
@@ -5724,7 +6117,7 @@ var SchemaManager = class {
5724
6117
  version: "",
5725
6118
  path: input.path ?? "",
5726
6119
  label: input.label,
5727
- inputPath: path8.resolve(this.config.__meta.rootDir, input.input),
6120
+ inputPath: path9.resolve(this.config.__meta.rootDir, input.input),
5728
6121
  params: input.params,
5729
6122
  importKey: "",
5730
6123
  downloadUrl: "",
@@ -5743,10 +6136,39 @@ var SchemaManager = class {
5743
6136
  );
5744
6137
  }
5745
6138
  }
6139
+ this.getPublishedSchemas();
5746
6140
  };
5747
- getLatestSchema = (path31) => this.processedSchemas[path31]?.at(0);
5748
- getSchemasForPath = (path31) => this.processedSchemas[path31];
6141
+ getLatestSchema = (path32) => this.processedSchemas[path32]?.at(0);
6142
+ getSchemasForPath = (path32) => this.processedSchemas[path32];
5749
6143
  getSchemaImports = () => Object.values(this.processedSchemas).flat().filter((s) => s.importKey);
6144
+ getPublishedSchemas = () => {
6145
+ if (this.publishedSchemas) return this.publishedSchemas;
6146
+ const publications = /* @__PURE__ */ new Map();
6147
+ for (const apiConfig of ensureArray(this.config.apis ?? [])) {
6148
+ if (apiConfig.type !== "file" || !apiConfig.path || !apiConfig.publish) {
6149
+ continue;
6150
+ }
6151
+ const primarySchema = this.getLatestSchema(apiConfig.path);
6152
+ if (!primarySchema) continue;
6153
+ const urlPath = joinUrl(this.config.basePath, apiConfig.publish.path);
6154
+ const existing = publications.get(urlPath);
6155
+ if (existing) {
6156
+ throw new Error(
6157
+ `OpenAPI publication path "${urlPath}" is configured by both "${existing.apiPath}" and "${apiConfig.path}". Configure a unique path for each published API.`
6158
+ );
6159
+ }
6160
+ publications.set(
6161
+ urlPath,
6162
+ createOpenApiPublication({
6163
+ apiPath: apiConfig.path,
6164
+ urlPath,
6165
+ schema: primarySchema.schema
6166
+ })
6167
+ );
6168
+ }
6169
+ this.publishedSchemas = Array.from(publications.values());
6170
+ return this.publishedSchemas;
6171
+ };
5750
6172
  getUrlToFilePathMap = () => {
5751
6173
  const map = /* @__PURE__ */ new Map();
5752
6174
  const apis = ensureArray(this.config.apis ?? []);
@@ -5767,7 +6189,7 @@ var SchemaManager = class {
5767
6189
  };
5768
6190
  createSchemaPath = (inputPath, versionPath, apiPath, params, config) => {
5769
6191
  const suffix = paramsSuffix(params);
5770
- const extension = suffix ? ".json" : path8.extname(inputPath);
6192
+ const extension = suffix ? ".json" : path9.extname(inputPath);
5771
6193
  const fileName = config?.schemaDownload?.fileName ?? this.config.defaults?.apis?.schemaDownload?.fileName ?? "schema";
5772
6194
  return joinUrl(
5773
6195
  this.config.basePath,
@@ -5779,7 +6201,7 @@ var SchemaManager = class {
5779
6201
  };
5780
6202
 
5781
6203
  // src/vite/plugin-config-reload.ts
5782
- import path11 from "node:path";
6204
+ import path12 from "node:path";
5783
6205
  import colors3 from "picocolors";
5784
6206
 
5785
6207
  // src/vite/plugin-navigation.ts
@@ -5787,7 +6209,7 @@ import { stringify as stringify3 } from "javascript-stringify";
5787
6209
  import { isElement } from "react-is";
5788
6210
 
5789
6211
  // src/config/validators/NavigationSchema.ts
5790
- import path9 from "node:path";
6212
+ import path10 from "node:path";
5791
6213
  import { glob } from "glob";
5792
6214
  import { fromMarkdown } from "mdast-util-from-markdown";
5793
6215
  import { mdxFromMarkdown } from "mdast-util-mdx";
@@ -5837,7 +6259,7 @@ var extractRichH1 = (content) => {
5837
6259
  }
5838
6260
  };
5839
6261
  var isNavigationItem = (item) => item !== void 0;
5840
- var toPosixPath = (filePath) => filePath.split(path9.win32.sep).join(path9.posix.sep);
6262
+ var toPosixPath = (filePath) => filePath.split(path10.win32.sep).join(path10.posix.sep);
5841
6263
  var NavigationResolver = class {
5842
6264
  rootDir;
5843
6265
  globPatterns;
@@ -5970,13 +6392,13 @@ var NavigationResolver = class {
5970
6392
 
5971
6393
  // src/vite/debug.ts
5972
6394
  import { mkdir, writeFile } from "node:fs/promises";
5973
- import path10 from "node:path";
6395
+ import path11 from "node:path";
5974
6396
  async function writePluginDebugCode(rootDir, pluginName, code, extension = "js") {
5975
6397
  if (process.env.ZUDOKU_BUILD_DEBUG) {
5976
- const debugDir = path10.join(rootDir, "dist", "debug");
6398
+ const debugDir = path11.join(rootDir, "dist", "debug");
5977
6399
  await mkdir(debugDir, { recursive: true });
5978
6400
  await writeFile(
5979
- path10.join(debugDir, `${pluginName}.${extension}`),
6401
+ path11.join(debugDir, `${pluginName}.${extension}`),
5980
6402
  typeof code === "string" ? code : code.join("\n")
5981
6403
  );
5982
6404
  }
@@ -6087,7 +6509,7 @@ var viteConfigReloadPlugin = () => ({
6087
6509
  });
6088
6510
  logger.info(
6089
6511
  colors3.blue(
6090
- `Config ${path11.basename(currentConfig.__meta.configPath)} changed. Reloading...`
6512
+ `Config ${path12.basename(currentConfig.__meta.configPath)} changed. Reloading...`
6091
6513
  ),
6092
6514
  { timestamp: true }
6093
6515
  );
@@ -6097,6 +6519,7 @@ var viteConfigReloadPlugin = () => ({
6097
6519
 
6098
6520
  // src/vite/plugin-api.ts
6099
6521
  var PROCESSED_STORE_SUBPATH = "node_modules/.zudoku/processed";
6522
+ var schemaConfigurationChanged = (current, next) => current.basePath !== next.basePath || !deepEqual2(current.apis, next.apis);
6100
6523
  var warn = (message) => {
6101
6524
  console.warn(`[zudoku] ${message}`);
6102
6525
  };
@@ -6121,11 +6544,11 @@ var viteApiPlugin = async () => {
6121
6544
  const resolvedVirtualModuleId4 = `\0${virtualModuleId4}`;
6122
6545
  const initialConfig = getCurrentConfig();
6123
6546
  const zuploProcessors = ZuploEnv.isZuplo ? await runnerImport3(
6124
- path12.resolve(getZudokuRootDir(), "src/zuplo/with-zuplo-processors.ts")
6547
+ path13.resolve(getZudokuRootDir(), "src/zuplo/with-zuplo-processors.ts")
6125
6548
  ).then((m) => m.module.default(initialConfig.__meta.rootDir)) : [];
6126
6549
  const buildConfig = await getBuildConfig();
6127
6550
  const buildProcessors = buildConfig?.processors ?? [];
6128
- const tmpStoreDir = path12.posix.join(
6551
+ const tmpStoreDir = path13.posix.join(
6129
6552
  initialConfig.__meta.rootDir,
6130
6553
  PROCESSED_STORE_SUBPATH
6131
6554
  );
@@ -6135,8 +6558,8 @@ var viteApiPlugin = async () => {
6135
6558
  config: initialConfig,
6136
6559
  processors
6137
6560
  });
6138
- await fs2.rm(tmpStoreDir, { recursive: true, force: true });
6139
- await fs2.mkdir(tmpStoreDir, { recursive: true });
6561
+ await fs3.rm(tmpStoreDir, { recursive: true, force: true });
6562
+ await fs3.mkdir(tmpStoreDir, { recursive: true });
6140
6563
  await schemaManager.processAllSchemas();
6141
6564
  return {
6142
6565
  name: "zudoku-api-plugins",
@@ -6144,19 +6567,12 @@ var viteApiPlugin = async () => {
6144
6567
  schemaManager.getAllTrackedFiles().forEach((file) => this.addWatchFile(file));
6145
6568
  },
6146
6569
  configureServer(server) {
6147
- server.middlewares.use(async (req, res, next) => {
6148
- if (req.method !== "GET" || !req.url) return next();
6149
- if (!req.url.toLowerCase().endsWith(".json") && !req.url.toLowerCase().endsWith(".yaml")) {
6150
- return next();
6151
- }
6152
- const pathMap = schemaManager.getUrlToFilePathMap();
6153
- const inputPath = pathMap.get(req.url);
6154
- if (!inputPath) return next();
6155
- const content = await fs2.readFile(inputPath, "utf-8");
6156
- const mimeType = path12.extname(inputPath).toLowerCase() === ".json" ? "application/json" : "application/x-yaml";
6157
- res.setHeader("Content-Type", `${mimeType}; charset=utf-8`);
6158
- return res.end(content);
6159
- });
6570
+ server.middlewares.use(
6571
+ createOpenApiDevMiddleware({
6572
+ getPublications: () => schemaManager.getPublishedSchemas(),
6573
+ getDownloadPathMap: () => schemaManager.getUrlToFilePathMap()
6574
+ })
6575
+ );
6160
6576
  server.watcher.on("change", async (id) => {
6161
6577
  const mainFiles = schemaManager.getFilesToReprocess(id);
6162
6578
  if (mainFiles.length === 0) return;
@@ -6193,7 +6609,7 @@ var viteApiPlugin = async () => {
6193
6609
  async load(id) {
6194
6610
  if (id !== resolvedVirtualModuleId4) return;
6195
6611
  const config = getCurrentConfig();
6196
- if (!deepEqual2(schemaManager.config.apis, config.apis)) {
6612
+ if (schemaConfigurationChanged(schemaManager.config, config)) {
6197
6613
  schemaManager.config = config;
6198
6614
  await schemaManager.processAllSchemas();
6199
6615
  schemaManager.getAllTrackedFiles().forEach((file) => this.addWatchFile(file));
@@ -6382,11 +6798,15 @@ var viteApiPlugin = async () => {
6382
6798
  const pathMap = schemaManager.getUrlToFilePathMap();
6383
6799
  if (process.env.NODE_ENV !== "production") return;
6384
6800
  for (const [urlPath, inputPath] of pathMap) {
6385
- const content = await fs2.readFile(inputPath, "utf-8");
6386
- const outputPath = path12.join(config.__meta.rootDir, "dist", urlPath);
6387
- await fs2.mkdir(path12.dirname(outputPath), { recursive: true });
6388
- await fs2.writeFile(outputPath, content, "utf-8");
6801
+ const content = await fs3.readFile(inputPath, "utf-8");
6802
+ const outputPath = path13.join(config.__meta.rootDir, "dist", urlPath);
6803
+ await fs3.mkdir(path13.dirname(outputPath), { recursive: true });
6804
+ await fs3.writeFile(outputPath, content, "utf-8");
6389
6805
  }
6806
+ await writeOpenApiPublications(
6807
+ path13.join(config.__meta.rootDir, "dist"),
6808
+ schemaManager.getPublishedSchemas()
6809
+ );
6390
6810
  }
6391
6811
  };
6392
6812
  };
@@ -6549,7 +6969,7 @@ var viteDocMetadataPlugin = () => ({
6549
6969
  });
6550
6970
 
6551
6971
  // src/vite/plugin-docs.ts
6552
- import path13 from "node:path";
6972
+ import path14 from "node:path";
6553
6973
  import { glob as glob3 } from "glob";
6554
6974
  import globParent from "glob-parent";
6555
6975
 
@@ -6616,7 +7036,7 @@ var globMarkdownFiles = async (config, options = { absolute: false }) => {
6616
7036
  if (process.env.NODE_ENV !== "development") {
6617
7037
  const draftStatuses = await Promise.all(
6618
7038
  globbedFiles.map(async (file) => {
6619
- const absolutePath = path13.resolve(config.__meta.rootDir, file);
7039
+ const absolutePath = path14.resolve(config.__meta.rootDir, file);
6620
7040
  const { data } = await readFrontmatter(absolutePath);
6621
7041
  return { file, isDraft: data.draft === true };
6622
7042
  })
@@ -6629,9 +7049,9 @@ var globMarkdownFiles = async (config, options = { absolute: false }) => {
6629
7049
  if (draftFiles.has(file)) {
6630
7050
  continue;
6631
7051
  }
6632
- const relativePath = path13.posix.relative(parent, file);
7052
+ const relativePath = path14.posix.relative(parent, file);
6633
7053
  const routePath = ensureLeadingSlash(relativePath.replace(/\.mdx?$/, ""));
6634
- const filePath = options.absolute ? path13.resolve(config.__meta.rootDir, file) : file;
7054
+ const filePath = options.absolute ? path14.resolve(config.__meta.rootDir, file) : file;
6635
7055
  fileMapping[routePath] = filePath;
6636
7056
  }
6637
7057
  }
@@ -6715,8 +7135,168 @@ var plugin_docs_default = viteDocsPlugin;
6715
7135
 
6716
7136
  // src/vite/plugin-markdown-export.ts
6717
7137
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
6718
- import path14 from "node:path";
7138
+ import path15 from "node:path";
7139
+
7140
+ // src/lib/util/contentNegotiation.ts
7141
+ var representations = [
7142
+ {
7143
+ contentType: "text/html",
7144
+ type: "text",
7145
+ subtype: "html",
7146
+ parameters: { charset: "utf-8" },
7147
+ serverOrder: 0
7148
+ },
7149
+ {
7150
+ contentType: "text/markdown",
7151
+ type: "text",
7152
+ subtype: "markdown",
7153
+ parameters: { charset: "utf-8" },
7154
+ serverOrder: 1
7155
+ }
7156
+ ];
7157
+ var tokenPattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
7158
+ var mediaRangePattern = /^([!#$%&'*+\-.^_`|~0-9A-Za-z]+|\*)\/([!#$%&'*+\-.^_`|~0-9A-Za-z]+|\*)$/;
7159
+ var qualityPattern = /^(?:0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?)$/;
7160
+ var splitOutsideQuotes = (value, delimiter) => {
7161
+ const parts = [];
7162
+ let start = 0;
7163
+ let quoted = false;
7164
+ let escaped = false;
7165
+ for (let index = 0; index < value.length; index += 1) {
7166
+ const character = value[index];
7167
+ if (escaped) {
7168
+ escaped = false;
7169
+ continue;
7170
+ }
7171
+ if (quoted && character === "\\") {
7172
+ escaped = true;
7173
+ continue;
7174
+ }
7175
+ if (character === '"') {
7176
+ quoted = !quoted;
7177
+ continue;
7178
+ }
7179
+ if (!quoted && character === delimiter) {
7180
+ parts.push(value.slice(start, index));
7181
+ start = index + 1;
7182
+ }
7183
+ }
7184
+ parts.push(value.slice(start));
7185
+ return parts;
7186
+ };
7187
+ var parseParameterValue = (value) => {
7188
+ const trimmedValue = value.trim();
7189
+ if (tokenPattern.test(trimmedValue)) {
7190
+ return trimmedValue.toLowerCase();
7191
+ }
7192
+ if (!trimmedValue.startsWith('"') || !trimmedValue.endsWith('"')) {
7193
+ return void 0;
7194
+ }
7195
+ const innerValue = trimmedValue.slice(1, -1);
7196
+ return innerValue.replace(/\\(.)/g, "$1").toLowerCase();
7197
+ };
7198
+ var parseMediaRange = (value, order) => {
7199
+ const [rawMediaRange, ...rawParameters] = splitOutsideQuotes(value, ";");
7200
+ const mediaRangeMatch = rawMediaRange?.trim().toLowerCase().match(mediaRangePattern);
7201
+ if (!mediaRangeMatch) {
7202
+ return void 0;
7203
+ }
7204
+ const [, type, subtype] = mediaRangeMatch;
7205
+ if (!type || !subtype || type === "*" && subtype !== "*") {
7206
+ return void 0;
7207
+ }
7208
+ const parameters = /* @__PURE__ */ new Map();
7209
+ let quality = 1;
7210
+ let foundQuality = false;
7211
+ for (const rawParameter of rawParameters) {
7212
+ const separatorIndex = rawParameter.indexOf("=");
7213
+ if (separatorIndex === -1) {
7214
+ return void 0;
7215
+ }
7216
+ const name = rawParameter.slice(0, separatorIndex).trim().toLowerCase();
7217
+ const rawValue = rawParameter.slice(separatorIndex + 1).trim();
7218
+ if (!tokenPattern.test(name)) {
7219
+ return void 0;
7220
+ }
7221
+ if (name === "q") {
7222
+ if (foundQuality || !qualityPattern.test(rawValue)) {
7223
+ return void 0;
7224
+ }
7225
+ quality = Number(rawValue);
7226
+ foundQuality = true;
7227
+ continue;
7228
+ }
7229
+ const parameterValue = parseParameterValue(rawValue);
7230
+ if (parameterValue === void 0 || parameters.has(name)) {
7231
+ return void 0;
7232
+ }
7233
+ parameters.set(name, parameterValue);
7234
+ }
7235
+ return { type, subtype, parameters, quality, order };
7236
+ };
7237
+ var getSpecificity = (range) => {
7238
+ if (range.type === "*") {
7239
+ return 0;
7240
+ }
7241
+ if (range.subtype === "*") {
7242
+ return 1;
7243
+ }
7244
+ return 2;
7245
+ };
7246
+ var matchesRepresentation = (range, representation) => {
7247
+ if (range.type !== "*" && range.type !== representation.type) {
7248
+ return false;
7249
+ }
7250
+ if (range.subtype !== "*" && range.subtype !== representation.subtype) {
7251
+ return false;
7252
+ }
7253
+ return [...range.parameters].every(
7254
+ ([name, value]) => representation.parameters[name]?.toLowerCase() === value
7255
+ );
7256
+ };
7257
+ var compareRangeMatches = (left, right) => right.specificity - left.specificity || right.parameterCount - left.parameterCount || right.quality - left.quality || left.rangeOrder - right.rangeOrder;
7258
+ var getRepresentationMatch = (ranges, representation) => ranges.filter((range) => matchesRepresentation(range, representation)).map((range) => ({
7259
+ contentType: representation.contentType,
7260
+ quality: range.quality,
7261
+ specificity: getSpecificity(range),
7262
+ parameterCount: range.parameters.size,
7263
+ rangeOrder: range.order,
7264
+ serverOrder: representation.serverOrder
7265
+ })).sort(compareRangeMatches)[0];
7266
+ var compareRepresentations = (left, right) => right.quality - left.quality || right.specificity - left.specificity || right.parameterCount - left.parameterCount || left.rangeOrder - right.rangeOrder || left.serverOrder - right.serverOrder;
7267
+ var negotiateContentType = (acceptHeader) => {
7268
+ if (!acceptHeader?.trim()) {
7269
+ return "text/html";
7270
+ }
7271
+ const ranges = splitOutsideQuotes(acceptHeader, ",").map((value, order) => parseMediaRange(value.trim(), order)).filter((range) => range !== void 0);
7272
+ const match = representations.map((representation) => getRepresentationMatch(ranges, representation)).filter((result) => result !== void 0).filter((result) => result.quality > 0).sort(compareRepresentations)[0];
7273
+ return match?.contentType ?? null;
7274
+ };
7275
+ var addAcceptToVary = (varyHeader) => {
7276
+ const fields = (varyHeader ?? "").split(",").map((field) => field.trim()).filter(Boolean);
7277
+ if (fields.some((field) => field === "*")) {
7278
+ return "*";
7279
+ }
7280
+ const seenFields = /* @__PURE__ */ new Set();
7281
+ const uniqueFields = fields.filter((field) => {
7282
+ const normalizedField = field.toLowerCase();
7283
+ if (seenFields.has(normalizedField)) {
7284
+ return false;
7285
+ }
7286
+ seenFields.add(normalizedField);
7287
+ return true;
7288
+ });
7289
+ if (!seenFields.has("accept")) {
7290
+ uniqueFields.push("Accept");
7291
+ }
7292
+ return uniqueFields.join(", ");
7293
+ };
7294
+
7295
+ // src/vite/plugin-markdown-export.ts
6719
7296
  init_joinUrl();
7297
+ init_markdown_representation();
7298
+ var MARKDOWN_FILES_MODULE_ID = "virtual:zudoku-markdown-files";
7299
+ var RESOLVED_MARKDOWN_FILES_MODULE_ID = `\0${MARKDOWN_FILES_MODULE_ID}`;
6720
7300
  var processMarkdownFile = async (filePath) => {
6721
7301
  const { content: markdownContent, data: frontmatter } = await readFrontmatter(filePath);
6722
7302
  let finalMarkdown = markdownContent;
@@ -6732,19 +7312,97 @@ ${markdownContent}`;
6732
7312
  };
6733
7313
  var getMarkdownOutputPath = (distDir, routePath) => {
6734
7314
  const segments = routePath === "/" ? ["index"] : routePath.split("/").filter(Boolean);
6735
- return `${path14.join(distDir, ...segments)}.md`;
7315
+ return `${path15.join(distDir, ...segments)}.md`;
6736
7316
  };
6737
- var resolveMarkdownRoutePath = (requestUrl, basePath) => {
6738
- const pathname = requestUrl.split(/[?#]/)[0] ?? requestUrl;
6739
- const routePath = joinUrl(
6740
- pathname.slice(basePath.length).replace(/\.mdx?$/, "")
7317
+ var writeMarkdownInfo = async (markdownInfoPath, markdownFileInfos) => {
7318
+ await mkdir2(path15.dirname(markdownInfoPath), { recursive: true });
7319
+ await writeFile2(
7320
+ markdownInfoPath,
7321
+ JSON.stringify(markdownFileInfos, null, 2),
7322
+ "utf-8"
6741
7323
  );
7324
+ };
7325
+ var resolveMarkdownRoutePath = (requestUrl, basePath) => {
7326
+ const pathname = requestUrl.split(/[?#]/)[0]?.replace(/\.mdx?$/, "");
7327
+ if (!pathname) return;
7328
+ const routePath = resolveDocumentationRoutePath(pathname, basePath);
7329
+ if (!routePath) return;
6742
7330
  if (routePath === "/index") {
6743
7331
  return "/";
6744
7332
  }
6745
7333
  return routePath;
6746
7334
  };
6747
7335
  var needsMdFiles = (config) => config.docs.publishMarkdown || config.docs.llms.llmsTxt || config.docs.llms.llmsTxtFull;
7336
+ var isContentNegotiationEnabled = (config) => config.docs.publishMarkdown && config.docs.contentNegotiation;
7337
+ var resolveMarkdownFiles = async (config) => {
7338
+ const files = await resolveCustomNavigationPaths(
7339
+ config,
7340
+ await globMarkdownFiles(config, { absolute: true })
7341
+ );
7342
+ if (config.docs.llms.includeProtected || !config.protectedRoutes) {
7343
+ return files;
7344
+ }
7345
+ const patterns = Object.keys(config.protectedRoutes);
7346
+ return Object.fromEntries(
7347
+ Object.entries(files).filter(
7348
+ ([routePath]) => !matchesAnyProtectedPattern(patterns, routePath)
7349
+ )
7350
+ );
7351
+ };
7352
+ var loadMarkdownContents = async (markdownFiles) => Object.fromEntries(
7353
+ await Promise.all(
7354
+ Object.entries(markdownFiles).map(async ([routePath, filePath]) => [
7355
+ encodeDocumentationRoutePath(routePath),
7356
+ (await processMarkdownFile(filePath)).content
7357
+ ])
7358
+ )
7359
+ );
7360
+ var createMarkdownDevMiddleware = ({
7361
+ config,
7362
+ getMarkdownFiles
7363
+ }) => async (req, res, next) => {
7364
+ if (!req.url || req.method !== "GET" && req.method !== "HEAD") {
7365
+ return next();
7366
+ }
7367
+ const basePath = joinUrl(config.basePath);
7368
+ const pathname = req.url.split(/[?#]/)[0] ?? req.url;
7369
+ const isExplicitMarkdownRequest = pathname.endsWith(".md");
7370
+ const routePath = isExplicitMarkdownRequest ? resolveMarkdownRoutePath(req.url, basePath) : resolveDocumentationRoutePath(req.url, basePath);
7371
+ if (!routePath) return next();
7372
+ const filePath = Object.entries(getMarkdownFiles()).find(
7373
+ ([configuredRoutePath]) => encodeDocumentationRoutePath(configuredRoutePath) === routePath
7374
+ )?.[1];
7375
+ if (!filePath) return next();
7376
+ if (!isExplicitMarkdownRequest && isContentNegotiationEnabled(config)) {
7377
+ const negotiatedType = negotiateContentType(req.headers.accept);
7378
+ res.setHeader("Vary", addAcceptToVary(res.getHeader("Vary")?.toString()));
7379
+ res.setHeader(
7380
+ "Link",
7381
+ appendLinkHeader(
7382
+ res.getHeader("Link"),
7383
+ getMarkdownAlternateLink(routePath, config.basePath)
7384
+ )
7385
+ );
7386
+ if (negotiatedType === null) {
7387
+ res.statusCode = 406;
7388
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
7389
+ return res.end(req.method === "HEAD" ? void 0 : "Not Acceptable");
7390
+ }
7391
+ if (negotiatedType === "text/html") {
7392
+ return next();
7393
+ }
7394
+ } else if (!isExplicitMarkdownRequest) {
7395
+ return next();
7396
+ }
7397
+ try {
7398
+ const { content } = await processMarkdownFile(filePath);
7399
+ res.setHeader("Content-Type", "text/markdown; charset=utf-8");
7400
+ return res.end(req.method === "HEAD" ? void 0 : content);
7401
+ } catch (error) {
7402
+ console.warn(`Failed to serve markdown for ${routePath}:`, error);
7403
+ return next();
7404
+ }
7405
+ };
6748
7406
  var viteMarkdownExportPlugin = () => {
6749
7407
  let markdownFiles = {};
6750
7408
  let markdownFileInfos = [];
@@ -6753,58 +7411,40 @@ var viteMarkdownExportPlugin = () => {
6753
7411
  applyToEnvironment(env) {
6754
7412
  return env.name === "ssr";
6755
7413
  },
7414
+ resolveId(id) {
7415
+ if (id === MARKDOWN_FILES_MODULE_ID) {
7416
+ return RESOLVED_MARKDOWN_FILES_MODULE_ID;
7417
+ }
7418
+ },
7419
+ async load(id) {
7420
+ if (id === RESOLVED_MARKDOWN_FILES_MODULE_ID) {
7421
+ return `export default ${JSON.stringify(await loadMarkdownContents(markdownFiles))};`;
7422
+ }
7423
+ },
6756
7424
  async buildStart() {
6757
7425
  const config = getCurrentConfig();
6758
7426
  if (config.__meta.mode === "standalone" || !needsMdFiles(config)) {
6759
7427
  return;
6760
7428
  }
6761
- markdownFiles = await resolveCustomNavigationPaths(
6762
- config,
6763
- await globMarkdownFiles(config, { absolute: true })
6764
- );
6765
- if (!config.docs.llms.includeProtected) {
6766
- const protectedRoutes = config.protectedRoutes;
6767
- if (protectedRoutes) {
6768
- const patterns = Object.keys(protectedRoutes);
6769
- for (const routePath of Object.keys(markdownFiles)) {
6770
- if (matchesAnyProtectedPattern(patterns, routePath)) {
6771
- delete markdownFiles[routePath];
6772
- }
6773
- }
6774
- }
6775
- }
7429
+ markdownFiles = await resolveMarkdownFiles(config);
6776
7430
  },
6777
7431
  async configureServer(server) {
6778
7432
  const config = getCurrentConfig();
6779
7433
  if (!needsMdFiles(config)) return;
6780
- markdownFiles = await resolveCustomNavigationPaths(
6781
- config,
6782
- await globMarkdownFiles(config, { absolute: true })
7434
+ markdownFiles = await resolveMarkdownFiles(config);
7435
+ server.middlewares.use(
7436
+ createMarkdownDevMiddleware({
7437
+ config,
7438
+ getMarkdownFiles: () => markdownFiles
7439
+ })
6783
7440
  );
6784
- server.middlewares.use(async (req, res, next) => {
6785
- if (req.method !== "GET" || !req.url?.endsWith(".md")) {
6786
- return next();
6787
- }
6788
- const basePath = joinUrl(config.basePath);
6789
- const routePath = resolveMarkdownRoutePath(req.url, basePath);
6790
- const filePath = markdownFiles[routePath];
6791
- if (!filePath) return next();
6792
- try {
6793
- const { content } = await processMarkdownFile(filePath);
6794
- res.setHeader("Content-Type", "text/markdown; charset=utf-8");
6795
- res.end(content);
6796
- } catch (error) {
6797
- console.warn(`Failed to serve markdown for ${routePath}:`, error);
6798
- return next();
6799
- }
6800
- });
6801
7441
  },
6802
7442
  async closeBundle() {
6803
7443
  const config = getCurrentConfig();
6804
- if (process.env.NODE_ENV !== "production" || Object.keys(markdownFiles).length === 0 || !needsMdFiles(config)) {
7444
+ if (process.env.NODE_ENV !== "production" || !needsMdFiles(config)) {
6805
7445
  return;
6806
7446
  }
6807
- const distDir = path14.join(
7447
+ const distDir = path15.join(
6808
7448
  config.__meta.rootDir,
6809
7449
  "dist",
6810
7450
  config.basePath ?? ""
@@ -6825,22 +7465,18 @@ var viteMarkdownExportPlugin = () => {
6825
7465
  content: finalMarkdown
6826
7466
  });
6827
7467
  const outputPath = getMarkdownOutputPath(distDir, routePath);
6828
- await mkdir2(path14.dirname(outputPath), { recursive: true });
7468
+ await mkdir2(path15.dirname(outputPath), { recursive: true });
6829
7469
  await writeFile2(outputPath, finalMarkdown, "utf-8");
6830
7470
  } catch (error) {
6831
7471
  console.warn(`Failed to export markdown for ${routePath}:`, error);
6832
7472
  }
6833
7473
  }
6834
- if (config.docs.llms.llmsTxt || config.docs.llms.llmsTxtFull) {
6835
- const markdownInfoPath = path14.join(
7474
+ if (config.docs.llms.llmsTxt || config.docs.llms.llmsTxtFull || isContentNegotiationEnabled(config)) {
7475
+ const markdownInfoPath = path15.join(
6836
7476
  config.__meta.rootDir,
6837
7477
  "node_modules/.zudoku/markdown-info.json"
6838
7478
  );
6839
- await writeFile2(
6840
- markdownInfoPath,
6841
- JSON.stringify(markdownFileInfos, null, 2),
6842
- "utf-8"
6843
- );
7479
+ await writeMarkdownInfo(markdownInfoPath, markdownFileInfos);
6844
7480
  }
6845
7481
  }
6846
7482
  };
@@ -6984,9 +7620,9 @@ var remarkCodeTabs = () => (tree) => {
6984
7620
  };
6985
7621
 
6986
7622
  // src/vite/mdx/remark-inject-filepath.ts
6987
- import path15 from "node:path";
7623
+ import path16 from "node:path";
6988
7624
  var remarkInjectFilepath = (rootDir) => (tree, vfile) => {
6989
- const relativePath = path15.relative(rootDir, vfile.path).split(path15.sep).join(path15.posix.sep);
7625
+ const relativePath = path16.relative(rootDir, vfile.path).split(path16.sep).join(path16.posix.sep);
6990
7626
  tree.children.unshift(exportMdxjsConst("__filepath", relativePath));
6991
7627
  };
6992
7628
 
@@ -7073,7 +7709,7 @@ var remarkLastModified = () => {
7073
7709
  };
7074
7710
 
7075
7711
  // src/vite/mdx/remark-link-rewrite.ts
7076
- import path16 from "node:path";
7712
+ import path17 from "node:path";
7077
7713
  import { visit as visit5 } from "unist-util-visit";
7078
7714
  var markdownExtension = /\.mdx?$/;
7079
7715
  var resolveToRoute = (url, filePath, routesByFile) => {
@@ -7081,7 +7717,7 @@ var resolveToRoute = (url, filePath, routesByFile) => {
7081
7717
  if (!markdownExtension.test(pathname) || pathname.startsWith("/")) {
7082
7718
  return void 0;
7083
7719
  }
7084
- const targetFile = path16.resolve(path16.dirname(filePath), pathname).replace(markdownExtension, "");
7720
+ const targetFile = path17.resolve(path17.dirname(filePath), pathname).replace(markdownExtension, "");
7085
7721
  const route = routesByFile.get(targetFile);
7086
7722
  if (route === void 0) return void 0;
7087
7723
  return `${route}${suffix}`;
@@ -7096,11 +7732,11 @@ var remarkLinkRewrite = (basePath = "", routesByFile = /* @__PURE__ */ new Map()
7096
7732
  node.url = resolved;
7097
7733
  return;
7098
7734
  }
7099
- const base = path16.posix.join(basePath);
7735
+ const base = path17.posix.join(basePath);
7100
7736
  if (basePath && node.url.startsWith(base)) {
7101
7737
  node.url = node.url.slice(base.length);
7102
7738
  } else if (!node.url.startsWith("/") && !node.url.startsWith("#")) {
7103
- node.url = path16.posix.join("..", node.url);
7739
+ node.url = path17.posix.join("..", node.url);
7104
7740
  }
7105
7741
  node.url = node.url.replace(/\.mdx?(#.*)?$/, "$1");
7106
7742
  });
@@ -7609,8 +8245,8 @@ var protectedAnnotatorPlugin = () => ({
7609
8245
  });
7610
8246
 
7611
8247
  // src/vite/config.ts
7612
- var getAppClientEntryPath = () => path17.posix.join(getZudokuRootDir(), "src/app/entry.client.tsx");
7613
- var getAppServerEntryPath = () => path17.posix.join(getZudokuRootDir(), "src/app/entry.server.tsx");
8248
+ var getAppClientEntryPath = () => path18.posix.join(getZudokuRootDir(), "src/app/entry.client.tsx");
8249
+ var getAppServerEntryPath = () => path18.posix.join(getZudokuRootDir(), "src/app/entry.server.tsx");
7614
8250
  var hasLoggedCdnInfo = false;
7615
8251
  var MEDIA_REGEX = /\.(a?png|jpe?g|gif|bmp|svg|webp|tiff|ico|webm|ogg|mp3|wav|m4a|avif|mp4)/i;
7616
8252
  var defineEnvVars = (vars) => Object.fromEntries(
@@ -7644,7 +8280,7 @@ async function getViteConfig(dir, configEnv, options = {}) {
7644
8280
  );
7645
8281
  if (ZuploEnv.isZuplo) {
7646
8282
  dotenv.config({
7647
- path: path17.resolve(config.__meta.rootDir, "../.env.zuplo"),
8283
+ path: path18.resolve(config.__meta.rootDir, "../.env.zuplo"),
7648
8284
  quiet: true
7649
8285
  });
7650
8286
  }
@@ -7704,7 +8340,7 @@ async function getViteConfig(dir, configEnv, options = {}) {
7704
8340
  sourcemap: true,
7705
8341
  target: "es2022",
7706
8342
  chunkSizeWarningLimit: 1500,
7707
- outDir: path17.resolve(path17.join(dir, "dist", config.basePath ?? "")),
8343
+ outDir: path18.resolve(path18.join(dir, "dist", config.basePath ?? "")),
7708
8344
  emptyOutDir: false,
7709
8345
  rolldownOptions: {
7710
8346
  external: [joinUrl(config.basePath, "/pagefind/pagefind.js")],
@@ -7734,8 +8370,8 @@ async function getViteConfig(dir, configEnv, options = {}) {
7734
8370
  external: ["@shikijs/themes", "@shikijs/langs"]
7735
8371
  },
7736
8372
  build: {
7737
- outDir: path17.resolve(
7738
- path17.join(dir, "dist", config.basePath ?? "", "server")
8373
+ outDir: path18.resolve(
8374
+ path18.join(dir, "dist", config.basePath ?? "", "server")
7739
8375
  ),
7740
8376
  copyPublicDir: false,
7741
8377
  rolldownOptions: {
@@ -7750,7 +8386,7 @@ async function getViteConfig(dir, configEnv, options = {}) {
7750
8386
  if (filename.startsWith(`${PROTECTED_CHUNK_DIR}/`)) {
7751
8387
  return joinUrl(config.basePath, `/${filename}`);
7752
8388
  }
7753
- if (cdnUrl?.base && [".js", ".css"].includes(path17.extname(filename))) {
8389
+ if (cdnUrl?.base && [".js", ".css"].includes(path18.extname(filename))) {
7754
8390
  return joinUrl(cdnUrl.base, filename);
7755
8391
  }
7756
8392
  if (cdnUrl?.media && MEDIA_REGEX.test(filename)) {
@@ -7760,7 +8396,7 @@ async function getViteConfig(dir, configEnv, options = {}) {
7760
8396
  }
7761
8397
  },
7762
8398
  optimizeDeps: {
7763
- entries: [path17.posix.join(getZudokuRootDir(), "src/{app,lib}/**")],
8399
+ entries: [path18.posix.join(getZudokuRootDir(), "src/{app,lib}/**")],
7764
8400
  exclude: ["zudoku"],
7765
8401
  include: [
7766
8402
  "@mdx-js/react",
@@ -7857,10 +8493,10 @@ ${cssLinks}
7857
8493
 
7858
8494
  // src/vite/manifest.ts
7859
8495
  import { writeFile as writeFile3 } from "node:fs/promises";
7860
- import path18 from "node:path";
8496
+ import path19 from "node:path";
7861
8497
  var writeManifest = async (distDir, config) => {
7862
8498
  await writeFile3(
7863
- path18.join(distDir, MANIFEST_FILENAME),
8499
+ path19.join(distDir, MANIFEST_FILENAME),
7864
8500
  `${JSON.stringify(buildManifest(config), null, 2)}
7865
8501
  `,
7866
8502
  "utf-8"
@@ -7869,17 +8505,442 @@ var writeManifest = async (distDir, config) => {
7869
8505
 
7870
8506
  // src/vite/output.ts
7871
8507
  import assert from "node:assert";
7872
- import { cp, mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
7873
- import path19 from "node:path";
8508
+ import { existsSync } from "node:fs";
8509
+ import { cp, mkdir as mkdir3, readdir, rm, writeFile as writeFile4 } from "node:fs/promises";
8510
+ import path20 from "node:path";
7874
8511
  init_joinUrl();
8512
+
8513
+ // src/vite/vercel-markdown-middleware.ts
8514
+ var normalizePath2 = (value) => {
8515
+ const pathname = value.split("/").filter(Boolean).map((segment) => {
8516
+ try {
8517
+ return encodeURIComponent(decodeURIComponent(segment));
8518
+ } catch {
8519
+ return encodeURIComponent(segment);
8520
+ }
8521
+ }).join("/");
8522
+ return pathname ? `/${pathname}` : "/";
8523
+ };
8524
+ var resolveRoutePath = (basePath, routePath) => {
8525
+ const normalizedRoutePath = normalizePath2(routePath);
8526
+ if (normalizedRoutePath === "/") {
8527
+ return basePath;
8528
+ }
8529
+ return normalizePath2(`${basePath}/${normalizedRoutePath}`);
8530
+ };
8531
+ var resolveMarkdownPath = (basePath, routePath) => {
8532
+ const normalizedRoutePath = normalizePath2(routePath);
8533
+ if (normalizedRoutePath === "/") {
8534
+ return normalizePath2(`${basePath}/index.md`);
8535
+ }
8536
+ return `${resolveRoutePath(basePath, routePath)}.md`;
8537
+ };
8538
+ var generateVercelMarkdownMiddleware = ({
8539
+ basePath = "/",
8540
+ knownCanonicalRoutePaths,
8541
+ markdownCanonicalRoutePaths,
8542
+ markdownNotFoundBody,
8543
+ passthroughPaths = []
8544
+ }) => {
8545
+ const normalizedBasePath = normalizePath2(basePath);
8546
+ const knownRoutes = new Set(
8547
+ knownCanonicalRoutePaths.map(
8548
+ (routePath) => resolveRoutePath(normalizedBasePath, routePath)
8549
+ )
8550
+ );
8551
+ const markdownRoutes = new Map(
8552
+ markdownCanonicalRoutePaths.map((routePath) => [
8553
+ resolveRoutePath(normalizedBasePath, routePath),
8554
+ resolveMarkdownPath(normalizedBasePath, routePath)
8555
+ ])
8556
+ );
8557
+ const normalizedPassthroughPaths = passthroughPaths.map(normalizePath2);
8558
+ for (const routePath of markdownRoutes.keys()) {
8559
+ if (!knownRoutes.has(routePath)) {
8560
+ throw new Error(
8561
+ `Markdown route "${routePath}" is not present in knownCanonicalRoutePaths`
8562
+ );
8563
+ }
8564
+ }
8565
+ return `
8566
+ const BASE_PATH = ${JSON.stringify(normalizedBasePath)};
8567
+ const KNOWN_ROUTES = new Set(${JSON.stringify([...knownRoutes])});
8568
+ const MARKDOWN_ROUTES = new Map(${JSON.stringify([...markdownRoutes])});
8569
+ const PASSTHROUGH_PATHS = new Set(${JSON.stringify(normalizedPassthroughPaths)});
8570
+ const MARKDOWN_NOT_FOUND_BODY = ${JSON.stringify(markdownNotFoundBody)};
8571
+ const NEGOTIATED_VARY = "Accept, Accept-Encoding";
8572
+
8573
+ const REPRESENTATIONS = [
8574
+ {
8575
+ contentType: "text/html",
8576
+ type: "text",
8577
+ subtype: "html",
8578
+ parameters: { charset: "utf-8" },
8579
+ serverOrder: 0,
8580
+ },
8581
+ {
8582
+ contentType: "text/markdown",
8583
+ type: "text",
8584
+ subtype: "markdown",
8585
+ parameters: { charset: "utf-8" },
8586
+ serverOrder: 1,
8587
+ },
8588
+ ];
8589
+
8590
+ const TOKEN_PATTERN = /^[!#$%&'*+\\-.^_\`|~0-9A-Za-z]+$/;
8591
+ const MEDIA_RANGE_PATTERN =
8592
+ /^([!#$%&'*+\\-.^_\`|~0-9A-Za-z]+|\\*)\\/([!#$%&'*+\\-.^_\`|~0-9A-Za-z]+|\\*)$/;
8593
+ const QUALITY_PATTERN = /^(?:0(?:\\.[0-9]{0,3})?|1(?:\\.0{0,3})?)$/;
8594
+
8595
+ const splitOutsideQuotes = (value, delimiter) => {
8596
+ const parts = [];
8597
+ let start = 0;
8598
+ let quoted = false;
8599
+ let escaped = false;
8600
+
8601
+ for (let index = 0; index < value.length; index += 1) {
8602
+ const character = value[index];
8603
+ if (escaped) {
8604
+ escaped = false;
8605
+ continue;
8606
+ }
8607
+ if (quoted && character === "\\\\") {
8608
+ escaped = true;
8609
+ continue;
8610
+ }
8611
+ if (character === '"') {
8612
+ quoted = !quoted;
8613
+ continue;
8614
+ }
8615
+ if (!quoted && character === delimiter) {
8616
+ parts.push(value.slice(start, index));
8617
+ start = index + 1;
8618
+ }
8619
+ }
8620
+
8621
+ parts.push(value.slice(start));
8622
+ return parts;
8623
+ };
8624
+
8625
+ const parseParameterValue = (value) => {
8626
+ const trimmedValue = value.trim();
8627
+ if (TOKEN_PATTERN.test(trimmedValue)) {
8628
+ return trimmedValue.toLowerCase();
8629
+ }
8630
+ if (!trimmedValue.startsWith('"') || !trimmedValue.endsWith('"')) {
8631
+ return undefined;
8632
+ }
8633
+ return trimmedValue.slice(1, -1).replace(/\\\\(.)/g, "$1").toLowerCase();
8634
+ };
8635
+
8636
+ const parseMediaRange = (value, order) => {
8637
+ const [rawMediaRange, ...rawParameters] = splitOutsideQuotes(value, ";");
8638
+ const mediaRangeMatch = rawMediaRange
8639
+ ?.trim()
8640
+ .toLowerCase()
8641
+ .match(MEDIA_RANGE_PATTERN);
8642
+ if (!mediaRangeMatch) return undefined;
8643
+
8644
+ const [, type, subtype] = mediaRangeMatch;
8645
+ if (!type || !subtype || (type === "*" && subtype !== "*")) {
8646
+ return undefined;
8647
+ }
8648
+
8649
+ const parameters = new Map();
8650
+ let quality = 1;
8651
+ let foundQuality = false;
8652
+
8653
+ for (const rawParameter of rawParameters) {
8654
+ const separatorIndex = rawParameter.indexOf("=");
8655
+ if (separatorIndex === -1) return undefined;
8656
+
8657
+ const name = rawParameter.slice(0, separatorIndex).trim().toLowerCase();
8658
+ const rawValue = rawParameter.slice(separatorIndex + 1).trim();
8659
+ if (!TOKEN_PATTERN.test(name)) return undefined;
8660
+
8661
+ if (name === "q") {
8662
+ if (foundQuality || !QUALITY_PATTERN.test(rawValue)) return undefined;
8663
+ quality = Number(rawValue);
8664
+ foundQuality = true;
8665
+ continue;
8666
+ }
8667
+
8668
+ // RFC 9110 no longer defines accept extensions. Every non-q parameter is
8669
+ // a media type parameter and participates in representation matching.
8670
+ const parameterValue = parseParameterValue(rawValue);
8671
+ if (parameterValue === undefined || parameters.has(name)) return undefined;
8672
+ parameters.set(name, parameterValue);
8673
+ }
8674
+
8675
+ return { type, subtype, parameters, quality, order };
8676
+ };
8677
+
8678
+ const getSpecificity = (range) => {
8679
+ if (range.type === "*") return 0;
8680
+ if (range.subtype === "*") return 1;
8681
+ return 2;
8682
+ };
8683
+
8684
+ const matchesRepresentation = (range, representation) => {
8685
+ if (range.type !== "*" && range.type !== representation.type) return false;
8686
+ if (range.subtype !== "*" && range.subtype !== representation.subtype) {
8687
+ return false;
8688
+ }
8689
+ return [...range.parameters].every(
8690
+ ([name, value]) => representation.parameters[name]?.toLowerCase() === value,
8691
+ );
8692
+ };
8693
+
8694
+ const compareRangeMatches = (left, right) =>
8695
+ right.specificity - left.specificity ||
8696
+ right.parameterCount - left.parameterCount ||
8697
+ right.quality - left.quality ||
8698
+ left.rangeOrder - right.rangeOrder;
8699
+
8700
+ const getRepresentationMatch = (ranges, representation) =>
8701
+ ranges
8702
+ .filter((range) => matchesRepresentation(range, representation))
8703
+ .map((range) => ({
8704
+ contentType: representation.contentType,
8705
+ quality: range.quality,
8706
+ specificity: getSpecificity(range),
8707
+ parameterCount: range.parameters.size,
8708
+ rangeOrder: range.order,
8709
+ serverOrder: representation.serverOrder,
8710
+ }))
8711
+ .sort(compareRangeMatches)[0];
8712
+
8713
+ const compareRepresentations = (left, right) =>
8714
+ right.quality - left.quality ||
8715
+ right.specificity - left.specificity ||
8716
+ right.parameterCount - left.parameterCount ||
8717
+ left.rangeOrder - right.rangeOrder ||
8718
+ left.serverOrder - right.serverOrder;
8719
+
8720
+ const negotiateContentType = (acceptHeader) => {
8721
+ if (!acceptHeader?.trim()) return "text/html";
8722
+
8723
+ const ranges = splitOutsideQuotes(acceptHeader, ",")
8724
+ .map((value, order) => parseMediaRange(value.trim(), order))
8725
+ .filter((range) => range !== undefined);
8726
+ const match = REPRESENTATIONS
8727
+ .map((representation) => getRepresentationMatch(ranges, representation))
8728
+ .filter((result) => result !== undefined)
8729
+ .filter((result) => result.quality > 0)
8730
+ .sort(compareRepresentations)[0];
8731
+ return match?.contentType ?? null;
8732
+ };
8733
+
8734
+ const normalizeRequestPath = (pathname) => {
8735
+ const normalized = pathname
8736
+ .split("/")
8737
+ .filter(Boolean)
8738
+ .map((segment) => {
8739
+ try {
8740
+ return encodeURIComponent(decodeURIComponent(segment));
8741
+ } catch {
8742
+ return encodeURIComponent(segment);
8743
+ }
8744
+ })
8745
+ .join("/");
8746
+ return normalized ? "/" + normalized : "/";
8747
+ };
8748
+
8749
+ const isWithinBasePath = (pathname) =>
8750
+ BASE_PATH === "/" || pathname === BASE_PATH || pathname.startsWith(BASE_PATH + "/");
8751
+
8752
+ const isExtensionlessPath = (pathname) => {
8753
+ const lastSegment = pathname.split("/").filter(Boolean).at(-1);
8754
+ return lastSegment === undefined || !lastSegment.includes(".");
8755
+ };
8756
+
8757
+ const isMarkdownPath = (pathname) =>
8758
+ pathname.endsWith(".md") || pathname.endsWith(".mdx");
8759
+
8760
+ const getAlternateLink = (markdownPath) =>
8761
+ "<" + markdownPath + '>; rel="alternate"; type="text/markdown"';
8762
+
8763
+ const continueRequest = (headers = undefined) => {
8764
+ const response = new Response(null);
8765
+ response.headers.set("x-middleware-next", "1");
8766
+ if (headers) {
8767
+ for (const [name, value] of Object.entries(headers)) {
8768
+ response.headers.set(name, value);
8769
+ }
8770
+ }
8771
+ return response;
8772
+ };
8773
+
8774
+ const negotiatedHeaders = (markdownPath) => ({
8775
+ Vary: NEGOTIATED_VARY,
8776
+ Link: getAlternateLink(markdownPath),
8777
+ });
8778
+
8779
+ export default function middleware(request) {
8780
+ if (request.method !== "GET" && request.method !== "HEAD") {
8781
+ return continueRequest();
8782
+ }
8783
+
8784
+ const requestUrl = new URL(request.url);
8785
+ const pathname = normalizeRequestPath(requestUrl.pathname);
8786
+ const markdownPath = MARKDOWN_ROUTES.get(pathname);
8787
+
8788
+ if (markdownPath) {
8789
+ const negotiatedType = negotiateContentType(request.headers.get("Accept"));
8790
+ const headers = negotiatedHeaders(markdownPath);
8791
+
8792
+ if (negotiatedType === null) {
8793
+ return new Response(request.method === "HEAD" ? null : "Not Acceptable", {
8794
+ status: 406,
8795
+ headers: {
8796
+ ...headers,
8797
+ "Content-Type": "text/plain; charset=utf-8",
8798
+ },
8799
+ });
8800
+ }
8801
+
8802
+ if (negotiatedType === "text/html") {
8803
+ return continueRequest(headers);
8804
+ }
8805
+
8806
+ requestUrl.pathname = markdownPath;
8807
+ return new Response(null, {
8808
+ headers: {
8809
+ ...headers,
8810
+ "Content-Type": "text/markdown; charset=utf-8",
8811
+ "x-middleware-rewrite": requestUrl.toString(),
8812
+ },
8813
+ });
8814
+ }
8815
+
8816
+ if (KNOWN_ROUTES.has(pathname) || PASSTHROUGH_PATHS.has(pathname)) {
8817
+ return continueRequest();
8818
+ }
8819
+
8820
+ if (isWithinBasePath(pathname) && isMarkdownPath(pathname)) {
8821
+ return new Response(
8822
+ request.method === "HEAD" ? null : MARKDOWN_NOT_FOUND_BODY,
8823
+ {
8824
+ status: 404,
8825
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
8826
+ },
8827
+ );
8828
+ }
8829
+
8830
+ if (!isWithinBasePath(pathname) || !isExtensionlessPath(pathname)) {
8831
+ return continueRequest();
8832
+ }
8833
+
8834
+ const negotiatedType = negotiateContentType(request.headers.get("Accept"));
8835
+ if (negotiatedType === null) {
8836
+ return new Response(request.method === "HEAD" ? null : "Not Acceptable", {
8837
+ status: 406,
8838
+ headers: {
8839
+ "Content-Type": "text/plain; charset=utf-8",
8840
+ Vary: NEGOTIATED_VARY,
8841
+ },
8842
+ });
8843
+ }
8844
+
8845
+ if (negotiatedType === "text/html") {
8846
+ return continueRequest({ Vary: NEGOTIATED_VARY });
8847
+ }
8848
+
8849
+ return new Response(
8850
+ request.method === "HEAD" ? null : MARKDOWN_NOT_FOUND_BODY,
8851
+ {
8852
+ status: 404,
8853
+ headers: {
8854
+ "Content-Type": "text/markdown; charset=utf-8",
8855
+ Vary: NEGOTIATED_VARY,
8856
+ },
8857
+ },
8858
+ );
8859
+ }
8860
+ `.trimStart();
8861
+ };
8862
+
8863
+ // src/vite/output.ts
7875
8864
  var pkgJson = getZudokuPackageJson();
8865
+ var MARKDOWN_MIDDLEWARE_PATH = "zudoku-markdown";
8866
+ var HTML_EXTENSION = ".html";
8867
+ var CLEAN_URL_ROUTES = [
8868
+ {
8869
+ src: "^/(?:(.+)/)?index(?:\\.html)?/?$",
8870
+ headers: { Location: "/$1" },
8871
+ status: 308
8872
+ },
8873
+ {
8874
+ src: "^/(.*)\\.html/?$",
8875
+ headers: { Location: "/$1" },
8876
+ status: 308
8877
+ }
8878
+ ];
8879
+ var listStaticFiles = async (rootDir, currentDir = rootDir) => {
8880
+ if (!existsSync(currentDir)) return [];
8881
+ const entries = await readdir(currentDir, { withFileTypes: true });
8882
+ const files = await Promise.all(
8883
+ entries.map(async (entry) => {
8884
+ const entryPath = path20.join(currentDir, entry.name);
8885
+ if (entry.isDirectory()) {
8886
+ return listStaticFiles(rootDir, entryPath);
8887
+ }
8888
+ if (!entry.isFile()) return [];
8889
+ return [path20.relative(rootDir, entryPath).split(path20.sep).join("/")];
8890
+ })
8891
+ );
8892
+ return files.flat().sort();
8893
+ };
8894
+ var getCleanPath = (filename) => {
8895
+ const pathWithoutExtension = filename.slice(0, -HTML_EXTENSION.length);
8896
+ if (pathWithoutExtension === "index") return "";
8897
+ if (pathWithoutExtension.endsWith("/index")) {
8898
+ return pathWithoutExtension.slice(0, -"/index".length);
8899
+ }
8900
+ return pathWithoutExtension;
8901
+ };
8902
+ var getCleanUrlOverrides = (staticHtmlFiles) => {
8903
+ const sourceByCleanPath = /* @__PURE__ */ new Map();
8904
+ return Object.fromEntries(
8905
+ staticHtmlFiles.map((filename) => {
8906
+ const cleanPath = getCleanPath(filename);
8907
+ const existingSource = sourceByCleanPath.get(cleanPath);
8908
+ invariant(
8909
+ !existingSource || existingSource === filename,
8910
+ `Cannot generate the Vercel clean URL "${cleanPath}" for both "${existingSource}" and "${filename}"`
8911
+ );
8912
+ sourceByCleanPath.set(cleanPath, filename);
8913
+ return [filename, { path: cleanPath }];
8914
+ })
8915
+ );
8916
+ };
8917
+ var cleanVercelOutput = async (dir) => {
8918
+ if (!process.env.VERCEL) return;
8919
+ await rm(path20.join(dir, ".vercel/output"), {
8920
+ recursive: true,
8921
+ force: true
8922
+ });
8923
+ };
7876
8924
  function generateOutput({
7877
8925
  config,
7878
8926
  redirects,
7879
- rewrites = []
8927
+ rewrites = [],
8928
+ markdownNegotiation,
8929
+ staticHtmlFiles = []
7880
8930
  }) {
7881
8931
  const routes = [];
7882
- for (const redirect of redirects) {
8932
+ if (staticHtmlFiles.length > 0) {
8933
+ routes.push(...CLEAN_URL_ROUTES);
8934
+ }
8935
+ const uniqueRedirects = [
8936
+ ...new Map(
8937
+ redirects.map((redirect) => [
8938
+ JSON.stringify([redirect.from, redirect.to]),
8939
+ redirect
8940
+ ])
8941
+ ).values()
8942
+ ];
8943
+ for (const redirect of uniqueRedirects) {
7883
8944
  routes.push({
7884
8945
  src: redirect.from,
7885
8946
  dest: redirect.to,
@@ -7904,6 +8965,19 @@ function generateOutput({
7904
8965
  continue: true
7905
8966
  });
7906
8967
  }
8968
+ if (markdownNegotiation) {
8969
+ const basePath = joinUrl(config.basePath);
8970
+ const escapedBasePath = basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8971
+ routes.push({
8972
+ // Keep the path matcher broad because valid documentation slugs can end
8973
+ // in dotted versions such as `/api/1.0.0`. The middleware itself
8974
+ // cheaply passes through assets and non-document routes.
8975
+ src: basePath === "/" ? "/(.*)" : `${escapedBasePath}(?:/(.*))?`,
8976
+ methods: ["GET", "HEAD"],
8977
+ middlewarePath: MARKDOWN_MIDDLEWARE_PATH,
8978
+ continue: true
8979
+ });
8980
+ }
7907
8981
  if (rewrites.length > 0) {
7908
8982
  routes.push({ handle: "filesystem" });
7909
8983
  for (const rewrite of rewrites) {
@@ -7913,40 +8987,82 @@ function generateOutput({
7913
8987
  });
7914
8988
  }
7915
8989
  }
8990
+ const overrides = getCleanUrlOverrides(staticHtmlFiles);
7916
8991
  const output = {
7917
8992
  version: 3,
7918
8993
  framework: {
7919
8994
  version: pkgJson.version
7920
8995
  },
7921
- routes
8996
+ routes,
8997
+ ...Object.keys(overrides).length > 0 && { overrides }
7922
8998
  };
7923
8999
  return output;
7924
9000
  }
7925
9001
  async function writeOutput(dir, {
7926
9002
  config,
7927
9003
  redirects,
7928
- rewrites
9004
+ rewrites,
9005
+ markdownNegotiation
7929
9006
  }) {
7930
- const output = generateOutput({ config, redirects, rewrites });
7931
- const outputDir = process.env.VERCEL ? path19.join(dir, ".vercel/output") : path19.join(dir, "dist/.output");
9007
+ const vercelMarkdownNegotiation = process.env.VERCEL ? markdownNegotiation : void 0;
9008
+ const staticFiles = process.env.VERCEL ? await listStaticFiles(path20.join(dir, ".vercel/output/static")) : [];
9009
+ const staticHtmlFiles = staticFiles.filter(
9010
+ (filename) => filename.endsWith(HTML_EXTENSION)
9011
+ );
9012
+ const staticPassthroughPaths = staticFiles.filter((filename) => {
9013
+ const basename = path20.posix.basename(filename);
9014
+ return !basename.includes(".") || basename.endsWith(".md") || basename.endsWith(".mdx");
9015
+ }).map((filename) => `/${filename}`);
9016
+ const output = generateOutput({
9017
+ config,
9018
+ redirects,
9019
+ rewrites,
9020
+ markdownNegotiation: vercelMarkdownNegotiation,
9021
+ staticHtmlFiles
9022
+ });
9023
+ const outputDir = process.env.VERCEL ? path20.join(dir, ".vercel/output") : path20.join(dir, "dist/.output");
7932
9024
  await mkdir3(outputDir, { recursive: true });
7933
- const outputFile = path19.join(outputDir, "config.json");
9025
+ const outputFile = path20.join(outputDir, "config.json");
7934
9026
  await writeFile4(outputFile, JSON.stringify(output, null, 2), "utf-8");
7935
9027
  if (process.env.VERCEL) {
9028
+ if (vercelMarkdownNegotiation) {
9029
+ const functionDir = path20.join(
9030
+ outputDir,
9031
+ "functions",
9032
+ `${MARKDOWN_MIDDLEWARE_PATH}.func`
9033
+ );
9034
+ await mkdir3(functionDir, { recursive: true });
9035
+ await Promise.all([
9036
+ writeFile4(
9037
+ path20.join(functionDir, "index.js"),
9038
+ generateVercelMarkdownMiddleware({
9039
+ basePath: config.basePath,
9040
+ ...vercelMarkdownNegotiation,
9041
+ passthroughPaths: staticPassthroughPaths
9042
+ }),
9043
+ "utf-8"
9044
+ ),
9045
+ writeFile4(
9046
+ path20.join(functionDir, ".vc-config.json"),
9047
+ JSON.stringify({ runtime: "edge", entrypoint: "index.js" }, null, 2),
9048
+ "utf-8"
9049
+ )
9050
+ ]);
9051
+ }
7936
9052
  console.log("Wrote Vercel output to", outputDir);
7937
9053
  }
7938
9054
  }
7939
9055
 
7940
9056
  // src/vite/prerender/prerender.ts
7941
9057
  import { readFileSync as readFileSync2 } from "node:fs";
7942
- import { readFile as readFile3, rm } from "node:fs/promises";
9058
+ import { readFile as readFile3, rm as rm2 } from "node:fs/promises";
7943
9059
  import os from "node:os";
7944
- import path22 from "node:path";
9060
+ import path23 from "node:path";
7945
9061
  import { pathToFileURL } from "node:url";
7946
9062
  import { createIndex } from "pagefind";
7947
9063
  import colors7 from "picocolors";
7948
9064
  import PiscinaImport from "piscina";
7949
- init_joinUrl();
9065
+ init_markdown_representation();
7950
9066
 
7951
9067
  // src/vite/reporter.ts
7952
9068
  function writeLine(output) {
@@ -7979,9 +9095,9 @@ function throttle(fn) {
7979
9095
 
7980
9096
  // src/vite/sitemap.ts
7981
9097
  init_joinUrl();
7982
- import { createWriteStream, existsSync } from "node:fs";
9098
+ import { createWriteStream, existsSync as existsSync2 } from "node:fs";
7983
9099
  import { mkdir as mkdir4 } from "node:fs/promises";
7984
- import path20 from "node:path";
9100
+ import path21 from "node:path";
7985
9101
  import colors5 from "picocolors";
7986
9102
  import { SitemapStream } from "sitemap";
7987
9103
  async function generateSitemap({
@@ -7995,11 +9111,11 @@ async function generateSitemap({
7995
9111
  return;
7996
9112
  }
7997
9113
  const sitemap = new SitemapStream({ hostname: config.siteUrl });
7998
- const outputDir = path20.resolve(baseOutputDir, config.outDir ?? "");
7999
- if (!existsSync(outputDir)) {
9114
+ const outputDir = path21.resolve(baseOutputDir, config.outDir ?? "");
9115
+ if (!existsSync2(outputDir)) {
8000
9116
  await mkdir4(outputDir, { recursive: true });
8001
9117
  }
8002
- const sitemapOutputPath = path20.join(outputDir, "sitemap.xml");
9118
+ const sitemapOutputPath = path21.join(outputDir, "sitemap.xml");
8003
9119
  const writeStream = createWriteStream(sitemapOutputPath);
8004
9120
  sitemap.pipe(writeStream);
8005
9121
  let lastmod;
@@ -8029,17 +9145,17 @@ async function generateSitemap({
8029
9145
 
8030
9146
  // src/vite/prerender/utils.ts
8031
9147
  init_joinUrl();
8032
- var resolveRoutePath = (path31) => {
8033
- const segments = path31.split("/");
9148
+ var resolveRoutePath2 = (path32) => {
9149
+ const segments = path32.split("/");
8034
9150
  if (segments.some((s) => s.startsWith(":") && !s.endsWith("?"))) {
8035
9151
  return void 0;
8036
9152
  }
8037
9153
  return segments.filter((s) => !s.startsWith(":")).join("/") || void 0;
8038
9154
  };
8039
- var isSkipped = (path31) => path31.includes("*") || /^\d+$/.test(path31);
9155
+ var isSkipped = (path32) => path32.includes("*") || /^\d+$/.test(path32);
8040
9156
  var resolveRoutes = (routes, parentPath = "") => routes.flatMap((route) => {
8041
9157
  if (route.path && isSkipped(route.path)) return [];
8042
- const routePath = route.path ? resolveRoutePath(route.path) : void 0;
9158
+ const routePath = route.path ? resolveRoutePath2(route.path) : void 0;
8043
9159
  if (route.path && !routePath) return [];
8044
9160
  const fullPath = routePath ? routePath.startsWith("/") ? routePath : joinUrl(parentPath, routePath) : parentPath;
8045
9161
  const hasStrippedParams = route.path?.split("/").some((s) => s.startsWith(":")) ?? false;
@@ -8065,6 +9181,12 @@ var collectRewrites = (resolved) => resolved.flatMap((r) => [
8065
9181
  ...collectRewrites(r.children)
8066
9182
  ]);
8067
9183
  var routesToPaths = (routes) => collectPaths(resolveRoutes(routes), "");
9184
+ var routesToPrerenderPaths = (routes, redirects = []) => Array.from(
9185
+ /* @__PURE__ */ new Set([
9186
+ ...routesToPaths(routes),
9187
+ ...redirects.map(({ from }) => joinUrl(from))
9188
+ ])
9189
+ );
8068
9190
  var routesToRewrites = (routes) => collectRewrites(resolveRoutes(routes));
8069
9191
  var selectPagesToIndex = (pages, paths) => {
8070
9192
  const withUrl = pages.flatMap(({ indexStatusCode, html }, i) => {
@@ -8095,12 +9217,12 @@ var prerender = async ({
8095
9217
  serverConfigFilename,
8096
9218
  writeRedirects = true
8097
9219
  }) => {
8098
- const distDir = path22.join(dir, "dist", basePath);
9220
+ const distDir = path23.join(dir, "dist", basePath);
8099
9221
  const serverConfigPath = pathToFileURL(
8100
- path22.join(distDir, "server", serverConfigFilename)
9222
+ path23.join(distDir, "server", serverConfigFilename)
8101
9223
  ).href;
8102
9224
  const entryServerPath = pathToFileURL(
8103
- path22.join(distDir, "server/entry.server.js")
9225
+ path23.join(distDir, "server/entry.server.js")
8104
9226
  ).href;
8105
9227
  const rawConfig = await import(serverConfigPath).then((m) => m.default);
8106
9228
  const config = validateConfig(await runPluginTransformConfig(rawConfig));
@@ -8108,13 +9230,8 @@ var prerender = async ({
8108
9230
  const module = await import(entryServerPath);
8109
9231
  const getRoutes = module.getRoutesByConfig;
8110
9232
  const routes = getRoutes(config);
8111
- const paths = routesToPaths(routes);
9233
+ const paths = routesToPrerenderPaths(routes, config.redirects);
8112
9234
  const rewrites = routesToRewrites(routes);
8113
- if (config.redirects) {
8114
- for (const r of config.redirects) {
8115
- paths.push(joinUrl(r.from));
8116
- }
8117
- }
8118
9235
  const { maxThreads, maxOldGenerationSizeMb } = getWorkerScaling(
8119
9236
  buildConfig?.prerender?.workers
8120
9237
  );
@@ -8219,7 +9336,7 @@ var prerender = async ({
8219
9336
  }
8220
9337
  if (isTTY()) writeLine("");
8221
9338
  const { outputPath } = await pagefindIndex.writeFiles({
8222
- outputPath: path22.join(distDir, "pagefind")
9339
+ outputPath: path23.join(distDir, "pagefind")
8223
9340
  });
8224
9341
  if (outputPath) {
8225
9342
  const duration = (performance.now() - pagefindStart) / 1e3;
@@ -8239,7 +9356,7 @@ var prerender = async ({
8239
9356
  redirectUrls
8240
9357
  });
8241
9358
  const llmsConfig = config.docs.llms;
8242
- const markdownInfoPath = path22.join(
9359
+ const markdownInfoPath = path23.join(
8243
9360
  dir,
8244
9361
  "node_modules/.zudoku/markdown-info.json"
8245
9362
  );
@@ -8258,22 +9375,38 @@ var prerender = async ({
8258
9375
  siteName: config.site?.title,
8259
9376
  llmsTxt: llmsConfig.llmsTxt,
8260
9377
  llmsTxtFull: llmsConfig.llmsTxtFull,
9378
+ title: llmsConfig.title,
9379
+ description: llmsConfig.description,
9380
+ instructions: llmsConfig.instructions,
8261
9381
  redirectUrls
8262
9382
  });
8263
9383
  }
9384
+ const contentNegotiationEnabled = config.docs.publishMarkdown && config.docs.contentNegotiation;
9385
+ const markdownNotFound = contentNegotiationEnabled ? getMarkdownNotFound({
9386
+ basePath: config.basePath,
9387
+ includeLlmsTxt: llmsConfig.llmsTxt ?? false,
9388
+ markdownRoutePaths: markdownFileInfos.map((info) => info.routePath),
9389
+ sitemapOutDir: config.sitemap ? config.sitemap.outDir ?? "" : void 0
9390
+ }) : void 0;
8264
9391
  if (!config.docs.publishMarkdown) {
8265
9392
  await Promise.all(
8266
9393
  markdownFileInfos.map((info) => {
8267
9394
  const outputPath = getMarkdownOutputPath(distDir, info.routePath);
8268
- if (!path22.resolve(outputPath).startsWith(path22.resolve(distDir))) {
9395
+ if (!path23.resolve(outputPath).startsWith(path23.resolve(distDir))) {
8269
9396
  return;
8270
9397
  }
8271
- return rm(outputPath).catch(() => {
9398
+ return rm2(outputPath).catch(() => {
8272
9399
  });
8273
9400
  })
8274
9401
  );
8275
9402
  }
8276
- return { workerResults, rewrites };
9403
+ return {
9404
+ workerResults,
9405
+ rewrites,
9406
+ knownRoutes: paths,
9407
+ markdownRoutes: markdownFileInfos.map((info) => info.routePath),
9408
+ markdownNotFound
9409
+ };
8277
9410
  };
8278
9411
  var getWorkerScaling = (workersOverride) => {
8279
9412
  const PER_WORKER_HEAP_LIMIT_MB = 4096;
@@ -8323,8 +9456,8 @@ var getContainerMemoryLimitMb = () => {
8323
9456
  };
8324
9457
 
8325
9458
  // src/vite/protected/build.ts
8326
- import { mkdir as mkdir5, readdir, readFile as readFile4, rename, rm as rm2 } from "node:fs/promises";
8327
- import path23 from "node:path";
9459
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile4, rename, rm as rm3 } from "node:fs/promises";
9460
+ import path24 from "node:path";
8328
9461
  init_joinUrl();
8329
9462
  var assertProtectedPatternsCovered = (config) => {
8330
9463
  const { patterns } = getProtectedSourceMatcher(config);
@@ -8346,11 +9479,11 @@ var findProtectedLeaks = (output) => {
8346
9479
  const visited = /* @__PURE__ */ new Set();
8347
9480
  const stack = [{ fileName: entry.fileName, path: [entry.fileName] }];
8348
9481
  while (stack.length > 0) {
8349
- const { fileName, path: path31 } = stack.pop();
9482
+ const { fileName, path: path32 } = stack.pop();
8350
9483
  if (visited.has(fileName)) continue;
8351
9484
  visited.add(fileName);
8352
9485
  for (const imp of byFileName.get(fileName)?.imports ?? []) {
8353
- const next = [...path31, imp];
9486
+ const next = [...path32, imp];
8354
9487
  if (isProtected(imp)) {
8355
9488
  leaks.push(next.join(" -> "));
8356
9489
  continue;
@@ -8381,27 +9514,27 @@ This eagerly pulls gated content into the public bundle. Check that nothing in n
8381
9514
  );
8382
9515
  };
8383
9516
  var moveProtectedChunks = async (clientOutDir, serverOutDir) => {
8384
- const srcDir = path23.join(clientOutDir, PROTECTED_CHUNK_DIR);
8385
- const files = await readdir(srcDir).catch((err) => {
9517
+ const srcDir = path24.join(clientOutDir, PROTECTED_CHUNK_DIR);
9518
+ const files = await readdir2(srcDir).catch((err) => {
8386
9519
  if (err.code === "ENOENT") return null;
8387
9520
  throw err;
8388
9521
  });
8389
9522
  if (!files) return;
8390
- const destDir = path23.join(serverOutDir, PROTECTED_CHUNK_DIR);
9523
+ const destDir = path24.join(serverOutDir, PROTECTED_CHUNK_DIR);
8391
9524
  await mkdir5(destDir, { recursive: true });
8392
9525
  await Promise.all(
8393
9526
  files.map(
8394
- (file) => rename(path23.join(srcDir, file), path23.join(destDir, file))
9527
+ (file) => rename(path24.join(srcDir, file), path24.join(destDir, file))
8395
9528
  )
8396
9529
  );
8397
- const leftover = await readdir(srcDir).catch(() => []);
9530
+ const leftover = await readdir2(srcDir).catch(() => []);
8398
9531
  if (leftover.length > 0) {
8399
9532
  throw new Error(
8400
9533
  `moveProtectedChunks left ${leftover.length} file(s) in ${srcDir}: ${leftover.join(", ")}.
8401
9534
  These would be served publicly. Aborting build.`
8402
9535
  );
8403
9536
  }
8404
- await rm2(srcDir, { recursive: true, force: true });
9537
+ await rm3(srcDir, { recursive: true, force: true });
8405
9538
  };
8406
9539
  var assertCloudflareWranglerGatesProtected = async (dir, config) => {
8407
9540
  const { enabled } = getProtectedSourceMatcher(config);
@@ -8409,7 +9542,7 @@ var assertCloudflareWranglerGatesProtected = async (dir, config) => {
8409
9542
  const protectedPrefix = `${joinUrl(config.basePath, PROTECTED_CHUNK_DIR)}/`;
8410
9543
  const candidates = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
8411
9544
  for (const name of candidates) {
8412
- const file = await readFile4(path23.join(dir, name), "utf-8").catch(
9545
+ const file = await readFile4(path24.join(dir, name), "utf-8").catch(
8413
9546
  () => void 0
8414
9547
  );
8415
9548
  if (file === void 0) continue;
@@ -8437,8 +9570,11 @@ async function runBuild(options) {
8437
9570
  const builder2 = await createBuilder(viteConfig);
8438
9571
  invariant(builder2.environments.client, "Client environment is missing");
8439
9572
  invariant(builder2.environments.ssr, "SSR environment is missing");
8440
- const distDir = path24.resolve(path24.join(dir, "dist"));
8441
- await rm3(distDir, { recursive: true, force: true });
9573
+ const distDir = path25.resolve(path25.join(dir, "dist"));
9574
+ await Promise.all([
9575
+ rm4(distDir, { recursive: true, force: true }),
9576
+ cleanVercelOutput(dir)
9577
+ ]);
8442
9578
  const [clientResult, serverResult] = await Promise.all([
8443
9579
  builder2.build(builder2.environments.client),
8444
9580
  builder2.build(builder2.environments.ssr)
@@ -8487,13 +9623,13 @@ async function runBuild(options) {
8487
9623
  await assertCloudflareWranglerGatesProtected(dir, config);
8488
9624
  }
8489
9625
  await writeFile6(
8490
- path24.join(distDir, "package.json"),
9626
+ path25.join(distDir, "package.json"),
8491
9627
  `${JSON.stringify({ type: "module" }, null, 2)}
8492
9628
  `,
8493
9629
  "utf-8"
8494
9630
  );
8495
9631
  await writeManifest(distDir, config);
8496
- await rm3(path24.join(clientOutDir, "index.html"), { force: true });
9632
+ await rm4(path25.join(clientOutDir, "index.html"), { force: true });
8497
9633
  } else {
8498
9634
  await runPrerender({
8499
9635
  dir,
@@ -8517,43 +9653,54 @@ var runPrerender = async (options) => {
8517
9653
  const issuer = await getIssuer(config);
8518
9654
  const serverConfigFilename = findServerConfigFilename(serverResult);
8519
9655
  try {
8520
- const { workerResults, rewrites } = await prerender({
9656
+ const {
9657
+ workerResults,
9658
+ rewrites,
9659
+ knownRoutes,
9660
+ markdownRoutes,
9661
+ markdownNotFound
9662
+ } = await prerender({
8521
9663
  html,
8522
9664
  dir,
8523
9665
  basePath: config.basePath,
8524
9666
  serverConfigFilename,
8525
9667
  writeRedirects: process.env.VERCEL === void 0
8526
9668
  });
8527
- const indexHtml = path24.join(clientOutDir, "index.html");
9669
+ const indexHtml = path25.join(clientOutDir, "index.html");
8528
9670
  if (!workerResults.find((r) => r.outputPath === indexHtml)) {
8529
9671
  await writeFile6(indexHtml, html, "utf-8");
8530
9672
  }
8531
9673
  const statusPages = workerResults.flatMap(
8532
- (r) => /^(400|404|500)\.html$/.test(path24.basename(r.outputPath)) ? r.outputPath : []
9674
+ (r) => /^(400|404|500)\.html$/.test(path25.basename(r.outputPath)) ? r.outputPath : []
8533
9675
  );
8534
9676
  for (const statusPage of statusPages) {
8535
9677
  await rename2(
8536
9678
  statusPage,
8537
- path24.join(dir, DIST_DIR, path24.basename(statusPage))
9679
+ path25.join(dir, DIST_DIR, path25.basename(statusPage))
8538
9680
  );
8539
9681
  }
8540
- await rm3(serverOutDir, { recursive: true, force: true });
9682
+ await rm4(serverOutDir, { recursive: true, force: true });
8541
9683
  if (process.env.VERCEL) {
8542
- await mkdir6(path24.join(dir, ".vercel/output/static"), { recursive: true });
9684
+ await mkdir6(path25.join(dir, ".vercel/output/static"), { recursive: true });
8543
9685
  await rename2(
8544
- path24.join(dir, DIST_DIR),
8545
- path24.join(dir, ".vercel/output/static")
9686
+ path25.join(dir, DIST_DIR),
9687
+ path25.join(dir, ".vercel/output/static")
8546
9688
  );
8547
9689
  }
8548
9690
  await writeOutput(dir, {
8549
9691
  config,
8550
9692
  redirects: workerResults.flatMap((r) => r.redirect ?? []),
8551
- rewrites
9693
+ rewrites,
9694
+ markdownNegotiation: markdownNotFound ? {
9695
+ knownCanonicalRoutePaths: knownRoutes,
9696
+ markdownCanonicalRoutePaths: markdownRoutes,
9697
+ markdownNotFoundBody: markdownNotFound
9698
+ } : void 0
8552
9699
  });
8553
9700
  if (ZuploEnv.isZuplo && issuer) {
8554
9701
  const provider = config.authentication?.type;
8555
9702
  await writeFile6(
8556
- path24.join(dir, DIST_DIR, ".output/zuplo.json"),
9703
+ path25.join(dir, DIST_DIR, ".output/zuplo.json"),
8557
9704
  JSON.stringify({ issuer, provider }, null, 2),
8558
9705
  "utf-8"
8559
9706
  );
@@ -8565,8 +9712,8 @@ var runPrerender = async (options) => {
8565
9712
  };
8566
9713
  var findUserEntry = (dir) => {
8567
9714
  for (const ext of ["ts", "tsx", "js", "mjs"]) {
8568
- const candidate = path24.join(dir, `zudoku.server.${ext}`);
8569
- if (existsSync2(candidate)) return candidate;
9715
+ const candidate = path25.join(dir, `zudoku.server.${ext}`);
9716
+ if (existsSync3(candidate)) return candidate;
8570
9717
  }
8571
9718
  };
8572
9719
  var bundleSSREntry = async (options) => {
@@ -8576,15 +9723,15 @@ var bundleSSREntry = async (options) => {
8576
9723
  let entryPoint = userEntry;
8577
9724
  let tempEntryPath;
8578
9725
  if (!entryPoint) {
8579
- tempEntryPath = path24.join(dir, "__ssr-entry.ts");
9726
+ tempEntryPath = path25.join(dir, "__ssr-entry.ts");
8580
9727
  const templateContent = await readFile5(
8581
- path24.join(packageRoot, "src/vite/ssr-templates", `${adapter}.ts`),
9728
+ path25.join(packageRoot, "src/vite/ssr-templates", `${adapter}.ts`),
8582
9729
  "utf-8"
8583
9730
  );
8584
9731
  await writeFile6(tempEntryPath, templateContent, "utf-8");
8585
9732
  entryPoint = tempEntryPath;
8586
9733
  }
8587
- const frameworkPath = path24.join(serverOutDir, "entry.server.js");
9734
+ const frameworkPath = path25.join(serverOutDir, "entry.server.js");
8588
9735
  try {
8589
9736
  await esbuild({
8590
9737
  entryPoints: [entryPoint],
@@ -8592,9 +9739,9 @@ var bundleSSREntry = async (options) => {
8592
9739
  platform: ["node", "lambda"].includes(adapter) ? "node" : "neutral",
8593
9740
  target: "es2022",
8594
9741
  format: "esm",
8595
- outfile: path24.join(serverOutDir, "entry.js"),
9742
+ outfile: path25.join(serverOutDir, "entry.js"),
8596
9743
  external: ["./zudoku.config.js"],
8597
- nodePaths: [path24.join(packageRoot, "node_modules")],
9744
+ nodePaths: [path25.join(packageRoot, "node_modules")],
8598
9745
  banner: { js: "// Bundled SSR entry" },
8599
9746
  define: {
8600
9747
  __ZUDOKU_TEMPLATE__: JSON.stringify(html)
@@ -8611,15 +9758,15 @@ var bundleSSREntry = async (options) => {
8611
9758
  ]
8612
9759
  });
8613
9760
  await Promise.all([
8614
- rm3(frameworkPath, { force: true }),
8615
- rm3(`${frameworkPath}.map`, { force: true }),
8616
- rm3(path24.join(serverOutDir, "assets"), {
9761
+ rm4(frameworkPath, { force: true }),
9762
+ rm4(`${frameworkPath}.map`, { force: true }),
9763
+ rm4(path25.join(serverOutDir, "assets"), {
8617
9764
  recursive: true,
8618
9765
  force: true
8619
9766
  })
8620
9767
  ]);
8621
9768
  } finally {
8622
- if (tempEntryPath) await rm3(tempEntryPath, { force: true });
9769
+ if (tempEntryPath) await rm4(tempEntryPath, { force: true });
8623
9770
  }
8624
9771
  };
8625
9772
 
@@ -8633,11 +9780,11 @@ function printWarningToConsole(message) {
8633
9780
  }
8634
9781
 
8635
9782
  // src/cli/preview/handler.ts
8636
- import path25 from "node:path";
9783
+ import path26 from "node:path";
8637
9784
  import { preview as vitePreview } from "vite";
8638
9785
  var DEFAULT_PREVIEW_PORT = 4e3;
8639
9786
  async function preview(argv) {
8640
- const dir = path25.resolve(process.cwd(), argv.dir);
9787
+ const dir = path26.resolve(process.cwd(), argv.dir);
8641
9788
  const viteConfig = await getViteConfig(dir, {
8642
9789
  command: "serve",
8643
9790
  mode: "production",
@@ -8675,7 +9822,7 @@ async function build(argv) {
8675
9822
  printDiagnosticsToConsole(`Starting Zudoku build v${packageJson2.version}`);
8676
9823
  printDiagnosticsToConsole("");
8677
9824
  printDiagnosticsToConsole("");
8678
- const dir = path26.resolve(process.cwd(), argv.dir);
9825
+ const dir = path27.resolve(process.cwd(), argv.dir);
8679
9826
  try {
8680
9827
  await runBuild({
8681
9828
  dir,
@@ -8834,13 +9981,13 @@ var build_default = {
8834
9981
 
8835
9982
  // src/cli/dev/handler.ts
8836
9983
  init_joinUrl();
8837
- import path29 from "node:path";
9984
+ import path30 from "node:path";
8838
9985
 
8839
9986
  // src/vite/dev-server.ts
8840
- import fs3 from "node:fs/promises";
9987
+ import fs4 from "node:fs/promises";
8841
9988
  import http from "node:http";
8842
9989
  import https from "node:https";
8843
- import path28 from "node:path";
9990
+ import path29 from "node:path";
8844
9991
  import { stripVTControlCharacters } from "node:util";
8845
9992
  import { createHttpTerminator } from "http-terminator";
8846
9993
  import {
@@ -8872,7 +10019,7 @@ async function findAvailablePort(startPort) {
8872
10019
  init_joinUrl();
8873
10020
 
8874
10021
  // src/vite/pagefind-dev-index.ts
8875
- import path27 from "node:path";
10022
+ import path28 from "node:path";
8876
10023
  import { createIndex as createIndex2 } from "pagefind";
8877
10024
  import { isRunnableDevEnvironment } from "vite";
8878
10025
  init_joinUrl();
@@ -8926,7 +10073,7 @@ async function* buildPagefindDevIndex(vite, config) {
8926
10073
  path: urlPath
8927
10074
  };
8928
10075
  }
8929
- const outputPath = path27.join(vite.config.publicDir, "pagefind");
10076
+ const outputPath = path28.join(vite.config.publicDir, "pagefind");
8930
10077
  await pagefindIndex.writeFiles({ outputPath });
8931
10078
  yield { type: "complete", success: true, indexed };
8932
10079
  }
@@ -8946,9 +10093,9 @@ var DevServer = class {
8946
10093
  this.protocol = "https";
8947
10094
  const { dir } = this.#options;
8948
10095
  const [key, cert, ca] = await Promise.all([
8949
- fs3.readFile(path28.resolve(dir, config.https.key)),
8950
- fs3.readFile(path28.resolve(dir, config.https.cert)),
8951
- config.https.ca ? fs3.readFile(path28.resolve(dir, config.https.ca)) : void 0
10096
+ fs4.readFile(path29.resolve(dir, config.https.key)),
10097
+ fs4.readFile(path29.resolve(dir, config.https.cert)),
10098
+ config.https.ca ? fs4.readFile(path29.resolve(dir, config.https.ca)) : void 0
8952
10099
  ]);
8953
10100
  return https.createServer({ key, cert, ca });
8954
10101
  }
@@ -8976,7 +10123,7 @@ var DevServer = class {
8976
10123
  // built-in transform middleware which would treat the path as a static asset.
8977
10124
  name: "zudoku:entry-client",
8978
10125
  configureServer(server2) {
8979
- const entryPath = path28.posix.join(
10126
+ const entryPath = path29.posix.join(
8980
10127
  server2.config.base,
8981
10128
  "/__z/entry.client.tsx"
8982
10129
  );
@@ -9088,14 +10235,14 @@ var DevServer = class {
9088
10235
  `Server-side rendering ${this.#options.ssr ? "enabled" : "disabled"}`
9089
10236
  );
9090
10237
  if (config.search?.type === "pagefind") {
9091
- const pagefindPath = path28.join(
10238
+ const pagefindPath = path29.join(
9092
10239
  vite.config.publicDir,
9093
10240
  "pagefind/pagefind.js"
9094
10241
  );
9095
- const exists = await fs3.stat(pagefindPath).catch(() => false);
10242
+ const exists = await fs4.stat(pagefindPath).catch(() => false);
9096
10243
  if (!exists) {
9097
- await fs3.mkdir(path28.dirname(pagefindPath), { recursive: true });
9098
- await fs3.writeFile(pagefindPath, 'throw new Error("NOT_BUILT_YET");');
10244
+ await fs4.mkdir(path29.dirname(pagefindPath), { recursive: true });
10245
+ await fs4.writeFile(pagefindPath, 'throw new Error("NOT_BUILT_YET");');
9099
10246
  }
9100
10247
  }
9101
10248
  vite.middlewares.use(async (req, res) => {
@@ -9196,7 +10343,7 @@ var DevServer = class {
9196
10343
  async function dev(argv) {
9197
10344
  const packageJson2 = getZudokuPackageJson();
9198
10345
  process.env.NODE_ENV = "development";
9199
- const dir = path29.resolve(process.cwd(), argv.dir);
10346
+ const dir = path30.resolve(process.cwd(), argv.dir);
9200
10347
  const server = new DevServer({
9201
10348
  dir,
9202
10349
  argPort: argv.port,
@@ -9301,7 +10448,7 @@ var previewCommand = {
9301
10448
  var preview_default = previewCommand;
9302
10449
 
9303
10450
  // src/cli/common/outdated.ts
9304
- import { existsSync as existsSync3, mkdirSync } from "node:fs";
10451
+ import { existsSync as existsSync4, mkdirSync } from "node:fs";
9305
10452
  import { readFile as readFile6, writeFile as writeFile7 } from "node:fs/promises";
9306
10453
  import { join } from "node:path";
9307
10454
  import colors10 from "picocolors";
@@ -9352,12 +10499,12 @@ function box(message, {
9352
10499
 
9353
10500
  // src/cli/common/xdg/lib.ts
9354
10501
  import { homedir } from "node:os";
9355
- import path30 from "node:path";
10502
+ import path31 from "node:path";
9356
10503
  function defineDirectoryWithFallback(xdgName, fallback) {
9357
10504
  if (process.env[xdgName]) {
9358
10505
  return process.env[xdgName];
9359
10506
  } else {
9360
- return path30.join(homedir(), fallback);
10507
+ return path31.join(homedir(), fallback);
9361
10508
  }
9362
10509
  }
9363
10510
  var XDG_CONFIG_HOME = defineDirectoryWithFallback(
@@ -9372,15 +10519,15 @@ var XDG_STATE_HOME = defineDirectoryWithFallback(
9372
10519
  "XDG_DATA_HOME",
9373
10520
  ".local/state"
9374
10521
  );
9375
- var ZUDOKU_XDG_CONFIG_HOME = path30.join(
10522
+ var ZUDOKU_XDG_CONFIG_HOME = path31.join(
9376
10523
  XDG_CONFIG_HOME,
9377
10524
  CLI_XDG_FOLDER_NAME
9378
10525
  );
9379
- var ZUDOKU_XDG_DATA_HOME = path30.join(
10526
+ var ZUDOKU_XDG_DATA_HOME = path31.join(
9380
10527
  XDG_DATA_HOME,
9381
10528
  CLI_XDG_FOLDER_NAME
9382
10529
  );
9383
- var ZUDOKU_XDG_STATE_HOME = path30.join(
10530
+ var ZUDOKU_XDG_STATE_HOME = path31.join(
9384
10531
  XDG_STATE_HOME,
9385
10532
  CLI_XDG_FOLDER_NAME
9386
10533
  );
@@ -9422,12 +10569,12 @@ async function getLatestVersion() {
9422
10569
  return void 0;
9423
10570
  }
9424
10571
  async function getVersionCheckInfo() {
9425
- if (!existsSync3(ZUDOKU_XDG_STATE_HOME)) {
10572
+ if (!existsSync4(ZUDOKU_XDG_STATE_HOME)) {
9426
10573
  mkdirSync(ZUDOKU_XDG_STATE_HOME, { recursive: true });
9427
10574
  }
9428
10575
  const versionCheckPath = join(ZUDOKU_XDG_STATE_HOME, VERSION_CHECK_FILE);
9429
10576
  let versionCheckInfo;
9430
- if (existsSync3(versionCheckPath)) {
10577
+ if (existsSync4(versionCheckPath)) {
9431
10578
  try {
9432
10579
  versionCheckInfo = await readFile6(versionCheckPath, "utf-8").then(
9433
10580
  JSON.parse