@@ -5,6 +5,7 @@ import { log } from './log';
55
66import { ALIAS_AS_RELATIVE , ALIAS_AS_RESOLVABLE } from './common' ;
77
8+ /** Type guard for plain literal nodes; rejects template literals with interpolations. */
89function isLiteral ( node : babelTypes . Node ) : node is babelTypes . Literal {
910 if ( node == null ) {
1011 return false ;
@@ -21,6 +22,7 @@ function isLiteral(node: babelTypes.Node): node is babelTypes.Literal {
2122 return true ;
2223}
2324
25+ /** Extracts the runtime value of a literal. Throws on null/regexp — never valid module specifiers. */
2426function getLiteralValue ( node : babelTypes . Literal ) {
2527 if ( node . type === 'TemplateLiteral' ) {
2628 return node . quasis [ 0 ] . value . raw ;
@@ -37,6 +39,7 @@ function getLiteralValue(node: babelTypes.Literal) {
3739 return node . value ;
3840}
3941
42+ /** Renders an import specifier list back to source (`a, { b, c as d }`) for log output. */
4043function reconstructSpecifiers (
4144 specs : (
4245 | babelTypes . ImportDefaultSpecifier
@@ -79,6 +82,7 @@ function reconstructSpecifiers(
7982 return defaults . join ( ', ' ) ;
8083}
8184
85+ /** Prints any AST node back to a single-line source string, used when an arg isn't a literal. */
8286function reconstruct ( node : babelTypes . Node ) {
8387 let v = generate ( node , { comments : false } ) . code . replace ( / \n / g, '' ) ;
8488 let v2 ;
@@ -102,6 +106,7 @@ interface Was {
102106 v3 ?: string ;
103107}
104108
109+ /** Fills a template (e.g. `require({v1}{c2}{v2})`) with captured args to produce the printable form of a match. */
105110function forge ( pattern : string , was : Was ) {
106111 return pattern
107112 . replace ( '{c1}' , ', ' )
@@ -112,6 +117,7 @@ function forge(pattern: string, was: Was) {
112117 . replace ( '{v3}' , was . v3 ? was . v3 : '' ) ;
113118}
114119
120+ /** Guards the 2nd arg of require/require.resolve — only pkg's `must-exclude`/`may-exclude` markers are honored. */
115121function valid2 ( v2 ?: Was [ 'v2' ] ) {
116122 return (
117123 v2 === undefined ||
@@ -121,6 +127,7 @@ function valid2(v2?: Was['v2']) {
121127 ) ;
122128}
123129
130+ /** Matches `require.resolve("lit"[, "lit"])`. Returns captured args or null. */
124131function visitorRequireResolve ( n : babelTypes . Node ) {
125132 if ( ! babelTypes . isCallExpression ( n ) ) {
126133 return null ;
@@ -150,6 +157,7 @@ function visitorRequireResolve(n: babelTypes.Node) {
150157 } ;
151158}
152159
160+ /** Matches `require("lit"[, "lit"])`. Returns captured args or null. */
153161function visitorRequire ( n : babelTypes . Node ) {
154162 if ( ! babelTypes . isCallExpression ( n ) ) {
155163 return null ;
@@ -173,6 +181,7 @@ function visitorRequire(n: babelTypes.Node) {
173181 } ;
174182}
175183
184+ /** Matches a static ESM `import … from "lit"` declaration. */
176185function visitorImport ( n : babelTypes . Node ) {
177186 if ( ! babelTypes . isImportDeclaration ( n ) ) {
178187 return null ;
@@ -181,6 +190,32 @@ function visitorImport(n: babelTypes.Node) {
181190 return { v1 : n . source . value , v3 : reconstructSpecifiers ( n . specifiers ) } ;
182191}
183192
193+ /** Matches dynamic `import("lit")` so bundler-emitted chunk splits get walked like static imports. */
194+ function visitorDynamicImport ( n : babelTypes . Node ) {
195+ if ( ! babelTypes . isCallExpression ( n ) ) {
196+ return null ;
197+ }
198+
199+ if ( n . callee . type !== 'Import' ) {
200+ return null ;
201+ }
202+
203+ if ( ! n . arguments || ! isLiteral ( n . arguments [ 0 ] ) ) {
204+ return null ;
205+ }
206+
207+ // Module specifiers are always strings — reject `import(0)` / `import(true)`
208+ // so a non-string value can't reach the walker's alias handling.
209+ const value = getLiteralValue ( n . arguments [ 0 ] as babelTypes . Literal ) ;
210+
211+ if ( typeof value !== 'string' ) {
212+ return null ;
213+ }
214+
215+ return { v1 : value } ;
216+ }
217+
218+ /** Matches `path.join(__dirname, "lit")` — treats the joined path as a snapshot asset reference. */
184219function visitorPathJoin ( n : babelTypes . Node ) {
185220 if ( ! babelTypes . isCallExpression ( n ) ) {
186221 return null ;
@@ -221,6 +256,11 @@ function visitorPathJoin(n: babelTypes.Node) {
221256 return { v1 : getLiteralValue ( n . arguments [ 1 ] as babelTypes . StringLiteral ) } ;
222257}
223258
259+ /**
260+ * Runs each literal-arg matcher in order and returns the first hit as a
261+ * `{alias, aliasType, mustExclude?, mayExclude?}` derivative for the walker to
262+ * bundle. When `test` is true returns a printable form (used by unit tests).
263+ */
224264export function visitorSuccessful ( node : babelTypes . Node , test = false ) {
225265 let was : Was | null = visitorRequireResolve ( node ) ;
226266
@@ -270,6 +310,16 @@ export function visitorSuccessful(node: babelTypes.Node, test = false) {
270310 return { alias : was . v1 , aliasType : ALIAS_AS_RESOLVABLE } ;
271311 }
272312
313+ was = visitorDynamicImport ( node ) ;
314+
315+ if ( was ) {
316+ if ( test ) {
317+ return forge ( 'import({v1})' , was ) ;
318+ }
319+
320+ return { alias : was . v1 , aliasType : ALIAS_AS_RESOLVABLE } ;
321+ }
322+
273323 was = visitorPathJoin ( node ) ;
274324
275325 if ( was ) {
@@ -283,6 +333,7 @@ export function visitorSuccessful(node: babelTypes.Node, test = false) {
283333 return null ;
284334}
285335
336+ /** Matches `require.resolve(<non-literal>[, "lit"])` — feeds the "Cannot resolve" warning path. */
286337function nonLiteralRequireResolve ( n : babelTypes . Node ) {
287338 if ( ! babelTypes . isCallExpression ( n ) ) {
288339 return null ;
@@ -322,6 +373,7 @@ function nonLiteralRequireResolve(n: babelTypes.Node) {
322373 } ;
323374}
324375
376+ /** Matches `require(<non-literal>[, "lit"])` — feeds the "Cannot resolve" warning path. */
325377function nonLiteralRequire ( n : babelTypes . Node ) {
326378 if ( ! babelTypes . isCallExpression ( n ) ) {
327379 return null ;
@@ -355,6 +407,7 @@ function nonLiteralRequire(n: babelTypes.Node) {
355407 } ;
356408}
357409
410+ /** Entry visitor for dynamic requires whose target isn't known at build time — returns the alias to warn about. */
358411export function visitorNonLiteral ( n : babelTypes . Node ) {
359412 const was = nonLiteralRequireResolve ( n ) || nonLiteralRequire ( n ) ;
360413
@@ -373,6 +426,7 @@ export function visitorNonLiteral(n: babelTypes.Node) {
373426 return null ;
374427}
375428
429+ /** Loose `require(...)` match (no literal gate) — used only to surface malformed-require diagnostics. */
376430function isRequire ( n : babelTypes . Node ) {
377431 if ( ! babelTypes . isCallExpression ( n ) ) {
378432 return null ;
@@ -395,6 +449,7 @@ function isRequire(n: babelTypes.Node) {
395449 return { v1 : reconstruct ( n . arguments [ 0 ] ) } ;
396450}
397451
452+ /** Loose `require.resolve(...)` match (no literal gate) — used only for malformed-require diagnostics. */
398453function isRequireResolve ( n : babelTypes . Node ) {
399454 if ( ! babelTypes . isCallExpression ( n ) ) {
400455 return null ;
@@ -423,6 +478,7 @@ function isRequireResolve(n: babelTypes.Node) {
423478 return { v1 : reconstruct ( n . arguments [ 0 ] ) } ;
424479}
425480
481+ /** Fires on require/require.resolve shapes the literal matchers rejected (wrong arg count, etc.). */
426482export function visitorMalformed ( n : babelTypes . Node ) {
427483 const was = isRequireResolve ( n ) || isRequire ( n ) ;
428484
@@ -433,6 +489,7 @@ export function visitorMalformed(n: babelTypes.Node) {
433489 return null ;
434490}
435491
492+ /** Flags `path.resolve(...)` so the walker can warn that it resolves against `process.cwd()` at runtime, not `__dirname`. */
436493export function visitorUseSCWD ( n : babelTypes . Node ) {
437494 if ( ! babelTypes . isCallExpression ( n ) ) {
438495 return null ;
@@ -463,6 +520,11 @@ export function visitorUseSCWD(n: babelTypes.Node) {
463520
464521type VisitorFunction = ( node : babelTypes . Node , trying ?: boolean ) => boolean ;
465522
523+ /**
524+ * Iterative DFS over the AST. `visitor` returns true to descend into children;
525+ * `trying` is propagated inside try/catch bodies so the walker can downgrade
526+ * downstream warnings to debug.
527+ */
466528function traverse ( ast : babelTypes . File , visitor : VisitorFunction ) {
467529 // modified esprima-walk to support
468530 // visitor return value and "trying" flag
@@ -495,18 +557,37 @@ function traverse(ast: babelTypes.File, visitor: VisitorFunction) {
495557 }
496558}
497559
498- export function parse ( body : string ) {
560+ /**
561+ * `babel.parse` wrapper. `isEsm` selects `sourceType: 'module'` so `import.meta`
562+ * / top-level await parse cleanly. `decorators-legacy` is enabled so third-party
563+ * sources that ship raw `@decorator` syntax (fontkit, older MobX/Nest builds,
564+ * etc.) don't trip the parser and silently drop their dependency graph.
565+ */
566+ export function parse ( body : string , isEsm = false ) {
499567 return babel . parse ( body , {
500568 allowImportExportEverywhere : true ,
501569 allowReturnOutsideFunction : true ,
570+ sourceType : isEsm ? 'module' : 'script' ,
571+ plugins : [ 'decorators-legacy' ] ,
502572 } ) ;
503573}
504574
505- export function detect ( body : string , visitor : VisitorFunction , file ?: string ) {
575+ /**
576+ * Parses `body` and walks the AST with `visitor`. Parse failures are logged
577+ * (not thrown) so one unparseable file doesn't abort the whole build — but the
578+ * file's dependencies are then skipped, which is why callers must pass the
579+ * correct `isEsm` flag.
580+ */
581+ export function detect (
582+ body : string ,
583+ visitor : VisitorFunction ,
584+ file ?: string ,
585+ isEsm = false ,
586+ ) {
506587 let json ;
507588
508589 try {
509- json = parse ( body ) ;
590+ json = parse ( body , isEsm ) ;
510591 } catch ( error ) {
511592 const fileInfo = file ? ` in ${ file } ` : '' ;
512593 log . warn ( `Babel parse has failed: ${ ( error as Error ) . message } ${ fileInfo } ` ) ;
0 commit comments