@dogsbay/format-docusaurus 0.2.0-beta.78

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Docusaurus frontmatter extraction.
3
+ *
4
+ * Returns a typed subset of the well-known docs frontmatter fields plus the
5
+ * full raw record (so downstream exporters can read custom fields) and the
6
+ * body with the YAML block stripped.
7
+ *
8
+ * Docs: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs#markdown-front-matter
9
+ */
10
+ import matter from "gray-matter";
11
+ export function extractFrontmatter(source) {
12
+ // No frontmatter fence → nothing to strip.
13
+ if (source.split("\n")[0]?.trim() !== "---") {
14
+ return { frontmatter: {}, raw: {}, body: source };
15
+ }
16
+ let raw = {};
17
+ let body = source;
18
+ try {
19
+ const result = matter(source);
20
+ raw = result.data ?? {};
21
+ body = result.content;
22
+ }
23
+ catch {
24
+ // Malformed YAML — fall back to manual fence stripping, leave raw empty.
25
+ const lines = source.split("\n");
26
+ let end = 1;
27
+ while (end < lines.length && lines[end]?.trim() !== "---")
28
+ end++;
29
+ body = lines.slice(end + 1).join("\n");
30
+ }
31
+ const frontmatter = {
32
+ id: typeof raw.id === "string" ? raw.id : undefined,
33
+ title: typeof raw.title === "string" ? raw.title : undefined,
34
+ slug: typeof raw.slug === "string" ? raw.slug : undefined,
35
+ sidebar_position: typeof raw.sidebar_position === "number" ? raw.sidebar_position : undefined,
36
+ sidebar_label: typeof raw.sidebar_label === "string" ? raw.sidebar_label : undefined,
37
+ sidebar_class_name: typeof raw.sidebar_class_name === "string" ? raw.sidebar_class_name : undefined,
38
+ description: typeof raw.description === "string" ? raw.description : undefined,
39
+ hide_title: typeof raw.hide_title === "boolean" ? raw.hide_title : undefined,
40
+ draft: typeof raw.draft === "boolean" ? raw.draft : undefined,
41
+ tags: raw.tags,
42
+ pagination_label: typeof raw.pagination_label === "string" ? raw.pagination_label : undefined,
43
+ };
44
+ return { frontmatter, raw, body };
45
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @dogsbay/format-docusaurus — generic Docusaurus importer.
3
+ *
4
+ * Public API. Site-specific behaviour is provided by injected adapters
5
+ * (see the DocusaurusAdapter interface); this package ships none.
6
+ */
7
+ export { docusaurusToTree, type ParseOptions } from "./parse-mdx.js";
8
+ export { extractFrontmatter, type DocusaurusFrontmatter } from "./frontmatter.js";
9
+ export { parseCodeMeta, type CodeMeta } from "./code-meta.js";
10
+ export { findConfig, parseDocusaurusConfig, selectInstance, type DocsInstance, } from "./config.js";
11
+ export { buildNav, buildNavFromDirectory, docIdToFilePath, makeDocHref, makeDocLabel, } from "./nav.js";
12
+ export { buildNavFromSidebars, loadSidebars, type SidebarContext, type RawSidebars, type RawSidebarItem, } from "./sidebars.js";
13
+ export { loadCjs } from "./load-cjs.js";
14
+ export type { DocusaurusAdapter, ComponentMapping, PreprocessContext, } from "./adapter.js";
15
+ export { plugin } from "./cli.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @dogsbay/format-docusaurus — generic Docusaurus importer.
3
+ *
4
+ * Public API. Site-specific behaviour is provided by injected adapters
5
+ * (see the DocusaurusAdapter interface); this package ships none.
6
+ */
7
+ export { docusaurusToTree } from "./parse-mdx.js";
8
+ export { extractFrontmatter } from "./frontmatter.js";
9
+ export { parseCodeMeta } from "./code-meta.js";
10
+ export { findConfig, parseDocusaurusConfig, selectInstance, } from "./config.js";
11
+ export { buildNav, buildNavFromDirectory, docIdToFilePath, makeDocHref, makeDocLabel, } from "./nav.js";
12
+ export { buildNavFromSidebars, loadSidebars, } from "./sidebars.js";
13
+ export { loadCjs } from "./load-cjs.js";
14
+ export { plugin } from "./cli.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Load a CJS/ESM module from an absolute or relative path and return its
3
+ * default export (or the whole module namespace for named-only exports).
4
+ */
5
+ export declare function loadCjs<T = unknown>(modulePath: string): Promise<T>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Single trust boundary for executing JavaScript from the source repo.
3
+ *
4
+ * Docusaurus config and sidebar files (`docusaurus.config.js`, `sidebars*.js`)
5
+ * are CommonJS modules that export plain objects/arrays. The simplest faithful
6
+ * way to read them is to `require()` them — this handles their own internal
7
+ * `require('./releases.json')` calls, computed values, and helper imports.
8
+ *
9
+ * SECURITY: this EXECUTES JavaScript from the source repository. That is
10
+ * acceptable for a local migration CLI pointed at a trusted repo (the same
11
+ * trust you already extend by running its `make build`). It is NOT safe for
12
+ * untrusted input. All such execution funnels through this one function so it
13
+ * can later be swapped for a sandboxed `node:vm` / worker evaluation without
14
+ * touching callers.
15
+ *
16
+ * Supports both `module.exports = X` (CommonJS) and `export default X` /
17
+ * named exports (ESM `.mjs` or `.js` with `"type":"module"`).
18
+ */
19
+ import { createRequire } from "node:module";
20
+ import { pathToFileURL } from "node:url";
21
+ import { resolve } from "node:path";
22
+ const require = createRequire(import.meta.url);
23
+ /**
24
+ * Load a CJS/ESM module from an absolute or relative path and return its
25
+ * default export (or the whole module namespace for named-only exports).
26
+ */
27
+ export async function loadCjs(modulePath) {
28
+ const abs = resolve(modulePath);
29
+ // Try CommonJS require first (the common case for Docusaurus config/sidebars).
30
+ try {
31
+ // Bust the require cache so re-imports in the same process re-read the file.
32
+ delete require.cache[require.resolve(abs)];
33
+ const mod = require(abs);
34
+ return (mod?.default ?? mod);
35
+ }
36
+ catch (err) {
37
+ // Fall back to dynamic import for ESM modules (.mjs / "type":"module").
38
+ const isEsmError = err instanceof Error &&
39
+ /require\(\) of ES Module|Cannot use import statement|ERR_REQUIRE_ESM/.test(err.message);
40
+ if (!isEsmError)
41
+ throw err;
42
+ }
43
+ const mod = await import(pathToFileURL(abs).href);
44
+ return (mod.default ?? mod);
45
+ }
package/dist/nav.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { NavItem } from "@dogsbay/types";
2
+ /** Resolve a Docusaurus doc id to an on-disk file within the instance dir. */
3
+ export declare function docIdToFilePath(instanceDir: string, id: string): string | null;
4
+ /** doc id → href under hrefPrefix; trailing "/index" is dropped. */
5
+ export declare function makeDocHref(hrefPrefix: string): (id: string) => string;
6
+ /** doc id → label from frontmatter (sidebar_label/title), H1, or humanized id. */
7
+ export declare function makeDocLabel(instanceDir: string): (id: string) => string;
8
+ /**
9
+ * Build nav by scanning a directory tree. Used for `autogenerated` sidebar
10
+ * items and as the whole-instance fallback when there is no sidebar file.
11
+ */
12
+ export declare function buildNavFromDirectory(instanceDir: string, hrefPrefix: string, scanDir?: string): NavItem[];
13
+ export interface BuildNavOptions {
14
+ instanceDir: string;
15
+ hrefPrefix: string;
16
+ /** Absolute path to the sidebar module (from the config's sidebarPath). */
17
+ sidebarPath?: string | null;
18
+ }
19
+ export declare function buildNav(opts: BuildNavOptions): Promise<NavItem[]>;
package/dist/nav.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Navigation builder for a Docusaurus docs instance.
3
+ *
4
+ * Mode 1 (preferred): explicit sidebar from `sidebarPath` → buildNavFromSidebars.
5
+ * Mode 2 (fallback): scan the instance directory, reading `_category_.json` and
6
+ * frontmatter `sidebar_position`/`sidebar_label`, sorted by position.
7
+ *
8
+ * `buildNav` picks Mode 1 when a sidebar is present and loadable, else Mode 2.
9
+ */
10
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
11
+ import { join, basename } from "node:path";
12
+ import { extractFrontmatter } from "./frontmatter.js";
13
+ import { buildNavFromSidebars, loadSidebars, } from "./sidebars.js";
14
+ const DOC_EXTS = [".mdx", ".md"];
15
+ /** Resolve a Docusaurus doc id to an on-disk file within the instance dir. */
16
+ export function docIdToFilePath(instanceDir, id) {
17
+ for (const ext of DOC_EXTS) {
18
+ const direct = join(instanceDir, id + ext);
19
+ if (existsSync(direct))
20
+ return direct;
21
+ }
22
+ for (const ext of DOC_EXTS) {
23
+ const asIndex = join(instanceDir, id, "index" + ext);
24
+ if (existsSync(asIndex))
25
+ return asIndex;
26
+ }
27
+ return null;
28
+ }
29
+ /** Humanize a slug segment: "managed-public-cloud" → "Managed public cloud". */
30
+ function humanize(segment) {
31
+ const words = segment.replace(/[-_]+/g, " ").trim();
32
+ return words.charAt(0).toUpperCase() + words.slice(1);
33
+ }
34
+ /** First H1 in a markdown body, if any. */
35
+ function firstH1(body) {
36
+ const m = body.match(/^#\s+(.+?)\s*$/m);
37
+ return m ? m[1].trim() : undefined;
38
+ }
39
+ /** doc id → href under hrefPrefix; trailing "/index" is dropped. */
40
+ export function makeDocHref(hrefPrefix) {
41
+ const prefix = hrefPrefix.replace(/\/$/, "");
42
+ return (id) => {
43
+ const clean = id.replace(/\/index$/, "").replace(/^index$/, "");
44
+ return clean ? `${prefix}/${clean}` : prefix;
45
+ };
46
+ }
47
+ /** doc id → label from frontmatter (sidebar_label/title), H1, or humanized id. */
48
+ export function makeDocLabel(instanceDir) {
49
+ return (id) => {
50
+ const filePath = docIdToFilePath(instanceDir, id);
51
+ if (filePath) {
52
+ const src = readFileSync(filePath, "utf-8");
53
+ const { frontmatter, body } = extractFrontmatter(src);
54
+ if (frontmatter.sidebar_label)
55
+ return frontmatter.sidebar_label;
56
+ if (frontmatter.title)
57
+ return frontmatter.title;
58
+ const h1 = firstH1(body);
59
+ if (h1)
60
+ return h1;
61
+ }
62
+ const seg = id.replace(/\/index$/, "").split("/").pop() || id;
63
+ return humanize(seg);
64
+ };
65
+ }
66
+ function readCategoryMeta(dir) {
67
+ const p = join(dir, "_category_.json");
68
+ if (!existsSync(p))
69
+ return {};
70
+ try {
71
+ return JSON.parse(readFileSync(p, "utf-8"));
72
+ }
73
+ catch {
74
+ return {};
75
+ }
76
+ }
77
+ /**
78
+ * Build nav by scanning a directory tree. Used for `autogenerated` sidebar
79
+ * items and as the whole-instance fallback when there is no sidebar file.
80
+ */
81
+ export function buildNavFromDirectory(instanceDir, hrefPrefix, scanDir = instanceDir) {
82
+ const docHref = makeDocHref(hrefPrefix);
83
+ const entries = [];
84
+ for (const name of readdirSync(scanDir)) {
85
+ if (name.startsWith("_"))
86
+ continue; // _category_.json, _includes, etc.
87
+ const full = join(scanDir, name);
88
+ const stat = statSync(full);
89
+ if (stat.isDirectory()) {
90
+ const meta = readCategoryMeta(full);
91
+ const children = buildNavFromDirectory(instanceDir, hrefPrefix, full);
92
+ if (children.length === 0)
93
+ continue;
94
+ const nav = { label: meta.label ?? humanize(name), children };
95
+ if (meta.link?.type === "doc" && meta.link.id)
96
+ nav.href = docHref(meta.link.id);
97
+ entries.push({ nav, position: meta.position ?? Number.MAX_SAFE_INTEGER, sortKey: name });
98
+ continue;
99
+ }
100
+ if (!DOC_EXTS.some((e) => name.endsWith(e)))
101
+ continue;
102
+ if (name === "index.mdx" || name === "index.md")
103
+ continue; // category overview, not a child
104
+ const src = readFileSync(full, "utf-8");
105
+ const { frontmatter, body } = extractFrontmatter(src);
106
+ const id = relativeDocId(instanceDir, full);
107
+ const label = frontmatter.sidebar_label ?? frontmatter.title ?? firstH1(body) ?? humanize(basename(name).replace(/\.mdx?$/, ""));
108
+ entries.push({
109
+ nav: { label, href: docHref(id) },
110
+ position: frontmatter.sidebar_position ?? Number.MAX_SAFE_INTEGER,
111
+ sortKey: name,
112
+ });
113
+ }
114
+ entries.sort((a, b) => a.position !== b.position ? a.position - b.position : a.sortKey.localeCompare(b.sortKey));
115
+ return entries.map((e) => e.nav);
116
+ }
117
+ /** File path → doc id relative to the instance dir (no extension, "/index" kept). */
118
+ function relativeDocId(instanceDir, filePath) {
119
+ let rel = filePath.slice(instanceDir.length).replace(/\\/g, "/").replace(/^\//, "");
120
+ rel = rel.replace(/\.mdx?$/, "");
121
+ return rel;
122
+ }
123
+ export async function buildNav(opts) {
124
+ const { instanceDir, hrefPrefix, sidebarPath } = opts;
125
+ if (sidebarPath && existsSync(sidebarPath)) {
126
+ const ctx = {
127
+ docHref: makeDocHref(hrefPrefix),
128
+ docLabel: makeDocLabel(instanceDir),
129
+ expandAutogenerated: (dirName) => buildNavFromDirectory(instanceDir, hrefPrefix, join(instanceDir, dirName)),
130
+ };
131
+ try {
132
+ const sidebars = await loadSidebars(sidebarPath);
133
+ const nav = buildNavFromSidebars(sidebars, ctx);
134
+ if (nav.length > 0)
135
+ return nav;
136
+ }
137
+ catch (err) {
138
+ console.warn(`Warning: could not load sidebar ${sidebarPath} (${err.message}); falling back to directory scan.`);
139
+ }
140
+ }
141
+ return buildNavFromDirectory(instanceDir, hrefPrefix);
142
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Generic Docusaurus MDX parser.
3
+ *
4
+ * Reads a standard Docusaurus `.mdx`/`.md` body and produces TreeNode[].
5
+ * Handles only built-in Docusaurus syntax:
6
+ * - :::admonitions (note/tip/info/warning/danger/caution) with optional title
7
+ * - <Tabs> / <TabItem value label>
8
+ * - <details> / <summary>
9
+ * - <CodeBlock> and fenced code with metastrings (title/{ranges}/showLineNumbers)
10
+ * - <DocCardList> / <DocCard> (theme built-ins → placeholder + warning)
11
+ * - import/export MDX statements (stripped)
12
+ *
13
+ * Site-specific components, variable substitution, and partial includes are
14
+ * the job of an injected `DocusaurusAdapter` — never this file.
15
+ */
16
+ import type { TreeNode, NavItem } from "@dogsbay/types";
17
+ import type { DocusaurusAdapter, PreprocessContext } from "./adapter.js";
18
+ export interface ParseOptions {
19
+ adapter?: DocusaurusAdapter;
20
+ /** Docusaurus routeBasePath of the instance (e.g. "calico"); used for link rewrite. */
21
+ routeBasePath?: string;
22
+ /** Dogsbay href prefix to rewrite internal links to (default "/docs"). */
23
+ hrefPrefix?: string;
24
+ /**
25
+ * Slug of the file being parsed (e.g. "guides/install"). Relative doc links
26
+ * (`../foo.mdx`) resolve against its directory into absolute hrefs.
27
+ */
28
+ currentSlug?: string;
29
+ /**
30
+ * Asset-root prefix for root-absolute image srcs (e.g. "/_assets"). Set when
31
+ * the project's static dir is mounted into `_assets`; maps `/img/x` →
32
+ * `/_assets/img/x`. Omitted → image srcs are left as-is.
33
+ */
34
+ assetPrefix?: string;
35
+ /**
36
+ * Resolve a Docusaurus doc id → its served href + title. Used to fill in
37
+ * `card` nodes that reference a target by `docId` (e.g. `<DocCardLink>`).
38
+ */
39
+ resolveDoc?: (docId: string) => {
40
+ href: string;
41
+ title: string;
42
+ } | undefined;
43
+ /**
44
+ * Resolve the current page's sidebar-category children → nav items, used to
45
+ * build a real `cards` grid for `<DocCardList>`. Returns undefined when the
46
+ * page isn't a category index (→ DocCardList falls back to a placeholder).
47
+ */
48
+ resolveDocCardList?: (currentSlug: string) => NavItem[] | undefined;
49
+ /** Context passed to the adapter's preprocess hook. */
50
+ preprocessContext?: PreprocessContext;
51
+ /** Warning sink (e.g. for DocCardList placeholders). */
52
+ onWarn?: (msg: string) => void;
53
+ }
54
+ export declare function docusaurusToTree(source: string, options?: ParseOptions): TreeNode[];