@writedocs/generator 0.7.2 → 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 +28 -7
- 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 +3 -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.js +6 -1
- 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
|
@@ -10,6 +10,7 @@ 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
12
|
import { startUpdateCheck, showUpdateNotice } from '../src/cli/update-check.js';
|
|
13
|
+
import { readConfigText } from '../src/lib/config-file.js';
|
|
13
14
|
// O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
|
|
14
15
|
// que a plataforma importa por `@writedocs/generator/config-schema` - e o que
|
|
15
16
|
// faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
|
|
@@ -45,6 +46,9 @@ program
|
|
|
45
46
|
.name('writedocs')
|
|
46
47
|
.description('Static site generator for writedocs.json + MDX')
|
|
47
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)
|
|
48
52
|
// Every command checks for a newer writedocs (from a cache - see
|
|
49
53
|
// src/cli/update-check.js); the notice prints after the command's output.
|
|
50
54
|
.hook('preAction', (_program, command) => {
|
|
@@ -72,6 +76,9 @@ program
|
|
|
72
76
|
// src/cli/build-auth.js for the second layer: even someone who knows the
|
|
73
77
|
// command exists still can't run it without the right key.
|
|
74
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() })
|
|
75
82
|
.description('Build a static site into <dir>/dist')
|
|
76
83
|
.argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
|
|
77
84
|
.option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
|
|
@@ -79,8 +86,8 @@ program
|
|
|
79
86
|
.action(async (dir, opts) => {
|
|
80
87
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
81
88
|
// Project-scoped, not global: a .env sitting next to this project's own
|
|
82
|
-
// writedocs.json (
|
|
83
|
-
//
|
|
89
|
+
// writedocs.json (WRITEDOCS_API_KEY) is picked up automatically, so
|
|
90
|
+
// build doesn't need it exported by hand every
|
|
84
91
|
// session. dotenv never overwrites a var already set in the real
|
|
85
92
|
// environment - an explicit `export`/CI secret still wins over the file.
|
|
86
93
|
dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
|
|
@@ -106,7 +113,7 @@ program
|
|
|
106
113
|
throw new CliExit(1);
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
const rawText =
|
|
116
|
+
const rawText = readConfigText(contentDir);
|
|
110
117
|
const result = validateDocsConfig(rawText);
|
|
111
118
|
// Chave desconhecida na raiz e AVISO, nunca erro: a raiz nao e `.strict()`
|
|
112
119
|
// de proposito (tornar strict quebraria configs existentes), entao o Zod
|
|
@@ -186,7 +193,7 @@ program
|
|
|
186
193
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
187
194
|
const { preflightCheck } = await import('../src/cli/preflight.js');
|
|
188
195
|
preflightCheck(contentDir);
|
|
189
|
-
const configText =
|
|
196
|
+
const configText = readConfigText(contentDir);
|
|
190
197
|
const checking = step('Checking links');
|
|
191
198
|
// Generated OpenAPI pages are link targets too - same step dev/build run.
|
|
192
199
|
const { generateApiPages } = await import('../src/cli/generate-api-pages.js');
|
|
@@ -221,7 +228,7 @@ program
|
|
|
221
228
|
const contentDir = path.resolve(process.cwd(), dir);
|
|
222
229
|
const { preflightCheck } = await import('../src/cli/preflight.js');
|
|
223
230
|
preflightCheck(contentDir);
|
|
224
|
-
const configText =
|
|
231
|
+
const configText = readConfigText(contentDir);
|
|
225
232
|
const checking = step('Checking accessibility');
|
|
226
233
|
const { checkAccessibility } = await import('../src/lib/a11y-check.js');
|
|
227
234
|
const { formatContentIssues } = await import('../src/lib/content-check.js');
|
|
@@ -252,8 +259,10 @@ program
|
|
|
252
259
|
.option('--force', 'overwrite an existing writedocs.json')
|
|
253
260
|
.option('--dry-run', 'print the converted writedocs.json instead of writing it')
|
|
254
261
|
.action(async (dir, options) => {
|
|
255
|
-
|
|
256
|
-
|
|
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'];
|
|
257
266
|
if (mintlify === legacy) {
|
|
258
267
|
log.error(
|
|
259
268
|
mintlify
|
|
@@ -287,6 +296,18 @@ program
|
|
|
287
296
|
await runInit({ targetDir: path.resolve(process.cwd(), dir) });
|
|
288
297
|
});
|
|
289
298
|
|
|
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
|
+
|
|
290
311
|
program
|
|
291
312
|
.parseAsync(process.argv)
|
|
292
313
|
.then(showUpdateNotice)
|
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,5 +1,5 @@
|
|
|
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';
|
|
@@ -15,6 +15,7 @@ const REPEAT_WINDOW_MS = 2000;
|
|
|
15
15
|
export async function runDev({ contentDir, packageRoot, port, verbose = false }) {
|
|
16
16
|
const started = Date.now();
|
|
17
17
|
preflightCheck(contentDir);
|
|
18
|
+
writableInstallCheck(packageRoot);
|
|
18
19
|
const other = runningPreview(packageRoot);
|
|
19
20
|
if (other) {
|
|
20
21
|
log.error(`Another writedocs preview is already running${other.url ? ` at ${other.url}` : ''}.`);
|
|
@@ -29,6 +30,7 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false })
|
|
|
29
30
|
process.on('exit', () => removeLock(packageRoot));
|
|
30
31
|
|
|
31
32
|
const api = await generateApiPages({ contentDir });
|
|
33
|
+
sameDriveCheck(contentDir, packageRoot, { generatedPages: api.groups.length > 0 });
|
|
32
34
|
reportApiPages(api);
|
|
33
35
|
api.warnings.forEach((message) => log.warn(message));
|
|
34
36
|
|
|
@@ -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
|
+
}
|
package/src/cli/update.js
CHANGED
|
@@ -34,7 +34,12 @@ export async function installation(packageRoot, name) {
|
|
|
34
34
|
if (/\/_npx\//.test(posix)) return { kind: 'npx' };
|
|
35
35
|
if (/\/pnpm\/global\//.test(posix)) return { kind: 'global', command: 'pnpm', args: ['add', '-g', `${name}@latest`] };
|
|
36
36
|
if (/\/yarn\/global\//.test(posix)) return { kind: 'global', command: 'yarn', args: ['global', 'add', `${name}@latest`] };
|
|
37
|
-
|
|
37
|
+
// pnpm's default layout keeps the real package inside its store,
|
|
38
|
+
// <project>/node_modules/.pnpm/<name>@<version>/node_modules/<name>, and
|
|
39
|
+
// Node resolves the symlink to that path - so the project is what comes
|
|
40
|
+
// before .pnpm, not before the last node_modules/<name>.
|
|
41
|
+
const pnpmStore = posix.indexOf('/node_modules/.pnpm/');
|
|
42
|
+
const at = pnpmStore !== -1 ? pnpmStore : posix.lastIndexOf(`/node_modules/${name}`);
|
|
38
43
|
if (at === -1) return { kind: 'unknown' };
|
|
39
44
|
const container = posix.slice(0, at);
|
|
40
45
|
const npmRoot = await run('npm', ['root', '-g']);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { readConfigText } from '../lib/config-file.js';
|
|
3
4
|
|
|
4
5
|
// Astro's own `output: 'static'` redirects (both writedocs.json's `redirects`
|
|
5
6
|
// feature and the automatic "/" -> first-nav-page redirect in
|
|
@@ -31,7 +32,7 @@ import path from 'node:path';
|
|
|
31
32
|
// the one `redirects` array back out, nothing that needs zod's defaults
|
|
32
33
|
// or transforms.
|
|
33
34
|
export function writeRedirectsFile(distDir, contentDir) {
|
|
34
|
-
const writedocsJson = JSON.parse(
|
|
35
|
+
const writedocsJson = JSON.parse(readConfigText(contentDir));
|
|
35
36
|
const redirects = writedocsJson.redirects ?? [];
|
|
36
37
|
const lines = [];
|
|
37
38
|
|
|
@@ -658,7 +658,7 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
658
658
|
border-radius: 0.5rem;
|
|
659
659
|
border: none;
|
|
660
660
|
background: var(--wd-primary);
|
|
661
|
-
color:
|
|
661
|
+
color: var(--wd-on-primary);
|
|
662
662
|
font-size: 0.75rem;
|
|
663
663
|
font-weight: 600;
|
|
664
664
|
cursor: pointer;
|
|
@@ -931,7 +931,7 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
931
931
|
border-radius: 0.4rem;
|
|
932
932
|
border: none;
|
|
933
933
|
background: var(--wd-primary);
|
|
934
|
-
color:
|
|
934
|
+
color: var(--wd-on-primary);
|
|
935
935
|
font-size: 0.85rem;
|
|
936
936
|
font-weight: 600;
|
|
937
937
|
cursor: pointer;
|
|
@@ -1123,6 +1123,8 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
1123
1123
|
// after the base (protocol required), preserving method/headers/body,
|
|
1124
1124
|
// and returns CORS headers so the browser fetch() below succeeds even
|
|
1125
1125
|
// when the target API itself doesn't send Access-Control-Allow-Origin.
|
|
1126
|
+
// Only a fallback: a request goes straight to the API first, and through
|
|
1127
|
+
// here only when the browser blocks that (see the send handler below).
|
|
1126
1128
|
// Toggle via writedocs.json's `api.proxy` (see data-proxy on the tryit
|
|
1127
1129
|
// section below); the displayed curl/fetch/python snippets deliberately
|
|
1128
1130
|
// keep showing the real, non-proxied URL - only this in-browser request
|
|
@@ -1322,17 +1324,26 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
1322
1324
|
const responseBodyEl = section.querySelector<HTMLElement>('[data-role="response-body"] code');
|
|
1323
1325
|
const modalLangPanels = Array.from(section.querySelectorAll<HTMLElement>('[data-modal-lang-panel]'));
|
|
1324
1326
|
|
|
1327
|
+
// A reader's credentials last for the tab, not forever: sessionStorage
|
|
1328
|
+
// carries them across the pages of one visit and forgets them when the
|
|
1329
|
+
// tab closes. Older versions kept them in localStorage indefinitely -
|
|
1330
|
+
// a key found there is moved over once and deleted.
|
|
1325
1331
|
authInputs.forEach((input) => {
|
|
1326
|
-
const
|
|
1332
|
+
const storageKey = `wd-api-auth:${input.dataset.authName ?? ''}`;
|
|
1327
1333
|
try {
|
|
1328
|
-
const
|
|
1334
|
+
const legacy = localStorage.getItem(storageKey);
|
|
1335
|
+
if (legacy !== null) {
|
|
1336
|
+
if (sessionStorage.getItem(storageKey) === null) sessionStorage.setItem(storageKey, legacy);
|
|
1337
|
+
localStorage.removeItem(storageKey);
|
|
1338
|
+
}
|
|
1339
|
+
const saved = sessionStorage.getItem(storageKey);
|
|
1329
1340
|
if (saved) input.value = saved;
|
|
1330
1341
|
} catch {
|
|
1331
|
-
//
|
|
1342
|
+
// storage unavailable - auth just won't carry across pages
|
|
1332
1343
|
}
|
|
1333
1344
|
input.addEventListener('input', () => {
|
|
1334
1345
|
try {
|
|
1335
|
-
|
|
1346
|
+
sessionStorage.setItem(storageKey, input.value);
|
|
1336
1347
|
} catch {
|
|
1337
1348
|
// ignore
|
|
1338
1349
|
}
|
|
@@ -1714,9 +1725,13 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
1714
1725
|
}
|
|
1715
1726
|
|
|
1716
1727
|
const targetUrl = baseUrl + requestPath + (query.toString() ? `?${query.toString()}` : '');
|
|
1717
|
-
//
|
|
1718
|
-
//
|
|
1719
|
-
|
|
1728
|
+
// A "simple" request (CORS spec: GET/HEAD/POST, no JSON body, no
|
|
1729
|
+
// auth or custom headers) skips the preflight, so the server may
|
|
1730
|
+
// already have run it when the browser blocks the response.
|
|
1731
|
+
// Retrying one that changes something (a POST) through the proxy
|
|
1732
|
+
// could run it twice - so that one case isn't retried.
|
|
1733
|
+
const isSimple = ['GET', 'HEAD', 'POST'].includes(method) && Object.keys(headers).length === 0;
|
|
1734
|
+
const mayHaveRun = isSimple && method === 'POST';
|
|
1720
1735
|
|
|
1721
1736
|
if (sendBtn) {
|
|
1722
1737
|
sendBtn.disabled = true;
|
|
@@ -1727,8 +1742,23 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
1727
1742
|
responseBodyEl.textContent = '';
|
|
1728
1743
|
|
|
1729
1744
|
const startedAt = performance.now();
|
|
1745
|
+
let viaProxy = false;
|
|
1730
1746
|
try {
|
|
1731
|
-
|
|
1747
|
+
// Straight to the API first, so a reader's key and body only reach
|
|
1748
|
+
// the API itself. fetch() rejects when the browser blocks the
|
|
1749
|
+
// response (CORS) or can't reach the host - only then, and only
|
|
1750
|
+
// when writedocs.json's `api.proxy` allows it, the same request is
|
|
1751
|
+
// retried through writedocs' proxy (PROXY_BASE_URL above), and the
|
|
1752
|
+
// status line says so.
|
|
1753
|
+
let res: Response;
|
|
1754
|
+
try {
|
|
1755
|
+
res = await fetch(targetUrl, { method, headers, body });
|
|
1756
|
+
} catch (directErr) {
|
|
1757
|
+
if (!proxyEnabled || mayHaveRun) throw directErr;
|
|
1758
|
+
viaProxy = true;
|
|
1759
|
+
responseStatusEl.textContent = 'Blocked by the browser (CORS) - retrying through the writedocs proxy...';
|
|
1760
|
+
res = await fetch(`${PROXY_BASE_URL}${targetUrl}`, { method, headers, body });
|
|
1761
|
+
}
|
|
1732
1762
|
const elapsed = Math.round(performance.now() - startedAt);
|
|
1733
1763
|
const text = await res.text();
|
|
1734
1764
|
let pretty = text;
|
|
@@ -1737,11 +1767,15 @@ const securityJson = JSON.stringify(op?.security ?? []);
|
|
|
1737
1767
|
} catch {
|
|
1738
1768
|
// not JSON - show raw text as-is
|
|
1739
1769
|
}
|
|
1740
|
-
responseStatusEl.textContent = `${res.status} ${res.statusText} - ${elapsed}ms`;
|
|
1770
|
+
responseStatusEl.textContent = `${res.status} ${res.statusText} - ${elapsed}ms${viaProxy ? ' - sent through the writedocs proxy (the API blocks direct browser requests)' : ''}`;
|
|
1741
1771
|
responseStatusEl.className = `wd-api-response-status ${res.ok ? 'wd-api-status-ok' : 'wd-api-status-error'}`;
|
|
1742
1772
|
responseBodyEl.textContent = pretty;
|
|
1743
1773
|
} catch (err) {
|
|
1744
|
-
responseStatusEl.textContent =
|
|
1774
|
+
responseStatusEl.textContent = viaProxy
|
|
1775
|
+
? 'Request failed through the writedocs proxy too - the base URL may be unreachable from the internet.'
|
|
1776
|
+
: mayHaveRun && proxyEnabled
|
|
1777
|
+
? 'Blocked by the browser (CORS). Not retried through the proxy: this POST may already have reached the API.'
|
|
1778
|
+
: 'Request failed - likely blocked by CORS, or the base URL is unreachable from your browser.';
|
|
1745
1779
|
responseStatusEl.className = 'wd-api-response-status wd-api-status-error';
|
|
1746
1780
|
responseBodyEl.textContent = err instanceof Error ? err.message : String(err);
|
|
1747
1781
|
} finally {
|
|
@@ -27,7 +27,7 @@ import { extraClasses } from './class-names';
|
|
|
27
27
|
height: 1.8rem;
|
|
28
28
|
padding: 0.45rem;
|
|
29
29
|
box-sizing: border-box;
|
|
30
|
-
color:
|
|
30
|
+
color: var(--wd-on-primary);
|
|
31
31
|
z-index: 1;
|
|
32
32
|
}
|
|
33
33
|
span.wd-step-icon {
|
|
@@ -47,7 +47,7 @@ import { extraClasses } from './class-names';
|
|
|
47
47
|
height: 1.8rem;
|
|
48
48
|
border-radius: 50%;
|
|
49
49
|
background: var(--wd-primary);
|
|
50
|
-
color:
|
|
50
|
+
color: var(--wd-on-primary);
|
|
51
51
|
font-size: 0.85rem;
|
|
52
52
|
font-weight: 600;
|
|
53
53
|
display: flex;
|