evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
38.16k stars 1.15k forks source link

build react component emit bundles with import statement/ #2893

Open dante01yoon opened 1 year ago

dante01yoon commented 1 year ago

Hi, I'm trying build my react component for hydration for ssr. but my bundled file includes import statement with relative path, which needs entire node_modules folder placed in server.

how can I bundle appropriately ?

this is my esbuild config

const clientConfig ={
  bundle: true,
  entryPoints: ['src/entry-client.tsx'],
  platform: "browser",
  outfile: 'dist/main.js',
  format: "esm",
  loader: {
    '.js': 'jsx',
    '.ts': 'tsx',
    '.tsx': 'tsx',
  },
  // minify: true,
  external: Object.keys(dependencies),
  plugins: [
    {
      name: 'make-all-packages-external',
      setup(build) {
        let filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
        build.onResolve({ filter }, args => ({
          external: true,
          path: args.path,
        }));
      },
    },
  ],
  sourcemap: true,
}

build(clientConfig)

and then here is bundled file name main.js

(() => {
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
    get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
  }) : x)(function(x) {
    if (typeof require !== "undefined")
      return require.apply(this, arguments);
    throw new Error('Dynamic require of "' + x + '" is not supported');
  });

  // src/entry-client.tsx
  var import_client = __require("react-dom/client");
  var import_react_router_dom2 = __require("react-router-dom");

  // src/App.tsx
  var import_react_router_dom = __require("react-router-dom");

  // src/routes.ts
  var import_glob = __require("glob");
  var PagePathsWithComponents = (0, import_glob.sync)("./src/page/*.tsx");
  console.log("PagePathsWithComponents: ", PagePathsWithComponents);
  console.log({ PagePathsWithComponents });
  var routes = PagePathsWithComponents.map((path) => {
    const name = path.match(/\.\/pages\/(.*)\.tsx$/)[1];
    return {
      name,
      path: name === "Home" ? "/" : `/${name.toLowerCase()}`,
      component: PagePathsWithComponents[path].default
    };
  });
  var routesMap = routes.reduce((acc, cur) => {
    console.log("acc, cur", acc, cur);
    acc.set(cur.path, cur);
    return acc;
  }, /* @__PURE__ */ new Map());

  // src/App.tsx
  var import_jsx_runtime = __require("react/jsx-runtime");
  var App = () => {
    return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("html", { children: [
      /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("head", { children: [
        /* @__PURE__ */ (0, import_jsx_runtime.jsx)("meta", { charSet: "utf-8" }),
        /* @__PURE__ */ (0, import_jsx_runtime.jsx)("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
        /* @__PURE__ */ (0, import_jsx_runtime.jsx)("title", { children: "my App" })
      ] }),
      /* @__PURE__ */ (0, import_jsx_runtime.jsx)("body", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_router_dom.Routes, { children: Array.from(routesMap.values()).map(({ name, path, component: Element }) => {
        return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_router_dom.Route, { path, element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Element, {}) }, name);
      }) }) })
    ] });
  };

  // src/entry-client.tsx
  var import_jsx_runtime2 = __require("react/jsx-runtime");
  (0, import_client.hydrateRoot)(
    document,
    /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_router_dom2.BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(App, {}) })
  );
})();
//# sourceMappingURL=main.js.map
evanw commented 1 year ago

I'm confused. You have told esbuild to omit some files from your bundle, and they have been omitted. So it seems like esbuild is already doing what it was told to do. If you want to bundle react-router-dom then you can remove the plugin you added, and then it will be bundled. I'm probably not understanding what you're saying, sorry.

I'm trying build my react component for hydration for ssr

React hydration for SSR is not an intended use case for esbuild. You may need to use another tool instead that has been designed to do that.

dante01yoon commented 1 year ago

My bad. my bundled file has import statement of 'glob' package which can be used in node, not browser. I removed all node packages can't be used in browser and problem resolved.

dante01yoon commented 1 year ago

React hydration for SSR is not an intended use case for esbuild. You may need to use another tool instead that has been designed to do that.

Anyway, I wonder why hydration couldn't be done with esbuild? Isn't vitejs use esbuild for hydration?

evanw commented 1 year ago

AFAIK Vite doesn't use esbuild as a bundler. It only uses esbuild's transform API, which can do things such as removing TypeScript types and converting newer JavaScript syntax into older JavaScript syntax, but which doesn't process import paths at all.

dante01yoon commented 1 year ago

Ah sounds like babel does. I see. thank you!

dante01yoon commented 1 year ago

@evanw sorry to keep you stay here, but I see lots of react app bundled with esbuild. Is it ok to use esbuild to bundling tool for react?

evanw commented 1 year ago

There isn't really anything React-specific about esbuild outside of the fact that esbuild can convert JSX syntax to JS. So you can use esbuild to bundle React to the extent that React is a JavaScript library (esbuild will bundle it just like any other JavaScript library).

Other JavaScript build tools implement some additional React-specific things such as 'use client' directives for hybrid client/server React projects. That's not something esbuild does, so if you need those then you'll have to use different tools.

hyrious commented 1 year ago

React does introduce many complex server related things, I'd recommend you to use Next.js or some other frameworks which handle these server-side things more smoothly if you really want these features.

But for hydration, one usage is just to bake your component's html string into the html template, then use the hydrate method in client. In this case, you can made that in these steps:

  1. Adjust your build script for ssr html:

    // build.jsx
    import fs from "node:fs";
    import { renderToString } from "react-dom/server";
    import App from "./App.jsx";
    
    fs.writeFileSync("index.html", `<!DOCTYPE html>
    <html><title>Test</title><body>
     <div id="root">${renderToString(<App />)}</div>
     <script src="bundle.js"></script>
    </body></html>
    `);

    Note that since Node.js does not know JSX, you have to bundle this script first:

    esbuild build.jsx --bundle --platform=node --packages=external --jsx=automatic --outfile=build.js
    node build.js
  2. Adjust your client script to use hydrate:

    // main.jsx
    import { hydrateRoot } from "react-dom/client";
    import App from "./App";
    
    hydrateRoot(document.getElementById("root"), <App />);
    esbuild main.jsx --bundle --jsx=automatic --outfile=bundle.js

It should be mentioned that hydration does not solve all the problems, it only makes sure people could "see" things before the js bundle be fetched, where people actually cannot do anything.