safe-mdx 1.10.0 → 1.11.1
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/dynamic-esm-component.rsc.d.ts +2 -0
- package/dist/dynamic-esm-component.rsc.d.ts.map +1 -0
- package/dist/dynamic-esm-component.rsc.js +5 -0
- package/dist/dynamic-esm-component.rsc.js.map +1 -0
- package/dist/safe-mdx.d.ts +2 -1
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +210 -5
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +593 -0
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +2 -1
- package/src/dynamic-esm-component.rsc.tsx +5 -0
- package/src/safe-mdx.test.tsx +649 -0
- package/src/safe-mdx.tsx +213 -6
package/src/safe-mdx.test.tsx
CHANGED
|
@@ -3684,6 +3684,11 @@ test('modules prop: unresolved import produces error', () => {
|
|
|
3684
3684
|
expect(html).toMatchInlineSnapshot(`""`)
|
|
3685
3685
|
expect(visitor.errors).toMatchInlineSnapshot(`
|
|
3686
3686
|
[
|
|
3687
|
+
{
|
|
3688
|
+
"line": 1,
|
|
3689
|
+
"message": "Unresolved import "Missing" from "./nonexistent". The imported module could not be resolved, so these names are not available in the document.",
|
|
3690
|
+
"type": "expression",
|
|
3691
|
+
},
|
|
3687
3692
|
{
|
|
3688
3693
|
"line": 3,
|
|
3689
3694
|
"message": "Unsupported jsx component Missing",
|
|
@@ -4292,4 +4297,648 @@ test('scope with tagged template literal without generate', () => {
|
|
|
4292
4297
|
expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
|
|
4293
4298
|
})
|
|
4294
4299
|
|
|
4300
|
+
/* ── Error readability tests for agents ──────────────────────────────── */
|
|
4301
|
+
// These tests validate that safe-mdx produces clear, actionable error messages
|
|
4302
|
+
// with line numbers so AI agents can diagnose and fix MDX issues.
|
|
4303
|
+
|
|
4304
|
+
test('error: export function in MDX', () => {
|
|
4305
|
+
const code = dedent`
|
|
4306
|
+
export function MyHelper() {
|
|
4307
|
+
return "hello"
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
# Hello
|
|
4311
|
+
|
|
4312
|
+
<MyHelper />
|
|
4313
|
+
`
|
|
4314
|
+
|
|
4315
|
+
const { errors, html } = render(code)
|
|
4316
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4317
|
+
[
|
|
4318
|
+
{
|
|
4319
|
+
"line": 1,
|
|
4320
|
+
"message": "Unsupported named export "MyHelper". Export declarations are not evaluated, so exported values and components are not available in the document.",
|
|
4321
|
+
"type": "expression",
|
|
4322
|
+
},
|
|
4323
|
+
{
|
|
4324
|
+
"line": 7,
|
|
4325
|
+
"message": "Unsupported jsx component MyHelper",
|
|
4326
|
+
"type": "missing-component",
|
|
4327
|
+
},
|
|
4328
|
+
]
|
|
4329
|
+
`)
|
|
4330
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1>"`)
|
|
4331
|
+
})
|
|
4332
|
+
|
|
4333
|
+
test('error: export const in MDX', () => {
|
|
4334
|
+
const code = dedent`
|
|
4335
|
+
export const title = "My Page"
|
|
4336
|
+
export const items = [1, 2, 3]
|
|
4337
|
+
|
|
4338
|
+
# {title}
|
|
4339
|
+
|
|
4340
|
+
Items: {items.join(", ")}
|
|
4341
|
+
`
|
|
4342
|
+
|
|
4343
|
+
const { errors, html } = render(code)
|
|
4344
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4345
|
+
[
|
|
4346
|
+
{
|
|
4347
|
+
"line": 1,
|
|
4348
|
+
"message": "Unsupported named export "title". Export declarations are not evaluated, so exported values and components are not available in the document.",
|
|
4349
|
+
"type": "expression",
|
|
4350
|
+
},
|
|
4351
|
+
{
|
|
4352
|
+
"line": 2,
|
|
4353
|
+
"message": "Unsupported named export "items". Export declarations are not evaluated, so exported values and components are not available in the document.",
|
|
4354
|
+
"type": "expression",
|
|
4355
|
+
},
|
|
4356
|
+
{
|
|
4357
|
+
"line": 6,
|
|
4358
|
+
"message": "Failed to evaluate expression: items.join(", "). Functions are not supported",
|
|
4359
|
+
"type": "expression",
|
|
4360
|
+
},
|
|
4361
|
+
]
|
|
4362
|
+
`)
|
|
4363
|
+
expect(html).toMatchInlineSnapshot(`"<h1></h1><p>Items: </p>"`)
|
|
4364
|
+
})
|
|
4365
|
+
|
|
4366
|
+
test('error: import from non-existing file without modules prop', () => {
|
|
4367
|
+
const code = dedent`
|
|
4368
|
+
import { Card } from './components/card'
|
|
4369
|
+
import { Badge } from '../ui/badge'
|
|
4370
|
+
|
|
4371
|
+
# Hello
|
|
4372
|
+
|
|
4373
|
+
<Card title="test">content</Card>
|
|
4374
|
+
<Badge label="new" />
|
|
4375
|
+
`
|
|
4376
|
+
|
|
4377
|
+
const { errors, html } = render(code)
|
|
4378
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4379
|
+
[
|
|
4380
|
+
{
|
|
4381
|
+
"line": 1,
|
|
4382
|
+
"message": "Unresolved import "Card" from "./components/card". The imported module could not be resolved, so these names are not available in the document.",
|
|
4383
|
+
"type": "expression",
|
|
4384
|
+
},
|
|
4385
|
+
{
|
|
4386
|
+
"line": 2,
|
|
4387
|
+
"message": "Unresolved import "Badge" from "../ui/badge". The imported module could not be resolved, so these names are not available in the document.",
|
|
4388
|
+
"type": "expression",
|
|
4389
|
+
},
|
|
4390
|
+
{
|
|
4391
|
+
"line": 6,
|
|
4392
|
+
"message": "Unsupported jsx component Card",
|
|
4393
|
+
"type": "missing-component",
|
|
4394
|
+
},
|
|
4395
|
+
{
|
|
4396
|
+
"line": 7,
|
|
4397
|
+
"message": "Unsupported jsx component Badge",
|
|
4398
|
+
"type": "missing-component",
|
|
4399
|
+
},
|
|
4400
|
+
]
|
|
4401
|
+
`)
|
|
4402
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1>"`)
|
|
4403
|
+
})
|
|
4404
|
+
|
|
4405
|
+
test('error: import from non-existing file with modules prop (unresolved)', () => {
|
|
4406
|
+
const code = dedent`
|
|
4407
|
+
import { Card } from './components/card'
|
|
4408
|
+
|
|
4409
|
+
# Hello
|
|
4410
|
+
|
|
4411
|
+
<Card title="test">content</Card>
|
|
4412
|
+
`
|
|
4413
|
+
|
|
4414
|
+
const mdast = mdxParse(code)
|
|
4415
|
+
const visitor = new MdastToJsx({
|
|
4416
|
+
markdown: code,
|
|
4417
|
+
mdast,
|
|
4418
|
+
components,
|
|
4419
|
+
modules: {
|
|
4420
|
+
'./components/button.tsx': { Button: () => null },
|
|
4421
|
+
},
|
|
4422
|
+
baseUrl: './',
|
|
4423
|
+
})
|
|
4424
|
+
const result = visitor.run()
|
|
4425
|
+
const html = renderToStaticMarkup(result)
|
|
4426
|
+
expect(visitor.errors).toMatchInlineSnapshot(`
|
|
4427
|
+
[
|
|
4428
|
+
{
|
|
4429
|
+
"line": 1,
|
|
4430
|
+
"message": "Unresolved import "Card" from "./components/card". The imported module could not be resolved, so these names are not available in the document.",
|
|
4431
|
+
"type": "expression",
|
|
4432
|
+
},
|
|
4433
|
+
{
|
|
4434
|
+
"line": 5,
|
|
4435
|
+
"message": "Unsupported jsx component Card",
|
|
4436
|
+
"type": "missing-component",
|
|
4437
|
+
},
|
|
4438
|
+
]
|
|
4439
|
+
`)
|
|
4440
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1>"`)
|
|
4441
|
+
})
|
|
4442
|
+
|
|
4443
|
+
test('error: calling non-existing function in expression (no scope)', () => {
|
|
4444
|
+
const code = dedent`
|
|
4445
|
+
# Title
|
|
4446
|
+
|
|
4447
|
+
Result: {formatDate("2024-01-01")}
|
|
4448
|
+
|
|
4449
|
+
Value: {calculateTotal(100, 0.2)}
|
|
4450
|
+
`
|
|
4451
|
+
|
|
4452
|
+
const { errors, html } = render(code)
|
|
4453
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4454
|
+
[
|
|
4455
|
+
{
|
|
4456
|
+
"line": 3,
|
|
4457
|
+
"message": "Failed to evaluate expression: formatDate("2024-01-01"). Functions are not supported",
|
|
4458
|
+
"type": "expression",
|
|
4459
|
+
},
|
|
4460
|
+
{
|
|
4461
|
+
"line": 5,
|
|
4462
|
+
"message": "Failed to evaluate expression: calculateTotal(100, 0.2). Functions are not supported",
|
|
4463
|
+
"type": "expression",
|
|
4464
|
+
},
|
|
4465
|
+
]
|
|
4466
|
+
`)
|
|
4467
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Title</h1><p>Result: </p><p>Value: </p>"`)
|
|
4468
|
+
})
|
|
4469
|
+
|
|
4470
|
+
test('error: calling non-existing function in expression (with scope)', () => {
|
|
4471
|
+
const scope = {
|
|
4472
|
+
greeting: 'hello',
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
const code = dedent`
|
|
4476
|
+
# {greeting}
|
|
4477
|
+
|
|
4478
|
+
Result: {nonExistentFn("test")}
|
|
4479
|
+
|
|
4480
|
+
Upper: {greeting.toUpperCase()}
|
|
4481
|
+
`
|
|
4482
|
+
|
|
4483
|
+
const { errors, html } = render(code, undefined, undefined, undefined, scope)
|
|
4484
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4485
|
+
[
|
|
4486
|
+
{
|
|
4487
|
+
"line": 3,
|
|
4488
|
+
"message": "Failed to evaluate expression: nonExistentFn("test"). nonExistentFn is not defined. Available variables: greeting",
|
|
4489
|
+
"type": "expression",
|
|
4490
|
+
},
|
|
4491
|
+
]
|
|
4492
|
+
`)
|
|
4493
|
+
expect(html).toMatchInlineSnapshot(`"<h1>hello</h1><p>Result: </p><p>Upper: HELLO</p>"`)
|
|
4494
|
+
})
|
|
4495
|
+
|
|
4496
|
+
test('error: missing scope variable in expression (no scope at all)', () => {
|
|
4497
|
+
const code = dedent`
|
|
4498
|
+
# Hello
|
|
4499
|
+
|
|
4500
|
+
Name: {userName}
|
|
4501
|
+
|
|
4502
|
+
Count: {itemCount + 1}
|
|
4503
|
+
`
|
|
4504
|
+
|
|
4505
|
+
const { errors, html } = render(code)
|
|
4506
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4507
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1><p>Name: </p><p>Count: </p>"`)
|
|
4508
|
+
})
|
|
4509
|
+
|
|
4510
|
+
test('error: missing scope variable with strict mode', () => {
|
|
4511
|
+
const scope = {
|
|
4512
|
+
knownVar: 'exists',
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
const code = dedent`
|
|
4516
|
+
Known: {knownVar}
|
|
4517
|
+
|
|
4518
|
+
Missing: {unknownVar}
|
|
4519
|
+
`
|
|
4520
|
+
|
|
4521
|
+
const { errors, html } = render(code, undefined, undefined, undefined, scope, { strict: true })
|
|
4522
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4523
|
+
[
|
|
4524
|
+
{
|
|
4525
|
+
"line": 3,
|
|
4526
|
+
"message": "Failed to evaluate expression: unknownVar. unknownVar is not defined. Available variables: knownVar",
|
|
4527
|
+
"type": "expression",
|
|
4528
|
+
},
|
|
4529
|
+
]
|
|
4530
|
+
`)
|
|
4531
|
+
expect(html).toMatchInlineSnapshot(`"<p>Known: exists</p><p>Missing: </p>"`)
|
|
4532
|
+
})
|
|
4533
|
+
|
|
4534
|
+
test('error: multiple error types in single document', () => {
|
|
4535
|
+
const code = dedent`
|
|
4536
|
+
import { Missing } from './not-found'
|
|
4537
|
+
|
|
4538
|
+
# Title
|
|
4539
|
+
|
|
4540
|
+
<NonExistent>content</NonExistent>
|
|
4541
|
+
|
|
4542
|
+
{undefinedVar}
|
|
4543
|
+
|
|
4544
|
+
<Heading level={invalidFn()}>text</Heading>
|
|
4545
|
+
`
|
|
4546
|
+
|
|
4547
|
+
const { errors } = render(code)
|
|
4548
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4549
|
+
[
|
|
4550
|
+
{
|
|
4551
|
+
"line": 1,
|
|
4552
|
+
"message": "Unresolved import "Missing" from "./not-found". The imported module could not be resolved, so these names are not available in the document.",
|
|
4553
|
+
"type": "expression",
|
|
4554
|
+
},
|
|
4555
|
+
{
|
|
4556
|
+
"line": 5,
|
|
4557
|
+
"message": "Unsupported jsx component NonExistent",
|
|
4558
|
+
"type": "missing-component",
|
|
4559
|
+
},
|
|
4560
|
+
{
|
|
4561
|
+
"line": 9,
|
|
4562
|
+
"message": "Failed to evaluate expression attribute: level={invalidFn()}. Functions are not supported",
|
|
4563
|
+
"type": "expression",
|
|
4564
|
+
},
|
|
4565
|
+
{
|
|
4566
|
+
"line": 9,
|
|
4567
|
+
"message": "Expressions in jsx prop not evaluated: (level={invalidFn()})",
|
|
4568
|
+
"type": "expression",
|
|
4569
|
+
},
|
|
4570
|
+
]
|
|
4571
|
+
`)
|
|
4572
|
+
})
|
|
4573
|
+
|
|
4574
|
+
test('error: all errors have line numbers for agent debugging', () => {
|
|
4575
|
+
const code = dedent`
|
|
4576
|
+
# Line 1 heading
|
|
4577
|
+
|
|
4578
|
+
<Unknown1 />
|
|
4579
|
+
|
|
4580
|
+
Some text {badVar1}
|
|
4581
|
+
|
|
4582
|
+
<Unknown2 prop={badFn()} />
|
|
4583
|
+
|
|
4584
|
+
{badVar2 + 1}
|
|
4585
|
+
`
|
|
4586
|
+
|
|
4587
|
+
const { errors } = render(code)
|
|
4588
|
+
for (const err of errors) {
|
|
4589
|
+
expect(err.line).toBeDefined()
|
|
4590
|
+
expect(typeof err.line).toBe('number')
|
|
4591
|
+
expect(err.line).toBeGreaterThan(0)
|
|
4592
|
+
}
|
|
4593
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4594
|
+
[
|
|
4595
|
+
{
|
|
4596
|
+
"line": 3,
|
|
4597
|
+
"message": "Unsupported jsx component Unknown1",
|
|
4598
|
+
"type": "missing-component",
|
|
4599
|
+
},
|
|
4600
|
+
{
|
|
4601
|
+
"line": 7,
|
|
4602
|
+
"message": "Unsupported jsx component Unknown2",
|
|
4603
|
+
"type": "missing-component",
|
|
4604
|
+
},
|
|
4605
|
+
]
|
|
4606
|
+
`)
|
|
4607
|
+
})
|
|
4608
|
+
|
|
4609
|
+
test('error: export default in MDX', () => {
|
|
4610
|
+
const code = dedent`
|
|
4611
|
+
export default function Layout({ children }) {
|
|
4612
|
+
return children
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
# Hello
|
|
4616
|
+
|
|
4617
|
+
Some content
|
|
4618
|
+
`
|
|
4619
|
+
|
|
4620
|
+
const { errors, html } = render(code)
|
|
4621
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4622
|
+
[
|
|
4623
|
+
{
|
|
4624
|
+
"line": 1,
|
|
4625
|
+
"message": "Unsupported default export "Layout". Export declarations are not evaluated, so exported values and components are not available in the document.",
|
|
4626
|
+
"type": "expression",
|
|
4627
|
+
},
|
|
4628
|
+
]
|
|
4629
|
+
`)
|
|
4630
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1><p>Some content</p>"`)
|
|
4631
|
+
})
|
|
4632
|
+
|
|
4633
|
+
test('error: using exported const as component', () => {
|
|
4634
|
+
const code = dedent`
|
|
4635
|
+
export const Alert = ({ children }) => <div>{children}</div>
|
|
4636
|
+
|
|
4637
|
+
# Page
|
|
4638
|
+
|
|
4639
|
+
<Alert>This should fail</Alert>
|
|
4640
|
+
`
|
|
4641
|
+
|
|
4642
|
+
const { errors, html } = render(code)
|
|
4643
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4644
|
+
[
|
|
4645
|
+
{
|
|
4646
|
+
"line": 1,
|
|
4647
|
+
"message": "Unsupported named export "Alert". Export declarations are not evaluated, so exported values and components are not available in the document.",
|
|
4648
|
+
"type": "expression",
|
|
4649
|
+
},
|
|
4650
|
+
{
|
|
4651
|
+
"line": 5,
|
|
4652
|
+
"message": "Unsupported jsx component Alert",
|
|
4653
|
+
"type": "missing-component",
|
|
4654
|
+
},
|
|
4655
|
+
]
|
|
4656
|
+
`)
|
|
4657
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Page</h1>"`)
|
|
4658
|
+
})
|
|
4659
|
+
|
|
4660
|
+
test('error: invalid JSX expression in attribute', () => {
|
|
4661
|
+
const code = dedent`
|
|
4662
|
+
<Heading level={1 +}>broken expression</Heading>
|
|
4663
|
+
`
|
|
4664
|
+
|
|
4665
|
+
expect(() => mdxParse(code)).toThrow()
|
|
4666
|
+
})
|
|
4667
|
+
|
|
4668
|
+
test('error: nested missing components show correct line numbers', () => {
|
|
4669
|
+
const code = dedent`
|
|
4670
|
+
# Page Title
|
|
4671
|
+
|
|
4672
|
+
<Heading level={1}>
|
|
4673
|
+
Some text with <InlineWidget /> inside
|
|
4674
|
+
</Heading>
|
|
4675
|
+
|
|
4676
|
+
<Tabs items={["a", "b"]}>
|
|
4677
|
+
<TabPanel>
|
|
4678
|
+
<DeepNested>content</DeepNested>
|
|
4679
|
+
</TabPanel>
|
|
4680
|
+
</Tabs>
|
|
4681
|
+
`
|
|
4682
|
+
|
|
4683
|
+
const { errors } = render(code)
|
|
4684
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
4685
|
+
[
|
|
4686
|
+
{
|
|
4687
|
+
"line": 4,
|
|
4688
|
+
"message": "Unsupported jsx component InlineWidget",
|
|
4689
|
+
"type": "missing-component",
|
|
4690
|
+
},
|
|
4691
|
+
{
|
|
4692
|
+
"line": 8,
|
|
4693
|
+
"message": "Unsupported jsx component TabPanel",
|
|
4694
|
+
"type": "missing-component",
|
|
4695
|
+
},
|
|
4696
|
+
]
|
|
4697
|
+
`)
|
|
4698
|
+
})
|
|
4699
|
+
|
|
4700
|
+
/* ── Syntax error tests ──────────────────────────────────────────────── */
|
|
4701
|
+
// These tests verify that MDX parse errors produce readable messages with
|
|
4702
|
+
// line/column information so agents can locate and fix syntax issues.
|
|
4703
|
+
|
|
4704
|
+
function parseError(code: string) {
|
|
4705
|
+
try {
|
|
4706
|
+
mdxParse(code)
|
|
4707
|
+
return null
|
|
4708
|
+
} catch (e: any) {
|
|
4709
|
+
// remark/mdx errors store position in various places:
|
|
4710
|
+
// e.line/e.column, e.place, e.point, or e.name as "line:column"
|
|
4711
|
+
let line = e.line ?? e.place?.line ?? e.point?.line ?? null
|
|
4712
|
+
let column = e.column ?? e.place?.column ?? e.point?.column ?? null
|
|
4713
|
+
if (line == null && typeof e.name === 'string' && /^\d+:\d+/.test(e.name)) {
|
|
4714
|
+
const [l, c] = e.name.split(':').map(Number)
|
|
4715
|
+
line = l
|
|
4716
|
+
column = c
|
|
4717
|
+
}
|
|
4718
|
+
return {
|
|
4719
|
+
message: (e.reason ?? e.message?.split('\n')[0]) ?? String(e),
|
|
4720
|
+
line,
|
|
4721
|
+
column,
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
|
|
4726
|
+
test('syntax error: unclosed JSX tag', () => {
|
|
4727
|
+
const err = parseError(dedent`
|
|
4728
|
+
# Hello
|
|
4729
|
+
|
|
4730
|
+
<Card title="test">
|
|
4731
|
+
some content
|
|
4732
|
+
`)
|
|
4733
|
+
expect(err).toMatchInlineSnapshot(
|
|
4734
|
+
`
|
|
4735
|
+
{
|
|
4736
|
+
"column": 1,
|
|
4737
|
+
"line": 1,
|
|
4738
|
+
"message": "Expected a closing tag for \`<Card>\` (3:1-3:20)",
|
|
4739
|
+
}
|
|
4740
|
+
`)
|
|
4741
|
+
})
|
|
4742
|
+
|
|
4743
|
+
test('syntax error: mismatched closing tag', () => {
|
|
4744
|
+
const err = parseError(dedent`
|
|
4745
|
+
<Heading>text</Cards>
|
|
4746
|
+
`)
|
|
4747
|
+
expect(err).toMatchInlineSnapshot(
|
|
4748
|
+
`
|
|
4749
|
+
{
|
|
4750
|
+
"column": 14,
|
|
4751
|
+
"line": 1,
|
|
4752
|
+
"message": "Unexpected closing tag \`</Cards>\`, expected corresponding closing tag for \`<Heading>\` (1:1-1:10)",
|
|
4753
|
+
}
|
|
4754
|
+
`)
|
|
4755
|
+
})
|
|
4756
|
+
|
|
4757
|
+
test('syntax error: mismatched closing tag with markdown content inside', () => {
|
|
4758
|
+
const err = parseError(dedent`
|
|
4759
|
+
<Tip>
|
|
4760
|
+
If you like **Playwriter** please
|
|
4761
|
+
[star on GitHub](https://github.com/remorses/playwriter). It helps give more visibility to the project!
|
|
4762
|
+
</Tipx>
|
|
4763
|
+
`)
|
|
4764
|
+
expect(err).toMatchInlineSnapshot(`
|
|
4765
|
+
{
|
|
4766
|
+
"column": 1,
|
|
4767
|
+
"line": 4,
|
|
4768
|
+
"message": "Unexpected closing tag \`</Tipx>\`, expected corresponding closing tag for \`<Tip>\` (1:1-1:6)",
|
|
4769
|
+
}
|
|
4770
|
+
`)
|
|
4771
|
+
})
|
|
4772
|
+
|
|
4773
|
+
test('syntax error: mismatched opening tag with markdown content inside', () => {
|
|
4774
|
+
const err = parseError(dedent`
|
|
4775
|
+
<Tipx>
|
|
4776
|
+
If you like **Playwriter** please
|
|
4777
|
+
[star on GitHub](https://github.com/remorses/playwriter). It helps give more visibility to the project!
|
|
4778
|
+
</Tip>
|
|
4779
|
+
`)
|
|
4780
|
+
expect(err).toMatchInlineSnapshot(`
|
|
4781
|
+
{
|
|
4782
|
+
"column": 1,
|
|
4783
|
+
"line": 4,
|
|
4784
|
+
"message": "Unexpected closing tag \`</Tip>\`, expected corresponding closing tag for \`<Tipx>\` (1:1-1:7)",
|
|
4785
|
+
}
|
|
4786
|
+
`)
|
|
4787
|
+
})
|
|
4788
|
+
|
|
4789
|
+
test('syntax error: unclosed curly brace in expression', () => {
|
|
4790
|
+
const err = parseError(dedent`
|
|
4791
|
+
# Title
|
|
4792
|
+
|
|
4793
|
+
{something
|
|
4794
|
+
`)
|
|
4795
|
+
expect(err).toMatchInlineSnapshot(
|
|
4796
|
+
`
|
|
4797
|
+
{
|
|
4798
|
+
"column": 11,
|
|
4799
|
+
"line": 3,
|
|
4800
|
+
"message": "Unexpected end of file in expression, expected a corresponding closing brace for \`{\`",
|
|
4801
|
+
}
|
|
4802
|
+
`)
|
|
4803
|
+
})
|
|
4804
|
+
|
|
4805
|
+
test('syntax error: invalid expression in attribute', () => {
|
|
4806
|
+
const err = parseError(dedent`
|
|
4807
|
+
<Heading level={1 +}>text</Heading>
|
|
4808
|
+
`)
|
|
4809
|
+
expect(err).toMatchInlineSnapshot(
|
|
4810
|
+
`
|
|
4811
|
+
{
|
|
4812
|
+
"column": 20,
|
|
4813
|
+
"line": 1,
|
|
4814
|
+
"message": "Could not parse expression with acorn",
|
|
4815
|
+
}
|
|
4816
|
+
`)
|
|
4817
|
+
})
|
|
4818
|
+
|
|
4819
|
+
test('syntax error: unclosed string in expression', () => {
|
|
4820
|
+
const err = parseError(dedent`
|
|
4821
|
+
# Title
|
|
4822
|
+
|
|
4823
|
+
{"hello}
|
|
4824
|
+
`)
|
|
4825
|
+
expect(err).toMatchInlineSnapshot(
|
|
4826
|
+
`
|
|
4827
|
+
{
|
|
4828
|
+
"column": 2,
|
|
4829
|
+
"line": 3,
|
|
4830
|
+
"message": "Could not parse expression with acorn",
|
|
4831
|
+
}
|
|
4832
|
+
`)
|
|
4833
|
+
})
|
|
4834
|
+
|
|
4835
|
+
test('syntax error: malformed expression with double operator is actually valid (unary plus)', () => {
|
|
4836
|
+
const err = parseError(dedent`
|
|
4837
|
+
{1 + + + 2}
|
|
4838
|
+
`)
|
|
4839
|
+
// This is valid JS: unary plus operators, so no parse error
|
|
4840
|
+
expect(err).toMatchInlineSnapshot(`null`)
|
|
4841
|
+
})
|
|
4842
|
+
|
|
4843
|
+
test('syntax error: unclosed JSX attribute value', () => {
|
|
4844
|
+
const err = parseError(dedent`
|
|
4845
|
+
<Heading level={>text</Heading>
|
|
4846
|
+
`)
|
|
4847
|
+
expect(err).toMatchInlineSnapshot(`
|
|
4848
|
+
{
|
|
4849
|
+
"column": 32,
|
|
4850
|
+
"line": 1,
|
|
4851
|
+
"message": "Unexpected end of file in expression, expected a corresponding closing brace for \`{\`",
|
|
4852
|
+
}
|
|
4853
|
+
`)
|
|
4854
|
+
})
|
|
4855
|
+
|
|
4856
|
+
test('syntax error: JSX self-closing with wrong syntax', () => {
|
|
4857
|
+
const err = parseError(dedent`
|
|
4858
|
+
<Heading level={1} /
|
|
4859
|
+
`)
|
|
4860
|
+
expect(err).toMatchInlineSnapshot(
|
|
4861
|
+
`
|
|
4862
|
+
{
|
|
4863
|
+
"column": 21,
|
|
4864
|
+
"line": 1,
|
|
4865
|
+
"message": "Unexpected end of file after self-closing slash, expected \`>\` to end the tag",
|
|
4866
|
+
}
|
|
4867
|
+
`)
|
|
4868
|
+
})
|
|
4869
|
+
|
|
4870
|
+
test('syntax error: nested unclosed tags', () => {
|
|
4871
|
+
const err = parseError(dedent`
|
|
4872
|
+
# Title
|
|
4873
|
+
|
|
4874
|
+
<Card>
|
|
4875
|
+
<Badge label="new">
|
|
4876
|
+
</Card>
|
|
4877
|
+
`)
|
|
4878
|
+
expect(err).toMatchInlineSnapshot(
|
|
4879
|
+
`
|
|
4880
|
+
{
|
|
4881
|
+
"column": 1,
|
|
4882
|
+
"line": 5,
|
|
4883
|
+
"message": "Unexpected closing tag \`</Card>\`, expected corresponding closing tag for \`<Badge>\` (4:3-4:22)",
|
|
4884
|
+
}
|
|
4885
|
+
`)
|
|
4886
|
+
})
|
|
4887
|
+
|
|
4888
|
+
test('syntax error: export with invalid syntax', () => {
|
|
4889
|
+
const err = parseError(dedent`
|
|
4890
|
+
export const = "missing name"
|
|
4891
|
+
|
|
4892
|
+
# Title
|
|
4893
|
+
`)
|
|
4894
|
+
expect(err).toMatchInlineSnapshot(
|
|
4895
|
+
`
|
|
4896
|
+
{
|
|
4897
|
+
"column": 14,
|
|
4898
|
+
"line": 1,
|
|
4899
|
+
"message": "Could not parse import/exports with acorn",
|
|
4900
|
+
}
|
|
4901
|
+
`)
|
|
4902
|
+
})
|
|
4903
|
+
|
|
4904
|
+
test('syntax error: import with missing source', () => {
|
|
4905
|
+
const err = parseError(dedent`
|
|
4906
|
+
import { Card } from
|
|
4907
|
+
|
|
4908
|
+
# Title
|
|
4909
|
+
`)
|
|
4910
|
+
expect(err).toMatchInlineSnapshot(
|
|
4911
|
+
`
|
|
4912
|
+
{
|
|
4913
|
+
"column": 2,
|
|
4914
|
+
"line": 3,
|
|
4915
|
+
"message": "Could not parse import/exports with acorn",
|
|
4916
|
+
}
|
|
4917
|
+
`)
|
|
4918
|
+
})
|
|
4919
|
+
|
|
4920
|
+
test('error: ESM import with non-https URL shows esm-import error', () => {
|
|
4921
|
+
const code = dedent`
|
|
4922
|
+
import { Tool } from 'file:///etc/passwd'
|
|
4923
|
+
import { Safe } from 'https://esm.sh/safe-pkg'
|
|
4924
|
+
|
|
4925
|
+
# Test
|
|
4926
|
+
|
|
4927
|
+
<Tool />
|
|
4928
|
+
<Safe />
|
|
4929
|
+
`
|
|
4930
|
+
|
|
4931
|
+
const mdast = mdxParse(code)
|
|
4932
|
+
const visitor = new MdastToJsx({
|
|
4933
|
+
markdown: code,
|
|
4934
|
+
mdast,
|
|
4935
|
+
components,
|
|
4936
|
+
allowClientEsmImports: true,
|
|
4937
|
+
})
|
|
4938
|
+
visitor.run()
|
|
4939
|
+
const esmErrors = visitor.errors.filter(e => e.type === 'esm-import')
|
|
4940
|
+
expect(esmErrors.length).toBeGreaterThan(0)
|
|
4941
|
+
expect(esmErrors[0]!.message).toContain('Invalid import URL')
|
|
4942
|
+
})
|
|
4943
|
+
|
|
4295
4944
|
|