@rse/specbook 1.2.7 → 1.2.8
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/README.md +2 -0
- package/dst/specbook-cmd-export.js +44 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ SpecBook
|
|
|
6
6
|
|
|
7
7
|
**Markdown-based Specification Format**
|
|
8
8
|
|
|
9
|
+
[specbook.tools](https://specbook.tools)
|
|
10
|
+
|
|
9
11
|
[](https://github.com/rse)
|
|
10
12
|
[](https://github.com/rse)
|
|
11
13
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
6
7
|
/* Chokidar is aliased in "package.json", as Nunjucks declares the
|
|
7
8
|
(unused) Chokidar 3 as an optional peer dependency and the current
|
|
8
9
|
Chokidar under its plain name would raise a peer warning on install */
|
|
@@ -56,16 +57,38 @@ export const parseOutputSpec = (spec) => {
|
|
|
56
57
|
/* the quiet period the observed files have to stay silent for before a
|
|
57
58
|
burst of changes is coalesced into a single re-export */
|
|
58
59
|
const watchDelay = 1000;
|
|
60
|
+
/* the interval of the safety-net scan, which catches the changes the
|
|
61
|
+
file-system watcher missed: a watcher can die unnoticed -- macOS
|
|
62
|
+
tears down its FSEvents streams across a sleep/wake cycle -- and
|
|
63
|
+
would then leave the export stale for the rest of the process life */
|
|
64
|
+
const scanDelay = 30 * 1000;
|
|
65
|
+
/* take the modification time snapshot of the observed files, where an
|
|
66
|
+
absent file (a still missing artifact) is remembered as absent, too,
|
|
67
|
+
so its creation and removal are noticed like a modification */
|
|
68
|
+
const snapshot = (files) => {
|
|
69
|
+
const times = new Map();
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
try {
|
|
72
|
+
times.set(file, fs.statSync(file).mtimeMs);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
times.set(file, 0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return times;
|
|
79
|
+
};
|
|
59
80
|
/* keep a specification export in sync with its sources, where "run"
|
|
60
81
|
performs one export and returns the files to observe: the initial
|
|
61
82
|
export is performed up-front and every observed change triggers a
|
|
62
83
|
re-export once the sources fell silent again. The observed set is
|
|
63
84
|
re-synchronized after every run, as an edit can add or drop an
|
|
64
85
|
embedded asset. The returned promise settles once the initial export
|
|
65
|
-
is done, while the active watcher
|
|
86
|
+
is done, while the active watcher and the safety-net scan keep the
|
|
87
|
+
process alive afterwards */
|
|
66
88
|
export const watchSpecification = async (run, verbose) => {
|
|
67
89
|
/* perform the regular export before entering the observe loop */
|
|
68
90
|
let observed = await run();
|
|
91
|
+
let times = snapshot(observed);
|
|
69
92
|
const watcher = watch(observed, { ignoreInitial: true });
|
|
70
93
|
verbose(`observing ${literal(observed.length)} specification file(s) for changes`);
|
|
71
94
|
/* report the failures of the observation itself, as an unhandled
|
|
@@ -80,6 +103,7 @@ export const watchSpecification = async (run, verbose) => {
|
|
|
80
103
|
watcher.unwatch(observed.filter((file) => !files.includes(file)));
|
|
81
104
|
watcher.add(files.filter((file) => !observed.includes(file)));
|
|
82
105
|
observed = files;
|
|
106
|
+
times = snapshot(files);
|
|
83
107
|
};
|
|
84
108
|
/* restart the quiet period on every change, so a burst collapses
|
|
85
109
|
into a single re-export, and chain the runs onto each other, so a
|
|
@@ -88,7 +112,7 @@ export const watchSpecification = async (run, verbose) => {
|
|
|
88
112
|
and hence silently ending the observe loop) */
|
|
89
113
|
let chain = Promise.resolve();
|
|
90
114
|
let timer;
|
|
91
|
-
|
|
115
|
+
const schedule = () => {
|
|
92
116
|
if (timer !== undefined)
|
|
93
117
|
clearTimeout(timer);
|
|
94
118
|
timer = setTimeout(() => {
|
|
@@ -98,7 +122,24 @@ export const watchSpecification = async (run, verbose) => {
|
|
|
98
122
|
(err instanceof Error ? err.message : String(err)), "none");
|
|
99
123
|
});
|
|
100
124
|
}, watchDelay);
|
|
101
|
-
}
|
|
125
|
+
};
|
|
126
|
+
watcher.on("all", schedule);
|
|
127
|
+
/* compare the modification times regularly, so a change the
|
|
128
|
+
watcher never reported still reaches the export, and re-establish
|
|
129
|
+
the watchers on such a miss, as a watcher which lost a file never
|
|
130
|
+
regains it on its own and the following changes would be caught
|
|
131
|
+
by the scan only */
|
|
132
|
+
setInterval(() => {
|
|
133
|
+
for (const [file, time] of snapshot(observed))
|
|
134
|
+
if (times.get(file) !== time) {
|
|
135
|
+
verbose(`observing missed the change of "${literal(file)}" ` +
|
|
136
|
+
"-- re-establishing the file observation", "none");
|
|
137
|
+
watcher.unwatch(observed);
|
|
138
|
+
watcher.add(observed);
|
|
139
|
+
schedule();
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}, scanDelay);
|
|
102
143
|
};
|
|
103
144
|
/* render a specification into the requested format, where "realtime"
|
|
104
145
|
injects the client-side script of the live preview into the HTML */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"globals": "17.12.0",
|
|
55
55
|
"markdownlint": "0.41.1",
|
|
56
56
|
"markdownlint-cli2": "0.23.2",
|
|
57
|
-
"@types/node": "26.
|
|
57
|
+
"@types/node": "26.5.0",
|
|
58
58
|
"@types/subset-font": "2.3.0",
|
|
59
59
|
"@types/nunjucks": "3.2.6",
|
|
60
60
|
"@types/ws": "8.18.1",
|