@writedocs/generator 0.4.9 → 0.4.11

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 (63) hide show
  1. package/astro.config.mjs +39 -2
  2. package/bin/writedocs.js +23 -0
  3. package/package.json +1 -1
  4. package/src/cli/convert.js +82 -0
  5. package/src/cli/generate-api-pages.js +56 -3
  6. package/src/components/Accordion.astro +2 -1
  7. package/src/components/AccordionGroup.astro +4 -1
  8. package/src/components/ApiPlayground.astro +6 -2
  9. package/src/components/ApiReferencePanel.astro +6 -2
  10. package/src/components/Badge.astro +2 -0
  11. package/src/components/Callout.astro +2 -1
  12. package/src/components/Card.astro +2 -1
  13. package/src/components/CardGroup.astro +2 -1
  14. package/src/components/Check.astro +1 -1
  15. package/src/components/CodeBlock.astro +94 -0
  16. package/src/components/CodeGroup.astro +2 -1
  17. package/src/components/Color.astro +2 -1
  18. package/src/components/ColorItem.astro +2 -1
  19. package/src/components/ColorRow.astro +2 -1
  20. package/src/components/Column.astro +19 -0
  21. package/src/components/Columns.astro +1 -1
  22. package/src/components/Danger.astro +1 -1
  23. package/src/components/Expandable.astro +2 -1
  24. package/src/components/Frame.astro +2 -1
  25. package/src/components/GitHubRepo.astro +2 -1
  26. package/src/components/Hint.astro +2 -1
  27. package/src/components/Icon.astro +3 -2
  28. package/src/components/Image.astro +2 -1
  29. package/src/components/Info.astro +1 -1
  30. package/src/components/Note.astro +1 -1
  31. package/src/components/Panel.astro +2 -1
  32. package/src/components/Parameter.astro +2 -1
  33. package/src/components/Prompt.astro +2 -1
  34. package/src/components/RequestExample.astro +2 -1
  35. package/src/components/ResponseExample.astro +2 -1
  36. package/src/components/Searchbar.astro +2 -1
  37. package/src/components/Step.astro +2 -1
  38. package/src/components/Steps.astro +4 -1
  39. package/src/components/Tab.astro +2 -1
  40. package/src/components/Tabs.astro +2 -1
  41. package/src/components/Tile.astro +2 -1
  42. package/src/components/Tip.astro +1 -1
  43. package/src/components/TreeFile.astro +2 -1
  44. package/src/components/TreeFolder.astro +2 -1
  45. package/src/components/Update.astro +2 -1
  46. package/src/components/Video.astro +2 -1
  47. package/src/components/View.astro +2 -1
  48. package/src/components/Warning.astro +1 -1
  49. package/src/components/class-names.ts +8 -0
  50. package/src/components/index.ts +2 -0
  51. package/src/content.config.ts +23 -2
  52. package/src/lib/content-check.js +36 -6
  53. package/src/lib/mdx-auto-hydrate.js +12 -0
  54. package/src/lib/mdx-inject-builtins.js +15 -0
  55. package/src/lib/mdx-inline-react.js +202 -0
  56. package/src/lib/mdx-mintlify.js +65 -0
  57. package/src/lib/mdx-substitute-variables.js +17 -0
  58. package/src/lib/mdx-unknown-components.js +56 -3
  59. package/src/lib/mintlify-convert.js +599 -0
  60. package/src/lib/openapi-ref.js +44 -0
  61. package/src/lib/openapi-render.ts +10 -1
  62. package/src/lib/pages.js +89 -17
  63. package/src/pages/[...slug].astro +8 -2
@@ -11,6 +11,7 @@
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import { writedocsTempDir } from './writedocs-temp-dir.js';
14
+ import { specDirName } from './openapi-ref.js';
14
15
 
15
16
  export interface OpenApiSchema {
16
17
  type?: string;
@@ -58,10 +59,18 @@ export function operationFileName(method: string, urlPath: string): string {
58
59
  * every "operations" directory it finds for the target filename. Returns
59
60
  * null if no group's spec defines this operation (a stale/typo'd
60
61
  * `openapi:` frontmatter value). */
61
- export function findOperationFile(contentDir: string, method: string, urlPath: string): string | null {
62
+ export function findOperationFile(contentDir: string, method: string, urlPath: string, spec?: string | null): string | null {
62
63
  const openapiRoot = path.join(writedocsTempDir(contentDir), 'openapi');
63
64
  const filename = operationFileName(method, urlPath);
64
65
 
66
+ // A page that names its spec (Mintlify's "spec.json METHOD /path" -
67
+ // see lib/openapi-ref.js) looks in that spec's own operations first, so
68
+ // the same METHOD /path in two specs resolves to the right one.
69
+ if (spec) {
70
+ const own = path.join(openapiRoot, '_pages', specDirName(spec), 'operations', filename);
71
+ if (fs.existsSync(own)) return own;
72
+ }
73
+
65
74
  function walk(dir: string): string | null {
66
75
  if (!fs.existsSync(dir)) return null;
67
76
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
package/src/lib/pages.js CHANGED
@@ -7,28 +7,97 @@ import path from 'node:path';
7
7
  import matter from 'gray-matter';
8
8
 
9
9
  // Top-level folders of a content directory that are never scanned for
10
- // pages - build output, dependencies, and static assets.
10
+ // pages - build output, dependencies, and static assets. Any folder whose
11
+ // name starts with a dot (.git, .github, .claude, .mintlify, ...) is
12
+ // skipped too, at any depth - see findAllPages().
11
13
  export const EXCLUDED_TOP_LEVEL_DIRS = new Set(['node_modules', 'dist', '.astro', '.writedocs', 'public', '.git']);
12
14
 
13
- /** Recursively finds every .md/.mdx file under `contentDir` that has a
14
- * frontmatter block, skipping the handful of build/dependency
15
- * directories a real content directory tends to also contain (see
16
- * EXCLUDED_TOP_LEVEL_DIRS above) - docs/ is scanned exactly like any
15
+ /** A page's title when its frontmatter has none - Mintlify's rule, so a
16
+ * migrated page that relied on it keeps the same title: the file name
17
+ * without its extension, dashes and underscores as spaces, first letter
18
+ * capitalized. `guides/getting-started.mdx` -> "Getting started". */
19
+ export function titleFromPath(filePath) {
20
+ const base = String(filePath).split(/[\\/]/).pop().replace(/\.mdx?$/i, '');
21
+ const words = base.replace(/[-_]+/g, ' ').trim();
22
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : 'Untitled';
23
+ }
24
+
25
+ /** Mintlify's .mintignore, if the content directory has one: gitignore-
26
+ * style patterns for files and folders that aren't published. Supports
27
+ * comments, blank lines, `*`, `**`, `?`, a leading `/` (anchored to the
28
+ * content directory) and a trailing `/` (folders only); negation (`!`)
29
+ * isn't supported and such lines are skipped. Returns (relPath, isDir) =>
30
+ * boolean. */
31
+ function readIgnoreFile(contentDir) {
32
+ let text;
33
+ try {
34
+ text = fs.readFileSync(path.join(contentDir, '.mintignore'), 'utf-8');
35
+ } catch {
36
+ return () => false;
37
+ }
38
+ const rules = text
39
+ .split(/\r?\n/)
40
+ .map((l) => l.trim())
41
+ .filter((l) => l && !l.startsWith('#') && !l.startsWith('!'))
42
+ .map((pattern) => {
43
+ const dirOnly = pattern.endsWith('/');
44
+ let p = pattern.replace(/\/+$/, '');
45
+ const anchored = p.startsWith('/') || p.includes('/');
46
+ p = p.replace(/^\/+/, '');
47
+ const source = p
48
+ .split(/(\*\*\/?|\*|\?)/)
49
+ .map((part) =>
50
+ part === '**/' || part === '**' ? '(?:.*/)?' : part === '*' ? '[^/]*' : part === '?' ? '[^/]' : part.replace(/[.+^${}()|[\]\\]/g, '\\$&')
51
+ )
52
+ .join('');
53
+ const re = new RegExp(anchored ? `^${source}$` : `(?:^|/)${source}$`);
54
+ return { re, dirOnly };
55
+ });
56
+ return (rel, isDir) => rules.some((r) => (!r.dirOnly || isDir) && r.re.test(rel));
57
+ }
58
+
59
+ /** Page ids writedocs.json's navigation lists (strings in arrays, and
60
+ * groups' `page`) - read straight from the file, since this runs before
61
+ * (and independently of) config validation. Empty when there's no
62
+ * readable writedocs.json. */
63
+ function navigationPageIds(contentDir) {
64
+ let config;
65
+ try {
66
+ config = JSON.parse(fs.readFileSync(path.join(contentDir, 'writedocs.json'), 'utf-8'));
67
+ } catch {
68
+ return new Set();
69
+ }
70
+ const ids = new Set();
71
+ (function walk(node) {
72
+ if (Array.isArray(node)) node.forEach((item) => (typeof item === 'string' ? ids.add(item) : walk(item)));
73
+ else if (node && typeof node === 'object') {
74
+ for (const [key, value] of Object.entries(node)) {
75
+ if (key === 'page' && typeof value === 'string') ids.add(value);
76
+ else if (key !== 'openapi' && key !== 'href') walk(value);
77
+ }
78
+ }
79
+ })(config.navigation);
80
+ return ids;
81
+ }
82
+
83
+ /** Recursively finds every .md/.mdx file under `contentDir` that is a page:
84
+ * one with a frontmatter block, or one writedocs.json's navigation lists
85
+ * (a Mintlify page needs no frontmatter at all - see titleFromPath()).
86
+ * Skips EXCLUDED_TOP_LEVEL_DIRS, every dot-folder at any depth, and
87
+ * anything a .mintignore file lists. docs/ is scanned exactly like any
17
88
  * other folder, no special-casing. Returns POSIX-relative paths (from
18
- * `contentDir`) suitable to hand straight to Astro's `glob()` loader as
19
- * a literal `pattern` array - see content.config.ts's `pages`
20
- * collection, and [...slug].astro, which needs the identical list to
21
- * decide whether calling `getCollection('pages')` is worth doing at all
22
- * (see content.config.ts's own comment on why an empty collection still
23
- * needs to exist, just backed by a no-op loader, to avoid Astro's "does
24
- * not exist or is empty" warning). Also reused directly by
89
+ * `contentDir`) suitable to hand straight to Astro's `glob()` loader as a
90
+ * literal `pattern` array - see content.config.ts's `pages` collection,
91
+ * and [...slug].astro, which needs the identical list to decide whether
92
+ * calling `getCollection('pages')` is worth doing at all. Also reused by
25
93
  * astro.config.mjs's noindex/sitemap scan and by `writedocs validate`, so
26
- * both always see exactly the same file set that actually becomes a page.
27
- * Synchronous and re-run from scratch wherever it's called rather than
28
- * cached and shared across modules - cheap enough in practice (a docs
29
- * site's own file count) not to matter. */
94
+ * all of them see exactly the same file set. Synchronous and re-run from
95
+ * scratch wherever it's called - cheap enough for a docs site's file
96
+ * count. */
30
97
  export function findAllPages(contentDir) {
31
98
  const results = [];
99
+ const ignored = readIgnoreFile(contentDir);
100
+ const listed = navigationPageIds(contentDir);
32
101
  function walk(dir, relBase) {
33
102
  let entries;
34
103
  try {
@@ -41,10 +110,13 @@ export function findAllPages(contentDir) {
41
110
  const abs = path.join(dir, entry.name);
42
111
  if (entry.isDirectory()) {
43
112
  if (relBase === '' && EXCLUDED_TOP_LEVEL_DIRS.has(entry.name)) continue;
113
+ if (entry.name.startsWith('.')) continue;
114
+ if (ignored(rel, true)) continue;
44
115
  walk(abs, rel);
45
116
  continue;
46
117
  }
47
118
  if (!entry.isFile() || !/\.mdx?$/i.test(entry.name)) continue;
119
+ if (ignored(rel, false)) continue;
48
120
  let raw;
49
121
  try {
50
122
  raw = fs.readFileSync(abs, 'utf-8');
@@ -62,7 +134,7 @@ export function findAllPages(contentDir) {
62
134
  if (/^---\r?\n/.test(raw)) results.push(rel);
63
135
  continue;
64
136
  }
65
- if (Object.keys(data).length === 0) continue; // no frontmatter at all - not a page
137
+ if (Object.keys(data).length === 0 && !listed.has(fileIdForPath(rel))) continue; // not a page
66
138
  results.push(rel);
67
139
  }
68
140
  }
@@ -20,6 +20,7 @@ import {
20
20
  findAllPages,
21
21
  } from "../lib/config";
22
22
  import { writedocsTempDir } from "../lib/writedocs-temp-dir.js";
23
+ import { parseOpenApiRef } from "../lib/openapi-ref.js";
23
24
  import BaseLayout from "../layout/BaseLayout.astro";
24
25
  import Sidebar from "../layout/components/Sidebar.astro";
25
26
  import TableOfContents from "../layout/components/TableOfContents.astro";
@@ -55,6 +56,8 @@ import Check from "../components/Check.astro";
55
56
  import ParamField from "../components/ParamField.astro";
56
57
  import ResponseField from "../components/ResponseField.astro";
57
58
  import Columns from "../components/Columns.astro";
59
+ import Column from "../components/Column.astro";
60
+ import CodeBlock from "../components/CodeBlock.astro";
58
61
  import Tooltip from "../components/Tooltip.astro";
59
62
  import Update from "../components/Update.astro";
60
63
  import Tile from "../components/Tile.astro";
@@ -321,8 +324,9 @@ for (const e of entries) {
321
324
  navTitleByFileId.set(fileId, e.data.sidebarTitle ?? e.data.title);
322
325
  entryByFileId.set(fileId, e);
323
326
  // `hideApiMarker` (Mintlify's) drops the badge for this one page.
324
- if (e.data.openapi && !e.data.hideApiMarker) {
325
- methodByFileId.set(fileId, e.data.openapi.trim().split(/\s+/, 1)[0].toUpperCase());
327
+ const apiRef = parseOpenApiRef(e.data.openapi);
328
+ if (apiRef && !e.data.hideApiMarker) {
329
+ methodByFileId.set(fileId, apiRef.method);
326
330
  }
327
331
  }
328
332
  const titleForSlug = (slug: string) => titleByFileId.get(slug) ?? slug;
@@ -444,6 +448,8 @@ const components = {
444
448
  ParamField,
445
449
  ResponseField,
446
450
  Columns,
451
+ Column,
452
+ CodeBlock,
447
453
  Tooltip,
448
454
  Update,
449
455
  Tile,