jamdesk 1.1.129 → 1.1.131
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/__tests__/unit/deps-sync.test.js +6 -1
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/dist/__tests__/unit/migrate-mdx.test.js +54 -0
- package/dist/__tests__/unit/migrate-mdx.test.js.map +1 -1
- package/dist/__tests__/unit/migrate-noindex-warning.test.d.ts +2 -0
- package/dist/__tests__/unit/migrate-noindex-warning.test.d.ts.map +1 -0
- package/dist/__tests__/unit/migrate-noindex-warning.test.js +115 -0
- package/dist/__tests__/unit/migrate-noindex-warning.test.js.map +1 -0
- package/dist/__tests__/unit/risky-expression-scanner-sync.test.d.ts +2 -0
- package/dist/__tests__/unit/risky-expression-scanner-sync.test.d.ts.map +1 -0
- package/dist/__tests__/unit/risky-expression-scanner-sync.test.js +43 -0
- package/dist/__tests__/unit/risky-expression-scanner-sync.test.js.map +1 -0
- package/dist/__tests__/unit/snippet-component-convert.test.d.ts +2 -0
- package/dist/__tests__/unit/snippet-component-convert.test.d.ts.map +1 -0
- package/dist/__tests__/unit/snippet-component-convert.test.js +105 -0
- package/dist/__tests__/unit/snippet-component-convert.test.js.map +1 -0
- package/dist/__tests__/unit/snippet-scanner-sync.test.d.ts +2 -0
- package/dist/__tests__/unit/snippet-scanner-sync.test.d.ts.map +1 -0
- package/dist/__tests__/unit/snippet-scanner-sync.test.js +43 -0
- package/dist/__tests__/unit/snippet-scanner-sync.test.js.map +1 -0
- package/dist/__tests__/unit/validate-risky-expressions.test.d.ts +17 -0
- package/dist/__tests__/unit/validate-risky-expressions.test.d.ts.map +1 -0
- package/dist/__tests__/unit/validate-risky-expressions.test.js +111 -0
- package/dist/__tests__/unit/validate-risky-expressions.test.js.map +1 -0
- package/dist/__tests__/unit/validate-snippets.test.d.ts +19 -0
- package/dist/__tests__/unit/validate-snippets.test.d.ts.map +1 -0
- package/dist/__tests__/unit/validate-snippets.test.js +190 -0
- package/dist/__tests__/unit/validate-snippets.test.js.map +1 -0
- package/dist/commands/migrate/convert-mdx.d.ts +12 -0
- package/dist/commands/migrate/convert-mdx.d.ts.map +1 -1
- package/dist/commands/migrate/convert-mdx.js +72 -2
- package/dist/commands/migrate/convert-mdx.js.map +1 -1
- package/dist/commands/migrate/convert.d.ts +11 -0
- package/dist/commands/migrate/convert.d.ts.map +1 -1
- package/dist/commands/migrate/convert.js +22 -0
- package/dist/commands/migrate/convert.js.map +1 -1
- package/dist/commands/migrate/index.d.ts.map +1 -1
- package/dist/commands/migrate/index.js +12 -8
- package/dist/commands/migrate/index.js.map +1 -1
- package/dist/commands/migrate/snippet-component-convert.d.ts +45 -0
- package/dist/commands/migrate/snippet-component-convert.d.ts.map +1 -0
- package/dist/commands/migrate/snippet-component-convert.js +222 -0
- package/dist/commands/migrate/snippet-component-convert.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +39 -0
- package/dist/commands/validate.js.map +1 -1
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +4 -1
- package/dist/lib/deps.js.map +1 -1
- package/dist/lib/risky-expression-scanner.d.ts +32 -0
- package/dist/lib/risky-expression-scanner.d.ts.map +1 -0
- package/dist/lib/risky-expression-scanner.js +195 -0
- package/dist/lib/risky-expression-scanner.js.map +1 -0
- package/dist/lib/snippet-scanner.d.ts +78 -0
- package/dist/lib/snippet-scanner.d.ts.map +1 -0
- package/dist/lib/snippet-scanner.js +198 -0
- package/dist/lib/snippet-scanner.js.map +1 -0
- package/dist/lib/validate-risky-expressions.d.ts +34 -0
- package/dist/lib/validate-risky-expressions.d.ts.map +1 -0
- package/dist/lib/validate-risky-expressions.js +54 -0
- package/dist/lib/validate-risky-expressions.js.map +1 -0
- package/dist/lib/validate-snippets.d.ts +37 -0
- package/dist/lib/validate-snippets.d.ts.map +1 -0
- package/dist/lib/validate-snippets.js +71 -0
- package/dist/lib/validate-snippets.js.map +1 -0
- package/package.json +7 -2
- package/vendored/components/errors/MdxRenderBoundary.tsx +52 -0
- package/vendored/components/mdx/MDXComponents.tsx +3 -0
- package/vendored/components/mdx/MdxErrorBlock.tsx +42 -0
- package/vendored/lib/isr-build-executor.ts +13 -0
- package/vendored/lib/recma-collect-missing-refs.ts +51 -0
- package/vendored/lib/recma-guard-expressions.ts +230 -0
- package/vendored/lib/render-doc-page.tsx +121 -30
- package/vendored/lib/risky-expression-scanner.ts +225 -0
- package/vendored/lib/snippet-scanner.ts +237 -0
- package/vendored/lib/static-artifacts.ts +22 -10
- package/vendored/lib/static-file-route.ts +19 -8
- package/vendored/shared/status-reporter.ts +1 -1
- package/vendored/workspace-package-lock.json +11 -10
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +0 -2
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +0 -1
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js +0 -112
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +0 -1
- package/dist/__tests__/unit/language-filter.test.d.ts +0 -2
- package/dist/__tests__/unit/language-filter.test.d.ts.map +0 -1
- package/dist/__tests__/unit/language-filter.test.js +0 -166
- package/dist/__tests__/unit/language-filter.test.js.map +0 -1
- package/dist/lib/language-filter.d.ts +0 -31
- package/dist/lib/language-filter.d.ts.map +0 -1
- package/dist/lib/language-filter.js +0 -14
- package/dist/lib/language-filter.js.map +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risky-expression Validation (CLI)
|
|
3
|
+
*
|
|
4
|
+
* Scans MDX pages for `{x, y}`-style expressions that reference an undefined
|
|
5
|
+
* identifier — the literal-looking-braces bug (MDX evaluates `{…}` as
|
|
6
|
+
* JavaScript). On the cloud these throw at render but are silently dropped by
|
|
7
|
+
* the recma guard (never a 500) and surfaced as a `risky_expression` build
|
|
8
|
+
* warning. The CLI runs the SAME scanner locally so authors catch the dropped
|
|
9
|
+
* expression before pushing.
|
|
10
|
+
*
|
|
11
|
+
* Reuses the pure scanner from risky-expression-scanner.ts (synced from
|
|
12
|
+
* build-service) so the matching logic is byte-identical to the cloud build —
|
|
13
|
+
* no drift. PRECISION over recall, by design (see the scanner's header).
|
|
14
|
+
*
|
|
15
|
+
* Scans EVERY project MDX file (not just navigation pages), mirroring the
|
|
16
|
+
* cloud build's per-file scan in build.ts.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs-extra';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { findRiskyExpressions } from './risky-expression-scanner.js';
|
|
21
|
+
const RISKY_EXPRESSION_WARNING_CAP = 50;
|
|
22
|
+
/**
|
|
23
|
+
* Validate risky `{…}` expressions across the project's MDX files.
|
|
24
|
+
*
|
|
25
|
+
* @param projectDir - Absolute path to the docs project root.
|
|
26
|
+
* @param mdxFiles - MDX/MD file paths to scan. Each may be absolute or
|
|
27
|
+
* relative to projectDir. Callers should pass the SAME
|
|
28
|
+
* full project walk used for image-ref/snippet validation.
|
|
29
|
+
* @returns Array of RiskyExpressionWarning (empty when clean). Never throws.
|
|
30
|
+
*/
|
|
31
|
+
export async function validateRiskyExpressions(projectDir, mdxFiles) {
|
|
32
|
+
const warnings = [];
|
|
33
|
+
for (const file of mdxFiles) {
|
|
34
|
+
if (warnings.length >= RISKY_EXPRESSION_WARNING_CAP)
|
|
35
|
+
break;
|
|
36
|
+
const abs = path.isAbsolute(file) ? file : path.join(projectDir, file);
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = await fs.readFile(abs, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue; // missing file is its own check elsewhere
|
|
43
|
+
}
|
|
44
|
+
const relFile = path.relative(projectDir, abs);
|
|
45
|
+
const issues = findRiskyExpressions(content, relFile);
|
|
46
|
+
for (const issue of issues) {
|
|
47
|
+
if (warnings.length >= RISKY_EXPRESSION_WARNING_CAP)
|
|
48
|
+
break;
|
|
49
|
+
warnings.push({ file: relFile, message: issue.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return warnings;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=validate-risky-expressions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-risky-expressions.js","sourceRoot":"","sources":["../../src/lib/validate-risky-expressions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AASrE,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,UAAkB,EAClB,QAAkB;IAElB,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAE9C,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,QAAQ,CAAC,MAAM,IAAI,4BAA4B;YAAE,MAAM;QAE3D,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACvE,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,0CAA0C;QACtD,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACtD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,QAAQ,CAAC,MAAM,IAAI,4BAA4B;gBAAE,MAAM;YAC3D,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snippet Validation (CLI)
|
|
3
|
+
*
|
|
4
|
+
* Scans MDX pages for snippet-related author issues that would surface as
|
|
5
|
+
* build warnings on the cloud: (1) `<Snippet>` tag usage, which Jamdesk does
|
|
6
|
+
* not support; (2) `import … from '/snippets/…'` that references a file that
|
|
7
|
+
* doesn't exist in the project's snippets/ directory.
|
|
8
|
+
*
|
|
9
|
+
* Reuses the pure scanner from snippet-scanner.ts (synced from build-service)
|
|
10
|
+
* so the matching logic is byte-identical to the cloud build — no drift.
|
|
11
|
+
*
|
|
12
|
+
* Scans EVERY project MDX file (not just navigation pages), mirroring the
|
|
13
|
+
* cloud build's per-file scan in build.ts — an orphan page with a `<Snippet>`
|
|
14
|
+
* tag warns at build time, so the CLI must surface it too.
|
|
15
|
+
*/
|
|
16
|
+
import { type SnippetIssue } from './snippet-scanner.js';
|
|
17
|
+
export interface SnippetWarning {
|
|
18
|
+
/** Page path relative to projectDir (e.g. "guide/intro.mdx") */
|
|
19
|
+
file: string;
|
|
20
|
+
/** Discriminates the two warning variants (drives the CLI hint copy). */
|
|
21
|
+
variant: SnippetIssue['variant'];
|
|
22
|
+
/** Author-facing guidance message */
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate snippet usage across the project's MDX files.
|
|
27
|
+
*
|
|
28
|
+
* @param projectDir - Absolute path to the docs project root.
|
|
29
|
+
* @param mdxFiles - MDX/MD file paths to scan. Each may be absolute or
|
|
30
|
+
* relative to projectDir. Callers should pass the SAME
|
|
31
|
+
* full project walk used for image-ref validation so the
|
|
32
|
+
* CLI surfaces the same issues as the cloud build
|
|
33
|
+
* (orphan pages included, not just navigation pages).
|
|
34
|
+
* @returns Array of SnippetWarning (empty when clean). Never throws.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateSnippets(projectDir: string, mdxFiles: string[]): Promise<SnippetWarning[]>;
|
|
37
|
+
//# sourceMappingURL=validate-snippets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-snippets.d.ts","sourceRoot":"","sources":["../../src/lib/validate-snippets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAA0C,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjG,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IACjC,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CAwB3B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snippet Validation (CLI)
|
|
3
|
+
*
|
|
4
|
+
* Scans MDX pages for snippet-related author issues that would surface as
|
|
5
|
+
* build warnings on the cloud: (1) `<Snippet>` tag usage, which Jamdesk does
|
|
6
|
+
* not support; (2) `import … from '/snippets/…'` that references a file that
|
|
7
|
+
* doesn't exist in the project's snippets/ directory.
|
|
8
|
+
*
|
|
9
|
+
* Reuses the pure scanner from snippet-scanner.ts (synced from build-service)
|
|
10
|
+
* so the matching logic is byte-identical to the cloud build — no drift.
|
|
11
|
+
*
|
|
12
|
+
* Scans EVERY project MDX file (not just navigation pages), mirroring the
|
|
13
|
+
* cloud build's per-file scan in build.ts — an orphan page with a `<Snippet>`
|
|
14
|
+
* tag warns at build time, so the CLI must surface it too.
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'fs-extra';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { findSnippetIssues, normalizeSnippetRef } from './snippet-scanner.js';
|
|
19
|
+
/**
|
|
20
|
+
* Walk the project's snippet files and return the extensionless key set used
|
|
21
|
+
* by `findSnippetIssues` for membership lookups.
|
|
22
|
+
*/
|
|
23
|
+
async function collectSnippetKeys(projectDir) {
|
|
24
|
+
const snippetsDir = path.join(projectDir, 'snippets');
|
|
25
|
+
if (!(await fs.pathExists(snippetsDir))) {
|
|
26
|
+
return new Set();
|
|
27
|
+
}
|
|
28
|
+
const { glob } = await import('glob');
|
|
29
|
+
const files = await glob('**/*.{mdx,tsx,jsx}', {
|
|
30
|
+
cwd: snippetsDir,
|
|
31
|
+
ignore: ['node_modules/**'],
|
|
32
|
+
});
|
|
33
|
+
return new Set(files.map(normalizeSnippetRef));
|
|
34
|
+
}
|
|
35
|
+
const SNIPPET_WARNING_CAP = 50;
|
|
36
|
+
/**
|
|
37
|
+
* Validate snippet usage across the project's MDX files.
|
|
38
|
+
*
|
|
39
|
+
* @param projectDir - Absolute path to the docs project root.
|
|
40
|
+
* @param mdxFiles - MDX/MD file paths to scan. Each may be absolute or
|
|
41
|
+
* relative to projectDir. Callers should pass the SAME
|
|
42
|
+
* full project walk used for image-ref validation so the
|
|
43
|
+
* CLI surfaces the same issues as the cloud build
|
|
44
|
+
* (orphan pages included, not just navigation pages).
|
|
45
|
+
* @returns Array of SnippetWarning (empty when clean). Never throws.
|
|
46
|
+
*/
|
|
47
|
+
export async function validateSnippets(projectDir, mdxFiles) {
|
|
48
|
+
const snippetKeys = await collectSnippetKeys(projectDir);
|
|
49
|
+
const warnings = [];
|
|
50
|
+
for (const file of mdxFiles) {
|
|
51
|
+
if (warnings.length >= SNIPPET_WARNING_CAP)
|
|
52
|
+
break;
|
|
53
|
+
const abs = path.isAbsolute(file) ? file : path.join(projectDir, file);
|
|
54
|
+
let content;
|
|
55
|
+
try {
|
|
56
|
+
content = await fs.readFile(abs, 'utf-8');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue; // missing file is its own check elsewhere
|
|
60
|
+
}
|
|
61
|
+
const relFile = path.relative(projectDir, abs);
|
|
62
|
+
const issues = findSnippetIssues(content, relFile, snippetKeys);
|
|
63
|
+
for (const issue of issues) {
|
|
64
|
+
if (warnings.length >= SNIPPET_WARNING_CAP)
|
|
65
|
+
break;
|
|
66
|
+
warnings.push({ file: relFile, variant: issue.variant, message: issue.message });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return warnings;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=validate-snippets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-snippets.js","sourceRoot":"","sources":["../../src/lib/validate-snippets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAqB,MAAM,sBAAsB,CAAC;AAWjG;;;GAGG;AACH,KAAK,UAAU,kBAAkB,CAAC,UAAkB;IAClD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;QACxC,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;IACD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,oBAAoB,EAAE;QAC7C,GAAG,EAAE,WAAW;QAChB,MAAM,EAAE,CAAC,iBAAiB,CAAC;KAC5B,CAAC,CAAC;IACH,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAE/B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAAkB,EAClB,QAAkB;IAElB,MAAM,WAAW,GAAG,MAAM,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAqB,EAAE,CAAC;IAEtC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,QAAQ,CAAC,MAAM,IAAI,mBAAmB;YAAE,MAAM;QAElD,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACvE,IAAI,OAAe,CAAC;QACpB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,0CAA0C;QACtD,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAChE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,QAAQ,CAAC,MAAM,IAAI,mBAAmB;gBAAE,MAAM;YAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.131",
|
|
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",
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"chalk": "^5.3.0",
|
|
109
109
|
"commander": "^14.0.3",
|
|
110
110
|
"dictionary-en": "^4.0.0",
|
|
111
|
+
"estree-util-visit": "^2.0.0",
|
|
111
112
|
"fastest-levenshtein": "^1.0.16",
|
|
112
113
|
"fs-extra": "^11.2.0",
|
|
113
114
|
"github-slugger": "^2.0.0",
|
|
@@ -118,7 +119,11 @@
|
|
|
118
119
|
"nspell": "^2.1.5",
|
|
119
120
|
"open": "^11.0.0",
|
|
120
121
|
"ora": "^9.4.0",
|
|
121
|
-
"
|
|
122
|
+
"remark-mdx": "^3.1.1",
|
|
123
|
+
"remark-parse": "^11.0.0",
|
|
124
|
+
"tar": "^7.5.15",
|
|
125
|
+
"unified": "^11.0.5",
|
|
126
|
+
"unist-util-visit": "^5.1.0"
|
|
122
127
|
},
|
|
123
128
|
"devDependencies": {
|
|
124
129
|
"@mdx-js/mdx": "^3.1.1",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MdxRenderBoundary — request-time error boundary around `<MDXRemote>`.
|
|
5
|
+
*
|
|
6
|
+
* User MDX is compiled and rendered at request time. A render-time throw —
|
|
7
|
+
* an undefined component ("Expected component `Snippet` to be defined") or a
|
|
8
|
+
* bare `{x, y}` expression (ReferenceError) — currently returns HTTP 500 with
|
|
9
|
+
* no boundary. These errors throw at RENDER (not compile), so only a render
|
|
10
|
+
* boundary can catch them. The compiled fallback is therefore the last line of
|
|
11
|
+
* defense for "docs never 500".
|
|
12
|
+
*
|
|
13
|
+
* Hand-rolled class component (React error boundaries require a class; there is
|
|
14
|
+
* no Hooks equivalent). NO new dependency — `react-error-boundary` is
|
|
15
|
+
* deliberately not used.
|
|
16
|
+
*
|
|
17
|
+
* `fallback` is a server-rendered element constructed by the server parent
|
|
18
|
+
* (e.g. `<MdxErrorBlock/>`, a server component) and passed in as a prop, so the
|
|
19
|
+
* client boundary never needs to import a server component.
|
|
20
|
+
*/
|
|
21
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
22
|
+
|
|
23
|
+
interface MdxRenderBoundaryProps {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
fallback: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MdxRenderBoundaryState {
|
|
29
|
+
hasError: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class MdxRenderBoundary extends Component<
|
|
33
|
+
MdxRenderBoundaryProps,
|
|
34
|
+
MdxRenderBoundaryState
|
|
35
|
+
> {
|
|
36
|
+
state: MdxRenderBoundaryState = { hasError: false };
|
|
37
|
+
|
|
38
|
+
static getDerivedStateFromError(): MdxRenderBoundaryState {
|
|
39
|
+
return { hasError: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
43
|
+
console.error('[mdx-render-boundary] caught render error', error);
|
|
44
|
+
// componentStack names the compiled MDX node that threw — the most useful
|
|
45
|
+
// triage signal when user MDX 500s in production.
|
|
46
|
+
console.error('[mdx-render-boundary] component stack', errorInfo.componentStack);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
render() {
|
|
50
|
+
return this.state.hasError ? this.props.fallback : this.props.children;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -43,6 +43,7 @@ import { View, ViewProvider, ViewSelector, ViewWrapper } from './View';
|
|
|
43
43
|
import { YouTube } from './YouTube';
|
|
44
44
|
import { Video } from './Video';
|
|
45
45
|
import { Visibility } from './Visibility';
|
|
46
|
+
import { MdxErrorBlock } from './MdxErrorBlock';
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Extract language from a pre element for tab label
|
|
@@ -253,6 +254,8 @@ export const MDXComponents = {
|
|
|
253
254
|
// search index). This React component is a fail-closed fallback for
|
|
254
255
|
// any <Visibility> that slips past both filters.
|
|
255
256
|
Visibility,
|
|
257
|
+
// Mintlify-style <Snippet file=…/> is unsupported (snippets are import-based) — placeholder, never throw.
|
|
258
|
+
Snippet: ({ file }: { file?: string }) => <MdxErrorBlock label={file ? `snippet ${file}` : 'snippet'} />,
|
|
256
259
|
// Sized images from preprocess-mdx ( syntax).
|
|
257
260
|
// These are output as <SizedImage> JSX so they go through component mapping
|
|
258
261
|
// (raw <img> JSX in MDX bypasses the components provider).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MdxErrorBlock — inline placeholder shown in place of MDX content that failed
|
|
3
|
+
* to render at request time (an undefined component, a bare `{x, y}` expression
|
|
4
|
+
* that throws a ReferenceError, etc.). Rendered as the fallback of
|
|
5
|
+
* `MdxRenderBoundary` so a single broken page degrades to a visible note
|
|
6
|
+
* instead of an HTTP 500.
|
|
7
|
+
*
|
|
8
|
+
* Server component, no client JS. Uses inline `style` with CSS-var fallbacks
|
|
9
|
+
* (mirrors OpenApiError) — docs themes don't guarantee Tailwind utilities are
|
|
10
|
+
* compiled, so Tailwind classes can't be relied on here. `role="note"` (a
|
|
11
|
+
* softer signal than OpenApiError's `role="alert"`) because a per-block render
|
|
12
|
+
* failure is informational, not a hard page-level error.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface MdxErrorBlockProps {
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MdxErrorBlock({ label }: MdxErrorBlockProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
role="note"
|
|
23
|
+
style={{
|
|
24
|
+
margin: '1rem 0',
|
|
25
|
+
padding: '0.75rem 1rem',
|
|
26
|
+
borderRadius: '8px',
|
|
27
|
+
border: '1px solid #f59e0b',
|
|
28
|
+
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
|
29
|
+
fontSize: '0.875rem',
|
|
30
|
+
lineHeight: 1.5,
|
|
31
|
+
color: 'var(--color-text-primary, #1e293b)',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<span style={{ fontWeight: 600 }}>⚠ This content couldn’t be displayed</span>
|
|
35
|
+
{label ? (
|
|
36
|
+
<span style={{ marginLeft: '0.375rem', color: 'var(--color-text-muted, #64748b)' }}>
|
|
37
|
+
({label})
|
|
38
|
+
</span>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -134,6 +134,19 @@ export function normalizeOpenApiRefKey(ref: unknown): string | null {
|
|
|
134
134
|
return normalized || null;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Re-export the pure snippet scanner so build.ts and the Task 3.2 test can
|
|
138
|
+
// continue importing from isr-build-executor without any changes, while the
|
|
139
|
+
// CLI (Task 3.4) imports from snippet-scanner.ts directly (no heavy deps).
|
|
140
|
+
export { normalizeSnippetRef, findSnippetIssues } from './snippet-scanner.js';
|
|
141
|
+
export type { SnippetIssue } from './snippet-scanner.js';
|
|
142
|
+
|
|
143
|
+
// Re-export the pure risky-expression scanner on the same channel. The build
|
|
144
|
+
// surfaces `risky_expression` warnings to tell authors which `{x, y}`-style
|
|
145
|
+
// expressions the recma render guard silently dropped (see
|
|
146
|
+
// lib/recma-guard-expressions.ts).
|
|
147
|
+
export { findRiskyExpressions } from './risky-expression-scanner.js';
|
|
148
|
+
export type { RiskyExpressionIssue } from './risky-expression-scanner.js';
|
|
149
|
+
|
|
137
150
|
/**
|
|
138
151
|
* Resolve a docs.json `api.openapi` value into the list of string refs the build
|
|
139
152
|
* collects/validates, plus any non-blocking warnings for forms the build can't
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { visit } from 'estree-util-visit';
|
|
2
|
+
import type { Program } from 'estree-jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* recmaCollectMissingRefs — names the components that user MDX references but
|
|
6
|
+
* the provider doesn't supply, so the caller can inject per-name placeholder
|
|
7
|
+
* fallbacks instead of letting `<Foo/>` throw `_missingMdxReference` at render
|
|
8
|
+
* (an HTTP 500). Part of the "docs never 500" wiring.
|
|
9
|
+
*
|
|
10
|
+
* MDX's compiled output emits, for every provider-expected component, a guard:
|
|
11
|
+
*
|
|
12
|
+
* if (!Foo) _missingMdxReference("Foo", true);
|
|
13
|
+
*
|
|
14
|
+
* The string-literal first argument is the referenced component name. Inline
|
|
15
|
+
* `export const X = …` components and imported components are LOCAL bindings,
|
|
16
|
+
* not provider lookups, so MDX never emits a `_missingMdxReference` for them —
|
|
17
|
+
* they don't appear here. (Verified against `@mdx-js/mdx` compiled output.)
|
|
18
|
+
*
|
|
19
|
+
* The plugin records the raw collected names on `collector.names`; the caller
|
|
20
|
+
* subtracts the provided component keys to get the genuinely-unknown set. We do
|
|
21
|
+
* NOT filter dotted names (e.g. `"Tree.Folder"`, emitted for `<Tree.Folder>`
|
|
22
|
+
* member access) here — the caller drops them, because a fallback keyed on a
|
|
23
|
+
* dotted string can't be looked up as a provider own-key anyway, and compound
|
|
24
|
+
* components are already redirected to their flat name by
|
|
25
|
+
* `recmaCompoundComponents` (which must run BEFORE this plugin in the
|
|
26
|
+
* `recmaPlugins` array so the flat-name rewrite is in place first).
|
|
27
|
+
*
|
|
28
|
+
* Factory-with-collector shape (not a bare plugin) because recma plugins can't
|
|
29
|
+
* return data — the caller reads `collector.names` after `compileMDX` resolves.
|
|
30
|
+
*/
|
|
31
|
+
export interface MissingRefCollector {
|
|
32
|
+
names: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recmaCollectMissingRefs(collector: MissingRefCollector) {
|
|
36
|
+
return () => (tree: Program) => {
|
|
37
|
+
visit(tree, (node) => {
|
|
38
|
+
if (
|
|
39
|
+
node.type !== 'CallExpression' ||
|
|
40
|
+
node.callee.type !== 'Identifier' ||
|
|
41
|
+
node.callee.name !== '_missingMdxReference'
|
|
42
|
+
) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const arg0 = node.arguments[0];
|
|
46
|
+
if (arg0 && arg0.type === 'Literal' && typeof arg0.value === 'string') {
|
|
47
|
+
collector.names.push(arg0.value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { generate } from 'astring';
|
|
2
|
+
import { visit } from 'estree-util-visit';
|
|
3
|
+
import type {
|
|
4
|
+
Program,
|
|
5
|
+
Node,
|
|
6
|
+
CallExpression,
|
|
7
|
+
Expression,
|
|
8
|
+
Property,
|
|
9
|
+
} from 'estree-jsx';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* recmaGuardExpressions — wraps every AUTHOR-WRITTEN MDX expression in a
|
|
13
|
+
* `try/catch` so a render-time throw degrades to a silent drop instead of an
|
|
14
|
+
* HTTP 500. The final, RSC-correct layer of "docs never 500".
|
|
15
|
+
*
|
|
16
|
+
* WHY a recma plugin and not an error boundary: user MDX is rendered at request
|
|
17
|
+
* time by `<MDXRemote>` (an async Server Component). A bare expression like
|
|
18
|
+
* `{x, y}` in prose compiles to a free `(x, y)` reference that throws
|
|
19
|
+
* `ReferenceError: x is not defined` at RENDER. A `'use client'` class error
|
|
20
|
+
* boundary CANNOT catch a throw from an async Server Component during the App
|
|
21
|
+
* Router server render — the throw bypasses it to the route `error.tsx` and
|
|
22
|
+
* returns 500 (verified at runtime: the boundary's `componentDidCatch` never
|
|
23
|
+
* fired). `renderToStaticMarkup` as a server-side trial render is also out — it
|
|
24
|
+
* false-positives on every async component ("a component suspended while
|
|
25
|
+
* responding to synchronous input"). The only RSC-safe place to neutralize the
|
|
26
|
+
* throw is the compiled output itself, here.
|
|
27
|
+
*
|
|
28
|
+
* WHAT it transforms: MDX compiles every author expression `{EXPR}` into a child
|
|
29
|
+
* (or attribute) value inside a `_jsx(...)` / `_jsxs(...)` call. This plugin
|
|
30
|
+
* finds those values and rewrites them. There are two catch behaviors by position:
|
|
31
|
+
*
|
|
32
|
+
* children/text position:
|
|
33
|
+
* EXPR → (() => { try { return (EXPR); }
|
|
34
|
+
* catch (e) { return e instanceof ReferenceError
|
|
35
|
+
* ? "{<literal>}" : undefined; } })()
|
|
36
|
+
* attribute position:
|
|
37
|
+
* EXPR → (() => { try { return (EXPR); } catch { return undefined; } })()
|
|
38
|
+
*
|
|
39
|
+
* On any throw the expression yields `undefined`, which React renders as nothing
|
|
40
|
+
* — the one broken node vanishes and the rest of the page renders normally. A
|
|
41
|
+
* valid expression returns exactly what it did before (the IIFE only adds a
|
|
42
|
+
* catch), so there is no behavioral change for working content and no
|
|
43
|
+
* double-render or async false-positive.
|
|
44
|
+
*
|
|
45
|
+
* RENDER-LITERAL refinement (children/text only): the most common author mistake
|
|
46
|
+
* is prose with literal braces or a typo'd variable — `coordinates: {x, y} pixel
|
|
47
|
+
* positions`. MDX evaluates `{x, y}` as JS and `x` is undefined → `ReferenceError`
|
|
48
|
+
* → the text used to vanish (`coordinates: pixel positions`). For text positions
|
|
49
|
+
* we instead render the literal source the author typed (`{x, y}`), reconstructed
|
|
50
|
+
* at COMPILE time via `astring` and embedded as a string. This applies ONLY to
|
|
51
|
+
* `ReferenceError` (undefined identifier — the "author meant literal / typo'd a
|
|
52
|
+
* ref" class); every other throw (e.g. a `TypeError` from member access on a
|
|
53
|
+
* defined-but-null value — a genuinely broken template) still returns `undefined`
|
|
54
|
+
* and drops, exactly as before. Attribute expressions never render a literal — a
|
|
55
|
+
* literal string in a prop is meaningless — so they keep the plain `undefined`
|
|
56
|
+
* catch.
|
|
57
|
+
*
|
|
58
|
+
* Author signal lives elsewhere: dropped expressions are surfaced to the author
|
|
59
|
+
* via the build-time `risky_expression` warning, NOT a per-render `console.*`
|
|
60
|
+
* here — a per-render log would flood SSR logs on every hit of a popular page
|
|
61
|
+
* (see builder/CLAUDE.md "No per-render console.warn in MDX components").
|
|
62
|
+
*
|
|
63
|
+
* SCOPE: covers expression throws (undefined identifiers, bad member access).
|
|
64
|
+
* It does NOT catch a *defined* component that throws inside its own render —
|
|
65
|
+
* `_jsx(Foo, props)` only constructs the element; `Foo`'s render throw is
|
|
66
|
+
* deferred past this layer. That residual class has no clean RSC-safe inline
|
|
67
|
+
* catch and was never observed in the wild.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
const JSX_CALLEES = new Set(['_jsx', '_jsxs', '_jsxDEV']);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A prop value is an "author expression" worth guarding when it is neither
|
|
74
|
+
* static text nor a compiler-emitted element/spread:
|
|
75
|
+
* - `Literal` → static text/number/string child; cannot throw.
|
|
76
|
+
* - `SpreadElement` → `{...props}`; structural, never an author expression.
|
|
77
|
+
* - a `_jsx*(…)` call → a nested element the compiler emitted; its own
|
|
78
|
+
* children get visited and guarded independently.
|
|
79
|
+
* Everything else in a `children`/attribute position came from author `{EXPR}`
|
|
80
|
+
* (identifiers, member access, calls, conditionals, sequences, objects) and is
|
|
81
|
+
* exactly what can throw at render — so it gets wrapped.
|
|
82
|
+
*/
|
|
83
|
+
function isAuthorExpr(node: Node | null | undefined): node is Expression {
|
|
84
|
+
if (!node || typeof (node as Node).type !== 'string') return false;
|
|
85
|
+
const type = (node as Node).type;
|
|
86
|
+
if (type === 'Literal') return false;
|
|
87
|
+
if (type === 'SpreadElement') return false;
|
|
88
|
+
if (type === 'JSXElement' || type === 'JSXFragment') return false;
|
|
89
|
+
if (
|
|
90
|
+
type === 'CallExpression' &&
|
|
91
|
+
(node as CallExpression).callee.type === 'Identifier' &&
|
|
92
|
+
JSX_CALLEES.has(((node as CallExpression).callee as { name: string }).name)
|
|
93
|
+
) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reconstruct an author expression's `{…}` source so a ReferenceError in a text
|
|
101
|
+
* position can render the literal the author typed instead of vanishing.
|
|
102
|
+
*
|
|
103
|
+
* astring wraps a top-level `SequenceExpression` in parens (`x, y` → `(x, y)`),
|
|
104
|
+
* but the author wrote `{x, y}` — so a sequence's parts are serialized
|
|
105
|
+
* individually and re-joined to avoid the spurious inner parens. Compact options
|
|
106
|
+
* keep multi-line nodes (objects) on a single line. Returns `null` if the node
|
|
107
|
+
* can't be serialized (unexpected node type), so the caller falls back to the
|
|
108
|
+
* plain `undefined`-returning guard rather than emitting broken output.
|
|
109
|
+
*/
|
|
110
|
+
function expressionToLiteral(expr: Expression): string | null {
|
|
111
|
+
const opts = { indent: '', lineEnd: '' } as const;
|
|
112
|
+
const ser = (node: Expression): string =>
|
|
113
|
+
generate(node as unknown as Parameters<typeof generate>[0], opts);
|
|
114
|
+
try {
|
|
115
|
+
const inner =
|
|
116
|
+
expr.type === 'SequenceExpression'
|
|
117
|
+
? expr.expressions.map((e) => ser(e as Expression)).join(', ')
|
|
118
|
+
: ser(expr);
|
|
119
|
+
return '{' + inner + '}';
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The identifier/string name of a `_jsx` prop key (`children`, `data-v`, …). */
|
|
126
|
+
function propKeyName(prop: Property): string | undefined {
|
|
127
|
+
const key = prop.key;
|
|
128
|
+
if (key.type === 'Identifier') return key.name;
|
|
129
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build the guarding IIFE around `expr`.
|
|
135
|
+
*
|
|
136
|
+
* `literal` (children/text positions only) is the `{…}` source to render when the
|
|
137
|
+
* expression throws a `ReferenceError`; pass `null` (attribute positions, or when
|
|
138
|
+
* serialization failed) to keep the plain `catch → undefined` behavior.
|
|
139
|
+
*
|
|
140
|
+
* literal: (() => { try { return (expr); }
|
|
141
|
+
* catch (e) { return e instanceof ReferenceError
|
|
142
|
+
* ? "{literal}" : undefined; } })()
|
|
143
|
+
* no literal: (() => { try { return (expr); } catch { return undefined; } })()
|
|
144
|
+
*/
|
|
145
|
+
function guard(expr: Expression, literal: string | null): CallExpression {
|
|
146
|
+
const catchReturn: Expression =
|
|
147
|
+
literal === null
|
|
148
|
+
? { type: 'Identifier', name: 'undefined' }
|
|
149
|
+
: {
|
|
150
|
+
type: 'ConditionalExpression',
|
|
151
|
+
test: {
|
|
152
|
+
type: 'BinaryExpression',
|
|
153
|
+
operator: 'instanceof',
|
|
154
|
+
left: { type: 'Identifier', name: 'e' },
|
|
155
|
+
right: { type: 'Identifier', name: 'ReferenceError' },
|
|
156
|
+
},
|
|
157
|
+
consequent: { type: 'Literal', value: literal },
|
|
158
|
+
alternate: { type: 'Identifier', name: 'undefined' },
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
type: 'CallExpression',
|
|
162
|
+
optional: false,
|
|
163
|
+
arguments: [],
|
|
164
|
+
callee: {
|
|
165
|
+
type: 'ArrowFunctionExpression',
|
|
166
|
+
id: null,
|
|
167
|
+
expression: false,
|
|
168
|
+
generator: false,
|
|
169
|
+
async: false,
|
|
170
|
+
params: [],
|
|
171
|
+
body: {
|
|
172
|
+
type: 'BlockStatement',
|
|
173
|
+
body: [
|
|
174
|
+
{
|
|
175
|
+
type: 'TryStatement',
|
|
176
|
+
block: {
|
|
177
|
+
type: 'BlockStatement',
|
|
178
|
+
body: [{ type: 'ReturnStatement', argument: expr }],
|
|
179
|
+
},
|
|
180
|
+
handler: {
|
|
181
|
+
type: 'CatchClause',
|
|
182
|
+
param: literal === null ? null : { type: 'Identifier', name: 'e' },
|
|
183
|
+
body: {
|
|
184
|
+
type: 'BlockStatement',
|
|
185
|
+
body: [{ type: 'ReturnStatement', argument: catchReturn }],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
finalizer: null,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
} as CallExpression;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function recmaGuardExpressions() {
|
|
197
|
+
return (tree: Program) => {
|
|
198
|
+
visit(tree, (node) => {
|
|
199
|
+
if (
|
|
200
|
+
node.type !== 'CallExpression' ||
|
|
201
|
+
node.callee.type !== 'Identifier' ||
|
|
202
|
+
!JSX_CALLEES.has(node.callee.name)
|
|
203
|
+
) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const props = node.arguments[1];
|
|
207
|
+
if (!props || props.type !== 'ObjectExpression') return;
|
|
208
|
+
|
|
209
|
+
for (const prop of props.properties) {
|
|
210
|
+
if (prop.type !== 'Property') continue;
|
|
211
|
+
// Render-literal fallback applies only to text flow (`children`); an
|
|
212
|
+
// attribute expression that throws degrades to `undefined` (a literal
|
|
213
|
+
// string in a prop slot is meaningless).
|
|
214
|
+
const isText = propKeyName(prop as Property) === 'children';
|
|
215
|
+
const literalOf = (e: Expression): string | null =>
|
|
216
|
+
isText ? expressionToLiteral(e) : null;
|
|
217
|
+
const value = (prop as Property).value;
|
|
218
|
+
if (value.type === 'ArrayExpression') {
|
|
219
|
+
// `children: [ "text", _jsx(...), (x, y), ... ]`
|
|
220
|
+
value.elements = value.elements.map((el) =>
|
|
221
|
+
isAuthorExpr(el) ? guard(el, literalOf(el)) : el,
|
|
222
|
+
);
|
|
223
|
+
} else if (isAuthorExpr(value)) {
|
|
224
|
+
// `children: x` (single child) or `someAttr: undefinedRef`
|
|
225
|
+
(prop as Property).value = guard(value, literalOf(value));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
}
|