@writedocs/generator 0.4.10 → 0.4.12
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 +5 -4
- package/bin/writedocs.js +44 -25
- package/package.json +1 -1
- package/src/cli/api-pages-output.js +11 -0
- package/src/cli/astro-output.js +146 -0
- package/src/cli/astro-worker.js +78 -0
- package/src/cli/build-auth.js +7 -5
- package/src/cli/build.js +104 -7
- package/src/cli/convert.js +37 -26
- package/src/cli/dev-lock.js +51 -0
- package/src/cli/dev.js +158 -6
- package/src/cli/generate-api-pages.js +15 -10
- package/src/cli/init.js +7 -5
- package/src/cli/output.js +160 -0
- package/src/cli/preflight.js +11 -8
- package/src/cli/run-astro.js +76 -46
- package/src/cli/run-pagefind.js +14 -9
- package/src/components/AppIcon.astro +2 -1
- package/src/lib/astro-log-destination.js +25 -0
- package/src/lib/cli-report.js +20 -0
- package/src/lib/mdx-inline-react.js +2 -1
- package/src/lib/mdx-unknown-components.js +5 -3
package/astro.config.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { remarkUnknownComponentFallback } from './src/lib/mdx-unknown-components
|
|
|
36
36
|
import { remarkExtractInlineReactComponents } from './src/lib/mdx-inline-react.js';
|
|
37
37
|
import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
|
|
38
38
|
import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
|
|
39
|
+
import { report } from './src/lib/cli-report.js';
|
|
39
40
|
import {
|
|
40
41
|
loadDocsConfig,
|
|
41
42
|
resolveCodeblockTheme,
|
|
@@ -105,7 +106,7 @@ const contentPublicDir = path.join(contentDir, 'public');
|
|
|
105
106
|
// document-relative URLs isn't meaningful anyway.
|
|
106
107
|
const siteUrl = resolveSiteUrl(docsConfig);
|
|
107
108
|
if (!siteUrl) {
|
|
108
|
-
|
|
109
|
+
report('info', 'No "domain" set in writedocs.json, so no sitemap.xml was generated.');
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/** Recursively lists every .md/.mdx file under `baseDir` (Node 20's
|
|
@@ -184,18 +185,18 @@ function singleSitemapFile() {
|
|
|
184
185
|
const chunks = fs.readdirSync(outDir).filter((f) => /^sitemap-\d+\.xml$/.test(f));
|
|
185
186
|
const target = path.join(outDir, 'sitemap.xml');
|
|
186
187
|
if (chunks.length !== 1) {
|
|
187
|
-
|
|
188
|
+
report('debug', `${chunks.length} sitemap files generated - keeping sitemap-index.xml.`);
|
|
188
189
|
return;
|
|
189
190
|
}
|
|
190
191
|
if (fs.existsSync(target)) {
|
|
191
|
-
|
|
192
|
+
report('info', 'public/sitemap.xml exists, so it was kept - the generated sitemap is sitemap-index.xml.');
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
195
|
fs.renameSync(path.join(outDir, chunks[0]), target);
|
|
195
196
|
fs.rmSync(path.join(outDir, 'sitemap-index.xml'), { force: true });
|
|
196
197
|
// O próprio plugin acabou de logar "sitemap-index.xml created"; esta
|
|
197
198
|
// linha diz o que de fato ficou no build.
|
|
198
|
-
|
|
199
|
+
report('debug', `Replaced ${chunks[0]} + sitemap-index.xml with a single sitemap.xml.`);
|
|
199
200
|
},
|
|
200
201
|
},
|
|
201
202
|
};
|
package/bin/writedocs.js
CHANGED
|
@@ -8,6 +8,7 @@ import { runDev } from '../src/cli/dev.js';
|
|
|
8
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
|
+
import { log, step, plural, color, CliExit, errorText } from '../src/cli/output.js';
|
|
11
12
|
// O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
|
|
12
13
|
// que a plataforma importa por `@writedocs/generator/config-schema` - e o que
|
|
13
14
|
// faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
|
|
@@ -49,11 +50,13 @@ program
|
|
|
49
50
|
.description('Start a local dev server with hot reload')
|
|
50
51
|
.argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
|
|
51
52
|
.option('-p, --port <port>', 'port to run on')
|
|
53
|
+
.option('--verbose', "also print Astro's and Vite's own output")
|
|
52
54
|
.action(async (dir, opts) => {
|
|
53
55
|
await runDev({
|
|
54
56
|
contentDir: path.resolve(process.cwd(), dir),
|
|
55
57
|
packageRoot,
|
|
56
58
|
port: opts.port,
|
|
59
|
+
verbose: Boolean(opts.verbose),
|
|
57
60
|
});
|
|
58
61
|
});
|
|
59
62
|
|
|
@@ -66,6 +69,7 @@ program
|
|
|
66
69
|
.description('Build a static site into <dir>/dist')
|
|
67
70
|
.argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
|
|
68
71
|
.option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
|
|
72
|
+
.option('--verbose', "also print Astro's and Vite's own output")
|
|
69
73
|
.action(async (dir, opts) => {
|
|
70
74
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
71
75
|
// Project-scoped, not global: a .env sitting next to this project's own
|
|
@@ -75,7 +79,7 @@ program
|
|
|
75
79
|
// environment - an explicit `export`/CI secret still wins over the file.
|
|
76
80
|
dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
|
|
77
81
|
await requireBuildKey(opts.key || process.env.WRITEDOCS_API_KEY);
|
|
78
|
-
await runBuild({ contentDir, packageRoot });
|
|
82
|
+
await runBuild({ contentDir, packageRoot, verbose: Boolean(opts.verbose) });
|
|
79
83
|
});
|
|
80
84
|
|
|
81
85
|
program
|
|
@@ -91,10 +95,9 @@ program
|
|
|
91
95
|
const configPath = path.join(contentDir, 'writedocs.json');
|
|
92
96
|
if (!fs.existsSync(configPath)) {
|
|
93
97
|
// Mesma mensagem que loadDocsConfig() ja da pro mesmo caso.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
process.exit(1);
|
|
98
|
+
log.error(`No writedocs.json found in ${contentDir}`);
|
|
99
|
+
log.detail(color.dim('Run "writedocs init" to create one.'));
|
|
100
|
+
throw new CliExit(1);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
const rawText = fs.readFileSync(configPath, 'utf-8');
|
|
@@ -109,15 +112,21 @@ program
|
|
|
109
112
|
// A versao longa (linha, frase humana, sugestao) em vez da curta que o
|
|
110
113
|
// `writedocs build` imprime: aqui o usuario pediu explicitamente um
|
|
111
114
|
// diagnostico, e tem a tela inteira pra ele.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
log.error(
|
|
116
|
+
result.kind === 'invalid_json'
|
|
117
|
+
? `${nomeArquivo} is not valid JSON`
|
|
118
|
+
: `${plural(result.issues.length, 'error')} in ${nomeArquivo}`
|
|
119
|
+
);
|
|
120
|
+
log.line();
|
|
121
|
+
log.line(formatValidationIssuesDetailed(result.issues, { fileName: nomeArquivo }));
|
|
122
|
+
log.line();
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
if (warnings.length > 0) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
log.warn(`${plural(warnings.length, 'warning')} in ${nomeArquivo}`);
|
|
127
|
+
log.line();
|
|
128
|
+
log.line(formatValidationIssuesDetailed(warnings, { fileName: nomeArquivo }));
|
|
129
|
+
log.line();
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
// The pages: everything that would fail the build (errors) or build
|
|
@@ -128,27 +137,35 @@ program
|
|
|
128
137
|
let content = null;
|
|
129
138
|
if (!options.configOnly) {
|
|
130
139
|
const { checkContent, formatContentIssues } = await import('../src/lib/content-check.js');
|
|
140
|
+
const checking = step('Checking pages');
|
|
131
141
|
content = await checkContent(contentDir, result.kind === 'invalid_json' ? null : rawText);
|
|
142
|
+
checking.stop();
|
|
132
143
|
if (content.errors.length > 0) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
log.error(`${plural(content.errors.length, 'error')} in the pages`);
|
|
145
|
+
log.line();
|
|
146
|
+
log.line(formatContentIssues(content.errors));
|
|
147
|
+
log.line();
|
|
136
148
|
}
|
|
137
149
|
if (content.warnings.length > 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
log.warn(`${plural(content.warnings.length, 'warning')} in the pages`);
|
|
151
|
+
log.line();
|
|
152
|
+
log.line(formatContentIssues(content.warnings));
|
|
153
|
+
log.line();
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
156
|
|
|
144
|
-
if (!result.ok || (content && content.errors.length > 0))
|
|
157
|
+
if (!result.ok || (content && content.errors.length > 0)) {
|
|
158
|
+
const total = (result.ok ? 0 : result.issues.length) + (content ? content.errors.length : 0);
|
|
159
|
+
log.error(`Not valid: ${plural(total, 'error')} to fix - the build would fail on these.`);
|
|
160
|
+
throw new CliExit(1);
|
|
161
|
+
}
|
|
145
162
|
// Aviso nao invalida: um config so com avisos continua valido, e sai 0 -
|
|
146
163
|
// o mesmo criterio que a plataforma usa pra marcar o projeto como `valid`.
|
|
147
|
-
|
|
164
|
+
log.success(`${nomeArquivo} is valid`);
|
|
148
165
|
if (content) {
|
|
149
|
-
|
|
150
|
-
`
|
|
151
|
-
(content.warnings.length ? `, ${content.warnings.length
|
|
166
|
+
log.success(
|
|
167
|
+
`Checked ${plural(content.pages, 'page')}: no errors` +
|
|
168
|
+
(content.warnings.length ? `, ${plural(content.warnings.length, 'warning')} above` : '')
|
|
152
169
|
);
|
|
153
170
|
}
|
|
154
171
|
});
|
|
@@ -165,8 +182,8 @@ program
|
|
|
165
182
|
// --mintlify is the only source today; the flag is still required so
|
|
166
183
|
// the command reads the same once other tools are added.
|
|
167
184
|
if (!options.mintlify && !options.docsJson) {
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
log.error('Say what to convert from: writedocs convert --mintlify [dir]');
|
|
186
|
+
throw new CliExit(1);
|
|
170
187
|
}
|
|
171
188
|
const { runConvert } = await import('../src/cli/convert.js');
|
|
172
189
|
await runConvert({
|
|
@@ -185,6 +202,8 @@ program
|
|
|
185
202
|
});
|
|
186
203
|
|
|
187
204
|
program.parseAsync(process.argv).catch((err) => {
|
|
188
|
-
|
|
205
|
+
// A command that already printed its own error throws CliExit.
|
|
206
|
+
if (err instanceof CliExit) process.exit(err.code);
|
|
207
|
+
log.error(errorText(err));
|
|
189
208
|
process.exit(1);
|
|
190
209
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// What generateApiPages() did, as ✓ lines - shared by `dev` and `build`.
|
|
2
|
+
import { log, plural, color } from './output.js';
|
|
3
|
+
|
|
4
|
+
export function reportApiPages({ groups, pageSpecs }) {
|
|
5
|
+
for (const { group, src, operations } of groups) {
|
|
6
|
+
log.success(`Generated ${plural(operations, 'API page')} for "${group}" ${color.dim(`from ${src}`)}`);
|
|
7
|
+
}
|
|
8
|
+
for (const { spec, operations } of pageSpecs) {
|
|
9
|
+
log.success(`Read ${plural(operations, 'operation')} from ${spec} ${color.dim('(named by pages)')}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Turns what Astro and Vite report (see astro-worker.js) into what the
|
|
2
|
+
// writedocs CLI shows: an error becomes { where, message, hint } - the
|
|
3
|
+
// author's file and line, and the reason in one sentence - instead of a
|
|
4
|
+
// bundler error with a stack trace; a log line is either something the
|
|
5
|
+
// author should see or noise to hide.
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
9
|
+
export const stripAnsi = (text) => String(text ?? '').replace(ANSI, '');
|
|
10
|
+
|
|
11
|
+
// Astro's formatted errors (core/messages/runtime.js formatErrorMessage)
|
|
12
|
+
// append these sections after the message itself.
|
|
13
|
+
const SECTION = /\n\s*(Stack trace|Hint|Error reference|Location|Caused by):/;
|
|
14
|
+
|
|
15
|
+
function relativeTo(contentDir, file) {
|
|
16
|
+
if (!file) return null;
|
|
17
|
+
const clean = file.replace(/^file:\/\/\/?/, '').replace(/[?#].*$/, '');
|
|
18
|
+
const abs = path.resolve(clean);
|
|
19
|
+
const rel = path.relative(contentDir, abs);
|
|
20
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
|
21
|
+
return rel.split(path.sep).join('/');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function position(line, column) {
|
|
25
|
+
if (!Number.isInteger(line) || line < 1) return '';
|
|
26
|
+
return Number.isInteger(column) && column > 0 ? `:${line}:${column}` : `:${line}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Everything an Astro/Vite error can say, as { where, message, hint }.
|
|
30
|
+
* `input` is a serialized error (astro-worker.js serializeError) or the
|
|
31
|
+
* text of an error log line. */
|
|
32
|
+
export function describeError(input, contentDir) {
|
|
33
|
+
const error = typeof input === 'string' ? { message: input } : input ?? {};
|
|
34
|
+
let text = stripAnsi(error.message).replace(/\r/g, '');
|
|
35
|
+
let file = error.loc?.file ?? null;
|
|
36
|
+
let line = error.loc?.line;
|
|
37
|
+
let column = error.loc?.column;
|
|
38
|
+
let hint = error.hint ? stripAnsi(error.hint).trim() : null;
|
|
39
|
+
let name = error.name;
|
|
40
|
+
|
|
41
|
+
// A bundler failure wraps the real error:
|
|
42
|
+
// Build failed with 1 error:
|
|
43
|
+
//
|
|
44
|
+
// [plugin @mdx-js/rolldown] /abs/docs/page.mdx:undefined:undefined
|
|
45
|
+
// MDXError: Expected a closing tag for `<Card>` (110:1-110:17)
|
|
46
|
+
// at ...
|
|
47
|
+
const bundled = text.match(/^Build failed with \d+ errors?:\s*\n+([\s\S]*)$/);
|
|
48
|
+
if (bundled) {
|
|
49
|
+
const [head, ...rest] = bundled[1].split('\n');
|
|
50
|
+
const plugin = head.match(/^\[plugin [^\]]+\]\s+(.+?)(?::(\d+|undefined):(\d+|undefined))?\s*$/);
|
|
51
|
+
if (plugin) {
|
|
52
|
+
file = file ?? plugin[1];
|
|
53
|
+
if (plugin[2] && plugin[2] !== 'undefined') line = line ?? Number(plugin[2]);
|
|
54
|
+
if (plugin[3] && plugin[3] !== 'undefined') column = column ?? Number(plugin[3]);
|
|
55
|
+
text = rest.join('\n');
|
|
56
|
+
} else {
|
|
57
|
+
text = bundled[1];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Astro's own sections: keep the hint, and take the file from the first
|
|
62
|
+
// stack frame when it's one of the author's files.
|
|
63
|
+
const sectionAt = text.search(SECTION);
|
|
64
|
+
if (sectionAt !== -1) {
|
|
65
|
+
const sections = text.slice(sectionAt);
|
|
66
|
+
text = text.slice(0, sectionAt);
|
|
67
|
+
const hintMatch = sections.match(/\n\s*Hint:\s*\n([\s\S]*?)(?=\n\s*(Stack trace|Error reference|Location|Caused by):|$)/);
|
|
68
|
+
if (hintMatch && !hint) hint = hintMatch[1].trim().replace(/\n\s+/g, ' ');
|
|
69
|
+
const frame = sections.match(/Stack trace:\s*\n\s*at (.+)/);
|
|
70
|
+
if (frame && !file) file = frame[1].trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Stack frames and an "SomethingError: " prefix are noise here.
|
|
74
|
+
text = text
|
|
75
|
+
.split('\n')
|
|
76
|
+
.filter((l) => !/^\s+at\s/.test(l) && !/^\s*\[\.\.\.\] See full stack trace/.test(l))
|
|
77
|
+
.join('\n')
|
|
78
|
+
.trim();
|
|
79
|
+
const prefixed = text.match(/^([A-Z][A-Za-z]*Error|YAMLException):\s+([\s\S]*)$/);
|
|
80
|
+
if (prefixed) {
|
|
81
|
+
name = name && name !== 'Error' ? name : prefixed[1];
|
|
82
|
+
text = prefixed[2];
|
|
83
|
+
}
|
|
84
|
+
text = text.replace(/^\[writedocs\]\s*/, '');
|
|
85
|
+
|
|
86
|
+
// micromark/MDX put the position at the end: "(110:1-110:17)" or "(3:5)".
|
|
87
|
+
const pos = text.match(/\s*\((\d+):(\d+)(?:-\d+:\d+)?\)\s*$/);
|
|
88
|
+
if (pos) {
|
|
89
|
+
line = line ?? Number(pos[1]);
|
|
90
|
+
column = column ?? Number(pos[2]);
|
|
91
|
+
text = text.slice(0, pos.index);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (name === 'YAMLException') text = `Frontmatter isn't valid YAML: ${text}`;
|
|
95
|
+
|
|
96
|
+
const rel = relativeTo(contentDir, file);
|
|
97
|
+
return {
|
|
98
|
+
where: rel ? `${rel}${position(line, column)}` : null,
|
|
99
|
+
message: text || 'Unknown error.',
|
|
100
|
+
hint: hint || null,
|
|
101
|
+
// The same error seen again - dev.js uses it to recall the file when
|
|
102
|
+
// a repeat of the error no longer names it.
|
|
103
|
+
key: `${text}|${position(line, column)}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A route Astro just wrote during a build (" ├─ /docs/x/index.html"),
|
|
108
|
+
* or null. */
|
|
109
|
+
export function builtRoute(event) {
|
|
110
|
+
if (event.type !== 'astro-log' || event.level !== 'info') return null;
|
|
111
|
+
const match = stripAnsi(event.message).match(/^\s*[├└]─\s+(\S+)/);
|
|
112
|
+
return match ? match[1] : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A dev request log ("[404] /nope/ 18ms") as { status, url }, or null. */
|
|
116
|
+
export function requestLog(event) {
|
|
117
|
+
if (event.type !== 'astro-log') return null;
|
|
118
|
+
const match = stripAnsi(event.message).match(/^\[(\d{3})\]\s+(?:\(rewrite\)\s+)?(?:[A-Z]+\s+)?(\S+)/);
|
|
119
|
+
return match ? { status: Number(match[1]), url: match[2] } : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** An Astro/Vite warning worth showing the author: one about their own
|
|
123
|
+
* files (it names a path inside the project), rewritten to relative
|
|
124
|
+
* paths. Everything else - bundle sizes, dependency optimization, route
|
|
125
|
+
* internals - is about writedocs itself, and stays hidden (--verbose shows
|
|
126
|
+
* it). */
|
|
127
|
+
export function authorWarning(event, contentDir) {
|
|
128
|
+
if (event.type !== 'astro-log' || event.level !== 'warn') return null;
|
|
129
|
+
const text = stripAnsi(event.message);
|
|
130
|
+
const variants = [contentDir, contentDir.split(path.sep).join('/'), contentDir.split('/').join('\\')];
|
|
131
|
+
if (!variants.some((v) => text.includes(v))) return null;
|
|
132
|
+
let out = text;
|
|
133
|
+
for (const v of variants) out = out.split(`${v}/`).join('').split(`${v}\\`).join('').split(v).join('.');
|
|
134
|
+
return out.trim();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One event as a plain line, for --verbose. */
|
|
138
|
+
export function verboseLine(event) {
|
|
139
|
+
if (event.type === 'astro-log') {
|
|
140
|
+
const label = event.label && event.label !== 'SKIP_FORMAT' ? `[${event.label}] ` : '';
|
|
141
|
+
const level = event.level === 'warn' || event.level === 'error' ? `${event.level.toUpperCase()} ` : '';
|
|
142
|
+
return `${level}${label}${stripAnsi(event.message)}`;
|
|
143
|
+
}
|
|
144
|
+
if (event.type === 'raw') return stripAnsi(event.line);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Runs Astro for `writedocs dev` and `writedocs build`, in a process of its
|
|
2
|
+
// own (spawned by run-astro.js). Astro is driven through its programmatic API
|
|
3
|
+
// rather than its CLI binary, so that:
|
|
4
|
+
// - every Astro/Vite log line goes to lib/astro-log-destination.js, which
|
|
5
|
+
// forwards it to the writedocs CLI instead of printing it;
|
|
6
|
+
// - a fatal error comes back here as an Error object, sent to the CLI with
|
|
7
|
+
// its file/line/hint intact, not as pre-formatted terminal text;
|
|
8
|
+
// - none of the `astro` CLI's own behavior applies: its one-dev-server-per-
|
|
9
|
+
// root lock (every writedocs project shares one root, packageRoot, so two
|
|
10
|
+
// projects' `writedocs dev` would refuse each other), and its automatic
|
|
11
|
+
// "run in the background" mode when it detects an AI agent.
|
|
12
|
+
//
|
|
13
|
+
// Messages to the parent (process.send):
|
|
14
|
+
// { type: 'astro-log', level, label, message } - from the log destination
|
|
15
|
+
// { type: 'ready', urls: { local, network } } - dev server listening
|
|
16
|
+
// { type: 'done' } - build finished
|
|
17
|
+
// { type: 'fatal', error } - see serializeError()
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { build, dev } from 'astro';
|
|
20
|
+
|
|
21
|
+
const command = process.argv[2];
|
|
22
|
+
const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT;
|
|
23
|
+
const port = process.env.WRITEDOCS_PORT ? Number(process.env.WRITEDOCS_PORT) : undefined;
|
|
24
|
+
|
|
25
|
+
function send(message) {
|
|
26
|
+
if (typeof process.send === 'function' && process.connected) process.send(message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function serializeError(err) {
|
|
30
|
+
if (!(err instanceof Error)) return { message: String(err) };
|
|
31
|
+
return {
|
|
32
|
+
name: err.name,
|
|
33
|
+
message: err.message,
|
|
34
|
+
hint: typeof err.hint === 'string' ? err.hint : undefined,
|
|
35
|
+
loc: err.loc && err.loc.file ? { file: err.loc.file, line: err.loc.line, column: err.loc.column } : undefined,
|
|
36
|
+
id: typeof err.id === 'string' ? err.id : undefined,
|
|
37
|
+
frame: typeof err.frame === 'string' ? err.frame : undefined,
|
|
38
|
+
stack: err.stack,
|
|
39
|
+
cause: err.cause ? serializeError(err.cause) : undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fail(err) {
|
|
44
|
+
send({ type: 'fatal', error: serializeError(err) });
|
|
45
|
+
// Let the IPC message leave before exiting.
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.on('uncaughtException', fail);
|
|
51
|
+
process.on('unhandledRejection', fail);
|
|
52
|
+
// The writedocs CLI went away (closed terminal, killed) - don't outlive it.
|
|
53
|
+
process.on('disconnect', () => process.exit(0));
|
|
54
|
+
|
|
55
|
+
const inlineConfig = {
|
|
56
|
+
root: packageRoot,
|
|
57
|
+
logLevel: 'info',
|
|
58
|
+
logger: { entrypoint: path.join(packageRoot, 'src', 'lib', 'astro-log-destination.js') },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (command === 'dev') {
|
|
63
|
+
const server = await dev({
|
|
64
|
+
...inlineConfig,
|
|
65
|
+
...(port ? { server: { port } } : {}),
|
|
66
|
+
});
|
|
67
|
+
send({ type: 'ready', urls: server.resolvedUrls ?? { local: [], network: [] } });
|
|
68
|
+
} else if (command === 'build') {
|
|
69
|
+
await build(inlineConfig);
|
|
70
|
+
send({ type: 'done' });
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error(`astro-worker: unknown command "${command}"`);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
await fail(err);
|
|
78
|
+
}
|
package/src/cli/build-auth.js
CHANGED
|
@@ -15,16 +15,18 @@
|
|
|
15
15
|
* See docs/dev/docs/deploy.mdx for how to deploy key-server/ and issue
|
|
16
16
|
* keys, and key-server/README.md for the service's own endpoints.
|
|
17
17
|
*/
|
|
18
|
+
import { log } from './output.js';
|
|
19
|
+
|
|
18
20
|
export const EX_TEMPFAIL = 75;
|
|
19
21
|
|
|
20
22
|
export async function requireBuildKey(providedKey) {
|
|
21
23
|
const serverUrl = process.env.WRITEDOCS_KEY_SERVER_URL;
|
|
22
24
|
if (!serverUrl) {
|
|
23
|
-
|
|
25
|
+
log.error('build is not available.');
|
|
24
26
|
process.exit(1);
|
|
25
27
|
}
|
|
26
28
|
if (!providedKey) {
|
|
27
|
-
|
|
29
|
+
log.error('build requires a valid --key (or WRITEDOCS_API_KEY).');
|
|
28
30
|
process.exit(1);
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -47,12 +49,12 @@ export async function requireBuildKey(providedKey) {
|
|
|
47
49
|
// Fails closed on a network error/timeout too, same as an explicit
|
|
48
50
|
// rejection - an unreachable authorization server is not treated as
|
|
49
51
|
// "no opinion, let it through".
|
|
50
|
-
|
|
52
|
+
log.error(`Could not reach the build authorization server: ${err.message}`);
|
|
51
53
|
process.exit(EX_TEMPFAIL);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
if (res.status >= 500) {
|
|
55
|
-
|
|
57
|
+
log.error(`The build authorization server failed (HTTP ${res.status}) - try again later.`);
|
|
56
58
|
process.exit(EX_TEMPFAIL);
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -63,7 +65,7 @@ export async function requireBuildKey(providedKey) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
if (!valid) {
|
|
66
|
-
|
|
68
|
+
log.error('build requires a valid --key (or WRITEDOCS_API_KEY) - the server rejected this one.');
|
|
67
69
|
process.exit(1);
|
|
68
70
|
}
|
|
69
71
|
}
|
package/src/cli/build.js
CHANGED
|
@@ -6,12 +6,88 @@ import { preflightCheck } 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';
|
|
9
|
+
import { log, step, plural, duration, displayPath, formatProblems, color, CliExit } from './output.js';
|
|
10
|
+
import { describeError, builtRoute, authorWarning, verboseLine } from './astro-output.js';
|
|
11
|
+
import { reportApiPages } from './api-pages-output.js';
|
|
9
12
|
|
|
10
|
-
export async function runBuild({ contentDir, packageRoot }) {
|
|
13
|
+
export async function runBuild({ contentDir, packageRoot, verbose = false }) {
|
|
14
|
+
const started = Date.now();
|
|
11
15
|
preflightCheck(contentDir);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
// Collected while building, printed together at the end - a warning in
|
|
18
|
+
// the middle of a spinner is easy to miss.
|
|
19
|
+
const warnings = [];
|
|
20
|
+
const notes = [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const addWarning = (where, message) => {
|
|
23
|
+
const key = `${where ?? ''}\n${message}`;
|
|
24
|
+
if (seen.has(key)) return;
|
|
25
|
+
seen.add(key);
|
|
26
|
+
warnings.push({ where, message });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const api = await generateApiPages({ contentDir });
|
|
30
|
+
reportApiPages(api);
|
|
31
|
+
api.warnings.forEach((message) => addWarning(null, message));
|
|
32
|
+
|
|
33
|
+
const building = step('Building pages');
|
|
34
|
+
const buildStarted = Date.now();
|
|
35
|
+
let pages = 0;
|
|
36
|
+
let fatal = null;
|
|
37
|
+
const rawTail = [];
|
|
38
|
+
const { exited } = runAstro('build', {
|
|
39
|
+
packageRoot,
|
|
40
|
+
contentDir,
|
|
41
|
+
onEvent(event) {
|
|
42
|
+
if (verbose) {
|
|
43
|
+
const line = verboseLine(event);
|
|
44
|
+
if (line !== null) log.line(color.dim(line));
|
|
45
|
+
}
|
|
46
|
+
if (event.type === 'fatal') {
|
|
47
|
+
fatal = event.error;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (event.type === 'report') {
|
|
51
|
+
if (event.level === 'warn') addWarning(event.where, event.message);
|
|
52
|
+
else if (event.level === 'info' && !notes.includes(event.message)) notes.push(event.message);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (event.type === 'raw') {
|
|
56
|
+
rawTail.push(event.line);
|
|
57
|
+
if (rawTail.length > 40) rawTail.shift();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const route = builtRoute(event);
|
|
61
|
+
if (route) {
|
|
62
|
+
if (route.endsWith('.html')) pages += 1;
|
|
63
|
+
building.update(`Building pages ${color.dim(`(${pages})`)}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const warning = authorWarning(event, contentDir);
|
|
67
|
+
if (warning) addWarning(null, warning);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const code = await exited;
|
|
71
|
+
|
|
72
|
+
if (fatal || code !== 0) {
|
|
73
|
+
building.fail('Build failed');
|
|
74
|
+
if (fatal) {
|
|
75
|
+
const problem = describeError(fatal, contentDir);
|
|
76
|
+
log.line();
|
|
77
|
+
log.line(formatProblems([{ where: problem.where, message: problem.message }]));
|
|
78
|
+
if (problem.hint) log.line(`\n${color.dim(` Hint: ${problem.hint}`)}`);
|
|
79
|
+
if (verbose && fatal.stack) log.line(`\n${color.dim(fatal.stack)}`);
|
|
80
|
+
} else if (rawTail.length) {
|
|
81
|
+
// No structured error (the worker itself crashed) - show what it printed.
|
|
82
|
+
log.line();
|
|
83
|
+
log.line(color.dim(rawTail.join('\n')));
|
|
84
|
+
}
|
|
85
|
+
log.line();
|
|
86
|
+
log.line(color.dim('Run "writedocs validate" to list every problem in the project at once.'));
|
|
87
|
+
throw new CliExit(1);
|
|
88
|
+
}
|
|
89
|
+
building.succeed(`Built ${plural(pages, 'page')} ${color.dim(`in ${duration(Date.now() - buildStarted)}`)}`);
|
|
90
|
+
|
|
15
91
|
const distDir = path.join(contentDir, 'dist');
|
|
16
92
|
// Astro just wrote its actual output to writedocsBuildStagingDir()
|
|
17
93
|
// (astro.config.mjs's own `outDir`), not distDir directly - see that
|
|
@@ -34,7 +110,28 @@ export async function runBuild({ contentDir, packageRoot }) {
|
|
|
34
110
|
// - this turns those into real instant edge redirects on hosts that read
|
|
35
111
|
// a `_redirects` file (Cloudflare Pages, Netlify), purely additively.
|
|
36
112
|
writeRedirectsFile(distDir, contentDir);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
113
|
+
|
|
114
|
+
const indexing = step('Indexing search');
|
|
115
|
+
try {
|
|
116
|
+
await runPagefind(distDir, { packageRoot });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
indexing.fail('Search indexing failed');
|
|
119
|
+
log.line();
|
|
120
|
+
log.line(color.dim(err.message));
|
|
121
|
+
throw new CliExit(1);
|
|
122
|
+
}
|
|
123
|
+
indexing.succeed('Indexed search');
|
|
124
|
+
|
|
125
|
+
if (warnings.length) {
|
|
126
|
+
log.line();
|
|
127
|
+
log.warn(plural(warnings.length, 'warning'));
|
|
128
|
+
log.line();
|
|
129
|
+
log.line(formatProblems(warnings));
|
|
130
|
+
}
|
|
131
|
+
if (notes.length) {
|
|
132
|
+
log.line();
|
|
133
|
+
notes.forEach((note) => log.info(note));
|
|
134
|
+
}
|
|
135
|
+
log.line();
|
|
136
|
+
log.success(`Done in ${duration(Date.now() - started)} ${color.dim('-')} site written to ${color.bold(displayPath(distDir))}`);
|
|
40
137
|
}
|