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

Dynamic import/require in resolveComponent/resolve function using modules? #957

Closed
maxonfjvipon opened this issue Oct 29, 2021 · 12 comments
Closed
Labels
react Related to the react adapter

Comments

@maxonfjvipon
Copy link

maxonfjvipon commented Oct 29, 2021

Versions:

  • @inertiajs/inertia version: 0.10.1
  • @inertiajs/inertia-react version: 0.7.1

Describe the problem:

Hello everyone. I use InertiaJS with Laravel. Also I use nWidart/laravel-modules. I want to render JS files from my modules dynamically. They are in Modules/Module-name/Resources/assets/js.
This is my controller

    public function showLoginForm(): Response
    {
        return Inertia::render('AdminPanel::Login');
    }

My webpack.mix

mix.js('resources/js/app.js', 'public/js')
    .react()
    .sass('resources/sass/app.scss', 'public/css')
    .webpackConfig({
        output: {chunkFilename: 'js/[name].js?id=[chunkhash]'},
        resolve: {
            alias: {
                '@': path.resolve(__dirname, 'resources/js'),
                '~': path.resolve(__dirname, 'Modules'),
            }
        },
    })

mix.version()

This is my app.js

const app = document.getElementById('app');

render(
    <InertiaApp
        initialPage={JSON.parse(app.dataset.page)}
        resolveComponent={async name => {
            let parts = name.split('::')
            if(parts.length > 1) {
                return import(`~/${parts[0]}/Resources/assets/js/Pages/${parts[1]}`).then(module => module.default)
            }else {
                return import('./src/Pages/' + name).then(module => module.default)
            }
        }}
    />,
    app
);

In this case I go many webpack errors like:

ERROR in ./node_modules/enhanced-resolve/lib/MainFieldPlugin.js 8:13-28
Module not found: Error: Can't resolve 'path' in '/Volumes/Storage/Shared/pump-manager/node_modules/enhanced-resolve/lib'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
        - install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "path": false }

I also tried with CreateInertiaApp and required functions - the same result

But if I use static path to module files like: ~/AdminPanel/Resources/assets/js/${parts[0]} then there are no errors and everything works fine. But that means I should describe all my modules and static paths to their js files in app.js. I would want to escape this.

I also checked this issue and this one but unsuccessfully... Someone has the same errors, someone has it works fine

Thanks

@maxonfjvipon maxonfjvipon added the react Related to the react adapter label Oct 29, 2021
@maxonfjvipon maxonfjvipon changed the title Dynamic import/require in resolveComponent function? Dynamic import/require in resolveComponent/resolve function using modules? Oct 29, 2021
@maxeckel
Copy link

Have you imported "path" into your webpack.mix?

Like: const path = require('path');

@maxonfjvipon
Copy link
Author

maxonfjvipon commented Oct 29, 2021

Have you imported "path" into your webpack.mix?

Like: const path = require('path');

Of course.
I said that it works fine when path in import function is static like: import(~/AdminPanel/Resources/assets/js/${name}). That means that tilda alias works. But when I replace AdminPanel to ${parts[0]} - it breaks

@rafalkrawiec
Copy link
Contributor

@maxonfjvipon Not 100% sure but it might be related to the webpack's dynamic imports being limited to specific directory only, meaning that you will be able to only bundle files in a single directory you provide in your dynamic expression.

I had the same problem with my project where I've tried to generate separate chunks for every page to avoid loading whole app in single JS file as it is treated as performance issue. I wasn't able to do that with standard dynamic import because I have my pages separated into multiple directories. I ended up using require.context (you can find the docs here: https://webpack.js.org/api/module-methods/#requirecontext).

Here is how I've configured my entrypoint file:

const app  = document.getElementById("app") as HTMLElement;
const init = JSON.parse(app.dataset.page as string);
const context = require.context("resources/js/Pages", true, /\.tsx$/, "lazy");

const resolver = (name: string) => context("resources/js/Pages/" + name + ".tsx").then((module: any) => {
  const page = module.default;

  // ... some default layouts staff here

  return page;
});

ReactDOM.render(<InertiaApp initialPage={init} resolveComponent={resolver} initialComponent={LoadingScreen}/>, app);

As you can see I declare files context which scans for all *.tsx files recursively inside my resources/js/Pages directory. Then instead of import() I use context() as a function which will resolve required module. As you might notice I use absolute paths in my project, but the approach with ~ should work as well.

I think it's worth to try it that way.
Hope that helps.

@maxonfjvipon
Copy link
Author

@maxonfjvipon Not 100% sure but it might be related to the webpack's dynamic imports being limited to specific directory only, meaning that you will be able to only bundle files in a single directory you provide in your dynamic expression.

I had the same problem with my project where I've tried to generate separate chunks for every page to avoid loading whole app in single JS file as it is treated as performance issue. I wasn't able to do that with standard dynamic import because I have my pages separated into multiple directories. I ended up using require.context (you can find the docs here: https://webpack.js.org/api/module-methods/#requirecontext).

Here is how I've configured my entrypoint file:

const app  = document.getElementById("app") as HTMLElement;
const init = JSON.parse(app.dataset.page as string);
const context = require.context("resources/js/Pages", true, /\.tsx$/, "lazy");

const resolver = (name: string) => context("resources/js/Pages/" + name + ".tsx").then((module: any) => {
  const page = module.default;

  // ... some default layouts staff here

  return page;
});

ReactDOM.render(<InertiaApp initialPage={init} resolveComponent={resolver} initialComponent={LoadingScreen}/>, app);

As you can see I declare files context which scans for all *.tsx files recursively inside my resources/js/Pages directory. Then instead of import() I use context() as a function which will resolve required module. As you might notice I use absolute paths in my project, but the approach with ~ should work as well.

I think it's worth to try it that way. Hope that helps.

Thanks for your answer.
So I tried this way, but it seems I did something wrong.
There is how I did it:
image

Now I don't have any errors, just I warning.
image
Also I have many loaded files:
image
But when I try to go to page that is located in module, I get this:
image
Also there is no any path to js files in map variable in app.js (if we check it via browser)

I also tried to move context variable out of resolver function, but then there are errors as before.

@maxonfjvipon
Copy link
Author

I also check this issue and it seems there is no way to call require with variables because of static analysis(

That's sad, but, we don't have a choise)

@interviadmin
Copy link

@maxonfjvipon It probably doesn't work because you're trying to use require.context in a "dynamic" context (by calling it inside resolver and also using a variable in directory path), where it should be used "statically" (you should prepare everything to load at the build time). Maybe try defining your context before the resolver like that:

const context = require.context('../../Modules', true, /\.js$/, 'lazy');

If you have other JS files in your modules which you don't want to include there a third argument of that context function is a standard RegExp so you might use something like /Pages\/(.+)\.js$/ to improve the filtering and catch only your pages.

That big list of files you get now when webpack builds, are actually your chunks, so every page will be generated as separate output, and will only be loaded when it's needed.

@rafalkrawiec
Copy link
Contributor

@maxonfjvipon Just in case, make sure to console.log context and check generated map with files paths. In my case I always get absolute paths in map, no matter if I use an absolute or relative path in require.context. This might be because I'm using absolute paths in my whole projects. My generated map looks like this:

var map = {
	"./Article/Index.tsx": [
		"./resources/js/Pages/Web/Article/Index.tsx",
		"resources_js_Pages_Web_Article_Index_tsx"
	],
	"./Article/Show.tsx": [
		"./resources/js/Pages/Web/Article/Show.tsx",
		"resources_js_Pages_Web_Article_Show_tsx"
	],
	"./Blog/Index.tsx": [
		"./resources/js/Pages/Web/Blog/Index.tsx",
		"resources_js_Pages_Web_Blog_Index_tsx"
	],
	"./Blog/Show.tsx": [
		"./resources/js/Pages/Web/Blog/Show.tsx",
		"resources_js_Pages_Web_Blog_Show_tsx"
	],
	"./Contact/Index.tsx": [
		"./resources/js/Pages/Web/Contact/Index.tsx",
		"resources_js_Pages_Web_Contact_Index_tsx"
	],
	"./Error.tsx": [
		"./resources/js/Pages/Web/Error.tsx",
		"resources_js_Pages_Web_Error_tsx"
	],
	"./Homepage/Index.tsx": [
		"./resources/js/Pages/Web/Homepage/Index.tsx",
		"resources_js_Pages_Web_Homepage_Index_tsx"
	],
	"resources/js/Pages/Web/Article/Index.tsx": [
		"./resources/js/Pages/Web/Article/Index.tsx",
		"resources_js_Pages_Web_Article_Index_tsx"
	],
	"resources/js/Pages/Web/Article/Show.tsx": [
		"./resources/js/Pages/Web/Article/Show.tsx",
		"resources_js_Pages_Web_Article_Show_tsx"
	],
	"resources/js/Pages/Web/Blog/Index.tsx": [
		"./resources/js/Pages/Web/Blog/Index.tsx",
		"resources_js_Pages_Web_Blog_Index_tsx"
	],
	"resources/js/Pages/Web/Blog/Show.tsx": [
		"./resources/js/Pages/Web/Blog/Show.tsx",
		"resources_js_Pages_Web_Blog_Show_tsx"
	],
	"resources/js/Pages/Web/Contact/Index.tsx": [
		"./resources/js/Pages/Web/Contact/Index.tsx",
		"resources_js_Pages_Web_Contact_Index_tsx"
	],
	"resources/js/Pages/Web/Error.tsx": [
		"./resources/js/Pages/Web/Error.tsx",
		"resources_js_Pages_Web_Error_tsx"
	],
	"resources/js/Pages/Web/Homepage/Index.tsx": [
		"./resources/js/Pages/Web/Homepage/Index.tsx",
		"resources_js_Pages_Web_Homepage_Index_tsx"
	]
};

What you actually provide later, using context(...).then(...) is a key from the map above. So in my case, when I return Web/Homepage/Index in response as a component name, I use context('resources/js/Pages/' + name + '.tsx').then(...) to load proper chunk.

Hope it helps.

@maxonfjvipon
Copy link
Author

@interviadmin @rafalkrawiec
There is a progress. Builds without errors and warnings
image
Also now I have chunks that were located from modules:
image
But still something wrong:
image
I think it should be it
image
In controller it looks like:
Screen Shot 2021-11-16 at 1 09 58 AM

@rafalkrawiec
Copy link
Contributor

@maxonfjvipon Seems like your if statement is wrong, as it never gets to that context() call. I think there should be parts.length > 1 as split method will return array.

@maxonfjvipon
Copy link
Author

maxonfjvipon commented Nov 15, 2021

@rafalkrawiec @interviadmin
I bow before you. It finally works. Thank you so much.
Now it looks like this:
image

I did not realize before that first argument in require.context() function and path in context are not the same! @rafalkrawiec - you were right about the map.
So we're done here. Thanks again. <3

@maxonfjvipon
Copy link
Author

maxonfjvipon commented Nov 28, 2021

Just in case, if somebody does not want to load a single chunk for every single page file.
You may do:
Module structure:
image
app.js in your module
image
Every page file should be exported as default function:
image
main app.js (in resources/js/)
image

So, now every js chunk will contain all js files for your module

@parth391
Copy link

parth391 commented Feb 8, 2022

this don't seem to work in 0.11.0 version

const context = require.context('../../Modules', true, /Pages\/(.+)\.js$/, 'lazy');

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => {
        let parts = name.split('::');
        if (parts.length > 1) {
            return context(`./Modules/${parts[0]}/Resources/assets/js/Pages/${parts[1]}.js`).then(module => module.default);
        } else {
            return require(`./Pages/${name}`);
        }
    },
    setup({ el, App, props }) {
        return render(<App {...props} />, el);
    },
});

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

No branches or pull requests

5 participants