dogsbay 0.2.0-beta.16 → 0.2.0-beta.18

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.
@@ -17,7 +17,8 @@
17
17
  import { existsSync } from "node:fs";
18
18
  import { dirname, isAbsolute, resolve } from "node:path";
19
19
  import pc from "picocolors";
20
- import { emitAstroPages, emitSiteConfig, emitConfigDerivedFiles, emitAgentReadinessFiles, emitMissingTranslationStubs, emitSwitcherMap, emitTaxonomyRoutes, emitPluginRuntime, normalizeBasePath, basePathSegments, } from "@dogsbay/format-astro";
20
+ import { emitAstroPages, emitPassthroughAstroPages, emitSiteConfig, emitConfigDerivedFiles, emitAgentReadinessFiles, emitMissingTranslationStubs, emitSwitcherMap, emitTaxonomyRoutes, emitPluginRuntime, normalizeBasePath, basePathSegments, } from "@dogsbay/format-astro";
21
+ import { collectPassthroughEntries } from "../passthrough-astro.js";
21
22
  import { configToAstroOptions, findConfig, loadConfig, resolveOutputDir, } from "../config/index.js";
22
23
  import { filterSourcesForMode, importContent, primaryModeFiltersAnything, } from "../import-content.js";
23
24
  import { resolveSource } from "../source-resolver.js";
@@ -170,7 +171,30 @@ export async function siteBuild(cwd, options) {
170
171
  // config-derived, so changes in dogsbay.config.yml propagate without
171
172
  // re-running site init.
172
173
  emitSiteConfig(outputDir, config.site.name, astroOpts);
173
- const { generated, outputNav } = await emitAstroPages(pages, nav, outputDir, astroOpts);
174
+ const { generated, outputNav, generatedPaths } = await emitAstroPages(pages, nav, outputDir, astroOpts);
175
+ // Passthrough Astro pages — hand-authored .astro files that live
176
+ // alongside the markdown sources and are listed in nav.yml. Picked
177
+ // up by walking the source directories for *.astro and intersecting
178
+ // with the resolved nav hrefs. Collisions with generated pages are
179
+ // a build error (loud, not silent overwrite). See
180
+ // plans/passthrough-astro-pages.md.
181
+ const passthroughCopies = [];
182
+ for (const sourceDir of resolvedSources) {
183
+ const entries = collectPassthroughEntries(sourceDir, outputNav, {
184
+ basePath: normalizeBasePath(astroOpts.basePath),
185
+ });
186
+ for (const entry of entries) {
187
+ passthroughCopies.push({
188
+ source: entry.source,
189
+ sourceAbs: entry.sourceAbs,
190
+ outputRelPath: entry.outputRelPath,
191
+ });
192
+ }
193
+ }
194
+ if (passthroughCopies.length > 0) {
195
+ const { copied } = emitPassthroughAstroPages(passthroughCopies, outputDir, generatedPaths);
196
+ console.log(` Copied ${copied} passthrough .astro page${copied === 1 ? "" : "s"}`);
197
+ }
174
198
  emitConfigDerivedFiles(outputDir, astroOpts);
175
199
  emitAgentReadinessFiles(pages, outputNav, outputDir, config.site.name, astroOpts);
176
200
  // switcherMap.json — per-page version-equivalent lookup. No-op
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Passthrough Astro page collection.
3
+ *
4
+ * Hand-authored `.astro` files that live under a content source's
5
+ * directory get copied verbatim to `src/pages/<basePath>/...` at
6
+ * build time. Authors opt in by listing the file in `nav.yml`; the
7
+ * intersection of "files on disk" and "hrefs in nav" defines the
8
+ * passthrough set.
9
+ *
10
+ * See plans/passthrough-astro-pages.md.
11
+ */
12
+ import { existsSync, readdirSync, statSync } from "node:fs";
13
+ import { join, posix, relative, sep } from "node:path";
14
+ /**
15
+ * Walk `contentDir` for `.astro` files, compute each one's canonical
16
+ * href, and return only the entries whose href appears in the
17
+ * resolved nav. Files outside the nav are ignored — passthrough is
18
+ * opt-in to avoid accidentally publishing scratch components.
19
+ */
20
+ export function collectPassthroughEntries(contentDir, nav, options) {
21
+ if (!existsSync(contentDir))
22
+ return [];
23
+ const navHrefs = collectNavHrefs(nav);
24
+ const candidates = walkAstroFiles(contentDir, contentDir);
25
+ const entries = [];
26
+ for (const sourceAbs of candidates) {
27
+ const source = toPosix(relative(contentDir, sourceAbs));
28
+ const href = sourceToHref(source, options.basePath);
29
+ if (!navHrefs.has(href))
30
+ continue;
31
+ entries.push({
32
+ source,
33
+ sourceAbs,
34
+ outputRelPath: sourceToOutputRelPath(source, options.basePath),
35
+ href,
36
+ });
37
+ }
38
+ return entries;
39
+ }
40
+ /**
41
+ * Recursively gather every `.astro` file under `dir`. Skips
42
+ * `node_modules`, dot-directories, and any directory named
43
+ * `_components` / `_partials` (a common convention for "this is
44
+ * shared, don't publish it" — leaves authors an obvious escape
45
+ * hatch when they want to ship private helpers alongside content).
46
+ */
47
+ function walkAstroFiles(dir, root) {
48
+ const out = [];
49
+ let entries = [];
50
+ try {
51
+ entries = readdirSync(dir, { withFileTypes: true });
52
+ }
53
+ catch {
54
+ return out;
55
+ }
56
+ for (const entry of entries) {
57
+ if (entry.name.startsWith("."))
58
+ continue;
59
+ if (entry.name === "node_modules")
60
+ continue;
61
+ if (entry.name === "_components" || entry.name === "_partials")
62
+ continue;
63
+ const full = join(dir, entry.name);
64
+ if (entry.isDirectory()) {
65
+ out.push(...walkAstroFiles(full, root));
66
+ }
67
+ else if (entry.isFile() && entry.name.endsWith(".astro")) {
68
+ out.push(full);
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ /**
74
+ * Compute the public-facing href for a source path. Mirrors the
75
+ * slug logic in `format-dogsbay-md/src/nav-file.ts:fileToHref` so a
76
+ * passthrough .astro file produces the same href as a hypothetical
77
+ * .md sibling at the same path.
78
+ *
79
+ * - "tutorials/playground.astro" → "/docs/tutorials/playground"
80
+ * - "reference/index.astro" → "/docs/reference"
81
+ * - "index.astro" → "/docs"
82
+ */
83
+ function sourceToHref(source, basePath) {
84
+ let slug = source.replace(/\.astro$/i, "");
85
+ if (slug.endsWith("/index"))
86
+ slug = slug.slice(0, -"/index".length);
87
+ if (slug === "index")
88
+ slug = "";
89
+ const prefix = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
90
+ return slug ? `${prefix}/${slug}` : prefix || "";
91
+ }
92
+ /**
93
+ * Compute the output path (relative to outputDir) where a passthrough
94
+ * source should be copied. Preserves the source's directory shape
95
+ * under `src/pages/<basePath segments>/`. Index files survive as
96
+ * `index.astro` so Astro's directory-routing matches the nav href.
97
+ */
98
+ function sourceToOutputRelPath(source, basePath) {
99
+ const baseSegs = basePath.split("/").filter((s) => s.length > 0);
100
+ const sourceSegs = source.split("/");
101
+ return ["src", "pages", ...baseSegs, ...sourceSegs].join(sep);
102
+ }
103
+ /** Walk a NavItem tree collecting every href into a Set. */
104
+ function collectNavHrefs(nav) {
105
+ const out = new Set();
106
+ const stack = [...nav];
107
+ while (stack.length > 0) {
108
+ const item = stack.pop();
109
+ if (item.href)
110
+ out.add(item.href);
111
+ if (item.children)
112
+ stack.push(...item.children);
113
+ }
114
+ return out;
115
+ }
116
+ /**
117
+ * Normalize backslashes (Windows path separators) to forward slashes
118
+ * before doing any href / slug computation. The slug shape is
119
+ * URL-flavoured even on Windows.
120
+ */
121
+ function toPosix(p) {
122
+ return p.split(sep).join(posix.sep);
123
+ }
124
+ /**
125
+ * Build the slug set covered by the passthrough entries. Used by
126
+ * site-build to assert no collision with generated slugs from
127
+ * `emitAstroPages`.
128
+ */
129
+ export function passthroughSlugs(entries) {
130
+ return new Set(entries.map((e) => e.href));
131
+ }
132
+ /**
133
+ * Convenience guard — verify every passthrough source still exists
134
+ * on disk. Walking already only returns existing files, but this
135
+ * helper is useful when entries are constructed externally (e.g. in
136
+ * tests).
137
+ */
138
+ export function assertPassthroughFilesExist(entries) {
139
+ for (const entry of entries) {
140
+ if (!existsSync(entry.sourceAbs)) {
141
+ throw new Error(`Passthrough Astro source missing: ${entry.source} ` +
142
+ `(resolved: ${entry.sourceAbs})`);
143
+ }
144
+ }
145
+ // Touch statSync to detect non-file entries (defensive)
146
+ for (const entry of entries) {
147
+ const st = statSync(entry.sourceAbs);
148
+ if (!st.isFile()) {
149
+ throw new Error(`Passthrough Astro source is not a file: ${entry.source}`);
150
+ }
151
+ }
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dogsbay",
3
- "version": "0.2.0-beta.16",
3
+ "version": "0.2.0-beta.18",
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": {
@@ -32,14 +32,14 @@
32
32
  "picocolors": "^1.1.0",
33
33
  "prompts": "^2.4.2",
34
34
  "yaml": "^2.8.3",
35
- "@dogsbay/format-mkdocs": "0.2.0-beta.16",
36
- "@dogsbay/format-astro": "0.2.0-beta.16",
37
- "@dogsbay/format-obsidian": "0.2.0-beta.16",
38
- "@dogsbay/format-mdx": "0.2.0-beta.16",
39
- "@dogsbay/format-openapi": "0.2.0-beta.16",
40
- "@dogsbay/format-starlight": "0.2.0-beta.16",
41
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.16",
42
- "@dogsbay/types": "0.2.0-beta.16"
35
+ "@dogsbay/format-mkdocs": "0.2.0-beta.18",
36
+ "@dogsbay/format-obsidian": "0.2.0-beta.18",
37
+ "@dogsbay/format-mdx": "0.2.0-beta.18",
38
+ "@dogsbay/format-starlight": "0.2.0-beta.18",
39
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.18",
40
+ "@dogsbay/format-openapi": "0.2.0-beta.18",
41
+ "@dogsbay/format-astro": "0.2.0-beta.18",
42
+ "@dogsbay/types": "0.2.0-beta.18"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.0.0",