11import { dirname , join , normalize , posix } from 'path' ;
22import type { Plugin , RollupCache } from 'rollup' ;
33import { rollup } from 'rollup' ;
4- import { existsSync , promises as fs } from 'fs' ;
4+ import { promises as fs } from 'fs' ;
55import { resolve , legacy as resolveLegacy } from 'resolve.exports' ;
66import commonjs from '@rollup/plugin-commonjs' ;
77import { processGlobalPlugin } from './process-global-plugin' ;
88import * as esbuild from 'esbuild' ;
99import { parse } from 'cjs-module-lexer' ;
10- import MagicString from 'magic-string' ;
1110import { fileURLToPath } from 'url' ;
11+ import { createRequire } from 'module' ;
1212import { jsExts } from '../middleware/js' ;
1313import { changeErrorMessage } from '../../utils' ;
1414
@@ -78,9 +78,9 @@ export const npmPlugin = ({ root }: { root: string }): Plugin => {
7878 const cachePath = join ( cacheDir , '@npm' , `${ resolved . idWithVersion } .js` ) ;
7979 const cached = await getFromCache ( cachePath ) ;
8080 if ( cached ) return cached ;
81- const result = await bundleNpmModule ( resolved . path , false ) ;
81+ const result = await bundleNpmModule ( resolved . path , id , false ) ;
8282 // Queue up a second-pass optimized/minified build
83- bundleNpmModule ( resolved . path , true ) . then ( ( optimizedResult ) => {
83+ bundleNpmModule ( resolved . path , id , true ) . then ( ( optimizedResult ) => {
8484 setInCache ( cachePath , optimizedResult ) ;
8585 } ) ;
8686 setInCache ( cachePath , result ) ;
@@ -89,17 +89,16 @@ export const npmPlugin = ({ root }: { root: string }): Plugin => {
8989 } ;
9090} ;
9191
92- const nodeResolve = async ( id : string , root : string ) => {
93- const pathChunks = id . split ( posix . sep ) ;
94- const isNpmNamespace = id [ 0 ] === '@' ;
95- const packageName = pathChunks . slice ( 0 , isNpmNamespace ? 2 : 1 ) ;
96- // If it is an npm namespace, then get the first two folders, otherwise just one
97- const pkgDir = join ( root , 'node_modules' , ...packageName ) ;
98- await fs . stat ( pkgDir ) . catch ( ( ) => {
99- throw new Error ( `Could not resolve ${ id } from ${ root } ` ) ;
100- } ) ;
101- // Path within imported module
102- const subPath = join ( ...pathChunks . slice ( isNpmNamespace ? 2 : 1 ) ) ;
92+ interface ResolveResult {
93+ path : string ;
94+ idWithVersion : string ;
95+ }
96+
97+ const resolveFromFolder = async (
98+ pkgDir : string ,
99+ subPath : string ,
100+ packageName : string [ ] ,
101+ ) : Promise < false | ResolveResult > => {
103102 const pkgJsonPath = join ( pkgDir , 'package.json' ) ;
104103 let pkgJson ;
105104 try {
@@ -133,31 +132,63 @@ const nodeResolve = async (id: string, root: string) => {
133132 if ( ! result && subPath === '.' )
134133 result = resolveLegacy ( pkgJson , { browser : false , fields : [ 'main' ] } ) ;
135134
136- if ( ! result ) {
135+ if ( ! result && ! ( 'exports' in pkgJson ) ) {
137136 const extensions = [ '.js' , '/index.js' , '.cjs' , '/index.cjs' ] ;
137+ // If this was not conditionally included, this would have infinite recursion
138+ if ( subPath !== '.' ) extensions . unshift ( '' ) ;
138139 for ( const extension of extensions ) {
139140 const path = normalize ( join ( pkgDir , subPath ) + extension ) ;
140- if ( existsSync ( path ) ) return { path, idWithVersion } ;
141+ const stats = await fs . stat ( path ) . catch ( ( ) => null ) ;
142+ if ( stats ) {
143+ if ( stats . isFile ( ) ) return { path, idWithVersion } ;
144+ if ( stats . isDirectory ( ) ) {
145+ // If you import some-package/foo and foo is a folder with a package.json in it,
146+ // resolve main fields from the package.json
147+ const result = await resolveFromFolder ( path , '.' , packageName ) ;
148+ if ( result ) return { path : result . path , idWithVersion } ;
149+ }
150+ }
141151 }
142-
143- throw new Error ( `Could not resolve ${ id } ` ) ;
144152 }
145153
154+ if ( ! result ) return false ;
146155 return { path : join ( pkgDir , result ) , idWithVersion } ;
147156} ;
148157
158+ const resolveCache = new Map < string , ResolveResult > ( ) ;
159+
160+ const resolveCacheKey = ( id : string , root : string ) => `${ id } \0\0${ root } ` ;
161+
162+ const nodeResolve = async ( id : string , root : string ) => {
163+ const cacheKey = resolveCacheKey ( id , root ) ;
164+ const cached = resolveCache . get ( cacheKey ) ;
165+ if ( cached ) return cached ;
166+ const pathChunks = id . split ( posix . sep ) ;
167+ const isNpmNamespace = id [ 0 ] === '@' ;
168+ const packageName = pathChunks . slice ( 0 , isNpmNamespace ? 2 : 1 ) ;
169+ // If it is an npm namespace, then get the first two folders, otherwise just one
170+ const pkgDir = join ( root , 'node_modules' , ...packageName ) ;
171+ await fs . stat ( pkgDir ) . catch ( ( ) => {
172+ throw new Error ( `Could not resolve ${ id } from ${ root } ` ) ;
173+ } ) ;
174+ // Path within imported module
175+ const subPath = join ( ...pathChunks . slice ( isNpmNamespace ? 2 : 1 ) ) ;
176+ const result = await resolveFromFolder ( pkgDir , subPath , packageName ) ;
177+ if ( result ) {
178+ resolveCache . set ( cacheKey , result ) ;
179+ return result ;
180+ }
181+
182+ throw new Error ( `Could not resolve ${ id } ` ) ;
183+ } ;
184+
149185const pluginNodeResolve = ( ) : Plugin => {
150186 return {
151187 name : 'node-resolve' ,
152188 resolveId ( id ) {
153189 if ( isBareImport ( id ) ) return { id : prefix + id , external : true } ;
154- if ( id . startsWith ( prefix ) ) {
155- return {
156- // Remove the leading slash, otherwise rollup turns it into a relative path up to disk root
157- id,
158- external : true ,
159- } ;
160- }
190+ // If requests already have the npm prefix, mark them as external
191+ if ( id . startsWith ( prefix ) ) return { id, external : true } ;
161192 } ,
162193 } ;
163194} ;
@@ -166,58 +197,60 @@ let npmCache: RollupCache | undefined;
166197
167198/**
168199 * Bundle am npm module entry path into a single file
169- * @param mod The module to bundle, including subpackage/path
200+ * @param mod The full path of the module to bundle, including subpackage/path
201+ * @param id The imported identifier
170202 * @param optimize Whether the bundle should be a minified/optimized bundle, or the default quick non-optimized bundle
171203 */
172- const bundleNpmModule = async ( mod : string , optimize : boolean ) => {
204+ const bundleNpmModule = async ( mod : string , id : string , optimize : boolean ) => {
205+ let namedExports : string [ ] = [ ] ;
206+ if ( dynamicCJSModules . has ( id ) ) {
207+ let isValidCJS = true ;
208+ try {
209+ const text = await fs . readFile ( mod , 'utf8' ) ;
210+ // Goal: Determine if it is ESM or CJS.
211+ // Try to parse it with cjs-module-lexer, if it fails, assume it is ESM
212+ // eslint-disable-next-line @cloudfour/typescript-eslint/await-thenable
213+ await parse ( text ) ;
214+ } catch {
215+ isValidCJS = false ;
216+ }
217+
218+ if ( isValidCJS ) {
219+ const require = createRequire ( import . meta. url ) ;
220+ // eslint-disable-next-line @cloudfour/typescript-eslint/no-var-requires
221+ const imported = require ( mod ) ;
222+ if ( typeof imported === 'object' && ! imported . __esModule )
223+ namedExports = Object . keys ( imported ) ;
224+ }
225+ }
226+
227+ const virtualEntry = '\0virtualEntry' ;
228+ const hasSyntheticNamedExports = namedExports . length > 0 ;
173229 const bundle = await rollup ( {
174- input : mod ,
230+ input : hasSyntheticNamedExports ? virtualEntry : mod ,
175231 cache : npmCache ,
176232 shimMissingExports : true ,
177233 treeshake : true ,
178234 preserveEntrySignatures : 'allow-extension' ,
179235 plugins : [
180- {
181- // This plugin fixes cases of module.exports = require('...')
182- // By default, the named exports from the required module are not generated
183- // This plugin detects those exports,
184- // and makes it so that @rollup /plugin-commonjs can see them and turn them into ES exports (via syntheticNamedExports)
185- // This edge case happens in React, so it was necessary to fix it.
186- name : 'cjs-module-lexer' ,
187- async transform ( code , id ) {
188- if ( id . startsWith ( '\0' ) ) return ;
189- const out = new MagicString ( code ) ;
190- const re =
191- / ( ^ | [ \s ; ] ) m o d u l e \. e x p o r t s \s * = \s * r e q u i r e \( [ " ' ] ( [ ^ " ' ] * ) [ " ' ] \) ( $ | [ \s ; ] ) / g;
192- let match ;
193- while ( ( match = re . exec ( code ) ) ) {
194- const [ , leadingWhitespace , moduleName , trailingWhitespace ] = match ;
195-
196- const resolved = await this . resolve ( moduleName , id ) ;
197- if ( ! resolved || resolved . external ) return ;
198-
199- try {
200- const text = await fs . readFile ( resolved . id , 'utf8' ) ;
201- // eslint-disable-next-line @cloudfour/typescript-eslint/await-thenable
202- const parsed = await parse ( text ) ;
203- let replacement = '' ;
204- for ( const exportName of parsed . exports ) {
205- replacement += `\nmodule.exports.${ exportName } = require("${ moduleName } ").${ exportName } ` ;
206- }
207-
208- out . overwrite (
209- match . index ,
210- re . lastIndex ,
211- leadingWhitespace + replacement + trailingWhitespace ,
212- ) ;
213- } catch {
214- return ;
236+ hasSyntheticNamedExports &&
237+ ( {
238+ // This plugin handles special-case packages whose named exports cannot be found via static analysis
239+ // For these packages, the package is require()'d, and the named exports are determined that way.
240+ // A virtual entry exports the named exports from the real entry package
241+ name : 'cjs-named-exports' ,
242+ resolveId ( id ) {
243+ if ( id === virtualEntry ) return virtualEntry ;
244+ } ,
245+ load ( id ) {
246+ if ( id === virtualEntry ) {
247+ const code = `export * from '${ mod } '
248+ export {${ namedExports . join ( ', ' ) } } from '${ mod } '
249+ export { default } from '${ mod } '` ;
250+ return code ;
215251 }
216- }
217-
218- return out . toString ( ) ;
219- } ,
220- } as Plugin ,
252+ } ,
253+ } as Plugin ) ,
221254 pluginNodeResolve ( ) ,
222255 processGlobalPlugin ( { NODE_ENV : 'development' } ) ,
223256 commonjs ( {
@@ -247,3 +280,9 @@ const bundleNpmModule = async (mod: string, optimize: boolean) => {
247280
248281 return output [ 0 ] . code ;
249282} ;
283+
284+ /**
285+ * Any package names in this set will need to have their named exports detected manually via require()
286+ * because the export names cannot be statically analyzed
287+ */
288+ const dynamicCJSModules = new Set ( [ 'prop-types' , 'react-dom' , 'react' ] ) ;
0 commit comments