@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.
@@ -1,57 +1,87 @@
1
- import { createRequire } from 'node:module';
2
1
  import path from 'node:path';
3
2
  import { spawn } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const workerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'astro-worker.js');
4
6
 
5
7
  /**
6
- * Resolves the astro CLI entrypoint relative to this package (not the
7
- * consumer's project), so it works regardless of how writedocs was
8
- * installed (flat or nested node_modules).
8
+ * Runs Astro (`command`: 'dev' or 'build') in a child process -
9
+ * astro-worker.js - and turns everything it reports into events for
10
+ * `onEvent`, so the CLI decides what reaches the terminal:
11
+ *
12
+ * { type: 'astro-log', level, label, message } - an Astro/Vite log line
13
+ * { type: 'report', level, message, where } - writedocs' own code
14
+ * (lib/cli-report.js)
15
+ * { type: 'raw', stream, line } - anything printed directly
16
+ * { type: 'ready', urls } / { type: 'done' } / { type: 'fatal', error }
17
+ *
18
+ * A raw "[writedocs] ..." line is turned into a 'report' too, so a
19
+ * console.warn that doesn't go through cli-report.js still shows up.
20
+ *
21
+ * Returns { child, exited } - `exited` resolves with the exit code.
9
22
  */
10
- function resolveAstroBin(packageRoot) {
11
- const require = createRequire(path.join(packageRoot, 'package.json'));
12
- const astroPkgPath = require.resolve('astro/package.json');
13
- const astroPkg = require(astroPkgPath);
14
- return path.join(path.dirname(astroPkgPath), astroPkg.bin.astro);
15
- }
23
+ export function runAstro(command, { packageRoot, contentDir, port, onEvent }) {
24
+ const child = spawn(process.execPath, [workerPath, command], {
25
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
26
+ // Explicit, not inherited: astro.config.mjs's own `outDir` lives
27
+ // inside packageRoot (writedocsBuildStagingDir() - see its own
28
+ // comment in writedocs-temp-dir.js for the full EXDEV story this is
29
+ // one half of), and Astro's static builder only stages its
30
+ // prerendered output *inside* that outDir - rather than falling
31
+ // back to `<process.cwd()>/.astro/.prerender/` - when outDir starts
32
+ // with process.cwd() (getOutDirWithinCwd, astro/dist/core/build/
33
+ // common.js). Left unset, this child would just inherit whatever
34
+ // directory the *parent* writedocs CLI process happened to be
35
+ // launched from - unrelated to packageRoot for a real globally-
36
+ // installed CLI invoked from wherever the user's shell happens to
37
+ // be - so the fallback branch would fire regardless, staging
38
+ // outside packageRoot again and reintroducing the exact module-
39
+ // resolution problem this whole design is meant to avoid. Pinning
40
+ // cwd to packageRoot here guarantees the "starts with cwd" check
41
+ // passes deterministically, independent of the parent process's own
42
+ // cwd.
43
+ cwd: packageRoot,
44
+ env: {
45
+ ...process.env,
46
+ WRITEDOCS_CONTENT_DIR: contentDir,
47
+ // Astro is always run with root: packageRoot (astro-worker.js), so
48
+ // every CollectionEntry's `filePath` comes back relative to *this*,
49
+ // not to contentDir or the process's cwd. Exposed so lib/config.ts's
50
+ // fileIdForEntry() can resolve it back into a docs/-relative file id
51
+ // - see that function for why.
52
+ WRITEDOCS_PACKAGE_ROOT: packageRoot,
53
+ ...(port ? { WRITEDOCS_PORT: String(port) } : {}),
54
+ },
55
+ });
16
56
 
17
- export function runAstro(args, { packageRoot, contentDir }) {
18
- const astroBin = resolveAstroBin(packageRoot);
19
- return new Promise((resolve, reject) => {
20
- const child = spawn(process.execPath, [astroBin, ...args], {
21
- stdio: 'inherit',
22
- // Explicit, not inherited: astro.config.mjs's own `outDir` lives
23
- // inside packageRoot (writedocsBuildStagingDir() - see its own
24
- // comment in writedocs-temp-dir.js for the full EXDEV story this is
25
- // one half of), and Astro's static builder only stages its
26
- // prerendered output *inside* that outDir - rather than falling
27
- // back to `<process.cwd()>/.astro/.prerender/` - when outDir starts
28
- // with process.cwd() (getOutDirWithinCwd, astro/dist/core/build/
29
- // common.js). Left unset, this child would just inherit whatever
30
- // directory the *parent* writedocs CLI process happened to be
31
- // launched from - unrelated to packageRoot for a real globally-
32
- // installed CLI invoked from wherever the user's shell happens to
33
- // be - so the fallback branch would fire regardless, staging
34
- // outside packageRoot again and reintroducing the exact module-
35
- // resolution problem this whole design is meant to avoid. Pinning
36
- // cwd to packageRoot here guarantees the "starts with cwd" check
37
- // passes deterministically, independent of the parent process's own
38
- // cwd.
39
- cwd: packageRoot,
40
- env: {
41
- ...process.env,
42
- WRITEDOCS_CONTENT_DIR: contentDir,
43
- // Astro is always invoked with `--root packageRoot` (see build.js/
44
- // dev.js), so every CollectionEntry's `filePath` comes back
45
- // relative to *this*, not to contentDir or the process's cwd.
46
- // Exposed so lib/config.ts's fileIdForEntry() can resolve it back
47
- // into a docs/-relative file id - see that function for why.
48
- WRITEDOCS_PACKAGE_ROOT: packageRoot,
49
- },
57
+ child.on('message', (message) => {
58
+ if (message && typeof message === 'object' && typeof message.type === 'string') onEvent(message);
59
+ });
60
+
61
+ for (const streamName of ['stdout', 'stderr']) {
62
+ let pending = '';
63
+ const emit = (line) => {
64
+ if (line.trim() === '') return;
65
+ const own = line.match(/^\[writedocs\]\s+(?:(\S+:\d+(?::\d+)?) - )?(.*)$/);
66
+ if (own) onEvent({ type: 'report', level: streamName === 'stderr' ? 'warn' : 'info', message: own[2], where: own[1] ?? null });
67
+ else onEvent({ type: 'raw', stream: streamName, line });
68
+ };
69
+ child[streamName].setEncoding('utf8');
70
+ child[streamName].on('data', (chunk) => {
71
+ pending += chunk;
72
+ const lines = pending.split(/\r?\n/);
73
+ pending = lines.pop();
74
+ lines.forEach(emit);
50
75
  });
51
- child.on('exit', (code) => {
52
- if (code === 0) resolve();
53
- else reject(new Error(`astro ${args[0]} exited with code ${code}`));
76
+ child[streamName].on('end', () => {
77
+ if (pending) emit(pending);
78
+ pending = '';
54
79
  });
80
+ }
81
+
82
+ const exited = new Promise((resolve, reject) => {
55
83
  child.on('error', reject);
84
+ child.on('close', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
56
85
  });
86
+ return { child, exited };
57
87
  }
@@ -7,12 +7,11 @@ import { spawn } from 'node:child_process';
7
7
  * directories from packageRoot - the same lookup algorithm Node itself
8
8
  * uses for bare-specifier resolution, just done by hand with fs instead
9
9
  * of require.resolve()/import.meta.resolve(). This is necessary (rather
10
- * than mirroring resolveAstroBin()'s simpler require.resolve()-based
11
- * approach in run-astro.js) because pagefind's own package.json declares
12
- * an "exports" map with only a "." entry scoped to the "import"
13
- * condition: require.resolve('pagefind/package.json') is blocked
14
- * outright (exports doesn't list a package.json subpath at all), and
15
- * require.resolve('pagefind') also fails under CJS require() semantics
10
+ * than a require.resolve()-based lookup) because pagefind's own
11
+ * package.json declares an "exports" map with only a "." entry scoped to
12
+ * the "import" condition: require.resolve('pagefind/package.json') is
13
+ * blocked outright (exports doesn't list a package.json subpath at all),
14
+ * and require.resolve('pagefind') also fails under CJS require() semantics
16
15
  * (no "require"/"default" condition for it to satisfy).
17
16
  * import.meta.resolve() would sidestep both, but needs Node 20.6+ -
18
17
  * newer than this package's documented minimum (20.3.0, see engines in
@@ -50,16 +49,22 @@ function resolvePagefindBin(packageRoot) {
50
49
  * `.wd-article`) - without that scoping, Pagefind falls back to indexing
51
50
  * every page's entire <body>, polluting every result with sidebar/topbar
52
51
  * chrome text repeated on every page.
52
+ *
53
+ * Pagefind's output is captured, not printed: the CLI prints its own step
54
+ * line, and Pagefind's text only when it fails (in the rejected Error).
53
55
  */
54
56
  export function runPagefind(siteDir, { packageRoot }) {
55
57
  const bin = resolvePagefindBin(packageRoot);
56
58
  return new Promise((resolve, reject) => {
57
59
  const child = spawn(process.execPath, [bin, '--site', siteDir, '--quiet'], {
58
- stdio: 'inherit',
60
+ stdio: ['ignore', 'pipe', 'pipe'],
59
61
  });
60
- child.on('exit', (code) => {
62
+ let output = '';
63
+ child.stdout.on('data', (d) => (output += d));
64
+ child.stderr.on('data', (d) => (output += d));
65
+ child.on('close', (code) => {
61
66
  if (code === 0) resolve();
62
- else reject(new Error(`pagefind exited with code ${code}`));
67
+ else reject(new Error(`pagefind exited with code ${code}${output.trim() ? `:\n${output.trim()}` : ''}`));
63
68
  });
64
69
  child.on('error', reject);
65
70
  });
@@ -8,6 +8,7 @@
8
8
  // don't each need their own copy of the resolveIcon() call + branch.
9
9
  import { Icon } from 'astro-icon/components';
10
10
  import { resolveIcon, iconExists } from '../lib/config';
11
+ import { report } from '../lib/cli-report.js';
11
12
 
12
13
  interface Props {
13
14
  icon?: string;
@@ -30,7 +31,7 @@ if (icon && resolved?.kind === 'iconify' && !iconExists(icon)) {
30
31
  const warned: Set<string> = ((globalThis as any).__wdWarnedIcons ??= new Set());
31
32
  if (!warned.has(icon)) {
32
33
  warned.add(icon);
33
- console.warn(`[writedocs] unknown icon "${icon}" - no installed icon set has it, so it's left out.`);
34
+ report('warn', `Unknown icon "${icon}" - no installed icon set has it, so it's left out.`);
34
35
  }
35
36
  resolved = null;
36
37
  }
@@ -0,0 +1,25 @@
1
+ // Astro logger destination for the writedocs CLI (Astro's `logger` config -
2
+ // see src/cli/astro-worker.js, which sets it). Every message Astro and Vite
3
+ // log - build steps, route lists, dev request logs, warnings, errors - comes
4
+ // through write() as a { level, label, message } event; instead of printing
5
+ // it, this hands the event to the writedocs CLI process over IPC, and the CLI
6
+ // (src/cli/output.js) decides what the author actually sees.
7
+ //
8
+ // Plain JS with no imports: Astro also bundles this module into the build's
9
+ // own server code (virtual:astro:logger), and it has to load there too.
10
+ // Outside the CLI - no IPC channel - it falls back to printing the message,
11
+ // so nothing is ever lost silently.
12
+ export default function writedocsLogDestination() {
13
+ return {
14
+ write(event) {
15
+ const message = String(event.message ?? '');
16
+ if (typeof process !== 'undefined' && typeof process.send === 'function' && process.connected) {
17
+ process.send({ type: 'astro-log', level: event.level, label: event.label ?? null, message });
18
+ return;
19
+ }
20
+ const line = event.label && event.label !== 'SKIP_FORMAT' ? `[${event.label}] ${message}` : message;
21
+ if (event.level === 'error' || event.level === 'warn') console.error(line);
22
+ else console.log(line);
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,20 @@
1
+ // How writedocs code that runs inside Astro - astro.config.mjs, remark
2
+ // plugins, components - tells the author something. Under the writedocs CLI
3
+ // the message goes over IPC to the CLI process (src/cli/run-astro.js), which
4
+ // prints it in its own format: warnings are listed at the end of a build,
5
+ // or as they happen in `writedocs dev`. Anywhere else (Astro run directly,
6
+ // a test) it's printed with a [writedocs] prefix.
7
+ //
8
+ // level: 'warn' - something the author should fix or know about;
9
+ // 'info' - a note about how the site was built (shown after a build);
10
+ // 'debug' - only with --verbose.
11
+ // where: optional "file:line" the message is about, relative to the project.
12
+ export function report(level, message, where) {
13
+ if (typeof process !== 'undefined' && typeof process.send === 'function' && process.connected) {
14
+ process.send({ type: 'report', level, message, where: where ?? null });
15
+ return;
16
+ }
17
+ const text = `[writedocs] ${where ? `${where} - ` : ''}${message}`;
18
+ if (level === 'warn') console.warn(text);
19
+ else console.log(text);
20
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import crypto from 'node:crypto';
4
4
  import { parse as acornParse } from 'acorn';
5
+ import { report } from './cli-report.js';
5
6
  import { writedocsTempDir } from './writedocs-temp-dir.js';
6
7
  import { MINTLIFY_HOOKS } from './mdx-mintlify.js';
7
8
  import { BUILTIN_COMPONENT_NAMES } from './mdx-inject-builtins.js';
@@ -137,7 +138,7 @@ export function remarkExtractInlineReactComponents() {
137
138
  const relPath = path.relative(contentDir, file.path).split(path.sep).join('/');
138
139
  for (const use of simplified) {
139
140
  const line = source.slice(0, use.offset).split('\n').length;
140
- console.warn(`[writedocs] ${relPath}:${line} - ${simplifiedBuiltinMessage(use).message}`);
141
+ report('warn', simplifiedBuiltinMessage(use).message, `${relPath}:${line}`);
141
142
  }
142
143
 
143
144
  const pageDir = path.dirname(file.path);
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { visit } from 'unist-util-visit';
3
3
  import { parse as acornParse } from 'acorn';
4
4
  import { BUILTIN_COMPONENT_NAMES } from './mdx-inject-builtins.js';
5
+ import { report } from './cli-report.js';
5
6
 
6
7
  // Which JSX elements in an MDX file are components writedocs can't resolve
7
8
  // - neither a built-in nor something the file imports or defines itself.
@@ -120,9 +121,10 @@ export function remarkUnknownComponentFallback() {
120
121
  : '(unknown file)';
121
122
  const stubs = new Set();
122
123
  for (const { node, name, line, inCode, root } of found) {
123
- console.warn(
124
- `[writedocs] ${filePath}${line ? `:${line}` : ''} - unknown component <${name}>, showing only its content. ` +
125
- 'Remove it, or define it in a snippet.'
124
+ report(
125
+ 'warn',
126
+ `Unknown component <${name}> - showing only its content. Remove it, or define it in a snippet.`,
127
+ `${filePath}${line ? `:${line}` : ''}`
126
128
  );
127
129
  if (inCode) {
128
130
  stubs.add(root);