@writedocs/generator 0.4.8 → 0.4.9
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 +6 -0
- package/bin/writedocs.js +167 -139
- package/package.json +2 -1
- package/src/components/AppIcon.astro +14 -2
- package/src/content.config.ts +6 -127
- 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 +232 -0
- package/src/lib/icons.js +109 -0
- package/src/lib/mdx-inject-builtins.js +1 -1
- package/src/lib/mdx-unknown-components.js +96 -0
- package/src/lib/pages.js +78 -0
package/astro.config.mjs
CHANGED
|
@@ -27,6 +27,7 @@ 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
29
|
import { remarkMintlifyTreeLists, remarkMintlifyPromptText } from './src/lib/mdx-mintlify.js';
|
|
30
|
+
import { remarkUnknownComponentFallback } from './src/lib/mdx-unknown-components.js';
|
|
30
31
|
import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
|
|
31
32
|
import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
|
|
32
33
|
import {
|
|
@@ -370,6 +371,11 @@ export default defineConfig({
|
|
|
370
371
|
// the tree when it looks for components to import.
|
|
371
372
|
remarkMintlifyTreeLists,
|
|
372
373
|
remarkMintlifyPromptText,
|
|
374
|
+
// An unknown component becomes a fragment (its children still
|
|
375
|
+
// render) with a warning, instead of failing the whole build - see
|
|
376
|
+
// lib/mdx-unknown-components.js. After remarkMintlifyTreeLists, so
|
|
377
|
+
// the <Tree.Folder>/<Tree.File> elements it adds count as known.
|
|
378
|
+
remarkUnknownComponentFallback,
|
|
373
379
|
remarkInjectBuiltinComponents,
|
|
374
380
|
// Runs after remarkInjectBuiltinComponents (order doesn't actually
|
|
375
381
|
// matter between them - that plugin only ever prepends an import
|
package/bin/writedocs.js
CHANGED
|
@@ -1,139 +1,167 @@
|
|
|
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('init')
|
|
158
|
+
.description('Scaffold a writedocs.json and starter docs/ folder')
|
|
159
|
+
.argument('[dir]', 'directory to scaffold into', '.')
|
|
160
|
+
.action(async (dir) => {
|
|
161
|
+
await runInit({ targetDir: path.resolve(process.cwd(), dir) });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
165
|
+
console.error(`[writedocs] ${err.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writedocs/generator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
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",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// and Card.astro (the two places writedocs.json icon strings get rendered)
|
|
8
8
|
// don't each need their own copy of the resolveIcon() call + branch.
|
|
9
9
|
import { Icon } from 'astro-icon/components';
|
|
10
|
-
import { resolveIcon } from '../lib/config';
|
|
10
|
+
import { resolveIcon, iconExists } from '../lib/config';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
icon?: string;
|
|
@@ -21,7 +21,19 @@ interface Props {
|
|
|
21
21
|
style?: string;
|
|
22
22
|
}
|
|
23
23
|
const { icon, class: className = 'wd-icon', style } = Astro.props as Props;
|
|
24
|
-
|
|
24
|
+
// An icon name no installed collection has renders nothing, with a warning
|
|
25
|
+
// (once per name - a sidebar icon renders on every page), instead of
|
|
26
|
+
// failing the whole build the way astro-icon does on a missing name.
|
|
27
|
+
// `writedocs validate` lists the same names ahead of time.
|
|
28
|
+
let resolved = icon ? resolveIcon(icon) : null;
|
|
29
|
+
if (icon && resolved?.kind === 'iconify' && !iconExists(icon)) {
|
|
30
|
+
const warned: Set<string> = ((globalThis as any).__wdWarnedIcons ??= new Set());
|
|
31
|
+
if (!warned.has(icon)) {
|
|
32
|
+
warned.add(icon);
|
|
33
|
+
console.warn(`[writedocs] unknown icon "${icon}" - no installed icon set has it, so it's left out.`);
|
|
34
|
+
}
|
|
35
|
+
resolved = null;
|
|
36
|
+
}
|
|
25
37
|
---
|
|
26
38
|
{resolved && (
|
|
27
39
|
resolved.kind === 'iconify' ? (
|
package/src/content.config.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { defineCollection } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
3
|
import type { Loader } from 'astro/loaders';
|
|
4
|
-
import { z } from 'astro/zod';
|
|
5
4
|
import fs from 'node:fs';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
-
import {
|
|
7
|
+
import { pageFrontmatterSchema, findAllPages } from './lib/config';
|
|
9
8
|
import { writedocsTempDir } from './lib/writedocs-temp-dir.js';
|
|
10
9
|
|
|
11
10
|
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
@@ -32,131 +31,11 @@ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
|
32
31
|
const generatedDocsDir = path.join(writedocsTempDir(contentDir), 'generated-docs');
|
|
33
32
|
const generatedDocsBase = pathToFileURL(generatedDocsDir + path.sep);
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Overrides the URL this page is served at, independent of where the
|
|
41
|
-
// file actually lives - writedocs.json's `pages` arrays always keep
|
|
42
|
-
// referencing the file's own path regardless. This is Astro's own
|
|
43
|
-
// glob()-loader convention (a `slug` frontmatter field becomes
|
|
44
|
-
// `entry.id` verbatim - see generateIdDefault in
|
|
45
|
-
// astro/dist/content/loaders/glob.js), not writedocs-specific
|
|
46
|
-
// behavior; declaring it here just brings it into the schema (and
|
|
47
|
-
// its docs) rather than leaving it an undocumented Astro feature.
|
|
48
|
-
// See fileIdForEntry() in lib/config.ts for how routes/links still
|
|
49
|
-
// resolve a page by its file id once this diverges from `entry.id`.
|
|
50
|
-
// Leading/trailing slashes are fine either way ("/", "/guides/x",
|
|
51
|
-
// "guides/x" and "guides/x/" all mean the same thing) - see
|
|
52
|
-
// normalizeEntryId() in lib/config.ts, which is what actually
|
|
53
|
-
// strips them before this value is ever used as a route or href.
|
|
54
|
-
slug: z.string().optional(),
|
|
55
|
-
// Marks this page as an OpenAPI operation reference: "METHOD /path"
|
|
56
|
-
// matching an operation in the owning group's OpenAPI spec, e.g.
|
|
57
|
-
// "GET /pets/{petId}" - [...slug].astro renders <ApiPlayground /> for
|
|
58
|
-
// any page with this field set, below the page's own MDX body (if
|
|
59
|
-
// any). Sites don't write this by hand for most pages; it's either
|
|
60
|
-
// set on a generated stub in the `generatedDocs` collection below (see
|
|
61
|
-
// generate-api-pages.js) or hand-authored to "eject" one specific
|
|
62
|
-
// operation into a real file (in `docs`) with custom prose - either
|
|
63
|
-
// way, the value is always exactly the same "METHOD /path" key
|
|
64
|
-
// generate-api-pages.js uses to look up the operation's full resolved
|
|
65
|
-
// schema/examples at render time.
|
|
66
|
-
openapi: z.string().optional(),
|
|
67
|
-
// Controls how much of the site's own chrome (topbar, sidebar, table of
|
|
68
|
-
// contents) wraps this page - see BaseLayout.astro/[...slug].astro for
|
|
69
|
-
// what each value actually removes:
|
|
70
|
-
// default - the normal three-column reading layout (all chrome).
|
|
71
|
-
// wide - drops the table of contents; the article itself also
|
|
72
|
-
// renders wider, for content that wants the extra room
|
|
73
|
-
// (wide tables, side-by-side images).
|
|
74
|
-
// frame - drops the sidebar and table of contents, but keeps the
|
|
75
|
-
// topbar and the article's own normal presentation
|
|
76
|
-
// ("frame" as in: still inside the site's outer frame).
|
|
77
|
-
// custom - drops the sidebar and table of contents, the
|
|
78
|
-
// auto-rendered <h1>, and prev/next nav, and skips the
|
|
79
|
-
// article's own prose width/padding too - a blank canvas
|
|
80
|
-
// for a hand-built landing/home page made entirely of
|
|
81
|
-
// components - but keeps the topbar, so the page still
|
|
82
|
-
// has site branding/nav/search/theme-toggle available.
|
|
83
|
-
// blank - everything 'custom' drops, plus the topbar too: no site
|
|
84
|
-
// chrome at all, just <slot />. For a page that wants to
|
|
85
|
-
// look nothing like the rest of the site (an auth screen,
|
|
86
|
-
// a print-style page).
|
|
87
|
-
//
|
|
88
|
-
// Two Mintlify values are accepted so a migrated page doesn't fail the
|
|
89
|
-
// build (see MINTLIFY_MODE_ALIASES below): `center` is the same layout
|
|
90
|
-
// as our `frame`, and `assistant` (a full-page Mintlify AI chat, which
|
|
91
|
-
// writedocs doesn't have) falls back to `default`. Mintlify's own
|
|
92
|
-
// `frame` means something else (a canvas that keeps the sidebar) - it's
|
|
93
|
-
// left as our `frame`; see docs/dev/docs/mintlify-compat.mdx.
|
|
94
|
-
mode: z
|
|
95
|
-
.preprocess(
|
|
96
|
-
(value) => (typeof value === 'string' && value in MINTLIFY_MODE_ALIASES ? MINTLIFY_MODE_ALIASES[value] : value),
|
|
97
|
-
z.enum(['default', 'wide', 'frame', 'custom', 'blank']),
|
|
98
|
-
)
|
|
99
|
-
.default('default'),
|
|
100
|
-
// Per-page meta tag overrides - same shape as writedocs.json's top-level
|
|
101
|
-
// `seo` (see seoFieldsSchema in lib/config.ts, the single source of
|
|
102
|
-
// truth for this shape). A page only needs to set the specific fields
|
|
103
|
-
// it wants to override; mergeSeo() falls back to the site-wide default
|
|
104
|
-
// for anything left unset. See BaseLayout.astro for where this and the
|
|
105
|
-
// site-wide seo actually get merged and rendered.
|
|
106
|
-
seo: seoFieldsSchema.optional(),
|
|
107
|
-
// Mintlify's spelling of `seo.noindex` - a top-level `noindex: true`.
|
|
108
|
-
// Folded into `seo` by the transform below, so everything downstream
|
|
109
|
-
// (the robots meta tag, sitemap.xml, llms.txt) keeps reading
|
|
110
|
-
// `seo.noindex` only.
|
|
111
|
-
noindex: z.boolean().optional(),
|
|
112
|
-
// Mintlify's short navigation label - used for the sidebar and the
|
|
113
|
-
// topbar dropdown menus in place of `title` ([...slug].astro's
|
|
114
|
-
// navTitleForSlug). The page's own <h1> and prev/next links keep `title`.
|
|
115
|
-
sidebarTitle: z.string().optional(),
|
|
116
|
-
// Mintlify's spellings of `seo.keywords`, `seo.ogImage`, `seo.ogType`
|
|
117
|
-
// and `seo.twitterCard`, folded into `seo` below like `noindex`.
|
|
118
|
-
keywords: z.array(z.string()).optional(),
|
|
119
|
-
'og:image': z.string().optional(),
|
|
120
|
-
'og:type': z.string().optional(),
|
|
121
|
-
'twitter:card': z.enum(['summary', 'summary_large_image']).optional(),
|
|
122
|
-
// Mintlify's sidebar/page-chrome fields - see NavTree.astro and
|
|
123
|
-
// [...slug].astro for where each is used:
|
|
124
|
-
// icon - shown before the page's label in the sidebar.
|
|
125
|
-
// tag - a short label after it (e.g. "NEW").
|
|
126
|
-
// deprecated - a "Deprecated" label in the sidebar and next
|
|
127
|
-
// to the page's <h1>.
|
|
128
|
-
// hidden - left out of the sidebar, dropdowns and
|
|
129
|
-
// prev/next, but still built and reachable by
|
|
130
|
-
// URL. Also noindexed, as on Mintlify.
|
|
131
|
-
// url - an external link: the page's sidebar entry
|
|
132
|
-
// links straight to it, and the page's own URL
|
|
133
|
-
// redirects there.
|
|
134
|
-
// hideFooterPagination - no prev/next links on this page.
|
|
135
|
-
// hideApiMarker - no HTTP method badge on this page's sidebar
|
|
136
|
-
// entry.
|
|
137
|
-
icon: z.string().optional(),
|
|
138
|
-
tag: z.string().optional(),
|
|
139
|
-
deprecated: z.boolean().optional(),
|
|
140
|
-
hidden: z.boolean().optional(),
|
|
141
|
-
url: z.string().optional(),
|
|
142
|
-
hideFooterPagination: z.boolean().optional(),
|
|
143
|
-
hideApiMarker: z.boolean().optional(),
|
|
144
|
-
}).transform(
|
|
145
|
-
({ noindex, keywords, 'og:image': ogImage, 'og:type': ogType, 'twitter:card': twitterCard, ...data }) => {
|
|
146
|
-
// Top-level Mintlify keys fill in `seo` only where the page's own `seo`
|
|
147
|
-
// doesn't already set that field - an explicit `seo` value always wins.
|
|
148
|
-
// A `hidden` page is noindexed unless it says otherwise.
|
|
149
|
-
const fromMintlify = { noindex: noindex ?? (data.hidden ? true : undefined), keywords, ogImage, ogType, twitterCard };
|
|
150
|
-
const seo = { ...data.seo };
|
|
151
|
-
let changed = false;
|
|
152
|
-
for (const [key, value] of Object.entries(fromMintlify)) {
|
|
153
|
-
if (value === undefined || seo[key as keyof typeof seo] !== undefined) continue;
|
|
154
|
-
(seo as Record<string, unknown>)[key] = value;
|
|
155
|
-
changed = true;
|
|
156
|
-
}
|
|
157
|
-
return changed ? { ...data, seo } : data;
|
|
158
|
-
},
|
|
159
|
-
);
|
|
34
|
+
// Page frontmatter - defined in lib/config-schema.ts as
|
|
35
|
+
// pageFrontmatterSchema, next to writedocs.json's own schema, so
|
|
36
|
+
// `writedocs validate` checks every page against exactly the schema the
|
|
37
|
+
// build uses (that file compiles to the plain JS validate loads).
|
|
38
|
+
const docsSchema = pageFrontmatterSchema;
|
|
160
39
|
|
|
161
40
|
/** Wraps another loader so it's skipped entirely - no filesystem scan, no
|
|
162
41
|
* "directory doesn't exist"/"no files found" warning - when its own base
|