@specglass/core 0.0.4 → 0.0.6
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/content/mdx-loader.js +5 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +6 -1
- package/dist/integration.js +4 -0
- package/dist/navigation/builder.d.ts +13 -0
- package/dist/navigation/builder.js +29 -8
- package/dist/openapi/code-generator.js +5 -3
- package/dist/openapi/example-generator.js +8 -1
- package/dist/openapi/types.d.ts +4 -0
- package/dist/openapi/utils.d.ts +13 -1
- package/dist/openapi/utils.js +18 -0
- package/dist/validation/frontmatter-validator.d.ts +54 -0
- package/dist/validation/frontmatter-validator.js +197 -0
- package/dist/validation/index.d.ts +17 -0
- package/dist/validation/index.js +17 -0
- package/dist/validation/link-validator.d.ts +84 -0
- package/dist/validation/link-validator.js +490 -0
- package/dist/validation/spec-drift-validator.d.ts +88 -0
- package/dist/validation/spec-drift-validator.js +276 -0
- package/dist/validation/types.d.ts +45 -0
- package/dist/validation/types.js +15 -0
- package/package.json +2 -1
- package/src/pages/api-reference/[...slug].astro +27 -16
- package/src/pages/index.astro +27 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI spec drift detection validator.
|
|
3
|
+
*
|
|
4
|
+
* Compares the current OpenAPI specification against a previously saved
|
|
5
|
+
* baseline snapshot to detect drift: added endpoints, removed endpoints,
|
|
6
|
+
* changed parameters, and modified response schemas.
|
|
7
|
+
*
|
|
8
|
+
* Follows the composable validator pattern established by `validateLinks`
|
|
9
|
+
* (Story 6.1) and `validateFrontmatter` (Story 6.2).
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { resolve, dirname, join, parse as parsePath } from "node:path";
|
|
16
|
+
import { parseOpenApiSpec } from "../openapi/parser.js";
|
|
17
|
+
import { SpecglassError } from "../errors/specglass-error.js";
|
|
18
|
+
// ─── Error codes ─────────────────────────────────────────────────────
|
|
19
|
+
export const SPEC_DRIFT_ENDPOINT_ADDED = "SPEC_DRIFT_ENDPOINT_ADDED";
|
|
20
|
+
export const SPEC_DRIFT_ENDPOINT_REMOVED = "SPEC_DRIFT_ENDPOINT_REMOVED";
|
|
21
|
+
export const SPEC_DRIFT_PARAMETER_CHANGED = "SPEC_DRIFT_PARAMETER_CHANGED";
|
|
22
|
+
export const SPEC_DRIFT_RESPONSE_CHANGED = "SPEC_DRIFT_RESPONSE_CHANGED";
|
|
23
|
+
export const SPEC_DRIFT_REQUEST_BODY_CHANGED = "SPEC_DRIFT_REQUEST_BODY_CHANGED";
|
|
24
|
+
export const SPEC_DRIFT_UNREACHABLE = "SPEC_DRIFT_UNREACHABLE";
|
|
25
|
+
export const SPEC_DRIFT_PARSE_ERROR = "SPEC_DRIFT_PARSE_ERROR";
|
|
26
|
+
// ─── Baseline snapshot directory ─────────────────────────────────────
|
|
27
|
+
const BASELINE_DIR = ".specglass";
|
|
28
|
+
// ─── Snapshot creation (Task 1) ──────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Build an endpoint fingerprint key: "METHOD /path".
|
|
31
|
+
*/
|
|
32
|
+
export function endpointKey(endpoint) {
|
|
33
|
+
return `${endpoint.method.toUpperCase()} ${endpoint.path}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a fingerprint for a single endpoint.
|
|
37
|
+
*/
|
|
38
|
+
export function fingerprintEndpoint(endpoint) {
|
|
39
|
+
return {
|
|
40
|
+
operationId: endpoint.operationId,
|
|
41
|
+
parameters: endpoint.parameters
|
|
42
|
+
.map((p) => `${p.name}:${p.in}:${p.schema.type ?? "unknown"}`)
|
|
43
|
+
.sort(),
|
|
44
|
+
requestBodyContentTypes: endpoint.requestBody
|
|
45
|
+
? Object.keys(endpoint.requestBody.content).sort()
|
|
46
|
+
: [],
|
|
47
|
+
responseStatusCodes: endpoint.responses
|
|
48
|
+
.map((r) => r.statusCode)
|
|
49
|
+
.sort(),
|
|
50
|
+
deprecated: endpoint.deprecated,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a baseline snapshot from a parsed OpenAPI specification.
|
|
55
|
+
*/
|
|
56
|
+
export function createSnapshot(spec, specPath) {
|
|
57
|
+
const endpoints = {};
|
|
58
|
+
for (const ep of spec.endpoints) {
|
|
59
|
+
endpoints[endpointKey(ep)] = fingerprintEndpoint(ep);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
specPath,
|
|
63
|
+
version: spec.info.version,
|
|
64
|
+
generatedAt: new Date().toISOString(),
|
|
65
|
+
endpoints,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ─── Baseline persistence (Task 2) ──────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the baseline file path for a given spec path.
|
|
71
|
+
* Baseline is stored at `<projectRoot>/.specglass/openapi-baseline-<specName>.json`.
|
|
72
|
+
* We use the spec filename (without extension) as the disambiguator.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveBaselinePath(projectRoot, specPath) {
|
|
75
|
+
const specName = parsePath(specPath).name;
|
|
76
|
+
return join(projectRoot, BASELINE_DIR, `openapi-baseline-${specName}.json`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Load a previously saved baseline snapshot from disk.
|
|
80
|
+
* Returns null if no baseline exists (first run).
|
|
81
|
+
*/
|
|
82
|
+
export async function loadBaseline(baselinePath) {
|
|
83
|
+
if (!existsSync(baselinePath)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const raw = await readFile(baselinePath, "utf-8");
|
|
88
|
+
return JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// Only treat JSON parse failures as "no baseline" — re-throw I/O errors
|
|
92
|
+
if (error instanceof SyntaxError) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Save a baseline snapshot to disk.
|
|
100
|
+
* Creates parent directories if they don't exist.
|
|
101
|
+
*/
|
|
102
|
+
export async function saveBaseline(snapshot, baselinePath) {
|
|
103
|
+
await mkdir(dirname(baselinePath), { recursive: true });
|
|
104
|
+
await writeFile(baselinePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
105
|
+
}
|
|
106
|
+
// ─── Drift detection (Task 3) ───────────────────────────────────────
|
|
107
|
+
/**
|
|
108
|
+
* Compare two snapshots and detect all drift changes.
|
|
109
|
+
*/
|
|
110
|
+
export function detectDrift(baseline, current) {
|
|
111
|
+
const changes = [];
|
|
112
|
+
const baselineKeys = new Set(Object.keys(baseline.endpoints));
|
|
113
|
+
const currentKeys = new Set(Object.keys(current.endpoints));
|
|
114
|
+
// Detect added endpoints
|
|
115
|
+
for (const key of currentKeys) {
|
|
116
|
+
if (!baselineKeys.has(key)) {
|
|
117
|
+
changes.push({
|
|
118
|
+
type: "added",
|
|
119
|
+
endpointKey: key,
|
|
120
|
+
details: `New endpoint added: ${key}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Detect removed endpoints
|
|
125
|
+
for (const key of baselineKeys) {
|
|
126
|
+
if (!currentKeys.has(key)) {
|
|
127
|
+
changes.push({
|
|
128
|
+
type: "removed",
|
|
129
|
+
endpointKey: key,
|
|
130
|
+
details: `Endpoint removed: ${key}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Detect changes in shared endpoints
|
|
135
|
+
for (const key of currentKeys) {
|
|
136
|
+
if (!baselineKeys.has(key))
|
|
137
|
+
continue;
|
|
138
|
+
const base = baseline.endpoints[key];
|
|
139
|
+
const curr = current.endpoints[key];
|
|
140
|
+
// Parameter changes
|
|
141
|
+
const baseParams = base.parameters.join(",");
|
|
142
|
+
const currParams = curr.parameters.join(",");
|
|
143
|
+
if (baseParams !== currParams) {
|
|
144
|
+
const added = curr.parameters.filter((p) => !base.parameters.includes(p));
|
|
145
|
+
const removed = base.parameters.filter((p) => !curr.parameters.includes(p));
|
|
146
|
+
const parts = [];
|
|
147
|
+
if (added.length > 0)
|
|
148
|
+
parts.push(`added: ${added.join(", ")}`);
|
|
149
|
+
if (removed.length > 0)
|
|
150
|
+
parts.push(`removed: ${removed.join(", ")}`);
|
|
151
|
+
changes.push({
|
|
152
|
+
type: "parameter-changed",
|
|
153
|
+
endpointKey: key,
|
|
154
|
+
details: `Parameters changed on ${key}: ${parts.join("; ")}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Response status code changes
|
|
158
|
+
const baseResponses = base.responseStatusCodes.join(",");
|
|
159
|
+
const currResponses = curr.responseStatusCodes.join(",");
|
|
160
|
+
if (baseResponses !== currResponses) {
|
|
161
|
+
const added = curr.responseStatusCodes.filter((r) => !base.responseStatusCodes.includes(r));
|
|
162
|
+
const removed = base.responseStatusCodes.filter((r) => !curr.responseStatusCodes.includes(r));
|
|
163
|
+
const parts = [];
|
|
164
|
+
if (added.length > 0)
|
|
165
|
+
parts.push(`added status codes: ${added.join(", ")}`);
|
|
166
|
+
if (removed.length > 0)
|
|
167
|
+
parts.push(`removed status codes: ${removed.join(", ")}`);
|
|
168
|
+
changes.push({
|
|
169
|
+
type: "response-changed",
|
|
170
|
+
endpointKey: key,
|
|
171
|
+
details: `Response schema changed on ${key}: ${parts.join("; ")}`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Request body content type changes
|
|
175
|
+
const baseContentTypes = base.requestBodyContentTypes.join(",");
|
|
176
|
+
const currContentTypes = curr.requestBodyContentTypes.join(",");
|
|
177
|
+
if (baseContentTypes !== currContentTypes) {
|
|
178
|
+
changes.push({
|
|
179
|
+
type: "request-body-changed",
|
|
180
|
+
endpointKey: key,
|
|
181
|
+
details: `Request body content types changed on ${key}: was [${base.requestBodyContentTypes.join(", ")}], now [${curr.requestBodyContentTypes.join(", ")}]`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return changes;
|
|
186
|
+
}
|
|
187
|
+
// ─── DriftChange → ValidationResult mapping (Task 3.6) ──────────────
|
|
188
|
+
const DRIFT_TYPE_TO_CODE = {
|
|
189
|
+
added: SPEC_DRIFT_ENDPOINT_ADDED,
|
|
190
|
+
removed: SPEC_DRIFT_ENDPOINT_REMOVED,
|
|
191
|
+
"parameter-changed": SPEC_DRIFT_PARAMETER_CHANGED,
|
|
192
|
+
"response-changed": SPEC_DRIFT_RESPONSE_CHANGED,
|
|
193
|
+
"request-body-changed": SPEC_DRIFT_REQUEST_BODY_CHANGED,
|
|
194
|
+
};
|
|
195
|
+
const DRIFT_TYPE_TO_HINT = {
|
|
196
|
+
added: "A new endpoint was added to the spec. Run `ndocs build` to generate its documentation.",
|
|
197
|
+
removed: "An endpoint was removed from the spec. Remove or redirect its documentation page.",
|
|
198
|
+
"parameter-changed": "Endpoint parameters have changed. Verify the documentation still matches the API.",
|
|
199
|
+
"response-changed": "Response schema has changed. Update example responses and status code documentation.",
|
|
200
|
+
"request-body-changed": "Request body content types have changed. Update request documentation and examples.",
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Map drift changes to ValidationResult objects.
|
|
204
|
+
*/
|
|
205
|
+
export function driftToValidationResults(changes, specPath) {
|
|
206
|
+
return changes.map((change) => ({
|
|
207
|
+
code: DRIFT_TYPE_TO_CODE[change.type],
|
|
208
|
+
message: change.details,
|
|
209
|
+
filePath: specPath,
|
|
210
|
+
severity: "warning",
|
|
211
|
+
hint: DRIFT_TYPE_TO_HINT[change.type],
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
// ─── Composable validator entry point (Task 5) ──────────────────────
|
|
215
|
+
/**
|
|
216
|
+
* Spec drift validator — composable validator for OpenAPI spec drift detection.
|
|
217
|
+
*
|
|
218
|
+
* @param context - Validator context with `specPaths` and `projectRoot`
|
|
219
|
+
* @returns Array of validation results for detected drift
|
|
220
|
+
*/
|
|
221
|
+
export async function validateSpecDrift(context) {
|
|
222
|
+
const specPaths = context.specPaths ?? [];
|
|
223
|
+
const projectRoot = context.projectRoot ?? context.contentDir;
|
|
224
|
+
if (specPaths.length === 0) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const results = [];
|
|
228
|
+
for (const rawSpecPath of specPaths) {
|
|
229
|
+
// Remote specs (URLs) must not be resolved as filesystem paths
|
|
230
|
+
const isUrl = rawSpecPath.startsWith("http://") || rawSpecPath.startsWith("https://");
|
|
231
|
+
const specPath = isUrl ? rawSpecPath : resolve(projectRoot, rawSpecPath);
|
|
232
|
+
const baselinePath = resolveBaselinePath(projectRoot, rawSpecPath);
|
|
233
|
+
// Parse current spec — catch unreachable/parse errors (Task 4)
|
|
234
|
+
let spec;
|
|
235
|
+
try {
|
|
236
|
+
spec = await parseOpenApiSpec(specPath);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const message = error instanceof SpecglassError
|
|
240
|
+
? error.message
|
|
241
|
+
: error instanceof Error
|
|
242
|
+
? error.message
|
|
243
|
+
: String(error);
|
|
244
|
+
// Distinguish unreachable (file not found, network error) from parse errors
|
|
245
|
+
const isUnreachable = /not found|ENOENT|EACCES|fetch failed|unreachable|network/i.test(message);
|
|
246
|
+
const code = isUnreachable ? SPEC_DRIFT_UNREACHABLE : SPEC_DRIFT_PARSE_ERROR;
|
|
247
|
+
results.push({
|
|
248
|
+
code,
|
|
249
|
+
message: `Spec drift check failed for ${rawSpecPath}: ${message}`,
|
|
250
|
+
filePath: specPath,
|
|
251
|
+
severity: "error",
|
|
252
|
+
hint: code === SPEC_DRIFT_UNREACHABLE
|
|
253
|
+
? "Check that the spec file exists and is accessible."
|
|
254
|
+
: "Check that the file is valid YAML/JSON and conforms to OpenAPI 3.x",
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Create snapshot from current spec
|
|
259
|
+
const currentSnapshot = createSnapshot(spec, rawSpecPath);
|
|
260
|
+
// Load baseline
|
|
261
|
+
const baseline = await loadBaseline(baselinePath);
|
|
262
|
+
if (baseline === null) {
|
|
263
|
+
// First run — save baseline silently (AC 3)
|
|
264
|
+
await saveBaseline(currentSnapshot, baselinePath);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// Detect drift
|
|
268
|
+
const changes = detectDrift(baseline, currentSnapshot);
|
|
269
|
+
if (changes.length > 0) {
|
|
270
|
+
results.push(...driftToValidationResults(changes, specPath));
|
|
271
|
+
}
|
|
272
|
+
// Always update baseline after comparison
|
|
273
|
+
await saveBaseline(currentSnapshot, baselinePath);
|
|
274
|
+
}
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation types — shared interfaces for the composable validator pattern.
|
|
3
|
+
*
|
|
4
|
+
* Each validator is a function that takes a ValidatorContext and returns
|
|
5
|
+
* an array of ValidationResult objects. Validators are composable and
|
|
6
|
+
* independently runnable (e.g., via `ndocs check --links-only`).
|
|
7
|
+
*/
|
|
8
|
+
import { SpecglassError } from "../errors/specglass-error.js";
|
|
9
|
+
/** Severity level for validation findings. */
|
|
10
|
+
export type ValidationSeverity = "error" | "warning";
|
|
11
|
+
/** A single validation finding with actionable context. */
|
|
12
|
+
export interface ValidationResult {
|
|
13
|
+
/** Namespaced error code (e.g., LINK_INTERNAL_BROKEN, FRONTMATTER_MISSING_TITLE). */
|
|
14
|
+
code: string;
|
|
15
|
+
/** Human-readable error message. */
|
|
16
|
+
message: string;
|
|
17
|
+
/** Absolute path to the source file containing the issue. */
|
|
18
|
+
filePath: string;
|
|
19
|
+
/** Line number in source file (1-indexed). */
|
|
20
|
+
line?: number;
|
|
21
|
+
/** Severity: 'error' blocks CI, 'warning' is informational. */
|
|
22
|
+
severity: ValidationSeverity;
|
|
23
|
+
/** Actionable remediation hint (e.g., "Did you mean /getting-started?"). */
|
|
24
|
+
hint?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Convert a ValidationResult to a SpecglassError.
|
|
28
|
+
* Bridges the validation subsystem with the core error convention.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toSpecglassError(result: ValidationResult): SpecglassError;
|
|
31
|
+
/** Context passed to every validator. */
|
|
32
|
+
export interface ValidatorContext {
|
|
33
|
+
/** Absolute path to the content directory root. */
|
|
34
|
+
contentDir: string;
|
|
35
|
+
/** All known page slugs for internal link resolution. */
|
|
36
|
+
knownPages: string[];
|
|
37
|
+
/** Whether to skip external link checks (--no-external flag). */
|
|
38
|
+
skipExternal?: boolean;
|
|
39
|
+
/** OpenAPI spec paths (relative to projectRoot) for drift detection. */
|
|
40
|
+
specPaths?: string[];
|
|
41
|
+
/** Project root directory for resolving spec paths and baseline storage. */
|
|
42
|
+
projectRoot?: string;
|
|
43
|
+
}
|
|
44
|
+
/** A composable validator function. */
|
|
45
|
+
export type Validator = (context: ValidatorContext) => Promise<ValidationResult[]>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation types — shared interfaces for the composable validator pattern.
|
|
3
|
+
*
|
|
4
|
+
* Each validator is a function that takes a ValidatorContext and returns
|
|
5
|
+
* an array of ValidationResult objects. Validators are composable and
|
|
6
|
+
* independently runnable (e.g., via `ndocs check --links-only`).
|
|
7
|
+
*/
|
|
8
|
+
import { SpecglassError } from "../errors/specglass-error.js";
|
|
9
|
+
/**
|
|
10
|
+
* Convert a ValidationResult to a SpecglassError.
|
|
11
|
+
* Bridges the validation subsystem with the core error convention.
|
|
12
|
+
*/
|
|
13
|
+
export function toSpecglassError(result) {
|
|
14
|
+
return new SpecglassError(result.message, result.code, result.filePath, result.line, result.hint);
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specglass/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Astro integration, config system, content collections, and OpenAPI spec parsing for Specglass",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"zod": "^3.23"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
+
"@types/js-yaml": "^4.0.9",
|
|
46
47
|
"astro": "^5.17",
|
|
47
48
|
"pagefind": "^1.4.0",
|
|
48
49
|
"typescript": "^5.5",
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* API Reference route page.
|
|
4
4
|
* Generates one page per endpoint from the virtual:specglass/openapi-data module.
|
|
5
5
|
* Maps parsed OpenAPI data into ApiReferencePage layout props.
|
|
6
|
+
* Also generates fallback pages for errored endpoints (FR40).
|
|
6
7
|
*/
|
|
7
|
-
import type { ParsedOpenApiSpec, ApiEndpoint } from "@specglass/core";
|
|
8
|
-
import { buildEndpointSlug } from "@specglass/core";
|
|
8
|
+
import type { ParsedOpenApiSpec, ApiEndpoint, ApiEndpointError } from "@specglass/core";
|
|
9
|
+
import { buildEndpointSlug, buildErrorEndpointSlug } from "@specglass/core";
|
|
9
10
|
import { config } from "virtual:specglass/config";
|
|
10
11
|
import { navigation } from "virtual:specglass/navigation";
|
|
11
12
|
import { openApiData } from "virtual:specglass/openapi-data";
|
|
@@ -16,24 +17,37 @@ export async function getStaticPaths() {
|
|
|
16
17
|
const paths: Array<{
|
|
17
18
|
params: { slug: string | undefined };
|
|
18
19
|
props: {
|
|
19
|
-
endpoint
|
|
20
|
+
endpoint?: ApiEndpoint;
|
|
21
|
+
endpointError?: ApiEndpointError;
|
|
20
22
|
allEndpoints: ApiEndpoint[];
|
|
23
|
+
allErrors: ApiEndpointError[];
|
|
21
24
|
specInfo: ParsedOpenApiSpec["info"];
|
|
22
25
|
securitySchemes: ParsedOpenApiSpec["securitySchemes"];
|
|
23
26
|
};
|
|
24
27
|
}> = [];
|
|
25
28
|
|
|
26
29
|
for (const spec of allSpecs) {
|
|
30
|
+
const sharedProps = {
|
|
31
|
+
allEndpoints: spec.endpoints,
|
|
32
|
+
allErrors: spec.errors,
|
|
33
|
+
specInfo: spec.info,
|
|
34
|
+
securitySchemes: spec.securitySchemes,
|
|
35
|
+
};
|
|
36
|
+
|
|
27
37
|
for (const endpoint of spec.endpoints) {
|
|
28
38
|
const slug = buildEndpointSlug(endpoint);
|
|
29
39
|
paths.push({
|
|
30
40
|
params: { slug },
|
|
31
|
-
props: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
props: { endpoint, ...sharedProps },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback pages for errored endpoints (FR40)
|
|
46
|
+
for (const error of spec.errors) {
|
|
47
|
+
const slug = buildErrorEndpointSlug(error);
|
|
48
|
+
paths.push({
|
|
49
|
+
params: { slug },
|
|
50
|
+
props: { endpointError: error, ...sharedProps },
|
|
37
51
|
});
|
|
38
52
|
}
|
|
39
53
|
|
|
@@ -41,12 +55,7 @@ export async function getStaticPaths() {
|
|
|
41
55
|
if (spec.endpoints.length > 0) {
|
|
42
56
|
paths.push({
|
|
43
57
|
params: { slug: undefined },
|
|
44
|
-
props: {
|
|
45
|
-
endpoint: spec.endpoints[0],
|
|
46
|
-
allEndpoints: spec.endpoints,
|
|
47
|
-
specInfo: spec.info,
|
|
48
|
-
securitySchemes: spec.securitySchemes,
|
|
49
|
-
},
|
|
58
|
+
props: { endpoint: spec.endpoints[0], ...sharedProps },
|
|
50
59
|
});
|
|
51
60
|
}
|
|
52
61
|
}
|
|
@@ -54,14 +63,16 @@ export async function getStaticPaths() {
|
|
|
54
63
|
return paths;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
const { endpoint, allEndpoints, specInfo, securitySchemes } = Astro.props;
|
|
66
|
+
const { endpoint, endpointError, allEndpoints, allErrors, specInfo, securitySchemes } = Astro.props;
|
|
58
67
|
---
|
|
59
68
|
|
|
60
69
|
<ApiReferencePage
|
|
61
70
|
endpoint={endpoint}
|
|
71
|
+
endpointError={endpointError}
|
|
62
72
|
navigation={navigation}
|
|
63
73
|
config={config}
|
|
64
74
|
allEndpoints={allEndpoints}
|
|
75
|
+
allErrors={allErrors ?? []}
|
|
65
76
|
specInfo={specInfo}
|
|
66
77
|
securitySchemes={securitySchemes}
|
|
67
78
|
/>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Root index — redirects to the first page in the navigation tree.
|
|
4
|
+
*
|
|
5
|
+
* Since there is no content at `/`, this page finds the first navigable
|
|
6
|
+
* page and redirects the user there. Falls back to `/getting-started`
|
|
7
|
+
* if the navigation tree is empty (shouldn't happen in practice).
|
|
8
|
+
*/
|
|
9
|
+
import { navigation } from "virtual:specglass/navigation";
|
|
10
|
+
import type { NavItem } from "@specglass/core";
|
|
11
|
+
|
|
12
|
+
function findFirstPage(items: NavItem[]): string | null {
|
|
13
|
+
for (const item of items) {
|
|
14
|
+
if (item.type === "page" && item.slug) {
|
|
15
|
+
return `/${item.slug}`;
|
|
16
|
+
}
|
|
17
|
+
if (item.type === "section" && item.children) {
|
|
18
|
+
const found = findFirstPage(item.children);
|
|
19
|
+
if (found) return found;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const target = findFirstPage(navigation.items) ?? "/getting-started";
|
|
26
|
+
return Astro.redirect(target);
|
|
27
|
+
---
|