sourcey 3.6.3 → 3.6.5

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 CHANGED
@@ -2,18 +2,14 @@
2
2
 
3
3
  > Docs straight from the source.
4
4
 
5
- Sourcey tells the whole product story. It turns specs, code, rich guides, changelog, roadmap pages, examples, and portable context files from your project into static HTML you can deploy anywhere.
5
+ Sourcey turns OpenAPI, MCP, Doxygen XML, godoc, rustdoc, and Markdown into one static HTML site you own: reference, guides, changelog, roadmap, and llms.txt from the same build, deployable anywhere.
6
6
 
7
- No dashboard. No runtime. No API calls to render your own documentation.
7
+ Everything renders at build time: no dashboard, no runtime, no API calls to render your own documentation.
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/sourcey)](https://www.npmjs.com/package/sourcey)
10
10
  [![build](https://img.shields.io/github/actions/workflow/status/sourcey/sourcey/ci.yml?branch=main)](https://github.com/sourcey/sourcey/actions)
11
11
  [![node](https://img.shields.io/node/v/sourcey)](https://nodejs.org)
12
12
  [![license](https://img.shields.io/npm/l/sourcey)](LICENSE)
13
- [![Go Reference](https://pkg.go.dev/badge/github.com/sourcey/sourcey/go/sourcey-godoc.svg)](https://pkg.go.dev/github.com/sourcey/sourcey/go/sourcey-godoc)
14
- [![Go Report Card](https://goreportcard.com/badge/github.com/sourcey/sourcey/go/sourcey-godoc)](https://goreportcard.com/report/github.com/sourcey/sourcey/go/sourcey-godoc)
15
- [![crates.io](https://img.shields.io/crates/v/sourcey.svg)](https://crates.io/crates/sourcey)
16
- [![docs.rs](https://docs.rs/sourcey/badge.svg)](https://docs.rs/sourcey)
17
13
 
18
14
  ```bash
19
15
  npx sourcey init
@@ -26,7 +22,7 @@ npx sourcey init
26
22
  ## Features
27
23
 
28
24
  - **OpenAPI 2.0, 3.0, 3.1, and 3.2**: full spec coverage including `QUERY` operations, response summaries, hierarchical tags, `deviceAuthorization` OAuth, `querystring` parameters, and `$self`-aware refs for multi-document APIs
29
- - **API reference from OpenAPI**: endpoints, parameters, request/response schemas, auto-generated code samples in 10 languages (cURL, JavaScript, TypeScript, Python, Go, Ruby, Java, PHP, Rust, C#)
25
+ - **API reference from OpenAPI**: endpoints, parameters, request/response schemas, and auto-generated code samples across 10 supported languages. The default set is cURL, JavaScript, and Python; configure TypeScript, Go, Ruby, Java, PHP, Rust, and C# when you want them
30
26
  - **MCP server documentation**: tools, resources, prompts rendered as browsable reference with JSON-RPC, TypeScript, and Python code samples. Color-coded method types, annotation badges, connection config cards
31
27
  - **Rich guides**: markdown pages with steps, cards, accordions, syntax-highlighted code blocks, and prose alongside your API reference
32
28
  - **MkDocs source import**: point a tab at `mkdocs.yml`; Sourcey reads `docs_dir` and `nav` so existing MkDocs markdown sites can render without hand-copying the sidebar structure
@@ -60,6 +56,9 @@ Then `sourcey init` to scaffold a new project, or `sourcey build` against an exi
60
56
 
61
57
  ### Standalone Go docs generator
62
58
 
59
+ [![Go Reference](https://pkg.go.dev/badge/github.com/sourcey/sourcey/go/sourcey-godoc.svg)](https://pkg.go.dev/github.com/sourcey/sourcey/go/sourcey-godoc)
60
+ [![Go Report Card](https://goreportcard.com/badge/github.com/sourcey/sourcey/go/sourcey-godoc)](https://goreportcard.com/report/github.com/sourcey/sourcey/go/sourcey-godoc)
61
+
63
62
  For Go-only consumers without a JavaScript toolchain, `sourcey-godoc` ships as a separate native binary. It produces static Go docs sites or portable `godoc.json` snapshots.
64
63
 
65
64
  | Path | Command |
@@ -68,17 +67,62 @@ For Go-only consumers without a JavaScript toolchain, `sourcey-godoc` ships as a
68
67
  | Homebrew | `brew install sourcey/tap/sourcey-godoc` |
69
68
  | Scoop | `scoop bucket add sourcey https://github.com/sourcey/scoop-bucket && scoop install sourcey-godoc` |
70
69
 
70
+ ### Rust snapshot converter
71
+
72
+ [![crates.io](https://img.shields.io/crates/v/sourcey.svg)](https://crates.io/crates/sourcey)
73
+ [![docs.rs](https://docs.rs/sourcey/badge.svg)](https://docs.rs/sourcey)
74
+
75
+ The `rustdoc()` adapter consumes nightly rustdoc JSON through the [`sourcey`](https://crates.io/crates/sourcey) companion crate and its `sourcey-rustdoc` converter, so CI can build Rust API docs from a committed snapshot on a stable toolchain. See [docs/adapters/rustdoc.md](docs/adapters/rustdoc.md).
76
+
71
77
  ## Quick start
72
78
 
73
79
  ```bash
74
- # From a single OpenAPI spec
75
- sourcey build api.yaml -o docs/
76
-
77
- # Multi-page site (reads sourcey.config.ts)
78
- sourcey build -o docs/
80
+ # Scaffold a project (detects OpenAPI specs and Doxyfiles)
81
+ npx sourcey init
79
82
 
80
83
  # Dev server with hot reload
81
84
  sourcey dev
85
+
86
+ # Build static HTML to dist/
87
+ sourcey build
88
+
89
+ # Quick build from a single OpenAPI spec
90
+ sourcey build api.yaml -o dist/
91
+ ```
92
+
93
+ In CI, the [`sourcey/build-docs`](https://github.com/sourcey/build-docs) GitHub Action runs the build and deploys to GitHub Pages; see [deploying](https://sourcey.com/docs/deploying).
94
+
95
+ ### Astro integration
96
+
97
+ Astro sites can mount Sourcey directly instead of running a separate docs CI job. The integration reads the same `sourcey.config.ts`, derives Sourcey's public `siteUrl` and `baseUrl` from Astro's `site`/`base` plus `routeBase`, serves docs in Astro dev, and writes docs into Astro's final build output.
98
+
99
+ ```typescript
100
+ // astro.config.ts
101
+ import { defineConfig } from "astro/config";
102
+ import sourcey from "sourcey/astro";
103
+
104
+ export default defineConfig({
105
+ site: "https://sourcey.com",
106
+ integrations: [
107
+ sourcey({
108
+ config: "./sourcey.config.ts",
109
+ routeBase: "/docs",
110
+ }),
111
+ ],
112
+ });
113
+ ```
114
+
115
+ You can also share an imported config object when you want one config module to feed both the CLI and Astro:
116
+
117
+ ```typescript
118
+ import { defineConfig } from "astro/config";
119
+ import sourcey from "sourcey/astro";
120
+ import docs from "./sourcey.config";
121
+
122
+ export default defineConfig({
123
+ site: "https://sourcey.com",
124
+ integrations: [sourcey({ config: docs, configDir: ".", routeBase: "/docs" })],
125
+ });
82
126
  ```
83
127
 
84
128
  ## Configuration
@@ -168,8 +212,8 @@ cd sourcey && npm install
168
212
  npm run build && npm test
169
213
  cd go/sourcey-godoc && go test ./... && go vet ./...
170
214
 
171
- # Run the demo site
172
- cd demo && npx tsx ../src/cli.ts dev
215
+ # Run the in-repo docs site against your working copy
216
+ cd docs && npx tsx ../src/cli.ts dev
173
217
  ```
174
218
 
175
219
  ## License
@@ -0,0 +1,89 @@
1
+ import type { Plugin } from "vite";
2
+ import type { PrettyUrls, ResolvedConfig, SourceyConfig } from "../config.js";
3
+ export interface SourceyAstroOptions {
4
+ /**
5
+ * A path to `sourcey.config.ts`, a directory containing it, or an already
6
+ * imported Sourcey config object. Defaults to `sourcey.config.ts` in the
7
+ * Astro project root.
8
+ */
9
+ config?: string | SourceyConfig | ResolvedConfig;
10
+ /**
11
+ * Directory used to resolve relative paths when `config` is an object.
12
+ * Defaults to the Astro project root.
13
+ */
14
+ configDir?: string;
15
+ /**
16
+ * URL mount point within the Astro site. Defaults to the Sourcey config
17
+ * `baseUrl` when present, otherwise `/docs`.
18
+ */
19
+ routeBase?: string;
20
+ /** Override the public Sourcey `baseUrl`. Defaults to Astro `base` + `routeBase`. */
21
+ baseUrl?: string;
22
+ /** Override the public Sourcey `siteUrl`. Defaults to Astro `site`. */
23
+ siteUrl?: string | false;
24
+ /** Override Sourcey pretty URL behavior for the Astro-mounted output. */
25
+ prettyUrls?: PrettyUrls;
26
+ /** Treat changelog warnings as build errors. */
27
+ strictChangelog?: boolean;
28
+ /** Enable or configure Astro dev-server integration. */
29
+ dev?: boolean | {
30
+ enabled?: boolean;
31
+ generateOgImages?: boolean;
32
+ };
33
+ /** Enable or configure Astro build integration. */
34
+ build?: boolean | {
35
+ enabled?: boolean;
36
+ generateOgImages?: boolean;
37
+ };
38
+ /**
39
+ * Allow writing Sourcey at Astro's output root. Off by default because the
40
+ * standalone renderer prunes its output directory before writing.
41
+ */
42
+ allowRootOutput?: boolean;
43
+ }
44
+ interface AstroIntegration {
45
+ name: string;
46
+ hooks: {
47
+ "astro:config:setup"?: (options: AstroConfigSetupOptions) => void | Promise<void>;
48
+ "astro:build:done"?: (options: AstroBuildDoneOptions) => void | Promise<void>;
49
+ };
50
+ }
51
+ interface AstroConfigSetupOptions {
52
+ command: "dev" | "build" | "preview" | "sync" | string;
53
+ config: AstroResolvedConfig;
54
+ logger: AstroLogger;
55
+ addWatchFile: (path: URL | string) => void;
56
+ createCodegenDir?: () => URL;
57
+ updateConfig: (config: {
58
+ vite?: {
59
+ plugins?: Plugin[];
60
+ };
61
+ }) => void;
62
+ }
63
+ interface AstroBuildDoneOptions {
64
+ dir: URL;
65
+ logger: AstroLogger;
66
+ }
67
+ interface AstroResolvedConfig {
68
+ root: URL;
69
+ site?: string;
70
+ base?: string;
71
+ }
72
+ interface AstroLogger {
73
+ info(message: string): void;
74
+ warn(message: string): void;
75
+ error(message: string): void;
76
+ debug?(message: string): void;
77
+ }
78
+ interface PreparedAstroSourcey {
79
+ config: ResolvedConfig;
80
+ configPath?: string;
81
+ routeBase: string;
82
+ outputRoute: string;
83
+ devOutputDir: string;
84
+ watchPaths: string[];
85
+ }
86
+ export default function sourceyAstro(options?: SourceyAstroOptions): AstroIntegration;
87
+ export declare function prepareAstroSourcey(options: SourceyAstroOptions, astroConfig: AstroResolvedConfig, codegenDir?: URL): Promise<PreparedAstroSourcey>;
88
+ export {};
89
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/astro/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,297 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, extname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { loadConfig, resolveConfigFromRaw } from "../config.js";
5
+ import { contentTypeForPath, outputPathCandidatesForRequest, requestPathMatchesBase, } from "../renderer/static-files.js";
6
+ import { buildSourceySite, collectSourceyWatchPaths, writeSourceySite } from "../site.js";
7
+ import { normalizeBaseUrl, normalizeSiteUrl } from "../site-url.js";
8
+ export default function sourceyAstro(options = {}) {
9
+ let prepared = null;
10
+ const prepareOnce = (astroConfig, codegenDir) => {
11
+ prepared ??= prepareAstroSourcey(options, astroConfig, codegenDir);
12
+ return prepared;
13
+ };
14
+ return {
15
+ name: "sourcey",
16
+ hooks: {
17
+ async "astro:config:setup"({ command, config, logger, addWatchFile, createCodegenDir, updateConfig, }) {
18
+ const codegenDir = createCodegenDir?.() ?? new URL("./.sourcey/", config.root);
19
+ const preparedSourcey = await prepareOnce(config, codegenDir);
20
+ for (const path of preparedSourcey.watchPaths) {
21
+ addWatchFile(path);
22
+ }
23
+ if (command === "dev" && phaseEnabled(options.dev, true)) {
24
+ updateConfig({
25
+ vite: {
26
+ plugins: [
27
+ sourceyAstroDevPlugin({
28
+ prepared: preparedSourcey,
29
+ logger,
30
+ strictChangelog: options.strictChangelog,
31
+ generateOgImages: phaseGenerateOgImages(options.dev, false),
32
+ }),
33
+ ],
34
+ },
35
+ });
36
+ }
37
+ },
38
+ async "astro:build:done"({ dir, logger }) {
39
+ if (!phaseEnabled(options.build, true))
40
+ return;
41
+ const fallbackConfig = {
42
+ root: new URL("./", dir),
43
+ };
44
+ const preparedSourcey = await (prepared ?? prepareOnce(fallbackConfig));
45
+ if (preparedSourcey.outputRoute === "" && !options.allowRootOutput) {
46
+ throw new Error(`sourcey/astro routeBase "/" would prune Astro's full output directory. ` +
47
+ `Use routeBase: "/docs" or set allowRootOutput: true intentionally.`);
48
+ }
49
+ const outputDir = fileURLToPath(new URL(`./${preparedSourcey.outputRoute}`, dir));
50
+ logger.info(`Sourcey: building docs at ${displayRoute(preparedSourcey.routeBase)}`);
51
+ const sourceySite = await buildSourceySite({
52
+ config: preparedSourcey.config,
53
+ outputDir,
54
+ strictChangelog: options.strictChangelog,
55
+ generateOgImages: phaseGenerateOgImages(options.build, true),
56
+ });
57
+ await writeSourceySite(sourceySite);
58
+ await writeAstroRouteAlias({
59
+ outputRoot: fileURLToPath(dir),
60
+ outputDir,
61
+ routeBase: preparedSourcey.routeBase,
62
+ });
63
+ logger.info(`Sourcey: wrote ${sourceySite.pageCount} page${sourceySite.pageCount === 1 ? "" : "s"}`);
64
+ },
65
+ },
66
+ };
67
+ }
68
+ export async function prepareAstroSourcey(options, astroConfig, codegenDir = new URL("./.sourcey/", astroConfig.root)) {
69
+ const rootDir = fileURLToPath(astroConfig.root);
70
+ const { config, configPath } = await loadSourceyConfigForAstro(options, rootDir);
71
+ const routeBase = normalizeRouteBase(options.routeBase ?? (config.baseUrl || "/docs"));
72
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? joinBasePaths(astroConfig.base, routeBase));
73
+ const siteUrl = options.siteUrl === false
74
+ ? undefined
75
+ : normalizeSiteUrl(options.siteUrl ?? astroConfig.site ?? config.siteUrl);
76
+ const mergedConfig = {
77
+ ...config,
78
+ baseUrl,
79
+ siteUrl,
80
+ prettyUrls: options.prettyUrls ?? config.prettyUrls,
81
+ };
82
+ const outputRoute = routeBase === "/" ? "" : `${routeBase.replace(/^\/+|\/+$/g, "")}/`;
83
+ const devOutputDir = fileURLToPath(new URL("./sourcey-output/", codegenDir));
84
+ const watchPaths = collectSourceyWatchPaths(mergedConfig, configPath);
85
+ return {
86
+ config: mergedConfig,
87
+ configPath,
88
+ routeBase,
89
+ outputRoute,
90
+ devOutputDir,
91
+ watchPaths,
92
+ };
93
+ }
94
+ async function loadSourceyConfigForAstro(options, rootDir) {
95
+ if (typeof options.config === "string") {
96
+ const configPath = resolveConfigPath(rootDir, options.config);
97
+ return { config: await loadConfig(configPath), configPath };
98
+ }
99
+ if (!options.config) {
100
+ const configPath = resolve(rootDir, "sourcey.config.ts");
101
+ return { config: await loadConfig(configPath), configPath };
102
+ }
103
+ if (isResolvedConfig(options.config)) {
104
+ return { config: options.config };
105
+ }
106
+ const configDir = resolve(rootDir, options.configDir ?? ".");
107
+ return { config: await resolveConfigFromRaw(options.config, configDir) };
108
+ }
109
+ function sourceyAstroDevPlugin(options) {
110
+ const { prepared, logger } = options;
111
+ let buildPromise = null;
112
+ let built = false;
113
+ async function rebuild() {
114
+ buildPromise ??= (async () => {
115
+ const sourceySite = await buildSourceySite({
116
+ config: prepared.config,
117
+ outputDir: prepared.devOutputDir,
118
+ strictChangelog: options.strictChangelog,
119
+ generateOgImages: options.generateOgImages,
120
+ });
121
+ await writeSourceySite(sourceySite);
122
+ built = true;
123
+ logger.info(`Sourcey: ready at ${displayRoute(prepared.routeBase)} (${sourceySite.pageCount} pages)`);
124
+ })().finally(() => {
125
+ buildPromise = null;
126
+ });
127
+ return buildPromise;
128
+ }
129
+ async function ensureBuilt() {
130
+ if (built)
131
+ return;
132
+ await rebuild();
133
+ }
134
+ return {
135
+ name: "sourcey:astro-dev",
136
+ configureServer(server) {
137
+ for (const path of prepared.watchPaths) {
138
+ server.watcher.add(path);
139
+ }
140
+ server.watcher.on("change", (file) => {
141
+ if (!shouldRebuildForChange(file, prepared.watchPaths))
142
+ return;
143
+ built = false;
144
+ rebuild()
145
+ .then(() => {
146
+ server.ws.send({ type: "full-reload" });
147
+ })
148
+ .catch((error) => {
149
+ const err = error instanceof Error ? error : new Error(String(error));
150
+ server.ssrFixStacktrace(err);
151
+ logger.error(`Sourcey: ${err.message}`);
152
+ server.ws.send({ type: "error", err: { message: err.message, stack: err.stack ?? "" } });
153
+ });
154
+ });
155
+ server.middlewares.use(async (req, res, next) => {
156
+ const url = req.url ?? "/";
157
+ const pathname = url.split("?", 1)[0] ?? "/";
158
+ if (url.startsWith("/@") ||
159
+ url.startsWith("/__vite") ||
160
+ url.startsWith("/node_modules/")) {
161
+ return next();
162
+ }
163
+ if (!requestPathMatchesBase(pathname, prepared.config.baseUrl)) {
164
+ return next();
165
+ }
166
+ try {
167
+ await ensureBuilt();
168
+ const file = await readGeneratedFile(prepared.devOutputDir, pathname, prepared.config.baseUrl, prepared.config.prettyUrls);
169
+ if (!file)
170
+ return next();
171
+ res.writeHead(200, {
172
+ "Content-Type": contentTypeForPath(file.outputPath),
173
+ "Cache-Control": "no-cache",
174
+ });
175
+ res.end(file.data);
176
+ }
177
+ catch (error) {
178
+ const err = error instanceof Error ? error : new Error(String(error));
179
+ server.ssrFixStacktrace(err);
180
+ logger.error(`Sourcey: ${err.message}`);
181
+ server.ws.send({ type: "error", err: { message: err.message, stack: err.stack ?? "" } });
182
+ res.writeHead(500, {
183
+ "Content-Type": "text/html; charset=utf-8",
184
+ "Cache-Control": "no-cache",
185
+ });
186
+ res.end(`<!DOCTYPE html><html><head><script type="module" src="/@vite/client"></script></head><body></body></html>`);
187
+ }
188
+ });
189
+ },
190
+ };
191
+ }
192
+ async function readGeneratedFile(outputDir, pathname, baseUrl, prettyUrls) {
193
+ for (const outputPath of outputPathCandidatesForRequest(pathname, baseUrl, prettyUrls)) {
194
+ const path = resolve(outputDir, outputPath);
195
+ if (!(await exists(path)))
196
+ continue;
197
+ const data = await readFile(path);
198
+ return {
199
+ outputPath,
200
+ data: shouldReadAsText(outputPath) ? data.toString("utf-8") : data,
201
+ };
202
+ }
203
+ return null;
204
+ }
205
+ async function exists(path) {
206
+ try {
207
+ await access(path);
208
+ return true;
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ function resolveConfigPath(rootDir, config) {
215
+ const candidate = resolve(rootDir, config);
216
+ return candidate.endsWith(".ts") ? candidate : resolve(candidate, "sourcey.config.ts");
217
+ }
218
+ function normalizeRouteBase(value) {
219
+ const normalized = normalizeBaseUrl(value);
220
+ return normalized || "/";
221
+ }
222
+ function joinBasePaths(...paths) {
223
+ const joined = paths
224
+ .map((path) => path?.trim())
225
+ .filter((path) => Boolean(path))
226
+ .flatMap((path) => path.split("/"))
227
+ .map((part) => part.trim())
228
+ .filter(Boolean)
229
+ .join("/");
230
+ return joined ? `/${joined}/` : "";
231
+ }
232
+ function displayRoute(routeBase) {
233
+ return routeBase === "/" ? "/" : routeBase.slice(0, -1);
234
+ }
235
+ async function writeAstroRouteAlias(options) {
236
+ if (options.routeBase === "/")
237
+ return;
238
+ const route = options.routeBase.replace(/^\/+|\/+$/g, "");
239
+ if (!route)
240
+ return;
241
+ const indexPath = resolve(options.outputDir, "index.html");
242
+ if (!(await exists(indexPath)))
243
+ return;
244
+ const aliasPath = resolve(options.outputRoot, `${route}.html`);
245
+ const html = await readFile(indexPath, "utf-8");
246
+ await mkdir(dirname(aliasPath), { recursive: true });
247
+ await writeFile(aliasPath, renderAstroRouteAlias(html, options.routeBase));
248
+ }
249
+ function renderAstroRouteAlias(html, routeBase) {
250
+ const basePath = routeBase.endsWith("/") ? routeBase : `${routeBase}/`;
251
+ return html
252
+ .replace(/\b(href|src)="([^"]+)"/g, (match, attr, value) => {
253
+ if (!shouldPrefixAliasUrl(value))
254
+ return match;
255
+ return `${attr}="${basePath}${value.replace(/^\.\//, "")}"`;
256
+ })
257
+ .replace(/(<meta\s+name="sourcey-search"\s+content=")([^"]+)(")/g, (match, prefix, value, suffix) => {
258
+ if (!shouldPrefixAliasUrl(value))
259
+ return match;
260
+ return `${prefix}${basePath}${value.replace(/^\.\//, "")}${suffix}`;
261
+ });
262
+ }
263
+ function shouldPrefixAliasUrl(value) {
264
+ return !/^(?:#|\/|[a-z][a-z0-9+.-]*:)/i.test(value);
265
+ }
266
+ function isResolvedConfig(config) {
267
+ return Array.isArray(config.tabs);
268
+ }
269
+ function phaseEnabled(phase, defaultEnabled) {
270
+ if (typeof phase === "boolean")
271
+ return phase;
272
+ return phase?.enabled ?? defaultEnabled;
273
+ }
274
+ function phaseGenerateOgImages(phase, defaultEnabled) {
275
+ return typeof phase === "object" ? phase.generateOgImages ?? defaultEnabled : defaultEnabled;
276
+ }
277
+ function shouldRebuildForChange(file, watchPaths) {
278
+ if (watchPaths.includes(file))
279
+ return true;
280
+ const ext = extname(file);
281
+ return ext === ".md" || ext === ".mdx" || ext === ".json" || ext === ".yml" || ext === ".yaml";
282
+ }
283
+ function shouldReadAsText(outputPath) {
284
+ switch (extname(outputPath).toLowerCase()) {
285
+ case ".html":
286
+ case ".css":
287
+ case ".js":
288
+ case ".mjs":
289
+ case ".json":
290
+ case ".svg":
291
+ case ".txt":
292
+ case ".xml":
293
+ return true;
294
+ default:
295
+ return false;
296
+ }
297
+ }
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { runIntrospector, GodocIntrospectorError } from "./core/godoc-introspect
8
8
  import { GODOC_SCHEMA_VERSION } from "./core/godoc-types.js";
9
9
  import { init } from "./init.js";
10
10
  import { formatChangelogDiagnostic, formatGodocDiagnostic, formatRustdocDiagnostic, } from "./site-assembly.js";
11
+ import { findPlaceholderServerUrls } from "./utils/server-warnings.js";
11
12
  import pkg from "../package.json" with { type: "json" };
12
13
  const build = defineCommand({
13
14
  meta: {
@@ -63,6 +64,7 @@ const build = defineCommand({
63
64
  strictChangelog: args.strictChangelog,
64
65
  });
65
66
  if (!args.quiet) {
67
+ logQuickBuildServerWarnings(result.spec);
66
68
  logChangelogDiagnostics(result.changelogDiagnostics);
67
69
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
68
70
  console.log(` Spec: ${result.spec.info.title} v${result.spec.info.version}`);
@@ -308,6 +310,12 @@ function logRustdocDiagnostics(diagnostics) {
308
310
  writer(` ${formatRustdocDiagnostic(diagnostic)}`);
309
311
  }
310
312
  }
313
+ function logQuickBuildServerWarnings(spec) {
314
+ const placeholderUrls = findPlaceholderServerUrls(spec);
315
+ for (const url of placeholderUrls) {
316
+ console.warn(` Warning: quick build is using placeholder server URL "${url}". Generated code samples will target that host. Use sourcey.config.ts or correct the source document before publishing.`);
317
+ }
318
+ }
311
319
  function parsePort(value) {
312
320
  if (!/^\d+$/.test(value)) {
313
321
  throw new Error(`Invalid port "${value}". Expected an integer from 1 to 65535.`);
@@ -1,12 +1,12 @@
1
1
  // Scroll tracker — highlights the active sidebar nav link as user scrolls.
2
2
  //
3
3
  // Observes [data-traverse-target] section anchors and toggles .active
4
- // on the matching #nav .nav-link. All visual styling is in sourcey.css;
4
+ // on the matching #nav fragment link. All visual styling is in sourcey.css;
5
5
  // this file only manages the class.
6
6
  (function () {
7
7
  function init() {
8
8
  var navbar = document.getElementById('navbar');
9
- var navLinks = document.querySelectorAll('#nav .nav-link');
9
+ var navLinks = document.querySelectorAll('#nav .nav-link, #nav .nav-group-link');
10
10
  var targets = document.querySelectorAll('[data-traverse-target]');
11
11
 
12
12
  // For prose pages, also track heading elements that TOC links point to
@@ -113,7 +113,7 @@
113
113
  if (tocEl) tocEl.addEventListener('click', function (e) { handleAnchorClick(e, '.toc-item'); });
114
114
 
115
115
  var navEl = document.getElementById('nav');
116
- if (navEl) navEl.addEventListener('click', function (e) { handleAnchorClick(e, '.nav-link'); });
116
+ if (navEl) navEl.addEventListener('click', function (e) { handleAnchorClick(e, '.nav-link, .nav-group-link'); });
117
117
 
118
118
  // Use scroll event for reliable activation at all positions.
119
119
  function onScroll() {