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.31",
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
- lastError = null;
529
+ lastFailure = null;
531
530
  break;
532
531
  } catch (err) {
533
- lastError = err;
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 (lastError) {
546
- throw lastError;
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 error = err as Partial<OpenApiValidationError>;
557
- if (error.type && error.specPath && error.message) {
558
- console.warn(formatOpenApiWarning(error as OpenApiValidationError));
559
- openApiError = error.message;
560
- } else {
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
- /** Collect OpenAPI spec files from project directory. Returns paths relative to openapi/. */
99
- export async function collectOpenApiFiles(projectDir: string): Promise<string[]> {
100
- const openapiDir = `${projectDir}/openapi`;
101
- if (!fs.existsSync(openapiDir)) return [];
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
- const files = await glob('**/*.{yaml,yml,json}', {
104
- cwd: openapiDir,
105
- ignore: ['node_modules/**'],
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
- return files;
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 {