@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.
- package/astro.config.mjs +45 -2
- package/bin/writedocs.js +190 -139
- package/package.json +2 -1
- package/src/cli/convert.js +82 -0
- package/src/cli/generate-api-pages.js +56 -3
- package/src/components/Accordion.astro +2 -1
- package/src/components/AccordionGroup.astro +4 -1
- package/src/components/ApiPlayground.astro +6 -2
- package/src/components/ApiReferencePanel.astro +6 -2
- package/src/components/AppIcon.astro +14 -2
- package/src/components/Badge.astro +2 -0
- package/src/components/Callout.astro +2 -1
- package/src/components/Card.astro +2 -1
- package/src/components/CardGroup.astro +2 -1
- package/src/components/Check.astro +1 -1
- package/src/components/CodeBlock.astro +94 -0
- package/src/components/CodeGroup.astro +2 -1
- package/src/components/Color.astro +2 -1
- package/src/components/ColorItem.astro +2 -1
- package/src/components/ColorRow.astro +2 -1
- package/src/components/Column.astro +19 -0
- package/src/components/Columns.astro +1 -1
- package/src/components/Danger.astro +1 -1
- package/src/components/Expandable.astro +2 -1
- package/src/components/Frame.astro +2 -1
- package/src/components/GitHubRepo.astro +2 -1
- package/src/components/Hint.astro +2 -1
- package/src/components/Icon.astro +3 -2
- package/src/components/Image.astro +2 -1
- package/src/components/Info.astro +1 -1
- package/src/components/Note.astro +1 -1
- package/src/components/Panel.astro +2 -1
- package/src/components/Parameter.astro +2 -1
- package/src/components/Prompt.astro +2 -1
- package/src/components/RequestExample.astro +2 -1
- package/src/components/ResponseExample.astro +2 -1
- package/src/components/Searchbar.astro +2 -1
- package/src/components/Step.astro +2 -1
- package/src/components/Steps.astro +4 -1
- package/src/components/Tab.astro +2 -1
- package/src/components/Tabs.astro +2 -1
- package/src/components/Tile.astro +2 -1
- package/src/components/Tip.astro +1 -1
- package/src/components/TreeFile.astro +2 -1
- package/src/components/TreeFolder.astro +2 -1
- package/src/components/Update.astro +2 -1
- package/src/components/Video.astro +2 -1
- package/src/components/View.astro +2 -1
- package/src/components/Warning.astro +1 -1
- package/src/components/class-names.ts +8 -0
- package/src/components/index.ts +2 -0
- package/src/content.config.ts +29 -129
- package/src/lib/config-schema.js +124 -0
- package/src/lib/config-schema.ts +1574 -1437
- package/src/lib/config.ts +7 -140
- package/src/lib/content-check.js +262 -0
- package/src/lib/icons.js +109 -0
- package/src/lib/mdx-auto-hydrate.js +12 -0
- package/src/lib/mdx-inject-builtins.js +16 -1
- package/src/lib/mdx-inline-react.js +202 -0
- package/src/lib/mdx-mintlify.js +65 -0
- package/src/lib/mdx-substitute-variables.js +17 -0
- package/src/lib/mdx-unknown-components.js +149 -0
- package/src/lib/mintlify-convert.js +599 -0
- package/src/lib/openapi-ref.js +44 -0
- package/src/lib/openapi-render.ts +10 -1
- package/src/lib/pages.js +150 -0
- 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 {
|
|
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
|
-
|
|
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
|
|
87
|
-
.argument('[dir]', 'content directory (contains writedocs.json)', '.')
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
console.error(
|
|
113
|
-
console.error(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.error(
|
|
119
|
-
console.error(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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" />}
|
|
@@ -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
|
-
|
|
23
|
-
const
|
|
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') ?? [];
|