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/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.errors.push({
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.errors.push(err)
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.errors.push({
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.errors.push(err)
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.errors.push(err),
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.errors.push({
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.errors.push({
751
+ this.pushError({
752
+ type: 'expression',
721
753
  message: `Failed to evaluate expression: ${
722
754
  node.value
723
755
  }. ${