@writedocs/generator 0.4.8 → 0.4.10

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 (68) hide show
  1. package/astro.config.mjs +45 -2
  2. package/bin/writedocs.js +190 -139
  3. package/package.json +2 -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/AppIcon.astro +14 -2
  11. package/src/components/Badge.astro +2 -0
  12. package/src/components/Callout.astro +2 -1
  13. package/src/components/Card.astro +2 -1
  14. package/src/components/CardGroup.astro +2 -1
  15. package/src/components/Check.astro +1 -1
  16. package/src/components/CodeBlock.astro +94 -0
  17. package/src/components/CodeGroup.astro +2 -1
  18. package/src/components/Color.astro +2 -1
  19. package/src/components/ColorItem.astro +2 -1
  20. package/src/components/ColorRow.astro +2 -1
  21. package/src/components/Column.astro +19 -0
  22. package/src/components/Columns.astro +1 -1
  23. package/src/components/Danger.astro +1 -1
  24. package/src/components/Expandable.astro +2 -1
  25. package/src/components/Frame.astro +2 -1
  26. package/src/components/GitHubRepo.astro +2 -1
  27. package/src/components/Hint.astro +2 -1
  28. package/src/components/Icon.astro +3 -2
  29. package/src/components/Image.astro +2 -1
  30. package/src/components/Info.astro +1 -1
  31. package/src/components/Note.astro +1 -1
  32. package/src/components/Panel.astro +2 -1
  33. package/src/components/Parameter.astro +2 -1
  34. package/src/components/Prompt.astro +2 -1
  35. package/src/components/RequestExample.astro +2 -1
  36. package/src/components/ResponseExample.astro +2 -1
  37. package/src/components/Searchbar.astro +2 -1
  38. package/src/components/Step.astro +2 -1
  39. package/src/components/Steps.astro +4 -1
  40. package/src/components/Tab.astro +2 -1
  41. package/src/components/Tabs.astro +2 -1
  42. package/src/components/Tile.astro +2 -1
  43. package/src/components/Tip.astro +1 -1
  44. package/src/components/TreeFile.astro +2 -1
  45. package/src/components/TreeFolder.astro +2 -1
  46. package/src/components/Update.astro +2 -1
  47. package/src/components/Video.astro +2 -1
  48. package/src/components/View.astro +2 -1
  49. package/src/components/Warning.astro +1 -1
  50. package/src/components/class-names.ts +8 -0
  51. package/src/components/index.ts +2 -0
  52. package/src/content.config.ts +29 -129
  53. package/src/lib/config-schema.js +124 -0
  54. package/src/lib/config-schema.ts +1574 -1437
  55. package/src/lib/config.ts +7 -140
  56. package/src/lib/content-check.js +262 -0
  57. package/src/lib/icons.js +109 -0
  58. package/src/lib/mdx-auto-hydrate.js +12 -0
  59. package/src/lib/mdx-inject-builtins.js +16 -1
  60. package/src/lib/mdx-inline-react.js +202 -0
  61. package/src/lib/mdx-mintlify.js +65 -0
  62. package/src/lib/mdx-substitute-variables.js +17 -0
  63. package/src/lib/mdx-unknown-components.js +149 -0
  64. package/src/lib/mintlify-convert.js +599 -0
  65. package/src/lib/openapi-ref.js +44 -0
  66. package/src/lib/openapi-render.ts +10 -1
  67. package/src/lib/pages.js +150 -0
  68. 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 })) {
@@ -0,0 +1,150 @@
1
+ // Which files in a content directory are pages. Plain JavaScript, not
2
+ // TypeScript, so `writedocs validate` can load it with plain Node (Node
3
+ // refuses to strip types under node_modules - see lib/icons.js's comment);
4
+ // lib/config.ts re-exports findAllPages() for everything inside Astro.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import matter from 'gray-matter';
8
+
9
+ // Top-level folders of a content directory that are never scanned for
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().
13
+ export const EXCLUDED_TOP_LEVEL_DIRS = new Set(['node_modules', 'dist', '.astro', '.writedocs', 'public', '.git']);
14
+
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
88
+ * other folder, no special-casing. Returns POSIX-relative paths (from
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
93
+ * astro.config.mjs's noindex/sitemap scan and by `writedocs validate`, so
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. */
97
+ export function findAllPages(contentDir) {
98
+ const results = [];
99
+ const ignored = readIgnoreFile(contentDir);
100
+ const listed = navigationPageIds(contentDir);
101
+ function walk(dir, relBase) {
102
+ let entries;
103
+ try {
104
+ entries = fs.readdirSync(dir, { withFileTypes: true });
105
+ } catch {
106
+ return;
107
+ }
108
+ for (const entry of entries) {
109
+ const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
110
+ const abs = path.join(dir, entry.name);
111
+ if (entry.isDirectory()) {
112
+ if (relBase === '' && EXCLUDED_TOP_LEVEL_DIRS.has(entry.name)) continue;
113
+ if (entry.name.startsWith('.')) continue;
114
+ if (ignored(rel, true)) continue;
115
+ walk(abs, rel);
116
+ continue;
117
+ }
118
+ if (!entry.isFile() || !/\.mdx?$/i.test(entry.name)) continue;
119
+ if (ignored(rel, false)) continue;
120
+ let raw;
121
+ try {
122
+ raw = fs.readFileSync(abs, 'utf-8');
123
+ } catch {
124
+ continue;
125
+ }
126
+ let data;
127
+ try {
128
+ ({ data } = matter(raw));
129
+ } catch {
130
+ // Frontmatter that isn't valid YAML: still a page (it opens a
131
+ // frontmatter block), so the error surfaces with this file's name -
132
+ // from `writedocs validate`, or Astro's own loader in the build -
133
+ // instead of as a bare YAML error thrown from here.
134
+ if (/^---\r?\n/.test(raw)) results.push(rel);
135
+ continue;
136
+ }
137
+ if (Object.keys(data).length === 0 && !listed.has(fileIdForPath(rel))) continue; // not a page
138
+ results.push(rel);
139
+ }
140
+ }
141
+ walk(contentDir, '');
142
+ return results;
143
+ }
144
+
145
+ /** The id writedocs.json's navigation refers to a page by: its path from
146
+ * the content directory, without the extension or a trailing `/index`
147
+ * (same algorithm as fileIdForEntry() in lib/config.ts). */
148
+ export function fileIdForPath(relativePath) {
149
+ return relativePath.replace(/\.mdx?$/i, '').replace(/\/index$/, '');
150
+ }
@@ -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,