@writedocs/generator 0.10.0 → 0.10.1
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 +3 -0
- package/package.json +1 -1
- package/src/cli/dev.js +72 -10
- package/src/cli/page-list-watch.js +66 -0
- package/src/lib/config-reload-integration.js +32 -0
package/astro.config.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { remarkExtractInlineReactComponents } from './src/lib/mdx-inline-react.j
|
|
|
37
37
|
import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
|
|
38
38
|
import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
|
|
39
39
|
import { mcpDevServer } from './src/lib/mcp-dev-integration.js';
|
|
40
|
+
import { configReload } from './src/lib/config-reload-integration.js';
|
|
40
41
|
import { canonicalPath } from './src/lib/canonical-path.js';
|
|
41
42
|
import { folderAddresses } from './src/lib/folder-redirects.js';
|
|
42
43
|
import { report } from './src/lib/cli-report.js';
|
|
@@ -324,6 +325,8 @@ export default defineConfig({
|
|
|
324
325
|
// /mcp in `writedocs dev`, as the build's dist/_worker.js serves it -
|
|
325
326
|
// see src/lib/mcp-dev-integration.js.
|
|
326
327
|
mcpDevServer({ version: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).version }),
|
|
328
|
+
// writedocs dev: an edit to writedocs.json reloads the open tab.
|
|
329
|
+
configReload({ configPath: path.join(contentDir, 'writedocs.json') }),
|
|
327
330
|
],
|
|
328
331
|
output: 'static',
|
|
329
332
|
// Dual Shiki themes for fenced code blocks (```) in MDX content, so
|
package/package.json
CHANGED
package/src/cli/dev.js
CHANGED
|
@@ -8,6 +8,7 @@ import { reportApiPages } from './api-pages-output.js';
|
|
|
8
8
|
import { runningPreview, writeLock, removeLock } from './dev-lock.js';
|
|
9
9
|
import { showUpdateNotice } from './update-check.js';
|
|
10
10
|
import { shouldOpenBrowser, openBrowser } from './open-browser.js';
|
|
11
|
+
import { watchPageList } from './page-list-watch.js';
|
|
11
12
|
|
|
12
13
|
// The same problem tends to arrive more than once in a row - Vite and
|
|
13
14
|
// Astro each log a failed page, and a page compiles for more than one
|
|
@@ -39,9 +40,13 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false, o
|
|
|
39
40
|
reportApiPages(api);
|
|
40
41
|
api.warnings.forEach((message) => log.warn(message));
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
let starting = step('Starting local preview');
|
|
43
44
|
let ready = false;
|
|
44
45
|
let browserOpened = false;
|
|
46
|
+
// Restarting after pages were added or removed (see the watcher below):
|
|
47
|
+
// when that restart began, and the port to come back on.
|
|
48
|
+
let restartStarted = null;
|
|
49
|
+
let currentPort = port;
|
|
45
50
|
|
|
46
51
|
const lastShown = new Map();
|
|
47
52
|
const once = (key, print) => {
|
|
@@ -82,11 +87,7 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false, o
|
|
|
82
87
|
|
|
83
88
|
let fatal = null;
|
|
84
89
|
const rawTail = [];
|
|
85
|
-
const
|
|
86
|
-
packageRoot,
|
|
87
|
-
contentDir,
|
|
88
|
-
port,
|
|
89
|
-
onEvent(event) {
|
|
90
|
+
const onEvent = (event) => {
|
|
90
91
|
if (verbose) {
|
|
91
92
|
const line = verboseLine(event);
|
|
92
93
|
if (line !== null) log.line(color.dim(line));
|
|
@@ -94,11 +95,24 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false, o
|
|
|
94
95
|
switch (event.type) {
|
|
95
96
|
case 'ready': {
|
|
96
97
|
ready = true;
|
|
97
|
-
starting.succeed(`Preview ready ${color.dim(`in ${duration(Date.now() - started)}`)}`);
|
|
98
98
|
const [local] = event.urls?.local ?? [];
|
|
99
99
|
writeLock(packageRoot, { contentDir, url: local ?? null });
|
|
100
100
|
const wanted = Number(port ?? 4321);
|
|
101
101
|
const actual = local ? Number(new URL(local).port) : wanted;
|
|
102
|
+
currentPort = actual;
|
|
103
|
+
// Pages changed while it was starting: restart now that it's up.
|
|
104
|
+
if (restartPending) {
|
|
105
|
+
const change = restartPending;
|
|
106
|
+
restartPending = false;
|
|
107
|
+
setTimeout(() => restartForPages(change), 0);
|
|
108
|
+
}
|
|
109
|
+
// Back after a restart: one line - the open tab reloads by itself.
|
|
110
|
+
if (restartStarted !== null) {
|
|
111
|
+
starting.succeed(`Preview updated ${color.dim(`in ${duration(Date.now() - restartStarted)}`)}`);
|
|
112
|
+
restartStarted = null;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
starting.succeed(`Preview ready ${color.dim(`in ${duration(Date.now() - started)}`)}`);
|
|
102
116
|
log.line();
|
|
103
117
|
log.line(` ${color.bold('Local:')} ${color.cyan(local ?? `http://localhost:${wanted}/`)}`);
|
|
104
118
|
if (actual !== wanted) log.line(color.dim(` Port ${wanted} was in use, so the preview is on ${actual}.`));
|
|
@@ -129,6 +143,14 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false, o
|
|
|
129
143
|
if (rawTail.length > 40) rawTail.shift();
|
|
130
144
|
return;
|
|
131
145
|
case 'astro-log': {
|
|
146
|
+
// A page file just deleted: Astro's list of page modules
|
|
147
|
+
// (.astro/content-modules.mjs) still points at it for a moment -
|
|
148
|
+
// "Failed to load url ... Does the file exist?" - until the restart
|
|
149
|
+
// the page-list watcher starts brings in a fresh list. Noise, not a
|
|
150
|
+
// problem in the site. Every other error still shows, and a
|
|
151
|
+
// preview that can't start still reports through 'fatal'.
|
|
152
|
+
if (event.level === 'error' && /content-modules\.mjs/.test(event.message) && /Does the file exist/.test(event.message)) return;
|
|
153
|
+
if (restartStarted !== null && event.level === 'error') return;
|
|
132
154
|
if (event.level === 'error') {
|
|
133
155
|
showError(describeError(stripAnsi(event.message), contentDir, { root: packageRoot }));
|
|
134
156
|
return;
|
|
@@ -145,7 +167,36 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false, o
|
|
|
145
167
|
return;
|
|
146
168
|
}
|
|
147
169
|
}
|
|
148
|
-
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Pages added or removed while the preview runs: its page list is read
|
|
173
|
+
// when it starts, so it restarts - on the same port; the open tab
|
|
174
|
+
// reconnects and reloads by itself. Edits to pages and to writedocs.json
|
|
175
|
+
// don't need this (lib/config-reload-integration.js).
|
|
176
|
+
let child = null;
|
|
177
|
+
let restarting = false;
|
|
178
|
+
let restartPending = false;
|
|
179
|
+
const restartForPages = ({ added, removed }) => {
|
|
180
|
+
const what = [
|
|
181
|
+
...added.slice(0, 3).map((file) => `${file} added`),
|
|
182
|
+
...removed.slice(0, 3).map((file) => `${file} removed`),
|
|
183
|
+
];
|
|
184
|
+
const more = added.length + removed.length - what.length;
|
|
185
|
+
log.info(`${what.join(', ')}${more > 0 ? ` and ${more} more` : ''} - restarting the preview so the page list is current.`);
|
|
186
|
+
restarting = true;
|
|
187
|
+
ready = false;
|
|
188
|
+
restartStarted = Date.now();
|
|
189
|
+
starting = step('Restarting the preview');
|
|
190
|
+
child?.kill();
|
|
191
|
+
};
|
|
192
|
+
const pageWatch = watchPageList(contentDir, (change) => {
|
|
193
|
+
if (stopping || restarting) return;
|
|
194
|
+
// Still starting: restart once it's up, not halfway.
|
|
195
|
+
if (!ready) {
|
|
196
|
+
restartPending = change;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
restartForPages(change);
|
|
149
200
|
});
|
|
150
201
|
|
|
151
202
|
// Ctrl+C: stop the server quietly. (The child gets the same signal from
|
|
@@ -155,12 +206,23 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false, o
|
|
|
155
206
|
const stop = () => {
|
|
156
207
|
if (stopping) process.exit(130);
|
|
157
208
|
stopping = true;
|
|
158
|
-
|
|
209
|
+
pageWatch.close();
|
|
210
|
+
child?.kill();
|
|
159
211
|
};
|
|
160
212
|
process.on('SIGINT', stop);
|
|
161
213
|
process.on('SIGTERM', stop);
|
|
162
214
|
|
|
163
|
-
|
|
215
|
+
let code;
|
|
216
|
+
for (;;) {
|
|
217
|
+
fatal = null;
|
|
218
|
+
rawTail.length = 0;
|
|
219
|
+
const run = runAstro('dev', { packageRoot, contentDir, port: currentPort, onEvent });
|
|
220
|
+
child = run.child;
|
|
221
|
+
code = await run.exited;
|
|
222
|
+
if (stopping || !restarting) break;
|
|
223
|
+
restarting = false;
|
|
224
|
+
}
|
|
225
|
+
pageWatch.close();
|
|
164
226
|
if (stopping) {
|
|
165
227
|
if (ready) log.line(color.dim('Preview stopped.'));
|
|
166
228
|
return;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `writedocs dev`: noticing pages added or removed. The preview's list of page
|
|
2
|
+
// files is read once, when it starts (content.config.ts hands findAllPages()'s
|
|
3
|
+
// list to Astro) - an edit to a page shows up at once, but a new page stays
|
|
4
|
+
// "not found" and a deleted one keeps being served. So this watches the
|
|
5
|
+
// content folder, and when the list findAllPages() gives changes, calls
|
|
6
|
+
// `onChange` - cli/dev.js restarts the preview then.
|
|
7
|
+
//
|
|
8
|
+
// It compares the list itself, not file events: a Markdown file that isn't
|
|
9
|
+
// a page (no frontmatter, not in the navigation), an edit to a page, or a
|
|
10
|
+
// save that writes a file twice doesn't restart anything.
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { findAllPages, EXCLUDED_TOP_LEVEL_DIRS } from '../lib/pages.js';
|
|
13
|
+
|
|
14
|
+
/** Wait this long after the last file event before checking - a rename, a
|
|
15
|
+
* copy of several files or an editor's save arrive as a burst of events. */
|
|
16
|
+
export const SETTLE_MS = 400;
|
|
17
|
+
|
|
18
|
+
/** Whether a changed file can change the page list: a .md/.mdx file, or
|
|
19
|
+
* writedocs.json (it lists pages that have no frontmatter), outside the
|
|
20
|
+
* folders findAllPages() never looks in. */
|
|
21
|
+
export function mayChangePages(relative) {
|
|
22
|
+
if (!relative) return true;
|
|
23
|
+
const parts = String(relative).split(/[\\/]/);
|
|
24
|
+
if (EXCLUDED_TOP_LEVEL_DIRS.has(parts[0]) || parts.some((part) => part.startsWith('.'))) return false;
|
|
25
|
+
const name = parts[parts.length - 1];
|
|
26
|
+
return /\.mdx?$/i.test(name) || name === 'writedocs.json';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Starts watching; returns `{ close }`. `onChange({ added, removed })` gets
|
|
30
|
+
* the page files (relative paths) that came and went. */
|
|
31
|
+
export function watchPageList(contentDir, onChange, { settleMs = SETTLE_MS } = {}) {
|
|
32
|
+
let current = new Set(findAllPages(contentDir));
|
|
33
|
+
let timer = null;
|
|
34
|
+
const check = () => {
|
|
35
|
+
let next;
|
|
36
|
+
try {
|
|
37
|
+
next = new Set(findAllPages(contentDir));
|
|
38
|
+
} catch {
|
|
39
|
+
return; // mid-save, or a folder being moved - the next event checks again
|
|
40
|
+
}
|
|
41
|
+
const added = [...next].filter((file) => !current.has(file));
|
|
42
|
+
const removed = [...current].filter((file) => !next.has(file));
|
|
43
|
+
if (!added.length && !removed.length) return;
|
|
44
|
+
current = next;
|
|
45
|
+
onChange({ added, removed });
|
|
46
|
+
};
|
|
47
|
+
let watcher;
|
|
48
|
+
try {
|
|
49
|
+
watcher = fs.watch(contentDir, { recursive: true }, (_event, filename) => {
|
|
50
|
+
if (!mayChangePages(filename)) return;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
timer = setTimeout(check, settleMs);
|
|
53
|
+
});
|
|
54
|
+
watcher.on('error', () => {});
|
|
55
|
+
} catch {
|
|
56
|
+
// A file system without recursive watching: pages added while `dev`
|
|
57
|
+
// runs need a restart, as before.
|
|
58
|
+
return { close() {} };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
close() {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
watcher.close();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// `writedocs dev`: an edit to writedocs.json reloads the open tab. The server
|
|
2
|
+
// already renders with the new file on the next request (lib/config.ts reads
|
|
3
|
+
// it each time), but nothing told the browser - the tab kept the old
|
|
4
|
+
// navigation until reloaded by hand. writedocs.json sits in the content
|
|
5
|
+
// folder, outside what Vite watches, so it's added to the watcher here, and
|
|
6
|
+
// a change sends Vite's full-reload.
|
|
7
|
+
//
|
|
8
|
+
// Pages added or removed need more than a reload - see cli/page-list-watch.js.
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/** Wait this long after the last change before reloading - an editor's save
|
|
12
|
+
* can arrive as more than one write. */
|
|
13
|
+
export const RELOAD_DELAY_MS = 150;
|
|
14
|
+
|
|
15
|
+
export function configReload({ configPath }) {
|
|
16
|
+
return {
|
|
17
|
+
name: 'writedocs-config-reload',
|
|
18
|
+
hooks: {
|
|
19
|
+
'astro:server:setup': ({ server }) => {
|
|
20
|
+
const target = path.resolve(configPath);
|
|
21
|
+
const same = (file) => path.resolve(file).toLowerCase() === target.toLowerCase();
|
|
22
|
+
server.watcher.add(target);
|
|
23
|
+
let timer = null;
|
|
24
|
+
server.watcher.on('change', (file) => {
|
|
25
|
+
if (!same(file)) return;
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = setTimeout(() => server.ws.send({ type: 'full-reload', path: '*' }), RELOAD_DELAY_MS);
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|