@@ -10,7 +10,9 @@ type RegisteredModule = {
1010 // lazily parsed json string
1111 parsedJson ?: any
1212}
13+ type ModuleResolutions = ( subpath : string ) => string
1314const registeredModules = new Map < string , RegisteredModule > ( )
15+ const memoizedPackageResolvers = new WeakMap < RegisteredModule , ModuleResolutions > ( )
1416
1517const require = createRequire ( import . meta. url )
1618
@@ -30,6 +32,187 @@ function parseJson(matchedModule: RegisteredModule) {
3032 }
3133}
3234
35+ function normalizePackageExports ( exports : any ) {
36+ if ( typeof exports === 'string' ) {
37+ return { '.' : exports }
38+ }
39+
40+ if ( Array . isArray ( exports ) ) {
41+ return Object . fromEntries ( exports . map ( ( entry ) => [ entry , entry ] ) )
42+ }
43+
44+ return exports
45+ }
46+
47+ type Condition = string // 'import', 'require', 'default', 'node-addon' etc
48+ type SubpathMatcher = string
49+ type ConditionalTarget = { [ key in Condition ] : string | ConditionalTarget }
50+ type SubpathTarget = string | ConditionalTarget
51+ /**
52+ * @example
53+ * {
54+ * ".": "./main.js",
55+ * "./foo": {
56+ * "import": "./foo.js",
57+ * "require": "./foo.cjs"
58+ * }
59+ * }
60+ */
61+ type NormalizedExports = Record < SubpathMatcher , SubpathTarget | Record < Condition , SubpathTarget > >
62+
63+ // https://github.com/nodejs/node/blob/6fd67ec6e3ccbdfcfa0300b9b742040a0607a4bc/lib/internal/modules/esm/resolve.js#L555
64+ function isConditionalExportsMainSugar ( exports : any ) {
65+ if ( typeof exports === 'string' || Array . isArray ( exports ) ) {
66+ return true
67+ }
68+ if ( typeof exports !== 'object' || exports === null ) {
69+ return false
70+ }
71+
72+ // not doing validation at this point, if the package.json was misconfigured
73+ // we would not get to this point as it would throw when running `next build`
74+ const keys = Object . keys ( exports )
75+ return keys . length > 0 && ( keys [ 0 ] === '' || keys [ 0 ] [ 0 ] !== '.' )
76+ }
77+
78+ // https://github.com/nodejs/node/blob/6fd67ec6e3ccbdfcfa0300b9b742040a0607a4bc/lib/internal/modules/esm/resolve.js#L671
79+ function patternKeyCompare ( a : string , b : string ) {
80+ const aPatternIndex = a . indexOf ( '*' )
81+ const bPatternIndex = b . indexOf ( '*' )
82+ const baseLenA = aPatternIndex === - 1 ? a . length : aPatternIndex + 1
83+ const baseLenB = bPatternIndex === - 1 ? b . length : bPatternIndex + 1
84+ if ( baseLenA > baseLenB ) {
85+ return - 1
86+ }
87+ if ( baseLenB > baseLenA ) {
88+ return 1
89+ }
90+ if ( aPatternIndex === - 1 ) {
91+ return 1
92+ }
93+ if ( bPatternIndex === - 1 ) {
94+ return - 1
95+ }
96+ if ( a . length > b . length ) {
97+ return - 1
98+ }
99+ if ( b . length > a . length ) {
100+ return 1
101+ }
102+ return 0
103+ }
104+
105+ function applyWildcardMatch ( target : string , bestMatchSubpath ?: string ) {
106+ return bestMatchSubpath ? target . replace ( '*' , bestMatchSubpath ) : target
107+ }
108+
109+ // https://github.com/nodejs/node/blob/323f19c18fea06b9234a0c945394447b077fe565/lib/internal/modules/helpers.js#L76
110+ const conditions = new Set ( [ 'require' , 'node' , 'node-addons' , 'default' ] )
111+
112+ // https://github.com/nodejs/node/blob/6fd67ec6e3ccbdfcfa0300b9b742040a0607a4bc/lib/internal/modules/esm/resolve.js#L480
113+ function matchConditions ( target : SubpathTarget , bestMatchSubpath ?: string ) {
114+ if ( typeof target === 'string' ) {
115+ return applyWildcardMatch ( target , bestMatchSubpath )
116+ }
117+
118+ if ( Array . isArray ( target ) && target . length > 0 ) {
119+ for ( const targetItem of target ) {
120+ return matchConditions ( targetItem , bestMatchSubpath )
121+ }
122+ }
123+
124+ if ( typeof target === 'object' && target !== null ) {
125+ for ( const [ condition , targetValue ] of Object . entries ( target ) ) {
126+ if ( conditions . has ( condition ) ) {
127+ return matchConditions ( targetValue , bestMatchSubpath )
128+ }
129+ }
130+ }
131+
132+ throw new Error ( 'Invalid package target' )
133+ }
134+
135+ function getPackageResolver ( packageJsonMatchedModule : RegisteredModule ) {
136+ const memoized = memoizedPackageResolvers . get ( packageJsonMatchedModule )
137+ if ( memoized ) {
138+ return memoized
139+ }
140+
141+ // https://nodejs.org/api/packages.html#package-entry-points
142+
143+ const pkgJson = parseJson ( packageJsonMatchedModule )
144+
145+ let exports : NormalizedExports | null = null
146+ if ( pkgJson . exports ) {
147+ // https://github.com/nodejs/node/blob/6fd67ec6e3ccbdfcfa0300b9b742040a0607a4bc/lib/internal/modules/esm/resolve.js#L590
148+ exports = isConditionalExportsMainSugar ( pkgJson . exports )
149+ ? { '.' : pkgJson . exports }
150+ : pkgJson . exports
151+ }
152+
153+ const resolveInPackage : ModuleResolutions = ( subpath : string ) => {
154+ if ( exports ) {
155+ const normalizedSubpath = subpath . length === 0 ? '.' : './' + subpath
156+
157+ // https://github.com/nodejs/node/blob/6fd67ec6e3ccbdfcfa0300b9b742040a0607a4bc/lib/internal/modules/esm/resolve.js#L594
158+ // simple case with matching as-is
159+ if (
160+ normalizedSubpath in exports &&
161+ ! normalizedSubpath . includes ( '*' ) &&
162+ ! normalizedSubpath . endsWith ( '/' )
163+ ) {
164+ return matchConditions ( exports [ normalizedSubpath ] )
165+ }
166+
167+ // https://github.com/nodejs/node/blob/6fd67ec6e3ccbdfcfa0300b9b742040a0607a4bc/lib/internal/modules/esm/resolve.js#L610
168+ let bestMatchKey = ''
169+ let bestMatchSubpath
170+ for ( const key of Object . keys ( exports ) ) {
171+ const patternIndex = key . indexOf ( '*' )
172+ if ( patternIndex !== - 1 && normalizedSubpath . startsWith ( key . slice ( 0 , patternIndex ) ) ) {
173+ const patternTrailer = key . slice ( patternIndex + 1 )
174+ if (
175+ normalizedSubpath . length > key . length &&
176+ normalizedSubpath . endsWith ( patternTrailer ) &&
177+ patternKeyCompare ( bestMatchKey , key ) === 1 &&
178+ key . lastIndexOf ( '*' ) === patternIndex
179+ ) {
180+ bestMatchKey = key
181+ bestMatchSubpath = normalizedSubpath . slice (
182+ patternIndex ,
183+ normalizedSubpath . length - patternTrailer . length ,
184+ )
185+ }
186+ }
187+ }
188+
189+ if ( bestMatchKey && typeof bestMatchSubpath === 'string' ) {
190+ const matchedTarget = exports [ bestMatchKey ]
191+ console . log ( {
192+ matchedTarget,
193+ bestMatchKey,
194+ bestMatchSubpath,
195+ subpath,
196+ } )
197+ return matchConditions ( matchedTarget , bestMatchSubpath )
198+ }
199+
200+ // if exports are defined, they are source of truth and any imports not allowed by it will fail
201+ throw new Error ( `Cannot find module '${ normalizedSubpath } '` )
202+ }
203+
204+ if ( subpath . length === 0 && pkgJson . main ) {
205+ return pkgJson . main
206+ }
207+
208+ return subpath
209+ }
210+
211+ memoizedPackageResolvers . set ( packageJsonMatchedModule , resolveInPackage )
212+
213+ return resolveInPackage
214+ }
215+
33216function seedCJSModuleCacheAndReturnTarget ( matchedModule : RegisteredModule , parent : Module ) {
34217 if ( matchedModule . loaded ) {
35218 return matchedModule . filepath
@@ -101,7 +284,6 @@ export function registerCJSModules(baseUrl: URL, modules: Map<string, string>) {
101284
102285 for ( const [ filename , source ] of modules . entries ( ) ) {
103286 const target = join ( basePath , filename )
104-
105287 registeredModules . set ( target , { source, loaded : false , filepath : target } )
106288 }
107289
@@ -135,14 +317,10 @@ export function registerCJSModules(baseUrl: URL, modules: Map<string, string>) {
135317
136318 let relativeTarget = moduleInPackagePath
137319
138- let pkgJson : any = null
139320 if ( maybePackageJson ) {
140- pkgJson = parseJson ( maybePackageJson )
321+ const packageResolver = getPackageResolver ( maybePackageJson )
141322
142- // TODO: exports and anything else like that
143- if ( moduleInPackagePath . length === 0 && pkgJson . main ) {
144- relativeTarget = pkgJson . main
145- }
323+ relativeTarget = packageResolver ( moduleInPackagePath )
146324 }
147325
148326 const potentialPath = join ( nodeModulePaths , packageName , relativeTarget )
0 commit comments