NativeScript / nativescript-dev-webpack

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

Different behavior with and without webpack #853

Open vakrilov opened 5 years ago

vakrilov commented 5 years ago

Not sure this classifies as a bug or whether it can be solved. Logging it, there might be other similar cases with libraries that depend on the global scope to do environment checks.

Environment Provide version numbers for the following components (information can be retrieved by running tns info in your project folder or by inspecting the package.json of the project):

Describe the bug I have different behavior when running with and without webpack

To Reproduce Create project:

$ tns create test-app --ts
$ cd test-app
$ npm i @datorama/akita@3.5.3 -s

Go to app.ts adn paste the following code before application.run:

(<any> global).window = {};
import { isNotBrowser } from "@datorama/akita";
console.log("isNotBrowser: " + isNotBrowser);

Run with and without bundle and get different results:

$ tns run ios --no-bundle
...
CONSOLE LOG file:///app/app.js:11:12: isNotBrowser: false

$ tns run ios --bundle
...
CONSOLE LOG file:///app/bundle.js:120:12: isNotBrowser: true

Expected behavior isNotBrowser to have the same value in both runs.

Explanation Here is how isNotBrowser is implemented in Akita (here):

export const isNotBrowser = typeof window === 'undefined'

When running WITHOUT bundle the window is attached in the global scope and then the library is evaluated after -> isNotBrowser: false as window is defined at the time of evaluation (as you would expect).

When running WITH bundle, the akita code-base is put inside the vendor.js and executed before, bundle.js (where the fake window is deified). The import { isNotBrowser } from "@datorama/akita"; is replaced with webpack-require which returns the already evaluated value of isNotBrowser. Thus the difference in behavior.

NathanaelA commented 4 years ago

Actually this issue actually can cause a lot more issues and I think we need to figure out some sort of easy solution...

Because vendor is evaluated and ran before the bundle; their is no easy way to polyfill any missing things that a non-nativescript npm module might be missing. For example I just ran into two of them today...

Normally in the past you could easily do a require('nativescript-websocket'); at the app.?s/main.?s files. And WebSocket would be setup for all other files after it...

However, in this fairly simple angular project; other files the the router calling the library that requires websockets causes webpack to prioritize other npm modules as a higher priority, than the nativescript-websocket. So then the order gets screwed up. The only way I was able to work around this issue was to EVERY single place one of the files that needed a polyfill was required, was to require a new file called "polyfills.js" before it.

edusperoni commented 4 years ago

It seems there IS an easy solution. webpack.config.js:

const entries = { bundle: [`${appFullPath}/polyfills.ts`, entryPath] };

app/polyfills.ts:

(<any> global).window = {};

Result:

JS: isNotBrowser: false

Also works:

main.ts:

import "./polyfills";
// rest of the code

The issue is that requires will run before any code in the file, so moving your polyfills to a separate file and then importing them will work.

Actual generated file. Note that all requires will run first, and since ./polyfills.ts is the first, it'll polyfill before everything else:

/***/ "./app.ts":
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var _polyfills__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./polyfills.ts");
/* harmony import */ var _polyfills__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_polyfills__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _datorama_akita__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("../node_modules/@datorama/akita/fesm5/datorama-akita.js");
/* harmony import */ var tns_core_modules_application__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("../node_modules/tns-core-modules/application/application.js");
/* harmony import */ var tns_core_modules_application__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tns_core_modules_application__WEBPACK_IMPORTED_MODULE_2__);

        let applicationCheckPlatform = __webpack_require__("../node_modules/tns-core-modules/application/application.js");
        if (applicationCheckPlatform.android && !global["__snapshot"]) {
            __webpack_require__("../node_modules/tns-core-modules/ui/frame/frame.js");
__webpack_require__("../node_modules/tns-core-modules/ui/frame/activity.js");
        }

            __webpack_require__("../node_modules/nativescript-dev-webpack/load-application-css-regular.js")();

        if (false) {}

            const context = __webpack_require__("./ sync recursive (?<!\\bApp_Resources\\b.*)\\.(xml|css|js|(?<!\\.d\\.)ts|(?<!\\b_[\\w-]*\\.)scss)$");
            global.registerWebpackModules(context);
            if (false) {}

        __webpack_require__("../node_modules/tns-core-modules/bundle-entry-points.js");
        /*
In NativeScript, the app.ts file is the entry point to your application.
You can use this file to perform app-level initialization, but the primary
purpose of the file is to pass control to the app’s first module.
*/

global.window = {};

console.log("isNotBrowser: " + _datorama_akita__WEBPACK_IMPORTED_MODULE_1__["isNotBrowser"]);

tns_core_modules_application__WEBPACK_IMPORTED_MODULE_2__["run"]({ moduleName: "app-root" });

As for nativescript-websockets, I used a plugin replacement from ws to nativescript-websockets and it worked well enough for my remote devtools ngrx implementation, so this solution may also apply: https://github.com/edusperoni/ngrx-ns/blob/master/webpack.config.js#L332

Edit: @NathanaelA sorry, I didn't read your last paragraph properly. When I imported it in main.ts I didn't need to polyfill anywhere else and was able to import and check isNotBrowser in other files.

Edit 2: Also worked with require("nativescript-websockets")