Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Vite #1134

Open
ksweetie opened this issue Jun 16, 2021 · 13 comments
Open

Support for Vite #1134

ksweetie opened this issue Jun 16, 2021 · 13 comments

Comments

@ksweetie
Copy link

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 ksweetie changed the title Add support for ViteJs Support for ViteJs Jun 16, 2021
@ksweetie ksweetie changed the title Support for ViteJs Support for Vite Jun 16, 2021
@BookOfGreg
Copy link
Member

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.

@pustomytnyk
Copy link

pustomytnyk commented Oct 5, 2021

In development mode Vite seem to use esbuild". It is possible to import components for esbuild using plugin similar to rails/jsbundling-rails@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]
})

@pacMakaveli
Copy link

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;
  });

@Alxzu
Copy link

Alxzu commented May 10, 2022

@pacMakaveli where that should be located? entrypoint? Thanks

@pacMakaveli
Copy link

@pacMakaveli where that should be located? entrypoint? Thanks

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

@Alxzu
Copy link

Alxzu commented Jul 1, 2022

Did anyone manage to implement vite + SSR successfully?

@ElMassimo
Copy link

ElMassimo commented Jul 1, 2022

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

@Amit89480
Copy link

Can i help in fixing this issue?

@memoxmrdl
Copy link

memoxmrdl commented Feb 3, 2023

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.

@paul-mesnilgrente
Copy link

@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
Copy link

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>)>

@ajesler-hatch
Copy link

ajesler-hatch commented Nov 9, 2023

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;
  }
}

@justin808
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests