safe-mdx 1.4.0 → 1.5.0
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 +191 -5
- package/dist/esm-parser.d.ts.map +1 -1
- package/dist/esm-parser.js +2 -0
- package/dist/esm-parser.js.map +1 -1
- package/dist/esm-parser.test.js +3 -0
- package/dist/esm-parser.test.js.map +1 -1
- package/dist/safe-mdx.d.ts +11 -1
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +29 -9
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +74 -0
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +1 -1
- package/src/esm-parser.test.ts +3 -0
- package/src/esm-parser.ts +2 -0
- package/src/safe-mdx.test.tsx +76 -0
- package/src/safe-mdx.tsx +39 -7
package/src/safe-mdx.tsx
CHANGED
|
@@ -29,7 +29,10 @@ export type RenderNode = (
|
|
|
29
29
|
transform: (node: MyRootContent) => ReactNode,
|
|
30
30
|
) => ReactNode | undefined
|
|
31
31
|
|
|
32
|
+
export type SafeMdxErrorType = 'validation' | 'missing-component' | 'expression' | 'esm-import'
|
|
33
|
+
|
|
32
34
|
export interface SafeMdxError {
|
|
35
|
+
type: SafeMdxErrorType
|
|
33
36
|
message: string
|
|
34
37
|
line?: number
|
|
35
38
|
schemaPath?: string
|
|
@@ -54,6 +57,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
54
57
|
addMarkdownLineNumbers = false,
|
|
55
58
|
modules,
|
|
56
59
|
baseUrl,
|
|
60
|
+
onError,
|
|
57
61
|
}: {
|
|
58
62
|
components?: ComponentsMap
|
|
59
63
|
markdown?: string
|
|
@@ -70,6 +74,9 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
70
74
|
/** Directory of the current MDX file, used to resolve relative import
|
|
71
75
|
* sources against `modules` keys. E.g. `'./pages/getting-started/'` */
|
|
72
76
|
baseUrl?: string
|
|
77
|
+
/** Called for each error during rendering (missing components, invalid props, failed expressions).
|
|
78
|
+
* Throw inside this callback to stop rendering on first error. */
|
|
79
|
+
onError?: (error: SafeMdxError) => void
|
|
73
80
|
}) {
|
|
74
81
|
const visitor = new MdastToJsx({
|
|
75
82
|
markdown,
|
|
@@ -82,6 +89,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
82
89
|
addMarkdownLineNumbers,
|
|
83
90
|
modules,
|
|
84
91
|
baseUrl,
|
|
92
|
+
onError,
|
|
85
93
|
})
|
|
86
94
|
const result = visitor.run()
|
|
87
95
|
return result
|
|
@@ -101,6 +109,7 @@ export class MdastToJsx {
|
|
|
101
109
|
addMarkdownLineNumbers: boolean
|
|
102
110
|
modules?: EagerModules
|
|
103
111
|
baseUrl?: string
|
|
112
|
+
onError?: (error: SafeMdxError) => void
|
|
104
113
|
|
|
105
114
|
constructor({
|
|
106
115
|
markdown: code = '',
|
|
@@ -113,6 +122,7 @@ export class MdastToJsx {
|
|
|
113
122
|
addMarkdownLineNumbers = false,
|
|
114
123
|
modules,
|
|
115
124
|
baseUrl,
|
|
125
|
+
onError,
|
|
116
126
|
}: {
|
|
117
127
|
markdown?: string
|
|
118
128
|
mdast: MyRootContent
|
|
@@ -127,6 +137,9 @@ export class MdastToJsx {
|
|
|
127
137
|
addMarkdownLineNumbers?: boolean
|
|
128
138
|
modules?: EagerModules
|
|
129
139
|
baseUrl?: string
|
|
140
|
+
/** Called for each error during rendering (missing components, invalid props, failed expressions).
|
|
141
|
+
* Throw inside this callback to stop rendering on first error. */
|
|
142
|
+
onError?: (error: SafeMdxError) => void
|
|
130
143
|
}) {
|
|
131
144
|
this.str = code
|
|
132
145
|
|
|
@@ -144,6 +157,7 @@ export class MdastToJsx {
|
|
|
144
157
|
|
|
145
158
|
this.modules = modules
|
|
146
159
|
this.baseUrl = baseUrl
|
|
160
|
+
this.onError = onError
|
|
147
161
|
|
|
148
162
|
this.c = {
|
|
149
163
|
...Object.fromEntries(
|
|
@@ -156,6 +170,11 @@ export class MdastToJsx {
|
|
|
156
170
|
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
pushError(error: SafeMdxError): void {
|
|
174
|
+
this.errors.push(error)
|
|
175
|
+
this.onError?.(error)
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
/**
|
|
160
179
|
* Resolve import declarations from an mdxjsEsm node against `this.modules`.
|
|
161
180
|
* Resolved components are added directly to `this.c` (the component map)
|
|
@@ -234,7 +253,8 @@ export class MdastToJsx {
|
|
|
234
253
|
if (result.issues) {
|
|
235
254
|
result.issues.forEach((issue) => {
|
|
236
255
|
const propPath = issue.path?.join('.') || 'unknown'
|
|
237
|
-
this.
|
|
256
|
+
this.pushError({
|
|
257
|
+
type: 'validation',
|
|
238
258
|
message: `Invalid props for component "${componentName}" at "${propPath}": ${issue.message}`,
|
|
239
259
|
line,
|
|
240
260
|
schemaPath: issue.path?.join('.'),
|
|
@@ -302,7 +322,7 @@ export class MdastToJsx {
|
|
|
302
322
|
extractComponentInfo(esmImportInfo)
|
|
303
323
|
Component = DynamicEsmComponent
|
|
304
324
|
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
305
|
-
this.
|
|
325
|
+
this.pushError(err)
|
|
306
326
|
})
|
|
307
327
|
let attrs = Object.fromEntries(attrsList)
|
|
308
328
|
|
|
@@ -318,7 +338,8 @@ export class MdastToJsx {
|
|
|
318
338
|
Component = accessWithDot(this.c, node.name)
|
|
319
339
|
|
|
320
340
|
if (!Component) {
|
|
321
|
-
this.
|
|
341
|
+
this.pushError({
|
|
342
|
+
type: 'missing-component',
|
|
322
343
|
message: `Unsupported jsx component ${node.name}`,
|
|
323
344
|
line: node.position?.start?.line,
|
|
324
345
|
})
|
|
@@ -327,7 +348,7 @@ export class MdastToJsx {
|
|
|
327
348
|
}
|
|
328
349
|
|
|
329
350
|
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
330
|
-
this.
|
|
351
|
+
this.pushError(err)
|
|
331
352
|
})
|
|
332
353
|
|
|
333
354
|
let attrs = Object.fromEntries(attrsList)
|
|
@@ -365,6 +386,7 @@ export class MdastToJsx {
|
|
|
365
386
|
: null
|
|
366
387
|
if (!tagName) {
|
|
367
388
|
onError?.({
|
|
389
|
+
type: 'expression',
|
|
368
390
|
message: 'JSX element missing component name',
|
|
369
391
|
line: line,
|
|
370
392
|
})
|
|
@@ -387,6 +409,7 @@ export class MdastToJsx {
|
|
|
387
409
|
Component = accessWithDot(this.c, tagName)
|
|
388
410
|
if (!Component) {
|
|
389
411
|
onError?.({
|
|
412
|
+
type: 'missing-component',
|
|
390
413
|
message: `Unsupported jsx component ${tagName} in attribute`,
|
|
391
414
|
line: line,
|
|
392
415
|
})
|
|
@@ -454,6 +477,7 @@ export class MdastToJsx {
|
|
|
454
477
|
} catch (error) {
|
|
455
478
|
// Return null if transformation fails
|
|
456
479
|
onError?.({
|
|
480
|
+
type: 'expression',
|
|
457
481
|
message: `Failed to transform JSX element: ${
|
|
458
482
|
error instanceof Error ? error.message : 'Unknown error'
|
|
459
483
|
}`,
|
|
@@ -495,6 +519,7 @@ export class MdastToJsx {
|
|
|
495
519
|
}
|
|
496
520
|
} catch (error) {
|
|
497
521
|
onError({
|
|
522
|
+
type: 'expression',
|
|
498
523
|
message: `Failed to evaluate expression attribute: ${attr.value
|
|
499
524
|
.replace(/\n+/g, ' ')
|
|
500
525
|
.replace(/ +/g, ' ')}. ${
|
|
@@ -508,6 +533,7 @@ export class MdastToJsx {
|
|
|
508
533
|
}
|
|
509
534
|
} catch (error) {
|
|
510
535
|
onError({
|
|
536
|
+
type: 'expression',
|
|
511
537
|
message: `Failed to evaluate expression attribute: ${attr.value
|
|
512
538
|
.replace(/\n+/g, ' ')
|
|
513
539
|
.replace(/ +/g, ' ')}. ${
|
|
@@ -520,6 +546,7 @@ export class MdastToJsx {
|
|
|
520
546
|
}
|
|
521
547
|
} else {
|
|
522
548
|
onError({
|
|
549
|
+
type: 'expression',
|
|
523
550
|
message: `Expressions in jsx props are not supported (${attr.value
|
|
524
551
|
.replace(/\n+/g, ' ')
|
|
525
552
|
.replace(/ +/g, ' ')})`,
|
|
@@ -531,6 +558,7 @@ export class MdastToJsx {
|
|
|
531
558
|
|
|
532
559
|
if (attr.type !== 'mdxJsxAttribute') {
|
|
533
560
|
onError({
|
|
561
|
+
type: 'expression',
|
|
534
562
|
message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
|
|
535
563
|
line: node.position?.start?.line,
|
|
536
564
|
})
|
|
@@ -597,6 +625,7 @@ export class MdastToJsx {
|
|
|
597
625
|
continue
|
|
598
626
|
} catch (error) {
|
|
599
627
|
onError({
|
|
628
|
+
type: 'expression',
|
|
600
629
|
message: `Failed to evaluate expression attribute: ${
|
|
601
630
|
attr.name
|
|
602
631
|
}={${v.value}}. ${
|
|
@@ -614,6 +643,7 @@ export class MdastToJsx {
|
|
|
614
643
|
}
|
|
615
644
|
|
|
616
645
|
onError({
|
|
646
|
+
type: 'expression',
|
|
617
647
|
message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
|
|
618
648
|
line: attr.position?.start?.line,
|
|
619
649
|
})
|
|
@@ -655,7 +685,7 @@ export class MdastToJsx {
|
|
|
655
685
|
// Parse ESM imports for client-side dynamic loading (only if allowed)
|
|
656
686
|
if (this.allowClientEsmImports) {
|
|
657
687
|
const parsedImports = parseEsmImports(node, (err) =>
|
|
658
|
-
this.
|
|
688
|
+
this.pushError(err),
|
|
659
689
|
)
|
|
660
690
|
parsedImports.forEach((value, key) => {
|
|
661
691
|
this.esmImports.set(key, value)
|
|
@@ -704,7 +734,8 @@ export class MdastToJsx {
|
|
|
704
734
|
Evaluate.evaluate.sync(expression)
|
|
705
735
|
return result
|
|
706
736
|
} catch (error) {
|
|
707
|
-
this.
|
|
737
|
+
this.pushError({
|
|
738
|
+
type: 'expression',
|
|
708
739
|
message: `Failed to evaluate expression: ${
|
|
709
740
|
node.value
|
|
710
741
|
}. ${
|
|
@@ -717,7 +748,8 @@ export class MdastToJsx {
|
|
|
717
748
|
}
|
|
718
749
|
}
|
|
719
750
|
} catch (error) {
|
|
720
|
-
this.
|
|
751
|
+
this.pushError({
|
|
752
|
+
type: 'expression',
|
|
721
753
|
message: `Failed to evaluate expression: ${
|
|
722
754
|
node.value
|
|
723
755
|
}. ${
|