@writedocs/generator 0.4.11 → 0.5.0

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
+ }
@@ -9,6 +9,7 @@
9
9
  // - an .mdx file doesn't parse as MDX
10
10
  // - writedocs.json's navigation lists a page that doesn't exist
11
11
  // - a redirect is a pattern (`/old/:slug`) rather than one exact path
12
+ // - a Markdown image with a relative path names a file that doesn't exist
12
13
  // Warnings - the build succeeds, but not as written:
13
14
  // - an unknown component (the build shows only its content - see
14
15
  // lib/mdx-unknown-components.js)
@@ -118,6 +119,28 @@ async function checkPage(contentDir, rel, errors, warnings) {
118
119
  );
119
120
  return;
120
121
  }
122
+ // A Markdown image with a relative path is imported by the build (Astro's
123
+ // image pipeline), so a missing file fails the whole build.
124
+ visit(tree, 'image', (node) => {
125
+ const url = String(node.url ?? '');
126
+ if (!url || url.startsWith('/') || url.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//')) return;
127
+ let target;
128
+ try {
129
+ target = path.resolve(path.dirname(path.join(contentDir, rel)), decodeURI(url.split(/[?#]/)[0]));
130
+ } catch {
131
+ return;
132
+ }
133
+ if (fs.existsSync(target)) return;
134
+ const line = node.position?.start?.line;
135
+ errors.push(
136
+ issue(
137
+ rel,
138
+ line ? line + lineOffset : undefined,
139
+ `Image ${url} doesn't exist - the build fails on it.`,
140
+ 'The path is relative to this page\'s folder. Fix it, or use a path from the project root, like "/images/example.png".'
141
+ )
142
+ );
143
+ });
121
144
  for (const { name, line } of findUnknownComponents(tree)) {
122
145
  warnings.push(
123
146
  issue(