jamdesk 1.1.31 → 1.1.32
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.32",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -70,10 +70,10 @@ import {
|
|
|
70
70
|
generateCodeExamples,
|
|
71
71
|
formatOpenApiWarning,
|
|
72
72
|
type OpenApiEndpointData,
|
|
73
|
-
type OpenApiValidationError,
|
|
74
73
|
type CodeExample,
|
|
75
74
|
type AuthMethod,
|
|
76
75
|
} from '@/lib/openapi';
|
|
76
|
+
import { classifyOpenApiLoadError } from '@/lib/openapi/classify-load-error';
|
|
77
77
|
import { extractLanguageFromPath, isValidLanguageCode } from '@/lib/language-utils';
|
|
78
78
|
import { findFirstNavPage } from '@/lib/find-first-nav-page';
|
|
79
79
|
import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
|
|
@@ -480,6 +480,7 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
480
480
|
let openApiError: string | null = null;
|
|
481
481
|
|
|
482
482
|
// OpenAPI spec parsing - supports both static and ISR modes
|
|
483
|
+
let lastFailure: { err: unknown; specPath: string } | null = null;
|
|
483
484
|
if (data.openapi && typeof data.openapi === 'string') {
|
|
484
485
|
try {
|
|
485
486
|
// Normalize config to array (handles string, array, or undefined)
|
|
@@ -510,8 +511,6 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
510
511
|
: null;
|
|
511
512
|
const contentDir = useIsr ? null : getContentDir();
|
|
512
513
|
|
|
513
|
-
let lastError: unknown = null;
|
|
514
|
-
|
|
515
514
|
for (let i = 0; i < specsToTry.length; i++) {
|
|
516
515
|
const specPath = specsToTry[i];
|
|
517
516
|
try {
|
|
@@ -527,10 +526,10 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
527
526
|
const { api } = await getCachedSpec(specPath, contentDir!);
|
|
528
527
|
openApiEndpointData = parseEndpoint(api, parsed.method, parsed.path, specPath);
|
|
529
528
|
}
|
|
530
|
-
|
|
529
|
+
lastFailure = null;
|
|
531
530
|
break;
|
|
532
531
|
} catch (err) {
|
|
533
|
-
|
|
532
|
+
lastFailure = { err, specPath };
|
|
534
533
|
const isLast = i === specsToTry.length - 1;
|
|
535
534
|
if (!isLast) {
|
|
536
535
|
// Lang variant (or intermediate candidate) failed — log so we
|
|
@@ -542,8 +541,8 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
542
541
|
}
|
|
543
542
|
}
|
|
544
543
|
|
|
545
|
-
if (
|
|
546
|
-
throw
|
|
544
|
+
if (lastFailure) {
|
|
545
|
+
throw lastFailure.err;
|
|
547
546
|
}
|
|
548
547
|
|
|
549
548
|
// Generate code examples
|
|
@@ -553,14 +552,11 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
553
552
|
openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod, languages });
|
|
554
553
|
}
|
|
555
554
|
} catch (err) {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
console.error(`Failed to parse OpenAPI for ${slug.join('/')}:`, err);
|
|
562
|
-
openApiError = 'Unexpected error loading OpenAPI specification';
|
|
563
|
-
}
|
|
555
|
+
const typed = classifyOpenApiLoadError(err, lastFailure?.specPath ?? null);
|
|
556
|
+
console.warn(formatOpenApiWarning(typed));
|
|
557
|
+
openApiError = typed.suggestion
|
|
558
|
+
? `${typed.message} — ${typed.suggestion}`
|
|
559
|
+
: typed.message;
|
|
564
560
|
}
|
|
565
561
|
}
|
|
566
562
|
|
|
@@ -107,7 +107,7 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
107
107
|
onClose={closeSidebar}
|
|
108
108
|
/>
|
|
109
109
|
|
|
110
|
-
<div className="flex-1 lg:ml-[295px] flex flex-col lg:h-screen bg-[var(--color-bg-content,var(--color-bg-primary))]">
|
|
110
|
+
<div className="flex-1 min-w-0 lg:ml-[295px] flex flex-col lg:h-screen bg-[var(--color-bg-content,var(--color-bg-primary))]">
|
|
111
111
|
<Header
|
|
112
112
|
config={config}
|
|
113
113
|
layout={layout}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import glob from 'fast-glob';
|
|
13
|
+
import { isPathWithinProject } from '../shared/path-security.js';
|
|
13
14
|
import type { DocsConfig } from './docs-types.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -95,17 +96,100 @@ export async function collectSnippetFiles(projectDir: string): Promise<string[]>
|
|
|
95
96
|
return files;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
/**
|
|
100
|
+
* An OpenAPI spec file resolved for upload.
|
|
101
|
+
*
|
|
102
|
+
* `key` is the R2 key suffix (after `{slug}/`) — it must match what the ISR
|
|
103
|
+
* fetcher computes from the docs.json path so upload and fetch stay in sync.
|
|
104
|
+
* `localPath` is the absolute file path on disk.
|
|
105
|
+
*/
|
|
106
|
+
export interface OpenApiFileRef {
|
|
107
|
+
key: string;
|
|
108
|
+
localPath: string;
|
|
109
|
+
}
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Collect OpenAPI spec files to upload for a project.
|
|
113
|
+
*
|
|
114
|
+
* Resolution strategy (symmetric with the ISR fetcher in `r2-content.ts`):
|
|
115
|
+
* 1. For each path in `docsConfig.api.openapi`, normalize it (strip leading
|
|
116
|
+
* `/`), then look for the file at `projectDir/<normalized>`. If not
|
|
117
|
+
* found, fall back to `projectDir/openapi/<normalized>` so short-name
|
|
118
|
+
* refs like `"foo.yaml"` keep working. Files are uploaded to R2 at
|
|
119
|
+
* `{slug}/<normalized>` regardless of disk location so the fetcher's
|
|
120
|
+
* `{slug}/<normalized>` lookup succeeds.
|
|
121
|
+
* 2. Additionally, scan `projectDir/openapi/**` and upload anything there
|
|
122
|
+
* at `{slug}/openapi/<relpath>`. Preserves the legacy convention where
|
|
123
|
+
* full-form MDX frontmatter (`/openapi/foo.yaml GET /pet`) works even
|
|
124
|
+
* when docs.json omits the ref.
|
|
125
|
+
*
|
|
126
|
+
* Missing referenced files are logged via `onMissing` (caller decides how
|
|
127
|
+
* to surface — build log, warning email, etc.) but never fail the build.
|
|
128
|
+
* HTTP(S) refs are skipped; they are fetched by the ISR renderer directly.
|
|
129
|
+
*/
|
|
130
|
+
export interface CollectOpenApiFilesOptions {
|
|
131
|
+
openapiRefs?: readonly string[];
|
|
132
|
+
onMissing?: (ref: string, searchedPaths: readonly string[]) => void;
|
|
133
|
+
}
|
|
107
134
|
|
|
108
|
-
|
|
135
|
+
export async function collectOpenApiFiles(
|
|
136
|
+
projectDir: string,
|
|
137
|
+
options: CollectOpenApiFilesOptions = {}
|
|
138
|
+
): Promise<OpenApiFileRef[]> {
|
|
139
|
+
const { openapiRefs = [], onMissing } = options;
|
|
140
|
+
const results: OpenApiFileRef[] = [];
|
|
141
|
+
const seenKeys = new Set<string>();
|
|
142
|
+
|
|
143
|
+
// docs.json is user-authored; `isPathWithinProject` rejects `..` traversal,
|
|
144
|
+
// null bytes, and URL-encoded separators. Does NOT follow symlinks — any
|
|
145
|
+
// leak still lands in the customer's own R2 prefix, so blast radius is
|
|
146
|
+
// self-contained.
|
|
147
|
+
|
|
148
|
+
// Resolve refs from docs.json.
|
|
149
|
+
for (const ref of openapiRefs) {
|
|
150
|
+
if (!ref) continue;
|
|
151
|
+
if (/^https?:\/\//i.test(ref)) continue; // URLs are fetched by the renderer.
|
|
152
|
+
|
|
153
|
+
const normalized = ref.replace(/^\//, '');
|
|
154
|
+
if (!normalized) continue;
|
|
155
|
+
|
|
156
|
+
const primary = path.join(projectDir, normalized);
|
|
157
|
+
const legacy = path.join(projectDir, 'openapi', normalized);
|
|
158
|
+
let localPath: string | null = null;
|
|
159
|
+
if (isPathWithinProject(primary, projectDir) && fs.existsSync(primary)) {
|
|
160
|
+
localPath = primary;
|
|
161
|
+
} else if (isPathWithinProject(legacy, projectDir) && fs.existsSync(legacy)) {
|
|
162
|
+
localPath = legacy;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!localPath) {
|
|
166
|
+
onMissing?.(ref, [primary, legacy]);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!seenKeys.has(normalized)) {
|
|
171
|
+
seenKeys.add(normalized);
|
|
172
|
+
results.push({ key: normalized, localPath });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Legacy scan: anything under `openapi/` gets uploaded at its relative
|
|
177
|
+
// path. Preserves full-form MDX refs that don't list the spec in docs.json.
|
|
178
|
+
const openapiDir = path.join(projectDir, 'openapi');
|
|
179
|
+
if (fs.existsSync(openapiDir)) {
|
|
180
|
+
const scanned = await glob('**/*.{yaml,yml,json}', {
|
|
181
|
+
cwd: openapiDir,
|
|
182
|
+
ignore: ['node_modules/**'],
|
|
183
|
+
});
|
|
184
|
+
for (const rel of scanned) {
|
|
185
|
+
const key = `openapi/${rel}`;
|
|
186
|
+
if (seenKeys.has(key)) continue;
|
|
187
|
+
seenKeys.add(key);
|
|
188
|
+
results.push({ key, localPath: path.join(openapiDir, rel) });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return results;
|
|
109
193
|
}
|
|
110
194
|
|
|
111
195
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isR2NotFound } from '../r2-content';
|
|
2
|
+
import { createFileNotFoundError, formatOpenApiError } from './errors';
|
|
3
|
+
import type { OpenApiValidationError } from './types';
|
|
4
|
+
|
|
5
|
+
/** Maps a raw load error to a typed OpenApiValidationError for the renderer. */
|
|
6
|
+
export function classifyOpenApiLoadError(
|
|
7
|
+
err: unknown,
|
|
8
|
+
specPath: string | null
|
|
9
|
+
): OpenApiValidationError {
|
|
10
|
+
// `'specPath' in err` not truthiness — createFrontmatterError uses `specPath: ''`.
|
|
11
|
+
if (err && typeof err === 'object' && 'type' in err && 'message' in err && 'specPath' in err) {
|
|
12
|
+
return err as OpenApiValidationError;
|
|
13
|
+
}
|
|
14
|
+
const fallbackPath = specPath || 'unknown';
|
|
15
|
+
if (isR2NotFound(err)) {
|
|
16
|
+
return createFileNotFoundError(fallbackPath);
|
|
17
|
+
}
|
|
18
|
+
return formatOpenApiError(err, fallbackPath);
|
|
19
|
+
}
|
|
@@ -22,7 +22,7 @@ export interface ProgressUpdate {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Warning types that can occur during builds (non-blocking) */
|
|
25
|
-
export type BuildWarningType = 'broken_link' | 'auto_migrate' | 'missing_asset' | 'missing_page';
|
|
25
|
+
export type BuildWarningType = 'broken_link' | 'auto_migrate' | 'missing_asset' | 'missing_page' | 'missing_openapi_ref';
|
|
26
26
|
|
|
27
27
|
/** Build warning structure */
|
|
28
28
|
export interface BuildWarning {
|