jamdesk 1.1.6 → 1.1.8
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 +46 -1
- package/dist/__tests__/unit/docs-json-writer.test.js +59 -1
- package/dist/__tests__/unit/docs-json-writer.test.js.map +1 -1
- package/dist/__tests__/unit/spellcheck-fix.test.d.ts +2 -0
- package/dist/__tests__/unit/spellcheck-fix.test.d.ts.map +1 -0
- package/dist/__tests__/unit/spellcheck-fix.test.js +82 -0
- package/dist/__tests__/unit/spellcheck-fix.test.js.map +1 -0
- package/dist/__tests__/unit/spellcheck-utils.test.d.ts +2 -0
- package/dist/__tests__/unit/spellcheck-utils.test.d.ts.map +1 -0
- package/dist/__tests__/unit/spellcheck-utils.test.js +234 -0
- package/dist/__tests__/unit/spellcheck-utils.test.js.map +1 -0
- package/dist/__tests__/unit/tech-words.test.d.ts +2 -0
- package/dist/__tests__/unit/tech-words.test.d.ts.map +1 -0
- package/dist/__tests__/unit/tech-words.test.js +31 -0
- package/dist/__tests__/unit/tech-words.test.js.map +1 -0
- package/dist/commands/spellcheck.d.ts +13 -0
- package/dist/commands/spellcheck.d.ts.map +1 -0
- package/dist/commands/spellcheck.js +144 -0
- package/dist/commands/spellcheck.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/deps.js +2 -2
- package/dist/lib/docs-json-writer.d.ts +6 -0
- package/dist/lib/docs-json-writer.d.ts.map +1 -1
- package/dist/lib/docs-json-writer.js +29 -0
- package/dist/lib/docs-json-writer.js.map +1 -1
- package/dist/lib/openapi/types.d.ts +11 -6
- package/dist/lib/openapi/types.d.ts.map +1 -1
- package/dist/lib/path-security.d.ts +3 -0
- package/dist/lib/path-security.d.ts.map +1 -1
- package/dist/lib/path-security.js +14 -1
- package/dist/lib/path-security.js.map +1 -1
- package/dist/lib/spellcheck-fix.d.ts +37 -0
- package/dist/lib/spellcheck-fix.d.ts.map +1 -0
- package/dist/lib/spellcheck-fix.js +292 -0
- package/dist/lib/spellcheck-fix.js.map +1 -0
- package/dist/lib/spellcheck-utils.d.ts +36 -0
- package/dist/lib/spellcheck-utils.d.ts.map +1 -0
- package/dist/lib/spellcheck-utils.js +138 -0
- package/dist/lib/spellcheck-utils.js.map +1 -0
- package/dist/lib/tech-words.d.ts +9 -0
- package/dist/lib/tech-words.d.ts.map +1 -0
- package/dist/lib/tech-words.js +118 -0
- package/dist/lib/tech-words.js.map +1 -0
- package/package.json +15 -10
- package/vendored/app/[[...slug]]/page.tsx +48 -13
- package/vendored/app/api/assets/[...path]/route.ts +2 -0
- package/vendored/components/layout/LayoutWrapper.tsx +3 -4
- package/vendored/components/mdx/ApiEndpoint.tsx +13 -2
- package/vendored/components/mdx/MDXComponents.tsx +16 -0
- package/vendored/components/mdx/OpenApiEndpoint.tsx +76 -36
- package/vendored/components/mdx/Tabs.tsx +1 -1
- package/vendored/components/mdx/Video.tsx +82 -0
- package/vendored/components/navigation/Header.tsx +3 -3
- package/vendored/components/navigation/Sidebar.tsx +3 -3
- package/vendored/components/ui/CodePanel.tsx +5 -5
- package/vendored/components/ui/CodePanelModal.tsx +3 -3
- package/vendored/components/ui/DevOnlyNotice.tsx +78 -0
- package/vendored/hooks/useChatPanel.tsx +21 -2
- package/vendored/hooks/useMediaQuery.ts +27 -0
- package/vendored/lib/build-endpoint-from-mdx.ts +66 -0
- package/vendored/lib/isr-build-executor.ts +1 -1
- package/vendored/lib/middleware-helpers.ts +6 -1
- package/vendored/lib/openapi/code-examples.ts +479 -99
- package/vendored/lib/openapi/index.ts +9 -1
- package/vendored/lib/openapi/types.ts +29 -5
- package/vendored/lib/preprocess-mdx.ts +103 -36
- package/vendored/lib/process-mdx-with-exports.ts +22 -14
- package/vendored/lib/remark-extract-param-fields.ts +134 -0
- package/vendored/lib/shiki-client.ts +12 -0
- package/vendored/lib/static-artifacts.ts +2 -0
- package/vendored/lib/static-file-route.ts +1 -1
- package/vendored/lib/url-safety.ts +122 -0
- package/vendored/next.config.js +7 -0
- package/vendored/schema/docs-schema.json +35 -4
- package/vendored/scripts/copy-files.cjs +60 -54
- package/vendored/scripts/validate-links.cjs +1 -1
- package/vendored/shared/path-security.ts +17 -1
|
@@ -22,7 +22,7 @@ export type {
|
|
|
22
22
|
OpenApiErrorType,
|
|
23
23
|
OpenApiValidationError,
|
|
24
24
|
ValidationResult,
|
|
25
|
-
|
|
25
|
+
CodeExample,
|
|
26
26
|
GeneratedPage,
|
|
27
27
|
CachedSpec,
|
|
28
28
|
} from './types';
|
|
@@ -58,9 +58,17 @@ export {
|
|
|
58
58
|
// Code Examples
|
|
59
59
|
export type { AuthMethod, CodeExampleConfig } from './code-examples';
|
|
60
60
|
export {
|
|
61
|
+
DEFAULT_LANGUAGES,
|
|
62
|
+
SUPPORTED_LANGUAGES,
|
|
61
63
|
generateCurlExample,
|
|
62
64
|
generatePythonExample,
|
|
63
65
|
generateJsExample,
|
|
66
|
+
generateGoExample,
|
|
67
|
+
generateRubyExample,
|
|
68
|
+
generateCsharpExample,
|
|
69
|
+
generateJavaExample,
|
|
70
|
+
generateRustExample,
|
|
71
|
+
generatePhpExample,
|
|
64
72
|
generateCodeExamples,
|
|
65
73
|
} from './code-examples';
|
|
66
74
|
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
|
9
|
+
import type { AuthMethod } from './code-examples';
|
|
9
10
|
|
|
10
11
|
// Re-export for convenience
|
|
11
12
|
export type { OpenAPI, OpenAPIV3, OpenAPIV3_1 };
|
|
13
|
+
export type { AuthMethod };
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* JSON Schema type (simplified for our use case)
|
|
@@ -201,12 +203,17 @@ export interface ValidationResult {
|
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
/**
|
|
204
|
-
*
|
|
206
|
+
* Single code example for a specific language
|
|
205
207
|
*/
|
|
206
|
-
export interface
|
|
207
|
-
curl
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
export interface CodeExample {
|
|
209
|
+
/** Language identifier (e.g., 'curl', 'python', 'go') */
|
|
210
|
+
id: string;
|
|
211
|
+
/** Display label for the tab (e.g., 'cURL', 'Python', 'Go') */
|
|
212
|
+
label: string;
|
|
213
|
+
/** Shiki language ID for syntax highlighting (e.g., 'bash', 'python', 'go') */
|
|
214
|
+
language: string;
|
|
215
|
+
/** Generated code string */
|
|
216
|
+
code: string;
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
/**
|
|
@@ -230,3 +237,20 @@ export interface CachedSpec {
|
|
|
230
237
|
version: '2.0' | '3.0' | '3.1';
|
|
231
238
|
timestamp: number;
|
|
232
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* User-supplied values from the API playground form.
|
|
243
|
+
* Passed to generateCodeExamplesFromValues to produce live code previews.
|
|
244
|
+
*/
|
|
245
|
+
export interface PlaygroundValues {
|
|
246
|
+
/** Raw credential value (token, key, base64 credentials) */
|
|
247
|
+
auth?: string;
|
|
248
|
+
/** Auth scheme from docs.json api.mdx.auth.method */
|
|
249
|
+
authMethod?: AuthMethod;
|
|
250
|
+
/** Custom header name for 'key' auth method, from docs.json api.mdx.auth.name */
|
|
251
|
+
authHeaderName?: string;
|
|
252
|
+
pathParams: Record<string, string>;
|
|
253
|
+
queryParams: Record<string, string>;
|
|
254
|
+
headerParams: Record<string, string>;
|
|
255
|
+
body: Record<string, unknown>;
|
|
256
|
+
}
|
|
@@ -317,8 +317,9 @@ export function ensureBlankLinesInJsx(content: string): string {
|
|
|
317
317
|
// Check it's not an HTML element or self-closing tag
|
|
318
318
|
if (/^<[a-z]/.test(trimmed)) return false;
|
|
319
319
|
|
|
320
|
-
//
|
|
321
|
-
|
|
320
|
+
// Code fences need blank line separation from JSX tags too — without it,
|
|
321
|
+
// MDX doesn't transition to markdown mode and the fence can be misinterpreted
|
|
322
|
+
if (/^```/.test(trimmed)) return true;
|
|
322
323
|
|
|
323
324
|
// Check it's not frontmatter
|
|
324
325
|
if (/^---/.test(trimmed)) return false;
|
|
@@ -409,6 +410,8 @@ export function normalizeJsxIndentation(content: string): string {
|
|
|
409
410
|
let baseIndent = 0; // The indentation of the outermost JSX opening tag
|
|
410
411
|
// Track if we're inside a fenced code block (``` ... ```)
|
|
411
412
|
let insideCodeFence = false;
|
|
413
|
+
// How many spaces to strip from code fence content (computed from fence marker normalization)
|
|
414
|
+
let fenceIndentShift = 0;
|
|
412
415
|
|
|
413
416
|
for (let i = 0; i < lines.length; i++) {
|
|
414
417
|
let line = lines[i];
|
|
@@ -418,12 +421,38 @@ export function normalizeJsxIndentation(content: string): string {
|
|
|
418
421
|
// Must check BEFORE any other processing to preserve code block content
|
|
419
422
|
if (trimmed.startsWith('```')) {
|
|
420
423
|
insideCodeFence = !insideCodeFence;
|
|
424
|
+
if (!insideCodeFence) {
|
|
425
|
+
// Closing fence: normalize its indentation, then reset shift
|
|
426
|
+
if (jsxDepth > 0 && fenceIndentShift > 0) {
|
|
427
|
+
const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
428
|
+
const newIndent = Math.max(0, currentIndent - fenceIndentShift);
|
|
429
|
+
line = ' '.repeat(newIndent) + trimmed;
|
|
430
|
+
}
|
|
431
|
+
fenceIndentShift = 0;
|
|
432
|
+
} else if (jsxDepth > 0) {
|
|
433
|
+
// Opening fence: normalize indentation and compute shift for content
|
|
434
|
+
const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
435
|
+
const excessIndent = currentIndent - baseIndent;
|
|
436
|
+
if (excessIndent >= 4) {
|
|
437
|
+
const newIndent = baseIndent + 2;
|
|
438
|
+
fenceIndentShift = currentIndent - newIndent;
|
|
439
|
+
line = ' '.repeat(newIndent) + trimmed;
|
|
440
|
+
} else {
|
|
441
|
+
fenceIndentShift = 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
421
444
|
result.push(line);
|
|
422
445
|
continue;
|
|
423
446
|
}
|
|
424
447
|
|
|
425
|
-
// If inside a code fence,
|
|
448
|
+
// If inside a code fence, apply the same indent shift as the fence marker
|
|
449
|
+
// to preserve relative indentation within the code block
|
|
426
450
|
if (insideCodeFence) {
|
|
451
|
+
if (fenceIndentShift > 0) {
|
|
452
|
+
const currentIndent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
453
|
+
const newIndent = Math.max(0, currentIndent - fenceIndentShift);
|
|
454
|
+
line = ' '.repeat(newIndent) + line.trimStart();
|
|
455
|
+
}
|
|
427
456
|
result.push(line);
|
|
428
457
|
continue;
|
|
429
458
|
}
|
|
@@ -700,6 +729,48 @@ export function transformImageDimensions(content: string): string {
|
|
|
700
729
|
return transformOutsideCodeBlocks(content, transformImageDimensionsInText);
|
|
701
730
|
}
|
|
702
731
|
|
|
732
|
+
// Keep in sync with VIDEO_EXTENSIONS in components/mdx/Video.tsx
|
|
733
|
+
const VIDEO_EXTENSIONS = ['.mp4', '.webm'];
|
|
734
|
+
|
|
735
|
+
/** Asset directory prefixes handled by path transforms */
|
|
736
|
+
const ASSET_DIRECTORIES = ['images', 'videos'] as const;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Convert markdown video syntax to <Video> JSX component.
|
|
740
|
+
*
|
|
741
|
+
* Converts:
|
|
742
|
+
* -  -> <Video src="/videos/demo.mp4" title="alt" />
|
|
743
|
+
* -  -> <Video src="/videos/demo.webm" />
|
|
744
|
+
*
|
|
745
|
+
* This must run BEFORE path transforms so the <Video> src attribute gets
|
|
746
|
+
* rewritten by transformRelativeImagePaths. Converting to JSX also avoids
|
|
747
|
+
* the <p><video> hydration error — MDX wraps markdown images in <p>, but
|
|
748
|
+
* JSX components get their own block context.
|
|
749
|
+
*/
|
|
750
|
+
function transformMarkdownVideos(content: string): string {
|
|
751
|
+
return transformOutsideCodeBlocks(content, transformMarkdownVideosInText);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function transformMarkdownVideosInText(text: string): string {
|
|
755
|
+
// Same pattern as markdown images: 
|
|
756
|
+
const pattern = /!\[([^\]]*)\]\(([^)\s]+)(\s+["'][^"']*["'])?\)/g;
|
|
757
|
+
|
|
758
|
+
return text.replace(pattern, (match, alt, url) => {
|
|
759
|
+
const urlStr = url as string;
|
|
760
|
+
// Skip external URLs and data URIs — only convert local video paths
|
|
761
|
+
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('data:')) {
|
|
762
|
+
return match;
|
|
763
|
+
}
|
|
764
|
+
const pathOnly = urlStr.split('?')[0].toLowerCase();
|
|
765
|
+
if (!VIDEO_EXTENSIONS.some(ext => pathOnly.endsWith(ext))) {
|
|
766
|
+
return match; // Not a video — leave as markdown image
|
|
767
|
+
}
|
|
768
|
+
const escapedAlt = (alt as string).replace(/"/g, '"');
|
|
769
|
+
const titleAttr = escapedAlt ? ` title="${escapedAlt}"` : '';
|
|
770
|
+
return `<Video src="${urlStr}"${titleAttr} />`;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
703
774
|
/**
|
|
704
775
|
* Transform Markdown image paths to use the Jamdesk asset prefix.
|
|
705
776
|
*
|
|
@@ -735,20 +806,11 @@ function normalizeMarkdownImageUrl(url: string, assetVersion?: string): string |
|
|
|
735
806
|
return null;
|
|
736
807
|
}
|
|
737
808
|
|
|
738
|
-
|
|
739
|
-
return null;
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
return appendAssetVersion(`${ASSET_PREFIX}${url}`, assetVersion);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (url.startsWith('./images/')) {
|
|
747
|
-
return appendAssetVersion(`${ASSET_PREFIX}/${url.slice(2)}`, assetVersion);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (url.startsWith('images/')) {
|
|
751
|
-
return appendAssetVersion(`${ASSET_PREFIX}/${url}`, assetVersion);
|
|
809
|
+
for (const dir of ASSET_DIRECTORIES) {
|
|
810
|
+
if (url.startsWith(`${ASSET_PREFIX}/${dir}/`)) return null;
|
|
811
|
+
if (url.startsWith(`/${dir}/`)) return appendAssetVersion(`${ASSET_PREFIX}${url}`, assetVersion);
|
|
812
|
+
if (url.startsWith(`./${dir}/`)) return appendAssetVersion(`${ASSET_PREFIX}/${url.slice(2)}`, assetVersion);
|
|
813
|
+
if (url.startsWith(`${dir}/`)) return appendAssetVersion(`${ASSET_PREFIX}/${url}`, assetVersion);
|
|
752
814
|
}
|
|
753
815
|
|
|
754
816
|
return null;
|
|
@@ -800,24 +862,23 @@ export function transformRelativeImagePaths(content: string, assetVersion?: stri
|
|
|
800
862
|
let result = content;
|
|
801
863
|
|
|
802
864
|
for (const prop of IMAGE_PATH_PROPS) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
865
|
+
for (const dir of ASSET_DIRECTORIES) {
|
|
866
|
+
const rewrite = (_match: string, quote: string, assetPath: string): string =>
|
|
867
|
+
`${prop}=${quote}${appendAssetVersion(`${ASSET_PREFIX}/${assetPath}`, assetVersion)}${quote}`;
|
|
806
868
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
);
|
|
869
|
+
// ./{dir}/... -> /_jd/{dir}/...
|
|
870
|
+
result = result.replace(
|
|
871
|
+
new RegExp(`${prop}=(["'])\\./(${dir}/[^"']+)\\1`, 'g'), rewrite,
|
|
872
|
+
);
|
|
873
|
+
// {dir}/... -> /_jd/{dir}/... (skip URLs, data URIs, paths starting with /)
|
|
874
|
+
result = result.replace(
|
|
875
|
+
new RegExp(`${prop}=(["'])(?!/|https?://|data:)(${dir}/[^"']+)\\1`, 'g'), rewrite,
|
|
876
|
+
);
|
|
877
|
+
// /{dir}/... -> /_jd/{dir}/...
|
|
878
|
+
result = result.replace(
|
|
879
|
+
new RegExp(`${prop}=(["'])/(${dir}/[^"']+)\\1`, 'g'), rewrite,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
821
882
|
}
|
|
822
883
|
|
|
823
884
|
return result;
|
|
@@ -863,8 +924,9 @@ export function containsView(content: string): boolean {
|
|
|
863
924
|
* 6. Un-indents JSX blocks that follow list items (prevents list context errors)
|
|
864
925
|
* 7. Ensures blank lines around markdown in JSX components
|
|
865
926
|
* 8. Normalizes indentation inside JSX components (prevents code block interpretation)
|
|
866
|
-
* 9.
|
|
867
|
-
* 10. Transforms
|
|
927
|
+
* 9. Converts markdown video syntax  to <Video> JSX
|
|
928
|
+
* 10. Transforms Markdown image dimension syntax to HTML img tags
|
|
929
|
+
* 11. Transforms relative image/video paths to absolute paths
|
|
868
930
|
*
|
|
869
931
|
* @param content - Raw MDX content
|
|
870
932
|
* @returns Preprocessed MDX content
|
|
@@ -901,6 +963,11 @@ export function preprocessMdx(content: string, options?: { assetVersion?: string
|
|
|
901
963
|
// 4+ space indentation from being interpreted as code blocks
|
|
902
964
|
processed = normalizeJsxIndentation(processed);
|
|
903
965
|
|
|
966
|
+
// Convert markdown video syntax  to <Video> JSX.
|
|
967
|
+
// Must run BEFORE transformImageDimensions and path transforms so the
|
|
968
|
+
// <Video> src attribute gets rewritten correctly.
|
|
969
|
+
processed = transformMarkdownVideos(processed);
|
|
970
|
+
|
|
904
971
|
// Transform Markdown images with dimension syntax to HTML img tags
|
|
905
972
|
// Must run BEFORE transformRelativeImagePaths (which works on src= attributes)
|
|
906
973
|
processed = transformImageDimensions(processed);
|
|
@@ -13,6 +13,8 @@ import remarkMdx from 'remark-mdx';
|
|
|
13
13
|
import { VFile } from 'vfile';
|
|
14
14
|
import { remarkExtractExports } from './remark-extract-exports';
|
|
15
15
|
import { compileInlineComponents } from './mdx-inline-components';
|
|
16
|
+
import { remarkExtractParamFields } from './remark-extract-param-fields';
|
|
17
|
+
import type { ExtractedParam } from './remark-extract-param-fields';
|
|
16
18
|
|
|
17
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
20
|
type AnyComponent = React.ComponentType<any>;
|
|
@@ -20,8 +22,17 @@ type AnyComponent = React.ComponentType<any>;
|
|
|
20
22
|
interface ProcessResult {
|
|
21
23
|
inlineComponents: Record<string, AnyComponent>;
|
|
22
24
|
hasInlineExports: boolean;
|
|
25
|
+
paramFields: ExtractedParam[];
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
// Build the unified processor once at module level (frozen + reusable).
|
|
29
|
+
// This avoids re-creating the pipeline on every ISR page request.
|
|
30
|
+
const mdxProcessor = unified()
|
|
31
|
+
.use(remarkParse)
|
|
32
|
+
.use(remarkMdx)
|
|
33
|
+
.use(remarkExtractExports)
|
|
34
|
+
.use(remarkExtractParamFields);
|
|
35
|
+
|
|
25
36
|
/**
|
|
26
37
|
* Extract and compile inline components from MDX source.
|
|
27
38
|
*
|
|
@@ -35,29 +46,25 @@ export async function extractInlineComponents(
|
|
|
35
46
|
): Promise<ProcessResult> {
|
|
36
47
|
// Defensive: return empty if source is not a valid string
|
|
37
48
|
if (!source || typeof source !== 'string') {
|
|
38
|
-
return { inlineComponents: {}, hasInlineExports: false };
|
|
49
|
+
return { inlineComponents: {}, hasInlineExports: false, paramFields: [] };
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
//
|
|
42
|
-
if (!source.includes('export const')) {
|
|
43
|
-
return { inlineComponents: {}, hasInlineExports: false };
|
|
52
|
+
// Fast-path: skip remark pipeline when neither inline exports nor ParamFields exist
|
|
53
|
+
if (!source.includes('export const') && !source.includes('ParamField')) {
|
|
54
|
+
return { inlineComponents: {}, hasInlineExports: false, paramFields: [] };
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
try {
|
|
47
|
-
const processor = unified().use(remarkParse).use(remarkMdx).use(remarkExtractExports);
|
|
48
|
-
|
|
49
|
-
// Create VFile to hold the content and collect metadata
|
|
50
58
|
const inputFile = new VFile({ value: source });
|
|
59
|
+
const tree = mdxProcessor.parse(inputFile);
|
|
60
|
+
await mdxProcessor.run(tree, inputFile);
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
const tree = processor.parse(inputFile);
|
|
54
|
-
await processor.run(tree, inputFile);
|
|
55
|
-
|
|
62
|
+
const paramFields: ExtractedParam[] = inputFile.data.paramFields ?? [];
|
|
56
63
|
const inlineExports = inputFile.data.inlineExports || {};
|
|
57
64
|
|
|
58
|
-
// No exports found
|
|
65
|
+
// No exports found — still return paramFields
|
|
59
66
|
if (Object.keys(inlineExports).length === 0) {
|
|
60
|
-
return { inlineComponents: {}, hasInlineExports: false };
|
|
67
|
+
return { inlineComponents: {}, hasInlineExports: false, paramFields };
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
// Compile with access to built-in MDX components
|
|
@@ -66,10 +73,11 @@ export async function extractInlineComponents(
|
|
|
66
73
|
return {
|
|
67
74
|
inlineComponents,
|
|
68
75
|
hasInlineExports: Object.keys(inlineExports).length > 0,
|
|
76
|
+
paramFields,
|
|
69
77
|
};
|
|
70
78
|
} catch (error) {
|
|
71
79
|
// Log but don't fail the build - return empty components
|
|
72
80
|
console.warn('[MDX] Failed to extract inline components:', error instanceof Error ? error.message : error);
|
|
73
|
-
return { inlineComponents: {}, hasInlineExports: false };
|
|
81
|
+
return { inlineComponents: {}, hasInlineExports: false, paramFields: [] };
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark plugin to extract <ParamField> data from MDX AST.
|
|
3
|
+
*
|
|
4
|
+
* Visits mdxJsxFlowElement nodes named 'ParamField' and extracts
|
|
5
|
+
* the param metadata (name, location, type, required, default) into
|
|
6
|
+
* file.data.paramFields for use by the API playground.
|
|
7
|
+
*
|
|
8
|
+
* ParamField syntax mirrors Mintlify's convention:
|
|
9
|
+
* <ParamField body="name" type="string" required>...</ParamField>
|
|
10
|
+
* The attribute name (body/query/path/header) determines the param location,
|
|
11
|
+
* and its string value is the param name.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Root } from 'mdast';
|
|
15
|
+
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
|
16
|
+
import { visit } from 'unist-util-visit';
|
|
17
|
+
import type { VFile } from 'vfile';
|
|
18
|
+
|
|
19
|
+
export interface ExtractedParam {
|
|
20
|
+
name: string;
|
|
21
|
+
in: 'body' | 'query' | 'path' | 'header';
|
|
22
|
+
type?: string;
|
|
23
|
+
required: boolean;
|
|
24
|
+
default?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module 'vfile' {
|
|
28
|
+
interface DataMap {
|
|
29
|
+
paramFields?: ExtractedParam[];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LOCATION_ATTRS = ['body', 'query', 'path', 'header'] as const;
|
|
34
|
+
type ParamLocation = (typeof LOCATION_ATTRS)[number];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse the raw value of a default attribute.
|
|
38
|
+
*
|
|
39
|
+
* Expression defaults like `default={1}` have their inner text as the raw
|
|
40
|
+
* value. We try JSON.parse first (handles numbers, booleans, quoted strings)
|
|
41
|
+
* and fall back to returning the raw string.
|
|
42
|
+
*/
|
|
43
|
+
function parseDefaultValue(raw: string): unknown {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function remarkExtractParamFields() {
|
|
52
|
+
return (tree: Root, file: VFile) => {
|
|
53
|
+
const params: ExtractedParam[] = [];
|
|
54
|
+
|
|
55
|
+
visit(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement) => {
|
|
56
|
+
if (node.name !== 'ParamField') return;
|
|
57
|
+
|
|
58
|
+
let paramName: string | undefined;
|
|
59
|
+
let paramIn: ParamLocation | undefined;
|
|
60
|
+
let paramType: string | undefined;
|
|
61
|
+
let paramRequired = false;
|
|
62
|
+
let paramDefault: unknown = undefined;
|
|
63
|
+
|
|
64
|
+
for (const attr of node.attributes) {
|
|
65
|
+
// Only process standard JSX attributes (not spread attributes)
|
|
66
|
+
if (attr.type !== 'mdxJsxAttribute') continue;
|
|
67
|
+
|
|
68
|
+
const attrName = attr.name as string;
|
|
69
|
+
const attrValue = attr.value;
|
|
70
|
+
|
|
71
|
+
// Location attribute: name is the location, value is the param name
|
|
72
|
+
if ((LOCATION_ATTRS as ReadonlyArray<string>).includes(attrName)) {
|
|
73
|
+
paramIn = attrName as ParamLocation;
|
|
74
|
+
// Value must be a plain string for the param name
|
|
75
|
+
if (typeof attrValue === 'string') {
|
|
76
|
+
paramName = attrValue;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (attrName === 'type') {
|
|
82
|
+
if (typeof attrValue === 'string') {
|
|
83
|
+
paramType = attrValue;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (attrName === 'required') {
|
|
89
|
+
if (attrValue === null) {
|
|
90
|
+
// Boolean attribute without value: <ParamField required>
|
|
91
|
+
paramRequired = true;
|
|
92
|
+
} else if (
|
|
93
|
+
typeof attrValue === 'object' &&
|
|
94
|
+
attrValue.type === 'mdxJsxAttributeValueExpression'
|
|
95
|
+
) {
|
|
96
|
+
// Expression syntax: required={true} or required={false}
|
|
97
|
+
paramRequired = parseDefaultValue(attrValue.value) === true;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (attrName === 'default') {
|
|
103
|
+
if (typeof attrValue === 'string') {
|
|
104
|
+
// Plain string default: default="created_at"
|
|
105
|
+
paramDefault = attrValue;
|
|
106
|
+
} else if (
|
|
107
|
+
attrValue != null &&
|
|
108
|
+
typeof attrValue === 'object' &&
|
|
109
|
+
attrValue.type === 'mdxJsxAttributeValueExpression'
|
|
110
|
+
) {
|
|
111
|
+
// Expression default: default={1}
|
|
112
|
+
paramDefault = parseDefaultValue(attrValue.value);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Only emit a param if we found a valid location + name pair
|
|
119
|
+
if (paramName !== undefined && paramIn !== undefined) {
|
|
120
|
+
params.push({
|
|
121
|
+
name: paramName,
|
|
122
|
+
in: paramIn,
|
|
123
|
+
type: paramType,
|
|
124
|
+
required: paramRequired,
|
|
125
|
+
default: paramDefault,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (params.length > 0) {
|
|
131
|
+
file.data.paramFields = params;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -27,6 +27,12 @@ const CLIENT_LANGUAGES: BundledLanguage[] = [
|
|
|
27
27
|
'bash',
|
|
28
28
|
'json',
|
|
29
29
|
'shell',
|
|
30
|
+
'go',
|
|
31
|
+
'ruby',
|
|
32
|
+
'csharp',
|
|
33
|
+
'java',
|
|
34
|
+
'rust',
|
|
35
|
+
'php',
|
|
30
36
|
];
|
|
31
37
|
|
|
32
38
|
// Dual themes for light/dark mode support
|
|
@@ -63,6 +69,12 @@ async function getHighlighter(): Promise<HighlighterCore> {
|
|
|
63
69
|
import('shiki/langs/bash.mjs'),
|
|
64
70
|
import('shiki/langs/json.mjs'),
|
|
65
71
|
import('shiki/langs/shell.mjs'),
|
|
72
|
+
import('shiki/langs/go.mjs'),
|
|
73
|
+
import('shiki/langs/ruby.mjs'),
|
|
74
|
+
import('shiki/langs/csharp.mjs'),
|
|
75
|
+
import('shiki/langs/java.mjs'),
|
|
76
|
+
import('shiki/langs/rust.mjs'),
|
|
77
|
+
import('shiki/langs/php.mjs'),
|
|
66
78
|
],
|
|
67
79
|
engine: createJavaScriptRegexEngine(),
|
|
68
80
|
});
|
|
@@ -25,7 +25,7 @@ export const STATIC_REVALIDATION_PATHS = STATIC_FILE_NAMES.flatMap(
|
|
|
25
25
|
const CONTENT_TYPES: Record<string, string> = {
|
|
26
26
|
'.xml': 'application/xml',
|
|
27
27
|
'.json': 'application/json',
|
|
28
|
-
'.txt': 'text/plain',
|
|
28
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// DO NOT EDIT — this file is auto-synced from shared/. Edit the source in shared/ and run ./scripts/sync-shared.sh
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SSRF protection: check if a URL is safe to proxy/fetch.
|
|
5
|
+
* Blocks private IPs, loopback, link-local, and metadata endpoints.
|
|
6
|
+
* Supports decimal, hex, octal, and mixed IP encodings (SSRF bypass vectors).
|
|
7
|
+
*
|
|
8
|
+
* Unlike sanitize-url.ts (proxy), this allows both HTTP and HTTPS
|
|
9
|
+
* since API playground requests may target staging/dev APIs over HTTP.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a hostname as an IPv4 address in any encoding.
|
|
14
|
+
* Handles dotted decimal, decimal integer, hex, octal, and mixed formats.
|
|
15
|
+
* Returns the 32-bit unsigned long, or null if not an IP.
|
|
16
|
+
*/
|
|
17
|
+
function parseIPv4ToLong(hostname: string): number | null {
|
|
18
|
+
// Pure decimal integer (e.g., 2130706433 = 127.0.0.1)
|
|
19
|
+
if (/^\d+$/.test(hostname)) {
|
|
20
|
+
const val = Number(hostname);
|
|
21
|
+
if (val >= 0 && val <= 0xFFFFFFFF) return val;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Hex integer (e.g., 0x7f000001 = 127.0.0.1)
|
|
25
|
+
if (/^0x[0-9a-f]+$/i.test(hostname)) {
|
|
26
|
+
const val = parseInt(hostname, 16);
|
|
27
|
+
if (val >= 0 && val <= 0xFFFFFFFF) return val;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Dotted notation (decimal, octal, or hex parts)
|
|
31
|
+
const parts = hostname.split('.');
|
|
32
|
+
if (parts.length < 1 || parts.length > 4) return null;
|
|
33
|
+
|
|
34
|
+
const parsed: number[] = [];
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
if (!part) return null;
|
|
37
|
+
let val: number;
|
|
38
|
+
if (/^0x[0-9a-f]+$/i.test(part)) {
|
|
39
|
+
val = parseInt(part, 16);
|
|
40
|
+
} else if (/^0\d+$/.test(part)) {
|
|
41
|
+
// Octal (e.g., 0177 = 127)
|
|
42
|
+
val = parseInt(part, 8);
|
|
43
|
+
} else if (/^\d+$/.test(part)) {
|
|
44
|
+
val = parseInt(part, 10);
|
|
45
|
+
} else {
|
|
46
|
+
return null; // Not a numeric IP
|
|
47
|
+
}
|
|
48
|
+
if (isNaN(val) || val < 0) return null;
|
|
49
|
+
parsed.push(val);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Convert to 32-bit long based on part count (matches BSD socket behavior)
|
|
53
|
+
if (parsed.length === 1) return parsed[0];
|
|
54
|
+
if (parsed.length === 2) {
|
|
55
|
+
if (parsed[0] > 0xFF || parsed[1] > 0xFFFFFF) return null;
|
|
56
|
+
return (parsed[0] << 24) | parsed[1];
|
|
57
|
+
}
|
|
58
|
+
if (parsed.length === 3) {
|
|
59
|
+
if (parsed[0] > 0xFF || parsed[1] > 0xFF || parsed[2] > 0xFFFF) return null;
|
|
60
|
+
return (parsed[0] << 24) | (parsed[1] << 16) | parsed[2];
|
|
61
|
+
}
|
|
62
|
+
if (parsed.length === 4) {
|
|
63
|
+
if (parsed.some(p => p > 0xFF)) return null;
|
|
64
|
+
return (parsed[0] << 24) | (parsed[1] << 16) | (parsed[2] << 8) | parsed[3];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if an IPv4 long integer falls in a private/reserved range.
|
|
71
|
+
*/
|
|
72
|
+
function isPrivateIPv4(ip: number): boolean {
|
|
73
|
+
// Use unsigned right-shift to ensure correct comparison for high addresses
|
|
74
|
+
const uip = ip >>> 0;
|
|
75
|
+
return (
|
|
76
|
+
(uip >>> 24) === 0 || // 0.0.0.0/8
|
|
77
|
+
(uip >>> 24) === 10 || // 10.0.0.0/8
|
|
78
|
+
(uip >>> 24) === 127 || // 127.0.0.0/8 (loopback)
|
|
79
|
+
(uip >>> 20) === 0xAC1 || // 172.16.0.0/12
|
|
80
|
+
(uip >>> 16) === 0xC0A8 || // 192.168.0.0/16
|
|
81
|
+
(uip >>> 16) === 0xA9FE || // 169.254.0.0/16 (link-local/metadata)
|
|
82
|
+
uip === 0xFFFFFFFF // 255.255.255.255 (broadcast)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns true if the URL is safe to fetch (not an internal/private address).
|
|
88
|
+
* Both HTTP and HTTPS are allowed — API playground may target dev APIs over HTTP.
|
|
89
|
+
* Returns false for invalid URLs, private IPs, localhost, and link-local addresses.
|
|
90
|
+
*/
|
|
91
|
+
export function isUrlSafe(rawUrl: string): boolean {
|
|
92
|
+
if (!rawUrl) return false;
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL(rawUrl);
|
|
95
|
+
|
|
96
|
+
// Only allow HTTP and HTTPS schemes
|
|
97
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
98
|
+
|
|
99
|
+
const hostname = url.hostname.toLowerCase();
|
|
100
|
+
|
|
101
|
+
// Block localhost by name
|
|
102
|
+
if (hostname === 'localhost') return false;
|
|
103
|
+
|
|
104
|
+
// Block ALL IPv6 addresses (bracketed hostnames).
|
|
105
|
+
// IPv6-embedded IPv4 (e.g. ::127.0.0.1 → ::7f00:1) bypasses prefix-based
|
|
106
|
+
// checks. Legitimate API endpoints virtually never use raw IPv6 literals,
|
|
107
|
+
// so blocking all bracketed hostnames is the safest approach.
|
|
108
|
+
if (hostname.startsWith('[')) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Parse as IPv4 (handles dotted, decimal, octal, hex, and mixed encodings)
|
|
113
|
+
const ipLong = parseIPv4ToLong(hostname);
|
|
114
|
+
if (ipLong !== null) {
|
|
115
|
+
if (isPrivateIPv4(ipLong)) return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/vendored/next.config.js
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
const nextConfig = {
|
|
15
15
|
// Hide dev indicator (floating icon in bottom-left)
|
|
16
16
|
devIndicators: false,
|
|
17
|
+
// Rewrite /_jd/ asset paths to public/ in dev (ISR middleware handles this in production)
|
|
18
|
+
async rewrites() {
|
|
19
|
+
return [
|
|
20
|
+
{ source: '/_jd/images/:path*', destination: '/images/:path*' },
|
|
21
|
+
{ source: '/_jd/videos/:path*', destination: '/videos/:path*' },
|
|
22
|
+
];
|
|
23
|
+
},
|
|
17
24
|
// Allow /_jd/ image paths with ?v= cache-busting query strings in <Image>
|
|
18
25
|
images: {
|
|
19
26
|
localPatterns: [
|