dogsbay 0.2.0-beta.115 → 0.2.0-beta.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/audit/deliverable-vars.d.ts +1 -1
  2. package/dist/audit/rules/structure/index.d.ts +2 -11
  3. package/dist/audit/rules/structure/index.js +6 -36
  4. package/dist/audit/rules/structure/nav-target-exists.d.ts +1 -1
  5. package/dist/audit/rules/structure/topicmap-valid.d.ts +1 -1
  6. package/dist/child-links.d.ts +19 -0
  7. package/dist/child-links.js +48 -0
  8. package/dist/commands/convert.js +26 -0
  9. package/dist/commands/migrate-mkdocs.js +5 -5
  10. package/dist/commands/migrate-myst.d.ts +19 -0
  11. package/dist/commands/migrate-myst.js +77 -60
  12. package/dist/commands/migrate-rst.d.ts +7 -0
  13. package/dist/commands/migrate-rst.js +86 -35
  14. package/dist/commands/site-build.js +76 -6
  15. package/dist/commands/site-check.d.ts +1 -0
  16. package/dist/commands/site-check.js +27 -12
  17. package/dist/commands/site-dev.d.ts +27 -0
  18. package/dist/commands/site-dev.js +60 -2
  19. package/dist/commands/site-release-diff.d.ts +17 -0
  20. package/dist/commands/site-release-diff.js +85 -0
  21. package/dist/config/defaults.js +1 -0
  22. package/dist/config/load.js +118 -1
  23. package/dist/config/schema.d.ts +91 -2
  24. package/dist/config/to-astro-options.js +4 -0
  25. package/dist/import-content.d.ts +59 -0
  26. package/dist/import-content.js +83 -4
  27. package/dist/index.js +47 -5
  28. package/dist/lib.d.ts +2 -0
  29. package/dist/lib.js +4 -0
  30. package/dist/registry.js +11 -3
  31. package/dist/release/compare.d.ts +43 -0
  32. package/dist/release/compare.js +419 -0
  33. package/dist/resolve-openapi.d.ts +31 -0
  34. package/dist/resolve-openapi.js +181 -0
  35. package/dist/resolve-relative-links.d.ts +32 -0
  36. package/dist/resolve-relative-links.js +80 -0
  37. package/dist/site-build/preprocess.d.ts +8 -0
  38. package/dist/site-build/preprocess.js +111 -9
  39. package/package.json +39 -27
  40. package/skills/platform/{agent-readiness → dogsbay-agent-readiness}/SKILL.md +2 -2
  41. package/skills/platform/{cli-commands → dogsbay-cli-commands}/SKILL.md +2 -2
  42. package/skills/platform/{config-yml → dogsbay-config-yml}/SKILL.md +7 -7
  43. package/skills/platform/dogsbay-documents/SKILL.md +222 -0
  44. package/skills/platform/{frontmatter-fields → dogsbay-frontmatter-fields}/SKILL.md +7 -7
  45. package/skills/platform/{markdown-directives → dogsbay-markdown-directives}/SKILL.md +8 -4
  46. package/skills/platform/{migration-shape → dogsbay-migration-shape}/SKILL.md +2 -2
  47. package/skills/platform/{multi-source → dogsbay-multi-source}/SKILL.md +1 -1
  48. package/skills/platform/{nav-file → dogsbay-nav-file}/SKILL.md +1 -1
  49. package/skills/platform/{openapi-source → dogsbay-openapi-source}/SKILL.md +1 -1
  50. package/skills/platform/{plugin-api → dogsbay-plugin-api}/SKILL.md +1 -1
  51. package/skills/platform/{project-anatomy → dogsbay-project-anatomy}/SKILL.md +1 -1
  52. package/skills/platform/{taxonomy-config → dogsbay-taxonomy-config}/SKILL.md +1 -1
  53. package/skills/platform/{theme-tokens → dogsbay-theme-tokens}/SKILL.md +1 -1
  54. package/dist/audit/dist/crawler.d.ts +0 -23
  55. package/dist/audit/dist/crawler.js +0 -71
  56. package/dist/audit/lib/collect-assets.d.ts +0 -7
  57. package/dist/audit/lib/collect-assets.js +0 -64
  58. package/dist/audit/lib/validate-refs.d.ts +0 -37
  59. package/dist/audit/lib/validate-refs.js +0 -132
  60. package/dist/audit/output/json.d.ts +0 -7
  61. package/dist/audit/output/json.js +0 -3
  62. package/dist/audit/output/text.d.ts +0 -6
  63. package/dist/audit/output/text.js +0 -84
  64. package/dist/audit/registry.d.ts +0 -55
  65. package/dist/audit/registry.js +0 -62
  66. package/dist/audit/rules/seo/description-length.d.ts +0 -16
  67. package/dist/audit/rules/seo/description-length.js +0 -26
  68. package/dist/audit/rules/seo/description-missing.d.ts +0 -15
  69. package/dist/audit/rules/seo/description-missing.js +0 -24
  70. package/dist/audit/rules/seo/dist-meta.d.ts +0 -22
  71. package/dist/audit/rules/seo/dist-meta.js +0 -65
  72. package/dist/audit/rules/seo/h1-brand-keyword.d.ts +0 -2
  73. package/dist/audit/rules/seo/h1-brand-keyword.js +0 -99
  74. package/dist/audit/rules/seo/index.d.ts +0 -11
  75. package/dist/audit/rules/seo/index.js +0 -63
  76. package/dist/audit/rules/seo/json-ld-parse.d.ts +0 -13
  77. package/dist/audit/rules/seo/json-ld-parse.js +0 -32
  78. package/dist/audit/rules/seo/json-ld-required-fields.d.ts +0 -21
  79. package/dist/audit/rules/seo/json-ld-required-fields.js +0 -116
  80. package/dist/audit/rules/seo/keyword-placement.d.ts +0 -2
  81. package/dist/audit/rules/seo/keyword-placement.js +0 -122
  82. package/dist/audit/rules/seo/sitemap.d.ts +0 -5
  83. package/dist/audit/rules/seo/sitemap.js +0 -357
  84. package/dist/audit/rules/seo/title-missing.d.ts +0 -20
  85. package/dist/audit/rules/seo/title-missing.js +0 -33
  86. package/dist/audit/rules/structure/asset-refs.d.ts +0 -23
  87. package/dist/audit/rules/structure/asset-refs.js +0 -41
  88. package/dist/audit/rules/structure/internal-links.d.ts +0 -22
  89. package/dist/audit/rules/structure/internal-links.js +0 -28
  90. package/dist/audit/rules/structure/locale-coherence.d.ts +0 -17
  91. package/dist/audit/rules/structure/locale-coherence.js +0 -32
  92. package/dist/audit/rules/structure/namespace-coherence.d.ts +0 -17
  93. package/dist/audit/rules/structure/namespace-coherence.js +0 -39
  94. package/dist/audit/rules/structure/unresolved-directives.d.ts +0 -2
  95. package/dist/audit/rules/structure/unresolved-directives.js +0 -108
  96. package/dist/audit/rules/structure/version-coherence.d.ts +0 -25
  97. package/dist/audit/rules/structure/version-coherence.js +0 -42
  98. package/dist/audit/run.d.ts +0 -85
  99. package/dist/audit/run.js +0 -93
  100. package/dist/audit/types.d.ts +0 -203
  101. package/dist/audit/types.js +0 -1
  102. package/dist/commands/agent-run.js +0 -48
  103. package/dist/commands/agent.d.ts +0 -6
  104. package/dist/commands/agent.js +0 -305
  105. package/dist/commands/chunk.js +0 -121
  106. package/dist/commands/corpus.js +0 -63
  107. package/dist/commands/embed.js +0 -256
  108. package/dist/commands/enrich.js +0 -192
  109. package/dist/commands/index-build.js +0 -59
  110. package/dist/commands/pull.d.ts +0 -69
  111. package/dist/commands/pull.js +0 -838
  112. package/dist/commands/search.js +0 -144
  113. package/dist/commands/source.js +0 -777
  114. package/dist/corpus/agent-config.js +0 -212
  115. package/dist/corpus/bench.js +0 -694
  116. package/dist/corpus/classify-structured.js +0 -1132
  117. package/dist/corpus/classify.js +0 -331
  118. package/dist/corpus/comparison.js +0 -999
  119. package/dist/corpus/compile-check.js +0 -307
  120. package/dist/corpus/curate.js +0 -808
  121. package/dist/corpus/delta.js +0 -314
  122. package/dist/corpus/endpoints.js +0 -102
  123. package/dist/corpus/exchange.js +0 -189
  124. package/dist/corpus/feed.js +0 -176
  125. package/dist/corpus/git.js +0 -34
  126. package/dist/corpus/init.js +0 -135
  127. package/dist/corpus/judgment-cache.js +0 -109
  128. package/dist/corpus/near-duplicates.js +0 -192
  129. package/dist/corpus/ordering.js +0 -182
  130. package/dist/corpus/pack.js +0 -83
  131. package/dist/corpus/partition.js +0 -0
  132. package/dist/corpus/propose.js +0 -365
  133. package/dist/corpus/pull.js +0 -96
  134. package/dist/corpus/record-validation.js +0 -167
  135. package/dist/corpus/record.js +0 -194
  136. package/dist/corpus/review.js +0 -116
  137. package/dist/corpus/skill.js +0 -444
  138. package/dist/corpus/sources.js +0 -199
  139. package/dist/corpus/store.js +0 -203
  140. package/dist/corpus/update.js +0 -577
  141. package/dist/corpus/versions.js +0 -54
@@ -1,4 +1,4 @@
1
- import type { Issue } from "./types.js";
1
+ import type { Issue } from "@dogsbay/site-check";
2
2
  /**
3
3
  * Root identifiers referenced by an expression: quoted strings and
4
4
  * dotted property tails stripped, reserved words and numbers excluded.
@@ -1,11 +1,2 @@
1
- /**
2
- * Idempotent — calling `registerStructureRules` more than once
3
- * is a no-op.
4
- */
5
- export declare function registerStructureRules(): void;
6
- /**
7
- * Test-only: reset the "registered" flag so unit tests can
8
- * register fresh after `_clearRegistry()`. Production code never
9
- * calls this.
10
- */
11
- export declare function _resetStructureRegistration(): void;
1
+ /** Idempotent, like the shared register* functions. */
2
+ export declare function registerCliStructureRules(): void;
@@ -1,47 +1,17 @@
1
1
  /**
2
- * Structure rule category — register all `structure/*` rules
3
- * into the global registry. Imported by the audit runner /
4
- * site-check command at startup.
5
- *
6
- * Rules landed in this category:
7
- * - structure/namespace-coherence (corpus, multi-source axis)
8
- *
9
- * Future additions:
10
- * - structure/orphan-pages (corpus; pages not reachable from nav)
11
- * - structure/duplicate-slugs (corpus; two sources produced the same URL)
2
+ * The Astro CLI's own structure rules, registered on top of the shared
3
+ * ones from `@dogsbay/site-check`. Both read nav files and topic maps
4
+ * from disk, which dogsbay-ssg resolves differently.
12
5
  */
13
- import { registerRule } from "../../registry.js";
14
- import { assetRefs } from "./asset-refs.js";
15
- import { internalLinks } from "./internal-links.js";
16
- import { localeCoherence } from "./locale-coherence.js";
17
- import { namespaceCoherence } from "./namespace-coherence.js";
6
+ import { registerRule } from "@dogsbay/site-check";
18
7
  import { navTargetExists } from "./nav-target-exists.js";
19
- import { unresolvedDirectives } from "./unresolved-directives.js";
20
8
  import { topicmapValid } from "./topicmap-valid.js";
21
- import { versionCoherence } from "./version-coherence.js";
22
9
  let registered = false;
23
- /**
24
- * Idempotent — calling `registerStructureRules` more than once
25
- * is a no-op.
26
- */
27
- export function registerStructureRules() {
10
+ /** Idempotent, like the shared register* functions. */
11
+ export function registerCliStructureRules() {
28
12
  if (registered)
29
13
  return;
30
14
  registered = true;
31
- registerRule(namespaceCoherence);
32
- registerRule(versionCoherence);
33
- registerRule(localeCoherence);
34
15
  registerRule(navTargetExists);
35
- registerRule(internalLinks);
36
- registerRule(assetRefs);
37
- registerRule(unresolvedDirectives);
38
16
  registerRule(topicmapValid);
39
17
  }
40
- /**
41
- * Test-only: reset the "registered" flag so unit tests can
42
- * register fresh after `_clearRegistry()`. Production code never
43
- * calls this.
44
- */
45
- export function _resetStructureRegistration() {
46
- registered = false;
47
- }
@@ -1,2 +1,2 @@
1
- import type { Rule } from "../../types.js";
1
+ import type { Rule } from "@dogsbay/site-check";
2
2
  export declare const navTargetExists: Rule;
@@ -8,5 +8,5 @@
8
8
  * the errors to `AuditSourceInfo.topicmapErrors`; this rule just reports
9
9
  * them with the standard exit-code/severity plumbing.
10
10
  */
11
- import type { Rule } from "../../types.js";
11
+ import type { Rule } from "@dogsbay/site-check";
12
12
  export declare const topicmapValid: Rule;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Realize `::child-links` on every page against the site's nav
3
+ * (plans/child-links.md). Shared by `site build` (after the nav is final) and
4
+ * `convert --to <anything but dogsbay-md>`; Dogsbay MD keeps the directive.
5
+ */
6
+ import type { ExportPage, NavItem } from "@dogsbay/types";
7
+ export interface SiteChildLinksResult {
8
+ realized: number;
9
+ /** `page: folder` for every listing that found no child pages. */
10
+ empty: string[];
11
+ /** `page: folder` for listings answered from the page directory, not the nav. */
12
+ fromDirectory: string[];
13
+ }
14
+ /**
15
+ * @param prefix the href prefix the nav was built with (`""` for `convert`,
16
+ * urlBase + basePath for `site build`): a page's nav href is
17
+ * `prefix/slug`, and a `folder=` path is site-relative, so it is prefixed too.
18
+ */
19
+ export declare function realizeSiteChildLinks(pages: ExportPage[], nav: NavItem[], prefix: string): SiteChildLinksResult;
@@ -0,0 +1,48 @@
1
+ import { directoryChildren, hasChildLinks, navKey, realizeChildLinks } from "@dogsbay/serialize-core";
2
+ /**
3
+ * @param prefix the href prefix the nav was built with (`""` for `convert`,
4
+ * urlBase + basePath for `site build`): a page's nav href is
5
+ * `prefix/slug`, and a `folder=` path is site-relative, so it is prefixed too.
6
+ */
7
+ export function realizeSiteChildLinks(pages, nav, prefix) {
8
+ const out = { realized: 0, empty: [], fromDirectory: [] };
9
+ if (!pages.some((p) => hasChildLinks(p.tree)))
10
+ return out;
11
+ const base = prefix.replace(/\/+$/, "");
12
+ const hrefOf = (slug) => `${base}/${slug.replace(/(^|\/)index$/, "")}`;
13
+ const info = new Map();
14
+ for (const page of pages) {
15
+ const { description, tags } = page.frontmatter ?? {};
16
+ info.set(navKey(hrefOf(page.slug)), {
17
+ title: page.title || undefined,
18
+ description: typeof description === "string" ? description : undefined,
19
+ tags: Array.isArray(tags) ? tags.map(String) : undefined,
20
+ // For `match=` (front-matter filters).
21
+ data: page.frontmatter,
22
+ });
23
+ }
24
+ // Trees carry SITE-relative links: Astro adds the base path itself, and an
25
+ // export target (a vault, MkDocs) has none. The nav is built with it on.
26
+ const outputHref = (href) => base && (href === base || href.startsWith(`${base}/`)) ? href.slice(base.length) || "/" : href;
27
+ let fallbackChildren;
28
+ for (const page of pages) {
29
+ if (!hasChildLinks(page.tree))
30
+ continue;
31
+ // Fragments, partials (`_`-prefixed segments) and redirect stubs are not pages to list.
32
+ fallbackChildren ??= directoryChildren(pages.filter((p) => !p.fragment && !p.redirect && !p.slug.split("/").some((seg) => seg.startsWith("_"))), hrefOf);
33
+ const r = realizeChildLinks(page.tree, {
34
+ nav,
35
+ fallbackChildren,
36
+ outputHref,
37
+ pageHref: hrefOf(page.slug),
38
+ folderHref: (folder) => `${base}/${folder.replace(/^\/+/, "")}`,
39
+ lookup: (href) => info.get(navKey(href)),
40
+ });
41
+ out.realized += r.realized;
42
+ for (const e of r.empty)
43
+ out.empty.push(`${page.slug}: ${e}`);
44
+ for (const e of r.fromFallback)
45
+ out.fromDirectory.push(`${page.slug}: ${e}`);
46
+ }
47
+ return out;
48
+ }
@@ -4,6 +4,7 @@ import YAML from "yaml";
4
4
  import pc from "picocolors";
5
5
  import { existsSync, realpathSync } from "node:fs";
6
6
  import { realizeIncludes, hasIncludes } from "@dogsbay/serialize-core";
7
+ import { realizeSiteChildLinks } from "../child-links.js";
7
8
  import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md";
8
9
  import { FORMATS as formats, DOCUSAURUS_ADAPTERS as docusaurusAdapters, loadAdapter, MDX_ADAPTERS as mdxAdapters, GATSBY_ADAPTERS as gatsbyAdapters, STARLIGHT_ADAPTERS as starlightAdapters, detectFormat, importerNames, exporterNames, } from "../formats.js";
9
10
  // Re-exported so tests and programmatic callers keep a stable import site.
@@ -606,6 +607,7 @@ export async function runConvert(source, options, log = console.log) {
606
607
  ["--distro", options.distro],
607
608
  ["--topic-map", options.topicMap],
608
609
  ["--loader", options.loader],
610
+ ["--api-schema", options.apiSchema],
609
611
  ]
610
612
  .filter(([flag, value]) => typeof value === "string" && !declared.has(flag) && !CONSUMED_BY_CONVERT.has(flag))
611
613
  .map(([flag]) => flag);
@@ -632,6 +634,18 @@ export async function runConvert(source, options, log = console.log) {
632
634
  // where the pages actually land.
633
635
  if (options.hrefPrefix === undefined)
634
636
  options.hrefPrefix = "";
637
+ // Template partials travel as `{% with %}{% include %}` TEXT that only a
638
+ // Dogsbay-MD build renders (as template pages). Any other target would show
639
+ // the directives as literal text, so refuse before importing anything.
640
+ if (options.partialMode !== undefined && !["inline", "include", "template"].includes(String(options.partialMode))) {
641
+ throw new ConvertError(`--partial-mode must be inline, include or template (got ${String(options.partialMode)})`, "");
642
+ }
643
+ if (options.partialMode === "template" && importer.name !== "starlight") {
644
+ throw new ConvertError(`--partial-mode template is supported for --from starlight (got --from ${importer.name})`, "Other importers keep their partials inline, or use --partial-mode include (Docusaurus).");
645
+ }
646
+ if (options.partialMode === "template" && exporter.name !== "dogsbay-md") {
647
+ throw new ConvertError(`--partial-mode template needs --to dogsbay-md (got --to ${exporter.name})`, "Template partials are rendered by a Dogsbay-MD build. For other targets use --partial-mode inline.");
648
+ }
635
649
  const importResult = await importer.import(sourceDir, options);
636
650
  const { pages, nav, contentRoot } = importResult;
637
651
  // Mount the importer's asset directories so the `/_assets/…` refs it
@@ -660,6 +674,18 @@ export async function runConvert(source, options, log = console.log) {
660
674
  // legitimately keep shared fragments above the content dir.
661
675
  const roots = contentRoot ? [resolve(contentRoot), sourceDir] : [sourceDir];
662
676
  includeLoss = realizePageIncludes(pages, roots, log, options.partialMode === "include");
677
+ // `::child-links` → the listing, from the importer's nav. Dogsbay MD keeps
678
+ // the directive (the next build lists pages added since).
679
+ const links = realizeSiteChildLinks(pages, nav, String(options.hrefPrefix ?? ""));
680
+ if (links.realized) {
681
+ log(` Child links: ${links.realized} listing(s) realized from the nav` +
682
+ (links.fromDirectory.length ? ` (${links.fromDirectory.length} from the page directory: not in the nav)` : ""));
683
+ }
684
+ if (links.empty.length) {
685
+ log(pc.yellow(` Warning: ${links.empty.length} child-links listing(s) found no child pages:`));
686
+ for (const e of links.empty.slice(0, 10))
687
+ log(pc.yellow(` ${e}`));
688
+ }
663
689
  }
664
690
  const output = resolve(options.output || `${source}-${exporter.name}`);
665
691
  await exporter.export(pages, nav, output, options);
@@ -95,7 +95,7 @@ export async function migrateMkdocs(source, options) {
95
95
  console.log(`Output: ${outputDir}`);
96
96
  console.log();
97
97
  // 1. Output skeleton. The canonical Dogsbay layout (see
98
- // packages/cli/skills/platform/migration-shape/SKILL.md) puts
98
+ // packages/cli/skills/platform/dogsbay-migration-shape/SKILL.md) puts
99
99
  // `content/` and `dogsbay.config.yml` at the root; the
100
100
  // generated Astro project lives under `./astro/` (created by
101
101
  // emitSiteScaffold below).
@@ -122,7 +122,7 @@ export async function migrateMkdocs(source, options) {
122
122
  console.log(pc.green(`Wrote`) + ` ${pageCount} pages to ./content/ as Dogsbay-MD`);
123
123
  // 3. Convert MkDocs nav: → ./content/nav.yml in canonical
124
124
  // single-key-map shape (- Label: file.md / nested children).
125
- // See packages/cli/skills/platform/nav-file/SKILL.md.
125
+ // See packages/cli/skills/platform/dogsbay-nav-file/SKILL.md.
126
126
  // Do NOT emit nav.json — runtime loader prefers it and would
127
127
  // silently override the human-edited yml. The loader auto-
128
128
  // detects nav.yml at the content root.
@@ -305,7 +305,7 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
305
305
  // Convert `.md` links to root-relative slugs at parse time.
306
306
  // baseUrl: "" gives `/foo` / `/tutorial/quickstart` form (no
307
307
  // basePath prefix — site build adds /docs at render time).
308
- // See packages/cli/skills/platform/migration-shape/SKILL.md
308
+ // See packages/cli/skills/platform/dogsbay-migration-shape/SKILL.md
309
309
  // → "Internal links — root-relative absolute slugs".
310
310
  linkRewrite: { baseUrl: "" },
311
311
  // fileInclude: `{* ../../docs_src/foo.py *}` in MkDocs sources
@@ -388,7 +388,7 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
388
388
  // resolved relative paths to root-relative `/img/...` form;
389
389
  // this walker just adds the `_assets/` prefix so the refs line
390
390
  // up with where copyAssets deposits files. See
391
- // packages/cli/skills/platform/migration-shape/SKILL.md
391
+ // packages/cli/skills/platform/dogsbay-migration-shape/SKILL.md
392
392
  // → "Asset folder".
393
393
  rewriteImageRefsToAssets(tree);
394
394
  // No frontmatter title lift in Phase 3: format-mkdocs's
@@ -889,7 +889,7 @@ function copyAssets(srcDocsDir, destPublicDir) {
889
889
  *
890
890
  * NOT the `{ label: "...", file: "..." }` shape — the runtime
891
891
  * validator throws on multi-key entries. See
892
- * packages/cli/skills/platform/nav-file/SKILL.md.
892
+ * packages/cli/skills/platform/dogsbay-nav-file/SKILL.md.
893
893
  */
894
894
  function convertNav(nav) {
895
895
  if (!Array.isArray(nav))
@@ -5,5 +5,24 @@ export interface MigrateMystOptions {
5
5
  siteUrl?: string;
6
6
  local?: boolean;
7
7
  quiet?: boolean;
8
+ /** "sphinx" | "translate"; auto when omitted (see chooseMystLoader). */
9
+ loader?: string;
8
10
  }
11
+ /**
12
+ * Which way to import a MyST project. A Sphinx project (conf.py) built by
13
+ * Sphinx resolves what the translator cannot see from one page at a time:
14
+ * `{ref}` to a label on another page, `{term}` to the glossary, extlinks
15
+ * roles, toctrees across documents. Measured on Ubuntu Server (255 MyST
16
+ * pages): 4 link errors through Sphinx against 657 translated. The
17
+ * translator stays the path for mystmd / Jupyter Book (no conf.py), for a
18
+ * machine without Sphinx, and for a repository whose code should not run.
19
+ */
20
+ export declare function chooseMystLoader(sourceDir: string, requested: string | undefined, has: {
21
+ sphinxSrc: boolean;
22
+ sphinx: boolean;
23
+ }): {
24
+ loader: "sphinx" | "translate";
25
+ note?: string;
26
+ error?: string;
27
+ };
9
28
  export declare function migrateMyst(source: string, options: MigrateMystOptions): Promise<void>;
@@ -15,12 +15,48 @@
15
15
  * Pure Markdown — no external runtime needed. Mirrors migrate-rst /
16
16
  * migrate-gitbook in shape.
17
17
  */
18
- import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
18
+ import { existsSync, mkdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
19
19
  import { basename, dirname, join, relative, resolve } from "node:path";
20
20
  import pc from "picocolors";
21
21
  import { stringify as stringifyYaml } from "yaml";
22
22
  import { emitSiteScaffold } from "@dogsbay/format-astro";
23
23
  import { importMyst } from "@dogsbay/format-myst/cli";
24
+ import { findSphinxSrc, pythonHasModule, sphinxAvailable } from "@dogsbay/format-rst";
25
+ import { migrateRst, mirrorAssets } from "./migrate-rst.js";
26
+ import { copyOpenApiSpecs } from "../resolve-openapi.js";
27
+ /**
28
+ * Which way to import a MyST project. A Sphinx project (conf.py) built by
29
+ * Sphinx resolves what the translator cannot see from one page at a time:
30
+ * `{ref}` to a label on another page, `{term}` to the glossary, extlinks
31
+ * roles, toctrees across documents. Measured on Ubuntu Server (255 MyST
32
+ * pages): 4 link errors through Sphinx against 657 translated. The
33
+ * translator stays the path for mystmd / Jupyter Book (no conf.py), for a
34
+ * machine without Sphinx, and for a repository whose code should not run.
35
+ */
36
+ export function chooseMystLoader(sourceDir, requested, has) {
37
+ if (requested && requested !== "sphinx" && requested !== "translate") {
38
+ return { loader: "translate", error: `--loader must be 'sphinx' or 'translate', not '${requested}'` };
39
+ }
40
+ if (requested === "translate")
41
+ return { loader: "translate" };
42
+ if (requested === "sphinx") {
43
+ if (!has.sphinxSrc)
44
+ return { loader: "translate", error: `--loader sphinx needs a conf.py under ${sourceDir}` };
45
+ if (!has.sphinx)
46
+ return { loader: "translate", error: "--loader sphinx needs Sphinx (and the project's extensions) for the Python Dogsbay uses (DOGSBAY_PYTHON)" };
47
+ return { loader: "sphinx" };
48
+ }
49
+ if (has.sphinxSrc && has.sphinx) {
50
+ return { loader: "sphinx", note: "a Sphinx project (conf.py): building it with Sphinx. Use --loader translate to translate the MyST without running project code." };
51
+ }
52
+ if (has.sphinxSrc) {
53
+ return {
54
+ loader: "translate",
55
+ note: "a Sphinx project (conf.py), but Sphinx with myst-parser is not installed for DOGSBAY_PYTHON: translating the MyST directly. Cross-page {ref} labels, {term} and extlinks roles will not resolve; install the project's docs requirements for a Sphinx build.",
56
+ };
57
+ }
58
+ return { loader: "translate" };
59
+ }
24
60
  function yamlQuote(s) {
25
61
  return /[:#&*!|>%@`,'"[\]{}]|^\s|\s$/.test(s) ? JSON.stringify(s) : s;
26
62
  }
@@ -65,68 +101,43 @@ function buildConfig(siteName, siteUrl) {
65
101
  function frontmatterBlock(title) {
66
102
  return `---\n${stringifyYaml({ title }).replace(/\n$/, "")}\n---\n\n`;
67
103
  }
68
- /**
69
- * Real page/doc assets — an allowlist (not a blocklist) so a MyST project
70
- * tree's `.py` config helpers, `conf.py`, `requirements.txt`, etc. don't get
71
- * swept into the migrated content/.
72
- */
73
- const ASSET_EXTS = new Set([
74
- ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".bmp",
75
- ".pdf", ".css", ".js", ".mjs", ".json", ".woff", ".woff2", ".ttf", ".otf",
76
- ".mp4", ".webm", ".mov", ".txt", ".csv", ".zip", ".tar", ".gz",
77
- ]);
78
- const SKIP_DIRS = new Set(["node_modules", ".git", "_build", "_templates", "__pycache__"]);
79
- /** Mirror asset files (by extension allowlist) from the docs root into content/. */
80
- function mirrorAssets(root, contentDir) {
81
- let count = 0;
82
- const walk = (dir) => {
83
- let entries;
84
- try {
85
- entries = readdirSync(dir);
86
- }
87
- catch {
88
- return;
89
- }
90
- for (const name of entries) {
91
- if (SKIP_DIRS.has(name))
92
- continue;
93
- const full = join(dir, name);
94
- let st;
95
- try {
96
- st = statSync(full);
97
- }
98
- catch {
99
- continue;
100
- }
101
- if (st.isDirectory()) {
102
- if (name.startsWith("."))
103
- continue;
104
- walk(full);
105
- continue;
106
- }
107
- const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
108
- if (!ASSET_EXTS.has(ext))
109
- continue; // skip .md, .py, conf.py, etc.
110
- const dest = join(contentDir, relative(root, full));
111
- try {
112
- mkdirSync(dirname(dest), { recursive: true });
113
- copyFileSync(full, dest);
114
- count += 1;
115
- }
116
- catch {
117
- /* ignore individual asset failures */
118
- }
119
- }
120
- };
121
- walk(root);
122
- return count;
123
- }
124
104
  export async function migrateMyst(source, options) {
125
105
  const sourceDir = resolve(source);
126
106
  if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {
127
107
  console.log(pc.red(`Error: source must be a directory: ${sourceDir}`));
128
108
  process.exit(1);
129
109
  }
110
+ const choice = chooseMystLoader(sourceDir, options.loader, {
111
+ sphinxSrc: findSphinxSrc(sourceDir) !== null,
112
+ // Sphinx alone cannot read MyST: myst-parser must be there too, or the
113
+ // build fails on a machine that has Sphinx for some other project.
114
+ sphinx: options.loader === "translate" ? false : sphinxAvailable() && pythonHasModule("myst_parser"),
115
+ });
116
+ if (choice.error) {
117
+ console.log(pc.red(`Error: ${choice.error}`));
118
+ process.exit(1);
119
+ }
120
+ if (choice.note)
121
+ console.log(pc.yellow(`myst: ${choice.note}`));
122
+ if (choice.loader === "sphinx") {
123
+ // The same pipeline as migrate-rst: Sphinx builds the MyST pages through
124
+ // myst-parser, and its XML is converted (toctree nav, labels, glossary,
125
+ // redirects). It prints the trust warning about running conf.py.
126
+ try {
127
+ return await migrateRst(source, { ...options, loader: "sphinx", corpusLabel: "MyST (Sphinx)", throwOnImportError: true });
128
+ }
129
+ catch (err) {
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ // Asked for explicitly: a failure is the answer.
132
+ if (options.loader === "sphinx") {
133
+ console.log(pc.red(`Error: ${msg}`));
134
+ process.exit(1);
135
+ }
136
+ // Chosen automatically: the project's extensions may simply not be
137
+ // installed. Translate instead, and say so.
138
+ console.log(pc.yellow(`myst: the Sphinx build failed; translating the MyST directly instead.\n ${msg.split("\n")[1] ?? msg}`));
139
+ }
140
+ }
130
141
  const siteName = options.siteName?.trim() || prettify(basename(sourceDir));
131
142
  const outputDir = resolve(options.output || `${basename(sourceDir)}-dogsbay`);
132
143
  const existingConfig = ["yml", "yaml", "json"]
@@ -146,9 +157,9 @@ export async function migrateMyst(source, options) {
146
157
  console.log(`Source: ${sourceDir}`);
147
158
  console.log(`Output: ${outputDir}`);
148
159
  console.log();
149
- let pages, nav, docsDir, navSource;
160
+ let pages, nav, docsDir, navSource, excludeAsset;
150
161
  try {
151
- ({ pages, nav, docsDir, navSource } = await importMyst(sourceDir, {}));
162
+ ({ pages, nav, docsDir, navSource, excludeAsset } = await importMyst(sourceDir, {}));
152
163
  }
153
164
  catch (err) {
154
165
  const msg = err instanceof Error ? err.message : String(err);
@@ -184,9 +195,15 @@ export async function migrateMyst(source, options) {
184
195
  console.log(pc.green(`Converted`) +
185
196
  ` ${pages.length} page(s)` +
186
197
  (collisions ? pc.yellow(` (${collisions} collision(s) skipped)`) : ""));
187
- const assetCount = mirrorAssets(docsDir, contentDir);
198
+ // migrate-rst's mirror: links are not followed (one in someone else's
199
+ // repo can name any file on this machine), and what conf.py excludes stays
200
+ // out.
201
+ const assetCount = mirrorAssets(docsDir, contentDir, excludeAsset);
188
202
  if (assetCount > 0)
189
203
  console.log(pc.green(`Mirrored`) + ` ${assetCount} asset(s)`);
204
+ const specCount = copyOpenApiSpecs(pages, docsDir, contentDir);
205
+ if (specCount > 0)
206
+ console.log(pc.green(`Copied`) + ` ${specCount} OpenAPI spec(s) the API reference pages embed`);
190
207
  writeFileSync(join(contentDir, "nav.yml"), `# Migrated by dogsbay migrate-myst.\n# Edit freely.\n\n${navToYaml(nav)}\n`);
191
208
  console.log(pc.green(`Wrote`) + ` content/nav.yml`);
192
209
  writeFileSync(join(outputDir, "dogsbay.config.yml"), buildConfig(siteName, options.siteUrl));
@@ -7,5 +7,12 @@ export interface MigrateRstOptions {
7
7
  loader?: string;
8
8
  local?: boolean;
9
9
  quiet?: boolean;
10
+ /** What the source is called in output; migrate-myst passes "MyST (Sphinx)". */
11
+ corpusLabel?: string;
12
+ /** Throw an import failure instead of exiting, so the caller can fall back. */
13
+ throwOnImportError?: boolean;
10
14
  }
15
+ /** Mirror asset files (by extension allowlist) from the docs root into
16
+ * content/, skipping what `exclude` names (a root-relative path). */
17
+ export declare function mirrorAssets(root: string, contentDir: string, exclude?: (rel: string) => boolean): number;
11
18
  export declare function migrateRst(source: string, options: MigrateRstOptions): Promise<void>;