@@ -4,23 +4,25 @@ import {createRequire} from "node:module";
44import op from "node:path" ;
55import { extname , join } from "node:path/posix" ;
66import { pathToFileURL } from "node:url" ;
7+ import commonjs from "@rollup/plugin-commonjs" ;
78import { nodeResolve } from "@rollup/plugin-node-resolve" ;
9+ import virtual from "@rollup/plugin-virtual" ;
810import { packageDirectory } from "pkg-dir" ;
911import type { AstNode , OutputChunk , Plugin , ResolveIdResult } from "rollup" ;
1012import { rollup } from "rollup" ;
1113import esbuild from "rollup-plugin-esbuild" ;
1214import { prepareOutput , toOsPath } from "./files.js" ;
1315import type { ImportReference } from "./javascript/imports.js" ;
1416import { isJavaScript , parseImports } from "./javascript/imports.js" ;
15- import { parseNpmSpecifier } from "./npm.js" ;
16- import { isPathImport } from "./path.js" ;
17+ import { parseNpmSpecifier , rewriteNpmImports } from "./npm.js" ;
18+ import { isPathImport , relativePath } from "./path.js" ;
1719import { faint } from "./tty.js" ;
1820
1921export async function resolveNodeImport ( root : string , spec : string ) : Promise < string > {
2022 return resolveNodeImportInternal ( op . join ( root , ".observablehq" , "cache" , "_node" ) , root , spec ) ;
2123}
2224
23- const bundlePromises = new Map < string , Promise < void > > ( ) ;
25+ const bundlePromises = new Map < string , Promise < string > > ( ) ;
2426
2527async function resolveNodeImportInternal ( cacheRoot : string , packageRoot : string , spec : string ) : Promise < string > {
2628 const { name, path = "." } = parseNpmSpecifier ( spec ) ;
@@ -31,24 +33,23 @@ async function resolveNodeImportInternal(cacheRoot: string, packageRoot: string,
3133 const { version} = JSON . parse ( await readFile ( op . join ( packageResolution , "package.json" ) , "utf-8" ) ) ;
3234 const resolution = `${ name } @${ version } /${ extname ( path ) ? path : path === "." ? "index.js" : `${ path } .js` } ` ;
3335 const outputPath = op . join ( cacheRoot , toOsPath ( resolution ) ) ;
34- if ( ! existsSync ( outputPath ) ) {
35- let promise = bundlePromises . get ( outputPath ) ;
36- if ( ! promise ) {
37- promise = ( async ( ) => {
38- process . stdout . write ( `${ spec } ${ faint ( "→" ) } ${ resolution } \n` ) ;
39- await prepareOutput ( outputPath ) ;
40- if ( isJavaScript ( pathResolution ) ) {
41- await writeFile ( outputPath , await bundle ( spec , cacheRoot , packageResolution ) ) ;
42- } else {
43- await copyFile ( pathResolution , outputPath ) ;
44- }
45- } ) ( ) ;
46- bundlePromises . set ( outputPath , promise ) ;
47- promise . catch ( console . error ) . then ( ( ) => bundlePromises . delete ( outputPath ) ) ;
36+ const resolutionPath = `/_node/${ resolution } ` ;
37+ if ( existsSync ( outputPath ) ) return resolutionPath ;
38+ let promise = bundlePromises . get ( outputPath ) ;
39+ if ( promise ) return promise ; // coalesce concurrent requests
40+ promise = ( async ( ) => {
41+ console . log ( `${ spec } ${ faint ( "→" ) } ${ outputPath } ` ) ;
42+ await prepareOutput ( outputPath ) ;
43+ if ( isJavaScript ( pathResolution ) ) {
44+ await writeFile ( outputPath , await bundle ( resolutionPath , spec , require , cacheRoot , packageResolution ) , "utf-8" ) ;
45+ } else {
46+ await copyFile ( pathResolution , outputPath ) ;
4847 }
49- await promise ;
50- }
51- return `/_node/${ resolution } ` ;
48+ return resolutionPath ;
49+ } ) ( ) ;
50+ promise . catch ( console . error ) . then ( ( ) => bundlePromises . delete ( outputPath ) ) ;
51+ bundlePromises . set ( outputPath , promise ) ;
52+ return promise ;
5253}
5354
5455/**
@@ -69,29 +70,59 @@ export function extractNodeSpecifier(path: string): string {
6970 return path . replace ( / ^ \/ _ n o d e \/ / , "" ) ;
7071}
7172
72- async function bundle ( input : string , cacheRoot : string , packageRoot : string ) : Promise < string > {
73+ /**
74+ * React (and its dependencies) are distributed as CommonJS modules, and worse,
75+ * they’re incompatible with cjs-module-lexer; so when we try to import them as
76+ * ES modules we only see a default export. We fix this by creating a shim
77+ * module that exports everything that is visible to require. I hope the React
78+ * team distributes ES modules soon…
79+ *
80+ * https://github.com/facebook/react/issues/11503
81+ */
82+ function isBadCommonJs ( specifier : string ) : boolean {
83+ const { name} = parseNpmSpecifier ( specifier ) ;
84+ return name === "react" || name === "react-dom" || name === "react-is" || name === "scheduler" ;
85+ }
86+
87+ function shimCommonJs ( specifier : string , require : NodeRequire ) : string {
88+ return `export {${ Object . keys ( require ( specifier ) ) } } from ${ JSON . stringify ( specifier ) } ;\n` ;
89+ }
90+
91+ async function bundle (
92+ path : string ,
93+ input : string ,
94+ require : NodeRequire ,
95+ cacheRoot : string ,
96+ packageRoot : string
97+ ) : Promise < string > {
7398 const bundle = await rollup ( {
74- input,
99+ input : isBadCommonJs ( input ) ? "-" : input ,
75100 plugins : [
76- nodeResolve ( { browser : true , rootDir : packageRoot } ) ,
101+ ... ( isBadCommonJs ( input ) ? [ ( virtual as any ) ( { "-" : shimCommonJs ( input , require ) } ) ] : [ ] ) ,
77102 importResolve ( input , cacheRoot , packageRoot ) ,
103+ nodeResolve ( { browser : true , rootDir : packageRoot } ) ,
104+ ( commonjs as any ) ( { esmExternals : true } ) ,
78105 esbuild ( {
79106 format : "esm" ,
80107 platform : "browser" ,
81108 target : [ "es2022" , "chrome96" , "firefox96" , "safari16" , "node18" ] ,
82109 exclude : [ ] , // don’t exclude node_modules
110+ define : { "process.env.NODE_ENV" : JSON . stringify ( "production" ) } ,
83111 minify : true
84112 } )
85113 ] ,
114+ external ( source ) {
115+ return source . startsWith ( "/_node/" ) ;
116+ } ,
86117 onwarn ( message , warn ) {
87118 if ( message . code === "CIRCULAR_DEPENDENCY" ) return ;
88119 warn ( message ) ;
89120 }
90121 } ) ;
91122 try {
92- const output = await bundle . generate ( { format : "es" } ) ;
93- const code = output . output . find ( ( o ) : o is OutputChunk => o . type === "chunk" ) ! . code ; // TODO don’t assume one chunk?
94- return code ;
123+ const output = await bundle . generate ( { format : "es" , exports : "named" } ) ;
124+ const code = output . output . find ( ( o ) : o is OutputChunk => o . type === "chunk" ) ! . code ;
125+ return rewriteNpmImports ( code , ( i ) => ( i . startsWith ( "/_node/" ) ? relativePath ( path , i ) : i ) ) ;
95126 } finally {
96127 await bundle . close ( ) ;
97128 }
0 commit comments