diff --git a/.changeset/get-dependencies-from-esm-without-main.md b/.changeset/get-dependencies-from-esm-without-main.md new file mode 100644 index 00000000000..614a898bea6 --- /dev/null +++ b/.changeset/get-dependencies-from-esm-without-main.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +Update `getDependenciesToBundle` to handle ESM packages without main exports. Note that these packages must expose `package.json` in their `exports` field so that their path can be resolved. diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index 116e94abde7..12cbe904c37 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -28,6 +28,7 @@ test.describe("compiler", () => { "esm-only-pkg", "esm-only-single-export", ...getDependenciesToBundle("esm-only-exports-pkg"), + ...getDependenciesToBundle("esm-only-nested-exports-pkg"), ], }; `, @@ -84,6 +85,13 @@ test.describe("compiler", () => { return
{esmOnlyPkg}
; } `, + "app/routes/esm-only-nested-exports-pkg.tsx": js` + import esmOnlyPkg from "esm-only-nested-exports-pkg/nested"; + + export default function EsmOnlyPkg() { + return
{esmOnlyPkg}
; + } + `, "app/routes/esm-only-single-export.tsx": js` import esmOnlyPkg from "esm-only-single-export"; @@ -129,6 +137,18 @@ test.describe("compiler", () => { "node_modules/esm-only-exports-pkg/esm-only-exports-pkg.js": js` export default "esm-only-exports-pkg"; `, + "node_modules/esm-only-nested-exports-pkg/package.json": json({ + name: "esm-only-nested-exports-pkg", + version: "1.0.0", + type: "module", + exports: { + "./package.json": "./package.json", + "./nested": "./esm-only-nested-exports-pkg.js", + }, + }), + "node_modules/esm-only-nested-exports-pkg/esm-only-nested-exports-pkg.js": js` + export default "esm-only-nested-exports-pkg"; + `, "node_modules/esm-only-single-export/package.json": json({ name: "esm-only-exports-pkg", version: "1.0.0", @@ -288,6 +308,18 @@ test.describe("compiler", () => { ); }); + test("allows consumption of ESM modules with only nested exports in CJS builds with `serverDependenciesToBundle` and `getDependenciesToBundle`", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + let res = await app.goto("/esm-only-nested-exports-pkg", true); + expect(res.status()).toBe(200); // server rendered fine + // rendered the page instead of the error boundary + expect(await app.getHtml("#esm-only-nested-exports-pkg")).toBe( + '
esm-only-nested-exports-pkg
' + ); + }); + test("allows consumption of packages with sub modules", async ({ page }) => { let app = new PlaywrightFixture(appFixture, page); let res = await app.goto("/package-with-submodule", true); diff --git a/packages/remix-dev/dependencies.ts b/packages/remix-dev/dependencies.ts index cbf53c10781..e3d6393aab8 100644 --- a/packages/remix-dev/dependencies.ts +++ b/packages/remix-dev/dependencies.ts @@ -40,6 +40,17 @@ export function getDependenciesToBundle(...pkg: string[]): string[] { return Array.from(aggregatedDeps); } +interface ErrorWithCode extends Error { + code: string; +} + +function isErrorWithCode(error: unknown): error is ErrorWithCode { + return ( + error instanceof Error && + typeof (error as NodeJS.ErrnoException).code === "string" + ); +} + function getPackageDependenciesRecursive( pkg: string, aggregatedDeps: Set, @@ -47,7 +58,18 @@ function getPackageDependenciesRecursive( ): void { visitedPackages.add(pkg); - let pkgPath = require.resolve(pkg); + let pkgPath: string; + try { + pkgPath = require.resolve(pkg); + } catch (err) { + if (isErrorWithCode(err) && err.code === "ERR_PACKAGE_PATH_NOT_EXPORTED") { + // Handle packages without main exports. + // They at least need to have package.json exported. + pkgPath = require.resolve(`${pkg}/package.json`); + } else { + throw err; + } + } let lastIndexOfPackageName = pkgPath.lastIndexOf(pkg); if (lastIndexOfPackageName !== -1) { pkgPath = pkgPath.substring(0, lastIndexOfPackageName);