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);