@writedocs/generator 0.7.1 → 0.7.3
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 +10 -4
- package/bin/writedocs.js +54 -16
- package/package.json +1 -1
- package/src/cli/build-auth.js +16 -9
- package/src/cli/build.js +3 -1
- package/src/cli/convert.js +6 -2
- package/src/cli/dev.js +7 -1
- package/src/cli/generate-api-pages.js +2 -2
- package/src/cli/init.js +3 -1
- package/src/cli/output.js +8 -1
- package/src/cli/preflight.js +63 -1
- package/src/cli/update-check-refresh.js +18 -0
- package/src/cli/update-check.js +104 -0
- package/src/cli/update.js +112 -0
- package/src/cli/write-redirects-file.js +2 -1
- package/src/components/ApiReferencePanel.astro +46 -12
- package/src/components/Steps.astro +2 -2
- package/src/layout/BaseLayout.astro +21 -6
- package/src/layout/styles/banner.css +1 -1
- package/src/lib/a11y-check.js +19 -42
- package/src/lib/asset-path.js +29 -0
- package/src/lib/color.js +49 -0
- package/src/lib/config-file.js +25 -0
- package/src/lib/config-schema.js +15 -12
- package/src/lib/config-schema.ts +20 -12
- package/src/lib/config.ts +18 -135
- package/src/lib/json-schema-descriptions.js +1 -1
- package/src/lib/mintlify-convert.js +2 -1
- package/src/lib/pages.js +121 -1
- package/src/lib/styles-asset-integration.js +1 -18
- package/src/lib/writedocs-legacy-convert.js +2 -1
- package/src/pages/[...slug].astro +11 -1
- package/src/pages/llms.txt.ts +5 -1
- package/src/scripts/search.ts +39 -14
- package/writedocs.schema.json +2 -2
package/astro.config.mjs
CHANGED
|
@@ -143,26 +143,33 @@ function walkMdFiles(baseDir) {
|
|
|
143
143
|
function collectNoindexIds(rootContentDir) {
|
|
144
144
|
const generatedDocsDir = path.join(writedocsTempDir(rootContentDir), 'generated-docs');
|
|
145
145
|
const ids = new Set();
|
|
146
|
+
// Whether some page is served at "/" itself. When none is, "/" is the
|
|
147
|
+
// redirect-only route [...slug].astro synthesizes (noindex, pointing at
|
|
148
|
+
// the first page) - which doesn't belong in the sitemap either.
|
|
149
|
+
let rootIsPage = false;
|
|
146
150
|
|
|
147
151
|
for (const relativeId of findAllPages(rootContentDir)) {
|
|
148
152
|
const file = path.join(rootContentDir, relativeId);
|
|
149
153
|
const { data } = matter(fs.readFileSync(file, 'utf-8'));
|
|
154
|
+
const pageId = normalizeEntryId(data?.slug ?? relativeId.replace(/\.mdx?$/i, '').replace(/\/index$/, ''));
|
|
155
|
+
if (pageId === 'index') rootIsPage = true;
|
|
150
156
|
// Top-level `noindex` is Mintlify's spelling, and a Mintlify `hidden`
|
|
151
157
|
// page is noindexed too - content.config.ts folds both into
|
|
152
158
|
// `seo.noindex` the same way (an explicit value wins).
|
|
153
159
|
// A frontmatter `url` page's route only redirects to that link, so it
|
|
154
160
|
// doesn't belong in the sitemap either.
|
|
155
161
|
if (!(data?.seo?.noindex ?? data?.noindex ?? data?.hidden === true) && !data?.url) continue;
|
|
156
|
-
|
|
157
|
-
ids.add(normalizeEntryId(data.slug ?? fallbackId));
|
|
162
|
+
ids.add(pageId);
|
|
158
163
|
}
|
|
159
164
|
// Generated OpenAPI stub pages always set an explicit `slug` (see
|
|
160
165
|
// generate-api-pages.js) - there's no file-path-derived id to fall back
|
|
161
166
|
// to the way there is for a hand-written page.
|
|
162
167
|
for (const file of walkMdFiles(generatedDocsDir)) {
|
|
163
168
|
const { data } = matter(fs.readFileSync(file, 'utf-8'));
|
|
169
|
+
if (data?.slug && normalizeEntryId(data.slug) === 'index') rootIsPage = true;
|
|
164
170
|
if ((data?.seo?.noindex ?? data?.noindex ?? data?.hidden === true) && data.slug) ids.add(normalizeEntryId(data.slug));
|
|
165
171
|
}
|
|
172
|
+
if (!rootIsPage) ids.add('index');
|
|
166
173
|
return ids;
|
|
167
174
|
}
|
|
168
175
|
|
|
@@ -513,8 +520,7 @@ export default defineConfig({
|
|
|
513
520
|
// from such a file never finds a package.json named "writedocs",
|
|
514
521
|
// so self-reference fails and Vite reports the bare specifier as
|
|
515
522
|
// unresolvable ("Rolldown failed to resolve import
|
|
516
|
-
// 'writedocs/components'").
|
|
517
|
-
// fix comes from.
|
|
523
|
+
// 'writedocs/components'").
|
|
518
524
|
// This alias bypasses self-reference resolution entirely for
|
|
519
525
|
// that one specifier - Vite's alias matching is a plain string
|
|
520
526
|
// match against the specifier itself, independent of which file
|
package/bin/writedocs.js
CHANGED
|
@@ -9,6 +9,8 @@ import { runBuild } from '../src/cli/build.js';
|
|
|
9
9
|
import { runInit } from '../src/cli/init.js';
|
|
10
10
|
import { requireBuildKey } from '../src/cli/build-auth.js';
|
|
11
11
|
import { log, step, plural, color, CliExit, errorText, stopActiveStep } from '../src/cli/output.js';
|
|
12
|
+
import { startUpdateCheck, showUpdateNotice } from '../src/cli/update-check.js';
|
|
13
|
+
import { readConfigText } from '../src/lib/config-file.js';
|
|
12
14
|
// O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
|
|
13
15
|
// que a plataforma importa por `@writedocs/generator/config-schema` - e o que
|
|
14
16
|
// faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
|
|
@@ -37,13 +39,21 @@ const packageRoot = path.resolve(__dirname, '..');
|
|
|
37
39
|
// package.json). `writedocs --version` should always reflect what actually
|
|
38
40
|
// got published, not whatever this string happened to say at the time this
|
|
39
41
|
// line was last hand-edited.
|
|
40
|
-
const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
42
|
+
const { name: packageName, version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
41
43
|
|
|
42
44
|
const program = new Command();
|
|
43
45
|
program
|
|
44
46
|
.name('writedocs')
|
|
45
47
|
.description('Static site generator for writedocs.json + MDX')
|
|
46
|
-
.version(version)
|
|
48
|
+
.version(version)
|
|
49
|
+
// The built-in `help <command>` prints any command's usage, hidden ones
|
|
50
|
+
// included - replaced by the `help` command at the bottom of this file.
|
|
51
|
+
.helpCommand(false)
|
|
52
|
+
// Every command checks for a newer writedocs (from a cache - see
|
|
53
|
+
// src/cli/update-check.js); the notice prints after the command's output.
|
|
54
|
+
.hook('preAction', (_program, command) => {
|
|
55
|
+
startUpdateCheck({ command: command.name(), name: packageName, version, packageRoot });
|
|
56
|
+
});
|
|
47
57
|
|
|
48
58
|
program
|
|
49
59
|
.command('dev')
|
|
@@ -66,6 +76,9 @@ program
|
|
|
66
76
|
// src/cli/build-auth.js for the second layer: even someone who knows the
|
|
67
77
|
// command exists still can't run it without the right key.
|
|
68
78
|
.command('build', { hidden: true })
|
|
79
|
+
// `build --help` prints the general help, which doesn't list it: hidden
|
|
80
|
+
// means not described anywhere (see the `help` command at the bottom too).
|
|
81
|
+
.configureHelp({ formatHelp: () => program.helpInformation() })
|
|
69
82
|
.description('Build a static site into <dir>/dist')
|
|
70
83
|
.argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
|
|
71
84
|
.option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
|
|
@@ -73,8 +86,8 @@ program
|
|
|
73
86
|
.action(async (dir, opts) => {
|
|
74
87
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
75
88
|
// Project-scoped, not global: a .env sitting next to this project's own
|
|
76
|
-
// writedocs.json (
|
|
77
|
-
//
|
|
89
|
+
// writedocs.json (WRITEDOCS_API_KEY) is picked up automatically, so
|
|
90
|
+
// build doesn't need it exported by hand every
|
|
78
91
|
// session. dotenv never overwrites a var already set in the real
|
|
79
92
|
// environment - an explicit `export`/CI secret still wins over the file.
|
|
80
93
|
dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
|
|
@@ -100,7 +113,7 @@ program
|
|
|
100
113
|
throw new CliExit(1);
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
const rawText =
|
|
116
|
+
const rawText = readConfigText(contentDir);
|
|
104
117
|
const result = validateDocsConfig(rawText);
|
|
105
118
|
// Chave desconhecida na raiz e AVISO, nunca erro: a raiz nao e `.strict()`
|
|
106
119
|
// de proposito (tornar strict quebraria configs existentes), entao o Zod
|
|
@@ -180,7 +193,7 @@ program
|
|
|
180
193
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
181
194
|
const { preflightCheck } = await import('../src/cli/preflight.js');
|
|
182
195
|
preflightCheck(contentDir);
|
|
183
|
-
const configText =
|
|
196
|
+
const configText = readConfigText(contentDir);
|
|
184
197
|
const checking = step('Checking links');
|
|
185
198
|
// Generated OpenAPI pages are link targets too - same step dev/build run.
|
|
186
199
|
const { generateApiPages } = await import('../src/cli/generate-api-pages.js');
|
|
@@ -215,7 +228,7 @@ program
|
|
|
215
228
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
216
229
|
const { preflightCheck } = await import('../src/cli/preflight.js');
|
|
217
230
|
preflightCheck(contentDir);
|
|
218
|
-
const configText =
|
|
231
|
+
const configText = readConfigText(contentDir);
|
|
219
232
|
const checking = step('Checking accessibility');
|
|
220
233
|
const { checkAccessibility } = await import('../src/lib/a11y-check.js');
|
|
221
234
|
const { formatContentIssues } = await import('../src/lib/content-check.js');
|
|
@@ -246,8 +259,10 @@ program
|
|
|
246
259
|
.option('--force', 'overwrite an existing writedocs.json')
|
|
247
260
|
.option('--dry-run', 'print the converted writedocs.json instead of writing it')
|
|
248
261
|
.action(async (dir, options) => {
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
// Commander camel-cases only on hyphens: `--docs.json` is stored under
|
|
263
|
+
// the key "docs.json", not `docsJson`.
|
|
264
|
+
const mintlify = options.mintlify || options['docs.json'];
|
|
265
|
+
const legacy = options.writedocs || options['config.json'];
|
|
251
266
|
if (mintlify === legacy) {
|
|
252
267
|
log.error(
|
|
253
268
|
mintlify
|
|
@@ -265,6 +280,14 @@ program
|
|
|
265
280
|
});
|
|
266
281
|
});
|
|
267
282
|
|
|
283
|
+
program
|
|
284
|
+
.command('update')
|
|
285
|
+
.description('Update writedocs to the latest version')
|
|
286
|
+
.action(async () => {
|
|
287
|
+
const { runUpdate } = await import('../src/cli/update.js');
|
|
288
|
+
await runUpdate({ name: packageName, version, packageRoot });
|
|
289
|
+
});
|
|
290
|
+
|
|
268
291
|
program
|
|
269
292
|
.command('init')
|
|
270
293
|
.description('Scaffold a writedocs.json and starter docs/ folder')
|
|
@@ -273,10 +296,25 @@ program
|
|
|
273
296
|
await runInit({ targetDir: path.resolve(process.cwd(), dir) });
|
|
274
297
|
});
|
|
275
298
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
// `writedocs help [command]` - commander's own, except that a hidden command
|
|
300
|
+
// (`build`) gets the general help, like a name that isn't a command at all.
|
|
301
|
+
const HIDDEN_COMMANDS = new Set(['build']);
|
|
302
|
+
program
|
|
303
|
+
.command('help')
|
|
304
|
+
.description('display help for command')
|
|
305
|
+
.argument('[command]')
|
|
306
|
+
.action((name) => {
|
|
307
|
+
const command = name && !HIDDEN_COMMANDS.has(name) ? program.commands.find((c) => c.name() === name) : undefined;
|
|
308
|
+
(command ?? program).help();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
program
|
|
312
|
+
.parseAsync(process.argv)
|
|
313
|
+
.then(showUpdateNotice)
|
|
314
|
+
.catch((err) => {
|
|
315
|
+
stopActiveStep();
|
|
316
|
+
// A command that already printed its own error throws CliExit.
|
|
317
|
+
if (!(err instanceof CliExit)) log.error(errorText(err));
|
|
318
|
+
showUpdateNotice();
|
|
319
|
+
process.exit(err instanceof CliExit ? err.code : 1);
|
|
320
|
+
});
|
package/package.json
CHANGED
package/src/cli/build-auth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `writedocs build` produces the artifact that actually gets deployed, so
|
|
3
3
|
* unlike `dev`/`init` it isn't freely runnable by anyone who installs the
|
|
4
|
-
* package - it's gated behind a key that a separate service
|
|
5
|
-
*
|
|
4
|
+
* package - it's gated behind a key that a separate service
|
|
5
|
+
* (writedocs-key-server, a sibling repo) actually decides the validity of.
|
|
6
6
|
*
|
|
7
7
|
* This deliberately isn't a local check. An earlier version compared the
|
|
8
8
|
* supplied key against an env var set on the same machine
|
|
@@ -12,19 +12,26 @@
|
|
|
12
12
|
* actually holds the set of issued keys means a key's validity is decided
|
|
13
13
|
* by whoever runs that server, not by whoever is running `build`.
|
|
14
14
|
*
|
|
15
|
-
* See docs/dev/docs/deploy.mdx for how to deploy key-server
|
|
16
|
-
* keys, and
|
|
15
|
+
* See docs/dev/docs/deploy.mdx for how to deploy writedocs-key-server and
|
|
16
|
+
* issue keys, and that repo's own README for the service's endpoints.
|
|
17
17
|
*/
|
|
18
18
|
import { log } from './output.js';
|
|
19
19
|
|
|
20
20
|
export const EX_TEMPFAIL = 75;
|
|
21
21
|
|
|
22
|
+
/** The authorization server every `build` asks. Fixed in the code, with no
|
|
23
|
+
* environment override: when the URL came from WRITEDOCS_KEY_SERVER_URL,
|
|
24
|
+
* anyone could point it at a 3-line server that answers
|
|
25
|
+
* `{ "valid": true }` and build without a key. Tests and CI use a real
|
|
26
|
+
* key instead (the WRITEDOCS_API_KEY secret - see
|
|
27
|
+
* scripts/smoke-installed-cli.sh). Honest limit: the source ships with the
|
|
28
|
+
* package, so someone willing to edit it can still remove this check - it
|
|
29
|
+
* raises the bar, it isn't a lock. Real protection is what the platform
|
|
30
|
+
* does with the output. */
|
|
31
|
+
export const KEY_SERVER_URL = 'https://proxy.writechoice.io:8787';
|
|
32
|
+
|
|
22
33
|
export async function requireBuildKey(providedKey) {
|
|
23
|
-
const serverUrl =
|
|
24
|
-
if (!serverUrl) {
|
|
25
|
-
log.error('build is not available.');
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
34
|
+
const serverUrl = KEY_SERVER_URL;
|
|
28
35
|
if (!providedKey) {
|
|
29
36
|
log.error('build requires a valid --key (or WRITEDOCS_API_KEY).');
|
|
30
37
|
process.exit(1);
|
package/src/cli/build.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { runAstro } from './run-astro.js';
|
|
4
4
|
import { runPagefind } from './run-pagefind.js';
|
|
5
|
-
import { preflightCheck } from './preflight.js';
|
|
5
|
+
import { preflightCheck, sameDriveCheck, writableInstallCheck } from './preflight.js';
|
|
6
6
|
import { generateApiPages } from './generate-api-pages.js';
|
|
7
7
|
import { writeRedirectsFile } from './write-redirects-file.js';
|
|
8
8
|
import { writedocsBuildStagingDir } from '../lib/writedocs-temp-dir.js';
|
|
@@ -13,6 +13,7 @@ import { reportApiPages } from './api-pages-output.js';
|
|
|
13
13
|
export async function runBuild({ contentDir, packageRoot, verbose = false }) {
|
|
14
14
|
const started = Date.now();
|
|
15
15
|
preflightCheck(contentDir);
|
|
16
|
+
writableInstallCheck(packageRoot);
|
|
16
17
|
|
|
17
18
|
// Collected while building, printed together at the end - a warning in
|
|
18
19
|
// the middle of a spinner is easy to miss.
|
|
@@ -27,6 +28,7 @@ export async function runBuild({ contentDir, packageRoot, verbose = false }) {
|
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const api = await generateApiPages({ contentDir });
|
|
31
|
+
sameDriveCheck(contentDir, packageRoot, { generatedPages: api.groups.length > 0 });
|
|
30
32
|
reportApiPages(api);
|
|
31
33
|
api.warnings.forEach((message) => addWarning(null, message));
|
|
32
34
|
|
package/src/cli/convert.js
CHANGED
|
@@ -10,7 +10,7 @@ import { loadMintlifyConfig, convertMintlifyConfig, formatNotes } from '../lib/m
|
|
|
10
10
|
import { loadLegacyConfig, convertLegacyConfig } from '../lib/writedocs-legacy-convert.js';
|
|
11
11
|
import { validateDocsConfig, formatValidationIssuesDetailed } from '../lib/config-schema.js';
|
|
12
12
|
import { checkContent, formatContentIssues } from '../lib/content-check.js';
|
|
13
|
-
import { log, step, plural, color, displayPath, CliExit } from './output.js';
|
|
13
|
+
import { log, step, plural, color, displayPath, CliExit, logToStderr } from './output.js';
|
|
14
14
|
|
|
15
15
|
const SOURCES = {
|
|
16
16
|
mintlify: {
|
|
@@ -39,6 +39,10 @@ const SOURCES = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export async function runConvert({ source = 'mintlify', contentDir, force = false, dryRun = false }) {
|
|
42
|
+
// --dry-run's stdout is the converted writedocs.json and nothing else, so
|
|
43
|
+
// `writedocs convert --dry-run > writedocs.json` works; the notes and the
|
|
44
|
+
// page check go to stderr.
|
|
45
|
+
if (dryRun) logToStderr();
|
|
42
46
|
const from = SOURCES[source];
|
|
43
47
|
const inputPath = path.join(contentDir, from.file);
|
|
44
48
|
if (!fs.existsSync(inputPath)) {
|
|
@@ -75,7 +79,7 @@ export async function runConvert({ source = 'mintlify', contentDir, force = fals
|
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
if (dryRun) {
|
|
78
|
-
|
|
82
|
+
process.stdout.write(text);
|
|
79
83
|
} else {
|
|
80
84
|
fs.writeFileSync(outPath, text);
|
|
81
85
|
log.success(`Converted ${from.file} to ${color.bold(displayPath(outPath))}`);
|
package/src/cli/dev.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { runAstro } from './run-astro.js';
|
|
2
|
-
import { preflightCheck } from './preflight.js';
|
|
2
|
+
import { preflightCheck, sameDriveCheck, writableInstallCheck } from './preflight.js';
|
|
3
3
|
import { generateApiPages } from './generate-api-pages.js';
|
|
4
4
|
import { log, step, duration, formatProblems, color, CliExit } from './output.js';
|
|
5
5
|
import { describeError, requestLog, authorWarning, verboseLine, stripAnsi } from './astro-output.js';
|
|
6
6
|
import { reportApiPages } from './api-pages-output.js';
|
|
7
7
|
import { runningPreview, writeLock, removeLock } from './dev-lock.js';
|
|
8
|
+
import { showUpdateNotice } from './update-check.js';
|
|
8
9
|
|
|
9
10
|
// The same problem tends to arrive more than once in a row - Vite and
|
|
10
11
|
// Astro each log a failed page, and a page compiles for more than one
|
|
@@ -14,6 +15,7 @@ const REPEAT_WINDOW_MS = 2000;
|
|
|
14
15
|
export async function runDev({ contentDir, packageRoot, port, verbose = false }) {
|
|
15
16
|
const started = Date.now();
|
|
16
17
|
preflightCheck(contentDir);
|
|
18
|
+
writableInstallCheck(packageRoot);
|
|
17
19
|
const other = runningPreview(packageRoot);
|
|
18
20
|
if (other) {
|
|
19
21
|
log.error(`Another writedocs preview is already running${other.url ? ` at ${other.url}` : ''}.`);
|
|
@@ -28,6 +30,7 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false })
|
|
|
28
30
|
process.on('exit', () => removeLock(packageRoot));
|
|
29
31
|
|
|
30
32
|
const api = await generateApiPages({ contentDir });
|
|
33
|
+
sameDriveCheck(contentDir, packageRoot, { generatedPages: api.groups.length > 0 });
|
|
31
34
|
reportApiPages(api);
|
|
32
35
|
api.warnings.forEach((message) => log.warn(message));
|
|
33
36
|
|
|
@@ -96,6 +99,9 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false })
|
|
|
96
99
|
log.line();
|
|
97
100
|
log.line(color.dim(' Edit any page and the preview updates. Press Ctrl+C to stop.'));
|
|
98
101
|
log.line();
|
|
102
|
+
// `dev` runs until Ctrl+C - the notice goes under the ready screen,
|
|
103
|
+
// not after the command like everywhere else.
|
|
104
|
+
showUpdateNotice();
|
|
99
105
|
return;
|
|
100
106
|
}
|
|
101
107
|
case 'fatal':
|
|
@@ -5,6 +5,7 @@ import matter from 'gray-matter';
|
|
|
5
5
|
import { writedocsTempDir } from '../lib/writedocs-temp-dir.js';
|
|
6
6
|
import { parseOpenApiRef, openApiOperationKey, specDirName } from '../lib/openapi-ref.js';
|
|
7
7
|
import { findAllPages } from '../lib/pages.js';
|
|
8
|
+
import { readConfigText } from '../lib/config-file.js';
|
|
8
9
|
import { HTTP_METHODS, collectOpenApiGroups } from '../lib/openapi-spec.js';
|
|
9
10
|
|
|
10
11
|
|
|
@@ -298,13 +299,12 @@ async function generateApiPagesForGroup({ contentDir, generatedDocsDir, group, o
|
|
|
298
299
|
*/
|
|
299
300
|
export async function generateApiPages({ contentDir }) {
|
|
300
301
|
const summary = { groups: [], pageSpecs: [], warnings: [] };
|
|
301
|
-
const writedocsJsonPath = path.join(contentDir, 'writedocs.json');
|
|
302
302
|
const generatedDocsDir = path.join(writedocsTempDir(contentDir), 'generated-docs');
|
|
303
303
|
const openapiOutDir = path.join(writedocsTempDir(contentDir), 'openapi');
|
|
304
304
|
|
|
305
305
|
let config;
|
|
306
306
|
try {
|
|
307
|
-
config = JSON.parse(
|
|
307
|
+
config = JSON.parse(readConfigText(contentDir));
|
|
308
308
|
} catch {
|
|
309
309
|
return summary; // preflightCheck() (run first, see dev.js/build.js) already reports this
|
|
310
310
|
}
|
package/src/cli/init.js
CHANGED
|
@@ -6,7 +6,9 @@ const WRITEDOCS_JSON = {
|
|
|
6
6
|
name: 'My Docs',
|
|
7
7
|
description: 'Documentation site built with Writedocs',
|
|
8
8
|
styles: {
|
|
9
|
-
|
|
9
|
+
// Readable as link text on both default backgrounds - a new project
|
|
10
|
+
// passes `writedocs a11y` (scripts/init.test.js).
|
|
11
|
+
colors: { primary: '#4f46e5', dark: { primary: '#a5b4fc' } },
|
|
10
12
|
},
|
|
11
13
|
navigation: [
|
|
12
14
|
{ group: 'Getting Started', pages: ['index', 'docs/getting-started'] },
|
package/src/cli/output.js
CHANGED
|
@@ -9,13 +9,20 @@
|
|
|
9
9
|
// it finishes. NO_COLOR turns colors off; FORCE_COLOR turns them on.
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
let stream = process.stdout;
|
|
13
13
|
// Output piped into something that stopped reading (`writedocs build | head`):
|
|
14
14
|
// nothing more can be shown, so stop instead of crashing on EPIPE.
|
|
15
15
|
stream.on('error', (err) => {
|
|
16
16
|
if (err.code === 'EPIPE') process.exit(0);
|
|
17
17
|
throw err;
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
/** Sends everything this module prints to stderr from here on - for a
|
|
21
|
+
* command whose stdout is data: `convert --dry-run` prints the converted
|
|
22
|
+
* writedocs.json there, so `> writedocs.json` gets exactly that. */
|
|
23
|
+
export function logToStderr() {
|
|
24
|
+
stream = process.stderr;
|
|
25
|
+
}
|
|
19
26
|
const interactive = Boolean(stream.isTTY) && process.env.TERM !== 'dumb' && !process.env.CI;
|
|
20
27
|
const useColor =
|
|
21
28
|
process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0'
|
package/src/cli/preflight.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { log, color, CliExit } from './output.js';
|
|
5
|
+
import { readConfigText } from '../lib/config-file.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Fast, dependency-light sanity check that runs before handing off to
|
|
@@ -28,7 +30,7 @@ export function preflightCheck(contentDir) {
|
|
|
28
30
|
throw new CliExit(1);
|
|
29
31
|
}
|
|
30
32
|
try {
|
|
31
|
-
JSON.parse(
|
|
33
|
+
JSON.parse(readConfigText(contentDir));
|
|
32
34
|
} catch (err) {
|
|
33
35
|
log.error(`writedocs.json is not valid JSON: ${err.message}`);
|
|
34
36
|
log.detail(color.dim('Run "writedocs validate" to see where.'));
|
|
@@ -41,3 +43,63 @@ export function preflightCheck(contentDir) {
|
|
|
41
43
|
// later, with a more specific error naming the missing page - see
|
|
42
44
|
// getStaticPaths() in src/pages/[...slug].astro.
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
/** The drive (`C:`) or UNC share a Windows path lives on, lowercased. */
|
|
48
|
+
function volumeOf(p) {
|
|
49
|
+
return path.win32.parse(path.win32.resolve(p)).root.replace(/[\\/]+$/, '').toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Windows only, before `dev`/`build` start Astro: the project has to be on
|
|
54
|
+
* the same drive as writedocs itself. Astro is run with writedocs' own
|
|
55
|
+
* folder as its root (see run-astro.js) and records every page's path
|
|
56
|
+
* relative to it - across drives there is no relative path, the
|
|
57
|
+
* `D:/...` it gets instead reads as a URL scheme, and the build dies with
|
|
58
|
+
* "The URL must be of scheme file". The same holds for the temp folder
|
|
59
|
+
* when it holds generated OpenAPI pages (`generatedPages` - writedocsTempDir()
|
|
60
|
+
* is Astro content then; the cache and inline React components there build
|
|
61
|
+
* fine across drives). This stops with what to do instead of that message.
|
|
62
|
+
* `platform` and `tempDir` are parameters for tests.
|
|
63
|
+
*/
|
|
64
|
+
export function sameDriveCheck(contentDir, packageRoot, { generatedPages = false, platform = process.platform, tempDir = os.tmpdir() } = {}) {
|
|
65
|
+
if (platform !== 'win32') return;
|
|
66
|
+
const install = volumeOf(packageRoot);
|
|
67
|
+
const project = volumeOf(contentDir);
|
|
68
|
+
if (project !== install) {
|
|
69
|
+
log.error(`This project is on ${project.toUpperCase()}, and writedocs is installed on ${install.toUpperCase()} - Astro can't build across drives on Windows.`);
|
|
70
|
+
log.detail(color.dim('Install writedocs in the project instead (npm install --save-dev @writedocs/generator, then npx writedocs ...),'));
|
|
71
|
+
log.detail(color.dim(`or move the project to drive ${install.toUpperCase()}.`));
|
|
72
|
+
throw new CliExit(1);
|
|
73
|
+
}
|
|
74
|
+
const temp = volumeOf(tempDir);
|
|
75
|
+
if (generatedPages && temp !== install) {
|
|
76
|
+
log.error(`The temp folder (${tempDir}) is on ${temp.toUpperCase()}, and writedocs is installed on ${install.toUpperCase()} - Astro can't build the generated API pages across drives on Windows.`);
|
|
77
|
+
log.detail(color.dim(`Point TEMP and TMP at a folder on drive ${install.toUpperCase()}, or install writedocs on ${temp.toUpperCase()}.`));
|
|
78
|
+
throw new CliExit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Before `dev`/`build` start Astro: writedocs has to be able to write inside
|
|
84
|
+
* its own folder - Astro runs with it as its root (run-astro.js), and keeps
|
|
85
|
+
* `.astro/`, Vite's `node_modules/.vite/` and the build's staging folder
|
|
86
|
+
* (writedocsBuildStagingDir()) there. A global install made with sudo is
|
|
87
|
+
* owned by root, and both commands then die on a bare EACCES/EPERM from
|
|
88
|
+
* deep inside Astro or Vite. A real write, not fs.accessSync(): on Windows
|
|
89
|
+
* that only looks at the read-only attribute, not at permissions.
|
|
90
|
+
*/
|
|
91
|
+
export function writableInstallCheck(packageRoot) {
|
|
92
|
+
const probe = path.join(packageRoot, '.astro', `.write-check-${process.pid}`);
|
|
93
|
+
try {
|
|
94
|
+
fs.mkdirSync(path.dirname(probe), { recursive: true });
|
|
95
|
+
fs.writeFileSync(probe, '');
|
|
96
|
+
fs.rmSync(probe, { force: true });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (err.code !== 'EACCES' && err.code !== 'EPERM') throw err;
|
|
99
|
+
log.error(`writedocs can't write to its own folder (${packageRoot}), and dev and build need to.`);
|
|
100
|
+
log.detail(color.dim('This happens with a global install made with sudo. Install writedocs in the project instead'));
|
|
101
|
+
log.detail(color.dim('(npm install --save-dev @writedocs/generator, then npx writedocs ...), or set npm up to install'));
|
|
102
|
+
log.detail(color.dim('globally without sudo: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally'));
|
|
103
|
+
throw new CliExit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Detached background process started by update-check.js: asks the npm
|
|
2
|
+
// registry for the latest version of package `process.argv[2]` and caches
|
|
3
|
+
// the answer for the next writedocs run. Prints nothing. A failure (offline,
|
|
4
|
+
// registry down) keeps the last known version but still records the
|
|
5
|
+
// attempt, so it's retried the next day, not on every command.
|
|
6
|
+
import { fetchLatestVersion, readCache, writeCache } from './update-check.js';
|
|
7
|
+
|
|
8
|
+
const name = process.argv[2];
|
|
9
|
+
let latest = null;
|
|
10
|
+
try {
|
|
11
|
+
latest = await fetchLatestVersion(name);
|
|
12
|
+
} catch {
|
|
13
|
+
const previous = readCache();
|
|
14
|
+
latest = previous?.name === name ? previous.latest : null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
writeCache({ name, latest, checkedAt: Date.now() });
|
|
18
|
+
} catch {}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// "A newer writedocs is available" - shown after any command's output.
|
|
2
|
+
//
|
|
3
|
+
// Never slows a command down: the notice comes from a cached answer, and
|
|
4
|
+
// when that's more than a day old, a detached background process
|
|
5
|
+
// (update-check-refresh.js) asks the npm registry again and rewrites the
|
|
6
|
+
// cache for the next run - the same approach as npm's own update notifier.
|
|
7
|
+
//
|
|
8
|
+
// Not shown for `build` (the WriteDocs platform runs it, not a person) or
|
|
9
|
+
// `update` itself, in CI, when output isn't a terminal, when writedocs runs
|
|
10
|
+
// from a source checkout (updated with git, not npm), or with
|
|
11
|
+
// WRITEDOCS_NO_UPDATE_CHECK set.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { log, color } from './output.js';
|
|
18
|
+
|
|
19
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
20
|
+
const SILENT_COMMANDS = new Set(['build', 'update']);
|
|
21
|
+
|
|
22
|
+
export function cacheFile() {
|
|
23
|
+
const base =
|
|
24
|
+
process.env.XDG_CACHE_HOME ||
|
|
25
|
+
(process.platform === 'win32' ? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local') : path.join(os.homedir(), '.cache'));
|
|
26
|
+
return path.join(base, 'writedocs', 'update-check.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readCache() {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(cacheFile(), 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeCache(data) {
|
|
38
|
+
const file = cacheFile();
|
|
39
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
+
fs.writeFileSync(file, JSON.stringify(data));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The registry the user's npm uses, or npm's own. */
|
|
44
|
+
export function registryUrl() {
|
|
45
|
+
const configured = process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY;
|
|
46
|
+
return (configured || 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The version npm's `latest` tag points at. Throws on a network error. */
|
|
50
|
+
export async function fetchLatestVersion(name, { timeoutMs = 5000 } = {}) {
|
|
51
|
+
const res = await fetch(`${registryUrl()}/-/package/${name.replace('/', '%2f')}/dist-tags`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
52
|
+
if (!res.ok) throw new Error(`the registry answered ${res.status}`);
|
|
53
|
+
const tags = await res.json();
|
|
54
|
+
if (typeof tags?.latest !== 'string') throw new Error('the registry has no "latest" version');
|
|
55
|
+
return tags.latest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether `latest` is a newer release than `current` - x.y.z only; a
|
|
59
|
+
* prerelease never counts as newer. */
|
|
60
|
+
export function isNewer(latest, current) {
|
|
61
|
+
const parse = (v) => /^(\d+)\.(\d+)\.(\d+)$/.exec(String(v).trim())?.slice(1).map(Number);
|
|
62
|
+
const a = parse(latest);
|
|
63
|
+
const b = parse(current);
|
|
64
|
+
if (!a || !b) return false;
|
|
65
|
+
for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] > b[i];
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isSourceCheckout(packageRoot) {
|
|
70
|
+
return fs.existsSync(path.join(packageRoot, '.git'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function enabled(command, packageRoot) {
|
|
74
|
+
if (SILENT_COMMANDS.has(command)) return false;
|
|
75
|
+
if (process.env.WRITEDOCS_NO_UPDATE_CHECK || process.env.CI) return false;
|
|
76
|
+
if (!process.stdout.isTTY) return false;
|
|
77
|
+
return !isSourceCheckout(packageRoot);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let state = null;
|
|
81
|
+
|
|
82
|
+
/** Called once at startup: loads the cached answer, and refreshes it in the
|
|
83
|
+
* background when it's old. */
|
|
84
|
+
export function startUpdateCheck({ command, name, version, packageRoot }) {
|
|
85
|
+
if (!enabled(command, packageRoot)) return;
|
|
86
|
+
const cache = readCache();
|
|
87
|
+
state = { name, version, latest: cache?.name === name ? cache.latest : null, shown: false };
|
|
88
|
+
if (cache?.name === name && Date.now() - (cache.checkedAt ?? 0) < DAY) return;
|
|
89
|
+
try {
|
|
90
|
+
const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), 'update-check-refresh.js');
|
|
91
|
+
const child = spawn(process.execPath, [worker, name], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
92
|
+
child.unref();
|
|
93
|
+
} catch {
|
|
94
|
+
// No notice this time - not worth failing a command over.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Prints the notice, if there's a newer version - once per run. */
|
|
99
|
+
export function showUpdateNotice() {
|
|
100
|
+
if (!state || state.shown || !state.latest || !isNewer(state.latest, state.version)) return;
|
|
101
|
+
state.shown = true;
|
|
102
|
+
log.line();
|
|
103
|
+
log.info(`writedocs ${color.bold(state.latest)} is available ${color.dim(`(you have ${state.version})`)}. Run ${color.cyan('writedocs update')} to update.`);
|
|
104
|
+
}
|