react-rails icon indicating copy to clipboard operation
react-rails copied to clipboard

Support for Vite

Open ksweetie opened this issue 3 years ago • 13 comments

This is a feature suggestion, and I imagine it would be a ton of work, so I mainly just wanted to start a thread for discussion.

React-Rails already supports both Sprockets and Webpack. Adding support for Vite would be great. There's a relatively new vite_ruby gem, and the author there commented on the feasibility of using it with react-rails. In my case, the app I wanted to try it with uses SSR, so I didn't get very far in testing it out.

Any thoughts?

ksweetie avatar Jun 16 '21 19:06 ksweetie

I'd be interested in seeing the popularity of vite or other packaging tools as I've not used it before. I'd also be interested in making sure one maintainer uses vite primarily so that the points brought up by @ElMassimo are kept in mind when working on it. The point about using glob for context is something that I'm personally unaware of purely due to unfamiliarity for instance.

So this is a +1 if it's popular or upcoming and we have someone who is qualified to maintain this gem that would use vite regularly.

BookOfGreg avatar Jun 21 '21 10:06 BookOfGreg

In development mode Vite seem to use esbuild". It is possible to import components for esbuild using plugin similar to https://github.com/rails/jsbundling-rails/compare/main...xiaohui-zhangxh:main (note how Stimulus controllers are imported by @xiaohui-zhangxh)

I tried this plugin (/components path and .jsx hardcoded)

const componentPath = (module) => module.replace('./components/', '').replace('.jsx', '')
const glob  = require('glob').sync
// thanks: https://github.com/thomaschaaf/esbuild-plugin-import-glob
// thanks: https://github.com/rails/jsbundling-rails/compare/main...xiaohui-zhangxh:main
const ImportGlobPlugin = () => ({
  name: 'require-context',
  setup: (build) => {
    build.onResolve({ filter: /\*/ }, async (args) => {
      if (args.resolveDir === '') {
        return; // Ignore unresolvable paths
      }
      return {
        path: args.path,
        namespace: 'import-glob',
        pluginData: {
          resolveDir: args.resolveDir,
        },
      };
    });

    build.onLoad({ filter: /.*/, namespace: 'import-glob' }, async (args) => {
      const files = (
        glob(args.path, {
          cwd: args.pluginData.resolveDir,
        })
      ).sort();

      let importerCode = `
        ${files
        .map((module, index) => {
          return `import * as module${index} from '${module}'`})
        .join(';')}
        export default [${files
        .map((module, index) => `module${index}.default`)
        .join(',')}];
        export const context = {
          ${files.map((module, index) => `'${componentPath(module)}': module${index}.default`).join(',')}
        }
      `;

      return { contents: importerCode, resolveDir: args.pluginData.resolveDir };
    });
  },
});

Then in entrypoint this makes my components globally visible to react-rails

import { context } from './components/**/*.jsx';
Object.keys(context).forEach((key) => {
  window[key] = context[key]
})

pustomytnyk avatar Oct 05 '21 13:10 pustomytnyk

Couldn't fix this using @pustomytnyk solution as it errors out because require is not defined. This seems to work for me.

  var context = import.meta.globEager('../components/*.{js,jsx}');
  
  Object.keys(context).forEach((path) => {
    let component = context[path].default;
  
    `import * as ${ component.name } from '${ path }'`;  
  
    window[component.name] = component;
  });

pacMakaveli avatar May 04 '22 15:05 pacMakaveli

@pacMakaveli where that should be located? entrypoint? Thanks

Alxzu avatar May 10 '22 18:05 Alxzu

@pacMakaveli where that should be located? entrypoint? Thanks

I've put it in my main application.js. app/packs/entrypoints/application.js

pacMakaveli avatar May 10 '22 21:05 pacMakaveli

Did anyone manage to implement vite + SSR successfully?

Alxzu avatar Jul 01 '22 18:07 Alxzu

@Alxzu Yes, although not in the context of react-rails. See this example with Inertia.js and React.

ElMassimo avatar Jul 01 '22 19:07 ElMassimo

Can i help in fixing this issue?

Amit89480 avatar Oct 12 '22 18:10 Amit89480

Hi!

I found a possible solution (for Vite Ruby) on this line number https://github.com/reactjs/react-rails/blob/master/react_ujs/index.js#L79 I saw that and understood how it was made the constructor so, I take the code and it change a little bit and I do the following approach:

// app/javascript/helpers/viteConstructorRequireContext.js

export const viteConstructorRequireContext = function(reqCtx) {
  const fromRequireContext = function(reqCtx) {
    return function(className) {
      var parts = className.split(".");
      var filename = parts.shift();
      var keys = parts;
      // Load the module:
      var componentPath = Object.keys(reqCtx).find((path => path.search(filename) > 0));
      var component = reqCtx[componentPath];
      // Then access each key:
      keys.forEach(function(k) {
        component = component[k];
      });
      component = component.default;
      return component;
    }
  }

  const fromCtx = fromRequireContext(reqCtx);
  return function(className) {
    var component;
    try {
      // `require` will raise an error if this className isn't found:
      component = fromCtx(className);
    } catch (firstErr) {
      console.error(firstErr);
    }
    return component;
  }
}
// app/javascript/entrypoints/application.jsx

import ReactRailsUJS from "react_ujs";
import { viteConstructorRequireContext } from "../helpers/viteGetConstructor";

const componentsRequireContext = import.meta.globEager("~/components/main/**/*.{js,jsx}");
ReactRailsUJS.getConstructor = viteConstructorRequireContext(componentsRequireContext);

I expect that this approach it works for you.

memoxmrdl avatar Feb 03 '23 06:02 memoxmrdl

@memoxmrdl That's awesome! I got the non-SSR pages working.

Is it going to work with SSR? I tried but nothing was displayed with no errors.

paul-mesnilgrente avatar Sep 21 '23 19:09 paul-mesnilgrente

Actually I found the exception:

=> #<React::ServerRendering::PrerenderError: Encountered error "#<ExecJS::ProgramError: TypeError: Cannot read properties of undefined (reading 'serverRender')>" when prerendering HelloWorld with {"name":"World"}
eval (eval at <anonymous> ((execjs):36:8), <anonymous>:6:45)
eval (eval at <anonymous> ((execjs):36:8), <anonymous>:18:13)
(execjs):36:8
(execjs):54:14
(execjs):1:40
Object.<anonymous> ((execjs):1:58)
Module._compile (node:internal/modules/cjs/loader:1103:14)
Object.Module._extensions..js (node:internal/modules/cjs/loader:1155:10)
Module.load (node:internal/modules/cjs/loader:981:32)
Function.Module._load (node:internal/modules/cjs/loader:822:12)
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/execjs-2.8.1/lib/execjs/external_runtime.rb:39:in `exec'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/execjs-2.8.1/lib/execjs/external_runtime.rb:21:in `eval'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering/exec_js_renderer.rb:39:in `render_from_parts'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering/exec_js_renderer.rb:20:in `render'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering/bundle_renderer.rb:40:in `render'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/bundler/gems/react-rails-7814b829e645/lib/react/server_rendering.rb:27:in `block in render'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool.rb:110:in `block (2 levels) in with'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool.rb:109:in `handle_interrupt'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool.rb:109:in `block in with'
/home/paul/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/connection_pool-2.4.1/lib/connection_pool[2] pry(#<React::Rails::ComponentMount>)>

paul-mesnilgrente avatar Sep 21 '23 20:09 paul-mesnilgrente

Hi!

I found a possible solution (for Vite Ruby) on this line number https://github.com/reactjs/react-rails/blob/master/react_ujs/index.js#L79 I saw that and understood how it was made the constructor so, I take the code and it change a little bit and I do the following approach:

Thanks for posting this memoxmrdl, it was super helpful. I found that if you have overlapping component names, then there is a chance it will return the wrong component. Eg searching for NewForm when you have components named NewForm and SpecialNewForm, you might get SpecialNewForm.

Our components tend to be named directory/Name.js[x] or directory/Name/index.js[x] so I've tweaked the code a little to work for our specific use case, and shared it here in case it is useful to anyone else. This isn't 100% perfect, but works for our use case and naming style. We don't use A.B to reference components, so I've removed the className splitting code.

Note that this won't work on windows as it assumes / for the path separator.

// Based on https://github.com/reactjs/react-rails/issues/1134#issuecomment-1415112288
export const viteConstructorRequireContext = function(reqCtx) {
  const componentNameMatcher = className => {
    return path => {
      return (
        path.includes(`/${className}.js`) || path.includes(`/${className}/index.js`)
      );
    };
  };

  const fromRequireContext = function(reqCtx) {
    return function(className) {
      const componentPath = Object.keys(reqCtx).find(componentNameMatcher(className));

      const component = reqCtx[componentPath];
      return component.default;
    }
  }

  const fromCtx = fromRequireContext(reqCtx);
  return function(className) {
    var component;
    try {
      // `require` will raise an error if this className isn't found:
      component = fromCtx(className);
    } catch (firstErr) {
      console.error(firstErr);
    }
    return component;
  }
}

ajesler-hatch avatar Nov 09 '23 01:11 ajesler-hatch

FWIW, I'm considering supporting Vite with https://github.com/shakacode/react_on_rails, including SSR. If anybody is interested in that, reply here, and consider our Slack Channel.

I suspect that Vite might work very easily with https://www.shakacode.com/react-on-rails-pro/, which is an easy migration from react-rails.

justin808 avatar Nov 09 '23 02:11 justin808