NativeScript / nativescript-dev-webpack

A package to help with webpacking NativeScript apps.
Apache License 2.0
97 stars 49 forks source link

dynamic import issue with webpack #989

Closed Serge-SDL closed 5 years ago

Serge-SDL commented 5 years ago

Hi,

I need to use dynamic import in my project to perform semthing like this (load a module js file stored in app documents):

        const file = knownFolders.documents().getFile('test.js');
        const jsCode = `
            module.exports = { test: function(str) { return 'x' + str; } }
        `;
        file.writeTextSync(jsCode);

        import(/* webpackIgnore: true */ file.path)
            .then((res) => {
                console.log('imported file res', res, res.test('y'));
            }).catch((err) => {
                console.log('error', err);
            });

this code work fine with useLegacyWorkflow: true, but failed with useLegacyWorkflow: false -> I got the error: "Not supported" on android -> I got the error: "Could not resolve the module specifier" on IOS

The webpack generate code looks ok:

        var file = tns_core_modules_file_system__WEBPACK_IMPORTED_MODULE_4__["knownFolders"].documents().getFile('test.js');
        var jsCode = "\n            module.exports = { test: function(str) { return 'x' + str; } }\n        ";
        file.writeTextSync(jsCode);
        import(/* webpackIgnore: true */ file.path)
            .then(function (res) {
            console.log('imported file res', res, res.test('y'));
        }).catch(function (err) {
            console.log('error', err);
        });

how can I fix this? I dont understand the error (what is not supported ?) thanks!

DimitarTachev commented 5 years ago

Hi @Serge-SDL,

As far as I know, the ES imports are not fully supported in the NativeScript runtimes and in order to get this working with the Bundle (Webpack) workflow, you need a common-js require call runtime.

In order to do that, you could use the __non_webpack_require__. In this way, Webpack will put a common-js require in the generated bundle and the NativeScript runtimes will be able to resolve it.

In other words, I suggest you try the following code:

const file =knownFolders.documents().getFile('test.js');
const jsCode = `
  module.exports = { test: function(str) { return 'x' + str; } }
`;
file.writeTextSync(jsCode);
const dynamicFile = __non_webpack_require__(file.path);
console.log(dynamicFile.test("---- This is working ----"));

I suppose that in the Legacy workflow you've tested this in a TypeScript app where the TypeScript compiler (tsc) is replacing the import with require.

UPDATE

Another possible workaround without using Webpack features is the global.require method - the require method exposed by the NativeScript runtimes.

const file =knownFolders.documents().getFile('test.js');
const jsCode = `
  module.exports = { test: function(str) { return 'x' + str; } }
`;
file.writeTextSync(jsCode);
const dynamicFile = global.require(file.path);
console.log(dynamicFile.test("---- This is working ----"));
Serge-SDL commented 5 years ago

Hi @DimitarTachev, thank you very much for your quick and very usefull answer! This work fine (both solutions)!

if I import a simple module everything woks perfectly, but if there are some require() in the code I import this doesn't work anymode :'(

for example something like:

const file =knownFolders.documents().getFile('test.js');
const jsCode = `
  require('nativescript-angular/router');
  module.exports = { test: function(str) { return 'x' + str; } }
`;
file.writeTextSync(jsCode);
const dynamicFile = __non_webpack_require__(file.path);
console.log(dynamicFile.test("---- This is working ----"));

to fix this I'am looking for a way to pass dependecies to require, as in SystemJS: SystemJS.set('@angular/router', SystemJS.newModule(AngularRouter)); or a way to have access to webpack function __webpack_require__ (or something like that...)

to you have any idea?? thanks!

DimitarTachev commented 5 years ago

@Serge-SDL, the require statement is not working because it is searching for the router files runtime but these files are bundled in vendor.js build time.

When the CLI prepares your app, it starts a Webpack process generating a few bundled files based on the specified entry points and split chunks (e.g. bundle.js and vendor.js). These files are containing only the required modules from your application (it's "tree shaking" the app).

For this reason, even if you find a way to tell your dynamic files to search for nativescript-angular/router in vendor.js, it could be missing there if you don't have any other references to this router module in your app (because of the tree shaking). Also, it will be a huge hack to use something like the internal __webpack_require__ hardcoded in your dynamic files and it will not work properly for production builds where the files are registered with ids instead of paths (e.g. nativescript-angular/router will be something like 141).

The easiest way of sharing node_modules between your app and dynamically imported modules is by using additional Webpack entry points. For example, open the webpack.config.js of your app and add the following code just after const entries = { bundle: entryPath };:

entries["tns_modules/nativescript-angular/router"] = "nativescript-angular/router";

In this way, the Webpack compilation will produce an additional file named <app-destination-in-platforms>/tns_modules/nativescript-angular/router.js which will be resolved both by the Runtimes when evaluating require('nativescript-angular/router'); and the __webpack_require__ calls in your application as the new entry point is both a file and part of the Webpack compilation.

Important note: The tns_modules part of the entry is required because of legacy code in the NativeScript runtimes module resolution. When evaluating the require call, the runtimes are searching in tns_modules instead of node_modules. In other words, bear in mind that as shown in the above-mentioned example, you have to register the entry points starting with tns_modules when you target something from node_modules.

The above-mentioned approach has one important limitation, you have to know and list as entry points all of your dynamically required modules. If this approach is not applicable for you and you need all node_modules available runtime (e.g. when you don't have an idea what kind of dynamic files will be required), you have to mark all dependencies as Webpack externals, copy the node_modules in tns_modules using the CopyWebpackPlugin and post-process the node_modules using the nativescript-dev-babel plugin. This approach is similar to the Legacy CLI prepare mechanism and it is much more complex and disables the whole tree-shaking of the node_modules. The build time of the app will become much slower and the app size will become much bigger. However, if you have such requirements, we could assist you with a configured webpack.config.js file and the proper Babel config as we are already using such approach for building the NativeScript Preview app where we cannot know what kind of files will be required by the Preview users when they execute tns preview in their apps or test something in the NativeScript Playground. More information about this approach can be found in this issue.

rynop commented 5 years ago

Angular 8 now uses dynamic imports for lazy-loaded routes. Ex:

old

{ path: '/cart', loadChildren: './cart/cart.module#CartModule' }

new

{ path: `/cart`, loadChildren: () => import(`./cart/cart.module`).then(m => m.CartModule) }

Does this issue prevent one from using this syntax? I have to admit I have not tried it on NS v6.0 yet. I tried on v5.4 and it didn't work.

edusperoni commented 5 years ago

IIRC, you have to change your tsconfig.tns.json and set "module": "esnext" https://github.com/Microsoft/TypeScript/issues/16675#issuecomment-309999772

DimitarTachev commented 5 years ago

Hi @rynop, @edusperoni.

This latest comments in this issue are about using the ES dynamic imports in plain JavaScript files that are directly executed Runtime.

The dynamic import statements in Angular8 are defined in TypeScript and processed by the AngularCompilerPlugin build time. In other words, most probably the generated JavaScript code will have some kind of a dynamic require or __webpack_require__ statements instead of the ES dynamic import().then.. calls and the NativeScript runtimes will be able to process it.

If you have any issues with the Angular 8 lazy routes with dynamic imports, please log them in the nativescript-angular repository.

Serge-SDL commented 5 years ago

@DimitarTachev a huge thank you for all your informations! that was wery usefull and I can't see how I could find this without you!

some further details: NS 5.4:

NS 6.0: my next move is upgrading to 6.0 version and it doesn't work :(

I got an issue I can't understand when I perform const modules = __non_webpack_require__(file.path);:

ERROR Error: com.tns.NativeScriptException: Failed to find module: "./......

what is this module?? "./.... ? Why there is no double quote at the end ?? do you have any idea or a way to debug? I'm a little bit lost...

Serge-SDL commented 5 years ago

@DimitarTachev

I tried to restart from scratch to avoir app complexity:

@Component({ selector: "Home", moduleId: module.id, templateUrl: "./home.component.html" }) export class HomeComponent implements OnInit {

constructor() {
    // Use the component constructor to inject providers.
}

ngOnInit(): void {

    const file = fs.knownFolders.currentApp().getFile('basic-module-example.js');
    console.log('js content', file.path, file.readTextSync());
    const modules = __non_webpack_require__(file.path);
    console.log('modules', modules);

}

}


-> got the same error as in my real project when I run tns run android:  `ERROR Error: com.tns.NativeScriptException: Failed to find module: "./......`

test source can be downloaded here:
https://bitbucket.org/sdlab/ns6-dynamic-import-example/src/master/

-> look like it's a nativescript 6 bug (same code work fine on ns5.4)
is it the right place? Or should I create a new bug report?

thanks!
DimitarTachev commented 5 years ago

@Serge-SDL,

I've never seen such ./...... module error :)

The attached application is running as expected on my side but its loading an empty basic-module-example.js file.

We will need the basic-module-example.js content in order to investigate this further as it's not generated by the application code from the provided repository.

P.S. I suppose I'm missing something but why do you need the CopyWebpackPlugin to copy all JavaScript files from your app? I believe you don't need this step.

Serge-SDL commented 5 years ago

@DimitarTachev Sorry for the missing basic-module-example.js, it was gitignored!, I just add it ...

with the debug tool (tns debug android), I found that the real error is:

err = Error: com.tns.NativeScriptException: Failed to find module: "./......
untime.js", relative to: app/tns_modules/tns-core-modules/nativescript-angular/ 

and is comming from this code in nativescript-angular/router.js: require("./..\..\..\runtime.js"); It looks like \r is interpreted as a line break

about the CopyWebpackPlugin that copy all js file: in fact I just need to copy basic-module-example.js but copying every js file is just a shortcut

DimitarTachev commented 5 years ago

@Serge-SDL,

After providing the missing js file, everything is still working as expected.

Reviewing your logs once again, I suppose that we could have an issue with the slashes in the path. Maybe you are running this on Windows?

Serge-SDL commented 5 years ago

@DimitarTachev thanks for your answer! yes I'm on windows :( and I thing something in webpack use windows path to generate this code: require("./..\..\..\runtime.js");require("./..\..\..\vendor.js")

do you have any idea to fix this? Maybe a webpack plugin that can perform a replace with regex? Do you know where I can find the code that generate this code? I think it's in the new version of nativescript-dev-webpack?

All the team is on windows... no possibility to change that :(

DimitarTachev commented 5 years ago

Hi @Serge-SDL,

Thanks for the additional details!

I've opened and merges a pull request with the Windows fix.

It's already available in nativescript-dev-webpack@rc and should be officially released in a few days.

Serge-SDL commented 5 years ago

Hi @DimitarTachev,

again a big thank you for your answers! Everything works well! I still have a last issue: I cannot find a way to make my project work with angular aot compilation. I think there is no way to compile a lib as aot (maybe it doesn't mean anything), but when I require the lib I have a compilation error (something like JIT compliler is not loaded).

Did you manage to fix that on your projects? or do you have an idea to manage this problem?

thanks!

DimitarTachev commented 5 years ago

@Serge-SDL,

I'm glad I was able to help you.

As far as I see, the basic-module-example.js is an Angular module which is not compiled with the Angular AOT compiler (it's compiled and prepared for JIT compilation).

I suppose that you will have to compile this file with the Angular AOT compiler in order to dynamically use it in Angular apps with AOT compilation. If you have any problems with that, please open a new issue as it's not related to the dynamic imports with Webpack anymore.

luiguild commented 4 years ago

Any update about this? I'm trying use dynamic imports on vue router...