@dogsbay/format-starlight 0.2.0-beta.13 → 0.2.0-beta.131
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/adapter.d.ts +69 -1
- package/dist/adapters/cloudflare.d.ts +183 -1
- package/dist/adapters/cloudflare.js +1995 -60
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +671 -6
- package/dist/fences.d.ts +16 -0
- package/dist/fences.js +49 -0
- package/dist/github-slug.d.ts +19 -0
- package/dist/github-slug.js +112 -0
- package/dist/js-literal.d.ts +12 -0
- package/dist/js-literal.js +249 -0
- package/dist/jsx-to-jinja.d.ts +62 -0
- package/dist/jsx-to-jinja.js +869 -0
- package/dist/parse-mdx.d.ts +61 -1
- package/dist/parse-mdx.js +465 -31
- package/dist/parse.js +11 -4
- package/dist/template-tokens.d.ts +63 -0
- package/dist/template-tokens.js +206 -0
- package/package.json +9 -7
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,15 @@ import { join, relative, dirname } from "node:path";
|
|
|
3
3
|
import { parseMeta } from "@dogsbay/types";
|
|
4
4
|
import { starlightToTree as starlightToTreeLegacy, extractFrontmatter } from "./parse.js";
|
|
5
5
|
import { starlightToTree as starlightToTreeMdx } from "./parse-mdx.js";
|
|
6
|
+
import { jsxPropsToJinja, newStats as newJsxStats } from "./jsx-to-jinja.js";
|
|
7
|
+
import { DirectiveTable, escapeStrayOpeners, restoreDirectives } from "./template-tokens.js";
|
|
8
|
+
import { TemplatePages, dogsbayMdToTree, extractHeadings, treeToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
|
|
6
9
|
import { cloudflareAdapter } from "./adapters/cloudflare.js";
|
|
7
10
|
import { buildNavFromDirectory, buildNavFromConfig } from "./nav.js";
|
|
8
|
-
const
|
|
11
|
+
export const STARLIGHT_ADAPTERS = {
|
|
9
12
|
cloudflare: cloudflareAdapter,
|
|
10
13
|
};
|
|
14
|
+
const ADAPTERS = STARLIGHT_ADAPTERS;
|
|
11
15
|
/**
|
|
12
16
|
* Detect if a directory contains a Starlight docs source.
|
|
13
17
|
*
|
|
@@ -136,22 +140,347 @@ async function importStarlight(source, opts) {
|
|
|
136
140
|
if (opts.partialsDir) {
|
|
137
141
|
partialsDir = opts.partialsDir;
|
|
138
142
|
}
|
|
143
|
+
// Where the images live. Found the same way as partialsDir, because it is
|
|
144
|
+
// the same Astro layout: `src/content/docs` and `src/assets` are siblings
|
|
145
|
+
// under `src/`.
|
|
146
|
+
//
|
|
147
|
+
// Without this the conversion emitted `/_assets/images/x.png` refs and
|
|
148
|
+
// nothing ever copied a file there — every local image on a converted site
|
|
149
|
+
// was a 404. The reference survived and the file did not, which is why a
|
|
150
|
+
// page count, a retention score and a clean exit all looked fine.
|
|
151
|
+
let assetsDir;
|
|
152
|
+
searchDir = docsDir;
|
|
153
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
154
|
+
const candidate = join(searchDir, "src", "assets");
|
|
155
|
+
if (existsSync(candidate)) {
|
|
156
|
+
assetsDir = candidate;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
const sibling = join(dirname(searchDir), "assets");
|
|
160
|
+
if (existsSync(sibling)) {
|
|
161
|
+
assetsDir = sibling;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const parent = dirname(searchDir);
|
|
165
|
+
if (parent === searchDir)
|
|
166
|
+
break;
|
|
167
|
+
searchDir = parent;
|
|
168
|
+
}
|
|
169
|
+
// Product/brand icons, mounted under `_assets/icons` to match the refs the
|
|
170
|
+
// adapters emit for them.
|
|
171
|
+
let iconsDir;
|
|
172
|
+
searchDir = docsDir;
|
|
173
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
174
|
+
const candidate = join(searchDir, "src", "icons");
|
|
175
|
+
if (existsSync(candidate)) {
|
|
176
|
+
iconsDir = candidate;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const parent = dirname(searchDir);
|
|
180
|
+
if (parent === searchDir)
|
|
181
|
+
break;
|
|
182
|
+
searchDir = parent;
|
|
183
|
+
}
|
|
184
|
+
// Component sources, for recovering an accessible description from a
|
|
185
|
+
// component we cannot render. Same sibling layout as assets and partials.
|
|
186
|
+
let componentsDir;
|
|
187
|
+
searchDir = docsDir;
|
|
188
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
189
|
+
const candidate = join(searchDir, "src", "components");
|
|
190
|
+
if (existsSync(candidate)) {
|
|
191
|
+
componentsDir = candidate;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
const parent = dirname(searchDir);
|
|
195
|
+
if (parent === searchDir)
|
|
196
|
+
break;
|
|
197
|
+
searchDir = parent;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The `aria-label` (or `<figcaption>`) a component declares about itself.
|
|
201
|
+
*
|
|
202
|
+
* Cloudflare's diagrams are Astro components — inline SVG, a web component,
|
|
203
|
+
* CSS animation — with no image file to carry across. But their `aria-label`
|
|
204
|
+
* is a full description written by their docs team, and it is the
|
|
205
|
+
* information the diagram exists to convey. Recovering it turns a silent
|
|
206
|
+
* hole into a described placeholder; nothing else about the component can
|
|
207
|
+
* survive a conversion to markdown.
|
|
208
|
+
*/
|
|
209
|
+
/** `tag\u0000how` → count of conversions to a lesser form. */
|
|
210
|
+
const degraded = new Map();
|
|
211
|
+
const DESCRIBED = "its description only: the diagram is drawn by the component";
|
|
212
|
+
/** Tag → component file, from the site's `src/components.ts` barrel (what `~/components` imports resolve to). */
|
|
213
|
+
let barrel;
|
|
214
|
+
let byName;
|
|
215
|
+
/**
|
|
216
|
+
* Where the page being parsed imports each component from: a path under
|
|
217
|
+
* `~/components/` (that file), `null` for the barrel (`~/components`),
|
|
218
|
+
* or `false` for anywhere else (a package, a relative file) — then no
|
|
219
|
+
* description is claimed: a same-named file under components/ would be the
|
|
220
|
+
* WRONG component, and its label would hide the loss as a success.
|
|
221
|
+
*/
|
|
222
|
+
let pageImports = new Map();
|
|
223
|
+
const importsOf = (src) => {
|
|
224
|
+
const map = new Map();
|
|
225
|
+
for (const m of src.matchAll(/^import\s+(?:(\w+)|\{([^}]*)\})\s+from\s+["']([^"']+)["']/gm)) {
|
|
226
|
+
const from = m[3];
|
|
227
|
+
const where = from === "~/components" ? null : from.startsWith("~/components/") && from.endsWith(".astro") ? from.slice("~/components/".length) : false;
|
|
228
|
+
if (m[1])
|
|
229
|
+
map.set(m[1], where);
|
|
230
|
+
for (const n of (m[2] ?? "").split(",")) {
|
|
231
|
+
const name = n.trim().split(/\s+as\s+/).pop();
|
|
232
|
+
if (name)
|
|
233
|
+
map.set(name, where === false ? false : null);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return map;
|
|
237
|
+
};
|
|
238
|
+
const componentFile = (tag) => {
|
|
239
|
+
if (!componentsDir)
|
|
240
|
+
return undefined;
|
|
241
|
+
const imported = pageImports.get(tag);
|
|
242
|
+
if (imported === false)
|
|
243
|
+
return undefined;
|
|
244
|
+
if (typeof imported === "string") {
|
|
245
|
+
const f = join(componentsDir, imported);
|
|
246
|
+
return existsSync(f) ? f : undefined;
|
|
247
|
+
}
|
|
248
|
+
if (!barrel) {
|
|
249
|
+
barrel = new Map();
|
|
250
|
+
try {
|
|
251
|
+
const src = readFileSync(join(dirname(componentsDir), "components.ts"), "utf-8");
|
|
252
|
+
for (const m of src.matchAll(/export\s*\{\s*default\s+as\s+(\w+)\s*\}\s*from\s*["']\.\/components\/([^"']+\.astro)["']/g)) {
|
|
253
|
+
barrel.set(m[1], join(componentsDir, m[2]));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// No barrel: the directory guesses below.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const candidates = [barrel.get(tag), join(componentsDir, `${tag}.astro`), join(componentsDir, "cf", `${tag}.astro`)];
|
|
261
|
+
const found = candidates.find((f) => !!f && existsSync(f));
|
|
262
|
+
if (found)
|
|
263
|
+
return found;
|
|
264
|
+
// Imported from the barrel but not in it: nothing to guess.
|
|
265
|
+
if (imported === null)
|
|
266
|
+
return undefined;
|
|
267
|
+
// Pages also import components by PATH (`~/components/realtime/sfu/X.astro`),
|
|
268
|
+
// outside the barrel: a name unique under components/ is that file.
|
|
269
|
+
if (!byName) {
|
|
270
|
+
byName = new Map();
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
const walk = (dir) => {
|
|
273
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
274
|
+
const p = join(dir, e.name);
|
|
275
|
+
if (e.isDirectory())
|
|
276
|
+
walk(p);
|
|
277
|
+
else if (e.name.endsWith(".astro")) {
|
|
278
|
+
const n = e.name.slice(0, -6);
|
|
279
|
+
if (seen.has(n))
|
|
280
|
+
byName.delete(n);
|
|
281
|
+
else
|
|
282
|
+
byName.set(n, p);
|
|
283
|
+
seen.add(n);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
try {
|
|
288
|
+
walk(componentsDir);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// An unreadable folder: fewer components described.
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return byName.get(tag);
|
|
295
|
+
};
|
|
296
|
+
const describeComponent = (tag) => {
|
|
297
|
+
const file = componentFile(tag);
|
|
298
|
+
if (!file)
|
|
299
|
+
return undefined;
|
|
300
|
+
const raw = readFileSync(file, "utf-8");
|
|
301
|
+
// The template only: a string in the frontmatter script is not a label.
|
|
302
|
+
const fmEnd = raw.startsWith("---") ? raw.indexOf("\n---", 3) : -1;
|
|
303
|
+
const source = raw.slice(fmEnd < 0 ? 0 : fmEnd + 4);
|
|
304
|
+
// 20+ characters: a sub-part label ("Tools") must not stand in for the
|
|
305
|
+
// whole diagram.
|
|
306
|
+
const aria = source.match(/aria-label="([^"{}]{20,})"/);
|
|
307
|
+
// A caption may hold markup (`<strong>`, links): its TEXT is the description.
|
|
308
|
+
const caption = aria ? undefined : source.match(/<figcaption[^>]*>([\s\S]*?)<\/figcaption>/);
|
|
309
|
+
// `{" "}` and other string literals are text; a dynamic `{…}` is refused
|
|
310
|
+
// (it would leave "Figure : …"); common entities decoded.
|
|
311
|
+
const captionSrc = caption?.[1].replace(/\{\s*(["'])([^"']*)\1\s*\}/g, "$2");
|
|
312
|
+
const captionText = captionSrc && !/[{}]/.test(captionSrc)
|
|
313
|
+
? captionSrc
|
|
314
|
+
.replace(/<[^>]+>/g, "")
|
|
315
|
+
.replace(/&(mdash|ndash|rsquo|lsquo|rdquo|ldquo|amp|nbsp|hellip|rarr);/g, (_m, e) => ({ mdash: "—", ndash: "–", rsquo: "’", lsquo: "‘", rdquo: "”", ldquo: "“", amp: "&", nbsp: " ", hellip: "…", rarr: "→" })[e])
|
|
316
|
+
: undefined;
|
|
317
|
+
const text = (aria?.[1] ?? (captionText && captionText.replace(/\s+/g, " ").trim().length >= 20 ? captionText : undefined))?.replace(/\s+/g, " ").trim();
|
|
318
|
+
// A placeholder is a loss: reported, not silent (9 uses were).
|
|
319
|
+
if (text)
|
|
320
|
+
degraded.set(`${tag}\u0000${DESCRIBED}`, (degraded.get(`${tag}\u0000${DESCRIBED}`) ?? 0) + 1);
|
|
321
|
+
return text;
|
|
322
|
+
};
|
|
139
323
|
// Parse all pages
|
|
140
324
|
const mdxFiles = walkMdxFiles(docsDir, docsDir);
|
|
141
325
|
const pages = [];
|
|
326
|
+
/** Components no builtin or adapter mapping claimed. */
|
|
327
|
+
const dropped = new Map();
|
|
328
|
+
const record = (tag, slug) => {
|
|
329
|
+
const entry = dropped.get(tag) ?? { count: 0, pages: new Set() };
|
|
330
|
+
entry.count++;
|
|
331
|
+
entry.pages.add(slug);
|
|
332
|
+
dropped.set(tag, entry);
|
|
333
|
+
};
|
|
142
334
|
// Custom taxonomy names from dogsbay.config.yml's `taxonomies:` block
|
|
143
335
|
// (threaded through cli's import-content.ts).
|
|
144
336
|
const taxonomyNames = Array.isArray(opts.taxonomyNames)
|
|
145
337
|
? opts.taxonomyNames
|
|
146
338
|
: undefined;
|
|
339
|
+
// Template mode (`--partial-mode template`, plans/page-templates.md):
|
|
340
|
+
// `<Render>` becomes `{% with %}{% include %}` and each referenced partial
|
|
341
|
+
// becomes a TEMPLATE partial. Only a Dogsbay-MD output can carry that
|
|
342
|
+
// (convert refuses other targets); `inline` is the default everywhere.
|
|
343
|
+
const templateMode = opts.partialMode === "template" && Boolean(partialsDir) && adapter?.resolveIncludes !== undefined;
|
|
344
|
+
// INLINE mode (the default) for Cloudflare is template mode PLUS rendering:
|
|
345
|
+
// the same conversion, then every page's includes are rendered in memory
|
|
346
|
+
// with the shared TemplatePages and the result parsed back. Inline output
|
|
347
|
+
// therefore equals the template-mode build by construction. (A resolver
|
|
348
|
+
// that spliced partial MDX into the page left unevaluated conditionals,
|
|
349
|
+
// stray code blocks and empty tabs.)
|
|
350
|
+
const inlineMode = !templateMode && opts.partialMode !== "include" && Boolean(partialsDir) && adapter?.name === "cloudflare";
|
|
351
|
+
const converting = templateMode || inlineMode;
|
|
352
|
+
// Inline mode (the default) renders each partial in place on the SAME
|
|
353
|
+
// converter template mode uses (inline-partials.ts).
|
|
354
|
+
const usedPartials = [];
|
|
355
|
+
/** Parameter names callers pass to each partial (`product/file`). */
|
|
356
|
+
const passedParams = new Map();
|
|
357
|
+
const restoreStats = { escaped: 0, inCode: 0 };
|
|
358
|
+
const inlined = new Map();
|
|
359
|
+
let templateIncludes = 0;
|
|
360
|
+
const apiRequests = { converted: 0, left: 0, missing: new Set() };
|
|
361
|
+
const apiSchema = loadApiSchema(opts.apiSchema);
|
|
362
|
+
// The schema is used by the converting modes only (inline, template):
|
|
363
|
+
// parsed and then silently ignored was the failure mode.
|
|
364
|
+
if (apiSchema && !converting) {
|
|
365
|
+
console.warn(`warning: --api-schema is used with the cloudflare adapter and a partials directory, in inline or template partial mode; it was ignored here.`);
|
|
366
|
+
}
|
|
367
|
+
const includeOptionsFor = (product, directives, onUse, names, pageDir) => converting
|
|
368
|
+
? {
|
|
369
|
+
mode: "template",
|
|
370
|
+
pageDir,
|
|
371
|
+
defaultProduct: product,
|
|
372
|
+
directives,
|
|
373
|
+
names,
|
|
374
|
+
onApiRequest: (converted) => (converted ? apiRequests.converted++ : apiRequests.left++),
|
|
375
|
+
apiSchema,
|
|
376
|
+
onApiMissing: (key) => apiRequests.missing.add(key),
|
|
377
|
+
onDegraded: (tag, how) => {
|
|
378
|
+
const key = `${tag}\u0000${how}`;
|
|
379
|
+
degraded.set(key, (degraded.get(key) ?? 0) + 1);
|
|
380
|
+
},
|
|
381
|
+
onTemplateInclude: ({ product: p, file, params }) => {
|
|
382
|
+
templateIncludes++;
|
|
383
|
+
const key = `${p}/${file}`;
|
|
384
|
+
usedPartials.push(key);
|
|
385
|
+
const passed = passedParams.get(key) ?? new Set();
|
|
386
|
+
for (const n of params)
|
|
387
|
+
passed.add(n);
|
|
388
|
+
passedParams.set(key, passed);
|
|
389
|
+
onUse?.();
|
|
390
|
+
},
|
|
391
|
+
onInlined: ({ reason }) => inlined.set(reason, (inlined.get(reason) ?? 0) + 1),
|
|
392
|
+
}
|
|
393
|
+
: undefined;
|
|
394
|
+
// Include mode splices partial TEXT into the page at parse time, so the
|
|
395
|
+
// escapes the page's partials wrote count for it too — only the partials
|
|
396
|
+
// it names (every partial's escapes would excuse leaks on unrelated pages).
|
|
397
|
+
// Converting modes scan each partial on its own, below.
|
|
398
|
+
const partialCache = new Map();
|
|
399
|
+
// A `<ProductChangelog>` page splices changelog ENTRIES into itself: their
|
|
400
|
+
// escapes count too (`Array\<Number>` in an entry was reported as a leak).
|
|
401
|
+
let changelogCache;
|
|
402
|
+
const changelogEscapes = () => {
|
|
403
|
+
if (changelogCache)
|
|
404
|
+
return changelogCache;
|
|
405
|
+
changelogCache = new Map();
|
|
406
|
+
const dir = partialsDir ? join(dirname(partialsDir), "changelog") : undefined;
|
|
407
|
+
const walkDir = (d) => {
|
|
408
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
409
|
+
const p = join(d, e.name);
|
|
410
|
+
if (e.isDirectory())
|
|
411
|
+
walkDir(p);
|
|
412
|
+
else if (/\.mdx?$/.test(e.name))
|
|
413
|
+
for (const n of escapedIn(readFileSync(p, "utf-8")).keys())
|
|
414
|
+
changelogCache.set(n, Infinity);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
if (dir && existsSync(dir))
|
|
419
|
+
walkDir(dir);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Unreadable: fewer escapes excused, never fewer leaks reported.
|
|
423
|
+
}
|
|
424
|
+
return changelogCache;
|
|
425
|
+
};
|
|
426
|
+
const includedEscapes = (source, product, depth = 0) => {
|
|
427
|
+
const names = new Map();
|
|
428
|
+
if (converting || !partialsDir || depth > 8)
|
|
429
|
+
return names;
|
|
430
|
+
for (const m of source.matchAll(/<Render\b[^>]*?\bfile=["']([^"']+)["'][^>]*>/g)) {
|
|
431
|
+
const prod = /\bproduct=["']([^"']+)["']/.exec(m[0])?.[1] ?? product;
|
|
432
|
+
const path = join(partialsDir, prod, `${m[1]}.mdx`);
|
|
433
|
+
let found = partialCache.get(path);
|
|
434
|
+
if (!found) {
|
|
435
|
+
let text = "";
|
|
436
|
+
try {
|
|
437
|
+
text = readFileSync(path, "utf-8");
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// A missing partial is reported by the include path, not here.
|
|
441
|
+
}
|
|
442
|
+
found = escapedIn(text, includedEscapes(text, prod, depth + 1));
|
|
443
|
+
partialCache.set(path, found);
|
|
444
|
+
}
|
|
445
|
+
// A partial can be included more than once: its escapes are not counted.
|
|
446
|
+
for (const n of found.keys())
|
|
447
|
+
names.set(n, Infinity);
|
|
448
|
+
}
|
|
449
|
+
return names;
|
|
450
|
+
};
|
|
147
451
|
for (const { filePath, slug } of mdxFiles) {
|
|
148
452
|
const content = readFileSync(filePath, "utf-8");
|
|
453
|
+
let usesTemplates = false;
|
|
454
|
+
const directives = new DirectiveTable();
|
|
149
455
|
const { frontmatter, raw, body } = extractFrontmatter(content);
|
|
456
|
+
pageImports = importsOf(body);
|
|
150
457
|
// Use new MDX parser (markdown-it-mdx-jsx plugin) by default
|
|
151
458
|
// Fall back to legacy regex parser with --legacy-parser flag
|
|
152
459
|
const useLegacy = opts.legacyParser === true;
|
|
153
460
|
const starlightToTree = useLegacy ? starlightToTreeLegacy : starlightToTreeMdx;
|
|
154
|
-
const tree = starlightToTree(body, {
|
|
461
|
+
const tree = starlightToTree(body, {
|
|
462
|
+
adapter,
|
|
463
|
+
partialsDir,
|
|
464
|
+
frontmatter: raw,
|
|
465
|
+
onDrop: (tag) => record(tag, slug),
|
|
466
|
+
describeComponent,
|
|
467
|
+
// A <Render> with no product= means the page's own product: its first path segment.
|
|
468
|
+
includeOptions: includeOptionsFor(slug.split("/")[0], directives, () => (usesTemplates = true), undefined, dirname(filePath)),
|
|
469
|
+
});
|
|
470
|
+
if (usesTemplates)
|
|
471
|
+
restoreDirectives(tree, directives, restoreStats);
|
|
472
|
+
// Catch what the callback cannot: a component nested inside a MAPPED
|
|
473
|
+
// container is not dropped, it LEAKS — its opening tag lands in the page
|
|
474
|
+
// as visible text and its body often becomes a code block, because nothing
|
|
475
|
+
// claimed the block. `<LinkTitleCard>` inside `<CardGrid>` did exactly
|
|
476
|
+
// that on Cloudflare's AI Search overview while the drop warning stayed
|
|
477
|
+
// silent.
|
|
478
|
+
//
|
|
479
|
+
// Scanning the finished tree catches every route into the output, present
|
|
480
|
+
// and future, in one place. `code` nodes are skipped: a code sample
|
|
481
|
+
// containing `<Response>` is content, not a leak.
|
|
482
|
+
for (const tag of leakedComponentTags(tree, escapedIn(body, new Map([...includedEscapes(body, slug.split("/")[0]), ...(body.includes("<ProductChangelog") ? changelogEscapes() : [])]))))
|
|
483
|
+
record(tag, slug);
|
|
155
484
|
// Extract headings from tree
|
|
156
485
|
const headings = tree
|
|
157
486
|
.filter((n) => n.type === "heading")
|
|
@@ -196,7 +525,9 @@ async function importStarlight(source, opts) {
|
|
|
196
525
|
],
|
|
197
526
|
});
|
|
198
527
|
}
|
|
199
|
-
|
|
528
|
+
// A template page's title is rendered too: a literal `{{` in it would be syntax.
|
|
529
|
+
// (Template mode only: in inline mode the page is rendered here and its title never is.)
|
|
530
|
+
const title = usesTemplates && templateMode ? escapeStrayOpeners(frontmatter.title || slug) : frontmatter.title || slug;
|
|
200
531
|
const meta = parseMeta(raw, { slug, taxonomyNames });
|
|
201
532
|
pages.push({
|
|
202
533
|
slug,
|
|
@@ -206,8 +537,164 @@ async function importStarlight(source, opts) {
|
|
|
206
537
|
redirect,
|
|
207
538
|
frontmatter: raw,
|
|
208
539
|
meta,
|
|
540
|
+
...(usesTemplates ? { template: true } : {}),
|
|
209
541
|
});
|
|
210
542
|
}
|
|
543
|
+
if (converting) {
|
|
544
|
+
const stats = newJsxStats();
|
|
545
|
+
const converted = new Set();
|
|
546
|
+
let undeclaredPassed = 0;
|
|
547
|
+
const partialPages = [];
|
|
548
|
+
for (let i = 0; i < usedPartials.length; i++) {
|
|
549
|
+
const key = usedPartials[i];
|
|
550
|
+
if (converted.has(key))
|
|
551
|
+
continue;
|
|
552
|
+
converted.add(key);
|
|
553
|
+
const [product, ...rest] = key.split("/");
|
|
554
|
+
const file = rest.join("/");
|
|
555
|
+
const source = readFileSync(join(partialsDir, product, `${file}.mdx`), "utf-8");
|
|
556
|
+
const { raw, body } = extractFrontmatter(source);
|
|
557
|
+
pageImports = importsOf(body);
|
|
558
|
+
const names = new Set();
|
|
559
|
+
const directives = new DirectiveTable();
|
|
560
|
+
const partialStats = { ...newJsxStats(), names, table: directives };
|
|
561
|
+
const withoutImports = body.replace(/^import\s+\{[\s\S]*?\}\s+from\s+["'][^"']*["']\s*;?\s*$/gm, "");
|
|
562
|
+
const jinja = jsxPropsToJinja(withoutImports, partialStats);
|
|
563
|
+
stats.converted += partialStats.converted;
|
|
564
|
+
stats.refused += partialStats.refused;
|
|
565
|
+
for (const s of partialStats.refusedSamples)
|
|
566
|
+
if (stats.refusedSamples.length < 20)
|
|
567
|
+
stats.refusedSamples.push(`${key}: ${s}`);
|
|
568
|
+
const tree = starlightToTreeMdx(jinja, {
|
|
569
|
+
adapter,
|
|
570
|
+
partialsDir,
|
|
571
|
+
onDrop: (tag) => record(tag, `_partials/${key}`),
|
|
572
|
+
describeComponent,
|
|
573
|
+
// Every partial a nested <Render> names is queued for conversion too.
|
|
574
|
+
// A nested <Render> with no product= is reported, not guessed: in
|
|
575
|
+
// Cloudflare it takes the product of the PAGE being built.
|
|
576
|
+
includeOptions: includeOptionsFor(undefined, directives, undefined, names),
|
|
577
|
+
});
|
|
578
|
+
restoreDirectives(tree, directives, restoreStats);
|
|
579
|
+
// Leaks inside a partial count too (they reach every page including it).
|
|
580
|
+
for (const tag of leakedComponentTags(tree, escapedIn(source)))
|
|
581
|
+
record(tag, `_partials/${key}`);
|
|
582
|
+
stats.markdownInlined += partialStats.markdownInlined;
|
|
583
|
+
// Cloudflare declares `params: [a, b?]` on most partials — the same
|
|
584
|
+
// syntax template partials use. One that reads props without declaring
|
|
585
|
+
// them gets every name it reads, as optional (Cloudflare does not
|
|
586
|
+
// validate those either).
|
|
587
|
+
const declared = Array.isArray(raw.params) && raw.params.every((p) => typeof p === "string") ? raw.params : undefined;
|
|
588
|
+
// Declared parameters, plus every `props` name the converted body reads
|
|
589
|
+
// or forwards, as optional: an undeclared read would otherwise be a
|
|
590
|
+
// strict-undefined error on every page that does not pass it.
|
|
591
|
+
const declaredNames = new Set((declared ?? []).map((p) => p.replace(/\?$/, "")));
|
|
592
|
+
const readOnly = [...names].filter((n) => !declaredNames.has(n)).sort().map((n) => `${n}?`);
|
|
593
|
+
const baseParams = declared || readOnly.length ? [...(declared ?? []), ...readOnly] : undefined;
|
|
594
|
+
// Each partial is its own MDX document upstream, so each numbers its
|
|
595
|
+
// own footnotes from 1; spliced into one page they collided and the
|
|
596
|
+
// first `[^1]:` won (warp-client/get-started lost its RHEL note).
|
|
597
|
+
relabelFootnotes(tree, (label) => `${key.split("/").pop().replace(/[^A-Za-z0-9_-]/g, "")}-${label}`);
|
|
598
|
+
const page = { slug: `_partials/${key}`, title: "", tree, headings: [], fragment: true, template: true };
|
|
599
|
+
partialPages.push({ key, page, baseParams });
|
|
600
|
+
pages.push(page);
|
|
601
|
+
}
|
|
602
|
+
// Parameters are settled only once every caller is known: a partial may
|
|
603
|
+
// be converted before another partial, later in the queue, passes it a
|
|
604
|
+
// name. Names callers pass that a partial does not declare were
|
|
605
|
+
// tolerated by Cloudflare's build and would be refused by a template
|
|
606
|
+
// partial, so they are declared as optional — and counted.
|
|
607
|
+
for (const { key, page, baseParams } of partialPages) {
|
|
608
|
+
let params = baseParams;
|
|
609
|
+
if (params) {
|
|
610
|
+
const known = new Set(params.map((p) => p.replace(/\?$/, "")));
|
|
611
|
+
const extra = [...(passedParams.get(key) ?? [])].filter((n) => !known.has(n)).sort();
|
|
612
|
+
if (extra.length) {
|
|
613
|
+
params = [...params, ...extra.map((n) => `${n}?`)];
|
|
614
|
+
undeclaredPassed += extra.length;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (params)
|
|
618
|
+
page.templateParams = params;
|
|
619
|
+
}
|
|
620
|
+
// Inline mode: render each page's includes now, parse the result back,
|
|
621
|
+
// and drop the partials (they are spliced into the pages).
|
|
622
|
+
let rendered = 0;
|
|
623
|
+
const renderFailures = [];
|
|
624
|
+
if (inlineMode) {
|
|
625
|
+
const fragments = new Map();
|
|
626
|
+
for (const { page } of partialPages) {
|
|
627
|
+
const body = treeToDogsbayMd(page.tree, page.templateParams ? { frontmatter: { params: page.templateParams } } : undefined);
|
|
628
|
+
// Same hazard as the page reparse below: a partial opening with `---`.
|
|
629
|
+
fragments.set(`${page.slug}.md`, page.templateParams ? body : `---\n---\n${body}`);
|
|
630
|
+
}
|
|
631
|
+
const templates = new TemplatePages({ load: (p) => fragments.get(p), sourceDefault: true });
|
|
632
|
+
const partialSet = new Set(partialPages.map((p) => p.page));
|
|
633
|
+
for (let k = pages.length - 1; k >= 0; k--)
|
|
634
|
+
if (partialSet.has(pages[k]))
|
|
635
|
+
pages.splice(k, 1);
|
|
636
|
+
for (const page of pages) {
|
|
637
|
+
if (!page.template)
|
|
638
|
+
continue;
|
|
639
|
+
const path = `${page.slug || "index"}.md`;
|
|
640
|
+
try {
|
|
641
|
+
const md = await templates.render(treeToDogsbayMd(page.tree), path, page.frontmatter ?? {});
|
|
642
|
+
// An empty front matter block first: a page whose body opens with
|
|
643
|
+
// a thematic break (`---`) was otherwise read as front matter, and
|
|
644
|
+
// everything up to the next rule vanished (waiting-room/get-started).
|
|
645
|
+
page.tree = dogsbayMdToTree(`---\n---\n${md}`).tree;
|
|
646
|
+
// Partial labels (`linux-1`) back to 1..n, in order of first reference.
|
|
647
|
+
const order = new Map();
|
|
648
|
+
collectFootnoteRefs(page.tree, (label) => order.has(label) || order.set(label, String(order.size + 1)));
|
|
649
|
+
if (order.size)
|
|
650
|
+
relabelFootnotes(page.tree, (label) => order.get(label) ?? label);
|
|
651
|
+
page.headings = extractHeadings(page.tree).map((h) => ({ depth: h.depth, text: h.text, slug: h.slug }));
|
|
652
|
+
delete page.template;
|
|
653
|
+
rendered++;
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
renderFailures.push(`${path}: ${err.message.slice(0, 160)}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// A page that did not render keeps directive nodes no exporter but
|
|
660
|
+
// dogsbay-md emits, and its partials were removed above: its partial
|
|
661
|
+
// content is simply gone. That must not exit 0 (the rule for includes
|
|
662
|
+
// everywhere, plans/page-templates.md).
|
|
663
|
+
if (renderFailures.length && opts.allowMissingIncludes !== true) {
|
|
664
|
+
throw new Error(`${renderFailures.length} page(s) could not render their partials:\n` +
|
|
665
|
+
renderFailures.slice(0, 20).map((f) => ` ${f}`).join("\n") +
|
|
666
|
+
"\nRe-run with --allow-missing-includes to write them without that content.");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const inlinedTotal = [...inlined.values()].reduce((a, b) => a + b, 0);
|
|
670
|
+
const lines = [
|
|
671
|
+
inlineMode
|
|
672
|
+
? `partials: ${templateIncludes} <Render> converted and rendered in place on ${rendered} page(s), ${converted.size} partial(s)`
|
|
673
|
+
: `template partials: ${templateIncludes} <Render> → {% include %}, ${converted.size} partial(s) converted`,
|
|
674
|
+
...(renderFailures.length ? [` ${renderFailures.length} page(s) could not be rendered; their partial content is MISSING (--allow-missing-includes):`, ...renderFailures.slice(0, 8).map((f) => ` ${f}`)] : []),
|
|
675
|
+
` props expressions: ${stats.converted} converted, ${stats.refused} left as written; ${stats.markdownInlined} <Markdown> wrapper(s) replaced by their text`,
|
|
676
|
+
` ${undeclaredPassed} parameter(s) callers pass but partials did not declare, added as optional`,
|
|
677
|
+
...(inlineMode ? [] : [` ${restoreStats.escaped} stray template opener(s) in template pages escaped to print literally`]),
|
|
678
|
+
...(apiRequests.converted || apiRequests.left
|
|
679
|
+
? [
|
|
680
|
+
` ${apiRequests.converted} <APIRequest> converted to curl ` +
|
|
681
|
+
(apiSchema
|
|
682
|
+
? `with titles, auth and permission panels from ${String(opts.apiSchema)} (${apiRequests.missing.size} operation(s) not in it${apiRequests.missing.size ? `: ${[...apiRequests.missing].slice(0, 3).join(", ")}${apiRequests.missing.size > 3 ? ", …" : ""}` : ""}); \`code\` line highlights are not carried`
|
|
683
|
+
: "(without the token-permission panel, title and `code` line highlights; the first two come from the OpenAPI schema — pass a local copy with --api-schema)") +
|
|
684
|
+
(apiRequests.left ? `; ${apiRequests.left} left as written (props not readable)` : ""),
|
|
685
|
+
]
|
|
686
|
+
: []),
|
|
687
|
+
...[...degraded].map(([key, n]) => {
|
|
688
|
+
const [tag, how] = key.split("\u0000");
|
|
689
|
+
return ` ${n} <${tag}> converted to a lesser form (${how})`;
|
|
690
|
+
}),
|
|
691
|
+
...(restoreStats.fencesMarked ? [` ${restoreStats.fencesMarked} code block(s) take a parameter and are marked {minja} so it renders`] : []),
|
|
692
|
+
...(restoreStats.inCode ? [` ${restoreStats.inCode} directive(s) landed inside code and will show literally — check those pages`] : []),
|
|
693
|
+
...(inlinedTotal ? [` ${inlinedTotal} <Render> left as tags or inlined the old way (reported as unmapped):`, ...[...inlined].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([r, n]) => ` ×${n} ${r}`)] : []),
|
|
694
|
+
...(stats.refusedSamples.length ? [" expressions left as written (first few):", ...stats.refusedSamples.slice(0, 8).map((s) => ` ${s}`)] : []),
|
|
695
|
+
];
|
|
696
|
+
console.warn(lines.join("\n"));
|
|
697
|
+
}
|
|
211
698
|
// Set site name from opts or infer
|
|
212
699
|
if (!opts.siteName && pages.length > 0) {
|
|
213
700
|
const indexPage = pages.find((p) => p.slug === "index" || p.slug === "");
|
|
@@ -215,16 +702,194 @@ async function importStarlight(source, opts) {
|
|
|
215
702
|
opts.siteName = indexPage.title;
|
|
216
703
|
}
|
|
217
704
|
}
|
|
218
|
-
|
|
705
|
+
// Say what was dropped, loudly and countably.
|
|
706
|
+
//
|
|
707
|
+
// A component nothing claimed is CONTENT THAT IS NOT ON THE PAGE — the
|
|
708
|
+
// Cloudflare AI Search overview lost its architecture diagram this way, and
|
|
709
|
+
// the only way anyone found out was comparing the converted page with the
|
|
710
|
+
// live one by eye. There is no ledger on this path, so the warning IS the
|
|
711
|
+
// report: it names each tag, how many times it appeared and how many pages
|
|
712
|
+
// it touched, so `convert … 2>&1 | grep "dropped"` answers "how much of this
|
|
713
|
+
// corpus does this affect" without reading any output files.
|
|
714
|
+
if (dropped.size > 0) {
|
|
715
|
+
const rows = [...dropped.entries()]
|
|
716
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
717
|
+
.map(([tag, d]) => ` <${tag}> ×${d.count} on ${d.pages.size} page(s): ${[...d.pages].slice(0, 3).join(", ")}${d.pages.size > 3 ? ", …" : ""}`);
|
|
718
|
+
const total = [...dropped.values()].reduce((n, d) => n + d.count, 0);
|
|
719
|
+
console.warn(`warning: ${total} unmapped component(s) — dropped, or leaked into the ` +
|
|
720
|
+
`page as literal text:\n${rows.join("\n")}\n` +
|
|
721
|
+
` Map them with an adapter, or accept the loss knowingly.`);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
pages,
|
|
725
|
+
nav,
|
|
726
|
+
// Contents land in `public/_assets/`, matching the `/_assets/…` refs the
|
|
727
|
+
// parser now emits for the `~/assets/` alias.
|
|
728
|
+
...(assetsDir || iconsDir
|
|
729
|
+
? {
|
|
730
|
+
assetMounts: [
|
|
731
|
+
...(assetsDir ? [{ dir: assetsDir }] : []),
|
|
732
|
+
...(iconsDir ? [{ dir: iconsDir, into: "icons" }] : []),
|
|
733
|
+
],
|
|
734
|
+
}
|
|
735
|
+
: {}),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Component tags left as literal text/HTML in a finished tree.
|
|
740
|
+
*
|
|
741
|
+
* A CamelCase tag only — `<ACCOUNT_ID>` and `<API_TOKEN>` are placeholders the
|
|
742
|
+
* docs write on purpose, and flagging those would bury the real signal.
|
|
743
|
+
*/
|
|
744
|
+
/** Rename every footnote label in a tree (references and definitions). */
|
|
745
|
+
function relabelFootnotes(nodes, rename) {
|
|
746
|
+
const walk = (value) => {
|
|
747
|
+
if (Array.isArray(value))
|
|
748
|
+
return value.forEach(walk);
|
|
749
|
+
if (!value || typeof value !== "object")
|
|
750
|
+
return;
|
|
751
|
+
const node = value;
|
|
752
|
+
if (node.type === "footnote-ref" && typeof node.label === "string")
|
|
753
|
+
node.label = rename(node.label);
|
|
754
|
+
if (node.type === "footnote-def" && node.props && typeof node.props === "object") {
|
|
755
|
+
const props = node.props;
|
|
756
|
+
if (typeof props.label === "string") {
|
|
757
|
+
const label = rename(props.label);
|
|
758
|
+
Object.assign(props, { label, id: `fn-${label}`, backrefId: `fnref-${label}` });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
for (const [k, v] of Object.entries(node))
|
|
762
|
+
if (k !== "props" && typeof v === "object")
|
|
763
|
+
walk(v);
|
|
764
|
+
};
|
|
765
|
+
walk(nodes);
|
|
766
|
+
}
|
|
767
|
+
function collectFootnoteRefs(nodes, visit) {
|
|
768
|
+
const walk = (value) => {
|
|
769
|
+
if (Array.isArray(value))
|
|
770
|
+
return value.forEach(walk);
|
|
771
|
+
if (!value || typeof value !== "object")
|
|
772
|
+
return;
|
|
773
|
+
const node = value;
|
|
774
|
+
if (node.type === "footnote-ref" && typeof node.label === "string")
|
|
775
|
+
visit(node.label);
|
|
776
|
+
for (const [k, v] of Object.entries(node))
|
|
777
|
+
if (k !== "props" && typeof v === "object")
|
|
778
|
+
walk(v);
|
|
779
|
+
};
|
|
780
|
+
walk(nodes);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Names the source writes ESCAPED (`Promise\<ArrayBuffer>`), with how many
|
|
784
|
+
* times: that is prose about a type, rendered as text, and parsed text no
|
|
785
|
+
* longer shows the backslash. Reporting them was ~40 false leaks
|
|
786
|
+
* (`<ArrayBuffer>`, `<Response>`, `<CryptoKey>` …) on the Cloudflare corpus.
|
|
787
|
+
* Each escape excuses ONE text occurrence, so a real leak of the same name
|
|
788
|
+
* on the page is still reported.
|
|
789
|
+
*/
|
|
790
|
+
function escapedIn(source, also) {
|
|
791
|
+
const names = new Map(also);
|
|
792
|
+
// `\<Name` and `<Name` (an entity, decoded to text by the parser: `<code>Promise<WorkflowInstance></code>`).
|
|
793
|
+
for (const m of source.matchAll(/(?:\\<|<)\/?([A-Z][A-Za-z0-9]*)/g))
|
|
794
|
+
names.set(m[1], (names.get(m[1]) ?? 0) + 1);
|
|
795
|
+
return names;
|
|
796
|
+
}
|
|
797
|
+
function leakedComponentTags(nodes, escapes = new Map()) {
|
|
798
|
+
const found = [];
|
|
799
|
+
const excused = new Map(escapes);
|
|
800
|
+
const scan = (list) => {
|
|
801
|
+
for (const node of list) {
|
|
802
|
+
if (node.type === "code" || node.type === "diagram")
|
|
803
|
+
continue;
|
|
804
|
+
const inline = (node.inline ?? []).filter((i) => i.type !== "code");
|
|
805
|
+
// Inline text is JOINED: a multi-line tag arrives as `<APIRequest`, then
|
|
806
|
+
// a separate newline node, and no single piece matched. Inline raw HTML
|
|
807
|
+
// is joined too, with text standing in as spaces, and scanned as HTML.
|
|
808
|
+
const text = inline.map((i) => (i.type === "html-inline" ? " " : (i.text ?? " "))).join("");
|
|
809
|
+
const html = inline.map((i) => (i.type === "html-inline" ? (i.html ?? "") : " ")).join("");
|
|
810
|
+
for (const [source, isText] of [[node.html, false], [html, false], [text, true]]) {
|
|
811
|
+
if (typeof source !== "string")
|
|
812
|
+
continue;
|
|
813
|
+
// Inline code is skipped above: `Promise<Response>` is a type, not a
|
|
814
|
+
// leaked component (it was 60 false reports).
|
|
815
|
+
// `</?` — a leak is often only the CLOSING tag, when the opener was
|
|
816
|
+
// consumed by the container above it but the closer had nothing to
|
|
817
|
+
// pair with. Matching openers alone missed exactly that case.
|
|
818
|
+
// A lowercase letter ANYWHERE, not straight after the capital: that
|
|
819
|
+
// skipped `<APIRequest>` and every other name opening with an acronym,
|
|
820
|
+
// while the point was only to skip ALL-CAPS placeholders (`<ACCOUNT_ID>`).
|
|
821
|
+
for (const m of source.matchAll(/<\/?([A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*)[\s/>]/g)) {
|
|
822
|
+
// Only TEXT is excused by an escape: in raw HTML `<String>` is a tag
|
|
823
|
+
// (a backticked `Map<Array<String>>` in a `<td>` is not code there).
|
|
824
|
+
const left = excused.get(m[1]) ?? 0;
|
|
825
|
+
if (isText && left > 0) {
|
|
826
|
+
excused.set(m[1], left - 1);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
found.push(m[1]);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (node.children)
|
|
833
|
+
scan(node.children);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
scan(nodes);
|
|
837
|
+
return found;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* `--api-schema <openapi.json>`: the operations `<APIRequest>` looks up, by
|
|
841
|
+
* `METHOD /path`. Read as JSON — never fetched, never executed; only the
|
|
842
|
+
* operation fields the component reads are kept (the file is ~11 MB).
|
|
843
|
+
*/
|
|
844
|
+
function loadApiSchema(file) {
|
|
845
|
+
if (typeof file !== "string" || !file)
|
|
846
|
+
return undefined;
|
|
847
|
+
let doc;
|
|
848
|
+
try {
|
|
849
|
+
doc = JSON.parse(readFileSync(file, "utf-8"));
|
|
850
|
+
}
|
|
851
|
+
catch (e) {
|
|
852
|
+
throw new Error(`--api-schema ${file}: not readable as JSON (${e.message})`);
|
|
853
|
+
}
|
|
854
|
+
const ops = new Map();
|
|
855
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
856
|
+
for (const [method, raw] of Object.entries(item ?? {})) {
|
|
857
|
+
if (!["get", "put", "post", "delete", "patch", "head", "options"].includes(method) || !raw || typeof raw !== "object")
|
|
858
|
+
continue;
|
|
859
|
+
const o = raw;
|
|
860
|
+
const security = Array.isArray(o.security) ? o.security.flatMap((r) => (r && typeof r === "object" ? Object.keys(r) : [])) : undefined;
|
|
861
|
+
const groups = Array.isArray(o["x-api-token-group"]) ? o["x-api-token-group"].filter((g) => typeof g === "string") : undefined;
|
|
862
|
+
ops.set(`${method.toUpperCase()} ${path}`, {
|
|
863
|
+
...(typeof o.summary === "string" ? { summary: o.summary } : {}),
|
|
864
|
+
...(security ? { security } : {}),
|
|
865
|
+
...(groups ? { tokenGroups: groups } : {}),
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return ops;
|
|
219
870
|
}
|
|
220
871
|
export const plugin = {
|
|
221
872
|
name: "starlight",
|
|
222
873
|
canImport: true,
|
|
223
874
|
canExport: false,
|
|
224
875
|
detectSource: detectStarlightSource,
|
|
225
|
-
exportOptions:
|
|
226
|
-
|
|
876
|
+
// importOptions, not exportOptions: this plugin declares `canExport: false`,
|
|
877
|
+
// so an export-side declaration described a flag on a command that does not
|
|
878
|
+
// exist — and `convert`'s "unusable flag" warning reads the IMPORT list, so
|
|
879
|
+
// `--adapter cloudflare` was reported as ignored while being consumed. That
|
|
880
|
+
// warning is load-bearing on this corpus: the adapter is the difference
|
|
881
|
+
// between every `<Render>` partial resolving and none of them.
|
|
882
|
+
importOptions: [
|
|
883
|
+
{
|
|
884
|
+
flags: "--adapter <name>",
|
|
885
|
+
description: `Site adapter for custom components (${Object.keys(STARLIGHT_ADAPTERS).join(", ")})`,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
flags: "--api-schema <file>",
|
|
889
|
+
description: "Cloudflare: a local copy of the API's OpenAPI schema (openapi.json), for <APIRequest> titles, auth and permission panels",
|
|
890
|
+
},
|
|
227
891
|
],
|
|
892
|
+
exportOptions: [],
|
|
228
893
|
async import(source, opts) {
|
|
229
894
|
return importStarlight(source, opts);
|
|
230
895
|
},
|