@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
package/astro.config.mjs CHANGED
@@ -26,7 +26,14 @@ import { remarkAutoHydrateSnippets } from './src/lib/mdx-auto-hydrate.js';
26
26
  import { remarkInjectBuiltinComponents } from './src/lib/mdx-inject-builtins.js';
27
27
  import { remarkTitleAnchorIds } from './src/lib/mdx-title-anchor-ids.js';
28
28
  import { remarkSubstituteVariables } from './src/lib/mdx-substitute-variables.js';
29
- import { remarkMintlifyTreeLists, remarkMintlifyPromptText } from './src/lib/mdx-mintlify.js';
29
+ import {
30
+ remarkMintlifyTreeLists,
31
+ remarkMintlifyPromptText,
32
+ remarkMintlifyReactHooks,
33
+ mintlifyReactHooksPlugin,
34
+ } from './src/lib/mdx-mintlify.js';
35
+ import { remarkUnknownComponentFallback } from './src/lib/mdx-unknown-components.js';
36
+ import { remarkExtractInlineReactComponents } from './src/lib/mdx-inline-react.js';
30
37
  import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
31
38
  import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
32
39
  import {
@@ -46,6 +53,13 @@ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
46
53
  // does the actual cross-filesystem-safe copy into contentDir/dist as an
47
54
  // explicit final step once the build itself is done.
48
55
  const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || path.dirname(fileURLToPath(import.meta.url));
56
+ // Where this package's dependencies are: the node_modules folder that
57
+ // contains an installed package (<project>/node_modules/@writedocs/
58
+ // generator -> <project>/node_modules), or packageRoot itself in a checkout,
59
+ // whose own node_modules is inside it. Used for the dev server's file-serving
60
+ // allow list below.
61
+ const nodeModulesAt = packageRoot.lastIndexOf(`${path.sep}node_modules${path.sep}`);
62
+ const dependencyRoot = nodeModulesAt === -1 ? packageRoot : packageRoot.slice(0, nodeModulesAt + `${path.sep}node_modules`.length);
49
63
 
50
64
  // Read here (rather than deferred to page-render time, where
51
65
  // loadDocsConfig() is also called from [...slug].astro) specifically so
@@ -370,6 +384,19 @@ export default defineConfig({
370
384
  // the tree when it looks for components to import.
371
385
  remarkMintlifyTreeLists,
372
386
  remarkMintlifyPromptText,
387
+ // An inline component that calls React hooks moves into a generated
388
+ // .jsx file so it runs as real React (and hydrates) - see
389
+ // lib/mdx-inline-react.js. Before remarkMintlifyReactHooks, which
390
+ // imports any hooks the page itself still calls (Mintlify
391
+ // pre-injects them), and before remarkAutoHydrateSnippets, which
392
+ // hydrates the component through its new .jsx import.
393
+ remarkExtractInlineReactComponents,
394
+ remarkMintlifyReactHooks,
395
+ // An unknown component becomes a fragment (its children still
396
+ // render) with a warning, instead of failing the whole build - see
397
+ // lib/mdx-unknown-components.js. After remarkMintlifyTreeLists, so
398
+ // the <Tree.Folder>/<Tree.File> elements it adds count as known.
399
+ remarkUnknownComponentFallback,
373
400
  remarkInjectBuiltinComponents,
374
401
  // Runs after remarkInjectBuiltinComponents (order doesn't actually
375
402
  // matter between them - that plugin only ever prepends an import
@@ -435,10 +462,26 @@ export default defineConfig({
435
462
  // working. See src/styles/global.css for the one @import that wires
436
463
  // Tailwind's utilities in.
437
464
  vite: {
465
+ // The dev server only serves files under its workspace root by default.
466
+ // A site's own snippets live in its content directory, and inline React
467
+ // components moved out of pages (lib/mdx-inline-react.js) in writedocs'
468
+ // temp directory - both need to reach the browser to hydrate. Setting
469
+ // `allow` replaces Vite's default rather than adding to it, so the
470
+ // package's dependencies must stay reachable too: when writedocs is
471
+ // installed, they sit next to it in the node_modules folder that
472
+ // contains it (React's own hydration client included), not inside
473
+ // packageRoot - see dependencyRoot above.
474
+ server: {
475
+ fs: {
476
+ allow: [packageRoot, dependencyRoot, contentDir, writedocsTempDir(contentDir)],
477
+ },
478
+ },
438
479
  // contentTailwindSource() must come before tailwindcss(): both are
439
480
  // enforce: 'pre' transforms, which Vite runs in array order, and
440
481
  // Tailwind has to see the added @source when it compiles global.css.
441
- plugins: [contentTailwindSource(), tailwindcss()],
482
+ // mintlifyReactHooksPlugin() does for a site's .jsx/.tsx snippets what
483
+ // remarkMintlifyReactHooks does for its MDX pages.
484
+ plugins: [contentTailwindSource(), tailwindcss(), mintlifyReactHooksPlugin(contentDir)],
442
485
  resolve: {
443
486
  alias: [
444
487
  // Lets a page's MDX write `import Foo from '/snippets/foo.mdx'`
package/bin/writedocs.js CHANGED
@@ -1,139 +1,190 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import dotenv from 'dotenv';
7
- import { runDev } from '../src/cli/dev.js';
8
- import { runBuild } from '../src/cli/build.js';
9
- import { runInit } from '../src/cli/init.js';
10
- import { requireBuildKey } from '../src/cli/build-auth.js';
11
- // O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
12
- // que a plataforma importa por `@writedocs/generator/config-schema` - e o que
13
- // faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
14
- // cada um manter a sua copia do schema.
15
- //
16
- // `.js`, e nao o `.ts` que e a fonte de verdade: este arquivo e carregado por
17
- // node puro, sem o transform do Astro/Vite, e o Node RECUSA type stripping para
18
- // qualquer arquivo sob node_modules - por desenho, em qualquer versao
19
- // (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). Ou seja: importar o `.ts` aqui
20
- // funciona a partir do checkout e falha 100% das vezes para quem instala o
21
- // pacote, que foi exatamente o que quebrou o `validate` no 0.2.0. O `.js` e
22
- // gerado do `.ts` pelo script `prepare` (package.json), entao ele existe tanto
23
- // no checkout quanto dentro do tarball, e o consumidor nao roda build nenhum.
24
- // Ver a mesma regra ja escrita em src/cli/write-redirects-file.js.
25
- import {
26
- validateDocsConfig,
27
- formatValidationIssuesDetailed,
28
- unknownRootKeyIssues,
29
- } from '../src/lib/config-schema.js';
30
-
31
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
- const packageRoot = path.resolve(__dirname, '..');
33
- // Read rather than hardcode - a literal version string here silently drifts
34
- // from package.json's own "version" the moment either one is bumped without
35
- // the other (exactly what `npm version <bump>` does: it only touches
36
- // package.json). `writedocs --version` should always reflect what actually
37
- // got published, not whatever this string happened to say at the time this
38
- // line was last hand-edited.
39
- const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
40
-
41
- const program = new Command();
42
- program
43
- .name('writedocs')
44
- .description('Static site generator for writedocs.json + MDX')
45
- .version(version);
46
-
47
- program
48
- .command('dev')
49
- .description('Start a local dev server with hot reload')
50
- .argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
51
- .option('-p, --port <port>', 'port to run on')
52
- .action(async (dir, opts) => {
53
- await runDev({
54
- contentDir: path.resolve(process.cwd(), dir),
55
- packageRoot,
56
- port: opts.port,
57
- });
58
- });
59
-
60
- program
61
- // { hidden: true } keeps `build` out of `writedocs --help` - it's not a
62
- // command ordinary users are meant to discover or run themselves. See
63
- // src/cli/build-auth.js for the second layer: even someone who knows the
64
- // command exists still can't run it without the right key.
65
- .command('build', { hidden: true })
66
- .description('Build a static site into <dir>/dist')
67
- .argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
68
- .option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
69
- .action(async (dir, opts) => {
70
- const contentDir = path.resolve(process.cwd(), dir);
71
- // Project-scoped, not global: a .env sitting next to this project's own
72
- // writedocs.json (WRITEDOCS_KEY_SERVER_URL, WRITEDOCS_API_KEY) is picked
73
- // up automatically, so build doesn't need those exported by hand every
74
- // session. dotenv never overwrites a var already set in the real
75
- // environment - an explicit `export`/CI secret still wins over the file.
76
- dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
77
- await requireBuildKey(opts.key || process.env.WRITEDOCS_API_KEY);
78
- await runBuild({ contentDir, packageRoot });
79
- });
80
-
81
- program
82
- // Visivel, ao contrario do `build`: validar a propria configuracao e
83
- // exatamente o tipo de coisa que um usuario deve poder rodar sozinho, no CI
84
- // inclusive. Sem chave, sem dotenv, sem rede - le um arquivo e sai 0 ou 1.
85
- .command('validate')
86
- .description('Validate writedocs.json against the schema')
87
- .argument('[dir]', 'content directory (contains writedocs.json)', '.')
88
- .action(async (dir) => {
89
- const contentDir = path.resolve(process.cwd(), dir);
90
- const configPath = path.join(contentDir, 'writedocs.json');
91
- if (!fs.existsSync(configPath)) {
92
- // Mesma mensagem que loadDocsConfig() ja da pro mesmo caso.
93
- console.error(
94
- `[writedocs] writedocs.json not found at ${configPath}. Run "writedocs init" to scaffold one.`
95
- );
96
- process.exit(1);
97
- }
98
-
99
- const rawText = fs.readFileSync(configPath, 'utf-8');
100
- const result = validateDocsConfig(rawText);
101
- // Chave desconhecida na raiz e AVISO, nunca erro: a raiz nao e `.strict()`
102
- // de proposito (tornar strict quebraria configs existentes), entao o Zod
103
- // descarta a chave em silencio. Sem JSON parseavel nao ha chave pra avaliar.
104
- const warnings = result.kind === 'invalid_json' ? [] : unknownRootKeyIssues(rawText);
105
- const nomeArquivo = path.basename(configPath);
106
-
107
- if (!result.ok) {
108
- // A versao longa (linha, frase humana, sugestao) em vez da curta que o
109
- // `writedocs build` imprime: aqui o usuario pediu explicitamente um
110
- // diagnostico, e tem a tela inteira pra ele.
111
- console.error(`[writedocs] ${configPath} failed validation:\n`);
112
- console.error(formatValidationIssuesDetailed(result.issues, { fileName: nomeArquivo }));
113
- console.error('');
114
- }
115
-
116
- if (warnings.length > 0) {
117
- console.error(`[writedocs] ${warnings.length} warning${warnings.length === 1 ? '' : 's'}:\n`);
118
- console.error(formatValidationIssuesDetailed(warnings, { fileName: nomeArquivo }));
119
- console.error('');
120
- }
121
-
122
- if (!result.ok) process.exit(1);
123
- // Aviso nao invalida: um config so com avisos continua valido, e sai 0 -
124
- // o mesmo criterio que a plataforma usa pra marcar o projeto como `valid`.
125
- console.log(`[writedocs] ${configPath} is valid.`);
126
- });
127
-
128
- program
129
- .command('init')
130
- .description('Scaffold a writedocs.json and starter docs/ folder')
131
- .argument('[dir]', 'directory to scaffold into', '.')
132
- .action(async (dir) => {
133
- await runInit({ targetDir: path.resolve(process.cwd(), dir) });
134
- });
135
-
136
- program.parseAsync(process.argv).catch((err) => {
137
- console.error(`[writedocs] ${err.message}`);
138
- process.exit(1);
139
- });
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import dotenv from 'dotenv';
7
+ import { runDev } from '../src/cli/dev.js';
8
+ import { runBuild } from '../src/cli/build.js';
9
+ import { runInit } from '../src/cli/init.js';
10
+ import { requireBuildKey } from '../src/cli/build-auth.js';
11
+ // O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
12
+ // que a plataforma importa por `@writedocs/generator/config-schema` - e o que
13
+ // faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
14
+ // cada um manter a sua copia do schema.
15
+ //
16
+ // `.js`, e nao o `.ts` que e a fonte de verdade: este arquivo e carregado por
17
+ // node puro, sem o transform do Astro/Vite, e o Node RECUSA type stripping para
18
+ // qualquer arquivo sob node_modules - por desenho, em qualquer versao
19
+ // (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). Ou seja: importar o `.ts` aqui
20
+ // funciona a partir do checkout e falha 100% das vezes para quem instala o
21
+ // pacote, que foi exatamente o que quebrou o `validate` no 0.2.0. O `.js` e
22
+ // gerado do `.ts` pelo script `prepare` (package.json), entao ele existe tanto
23
+ // no checkout quanto dentro do tarball, e o consumidor nao roda build nenhum.
24
+ // Ver a mesma regra ja escrita em src/cli/write-redirects-file.js.
25
+ import {
26
+ validateDocsConfig,
27
+ formatValidationIssuesDetailed,
28
+ unknownRootKeyIssues,
29
+ } from '../src/lib/config-schema.js';
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const packageRoot = path.resolve(__dirname, '..');
33
+ // Read rather than hardcode - a literal version string here silently drifts
34
+ // from package.json's own "version" the moment either one is bumped without
35
+ // the other (exactly what `npm version <bump>` does: it only touches
36
+ // package.json). `writedocs --version` should always reflect what actually
37
+ // got published, not whatever this string happened to say at the time this
38
+ // line was last hand-edited.
39
+ const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
40
+
41
+ const program = new Command();
42
+ program
43
+ .name('writedocs')
44
+ .description('Static site generator for writedocs.json + MDX')
45
+ .version(version);
46
+
47
+ program
48
+ .command('dev')
49
+ .description('Start a local dev server with hot reload')
50
+ .argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
51
+ .option('-p, --port <port>', 'port to run on')
52
+ .action(async (dir, opts) => {
53
+ await runDev({
54
+ contentDir: path.resolve(process.cwd(), dir),
55
+ packageRoot,
56
+ port: opts.port,
57
+ });
58
+ });
59
+
60
+ program
61
+ // { hidden: true } keeps `build` out of `writedocs --help` - it's not a
62
+ // command ordinary users are meant to discover or run themselves. See
63
+ // src/cli/build-auth.js for the second layer: even someone who knows the
64
+ // command exists still can't run it without the right key.
65
+ .command('build', { hidden: true })
66
+ .description('Build a static site into <dir>/dist')
67
+ .argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
68
+ .option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
69
+ .action(async (dir, opts) => {
70
+ const contentDir = path.resolve(process.cwd(), dir);
71
+ // Project-scoped, not global: a .env sitting next to this project's own
72
+ // writedocs.json (WRITEDOCS_KEY_SERVER_URL, WRITEDOCS_API_KEY) is picked
73
+ // up automatically, so build doesn't need those exported by hand every
74
+ // session. dotenv never overwrites a var already set in the real
75
+ // environment - an explicit `export`/CI secret still wins over the file.
76
+ dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
77
+ await requireBuildKey(opts.key || process.env.WRITEDOCS_API_KEY);
78
+ await runBuild({ contentDir, packageRoot });
79
+ });
80
+
81
+ program
82
+ // Visivel, ao contrario do `build`: validar a propria configuracao e
83
+ // exatamente o tipo de coisa que um usuario deve poder rodar sozinho, no CI
84
+ // inclusive. Sem chave, sem dotenv, sem rede - le um arquivo e sai 0 ou 1.
85
+ .command('validate')
86
+ .description('Validate writedocs.json and every page - frontmatter, MDX, navigation, components, icons')
87
+ .argument('[dir]', 'content directory (contains writedocs.json)', '.')
88
+ .option('--config-only', 'check writedocs.json only, not the pages')
89
+ .action(async (dir, options) => {
90
+ const contentDir = path.resolve(process.cwd(), dir);
91
+ const configPath = path.join(contentDir, 'writedocs.json');
92
+ if (!fs.existsSync(configPath)) {
93
+ // Mesma mensagem que loadDocsConfig() ja da pro mesmo caso.
94
+ console.error(
95
+ `[writedocs] writedocs.json not found at ${configPath}. Run "writedocs init" to scaffold one.`
96
+ );
97
+ process.exit(1);
98
+ }
99
+
100
+ const rawText = fs.readFileSync(configPath, 'utf-8');
101
+ const result = validateDocsConfig(rawText);
102
+ // Chave desconhecida na raiz e AVISO, nunca erro: a raiz nao e `.strict()`
103
+ // de proposito (tornar strict quebraria configs existentes), entao o Zod
104
+ // descarta a chave em silencio. Sem JSON parseavel nao ha chave pra avaliar.
105
+ const warnings = result.kind === 'invalid_json' ? [] : unknownRootKeyIssues(rawText);
106
+ const nomeArquivo = path.basename(configPath);
107
+
108
+ if (!result.ok) {
109
+ // A versao longa (linha, frase humana, sugestao) em vez da curta que o
110
+ // `writedocs build` imprime: aqui o usuario pediu explicitamente um
111
+ // diagnostico, e tem a tela inteira pra ele.
112
+ console.error(`[writedocs] ${configPath} failed validation:\n`);
113
+ console.error(formatValidationIssuesDetailed(result.issues, { fileName: nomeArquivo }));
114
+ console.error('');
115
+ }
116
+
117
+ if (warnings.length > 0) {
118
+ console.error(`[writedocs] ${warnings.length} warning${warnings.length === 1 ? '' : 's'}:\n`);
119
+ console.error(formatValidationIssuesDetailed(warnings, { fileName: nomeArquivo }));
120
+ console.error('');
121
+ }
122
+
123
+ // The pages: everything that would fail the build (errors) or build
124
+ // differently than written (warnings), in one pass - see
125
+ // src/lib/content-check.js. Runs even when writedocs.json itself is
126
+ // invalid, so one run reports both; only the navigation/icon checks
127
+ // need writedocs.json to at least be valid JSON.
128
+ let content = null;
129
+ if (!options.configOnly) {
130
+ const { checkContent, formatContentIssues } = await import('../src/lib/content-check.js');
131
+ content = await checkContent(contentDir, result.kind === 'invalid_json' ? null : rawText);
132
+ if (content.errors.length > 0) {
133
+ console.error(`[writedocs] ${content.errors.length} error${content.errors.length === 1 ? '' : 's'} in the pages:\n`);
134
+ console.error(formatContentIssues(content.errors));
135
+ console.error('');
136
+ }
137
+ if (content.warnings.length > 0) {
138
+ console.error(`[writedocs] ${content.warnings.length} warning${content.warnings.length === 1 ? '' : 's'} in the pages:\n`);
139
+ console.error(formatContentIssues(content.warnings));
140
+ console.error('');
141
+ }
142
+ }
143
+
144
+ if (!result.ok || (content && content.errors.length > 0)) process.exit(1);
145
+ // Aviso nao invalida: um config so com avisos continua valido, e sai 0 -
146
+ // o mesmo criterio que a plataforma usa pra marcar o projeto como `valid`.
147
+ console.log(`[writedocs] ${configPath} is valid.`);
148
+ if (content) {
149
+ console.log(
150
+ `[writedocs] Checked ${content.pages} page${content.pages === 1 ? '' : 's'}: no errors` +
151
+ (content.warnings.length ? `, ${content.warnings.length} warning${content.warnings.length === 1 ? '' : 's'} above.` : '.')
152
+ );
153
+ }
154
+ });
155
+
156
+ program
157
+ .command('convert')
158
+ .description("Convert another docs tool's config into writedocs.json")
159
+ .argument('[dir]', 'project directory (contains docs.json)', '.')
160
+ .option('--mintlify', "convert a Mintlify project's docs.json")
161
+ .option('--docs.json', 'same as --mintlify')
162
+ .option('--force', 'overwrite an existing writedocs.json')
163
+ .option('--dry-run', 'print the converted writedocs.json instead of writing it')
164
+ .action(async (dir, options) => {
165
+ // --mintlify is the only source today; the flag is still required so
166
+ // the command reads the same once other tools are added.
167
+ if (!options.mintlify && !options.docsJson) {
168
+ console.error('[writedocs] Say what to convert from: writedocs convert --mintlify [dir]');
169
+ process.exit(1);
170
+ }
171
+ const { runConvert } = await import('../src/cli/convert.js');
172
+ await runConvert({
173
+ contentDir: path.resolve(process.cwd(), dir),
174
+ force: Boolean(options.force),
175
+ dryRun: Boolean(options.dryRun),
176
+ });
177
+ });
178
+
179
+ program
180
+ .command('init')
181
+ .description('Scaffold a writedocs.json and starter docs/ folder')
182
+ .argument('[dir]', 'directory to scaffold into', '.')
183
+ .action(async (dir) => {
184
+ await runInit({ targetDir: path.resolve(process.cwd(), dir) });
185
+ });
186
+
187
+ program.parseAsync(process.argv).catch((err) => {
188
+ console.error(`[writedocs] ${err.message}`);
189
+ process.exit(1);
190
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedocs/generator",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,7 @@
59
59
  "@iconify-json/ri": "^1.2.10",
60
60
  "@iconify-json/simple-icons": "^1.2.89",
61
61
  "@iconify-json/tabler": "^1.2.35",
62
+ "@mdx-js/mdx": "^3.1.1",
62
63
  "@shikijs/transformers": "^4.3.1",
63
64
  "@tailwindcss/vite": "^4.3.2",
64
65
  "astro": "7.2.9",
@@ -0,0 +1,82 @@
1
+ // `writedocs convert --mintlify [dir]` - turns a Mintlify project's
2
+ // docs.json into writedocs.json, in the same folder, then checks the pages
3
+ // the same way `writedocs validate` does. The conversion itself is
4
+ // lib/mintlify-convert.js; this file is the CLI around it.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { loadMintlifyConfig, convertMintlifyConfig, formatNotes } from '../lib/mintlify-convert.js';
8
+ import { validateDocsConfig, formatValidationIssuesDetailed } from '../lib/config-schema.js';
9
+ import { checkContent, formatContentIssues } from '../lib/content-check.js';
10
+
11
+ export async function runConvert({ contentDir, force = false, dryRun = false }) {
12
+ const docsJsonPath = path.join(contentDir, 'docs.json');
13
+ if (!fs.existsSync(docsJsonPath)) {
14
+ const legacy = path.join(contentDir, 'mint.json');
15
+ if (fs.existsSync(legacy)) {
16
+ console.error(
17
+ `[writedocs] Found mint.json, Mintlify's older config format. Run \`npx mint upgrade\` in ${contentDir} to turn it into docs.json, then run this again.`
18
+ );
19
+ } else {
20
+ console.error(`[writedocs] No docs.json found in ${contentDir}.`);
21
+ }
22
+ process.exit(1);
23
+ }
24
+ const outPath = path.join(contentDir, 'writedocs.json');
25
+ if (!dryRun && !force && fs.existsSync(outPath)) {
26
+ console.error(`[writedocs] ${outPath} already exists. Pass --force to overwrite it, or --dry-run to only see the result.`);
27
+ process.exit(1);
28
+ }
29
+
30
+ let docs;
31
+ try {
32
+ docs = loadMintlifyConfig(docsJsonPath);
33
+ } catch (err) {
34
+ console.error(`[writedocs] Couldn't read ${docsJsonPath}: ${err.message}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const { config, notes } = convertMintlifyConfig(docs);
39
+ const text = `${JSON.stringify(config, null, 2)}\n`;
40
+
41
+ // The result must pass writedocs' own schema - if it doesn't, that's a
42
+ // converter bug, and writing it would only hand the author a broken file.
43
+ const result = validateDocsConfig(text);
44
+ if (!result.ok) {
45
+ console.error('[writedocs] The converted writedocs.json is not valid - this is a bug in the converter, please report it:\n');
46
+ console.error(formatValidationIssuesDetailed(result.issues));
47
+ console.error(`\n${text}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ if (dryRun) {
52
+ console.log(text);
53
+ } else {
54
+ fs.writeFileSync(outPath, text);
55
+ console.log(`[writedocs] Converted ${path.basename(docsJsonPath)} -> ${outPath}`);
56
+ }
57
+
58
+ if (notes.length) {
59
+ console.log(`\n[writedocs] ${notes.length} thing${notes.length === 1 ? '' : 's'} couldn't be carried over as-is:\n`);
60
+ console.log(formatNotes(notes));
61
+ }
62
+
63
+ // The pages, checked against the converted config - what's left to fix
64
+ // before the first build.
65
+ const content = await checkContent(contentDir, text);
66
+ if (content.errors.length) {
67
+ console.log(`\n[writedocs] ${content.errors.length} error${content.errors.length === 1 ? '' : 's'} in the pages - the build would fail on these:\n`);
68
+ console.log(formatContentIssues(content.errors));
69
+ }
70
+ if (content.warnings.length) {
71
+ console.log(`\n[writedocs] ${content.warnings.length} warning${content.warnings.length === 1 ? '' : 's'} in the pages:\n`);
72
+ console.log(formatContentIssues(content.warnings));
73
+ }
74
+ console.log(
75
+ `\n[writedocs] Checked ${content.pages} page${content.pages === 1 ? '' : 's'}: ${content.errors.length} error${content.errors.length === 1 ? '' : 's'}, ${content.warnings.length} warning${content.warnings.length === 1 ? '' : 's'}.`
76
+ );
77
+ if (!config.domain) {
78
+ console.log(
79
+ '[writedocs] Next: set "domain" in writedocs.json to your site\'s URL - it turns on sitemap.xml and absolute links for social previews. (Mintlify sets this in its dashboard, not docs.json.)'
80
+ );
81
+ }
82
+ }
@@ -3,6 +3,8 @@ import path from 'node:path';
3
3
  import SwaggerParser from '@apidevtools/swagger-parser';
4
4
  import matter from 'gray-matter';
5
5
  import { writedocsTempDir } from '../lib/writedocs-temp-dir.js';
6
+ import { parseOpenApiRef, openApiOperationKey, specDirName } from '../lib/openapi-ref.js';
7
+ import { findAllPages } from '../lib/pages.js';
6
8
 
7
9
  const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
8
10
 
@@ -99,10 +101,17 @@ function scanDirForOverrides(dir, relBase, overrides) {
99
101
  }
100
102
  if (!/\.mdx?$/i.test(entry.name)) continue;
101
103
  const raw = fs.readFileSync(full, 'utf-8');
102
- const { data } = matter(raw);
104
+ let data;
105
+ try {
106
+ ({ data } = matter(raw));
107
+ } catch {
108
+ continue; // invalid frontmatter - `writedocs validate` and the build report it
109
+ }
103
110
  if (typeof data.openapi !== 'string') continue;
104
111
  const fileId = rel.replace(/\.mdx?$/i, '').replace(/\/index$/, '');
105
- overrides.set(data.openapi.trim().replace(/\s+/g, ' '), fileId);
112
+ // Keyed by "METHOD /path" whichever form the page wrote - Mintlify's
113
+ // "spec.json METHOD /path" included (see lib/openapi-ref.js).
114
+ overrides.set(openApiOperationKey(data.openapi) ?? data.openapi.trim().replace(/\s+/g, ' '), fileId);
106
115
  }
107
116
  }
108
117
 
@@ -337,7 +346,6 @@ export async function generateApiPages({ contentDir }) {
337
346
  rmrf(openapiOutDir);
338
347
 
339
348
  const groups = collectOpenApiGroups(config.navigation);
340
- if (groups.length === 0) return;
341
349
 
342
350
  const seenPaths = new Map(); // normalized path -> owning group's label
343
351
  for (const group of groups) {
@@ -356,4 +364,49 @@ export async function generateApiPages({ contentDir }) {
356
364
  for (const group of groups) {
357
365
  await generateApiPagesForGroup({ contentDir, generatedDocsDir, group, overrides });
358
366
  }
367
+
368
+ await parsePageSpecs({ contentDir, openapiOutDir, groupSpecs: groups.map((g) => path.resolve(contentDir, g.openapi.src)) });
369
+ }
370
+
371
+ /** Mintlify's page-level form, `openapi: "/spec.json METHOD /path"`: the
372
+ * page names its spec itself instead of belonging to an openapi group in
373
+ * writedocs.json. Every such spec (that no group already parsed) is parsed
374
+ * here and its operations written under openapi/_pages/<spec>/operations/,
375
+ * where findOperationFile() (lib/openapi-render.ts) looks for them. No
376
+ * pages are generated - the pages that name the spec are the pages. A
377
+ * spec that's missing or doesn't parse is a warning, not a build failure:
378
+ * its pages show the playground's own "no operation found" notice. */
379
+ async function parsePageSpecs({ contentDir, openapiOutDir, groupSpecs }) {
380
+ const specs = new Set();
381
+ for (const rel of findAllPages(contentDir)) {
382
+ let data;
383
+ try {
384
+ ({ data } = matter(fs.readFileSync(path.join(contentDir, rel), 'utf-8')));
385
+ } catch {
386
+ continue;
387
+ }
388
+ const ref = parseOpenApiRef(data?.openapi);
389
+ if (ref?.spec) specs.add(ref.spec);
390
+ }
391
+ for (const spec of specs) {
392
+ const specPath = path.resolve(contentDir, spec);
393
+ if (groupSpecs.includes(specPath)) continue;
394
+ if (!fs.existsSync(specPath)) {
395
+ console.warn(`[writedocs] Pages name the OpenAPI spec "${spec}", which doesn't exist - their API playground shows a notice instead.`);
396
+ continue;
397
+ }
398
+ let operations;
399
+ try {
400
+ operations = buildOperations(await SwaggerParser.dereference(specPath));
401
+ } catch (err) {
402
+ console.warn(`[writedocs] Couldn't parse the OpenAPI spec "${spec}" that pages name: ${err.message}`);
403
+ continue;
404
+ }
405
+ const outDir = path.join(openapiOutDir, '_pages', specDirName(spec), 'operations');
406
+ fs.mkdirSync(outDir, { recursive: true });
407
+ for (const operation of operations) {
408
+ fs.writeFileSync(path.join(outDir, operationFileName(operation.method, operation.path)), JSON.stringify(operation, null, 2));
409
+ }
410
+ console.log(`[writedocs] Parsed OpenAPI spec "${spec}" for the pages that name it (${operations.length} operations)`);
411
+ }
359
412
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { extraClasses } from './class-names';
2
3
  import AppIcon from "./AppIcon.astro";
3
4
 
4
5
  interface Props {
@@ -32,7 +33,7 @@ function slugify(value: string): string {
32
33
  const titleId = _titleId ?? slugify(title);
33
34
  ---
34
35
 
35
- <details class="wd-accordion" open={defaultOpen}>
36
+ <details class:list={["wd-accordion", extraClasses(Astro.props)]} open={defaultOpen}>
36
37
  <summary>
37
38
  <span class="wd-accordion-heading">
38
39
  {icon && <AppIcon icon={icon} class="wd-accordion-icon" />}
@@ -1,4 +1,7 @@
1
- <div class="wd-accordion-group">
1
+ ---
2
+ import { extraClasses } from './class-names';
3
+ ---
4
+ <div class:list={["wd-accordion-group", extraClasses(Astro.props)]}>
2
5
  <slot />
3
6
  </div>
4
7
  <style is:global>
@@ -12,6 +12,7 @@ import {
12
12
  findOperationFile,
13
13
  type OpenApiOperation,
14
14
  } from '../lib/openapi-render';
15
+ import { parseOpenApiRef } from '../lib/openapi-ref.js';
15
16
 
16
17
  interface Props {
17
18
  operation: string; // "METHOD /path", matching a page's `openapi` frontmatter
@@ -19,8 +20,11 @@ interface Props {
19
20
  }
20
21
  const { operation, contentDir } = Astro.props as Props;
21
22
 
22
- const [method = '', urlPath = ''] = operation.trim().split(/\s+/, 2);
23
- const opFile = findOperationFile(contentDir, method, urlPath);
23
+ // "METHOD /path", or Mintlify's "spec.json METHOD /path" - see lib/openapi-ref.js.
24
+ const ref = parseOpenApiRef(operation);
25
+ const method = ref?.method ?? '';
26
+ const urlPath = ref?.path ?? '';
27
+ const opFile = ref ? findOperationFile(contentDir, method, urlPath, ref.spec) : null;
24
28
  const op: OpenApiOperation | null = opFile ? JSON.parse(fs.readFileSync(opFile, 'utf-8')) : null;
25
29
 
26
30
  const pathParams = op?.parameters.filter((p) => p.in === 'path') ?? [];