dogsbay 0.2.0-beta.7 → 0.2.0-beta.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/dist/commands/site-dev.js +167 -13
- package/package.json +9 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `dogsbay site dev` and `dogsbay site preview` — thin wrappers that
|
|
3
|
-
* run `dogsbay site build
|
|
4
|
-
* CLI inside the site directory.
|
|
3
|
+
* run `dogsbay site build`, watch content for changes, and hand
|
|
4
|
+
* control over to the Astro CLI inside the site directory.
|
|
5
5
|
*
|
|
6
6
|
* dev → astro dev (live HMR for already-built pages)
|
|
7
7
|
* preview → astro build && astro preview
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* existing script) is *not* what runs here — we shell out to `astro`
|
|
12
12
|
* directly so this works whether the user uses pnpm / npm / yarn.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* `site dev` also installs a content watcher that re-runs
|
|
15
|
+
* `dogsbay site build` whenever a markdown / yaml / json file under
|
|
16
|
+
* any configured source path changes (or `dogsbay.config.yml`
|
|
17
|
+
* itself). Astro's own dev server then hot-reloads the regenerated
|
|
18
|
+
* `.astro` pages. Without this, NEW files in `content/` weren't
|
|
19
|
+
* picked up by site dev — only edits to existing files surfaced
|
|
20
|
+
* (because Astro's watcher only sees `astro/src/`).
|
|
17
21
|
*/
|
|
18
22
|
import { spawn } from "node:child_process";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
23
|
+
import { existsSync, watch as fsWatch } from "node:fs";
|
|
20
24
|
import { dirname, join, resolve } from "node:path";
|
|
21
25
|
import pc from "picocolors";
|
|
22
26
|
import { findConfig, loadConfig, resolveOutputDir } from "../config/index.js";
|
|
@@ -63,18 +67,26 @@ function runPackageManagerInstall(pm, cwd) {
|
|
|
63
67
|
});
|
|
64
68
|
}
|
|
65
69
|
export async function siteDev(cwd, options, runner = defaultRunner) {
|
|
66
|
-
const siteRoot = await prepareForAstro(cwd, options);
|
|
67
|
-
const
|
|
68
|
-
|
|
70
|
+
const { siteRoot, outputDir } = await prepareForAstro(cwd, options);
|
|
71
|
+
const stopWatcher = startContentWatcher(siteRoot, outputDir, options);
|
|
72
|
+
try {
|
|
73
|
+
const code = await runner(outputDir, ["dev"]);
|
|
74
|
+
stopWatcher();
|
|
75
|
+
process.exit(code);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
stopWatcher();
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
69
81
|
}
|
|
70
82
|
export async function sitePreview(cwd, options, runner = defaultRunner) {
|
|
71
|
-
const
|
|
83
|
+
const { outputDir } = await prepareForAstro(cwd, options);
|
|
72
84
|
// Two-step: produce dist/ then serve it. Each spawn is independent
|
|
73
85
|
// so we can still surface its exit code cleanly.
|
|
74
|
-
const buildCode = await runner(
|
|
86
|
+
const buildCode = await runner(outputDir, ["build"]);
|
|
75
87
|
if (buildCode !== 0)
|
|
76
88
|
process.exit(buildCode);
|
|
77
|
-
const previewCode = await runner(
|
|
89
|
+
const previewCode = await runner(outputDir, ["preview"]);
|
|
78
90
|
process.exit(previewCode);
|
|
79
91
|
}
|
|
80
92
|
async function prepareForAstro(cwd, options) {
|
|
@@ -117,7 +129,149 @@ async function prepareForAstro(cwd, options) {
|
|
|
117
129
|
publish: options.full === true,
|
|
118
130
|
});
|
|
119
131
|
}
|
|
120
|
-
return outputDir;
|
|
132
|
+
return { siteRoot, outputDir };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Watch the project's content paths + config file, and re-run
|
|
136
|
+
* `dogsbay site build` whenever something changes. Astro's own
|
|
137
|
+
* watcher then picks up the regenerated `astro/src/pages/*.astro`
|
|
138
|
+
* files and hot-reloads.
|
|
139
|
+
*
|
|
140
|
+
* Returns a cleanup fn that closes all the watchers.
|
|
141
|
+
*
|
|
142
|
+
* Debounces aggressively — many editors fire 3-5 fs events per save
|
|
143
|
+
* (write + rename + close-write etc.), and a single Vim save bursts
|
|
144
|
+
* across multiple files. 300ms is the sweet spot: low enough that
|
|
145
|
+
* the user feels the rebuild as immediate, high enough to coalesce
|
|
146
|
+
* a save burst into one rebuild.
|
|
147
|
+
*/
|
|
148
|
+
function startContentWatcher(siteRoot, outputDir, options) {
|
|
149
|
+
// Re-load the config to know which paths to watch. We load it
|
|
150
|
+
// from disk on first event too (so config edits update the watch
|
|
151
|
+
// set on the fly).
|
|
152
|
+
let config = loadConfig(findOrFail(siteRoot, options.config));
|
|
153
|
+
// Track which paths we've armed a watcher on. fs.watch with
|
|
154
|
+
// recursive: true scoops up everything under each root.
|
|
155
|
+
const watchers = new Set();
|
|
156
|
+
const armed = new Set();
|
|
157
|
+
const arm = (root) => {
|
|
158
|
+
if (armed.has(root))
|
|
159
|
+
return;
|
|
160
|
+
if (!existsSync(root))
|
|
161
|
+
return;
|
|
162
|
+
armed.add(root);
|
|
163
|
+
try {
|
|
164
|
+
const w = fsWatch(root, { recursive: true }, (_event, filename) => {
|
|
165
|
+
if (!filename)
|
|
166
|
+
return;
|
|
167
|
+
const fname = filename.toString();
|
|
168
|
+
if (shouldIgnore(fname))
|
|
169
|
+
return;
|
|
170
|
+
scheduleBuild();
|
|
171
|
+
});
|
|
172
|
+
watchers.add(w);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.error(pc.yellow(` warn: could not watch ${root}: ${err.message}`));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
// Always watch the config file itself (one level up — fs.watch on
|
|
179
|
+
// a single file works on macOS/Windows; on Linux some filesystems
|
|
180
|
+
// require watching the parent dir).
|
|
181
|
+
arm(siteRoot);
|
|
182
|
+
// Watch each local source path declared in the config.
|
|
183
|
+
for (const source of config.content?.sources ?? []) {
|
|
184
|
+
if (typeof source.path === "string") {
|
|
185
|
+
const abs = resolve(siteRoot, source.path);
|
|
186
|
+
arm(abs);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Debounced rebuild loop. If a build is in flight when a new
|
|
190
|
+
// event arrives, mark dirty + rebuild after the current one
|
|
191
|
+
// finishes. Avoids overlapping builds racing on the same files.
|
|
192
|
+
let timer = null;
|
|
193
|
+
let building = false;
|
|
194
|
+
let dirty = false;
|
|
195
|
+
const scheduleBuild = () => {
|
|
196
|
+
dirty = true;
|
|
197
|
+
if (timer)
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
timer = setTimeout(runBuild, 300);
|
|
200
|
+
};
|
|
201
|
+
const runBuild = async () => {
|
|
202
|
+
if (building)
|
|
203
|
+
return; // a build is in flight; the dirty flag handles re-run
|
|
204
|
+
if (!dirty)
|
|
205
|
+
return;
|
|
206
|
+
dirty = false;
|
|
207
|
+
building = true;
|
|
208
|
+
try {
|
|
209
|
+
console.log(pc.cyan("[dogsbay] content changed — rebuilding…"));
|
|
210
|
+
// Reload config to pick up dogsbay.config.yml edits.
|
|
211
|
+
config = loadConfig(findOrFail(siteRoot, options.config));
|
|
212
|
+
// Re-arm any newly-added source paths.
|
|
213
|
+
for (const source of config.content?.sources ?? []) {
|
|
214
|
+
if (typeof source.path === "string") {
|
|
215
|
+
arm(resolve(siteRoot, source.path));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
await siteBuild(siteRoot, {
|
|
219
|
+
includeDrafts: true,
|
|
220
|
+
publish: options.full === true,
|
|
221
|
+
});
|
|
222
|
+
console.log(pc.green("[dogsbay] rebuild complete"));
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error(pc.red(`[dogsbay] rebuild failed: ${err.message}`));
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
building = false;
|
|
229
|
+
// Coalesced changes during the build → run again.
|
|
230
|
+
if (dirty)
|
|
231
|
+
setImmediate(runBuild);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
// Diagnostic: show what's being watched on startup.
|
|
235
|
+
console.log(pc.gray(`[dogsbay] watching ${armed.size} path${armed.size === 1 ? "" : "s"} for content changes`));
|
|
236
|
+
return () => {
|
|
237
|
+
if (timer)
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
for (const w of watchers) {
|
|
240
|
+
try {
|
|
241
|
+
w.close();
|
|
242
|
+
}
|
|
243
|
+
catch { /* ignore */ }
|
|
244
|
+
}
|
|
245
|
+
watchers.clear();
|
|
246
|
+
armed.clear();
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Heuristic: skip events on paths the user doesn't care about.
|
|
251
|
+
* Filters out the output dir (would loop), VCS metadata, OS junk,
|
|
252
|
+
* and editor swap files.
|
|
253
|
+
*/
|
|
254
|
+
function shouldIgnore(filename) {
|
|
255
|
+
if (filename.startsWith(".git/") || filename === ".git")
|
|
256
|
+
return true;
|
|
257
|
+
if (filename.startsWith("node_modules/"))
|
|
258
|
+
return true;
|
|
259
|
+
if (filename.startsWith("astro/"))
|
|
260
|
+
return true; // output dir — would loop
|
|
261
|
+
if (filename.startsWith("dist/"))
|
|
262
|
+
return true;
|
|
263
|
+
if (filename.startsWith(".dogsbay/"))
|
|
264
|
+
return true; // skill symlinks
|
|
265
|
+
if (filename.endsWith(".swp") || filename.endsWith(".swo"))
|
|
266
|
+
return true;
|
|
267
|
+
if (filename.endsWith("~"))
|
|
268
|
+
return true;
|
|
269
|
+
if (filename.endsWith(".DS_Store"))
|
|
270
|
+
return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
function findOrFail(siteRoot, explicit) {
|
|
274
|
+
return resolveConfigPath(siteRoot, explicit);
|
|
121
275
|
}
|
|
122
276
|
function resolveConfigPath(startDir, explicit) {
|
|
123
277
|
if (explicit) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dogsbay",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.8",
|
|
4
4
|
"description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"picocolors": "^1.1.0",
|
|
32
32
|
"prompts": "^2.4.2",
|
|
33
33
|
"yaml": "^2.8.3",
|
|
34
|
-
"@dogsbay/format-mkdocs": "0.2.0-beta.
|
|
35
|
-
"@dogsbay/format-astro": "0.2.0-beta.
|
|
36
|
-
"@dogsbay/format-
|
|
37
|
-
"@dogsbay/format-
|
|
38
|
-
"@dogsbay/format-starlight": "0.2.0-beta.
|
|
39
|
-
"@dogsbay/format-dogsbay-md": "0.2.0-beta.
|
|
40
|
-
"@dogsbay/format-openapi": "0.2.0-beta.
|
|
41
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
34
|
+
"@dogsbay/format-mkdocs": "0.2.0-beta.8",
|
|
35
|
+
"@dogsbay/format-astro": "0.2.0-beta.8",
|
|
36
|
+
"@dogsbay/format-obsidian": "0.2.0-beta.8",
|
|
37
|
+
"@dogsbay/format-mdx": "0.2.0-beta.8",
|
|
38
|
+
"@dogsbay/format-starlight": "0.2.0-beta.8",
|
|
39
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.8",
|
|
40
|
+
"@dogsbay/format-openapi": "0.2.0-beta.8",
|
|
41
|
+
"@dogsbay/types": "0.2.0-beta.8"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^22.0.0",
|