remix: Setting server in remix.config.js fails when using express adapter

What version of Remix are you using?

1.4.3

Steps to Reproduce

  1. Create app using create-remix (express, just the basics, typescript)
  2. Set server in remix.config.js to let remix produce one consolidated build.
  3. npm run build works
  4. npm run dev fails

Sample code here: https://github.com/edwin177/remix-express/tree/express-server-build

Expected Behavior

Server should load

Actual Behavior

App crashes with the following error:

/home/xxx/remix-express/node_modules/@remix-run/server-runtime/routes.js:18
  return Object.entries(manifest).filter(([, route]) => route.parentId === parentId).map(([id, route]) => ({ ...route,
                ^
TypeError: Cannot convert undefined or null to object
    at Function.entries (<anonymous>)
    at Object.createRoutes (/home/xxx/remix-express/node_modules/@remix-run/server-runtime/routes.js:18:17)
    at Object.createRequestHandler (/home/xxx/remix-express/node_modules/@remix-run/server-runtime/server.js:26:25)
    at createRequestHandler (/home/xxx/remix-express/node_modules/@remix-run/express/server.js:35:28)
    at Object.<anonymous> (/home/xxx/remix-express/build/index.js:42:5)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 15
  • Comments: 16 (2 by maintainers)

Most upvoted comments

I ran into this with Remix 1.8.2. It seems like the problem is twofold.

Problem 1: overridden entrypoint to esbuild

Setting server in remix.config.js changes the esbuild config. I think the problem may be here? https://github.com/remix-run/remix/blob/8fb82b27bcf7174fcd83dd0fc63d99764ff264c1/packages/remix-dev/compiler/compilerServer.ts#L35-L43

As a result, build/index.js only includes things imported from the server file. As an extreme example, when the server file only includes

console.log("wat");

then build/index.js only includes that:

"use strict";

// server.ts
console.log("wat");

Problem 2: cyclic dependencies

If the server is bundled into the same file as the application, this creates a cyclic dependency.

  • You run node build/index.js to start the server
  • node requires build/index.js
  • While loading, it hits the createRequestHandler call, and attempts to require build/index.js
  • require returns {} to avoid an infinite loop
  • createRoutes throws because manifest is undefined

A workaround that doesn’t require creating a separate esbuild step

I worked around these issues by removing server from remix.config.js and putting the following code in app/entry.server.tsx

import run from "./run.server";

if (require.main === module) {
  process.nextTick(run);
}

The run function creates the express app and calls listen. Because it runs in the next tick, requiring build/index.js does not create a loop.

Solving the problems

I think if server is specified in the config, the compiler needs to produce two files. build/index.js would be produced from the server file and build/app.js would be the application code.

Hey folks, for whoever is running into this issue like I did this past week (Remix 1.18.1), I’ve managed to circumvent this problem with a mix of the suggestion from @mfadlyramadhan above and some custom setup I did for running the Express server during local development, including support for hot-module replacement.

i’ve created a repo here that you can take a look or even use it in case you want to have Remix + Express (TS) + development with HMR + Docker builds.

https://github.com/brunojppb/remix-express-typescript

The secret is mostly in the remix.config.js to only add the server entry during production builds as you can see here which avoids this bug.

...(isProd
    ? {
        server: "server/prod.ts",
        serverBuildPath: "build/index.js",
      }
    : {}),

Then I customised the Express server to have a dynamic entrypoint where the development and production setup can be adjusted based on your needs.

  1. In development, you have an entrypoint with Remix request handler and HMR replacement hooked up here.
  2. In production, you just read the build files generated by the compiler here.

Ideally, would be great if the Remix team could make the server entry work smoothly with the entire Express Adapter + TypeScript compilation setup.

Last, but not least, thanks to the Remix team for the incredible work put on this framework! 👏

CC: @raulfdm @leoweigand @dge808

This issue is still happening. (try upgrading from 1.2.3 to 1.7.4 Today)

Btw, still getting the same error with 1.7.0.

Sample code: https://github.com/edwin177/remix-express-1.7

I’m relatively new to remix so please correct me if I wrong. I’ve somehow managed to run the remix with express adapter, setting the server in remix.config.js to server.ts. Then in the server.ts I just use the import * as build from "@remix-run/dev/server-build". Here is what I did:

// rest of imports
import * as build from "@remix-run/dev/server-build";

// rest of server.ts

app.all(
  "*",
  process.env.NODE_ENV === "development"
    ? (req, res, next) => {
        purgeRequireCache();

        return createRequestHandler({
          build, // removed the require to BUILD_DIR
          mode: process.env.NODE_ENV,
        })(req, res, next);
      }
    : createRequestHandler({
        build, // removed the require to BUILD_DIR
        mode: process.env.NODE_ENV,
      })
);

// rest of server.ts

I don’t know if this is the correct way to do it. I have also looked on the other adapter templates and it shows the usage of the import * as build from "@remix-run/dev/server-build".

I’m sorry if my english is bad cause it’s not even my second language. Hopefully this helps as a workaround until we have the correct way to do it.

Hmm from the docs

A server entrypoint, relative to the root directory that becomes your server’s main module. If specified, Remix will compile this file along with your application into a single file to be deployed to your server. This file can use either a .js or .ts file extension.

I thought I could just throw in a server.ts but running node ./build/index.js & remix watch errors

[..]/indie-stack/node_modules/@remix-run/express/node_modules/@remix-run/server-runtime/dist/routes.js:18
  return Object.entries(manifest).filter(([, route]) => route.parentId === parentId).map(([id, route]) => ({ ...route,
                ^
TypeError: Cannot convert undefined or null to object
    at Function.entries (<anonymous>)
    at Object.createRoutes ([..]/indie-stack/node_modules/@remix-run/express/node_modules/@remix-run/server-runtime/dist/routes.js:18:17)
    at Object.createRequestHandler ([..]/indie-stack/node_modules/@remix-run/express/node_modules/@remix-run/server-runtime/dist/server.js:26:25)
    at createRequestHandler ([..]/indie-stack/node_modules/@remix-run/express/dist/server.js:34:28)
    at Object.<anonymous> ([..]/indie-stack/build/index.js:42:79)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)

Would be amazing if a custom Express (Fastify, Koa) typescript server wouldn’t require a custom pipeline but could “just” work like that and the docs actually sound like it should work, at least I understand them like that

Btw, I just tried this with v1.5.0, and I’m getting the same error as I did with v1.4.3.