@writedocs/generator 0.7.1 → 0.7.2
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/bin/writedocs.js +26 -9
- package/package.json +1 -1
- package/src/cli/dev.js +4 -0
- package/src/cli/update-check-refresh.js +18 -0
- package/src/cli/update-check.js +104 -0
- package/src/cli/update.js +107 -0
package/bin/writedocs.js
CHANGED
|
@@ -9,6 +9,7 @@ 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';
|
|
12
13
|
// O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
|
|
13
14
|
// que a plataforma importa por `@writedocs/generator/config-schema` - e o que
|
|
14
15
|
// faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
|
|
@@ -37,13 +38,18 @@ const packageRoot = path.resolve(__dirname, '..');
|
|
|
37
38
|
// package.json). `writedocs --version` should always reflect what actually
|
|
38
39
|
// got published, not whatever this string happened to say at the time this
|
|
39
40
|
// line was last hand-edited.
|
|
40
|
-
const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
41
|
+
const { name: packageName, version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
41
42
|
|
|
42
43
|
const program = new Command();
|
|
43
44
|
program
|
|
44
45
|
.name('writedocs')
|
|
45
46
|
.description('Static site generator for writedocs.json + MDX')
|
|
46
|
-
.version(version)
|
|
47
|
+
.version(version)
|
|
48
|
+
// Every command checks for a newer writedocs (from a cache - see
|
|
49
|
+
// src/cli/update-check.js); the notice prints after the command's output.
|
|
50
|
+
.hook('preAction', (_program, command) => {
|
|
51
|
+
startUpdateCheck({ command: command.name(), name: packageName, version, packageRoot });
|
|
52
|
+
});
|
|
47
53
|
|
|
48
54
|
program
|
|
49
55
|
.command('dev')
|
|
@@ -265,6 +271,14 @@ program
|
|
|
265
271
|
});
|
|
266
272
|
});
|
|
267
273
|
|
|
274
|
+
program
|
|
275
|
+
.command('update')
|
|
276
|
+
.description('Update writedocs to the latest version')
|
|
277
|
+
.action(async () => {
|
|
278
|
+
const { runUpdate } = await import('../src/cli/update.js');
|
|
279
|
+
await runUpdate({ name: packageName, version, packageRoot });
|
|
280
|
+
});
|
|
281
|
+
|
|
268
282
|
program
|
|
269
283
|
.command('init')
|
|
270
284
|
.description('Scaffold a writedocs.json and starter docs/ folder')
|
|
@@ -273,10 +287,13 @@ program
|
|
|
273
287
|
await runInit({ targetDir: path.resolve(process.cwd(), dir) });
|
|
274
288
|
});
|
|
275
289
|
|
|
276
|
-
program
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
290
|
+
program
|
|
291
|
+
.parseAsync(process.argv)
|
|
292
|
+
.then(showUpdateNotice)
|
|
293
|
+
.catch((err) => {
|
|
294
|
+
stopActiveStep();
|
|
295
|
+
// A command that already printed its own error throws CliExit.
|
|
296
|
+
if (!(err instanceof CliExit)) log.error(errorText(err));
|
|
297
|
+
showUpdateNotice();
|
|
298
|
+
process.exit(err instanceof CliExit ? err.code : 1);
|
|
299
|
+
});
|
package/package.json
CHANGED
package/src/cli/dev.js
CHANGED
|
@@ -5,6 +5,7 @@ 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
|
|
@@ -96,6 +97,9 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false })
|
|
|
96
97
|
log.line();
|
|
97
98
|
log.line(color.dim(' Edit any page and the preview updates. Press Ctrl+C to stop.'));
|
|
98
99
|
log.line();
|
|
100
|
+
// `dev` runs until Ctrl+C - the notice goes under the ready screen,
|
|
101
|
+
// not after the command like everywhere else.
|
|
102
|
+
showUpdateNotice();
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
case 'fatal':
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// `writedocs update` - installs the latest writedocs, the same way the running
|
|
2
|
+
// one was installed: globally with npm (the documented way), as a project
|
|
3
|
+
// dependency (npm, pnpm or yarn, by the project's lockfile), or globally
|
|
4
|
+
// with pnpm or yarn. npx and a source checkout get an explanation instead.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { log, step, color, CliExit } from './output.js';
|
|
9
|
+
import { fetchLatestVersion, isNewer, isSourceCheckout, writeCache } from './update-check.js';
|
|
10
|
+
|
|
11
|
+
/** Runs a command, capturing its output. Resolves { code, output }. */
|
|
12
|
+
function run(command, args, cwd) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
// npm/pnpm/yarn are .cmd scripts on Windows, which spawn() only runs
|
|
15
|
+
// through a shell - given as one string there (Node deprecates an
|
|
16
|
+
// argument list with `shell`). The arguments are fixed: flags and the
|
|
17
|
+
// package name, never user input.
|
|
18
|
+
const options = { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true };
|
|
19
|
+
const child = process.platform === 'win32' ? spawn([command, ...args].join(' '), { ...options, shell: true }) : spawn(command, args, options);
|
|
20
|
+
let output = '';
|
|
21
|
+
child.stdout.on('data', (d) => (output += d));
|
|
22
|
+
child.stderr.on('data', (d) => (output += d));
|
|
23
|
+
child.on('error', (err) => resolve({ code: 1, output: err.message }));
|
|
24
|
+
child.on('close', (code) => resolve({ code: code ?? 1, output }));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** How the running writedocs was installed:
|
|
29
|
+
* { kind: 'checkout' | 'npx' | 'unknown' } or
|
|
30
|
+
* { kind: 'global' | 'local', command, args, cwd }. */
|
|
31
|
+
export async function installation(packageRoot, name) {
|
|
32
|
+
if (isSourceCheckout(packageRoot)) return { kind: 'checkout' };
|
|
33
|
+
const posix = packageRoot.split(path.sep).join('/');
|
|
34
|
+
if (/\/_npx\//.test(posix)) return { kind: 'npx' };
|
|
35
|
+
if (/\/pnpm\/global\//.test(posix)) return { kind: 'global', command: 'pnpm', args: ['add', '-g', `${name}@latest`] };
|
|
36
|
+
if (/\/yarn\/global\//.test(posix)) return { kind: 'global', command: 'yarn', args: ['global', 'add', `${name}@latest`] };
|
|
37
|
+
const at = posix.lastIndexOf(`/node_modules/${name}`);
|
|
38
|
+
if (at === -1) return { kind: 'unknown' };
|
|
39
|
+
const container = posix.slice(0, at);
|
|
40
|
+
const npmRoot = await run('npm', ['root', '-g']);
|
|
41
|
+
const globalRoot = npmRoot.code === 0 ? npmRoot.output.trim().split(/\r?\n/).pop() : null;
|
|
42
|
+
if (globalRoot && path.resolve(globalRoot) === path.resolve(`${container}/node_modules`)) {
|
|
43
|
+
return { kind: 'global', command: 'npm', args: ['install', '-g', `${name}@latest`] };
|
|
44
|
+
}
|
|
45
|
+
const cwd = path.resolve(container);
|
|
46
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return { kind: 'local', command: 'pnpm', args: ['add', `${name}@latest`], cwd };
|
|
47
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return { kind: 'local', command: 'yarn', args: ['add', `${name}@latest`], cwd };
|
|
48
|
+
return { kind: 'local', command: 'npm', args: ['install', `${name}@latest`], cwd };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runUpdate({ name, version, packageRoot }) {
|
|
52
|
+
const checking = step('Checking for a new version');
|
|
53
|
+
let latest;
|
|
54
|
+
try {
|
|
55
|
+
latest = await fetchLatestVersion(name, { timeoutMs: 15000 });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
checking.fail("Couldn't check for a new version");
|
|
58
|
+
log.detail(color.dim(`${err.message}. Check your connection, and try again.`));
|
|
59
|
+
throw new CliExit(1);
|
|
60
|
+
}
|
|
61
|
+
checking.stop();
|
|
62
|
+
try {
|
|
63
|
+
writeCache({ name, latest, checkedAt: Date.now() });
|
|
64
|
+
} catch {}
|
|
65
|
+
if (!isNewer(latest, version)) {
|
|
66
|
+
log.success(`writedocs is up to date ${color.dim(`(${version})`)}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const install = await installation(packageRoot, name);
|
|
71
|
+
if (install.kind === 'checkout') {
|
|
72
|
+
log.info(`writedocs ${latest} is available, but this one runs from a source checkout (${packageRoot}).`);
|
|
73
|
+
log.detail(color.dim('Update it with git - `writedocs update` only updates installed copies.'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (install.kind === 'npx') {
|
|
77
|
+
log.info(`writedocs ${latest} is available. You're running writedocs through npx - to use the latest, run:`);
|
|
78
|
+
log.detail(color.cyan(`npx ${name}@latest <command>`));
|
|
79
|
+
log.detail(color.dim(`Or install it once, and use \`writedocs\` directly: npm install -g ${name}`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (install.kind === 'unknown') {
|
|
83
|
+
log.info(`writedocs ${latest} is available. Update it the way you installed it - for example:`);
|
|
84
|
+
log.detail(color.cyan(`npm install -g ${name}@latest`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const shown = `${install.command} ${install.args.join(' ')}`;
|
|
89
|
+
const updating = step(`Updating writedocs ${version} → ${latest} ${color.dim(`(${shown}${install.cwd ? ` in ${install.cwd}` : ''})`)}`);
|
|
90
|
+
const result = await run(install.command, install.args, install.cwd);
|
|
91
|
+
if (result.code !== 0) {
|
|
92
|
+
updating.fail(`Couldn't update writedocs - \`${shown}\` failed`);
|
|
93
|
+
const tail = result.output.trim().split(/\r?\n/).slice(-12).join('\n');
|
|
94
|
+
if (tail) {
|
|
95
|
+
log.line();
|
|
96
|
+
log.line(color.dim(tail));
|
|
97
|
+
}
|
|
98
|
+
if (/EACCES|permission denied/i.test(result.output)) {
|
|
99
|
+
log.line();
|
|
100
|
+
log.detail(
|
|
101
|
+
`npm needs permission to write to its global folder. Run ${color.cyan(`sudo ${shown}`)}, or set npm up to install without sudo: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
throw new CliExit(1);
|
|
105
|
+
}
|
|
106
|
+
updating.succeed(`Updated writedocs ${version} → ${color.bold(latest)}`);
|
|
107
|
+
}
|