jspm / generator

JSPM Import Map Generator
Apache License 2.0
166 stars 21 forks source link

Duplicated React module import when Node.js imports 'react-dom/server', but the renderToString function resolves the React library via an import map #122

Closed zachsa closed 2 years ago

zachsa commented 2 years ago

Using JSPM I generate the following import maps.

Browser import map

  {
    "scopes": {
      "../": {
        "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
        "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.client.js",
        "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.0.0/dev.jsx-runtime.js"
      },
      "https://ga.jspm.io/": {
        "process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/browser/process.js",
        "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
        "react-dom": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.index.js",
        "scheduler": "https://ga.jspm.io/npm:scheduler@0.21.0/dev.index.js"
      }
    }
  }

Node.js (without module flag)

{
  "scopes": {
    "./": {
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.client.js",
      "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.0.0/dev.jsx-runtime.js"
    },
    "https://ga.jspm.io/": {
      // deleted the node:process specifier
      "process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/process.js",
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.index.js",
      "scheduler": "https://ga.jspm.io/npm:scheduler@0.21.0/dev.index.js"
    }
  }
}

Node.js import map (with module flag)

{
  "scopes": {
    "./": {
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.client.js",
      "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.0.0/dev.jsx-runtime.js"
    },
    "https://ga.jspm.io/": {
      "process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/process.js",
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.index.js",
      "scheduler": "https://ga.jspm.io/npm:scheduler@0.21.0/dev.index.js"
    }
  }
}
Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
  TypeError: Cannot read properties of null (reading 'useState')
      at useState (https://ga.jspm.io/npm:react@18.0.0/dev.index.js:189:5711)
      at Counter (file:///home/zach/code/saeon/repositories/somisana/web/.cache/ssr.algoa-bay-forecast.js:84:30)
      at renderWithHooks (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5461:16)
      at renderIndeterminateComponent (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5534:15)
      at renderElement (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5749:7)
      at renderNodeDestructive (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5888:11)
      at retryTask (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6260:5)
      at performWork (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6307:7)
      at /home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6628:12
      at scheduleWork (/home/zach/code/saeon/repositories/somisana/web/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:82:3)

I can solve the React error in three ways:

  1. I can manually remove the top level react specifier in the node import map to get the following import map:
{
  "scopes": {
    "./": {
      "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.client.js",
      "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.0.0/dev.jsx-runtime.js"
    },
    "https://ga.jspm.io/": {
      "process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/process.js",
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.index.js",
      "scheduler": "https://ga.jspm.io/npm:scheduler@0.21.0/dev.index.js"
    }
  }
}
  1. I can include an import to react-dom/server on the client (and console.log the import so that the code is used), and as a result I get additional imports in both the browser and the node import map

new browser import map

  {
    "scopes": {
      "../": {
        "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
        "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.client.js",
        "react-dom/server": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.server.browser.js",
        "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.0.0/dev.jsx-runtime.js"
      },
      "https://ga.jspm.io/": {
        "process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/browser/process.js",
        "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
        "react-dom": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.index.js",
        "scheduler": "https://ga.jspm.io/npm:scheduler@0.21.0/dev.index.js"
      }
    }
  }

new node import map

{
  "scopes": {
    "./": {
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom/client": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.client.js",
      "react-dom/server": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.server.node.js",
      "react/jsx-runtime": "https://ga.jspm.io/npm:react@18.0.0/dev.jsx-runtime.js"
    },
    "https://ga.jspm.io/": {
      "buffer": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/buffer.js",
      "node:buffer": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/buffer.js",
      "node:process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/process.js",
      "node:stream": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/stream.js",
      "process": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/process.js",
      "react": "https://ga.jspm.io/npm:react@18.0.0/dev.index.js",
      "react-dom": "https://ga.jspm.io/npm:react-dom@18.0.0/dev.index.js",
      "scheduler": "https://ga.jspm.io/npm:scheduler@0.21.0/dev.index.js",
      "stream": "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.24/nodelibs/node/stream.js"
    }
  }
}

After manually deleting the node:buffer, node:process and node:stream specifiers then the code works as it should

  1. Instead of including the react-dom/server import in the client code (I'm generating the node and browser import maps from the client entry point), I can specify an HTTP import in the SSR code:
# import { renderToString } from 'react-dom/server' # instead of this
import {renderToString} from 'https://ga.jspm.io/npm:react-dom@18.0.0/dev.server.node.js' # do this

This works, although I would have to manually update the import when package.json versions of react and react-dom change. It seems like a better solution would be to define an import map for the Node app in addition to the client app, merge them, and use the compiled node import map as the import resolver. Is this possible?

Alternatively Would it be possible for the module to be cached by a hash of the code rather than the import location (seems like that is what's happening - sorry if that doesn't make sense, I'm not very familiar with how the import module cache works except that I can see that it exists)?

Koa.js SSR rendering This is my SSR logic (Koa.js )

import { renderToString } from 'react-dom/server' // I suspect that this is importing React separately from the import map
...

export default async ctx => {
  ...
  const { default : Component} = await import('./some/component/index.js')
  ctx.set('Content-type', 'text/html')
  ctx.body = html.replace(
      '<div id="root"></div>',
      `<div id="root">${renderToString(Component())}</div>`
    ) // The 'renderToString' function imports a different copy of React to what is in the module cache
}
zachsa commented 2 years ago

(thinking about this - it's easy enough to merge import maps by just creating a entry js file that imports the other entries for merging)

zachsa commented 2 years ago

This is not related to JSPM