Skip to content

RSC client/server manifests not emitted when monorepo has peer-dep doppelgangers of react-on-rails-rsc #3212

@AbanoubGhadban

Description

@AbanoubGhadban

Symptom

In the Pro dummy app on main, both react-client-manifest.json and react-server-client-manifest.json are not written by webpack. Every webpack compilation logs:

WARNING in Client runtime at react-on-rails-rsc/client was not found.
React Server Components module map file react-client-manifest.json was not created.

Same warning for react-server-client-manifest.json from the server bundle. As a downstream effect, every RSC page (/rsc_echo_props, /rsc_posts_page_over_http, etc.) errors at runtime with ENOENT: no such file or directory, open '.../react-client-manifest.json' and falls back to a broken client render.

How to reproduce

In a long-lived checkout where pnpm install has accumulated state across multiple webpack version bumps (i.e., not a fresh install):

cd react_on_rails_pro/spec/dummy
bundle exec rake react_on_rails:generate_packs
pnpm run build:dev

Both warnings fire; both manifest files are missing from public/webpack/development/ and ssr-generated/.

A fresh pnpm install (e.g., a brand-new worktree) does not reproduce, which has hidden the issue from CI and from anyone who tries the obvious "wipe node_modules and try again" remedy as their first step.

Root cause

The plugin in react-on-rails-rsc (path inside the package: dist/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js:87,141) detects whether webpack is bundling its runtime by strict path equality:

const clientFileNameOnClient = require.resolve("../client.browser.js"),
      clientFileNameOnServer = require.resolve("../client.node.js");
// ...
parser.hooks.program.tap("React Server Plugin", () => {
  const module = parser.state.module;
  if (module.resource === (_this.isServer ? clientFileNameOnServer : clientFileNameOnClient)
      && ((clientFileNameFound = !0), resolvedClientReferences))
    /* set up dep block */
});

If the === never matches, clientFileNameFound stays false and the manifest is never emitted (line 169).

The match fails in this monorepo because two physically separate copies of react-on-rails-rsc@19.0.4 exist on disk, both at the exact same npm version but resolved against different peer-dep contexts. Direct probe output captured by injecting a console.error into the parser hook:

[RSC-PROBE] isServer=false
  resource=/mnt/ssd/react_on_rails/node_modules/react-on-rails-rsc/dist/react-server-dom-webpack/client.browser.js
  expected=/mnt/ssd/react_on_rails/react_on_rails_pro/spec/dummy/node_modules/.pnpm/react-on-rails-rsc@19.0.4_…_webpack@5.103.0/node_modules/react-on-rails-rsc/dist/react-server-dom-webpack/client.browser.js
  match=false

Webpack imports the runtime from the workspace root copy; the plugin (loaded from the dummy's .pnpm/...webpack@5.103.0/... copy) computes require.resolve("../client.browser.js") from its own location and points to a different file. Both files are byte-identical.

Why we end up with two copies

pnpm-lock.yaml resolves react-on-rails-rsc@19.0.4 four separate ways, each tied to a different (react × react-dom × webpack) peer-dep combination:

react-on-rails-rsc@19.0.4(react-dom@19.2.3)(react@19.2.3)(webpack@5.105.2)
react-on-rails-rsc@19.0.4(react-dom@19.2.0)(react@19.2.0)(webpack@5.105.2)
react-on-rails-rsc@19.0.4(react-dom@19.2.3)(react@19.2.3)(webpack@5.104.1)
react-on-rails-rsc@19.0.4 (no peers)

The dummy itself uses webpack@5.103.0 (transitively via shakapacker@9.6.1). With node-linker=hoisted + shamefully-hoist=true (.npmrc), pnpm hoists one peer-dep variant to the workspace root and stashes the others under dummy/node_modules/.pnpm/.... The dummy's own resolution then ends up at a different path than the hoisted root copy that webpack happens to bundle from.

Two possible fix locations — assignee's call

I'm filing this assigned to me to decide between the two; both are defensible.

Option A — Fix the dummy app setup

Pin webpack to a single version via pnpm.overrides (or align all webpack/react/react-dom across workspace consumers) so pnpm produces only one peer-dep variant. Concretely, adding "webpack": "5.104.1" to overrides + a clean pnpm install reduces the four variants to one and the manifests are emitted on the next build.

Pro: isolated to dummy/workspace config; no upstream change.
Con: every monorepo that consumes react-on-rails-rsc and has more than one webpack version on its dep tree will hit this. We'd be papering over a real plugin fragility.

Option B — Fix react-on-rails-rsc plugin

Replace the strict module.resource === clientFileNameOnClient check with something that survives split installs — e.g., suffix-match on dist/react-server-dom-webpack/client.{browser,node}.js plus a walk up to package.json to verify name === 'react-on-rails-rsc'. Sketch:

function isReactOnRailsRSCRuntime(resource, isServer) {
  if (!resource) return false;
  const expectedSuffix = path.join('react-server-dom-webpack',
    isServer ? 'client.node.js' : 'client.browser.js');
  if (!resource.endsWith(expectedSuffix)) return false;
  let dir = path.dirname(resource);
  for (let i = 0; i < 10; i++) {
    const pkg = path.join(dir, 'package.json');
    if (fs.existsSync(pkg)) {
      try { return JSON.parse(fs.readFileSync(pkg, 'utf8')).name === 'react-on-rails-rsc'; }
      catch { return false; }
    }
    const parent = path.dirname(dir);
    if (parent === dir) return false;
    dir = parent;
  }
  return false;
}

Pro: plugin works correctly under any pnpm/yarn doppelganger configuration; one fix benefits every consumer.
Con: requires upstream change + release of react-on-rails-rsc.

Temporary workaround currently applied locally — DO NOT MERGE

To unblock visual inspection of issue #3211's reproduction page, I added "webpack": "5.104.1" to pnpm.overrides in the workspace root package.json and ran a clean pnpm install. This forces the doppelgangers to collapse to one copy and the manifests are emitted again. This change should not be merged to main — it's a local pin that papers over the real cause. Revert when this issue is properly resolved.

Suggested next steps for the assignee

  1. Decide A vs B (or both).
  2. If A: probably better to align webpack/react versions properly across workspace consumers rather than blunt-pinning, and restore the separate react_on_rails/spec/dummy and react_on_rails_pro/spec/dummy peer-dep contexts that originally diverged. The actual versions in the workspace today are 5.103.0 (Pro dummy via shakapacker), 5.104.1 (root + OSS dummy), 5.105.2 (transitively).
  3. If B: file as a separate issue against shakacode/react_on_rails_rsc and add a regression test. The plugin's existing tests likely all run in single-install scenarios.

Related

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions