safe-mdx 1.3.10 → 1.4.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/dist/esm-parser.test.js +176 -1
- package/dist/esm-parser.test.js.map +1 -1
- package/dist/parse.d.ts +44 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +127 -0
- package/dist/parse.js.map +1 -1
- package/dist/safe-mdx.d.ts +19 -1
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +54 -3
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +105 -0
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +1 -1
- package/src/esm-parser.test.ts +189 -1
- package/src/parse.ts +165 -0
- package/src/safe-mdx.test.tsx +114 -1
- package/src/safe-mdx.tsx +65 -1
package/src/parse.ts
CHANGED
|
@@ -9,6 +9,62 @@ import { parseHtmlToMdxAst, remarkMdxJsxNormalize } from './html/html-to-mdx-ast
|
|
|
9
9
|
|
|
10
10
|
export { parseHtmlToMdxAst, remarkMdxJsxNormalize }
|
|
11
11
|
|
|
12
|
+
/* ── Import extraction ──────────────────────────────────────────────── */
|
|
13
|
+
|
|
14
|
+
export type MdxImportSpecifier = {
|
|
15
|
+
/** Name used in MDX (e.g. `Card`, `MyButton`, `Utils`) */
|
|
16
|
+
local: string
|
|
17
|
+
/** Original export name. `'default'` for default imports, local name for named, `'*'` for namespace */
|
|
18
|
+
imported: string
|
|
19
|
+
type: 'named' | 'default' | 'namespace'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type MdxImport = {
|
|
23
|
+
/** Raw source string as written: `'./card'`, `'/snippets/ui'`, `'some-pkg'` */
|
|
24
|
+
source: string
|
|
25
|
+
specifiers: MdxImportSpecifier[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract all import declarations from a parsed mdast tree.
|
|
30
|
+
* Unlike `parseEsmImports`, this accepts ANY source (not just HTTPS URLs).
|
|
31
|
+
*/
|
|
32
|
+
export function extractImports(ast: Root): MdxImport[] {
|
|
33
|
+
const imports: MdxImport[] = []
|
|
34
|
+
|
|
35
|
+
for (const node of ast.children) {
|
|
36
|
+
if (node.type !== 'mdxjsEsm') continue
|
|
37
|
+
const estree = (node as any).data?.estree
|
|
38
|
+
if (!estree) continue
|
|
39
|
+
|
|
40
|
+
for (const statement of estree.body) {
|
|
41
|
+
if (statement.type !== 'ImportDeclaration') continue
|
|
42
|
+
const source = statement.source?.value
|
|
43
|
+
if (typeof source !== 'string') continue
|
|
44
|
+
|
|
45
|
+
const specifiers: MdxImportSpecifier[] = []
|
|
46
|
+
for (const spec of statement.specifiers ?? []) {
|
|
47
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
48
|
+
specifiers.push({ local: spec.local.name, imported: 'default', type: 'default' })
|
|
49
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
50
|
+
const importedName = spec.imported.type === 'Identifier'
|
|
51
|
+
? spec.imported.name
|
|
52
|
+
: String(spec.imported.value)
|
|
53
|
+
specifiers.push({ local: spec.local.name, imported: importedName, type: 'named' })
|
|
54
|
+
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
55
|
+
specifiers.push({ local: spec.local.name, imported: '*', type: 'namespace' })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (specifiers.length > 0) {
|
|
60
|
+
imports.push({ source, specifiers })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return imports
|
|
66
|
+
}
|
|
67
|
+
|
|
12
68
|
export function mdxParse(code: string) {
|
|
13
69
|
const file = mdxProcessor.processSync(code)
|
|
14
70
|
return file.data.ast as Root
|
|
@@ -95,6 +151,115 @@ export function remarkMarkAndUnravel() {
|
|
|
95
151
|
}
|
|
96
152
|
}
|
|
97
153
|
|
|
154
|
+
/* ── Module resolution ───────────────────────────────────────────────── */
|
|
155
|
+
|
|
156
|
+
/** Extensions tried when resolving a bare import against glob keys */
|
|
157
|
+
const RESOLVE_EXTENSIONS = [
|
|
158
|
+
'', '.tsx', '.ts', '.jsx', '.js', '.mdx', '.md',
|
|
159
|
+
'/index.tsx', '/index.ts', '/index.jsx', '/index.js',
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Given an import source and a baseUrl, resolve the source to a key
|
|
164
|
+
* that exists in `moduleKeys`. Handles:
|
|
165
|
+
* - Relative imports (`./card`) resolved from `baseUrl`
|
|
166
|
+
* - Absolute imports (`/snippets/card`) normalized to `./snippets/card`
|
|
167
|
+
* - Extension resolution (tries .tsx, .ts, .jsx, .js, .mdx, .md, /index.*)
|
|
168
|
+
*/
|
|
169
|
+
export function resolveModulePath(
|
|
170
|
+
source: string,
|
|
171
|
+
baseUrl: string,
|
|
172
|
+
moduleKeys: string[],
|
|
173
|
+
): string | undefined {
|
|
174
|
+
let normalized: string
|
|
175
|
+
|
|
176
|
+
if (source.startsWith('/')) {
|
|
177
|
+
// Absolute import from project root: /snippets/card → ./snippets/card
|
|
178
|
+
normalized = '.' + source
|
|
179
|
+
} else if (source.startsWith('./') || source.startsWith('../')) {
|
|
180
|
+
// Relative import: resolve from baseUrl
|
|
181
|
+
const joined = joinPaths(baseUrl, source)
|
|
182
|
+
if (!joined) return undefined // .. escaped above root
|
|
183
|
+
normalized = joined
|
|
184
|
+
} else {
|
|
185
|
+
// Bare specifier (npm package etc.) — not resolvable from glob
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Try each extension
|
|
190
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
191
|
+
const candidate = normalized + ext
|
|
192
|
+
if (moduleKeys.includes(candidate)) {
|
|
193
|
+
return candidate
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Simple path join that normalizes `./a/b/../c` segments.
|
|
201
|
+
* Both inputs and output use `./` prefix (matching Vite glob key format).
|
|
202
|
+
* Returns `undefined` if `..` escapes above the project root. */
|
|
203
|
+
function joinPaths(base: string, relative: string): string | undefined {
|
|
204
|
+
// Strip ./ prefix and trailing /
|
|
205
|
+
const baseParts = base.replace(/^\.\//, '').replace(/\/$/, '').split('/').filter(Boolean)
|
|
206
|
+
const relParts = relative.replace(/^\.\//, '').split('/').filter(Boolean)
|
|
207
|
+
|
|
208
|
+
for (const part of relParts) {
|
|
209
|
+
if (part === '..') {
|
|
210
|
+
if (baseParts.length === 0) return undefined // escaped above root
|
|
211
|
+
baseParts.pop()
|
|
212
|
+
} else if (part !== '.') {
|
|
213
|
+
baseParts.push(part)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return './' + baseParts.join('/')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type LazyGlob = Record<string, () => Promise<Record<string, any>>>
|
|
221
|
+
export type EagerModules = Record<string, Record<string, any>>
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Given a lazy Vite glob and a parsed mdast, resolve only the imported
|
|
225
|
+
* modules eagerly. Returns the exact shape `SafeMdxRenderer.modules` expects.
|
|
226
|
+
*
|
|
227
|
+
* Usage:
|
|
228
|
+
* ```ts
|
|
229
|
+
* const lazyGlob = import.meta.glob('./snippets/*.tsx')
|
|
230
|
+
* const mdast = mdxParse(mdxString)
|
|
231
|
+
* const modules = await resolveModules({ glob: lazyGlob, mdast, baseUrl: './pages/' })
|
|
232
|
+
* <SafeMdxRenderer modules={modules} baseUrl="./pages/" ... />
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export async function resolveModules({
|
|
236
|
+
glob,
|
|
237
|
+
mdast,
|
|
238
|
+
baseUrl,
|
|
239
|
+
}: {
|
|
240
|
+
glob: LazyGlob
|
|
241
|
+
mdast: Root
|
|
242
|
+
baseUrl: string
|
|
243
|
+
}): Promise<EagerModules> {
|
|
244
|
+
const imports = extractImports(mdast)
|
|
245
|
+
if (imports.length === 0) return {}
|
|
246
|
+
|
|
247
|
+
const keys = Object.keys(glob)
|
|
248
|
+
const result: EagerModules = {}
|
|
249
|
+
|
|
250
|
+
await Promise.all(
|
|
251
|
+
imports.map(async (imp) => {
|
|
252
|
+
const resolved = resolveModulePath(imp.source, baseUrl, keys)
|
|
253
|
+
if (!resolved || !glob[resolved]) return
|
|
254
|
+
// Avoid loading the same module twice
|
|
255
|
+
if (result[resolved]) return
|
|
256
|
+
result[resolved] = await glob[resolved]()
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return result
|
|
261
|
+
}
|
|
262
|
+
|
|
98
263
|
const mdxProcessor = remark()
|
|
99
264
|
.use(remarkMdx)
|
|
100
265
|
.use(remarkFrontmatter, ['yaml', 'toml'])
|
package/src/safe-mdx.test.tsx
CHANGED
|
@@ -3498,5 +3498,118 @@ test("skip unknown elements in complex nested HTML structures", () => {
|
|
|
3498
3498
|
</div>
|
|
3499
3499
|
`);
|
|
3500
3500
|
|
|
3501
|
-
|
|
3501
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Main Title</h1><article><header><h1>Article Title</h1></header><section><blockquote><p>A famous quote</p></blockquote></section><footer></footer></article><h2>Another Section</h2><div></div>"`);
|
|
3502
3502
|
});
|
|
3503
|
+
|
|
3504
|
+
/* ── modules prop tests ─────────────────────────────────────────────── */
|
|
3505
|
+
|
|
3506
|
+
test('modules prop: resolves named import from absolute path', () => {
|
|
3507
|
+
function CustomBadge({ label }: { label: string }) {
|
|
3508
|
+
return <span className="badge">{label}</span>
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
const code = dedent`
|
|
3512
|
+
import { CustomBadge } from '/snippets/badge'
|
|
3513
|
+
|
|
3514
|
+
# Hello
|
|
3515
|
+
|
|
3516
|
+
<CustomBadge label="Works" />
|
|
3517
|
+
`
|
|
3518
|
+
const mdast = mdxParse(code)
|
|
3519
|
+
const visitor = new MdastToJsx({
|
|
3520
|
+
markdown: code,
|
|
3521
|
+
mdast,
|
|
3522
|
+
components,
|
|
3523
|
+
modules: {
|
|
3524
|
+
'./snippets/badge.tsx': { CustomBadge },
|
|
3525
|
+
},
|
|
3526
|
+
baseUrl: './pages/',
|
|
3527
|
+
})
|
|
3528
|
+
const result = visitor.run()
|
|
3529
|
+
const html = renderToStaticMarkup(result)
|
|
3530
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1><span class="badge">Works</span>"`)
|
|
3531
|
+
expect(visitor.errors).toMatchInlineSnapshot(`[]`)
|
|
3532
|
+
})
|
|
3533
|
+
|
|
3534
|
+
test('modules prop: resolves default import from relative path', () => {
|
|
3535
|
+
function MyCard({ children }: { children?: React.ReactNode }) {
|
|
3536
|
+
return <div className="card">{children}</div>
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
const code = dedent`
|
|
3540
|
+
import MyCard from './card'
|
|
3541
|
+
|
|
3542
|
+
<MyCard>content</MyCard>
|
|
3543
|
+
`
|
|
3544
|
+
const mdast = mdxParse(code)
|
|
3545
|
+
const visitor = new MdastToJsx({
|
|
3546
|
+
markdown: code,
|
|
3547
|
+
mdast,
|
|
3548
|
+
components,
|
|
3549
|
+
modules: {
|
|
3550
|
+
'./pages/card.tsx': { default: MyCard },
|
|
3551
|
+
},
|
|
3552
|
+
baseUrl: './pages/',
|
|
3553
|
+
})
|
|
3554
|
+
const result = visitor.run()
|
|
3555
|
+
const html = renderToStaticMarkup(result)
|
|
3556
|
+
expect(html).toMatchInlineSnapshot(`"<div class="card">content</div>"`)
|
|
3557
|
+
expect(visitor.errors).toMatchInlineSnapshot(`[]`)
|
|
3558
|
+
})
|
|
3559
|
+
|
|
3560
|
+
test('modules prop: resolves namespace import with dot notation', () => {
|
|
3561
|
+
function Card({ title }: { title: string }) {
|
|
3562
|
+
return <div>{title}</div>
|
|
3563
|
+
}
|
|
3564
|
+
function Badge({ label }: { label: string }) {
|
|
3565
|
+
return <span>{label}</span>
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
const code = dedent`
|
|
3569
|
+
import * as UI from './ui'
|
|
3570
|
+
|
|
3571
|
+
<UI.Card title="hello" />
|
|
3572
|
+
<UI.Badge label="tag" />
|
|
3573
|
+
`
|
|
3574
|
+
const mdast = mdxParse(code)
|
|
3575
|
+
const visitor = new MdastToJsx({
|
|
3576
|
+
markdown: code,
|
|
3577
|
+
mdast,
|
|
3578
|
+
components,
|
|
3579
|
+
modules: {
|
|
3580
|
+
'./components/ui.tsx': { Card, Badge },
|
|
3581
|
+
},
|
|
3582
|
+
baseUrl: './components/',
|
|
3583
|
+
})
|
|
3584
|
+
const result = visitor.run()
|
|
3585
|
+
const html = renderToStaticMarkup(result)
|
|
3586
|
+
expect(html).toMatchInlineSnapshot(`"<div>hello</div><span>tag</span>"`)
|
|
3587
|
+
expect(visitor.errors).toMatchInlineSnapshot(`[]`)
|
|
3588
|
+
})
|
|
3589
|
+
|
|
3590
|
+
test('modules prop: unresolved import produces error', () => {
|
|
3591
|
+
const code = dedent`
|
|
3592
|
+
import { Missing } from './nonexistent'
|
|
3593
|
+
|
|
3594
|
+
<Missing />
|
|
3595
|
+
`
|
|
3596
|
+
const mdast = mdxParse(code)
|
|
3597
|
+
const visitor = new MdastToJsx({
|
|
3598
|
+
markdown: code,
|
|
3599
|
+
mdast,
|
|
3600
|
+
components,
|
|
3601
|
+
modules: {},
|
|
3602
|
+
baseUrl: './pages/',
|
|
3603
|
+
})
|
|
3604
|
+
const result = visitor.run()
|
|
3605
|
+
const html = renderToStaticMarkup(result)
|
|
3606
|
+
expect(html).toMatchInlineSnapshot(`""`)
|
|
3607
|
+
expect(visitor.errors).toMatchInlineSnapshot(`
|
|
3608
|
+
[
|
|
3609
|
+
{
|
|
3610
|
+
"line": 3,
|
|
3611
|
+
"message": "Unsupported jsx component Missing",
|
|
3612
|
+
},
|
|
3613
|
+
]
|
|
3614
|
+
`)
|
|
3615
|
+
})
|
package/src/safe-mdx.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
|
|
|
9
9
|
import { Fragment, ReactNode } from 'react'
|
|
10
10
|
import { DynamicEsmComponent } from 'safe-mdx/client'
|
|
11
11
|
import { extractComponentInfo, parseEsmImports } from './esm-parser.js'
|
|
12
|
+
import { resolveModulePath, type EagerModules } from './parse.js'
|
|
12
13
|
import { htmlToMdxAst } from './html/html-to-mdx-ast.js'
|
|
13
14
|
import { validHtmlElements, nativeTags } from './html/valid-html-elements.js'
|
|
14
15
|
|
|
@@ -51,6 +52,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
51
52
|
createElement,
|
|
52
53
|
allowClientEsmImports = false,
|
|
53
54
|
addMarkdownLineNumbers = false,
|
|
55
|
+
modules,
|
|
56
|
+
baseUrl,
|
|
54
57
|
}: {
|
|
55
58
|
components?: ComponentsMap
|
|
56
59
|
markdown?: string
|
|
@@ -60,6 +63,13 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
60
63
|
createElement?: CreateElementFunction
|
|
61
64
|
allowClientEsmImports?: boolean
|
|
62
65
|
addMarkdownLineNumbers?: boolean
|
|
66
|
+
/** Pre-resolved modules keyed by file path (e.g. from `import.meta.glob`).
|
|
67
|
+
* When MDX contains `import { Card } from './card'`, the import source is
|
|
68
|
+
* resolved against these keys using `baseUrl` for relative paths. */
|
|
69
|
+
modules?: EagerModules
|
|
70
|
+
/** Directory of the current MDX file, used to resolve relative import
|
|
71
|
+
* sources against `modules` keys. E.g. `'./pages/getting-started/'` */
|
|
72
|
+
baseUrl?: string
|
|
63
73
|
}) {
|
|
64
74
|
const visitor = new MdastToJsx({
|
|
65
75
|
markdown,
|
|
@@ -70,6 +80,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
70
80
|
createElement,
|
|
71
81
|
allowClientEsmImports,
|
|
72
82
|
addMarkdownLineNumbers,
|
|
83
|
+
modules,
|
|
84
|
+
baseUrl,
|
|
73
85
|
})
|
|
74
86
|
const result = visitor.run()
|
|
75
87
|
return result
|
|
@@ -87,6 +99,8 @@ export class MdastToJsx {
|
|
|
87
99
|
esmImports: Map<string, string> = new Map()
|
|
88
100
|
allowClientEsmImports: boolean
|
|
89
101
|
addMarkdownLineNumbers: boolean
|
|
102
|
+
modules?: EagerModules
|
|
103
|
+
baseUrl?: string
|
|
90
104
|
|
|
91
105
|
constructor({
|
|
92
106
|
markdown: code = '',
|
|
@@ -97,6 +111,8 @@ export class MdastToJsx {
|
|
|
97
111
|
createElement = React.createElement,
|
|
98
112
|
allowClientEsmImports = false,
|
|
99
113
|
addMarkdownLineNumbers = false,
|
|
114
|
+
modules,
|
|
115
|
+
baseUrl,
|
|
100
116
|
}: {
|
|
101
117
|
markdown?: string
|
|
102
118
|
mdast: MyRootContent
|
|
@@ -109,6 +125,8 @@ export class MdastToJsx {
|
|
|
109
125
|
createElement?: CreateElementFunction
|
|
110
126
|
allowClientEsmImports?: boolean
|
|
111
127
|
addMarkdownLineNumbers?: boolean
|
|
128
|
+
modules?: EagerModules
|
|
129
|
+
baseUrl?: string
|
|
112
130
|
}) {
|
|
113
131
|
this.str = code
|
|
114
132
|
|
|
@@ -124,6 +142,9 @@ export class MdastToJsx {
|
|
|
124
142
|
|
|
125
143
|
this.addMarkdownLineNumbers = addMarkdownLineNumbers
|
|
126
144
|
|
|
145
|
+
this.modules = modules
|
|
146
|
+
this.baseUrl = baseUrl
|
|
147
|
+
|
|
127
148
|
this.c = {
|
|
128
149
|
...Object.fromEntries(
|
|
129
150
|
nativeTags.map((tag) => {
|
|
@@ -132,6 +153,45 @@ export class MdastToJsx {
|
|
|
132
153
|
),
|
|
133
154
|
...components,
|
|
134
155
|
}
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve import declarations from an mdxjsEsm node against `this.modules`.
|
|
161
|
+
* Resolved components are added directly to `this.c` (the component map)
|
|
162
|
+
* so the existing `accessWithDot` lookup finds them.
|
|
163
|
+
*/
|
|
164
|
+
resolveImportsFromModules(node: MyRootContent): void {
|
|
165
|
+
const estree = (node as any).data?.estree
|
|
166
|
+
if (!estree) return
|
|
167
|
+
|
|
168
|
+
const moduleKeys = Object.keys(this.modules!)
|
|
169
|
+
|
|
170
|
+
for (const statement of estree.body) {
|
|
171
|
+
if (statement.type !== 'ImportDeclaration') continue
|
|
172
|
+
const source: string = statement.source?.value
|
|
173
|
+
if (typeof source !== 'string') continue
|
|
174
|
+
|
|
175
|
+
const resolved = resolveModulePath(source, this.baseUrl || './', moduleKeys)
|
|
176
|
+
if (!resolved) continue
|
|
177
|
+
const mod = this.modules![resolved]
|
|
178
|
+
if (!mod) continue
|
|
179
|
+
|
|
180
|
+
for (const spec of statement.specifiers ?? []) {
|
|
181
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
182
|
+
this.c[spec.local.name] = mod.default ?? mod
|
|
183
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
184
|
+
const importedName = spec.imported.type === 'Identifier'
|
|
185
|
+
? spec.imported.name
|
|
186
|
+
: String(spec.imported.value)
|
|
187
|
+
this.c[spec.local.name] = mod[importedName]
|
|
188
|
+
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
189
|
+
// Namespace import: import * as UI from '...'
|
|
190
|
+
// Supports <UI.Card> via accessWithDot
|
|
191
|
+
this.c[spec.local.name] = mod
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
135
195
|
}
|
|
136
196
|
|
|
137
197
|
addLineNumberToProps(
|
|
@@ -588,7 +648,11 @@ export class MdastToJsx {
|
|
|
588
648
|
|
|
589
649
|
switch (node.type) {
|
|
590
650
|
case 'mdxjsEsm': {
|
|
591
|
-
//
|
|
651
|
+
// Resolve imports from pre-loaded modules (server-side)
|
|
652
|
+
if (this.modules) {
|
|
653
|
+
this.resolveImportsFromModules(node)
|
|
654
|
+
}
|
|
655
|
+
// Parse ESM imports for client-side dynamic loading (only if allowed)
|
|
592
656
|
if (this.allowClientEsmImports) {
|
|
593
657
|
const parsedImports = parseEsmImports(node, (err) =>
|
|
594
658
|
this.errors.push(err),
|