11import fg from 'fast-glob' ;
2+ import * as fs from 'fs' ;
3+ import * as path from 'path' ;
24
35export type KeyValuePair = {
46 key : string ;
57 value : string ;
68} ;
79
8- function escapeRegex ( str : string ) {
9- return str . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
10- }
10+ // TypeScript's module resolution for directories checks these in order
11+ // @see https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution
12+ const TS_INDEX_FILES = [
13+ 'index.ts' ,
14+ 'index.tsx' ,
15+ 'index.mts' ,
16+ 'index.cts' ,
17+ 'index.d.ts' ,
18+ 'index.js' ,
19+ 'index.jsx' ,
20+ 'index.mjs' ,
21+ 'index.cjs' ,
22+ ] ;
23+
24+ /**
25+ * Resolves tsconfig wildcard paths.
26+ *
27+ * In tsconfig.json, paths like `@features/*` → `libs/features/src/*` work as follows:
28+ * - The `*` captures a single path segment (the module name)
29+ * - When importing `@features/feature-a`, TypeScript captures `feature-a`
30+ * - It then replaces `*` in the value pattern: `libs/features/src/feature-a`
31+ *
32+ * For discovery, we find all directories at the wildcard position that TypeScript
33+ * would recognize as valid modules (directories with index files or package.json).
34+ *
35+ * @see https://www.typescriptlang.org/tsconfig/#paths
36+ */
37+ export function resolveTsConfigWildcard (
38+ keyPattern : string ,
39+ valuePattern : string ,
40+ cwd : string ,
41+ ) : KeyValuePair [ ] {
42+ const normalizedPattern = valuePattern . replace ( / ^ \. ? \/ + / , '' ) ;
1143
12- // Convert package.json exports pattern to glob pattern
13- // * in exports means "one segment", but for glob we need **/* for deep matching
14- // Src: https://hirok.io/posts/package-json-exports#exposing-all-package-files
15- function convertExportsToGlob ( pattern : string ) {
16- return pattern . replace ( / (?< ! \* ) \* (? ! \* ) / g, '**/*' ) ;
17- }
44+ const asteriskIndex = normalizedPattern . indexOf ( '*' ) ;
45+ if ( asteriskIndex === - 1 ) {
46+ return [ ] ;
47+ }
1848
19- function compilePattern ( pattern : string ) {
20- const tokens = pattern . split ( / ( \* \* | \* ) / ) ;
21- const regexParts = [ ] ;
49+ const prefix = normalizedPattern . substring ( 0 , asteriskIndex ) ;
50+ const suffix = normalizedPattern . substring ( asteriskIndex + 1 ) ;
2251
23- for ( const token of tokens ) {
24- if ( token === '*' ) {
25- regexParts . push ( '(.*)' ) ;
26- } else {
27- regexParts . push ( escapeRegex ( token ) ) ;
28- }
52+ const searchPath = path . join ( cwd , prefix ) ;
53+
54+ let entries : string [ ] ;
55+ try {
56+ entries = fs . readdirSync ( searchPath ) ;
57+ } catch {
58+ return [ ] ;
2959 }
3060
31- return new RegExp ( `^${ regexParts . join ( '' ) } $` ) ;
32- }
61+ const keys : KeyValuePair [ ] = [ ] ;
62+
63+ for ( const entry of entries ) {
64+ const entryPath = path . join ( searchPath , entry ) ;
65+
66+ let stats : fs . Stats ;
67+ try {
68+ stats = fs . statSync ( entryPath ) ;
69+ } catch {
70+ continue ;
71+ }
3372
34- function withoutWildcard ( template : string , wildcardValues : string [ ] ) {
35- const tokens = template . split ( / ( \* \* | \* ) / ) ;
36- let result = '' ;
37- let i = 0 ;
38- for ( const token of tokens ) {
39- if ( token === '*' ) {
40- result += wildcardValues [ i ++ ] ;
41- } else {
42- result += token ;
73+ if ( ! stats . isDirectory ( ) ) {
74+ continue ;
4375 }
76+
77+ let modulePath = path . join ( prefix , entry , suffix ) . replace ( / \\ / g, '/' ) ;
78+ const fullPath = path . join ( cwd , modulePath ) ;
79+
80+ let fullPathStats : fs . Stats ;
81+ try {
82+ fullPathStats = fs . statSync ( fullPath ) ;
83+ } catch {
84+ continue ;
85+ }
86+
87+ if ( fullPathStats . isDirectory ( ) ) {
88+ const indexFile = TS_INDEX_FILES . find ( ( indexFile ) =>
89+ fs . existsSync ( path . join ( fullPath , indexFile ) ) ,
90+ ) ;
91+
92+ if ( ! indexFile ) continue ;
93+ modulePath = path . join ( modulePath , indexFile ) ;
94+ } else if ( ! fullPathStats . isFile ( ) ) {
95+ continue ;
96+ }
97+
98+ const key = keyPattern . replace ( '*' , entry ) ;
99+
100+ keys . push ( {
101+ key,
102+ value : modulePath ,
103+ } ) ;
44104 }
45- return result ;
105+
106+ return keys ;
46107}
47108
48- export function resolveWildcardKeys (
109+ /**
110+ * Resolves package.json exports wildcard patterns.
111+ *
112+ * In package.json exports, patterns like `./features/*.js` → `./src/features/*.js` work as follows:
113+ * - The `*` is a literal string replacement that can include path separators
114+ * - Importing `pkg/features/a/b.js` captures `a/b` and replaces `*` → `./src/features/a/b.js`
115+ * - This matches actual files, not directories
116+ *
117+ * @see https://nodejs.org/api/packages.html#subpath-patterns
118+ */
119+ export function resolvePackageJsonExportsWildcard (
49120 keyPattern : string ,
50121 valuePattern : string ,
51122 cwd : string ,
52123) : KeyValuePair [ ] {
53124 const normalizedPattern = valuePattern . replace ( / ^ \. ? \/ + / , '' ) ;
54125
55- const globPattern = convertExportsToGlob ( normalizedPattern ) ;
126+ const asteriskIndex = normalizedPattern . indexOf ( '*' ) ;
127+ if ( asteriskIndex === - 1 ) {
128+ return [ ] ;
129+ }
56130
57- const regex = compilePattern ( normalizedPattern ) ;
131+ const prefix = normalizedPattern . substring ( 0 , asteriskIndex ) ;
132+ const suffix = normalizedPattern . substring ( asteriskIndex + 1 ) ;
58133
59- const files = fg . sync ( globPattern , {
134+ // fast-glob requires **/* pattern for matching files at any depth
135+ const files = fg . sync ( prefix + '**/*' + suffix , {
60136 cwd,
61137 onlyFiles : true ,
62138 deep : Infinity ,
@@ -67,11 +143,14 @@ export function resolveWildcardKeys(
67143 for ( const file of files ) {
68144 const relPath = file . replace ( / \\ / g, '/' ) . replace ( / ^ \. \/ / , '' ) ;
69145
70- const wildcards = relPath . match ( regex ) ;
71- if ( ! wildcards ) continue ;
146+ const captured = suffix
147+ ? relPath . slice ( prefix . length , - suffix . length )
148+ : relPath . slice ( prefix . length ) ;
149+
150+ const key = keyPattern . replace ( '*' , captured ) ;
72151
73152 keys . push ( {
74- key : withoutWildcard ( keyPattern , wildcards . slice ( 1 ) ) ,
153+ key,
75154 value : relPath ,
76155 } ) ;
77156 }
0 commit comments