From e9cbadc95d69aae88476245af64f246bf57392af Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 11 Apr 2024 23:03:42 -0700 Subject: [PATCH] fix react import --- docs/lib/react.md | 13 +++++++++ package.json | 3 ++ src/node.ts | 49 ++++++++++++++++++++++++++------ yarn.lock | 71 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 docs/lib/react.md diff --git a/docs/lib/react.md b/docs/lib/react.md new file mode 100644 index 000000000..8eb524958 --- /dev/null +++ b/docs/lib/react.md @@ -0,0 +1,13 @@ +# React + +```js echo +import {createElement} from "react"; + +display(createElement); +``` + +```js echo +import {createRoot} from "react-dom/client"; + +display(createRoot); +``` diff --git a/package.json b/package.json index 3f6444fa6..1c3a57b2c 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@clack/prompts": "^0.7.0", "@observablehq/inputs": "^0.10.6", "@observablehq/runtime": "^5.9.4", + "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "acorn": "^8.11.2", "acorn-walk": "^8.3.0", @@ -118,6 +119,8 @@ "glob": "^10.3.10", "mocha": "^10.2.0", "prettier": "^3.0.3 <3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rimraf": "^5.0.5", "tempy": "^3.1.0", "typescript": "^5.2.2", diff --git a/src/node.ts b/src/node.ts index b155d98c2..f0bf6cedb 100644 --- a/src/node.ts +++ b/src/node.ts @@ -2,8 +2,9 @@ import {existsSync} from "node:fs"; import {copyFile, readFile, writeFile} from "node:fs/promises"; import {createRequire} from "node:module"; import op from "node:path"; -import {extname, join} from "node:path/posix"; +import {dirname, extname, join, relative} from "node:path/posix"; import {pathToFileURL} from "node:url"; +import commonjs from "@rollup/plugin-commonjs"; import {nodeResolve} from "@rollup/plugin-node-resolve"; import {packageDirectory} from "pkg-dir"; import type {AstNode, OutputChunk, Plugin, ResolveIdResult} from "rollup"; @@ -12,7 +13,7 @@ import esbuild from "rollup-plugin-esbuild"; import {prepareOutput, toOsPath} from "./files.js"; import type {ImportReference} from "./javascript/imports.js"; import {isJavaScript, parseImports} from "./javascript/imports.js"; -import {parseNpmSpecifier} from "./npm.js"; +import {parseNpmSpecifier, rewriteNpmImports} from "./npm.js"; import {isPathImport} from "./path.js"; import {faint} from "./tty.js"; @@ -38,7 +39,15 @@ async function resolveNodeImportInternal(cacheRoot: string, packageRoot: string, process.stdout.write(`${spec} ${faint("→")} ${resolution}\n`); await prepareOutput(outputPath); if (isJavaScript(pathResolution)) { - await writeFile(outputPath, await bundle(spec, cacheRoot, packageResolution)); + await writeFile( + outputPath, + await bundle( + `/_node/${resolution}`, + overrideNodeResolution(spec, packageResolution), + cacheRoot, + packageResolution + ) + ); } else { await copyFile(pathResolution, outputPath); } @@ -51,6 +60,25 @@ async function resolveNodeImportInternal(cacheRoot: string, packageRoot: string, return `/_node/${resolution}`; } +/** + * React (and its dependencies) are still distributed as CommonJS modules, which + * means we lose named exports when we bundle them as an ES module — there’s + * only a default export. We can fix this by importing the production bundle + * instead, which is (by luck) compatible with cjs-module-lexer. This is quite + * terrible, and I hope that the React team distributes ES modules soon. + * + * https://github.com/facebook/react/issues/11503 + */ +function overrideNodeResolution(specifier: string, packageResolution: string): string { + return specifier === "react" + ? op.join(packageResolution, "cjs", "react.production.min.js") + : specifier === "react-dom" || specifier === "react-dom/client" + ? op.join(packageResolution, "cjs", "react-dom.production.min.js") + : specifier === "scheduler" + ? op.join(packageResolution, "cjs", "scheduler.production.min.js") + : specifier; +} + /** * Resolves the direct dependencies of the specified node import path, such as * "/_node/d3-array@3.2.4/src/index.js", returning a set of node import paths. @@ -69,29 +97,34 @@ export function extractNodeSpecifier(path: string): string { return path.replace(/^\/_node\//, ""); } -async function bundle(input: string, cacheRoot: string, packageRoot: string): Promise { +async function bundle(path: string, input: string, cacheRoot: string, packageRoot: string): Promise { const bundle = await rollup({ input, plugins: [ - nodeResolve({browser: true, rootDir: packageRoot}), importResolve(input, cacheRoot, packageRoot), + nodeResolve({browser: true, rootDir: packageRoot}), + commonjs({esmExternals: true}), esbuild({ format: "esm", platform: "browser", target: ["es2022", "chrome96", "firefox96", "safari16", "node18"], exclude: [], // don’t exclude node_modules + define: {"process.env.NODE_ENV": JSON.stringify("production")}, minify: true }) ], + external(source) { + return source.startsWith("/_node/"); + }, onwarn(message, warn) { if (message.code === "CIRCULAR_DEPENDENCY") return; warn(message); } }); try { - const output = await bundle.generate({format: "es"}); - const code = output.output.find((o): o is OutputChunk => o.type === "chunk")!.code; // TODO don’t assume one chunk? - return code; + const output = await bundle.generate({format: "es", exports: "named"}); + const code = output.output.find((o): o is OutputChunk => o.type === "chunk")!.code; + return rewriteNpmImports(code, (i) => (i.startsWith("/_node/") ? relative(dirname(path), i) : i)); } finally { await bundle.close(); } diff --git a/yarn.lock b/yarn.lock index f8da13c48..c57eab3a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,7 +339,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -410,6 +410,18 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@rollup/plugin-commonjs@^25.0.7": + version "25.0.7" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" + integrity sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.30.3" + "@rollup/plugin-node-resolve@^15.2.3": version "15.2.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" @@ -523,7 +535,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== -"@types/estree@1.0.5", "@types/estree@^1.0.0": +"@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -1128,6 +1140,11 @@ commander@7: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + component-emitter@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" @@ -1970,7 +1987,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@8.1.0: +glob@8.1.0, glob@^8.0.3: version "8.1.0" resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== @@ -2353,6 +2370,13 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -2464,6 +2488,11 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -2602,6 +2631,13 @@ log-symbols@4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -2621,6 +2657,13 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +magic-string@^0.30.3: + version "0.30.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.9.tgz#8927ae21bfdd856310e07a1bc8dd5e73cb6c251d" + integrity sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -3036,6 +3079,21 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -3221,6 +3279,13 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"