diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5b81f7c..e0ec1877 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -35,5 +35,26 @@ Migration steps: [Provide a migration path for existing applications.] --> + -[CLA]: http://www.nativescript.org/cla \ No newline at end of file +[CLA]: http://www.nativescript.org/cla diff --git a/.gitignore b/.gitignore index 8f8de90d..fca848e3 100644 --- a/.gitignore +++ b/.gitignore @@ -31,10 +31,17 @@ jasmine-config/reporter.js bundle-config-loader.d.ts bundle-config-loader.js +xml-namespace-loader.d.ts +xml-namespace-loader.js + +css2json-loader.d.ts +css2json-loader.js + **/*.spec.js* **/*.spec.d.ts* hooks .DS_Store - +.nyc_output +coverage !projectHelpers.spec.js diff --git a/.npmignore b/.npmignore index 1c2c46d5..58343b36 100644 --- a/.npmignore +++ b/.npmignore @@ -7,6 +7,8 @@ demo *.spec.* .vscode/ .github/ +.nyc_output +coverage/ jasmine-config/ CONTRIBUTING.md CODE_OF_CONDUCT.md diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..3294892a --- /dev/null +++ b/.nycrc @@ -0,0 +1,5 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "exclude": ["/demo/**"], + "reporter": ["text", "lcov"] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba86021..66b0d205 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,129 @@ + +## [1.5.1](https://github.com/NativeScript/nativescript-dev-webpack/compare/v1.5.0...v1.5.1) (2020-02-25) + + +### Bug Fixes + +* `The provided Android NDK is vnull while the recommended one is v21.0.6113669` error in some cases ([23aa6c5](https://github.com/NativeScript/nativescript-dev-webpack/commit/23aa6c5)) +* AOT compilation of multiple workers should work ([af5cb84](https://github.com/NativeScript/nativescript-dev-webpack/commit/af5cb84)) +* replace extension coming from package.json ([a04c0f8](https://github.com/NativeScript/nativescript-dev-webpack/commit/a04c0f8)) + + +### Features + +* Add .kt extension to known entry types ([55b56c8](https://github.com/NativeScript/nativescript-dev-webpack/commit/55b56c8)) +* Add .kt extension to known ones ([6e145a4](https://github.com/NativeScript/nativescript-dev-webpack/commit/6e145a4)) + + + + +# [1.5.0](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.4.1...1.5.0) (2020-02-04) + + +### Bug Fixes + +* ensure the js snapshot entry dir if not created (avoid ENOENT error) ([2a0eaf6](https://github.com/NativeScript/nativescript-dev-webpack/commit/2a0eaf6)) +* stop searching for snapshot artefacts when the snapshot tools are skipped (it's a cloud build, there aren't any snapshot artefacts locally) ([b8da140](https://github.com/NativeScript/nativescript-dev-webpack/commit/b8da140)) + + +### Features + +* **dependencies:** updated `[@angular](https://github.com/angular)/compiler-cli` dependency ([1dbcbf2](https://github.com/NativeScript/nativescript-dev-webpack/commit/1dbcbf2)), closes [#1114](https://github.com/NativeScript/nativescript-dev-webpack/issues/1114) +* allow extending webpack.config.js through env ([69ace1e](https://github.com/NativeScript/nativescript-dev-webpack/commit/69ace1e)) + + + + +## [1.4.1](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.4.0...1.4.1) (2020-01-07) + + +### Bug Fixes + +* add missing tsconfig paths when the app is using only scoped modules and angular ([87ec157](https://github.com/NativeScript/nativescript-dev-webpack/commit/87ec157)) +* handle missing paths obj in tsconfig ([867a9f1](https://github.com/NativeScript/nativescript-dev-webpack/commit/867a9f1)) + + + +# [1.4.0](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.3.1...1.4.0) (2019-12-08) + +### Bug Fixes + +* add import of `.css` file into another `.css` file ([c5e4552](https://github.com/NativeScript/nativescript-dev-webpack/commit/c5e4552)) +* avoid duplicate modules from tns-core-modules and [@nativescript](https://github.com/nativescript)/core causing app crashes on Android ([b74b231](https://github.com/NativeScript/nativescript-dev-webpack/commit/b74b231)) +* bundle emitted on save without changes ([2d01df9](https://github.com/NativeScript/nativescript-dev-webpack/commit/2d01df9)), closes [/github.com/webpack/webpack/blob/4056506488c1e071dfc9a0127daa61bf531170bf/lib/Compiler.js#L326](https://github.com//github.com/webpack/webpack/blob/4056506488c1e071dfc9a0127daa61bf531170bf/lib/Compiler.js/issues/L326) +* fix module import of local css files ([2c0a36e](https://github.com/NativeScript/nativescript-dev-webpack/commit/2c0a36e)), closes [/github.com/webpack-contrib/css-loader/blob/967fb66da2545f04055eb0900a69f86e484dd842/src/utils.js#L220](https://github.com//github.com/webpack-contrib/css-loader/blob/967fb66da2545f04055eb0900a69f86e484dd842/src/utils.js/issues/L220) +* remove the tns-core-modules dependency in order to allow [@nativescrip](https://github.com/nativescrip)/core migration ([7d60958](https://github.com/NativeScript/nativescript-dev-webpack/commit/7d60958)) +* stop ignoring the initial hot updates ([d032e4c](https://github.com/NativeScript/nativescript-dev-webpack/commit/d032e4c)) +* stop on compilation error in typescript applications ([df7d122](https://github.com/NativeScript/nativescript-dev-webpack/commit/df7d122)) +* update worker loader in order to fix HMR ([5ad141e](https://github.com/NativeScript/nativescript-dev-webpack/commit/5ad141e)) + +### Features + +* snapshot in Docker on macOS with Android runtime 6.3.0 or higher as it will not contain snapshot tools for macOS anymore ([9e99683](https://github.com/NativeScript/nativescript-dev-webpack/commit/9e99683)) +* stop using the proxy `tns-core-modules` package when the `[@nativescript](https://github.com/nativescript)/core` is available ([061b270](https://github.com/NativeScript/nativescript-dev-webpack/commit/061b270)) + + + + +# [1.3.0](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.2.1...1.3.0) (2019-10-31) + + +### Bug Fixes + +* exclude files from tests folder from built application ([c61f10e](https://github.com/NativeScript/nativescript-dev-webpack/commit/c61f10e)) +* fix dependencies in package.json ([eefd042](https://github.com/NativeScript/nativescript-dev-webpack/commit/eefd042)), closes [/github.com/NativeScript/nativescript-dev-webpack/blob/master/bundle-config-loader.ts#L4](https://github.com//github.com/NativeScript/nativescript-dev-webpack/blob/master/bundle-config-loader.ts/issues/L4) [/github.com/NativeScript/nativescript-dev-webpack/blob/2978b81b5a8100774b2bb4a331ac8637205927b8/package.json#L54](https://github.com//github.com/NativeScript/nativescript-dev-webpack/blob/2978b81b5a8100774b2bb4a331ac8637205927b8/package.json/issues/L54) +* fix xxd path for local snapshots generation ([f63d493](https://github.com/NativeScript/nativescript-dev-webpack/commit/f63d493)) +* handle correctly webpack compilation errors ([363c4da](https://github.com/NativeScript/nativescript-dev-webpack/commit/363c4da)) +* handle imports like [@import](https://github.com/import) url("./xxx.css") ([8921120](https://github.com/NativeScript/nativescript-dev-webpack/commit/8921120)) +* search for the proper NDK executable on Windows ([f93bb6c](https://github.com/NativeScript/nativescript-dev-webpack/commit/f93bb6c)) +* Unbound namespace error with ios and android ([#1053](https://github.com/NativeScript/nativescript-dev-webpack/issues/1053)) ([6cd6efe](https://github.com/NativeScript/nativescript-dev-webpack/commit/6cd6efe)) + + +### Features + +* add useForImports option ([632af4f](https://github.com/NativeScript/nativescript-dev-webpack/commit/632af4f)) +* ensure valid CLI version when Windows snapshot is requested ([3a687c0](https://github.com/NativeScript/nativescript-dev-webpack/commit/3a687c0)) +* support [@nativescript](https://github.com/nativescript) scope in host resolver ([efda509](https://github.com/NativeScript/nativescript-dev-webpack/commit/efda509)) +* support useLibs though env.compileSnapshot and calculate the NDK path internally ([5431bd7](https://github.com/NativeScript/nativescript-dev-webpack/commit/5431bd7)) +* support V8 snapshots on Windows ([2e9b753](https://github.com/NativeScript/nativescript-dev-webpack/commit/2e9b753)) +* use css2json loader by default ([6b0c9ae](https://github.com/NativeScript/nativescript-dev-webpack/commit/6b0c9ae)) + + + + +## [1.2.1](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.2.0...1.2.1) (2019-10-03) + + +### Features + +* snapshot in docker container when the local tools are not available ([6861d22](https://github.com/NativeScript/nativescript-dev-webpack/commit/6861d22)) + + + + +# [1.2.0](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.1.1...1.2.0) (2019-09-03) + + +### Bug Fixes + +* register non-relative app.css module ([710acd7](https://github.com/NativeScript/nativescript-dev-webpack/commit/710acd7)) + + +### Features + +* support dynamic ES6 import ([4a07932](https://github.com/NativeScript/nativescript-dev-webpack/commit/4a07932)) + + + +## [1.1.1](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.1.0...1.1.1) (2019-08-22) + + +### Bug Fixes + +* add ia64 as supported architecture ([65d5d3f](https://github.com/NativeScript/nativescript-dev-webpack/commit/65d5d3f)) + + + # [1.1.0](https://github.com/NativeScript/nativescript-dev-webpack/compare/1.0.2...1.1.0) (2019-08-19) diff --git a/androidProjectHelpers.js b/androidProjectHelpers.js index e6a772a0..58ce9fa4 100644 --- a/androidProjectHelpers.js +++ b/androidProjectHelpers.js @@ -47,6 +47,16 @@ const getMksnapshotParams = (projectDir) => { } }; +const getRuntimeNdkRevision = (projectDir) => { + try { + const androidSettingsJSON = getAndroidSettingsJson(projectDir); + const result = androidSettingsJSON && androidSettingsJSON.ndkRevision; + return result; + } catch (e) { + return null; + } +}; + const getAndroidSettingsJson = projectDir => { const androidSettingsJsonPath = resolve(projectDir, PLATFORMS_ANDROID, "settings.json"); if (existsSync(androidSettingsJsonPath)) { @@ -62,5 +72,6 @@ module.exports = { ANDROID_CONFIGURATIONS_PATH, getAndroidRuntimeVersion, getAndroidV8Version, - getMksnapshotParams -}; \ No newline at end of file + getMksnapshotParams, + getRuntimeNdkRevision +}; diff --git a/apply-css-loader.js b/apply-css-loader.js index a543e525..41e0f674 100644 --- a/apply-css-loader.js +++ b/apply-css-loader.js @@ -1,18 +1,43 @@ +const cssLoaderWarning = "The apply-css-loader expects the file to be pre-processed by css-loader. It might not work properly. Please check your webpack configuration"; + +function checkForCssLoader(loaders, loaderIndex) { + if (!loaders) { + return false; + } + + for (var i = loaderIndex + 1; i < loaders.length; i++) { + const loader = loaders[i]; + if (loader.path && loader.path.indexOf("css-loader") > 0) { + return true; + } + } + + return false; +} + module.exports = function (content, map) { if (this.request.match(/\/app\.(css|scss|less|sass)$/)) { return content; } + + // Emit a warning if the file was not processed by the css-loader. + if (!checkForCssLoader(this.loaders, this.loaderIndex)) { + this.emitWarning(new Error(cssLoaderWarning)); + } + content += ` const application = require("tns-core-modules/application"); require("tns-core-modules/ui/styling/style-scope"); - exports.forEach(cssExport => { - if (cssExport.length > 1 && cssExport[1]) { - // applying the second item of the export as it contains the css contents - application.addCss(cssExport[1]); - } - }); - `; + if (typeof exports.forEach === "function") { + exports.forEach(cssExport => { + if (cssExport.length > 1 && cssExport[1]) { + // applying the second item of the export as it contains the css contents + application.addCss(cssExport[1]); + } + }); + } +`; this.callback(null, content, map); -} \ No newline at end of file +} diff --git a/bundle-config-loader.spec.ts b/bundle-config-loader.spec.ts index ec8ad99b..1d745cb7 100644 --- a/bundle-config-loader.spec.ts +++ b/bundle-config-loader.spec.ts @@ -39,6 +39,9 @@ const defaultTestFiles = { "./_app-variables.scss": false, // do not include scss partial files "./App_Resources/Android/src/main/res/values/colors.xml": false, // do not include App_Resources "./App_Resources/Android/src/main/AndroidManifest.xml": false, // do not include App_Resources + "./tests/example.js": false, // do not include files from tests folder + "./tests/component1/model1/file1.js": false, // do not include files from tests folder + "./components/tests/example.js": true, }; const loaderOptionsWithIgnore = { diff --git a/bundle-config-loader.ts b/bundle-config-loader.ts index 6732058c..34b4de27 100644 --- a/bundle-config-loader.ts +++ b/bundle-config-loader.ts @@ -3,13 +3,14 @@ import { loader } from "webpack"; import { getOptions } from "loader-utils"; import * as escapeRegExp from "escape-string-regexp"; -// Matches all source, markup and style files that are not in App_Resources -const defaultMatch = "(? { - global.__hmrSyncBackup({ type, path }); + global.__coreModulesLiveSync({ type, path }); }); }; - hmrUpdate().then(() => { - global.__initialHmrUpdate = false; - }) + // handle hot updated on initial app start + hmrUpdate(); } `; + let sourceModule = "tns-core-modules"; + + if (platform && platform !== "ios" && platform !== "android") { + sourceModule = `nativescript-platform-${platform}`; + } + source = ` - require("tns-core-modules/bundle-entry-points"); + require("${sourceModule}/bundle-entry-points"); ${source} `; @@ -96,4 +99,4 @@ const loader: loader.Loader = function (source, map) { }; -export default loader; \ No newline at end of file +export default loader; diff --git a/css2json-loader.js b/css2json-loader.js deleted file mode 100644 index 022d645b..00000000 --- a/css2json-loader.js +++ /dev/null @@ -1,29 +0,0 @@ -const parse = require("tns-core-modules/css").parse; -const nl = "\n"; - -module.exports = function (content, map) { - const ast = parse(content); - const dependencies = getImportsFrom(ast) - .map(mapURI) - .reduce((dependencies, { uri, requireURI }) => - dependencies + `global.registerModule(${uri}, () => require(${requireURI}));${nl}`, ""); - - const str = JSON.stringify(ast, (k, v) => k === "position" ? undefined : v); - this.callback(null, `${dependencies}module.exports = ${str};`, map); -} - -function getImportsFrom(ast) { - if (!ast || ast.type !== "stylesheet" || !ast.stylesheet) { - return []; - } - return ast.stylesheet.rules - .filter(rule => rule.type === "import") - .map(importRule => importRule.import.replace(/[\'\"]/gm, "")); -} - -function mapURI(uri) { - return { - uri: JSON.stringify(uri), - requireURI: JSON.stringify(uri[0] === "~" && uri[1] !== "/" ? uri.substr(1) : uri) - }; -} diff --git a/css2json-loader.spec.ts b/css2json-loader.spec.ts new file mode 100644 index 00000000..b5229cd8 --- /dev/null +++ b/css2json-loader.spec.ts @@ -0,0 +1,79 @@ +import css2jsonLoader from "./css2json-loader"; + +const importTestCases = [ + `@import url("custom.css");`, + `@import url('custom.css');`, + `@import url('custom.css') print;`, + `@import url("custom.css") print;`, + `@import url('custom.css') screen and (orientation:landscape);`, + `@import url("custom.css") screen and (orientation:landscape);`, + `@import 'custom.css';`, + `@import "custom.css";`, + `@import 'custom.css' screen;`, + `@import "custom.css" screen;`, + `@import url(custom.css);`, +] + +const someCSS = ` +.btn { + background-color: #7f9 +} +` + +describe("css2jsonLoader", () => { + it("converts CSS to JSON", (done) => { + const loaderContext = { + callback: (error, source: string, map) => { + expect(source).toContain(`{"type":"declaration","property":"background-color","value":"#7f9"}`); + + done(); + } + } + + css2jsonLoader.call(loaderContext, someCSS); + }) + + importTestCases.forEach((importTestCase) => { + it(`handles: ${importTestCase}`, (done) => { + + const loaderContext = { + callback: (error, source: string, map) => { + expect(source).toContain(`global.registerModule("./custom.css", () => require("./custom.css"))`); + expect(source).toContain(`{"type":"declaration","property":"background-color","value":"#7f9"}`); + + done(); + }, + } + + css2jsonLoader.call(loaderContext, importTestCase + someCSS); + }) + }); + + it("inlines css2json loader in imports if option is provided", (done) => { + const loaderContext = { + callback: (error, source: string, map) => { + expect(source).toContain(`global.registerModule("./custom.css", () => require("!nativescript-dev-webpack/css2json-loader?useForImports!./custom.css"))`); + expect(source).toContain(`{"type":"declaration","property":"background-color","value":"#7f9"}`); + + done(); + }, + query: { useForImports: true } + } + + css2jsonLoader.call(loaderContext, `@import url("custom.css");` + someCSS); + }) + + it("registers modules for paths starting with ~", (done) => { + const loaderContext = { + callback: (error, source: string, map) => { + expect(source).toContain(`global.registerModule("~custom.css", () => require("custom.css"))`); + expect(source).toContain(`global.registerModule("custom.css", () => require("custom.css"))`); + expect(source).toContain(`{"type":"declaration","property":"background-color","value":"#7f9"}`); + + done(); + } + } + + css2jsonLoader.call(loaderContext, `@import url("~custom.css");` + someCSS); + }) +}); diff --git a/css2json-loader.ts b/css2json-loader.ts new file mode 100644 index 00000000..19f75bfa --- /dev/null +++ b/css2json-loader.ts @@ -0,0 +1,62 @@ +import { parse, Import, Stylesheet } from "css"; +import { loader } from "webpack"; +import { getOptions, urlToRequest } from "loader-utils"; + +const betweenQuotesPattern = /('|")(.*?)\1/; +const unpackUrlPattern = /url\(([^\)]+)\)/; +const inlineLoader = "!nativescript-dev-webpack/css2json-loader?useForImports!" + +const loader: loader.Loader = function (content: string, map) { + const options = getOptions(this) || {}; + const inline = !!options.useForImports; + const requirePrefix = inline ? inlineLoader : ""; + + const ast = parse(content, undefined); + + let dependencies = []; + getImportRules(ast) + .map(extractUrlFromRule) + .map(createRequireUri) + .forEach(({ uri, requireURI }) => { + dependencies.push(`global.registerModule("${uri}", () => require("${requirePrefix}${requireURI}"));`); + + // Call registerModule with requireURI to handle cases like @import "~@nativescript/theme/css/blue.css"; + if (uri !== requireURI) { + dependencies.push(`global.registerModule("${requireURI}", () => require("${requirePrefix}${requireURI}"));`); + } + }); + const str = JSON.stringify(ast, (k, v) => k === "position" ? undefined : v); + this.callback(null, `${dependencies.join("\n")}module.exports = ${str};`, map); +} + +function getImportRules(ast: Stylesheet): Import[] { + if (!ast || (ast).type !== "stylesheet" || !ast.stylesheet) { + return []; + } + return ast.stylesheet.rules + .filter(rule => rule.type === "import" && (rule).import) +} + +/** + * Extracts the url from import rule (ex. `url("./platform.css")`) + */ +function extractUrlFromRule(importRule: Import): string { + const urlValue = importRule.import; + + const unpackedUrlMatch = urlValue.match(unpackUrlPattern); + const unpackedValue = unpackedUrlMatch ? unpackedUrlMatch[1] : urlValue + + const quotesMatch = unpackedValue.match(betweenQuotesPattern); + return quotesMatch ? quotesMatch[2] : unpackedValue; +}; + +function createRequireUri(uri): { uri: string, requireURI: string } { + return { + uri: uri, + requireURI: urlToRequest(uri) + }; +} + + + +export default loader; \ No newline at end of file diff --git a/demo/.gitignore b/demo/.gitignore index ee19ca4e..f8679d9c 100644 --- a/demo/.gitignore +++ b/demo/.gitignore @@ -17,4 +17,5 @@ vendor.js vendor.ts tsconfig.esm.json -mochawesome-report \ No newline at end of file +mochawesome-report +webpack.config.js \ No newline at end of file diff --git a/demo/AngularApp/app/App_Resources/Android/app.gradle b/demo/AngularApp/app/App_Resources/Android/app.gradle index e45b55b8..84cd3ad5 100644 --- a/demo/AngularApp/app/App_Resources/Android/app.gradle +++ b/demo/AngularApp/app/App_Resources/Android/app.gradle @@ -5,12 +5,11 @@ // compile 'com.android.support:recyclerview-v7:+' //} -android { - defaultConfig { +android { + defaultConfig { generatedDensities = [] - applicationId = "org.nativescript.AngularApp" - } - aaptOptions { - additionalParameters "--no-version-vectors" - } -} + } + aaptOptions { + additionalParameters "--no-version-vectors" + } +} diff --git a/demo/AngularApp/app/package.json b/demo/AngularApp/app/package.json index 80037c0e..d0929617 100644 --- a/demo/AngularApp/app/package.json +++ b/demo/AngularApp/app/package.json @@ -1,8 +1,9 @@ { "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" }, "main": "main.js", "name": "tns-template-hello-world-ng", "version": "3.3.0" -} \ No newline at end of file +} diff --git a/demo/AngularApp/custom-application-activity.webpack.config.js b/demo/AngularApp/custom-application-activity.webpack.config.js new file mode 100644 index 00000000..a02e2fef --- /dev/null +++ b/demo/AngularApp/custom-application-activity.webpack.config.js @@ -0,0 +1,13 @@ +const webpackConfig = require("./webpack.config"); +const path = require("path"); + +module.exports = env => { + env = env || {}; + env.appComponents = env.appComponents || []; + env.appComponents.push(path.resolve(__dirname, "app/activity.android.ts")); + + env.entries = env.entries || {}; + env.entries.application = "./application.android"; + const config = webpackConfig(env); + return config; +}; \ No newline at end of file diff --git a/demo/AngularApp/nsconfig.json b/demo/AngularApp/nsconfig.json new file mode 100644 index 00000000..2f45709d --- /dev/null +++ b/demo/AngularApp/nsconfig.json @@ -0,0 +1,3 @@ +{ + "webpackConfigPath": "custom-application-activity.webpack.config.js" +} \ No newline at end of file diff --git a/demo/AngularApp/package.json b/demo/AngularApp/package.json index 81f77544..013838ca 100644 --- a/demo/AngularApp/package.json +++ b/demo/AngularApp/package.json @@ -17,7 +17,6 @@ "@angular/compiler": "8.2.0", "@angular/core": "8.2.0", "@angular/forms": "8.2.0", - "@angular/http": "8.0.0-beta.10", "@angular/platform-browser": "8.2.0", "@angular/platform-browser-dynamic": "8.2.0", "@angular/router": "8.2.0", @@ -40,6 +39,7 @@ "chai-as-promised": "~7.1.1", "lazy": "1.0.11", "mocha": "~5.2.0", + "chai": "4.2.0", "mochawesome": "~3.1.2", "nativescript-dev-appium": "next", "nativescript-dev-webpack": "next", diff --git a/demo/AngularApp/tsconfig.tns.json b/demo/AngularApp/tsconfig.tns.json index 95f2ecee..9ce50ed9 100644 --- a/demo/AngularApp/tsconfig.tns.json +++ b/demo/AngularApp/tsconfig.tns.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig", "compilerOptions": { - "module": "es2015", + "module": "esNext", "moduleResolution": "node" } } diff --git a/demo/AngularApp/webpack.config.js b/demo/AngularApp/webpack.config.js deleted file mode 100644 index 04179234..00000000 --- a/demo/AngularApp/webpack.config.js +++ /dev/null @@ -1,320 +0,0 @@ -const { join, relative, resolve, sep, dirname } = require("path"); - -const webpack = require("webpack"); -const nsWebpack = require("nativescript-dev-webpack"); -const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target"); -const { nsReplaceBootstrap } = require("nativescript-dev-webpack/transformers/ns-replace-bootstrap"); -const { nsReplaceLazyLoader } = require("nativescript-dev-webpack/transformers/ns-replace-lazy-loader"); -const { nsSupportHmrNg } = require("nativescript-dev-webpack/transformers/ns-support-hmr-ng"); -const { getMainModulePath } = require("nativescript-dev-webpack/utils/ast-utils"); -const CleanWebpackPlugin = require("clean-webpack-plugin"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); -const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const { getAngularCompilerPlugin } = require("nativescript-dev-webpack/plugins/NativeScriptAngularCompilerPlugin"); -const hashSalt = Date.now().toString(); - -module.exports = env => { - // Add your custom Activities, Services and other Android app components here. - const appComponents = [ - "tns-core-modules/ui/frame", - "tns-core-modules/ui/frame/activity", - resolve(__dirname, "app/activity.android.ts") - ]; - - const platform = env && (env.android && "android" || env.ios && "ios"); - if (!platform) { - throw new Error("You need to provide a target platform!"); - } - - const AngularCompilerPlugin = getAngularCompilerPlugin(platform); - const projectRoot = __dirname; - - // Default destination inside platforms//... - const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); - - const { - // The 'appPath' and 'appResourcesPath' values are fetched from - // the nsconfig.json configuration file. - appPath = "src", - appResourcesPath = "App_Resources", - - // You can provide the following flags when running 'tns run android|ios' - aot, // --env.aot - snapshot, // --env.snapshot, - production, // --env.production - uglify, // --env.uglify - report, // --env.report - sourceMap, // --env.sourceMap - hiddenSourceMap, // --env.hiddenSourceMap - hmr, // --env.hmr, - unitTesting, // --env.unitTesting - verbose, // --env.verbose - } = env; - - const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; - const externals = nsWebpack.getConvertedExternals(env.externals); - const appFullPath = resolve(projectRoot, appPath); - const appResourcesFullPath = resolve(projectRoot, appResourcesPath); - const tsConfigName = "tsconfig.tns.json"; - const entryModule = `${nsWebpack.getEntryModule(appFullPath, platform)}.ts`; - const entryPath = `.${sep}${entryModule}`; - const entries = { bundle: entryPath, application: "./application.android" }; - const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("tns-core-modules") > -1); - if (platform === "ios" && !areCoreModulesExternal) { - entries["tns_modules/tns-core-modules/inspector_modules"] = "inspector_modules"; - }; - - const ngCompilerTransformers = []; - const additionalLazyModuleResources = []; - if (aot) { - ngCompilerTransformers.push(nsReplaceBootstrap); - } - - if (hmr) { - ngCompilerTransformers.push(nsSupportHmrNg); - } - - // when "@angular/core" is external, it's not included in the bundles. In this way, it will be used - // directly from node_modules and the Angular modules loader won't be able to resolve the lazy routes - // fixes https://github.com/NativeScript/nativescript-cli/issues/4024 - if (env.externals && env.externals.indexOf("@angular/core") > -1) { - const appModuleRelativePath = getMainModulePath(resolve(appFullPath, entryModule), tsConfigName); - if (appModuleRelativePath) { - const appModuleFolderPath = dirname(resolve(appFullPath, appModuleRelativePath)); - // include the lazy loader inside app module - ngCompilerTransformers.push(nsReplaceLazyLoader); - // include the new lazy loader path in the allowed ones - additionalLazyModuleResources.push(appModuleFolderPath); - } - } - - const ngCompilerPlugin = new AngularCompilerPlugin({ - hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]), - platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin, resolve(appFullPath, entryModule), projectRoot)), - mainPath: join(appFullPath, entryModule), - tsConfigPath: join(__dirname, tsConfigName), - skipCodeGeneration: !aot, - sourceMap: !!isAnySourceMapEnabled, - additionalLazyModuleResources: additionalLazyModuleResources - }); - - let sourceMapFilename = nsWebpack.getSourceMapFilename(hiddenSourceMap, __dirname, dist); - - const itemsToClean = [`${dist}/**/*`]; - if (platform === "android") { - itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "src", "main", "assets", "snapshots")}`); - itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "build", "configurations", "nativescript-android-snapshot")}`); - } - - nsWebpack.processAppComponents(appComponents, platform); - const config = { - mode: production ? "production" : "development", - context: appFullPath, - externals, - watchOptions: { - ignored: [ - appResourcesFullPath, - // Don't watch hidden files - "**/.*", - ] - }, - target: nativescriptTarget, - entry: entries, - output: { - pathinfo: false, - path: dist, - sourceMapFilename, - libraryTarget: "commonjs2", - filename: "[name].js", - globalObject: "global", - hashSalt - }, - resolve: { - extensions: [".ts", ".js", ".scss", ".css"], - // Resolve {N} system modules from tns-core-modules - modules: [ - resolve(__dirname, "node_modules/tns-core-modules"), - resolve(__dirname, "node_modules"), - "node_modules/tns-core-modules", - "node_modules", - ], - alias: { - '~': appFullPath - }, - symlinks: true - }, - resolveLoader: { - symlinks: false - }, - node: { - // Disable node shims that conflict with NativeScript - "http": false, - "timers": false, - "setImmediate": false, - "fs": "empty", - "__dirname": false, - }, - devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), - optimization: { - runtimeChunk: "single", - splitChunks: { - cacheGroups: { - vendor: { - name: "vendor", - chunks: "all", - test: (module, chunks) => { - const moduleName = module.nameForCondition ? module.nameForCondition() : ''; - return /[\\/]node_modules[\\/]/.test(moduleName) || - appComponents.some(comp => comp === moduleName); - }, - enforce: true, - }, - } - }, - minimize: !!uglify, - minimizer: [ - new TerserPlugin({ - parallel: true, - cache: true, - sourceMap: isAnySourceMapEnabled, - terserOptions: { - output: { - comments: false, - semicolons: !isAnySourceMapEnabled - }, - compress: { - // The Android SBG has problems parsing the output - // when these options are enabled - 'collapse_vars': platform !== "android", - sequences: platform !== "android", - } - } - }) - ], - }, - module: { - rules: [ - { - include: join(appFullPath, entryPath), - use: [ - // Require all Android app components - platform === "android" && { - loader: "nativescript-dev-webpack/android-app-components-loader", - options: { modules: appComponents } - }, - - { - loader: "nativescript-dev-webpack/bundle-config-loader", - options: { - angular: true, - loadCss: !snapshot, // load the application css if in debug mode - unitTesting, - appFullPath, - projectRoot, - ignoredFiles: nsWebpack.getUserDefinedEntries(entries, platform) - } - }, - ].filter(loader => !!loader) - }, - - { test: /\.html$|\.xml$/, use: "raw-loader" }, - - // tns-core-modules reads the app.css and its imports using css-loader - { - test: /[\/|\\]app\.css$/, - use: [ - "nativescript-dev-webpack/style-hot-loader", - { loader: "css-loader", options: { url: false } } - ] - }, - { - test: /[\/|\\]app\.scss$/, - use: [ - "nativescript-dev-webpack/style-hot-loader", - { loader: "css-loader", options: { url: false } }, - "sass-loader" - ] - }, - - // Angular components reference css files and their imports using raw-loader - { test: /\.css$/, exclude: /[\/|\\]app\.css$/, use: "raw-loader" }, - { test: /\.scss$/, exclude: /[\/|\\]app\.scss$/, use: ["raw-loader", "resolve-url-loader", "sass-loader"] }, - - { - test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/, - use: [ - "nativescript-dev-webpack/moduleid-compat-loader", - "nativescript-dev-webpack/lazy-ngmodule-hot-loader", - "@ngtools/webpack", - ] - }, - - // Mark files inside `@angular/core` as using SystemJS style dynamic imports. - // Removing this will cause deprecation warnings to appear. - { - test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, - parser: { system: true }, - }, - ], - }, - plugins: [ - // Define useful constants like TNS_WEBPACK - new webpack.DefinePlugin({ - "global.TNS_WEBPACK": "true", - "process": undefined, - }), - // Remove all files from the out dir. - new CleanWebpackPlugin(itemsToClean, { verbose: !!verbose }), - // Copy assets to out dir. Add your own globs as needed. - new CopyWebpackPlugin([ - { from: { glob: "fonts/**" } }, - { from: { glob: "**/*.jpg" } }, - { from: { glob: "**/*.png" } }, - ], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }), - new nsWebpack.GenerateNativeScriptEntryPointsPlugin("bundle"), - // For instructions on how to set up workers with webpack - // check out https://github.com/nativescript/worker-loader - new NativeScriptWorkerPlugin(), - ngCompilerPlugin, - // Does IPC communication with the {N} CLI to notify events when running in watch mode. - new nsWebpack.WatchStateLoggerPlugin(), - ], - }; - - if (report) { - // Generate report files for bundles content - config.plugins.push(new BundleAnalyzerPlugin({ - analyzerMode: "static", - openAnalyzer: false, - generateStatsFile: true, - reportFilename: resolve(projectRoot, "report", `report.html`), - statsFilename: resolve(projectRoot, "report", `stats.json`), - })); - } - - if (snapshot) { - config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ - chunk: "vendor", - angular: true, - requireModules: [ - "reflect-metadata", - "@angular/platform-browser", - "@angular/core", - "@angular/common", - "@angular/router", - "nativescript-angular/platform-static", - "nativescript-angular/router", - ], - projectRoot, - webpackConfig: config, - })); - } - - if (hmr) { - config.plugins.push(new webpack.HotModuleReplacementPlugin()); - } - - return config; -}; \ No newline at end of file diff --git a/demo/JavaScriptApp/app/app.android.css b/demo/JavaScriptApp/app/app.android.css index fd5d1243..859d3c5e 100644 --- a/demo/JavaScriptApp/app/app.android.css +++ b/demo/JavaScriptApp/app/app.android.css @@ -10,6 +10,7 @@ of writing your own CSS rules. For a full list of class names in the theme refer to http://docs.nativescript.org/ui/theme. */ @import '~nativescript-theme-core/css/core.light.css'; +@import 'app.common.css'; ActionBar { background-color: #7F9; diff --git a/demo/JavaScriptApp/app/app.common.css b/demo/JavaScriptApp/app/app.common.css new file mode 100644 index 00000000..e69de29b diff --git a/demo/JavaScriptApp/app/app.ios.css b/demo/JavaScriptApp/app/app.ios.css index ea07d338..964ae579 100644 --- a/demo/JavaScriptApp/app/app.ios.css +++ b/demo/JavaScriptApp/app/app.ios.css @@ -10,6 +10,7 @@ of writing your own CSS rules. For a full list of class names in the theme refer to http://docs.nativescript.org/ui/theme. */ @import '~nativescript-theme-core/css/core.light.css'; +@import 'app.common.css'; ActionBar { background-color: #999; diff --git a/demo/JavaScriptApp/app/package.json b/demo/JavaScriptApp/app/package.json index fa35743e..753ef1a0 100644 --- a/demo/JavaScriptApp/app/package.json +++ b/demo/JavaScriptApp/app/package.json @@ -1,8 +1,9 @@ { "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" }, "main": "app.js", "name": "tns-template-hello-world", "version": "3.3.0" -} \ No newline at end of file +} diff --git a/demo/JavaScriptApp/custom-application-activity.webpack.config.js b/demo/JavaScriptApp/custom-application-activity.webpack.config.js new file mode 100644 index 00000000..8c105595 --- /dev/null +++ b/demo/JavaScriptApp/custom-application-activity.webpack.config.js @@ -0,0 +1,13 @@ +const webpackConfig = require("./webpack.config"); +const path = require("path"); + +module.exports = env => { + env = env || {}; + env.appComponents = env.appComponents || []; + env.appComponents.push(path.resolve(__dirname, "app/activity.android.js")); + + env.entries = env.entries || {}; + env.entries.application = "./application.android"; + const config = webpackConfig(env); + return config; +}; \ No newline at end of file diff --git a/demo/JavaScriptApp/nsconfig.json b/demo/JavaScriptApp/nsconfig.json new file mode 100644 index 00000000..8d06e691 --- /dev/null +++ b/demo/JavaScriptApp/nsconfig.json @@ -0,0 +1,3 @@ +{ + "webpackConfigPath": "./custom-application-activity.webpack.config.js" +} \ No newline at end of file diff --git a/demo/JavaScriptApp/package.json b/demo/JavaScriptApp/package.json index 58364992..1a04d9af 100644 --- a/demo/JavaScriptApp/package.json +++ b/demo/JavaScriptApp/package.json @@ -24,6 +24,7 @@ "babel-types": "6.26.0", "babylon": "6.18.0", "lazy": "1.0.11", + "chai": "4.2.0", "mocha": "~5.2.0", "mochawesome": "~3.1.2", "nativescript-dev-appium": "next", diff --git a/demo/JavaScriptApp/webpack.config.js b/demo/JavaScriptApp/webpack.config.js deleted file mode 100644 index c4684e61..00000000 --- a/demo/JavaScriptApp/webpack.config.js +++ /dev/null @@ -1,259 +0,0 @@ -const { join, relative, resolve, sep } = require("path"); - -const webpack = require("webpack"); -const nsWebpack = require("nativescript-dev-webpack"); -const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target"); -const CleanWebpackPlugin = require("clean-webpack-plugin"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); -const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const hashSalt = Date.now().toString(); - -module.exports = env => { - // Add your custom Activities, Services and other android app components here. - const appComponents = [ - "tns-core-modules/ui/frame", - "tns-core-modules/ui/frame/activity", - resolve(__dirname, "app/activity.android.js") - ]; - - const platform = env && (env.android && "android" || env.ios && "ios"); - if (!platform) { - throw new Error("You need to provide a target platform!"); - } - - const platforms = ["ios", "android"]; - const projectRoot = __dirname; - - // Default destination inside platforms//... - const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); - - const { - // The 'appPath' and 'appResourcesPath' values are fetched from - // the nsconfig.json configuration file. - appPath = "app", - appResourcesPath = "app/App_Resources", - - // You can provide the following flags when running 'tns run android|ios' - snapshot, // --env.snapshot - production, // --env.production - uglify, // --env.uglify - report, // --env.report - sourceMap, // --env.sourceMap - hiddenSourceMap, // --env.hiddenSourceMap - hmr, // --env.hmr, - unitTesting, // --env.unitTesting, - verbose, // --env.verbose - } = env; - - const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; - const externals = nsWebpack.getConvertedExternals(env.externals); - const appFullPath = resolve(projectRoot, appPath); - const appResourcesFullPath = resolve(projectRoot, appResourcesPath); - - const entryModule = nsWebpack.getEntryModule(appFullPath, platform); - const entryPath = `.${sep}${entryModule}.js`; - const entries = { bundle: entryPath, application: "./application.android" }; - const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("tns-core-modules") > -1); - if (platform === "ios" && !areCoreModulesExternal) { - entries["tns_modules/tns-core-modules/inspector_modules"] = "inspector_modules"; - }; - - let sourceMapFilename = nsWebpack.getSourceMapFilename(hiddenSourceMap, __dirname, dist); - - const itemsToClean = [`${dist}/**/*`]; - if (platform === "android") { - itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "src", "main", "assets", "snapshots")}`); - itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "build", "configurations", "nativescript-android-snapshot")}`); - } - - nsWebpack.processAppComponents(appComponents, platform); - const config = { - mode: production ? "production" : "development", - context: appFullPath, - externals, - watchOptions: { - ignored: [ - appResourcesFullPath, - // Don't watch hidden files - "**/.*", - ] - }, - target: nativescriptTarget, - entry: entries, - output: { - pathinfo: false, - path: dist, - sourceMapFilename, - libraryTarget: "commonjs2", - filename: "[name].js", - globalObject: "global", - hashSalt - }, - resolve: { - extensions: [".js", ".scss", ".css"], - // Resolve {N} system modules from tns-core-modules - modules: [ - "node_modules/tns-core-modules", - "node_modules", - ], - alias: { - '~': appFullPath - }, - // don't resolve symlinks to symlinked modules - symlinks: true - }, - resolveLoader: { - // don't resolve symlinks to symlinked loaders - symlinks: false - }, - node: { - // Disable node shims that conflict with NativeScript - "http": false, - "timers": false, - "setImmediate": false, - "fs": "empty", - "__dirname": false, - }, - devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), - optimization: { - runtimeChunk: "single", - splitChunks: { - cacheGroups: { - vendor: { - name: "vendor", - chunks: "all", - test: (module, chunks) => { - const moduleName = module.nameForCondition ? module.nameForCondition() : ''; - return /[\\/]node_modules[\\/]/.test(moduleName) || - appComponents.some(comp => comp === moduleName); - - }, - enforce: true, - }, - } - }, - minimize: !!uglify, - minimizer: [ - new TerserPlugin({ - parallel: true, - cache: true, - sourceMap: isAnySourceMapEnabled, - terserOptions: { - output: { - comments: false, - semicolons: !isAnySourceMapEnabled - }, - compress: { - // The Android SBG has problems parsing the output - // when these options are enabled - 'collapse_vars': platform !== "android", - sequences: platform !== "android", - } - } - }) - ], - }, - module: { - rules: [ - { - include: join(appFullPath, entryPath), - use: [ - // Require all Android app components - platform === "android" && { - loader: "nativescript-dev-webpack/android-app-components-loader", - options: { modules: appComponents } - }, - - { - loader: "nativescript-dev-webpack/bundle-config-loader", - options: { - loadCss: !snapshot, // load the application css if in debug mode - unitTesting, - appFullPath, - projectRoot, - ignoredFiles: nsWebpack.getUserDefinedEntries(entries, platform) - } - }, - ].filter(loader => !!loader) - }, - - { - test: /\.(js|css|scss|html|xml)$/, - use: "nativescript-dev-webpack/hmr/hot-loader" - }, - - { test: /\.(html|xml)$/, use: "nativescript-dev-webpack/xml-namespace-loader" }, - - { - test: /\.css$/, - use: { loader: "css-loader", options: { url: false } } - }, - - { - test: /\.scss$/, - use: [ - { loader: "css-loader", options: { url: false } }, - "sass-loader" - ] - }, - ] - }, - plugins: [ - // Define useful constants like TNS_WEBPACK - new webpack.DefinePlugin({ - "global.TNS_WEBPACK": "true", - "process": undefined, - }), - // Remove all files from the out dir. - new CleanWebpackPlugin(itemsToClean, { verbose: !!verbose }), - // Copy assets to out dir. Add your own globs as needed. - new CopyWebpackPlugin([ - { from: { glob: "fonts/**" } }, - { from: { glob: "**/*.jpg" } }, - { from: { glob: "**/*.png" } }, - ], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }), - new nsWebpack.GenerateNativeScriptEntryPointsPlugin("bundle"), - - // For instructions on how to set up workers with webpack - // check out https://github.com/nativescript/worker-loader - new NativeScriptWorkerPlugin(), - new nsWebpack.PlatformFSPlugin({ - platform, - platforms, - }), - // Does IPC communication with the {N} CLI to notify events when running in watch mode. - new nsWebpack.WatchStateLoggerPlugin() - ], - }; - - if (report) { - // Generate report files for bundles content - config.plugins.push(new BundleAnalyzerPlugin({ - analyzerMode: "static", - openAnalyzer: false, - generateStatsFile: true, - reportFilename: resolve(projectRoot, "report", `report.html`), - statsFilename: resolve(projectRoot, "report", `stats.json`), - })); - } - - if (snapshot) { - config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ - chunk: "vendor", - requireModules: [ - "tns-core-modules/bundle-entry-points", - ], - projectRoot, - webpackConfig: config, - })); - } - - if (hmr) { - config.plugins.push(new webpack.HotModuleReplacementPlugin()); - } - - - return config; -}; \ No newline at end of file diff --git a/demo/TypeScriptApp/app/package.json b/demo/TypeScriptApp/app/package.json index a2e5436e..1a4b32df 100644 --- a/demo/TypeScriptApp/app/package.json +++ b/demo/TypeScriptApp/app/package.json @@ -1,8 +1,9 @@ { "android": { - "v8Flags": "--expose_gc" + "v8Flags": "--expose_gc", + "markingMode": "none" }, "main": "app.js", "name": "tns-template-hello-world-ts", "version": "3.3.0" -} \ No newline at end of file +} diff --git a/demo/TypeScriptApp/custom-application-activity.webpack.config.js b/demo/TypeScriptApp/custom-application-activity.webpack.config.js new file mode 100644 index 00000000..a02e2fef --- /dev/null +++ b/demo/TypeScriptApp/custom-application-activity.webpack.config.js @@ -0,0 +1,13 @@ +const webpackConfig = require("./webpack.config"); +const path = require("path"); + +module.exports = env => { + env = env || {}; + env.appComponents = env.appComponents || []; + env.appComponents.push(path.resolve(__dirname, "app/activity.android.ts")); + + env.entries = env.entries || {}; + env.entries.application = "./application.android"; + const config = webpackConfig(env); + return config; +}; \ No newline at end of file diff --git a/demo/TypeScriptApp/nsconfig.json b/demo/TypeScriptApp/nsconfig.json index a6d75472..564d5b27 100644 --- a/demo/TypeScriptApp/nsconfig.json +++ b/demo/TypeScriptApp/nsconfig.json @@ -1,3 +1,3 @@ { - "useLegacyWorkflow": false + "webpackConfigPath": "./custom-application-activity.webpack.config.js" } \ No newline at end of file diff --git a/demo/TypeScriptApp/package.json b/demo/TypeScriptApp/package.json index aabe8c7e..dd8eee9e 100644 --- a/demo/TypeScriptApp/package.json +++ b/demo/TypeScriptApp/package.json @@ -25,6 +25,7 @@ "babylon": "6.18.0", "lazy": "1.0.11", "mocha": "~5.2.0", + "chai": "4.2.0", "mochawesome": "~3.1.2", "nativescript-dev-appium": "next", "nativescript-dev-webpack": "next", diff --git a/demo/TypeScriptApp/tsconfig.tns.json b/demo/TypeScriptApp/tsconfig.tns.json index 95f2ecee..9ce50ed9 100644 --- a/demo/TypeScriptApp/tsconfig.tns.json +++ b/demo/TypeScriptApp/tsconfig.tns.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig", "compilerOptions": { - "module": "es2015", + "module": "esNext", "moduleResolution": "node" } } diff --git a/demo/TypeScriptApp/webpack.config.js b/demo/TypeScriptApp/webpack.config.js deleted file mode 100644 index 2418be9d..00000000 --- a/demo/TypeScriptApp/webpack.config.js +++ /dev/null @@ -1,290 +0,0 @@ -const { join, relative, resolve, sep } = require("path"); - -const webpack = require("webpack"); -const nsWebpack = require("nativescript-dev-webpack"); -const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target"); -const CleanWebpackPlugin = require("clean-webpack-plugin"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); -const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); -const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const hashSalt = Date.now().toString(); - -module.exports = env => { - // Add your custom Activities, Services and other Android app components here. - const appComponents = [ - "tns-core-modules/ui/frame", - "tns-core-modules/ui/frame/activity", - resolve(__dirname, "app/activity.android.ts") - ]; - - const platform = env && (env.android && "android" || env.ios && "ios"); - if (!platform) { - throw new Error("You need to provide a target platform!"); - } - - const platforms = ["ios", "android"]; - const projectRoot = __dirname; - - // Default destination inside platforms//... - const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); - - const { - // The 'appPath' and 'appResourcesPath' values are fetched from - // the nsconfig.json configuration file. - appPath = "app", - appResourcesPath = "app/App_Resources", - - // You can provide the following flags when running 'tns run android|ios' - snapshot, // --env.snapshot - production, // --env.production - uglify, // --env.uglify - report, // --env.report - sourceMap, // --env.sourceMap - hiddenSourceMap, // --env.hiddenSourceMap - hmr, // --env.hmr, - unitTesting, // --env.unitTesting, - verbose, // --env.verbose - } = env; - const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; - const externals = nsWebpack.getConvertedExternals(env.externals); - - const appFullPath = resolve(projectRoot, appPath); - const appResourcesFullPath = resolve(projectRoot, appResourcesPath); - - const entryModule = nsWebpack.getEntryModule(appFullPath, platform); - const entryPath = `.${sep}${entryModule}.ts`; - const entries = { bundle: entryPath, application: "./application.android" }; - - const tsConfigPath = resolve(projectRoot, "tsconfig.tns.json"); - - const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("tns-core-modules") > -1); - if (platform === "ios" && !areCoreModulesExternal) { - entries["tns_modules/tns-core-modules/inspector_modules"] = "inspector_modules"; - }; - - let sourceMapFilename = nsWebpack.getSourceMapFilename(hiddenSourceMap, __dirname, dist); - - const itemsToClean = [`${dist}/**/*`]; - if (platform === "android") { - itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "src", "main", "assets", "snapshots")}`); - itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "build", "configurations", "nativescript-android-snapshot")}`); - } - - nsWebpack.processAppComponents(appComponents, platform); - const config = { - mode: production ? "production" : "development", - context: appFullPath, - externals, - watchOptions: { - ignored: [ - appResourcesFullPath, - // Don't watch hidden files - "**/.*", - ] - }, - target: nativescriptTarget, - entry: entries, - output: { - pathinfo: false, - path: dist, - sourceMapFilename, - libraryTarget: "commonjs2", - filename: "[name].js", - globalObject: "global", - hashSalt - }, - resolve: { - extensions: [".ts", ".js", ".scss", ".css"], - // Resolve {N} system modules from tns-core-modules - modules: [ - resolve(__dirname, "node_modules/tns-core-modules"), - resolve(__dirname, "node_modules"), - "node_modules/tns-core-modules", - "node_modules", - ], - alias: { - '~': appFullPath - }, - // resolve symlinks to symlinked modules - symlinks: true - }, - resolveLoader: { - // don't resolve symlinks to symlinked loaders - symlinks: false - }, - node: { - // Disable node shims that conflict with NativeScript - "http": false, - "timers": false, - "setImmediate": false, - "fs": "empty", - "__dirname": false, - }, - devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), - optimization: { - runtimeChunk: "single", - splitChunks: { - cacheGroups: { - vendor: { - name: "vendor", - chunks: "all", - test: (module, chunks) => { - const moduleName = module.nameForCondition ? module.nameForCondition() : ''; - return /[\\/]node_modules[\\/]/.test(moduleName) || - appComponents.some(comp => comp === moduleName); - - }, - enforce: true, - }, - } - }, - minimize: !!uglify, - minimizer: [ - new TerserPlugin({ - parallel: true, - cache: true, - sourceMap: isAnySourceMapEnabled, - terserOptions: { - output: { - comments: false, - semicolons: !isAnySourceMapEnabled - }, - compress: { - // The Android SBG has problems parsing the output - // when these options are enabled - 'collapse_vars': platform !== "android", - sequences: platform !== "android", - } - } - }) - ], - }, - module: { - rules: [ - { - include: join(appFullPath, entryPath), - use: [ - // Require all Android app components - platform === "android" && { - loader: "nativescript-dev-webpack/android-app-components-loader", - options: { modules: appComponents } - }, - - { - loader: "nativescript-dev-webpack/bundle-config-loader", - options: { - loadCss: !snapshot, // load the application css if in debug mode - unitTesting, - appFullPath, - projectRoot, - ignoredFiles: nsWebpack.getUserDefinedEntries(entries, platform) - } - }, - ].filter(loader => !!loader) - }, - - { - test: /\.(ts|css|scss|html|xml)$/, - use: "nativescript-dev-webpack/hmr/hot-loader" - }, - - { test: /\.(html|xml)$/, use: "nativescript-dev-webpack/xml-namespace-loader" }, - - { - test: /\.css$/, - use: { loader: "css-loader", options: { url: false } } - }, - - { - test: /\.scss$/, - use: [ - { loader: "css-loader", options: { url: false } }, - "sass-loader" - ] - }, - - { - test: /\.ts$/, - use: { - loader: "ts-loader", - options: { - configFile: tsConfigPath, - // https://github.com/TypeStrong/ts-loader/blob/ea2fcf925ec158d0a536d1e766adfec6567f5fb4/README.md#faster-builds - // https://github.com/TypeStrong/ts-loader/blob/ea2fcf925ec158d0a536d1e766adfec6567f5fb4/README.md#hot-module-replacement - transpileOnly: true, - allowTsInNodeModules: true, - compilerOptions: { - sourceMap: isAnySourceMapEnabled, - declaration: false - } - }, - } - }, - ] - }, - plugins: [ - // Define useful constants like TNS_WEBPACK - new webpack.DefinePlugin({ - "global.TNS_WEBPACK": "true", - "process": undefined, - }), - // Remove all files from the out dir. - new CleanWebpackPlugin(itemsToClean, { verbose: !!verbose }), - // Copy assets to out dir. Add your own globs as needed. - new CopyWebpackPlugin([ - { from: { glob: "fonts/**" } }, - { from: { glob: "**/*.jpg" } }, - { from: { glob: "**/*.png" } }, - ], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }), - new nsWebpack.GenerateNativeScriptEntryPointsPlugin("bundle"), - // For instructions on how to set up workers with webpack - // check out https://github.com/nativescript/worker-loader - new NativeScriptWorkerPlugin(), - new nsWebpack.PlatformFSPlugin({ - platform, - platforms, - }), - // Does IPC communication with the {N} CLI to notify events when running in watch mode. - new nsWebpack.WatchStateLoggerPlugin(), - // https://github.com/TypeStrong/ts-loader/blob/ea2fcf925ec158d0a536d1e766adfec6567f5fb4/README.md#faster-builds - // https://github.com/TypeStrong/ts-loader/blob/ea2fcf925ec158d0a536d1e766adfec6567f5fb4/README.md#hot-module-replacement - new ForkTsCheckerWebpackPlugin({ - tsconfig: tsConfigPath, - async: false, - useTypescriptIncrementalApi: true, - memoryLimit: 4096 - }) - ], - }; - - if (report) { - // Generate report files for bundles content - config.plugins.push(new BundleAnalyzerPlugin({ - analyzerMode: "static", - openAnalyzer: false, - generateStatsFile: true, - reportFilename: resolve(projectRoot, "report", `report.html`), - statsFilename: resolve(projectRoot, "report", `stats.json`), - })); - } - - if (snapshot) { - config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ - chunk: "vendor", - requireModules: [ - "tns-core-modules/bundle-entry-points", - ], - projectRoot, - webpackConfig: config, - })); - } - - if (hmr) { - config.plugins.push(new webpack.HotModuleReplacementPlugin()); - } - - - return config; -}; \ No newline at end of file diff --git a/dependencyManager.js b/dependencyManager.js index 8f348013..4cfeddcd 100644 --- a/dependencyManager.js +++ b/dependencyManager.js @@ -73,7 +73,7 @@ function getRequiredDeps(packageJson) { } const deps = { - "@angular/compiler-cli": "8.2.0", + "@angular/compiler-cli": "~8.2.0", }; if (!dependsOn(packageJson, "@angular-devkit/build-angular")) { diff --git a/host/resolver.ts b/host/resolver.ts index 6cff232c..071c0222 100644 --- a/host/resolver.ts +++ b/host/resolver.ts @@ -3,7 +3,7 @@ import { statSync } from "fs"; export function getResolver(platforms: string[], explicitResolve?: string[], nsPackageFilters?: string[], platformSpecificExt?: string[]) { explicitResolve = explicitResolve || []; - nsPackageFilters = nsPackageFilters || ['nativescript', 'tns', 'ns']; + nsPackageFilters = nsPackageFilters || ['nativescript', 'tns', 'ns', '@nativescript']; platformSpecificExt = platformSpecificExt || [".ts", ".js", ".scss", ".less", ".css", ".html", ".xml", ".vue", ".json"]; return function (path: string) { diff --git a/index.js b/index.js index 7effb6c3..4846dc02 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,54 @@ const { Object.assign(exports, require("./plugins")); Object.assign(exports, require("./host/resolver")); +exports.processTsPathsForScopedModules = function ({ compilerOptions }) { + const tnsModulesOldPackage = "tns-core-modules"; + const tnsModulesNewPackage = "@nativescript/core"; + replacePathInCompilerOptions({ + compilerOptions, + targetPath: tnsModulesOldPackage, + replacementPath: tnsModulesNewPackage + }); + ensurePathInCompilerOptions({ + compilerOptions, + sourcePath: tnsModulesOldPackage, + destinationPath: `./node_modules/${tnsModulesNewPackage}` + }); + ensurePathInCompilerOptions({ + compilerOptions, + sourcePath: `${tnsModulesOldPackage}/*`, + destinationPath: `./node_modules/${tnsModulesNewPackage}/*` + }); +} + +exports.processTsPathsForScopedAngular = function ({ compilerOptions }) { + const nsAngularOldPackage = "nativescript-angular"; + const nsAngularNewPackage = "@nativescript/angular"; + replacePathInCompilerOptions({ + compilerOptions, + targetPath: nsAngularOldPackage, + replacementPath: nsAngularNewPackage + }); + ensurePathInCompilerOptions({ + compilerOptions, + sourcePath: nsAngularOldPackage, + destinationPath: `./node_modules/${nsAngularNewPackage}` + }); + ensurePathInCompilerOptions({ + compilerOptions, + sourcePath: `${nsAngularOldPackage}/*`, + destinationPath: `./node_modules/${nsAngularNewPackage}/*` + }); +} + +exports.hasRootLevelScopedModules = function ({ projectDir }) { + return hasRootLevelPackage({ projectDir, packageName: "@nativescript/core" }); +} + +exports.hasRootLevelScopedAngular = function ({ projectDir }) { + return hasRootLevelPackage({ projectDir, packageName: "@nativescript/angular" }); +} + exports.getAotEntryModule = function (appDirectory) { verifyEntryModuleDirectory(appDirectory); @@ -31,12 +79,14 @@ exports.getEntryModule = function (appDirectory, platform) { const entry = getPackageJsonEntry(appDirectory); const tsEntryPath = path.resolve(appDirectory, `${entry}.ts`); + const ktEntryPath = path.resolve(appDirectory, `${entry}.kt`); const jsEntryPath = path.resolve(appDirectory, `${entry}.js`); - let entryExists = existsSync(tsEntryPath) || existsSync(jsEntryPath); + let entryExists = existsSync(tsEntryPath) || existsSync(ktEntryPath) || existsSync(jsEntryPath); if (!entryExists && platform) { const platformTsEntryPath = path.resolve(appDirectory, `${entry}.${platform}.ts`); + const platformKtEntryPath = path.resolve(appDirectory, `${entry}.${platform}.kt`); const platformJsEntryPath = path.resolve(appDirectory, `${entry}.${platform}.js`); - entryExists = existsSync(platformTsEntryPath) || existsSync(platformJsEntryPath); + entryExists = existsSync(platformTsEntryPath) || existsSync(platformKtEntryPath) || existsSync(platformJsEntryPath); } if (!entryExists) { @@ -55,6 +105,8 @@ exports.getAppPath = (platform, projectDir) => { return `platforms/ios/${sanitizedName}/app`; } else if (isAndroid(platform)) { return ANDROID_APP_PATH; + } else if (hasPlatformPlugin(projectDir, platform)) { + return `platforms/${platform}/app`; } else { throw new Error(`Invalid platform: ${platform}`); } @@ -143,6 +195,13 @@ const sanitize = name => name .filter(char => /[a-zA-Z0-9]/.test(char)) .join(""); +function hasPlatformPlugin(appDirectory, platform) { + const packageJsonSource = getPackageJson(appDirectory); + const { dependencies } = packageJsonSource; + + return !!dependencies[`nativescript-platform-${platform}`]; +} + function getPackageJsonEntry(appDirectory) { const packageJsonSource = getPackageJson(appDirectory); const entry = packageJsonSource.main; @@ -151,7 +210,7 @@ function getPackageJsonEntry(appDirectory) { throw new Error(`${appDirectory}/package.json must contain a 'main' attribute!`); } - return entry.replace(/\.js$/i, ""); + return entry.replace(/\.js$/i, "").replace(/\.kt$/i, ""); } function verifyEntryModuleDirectory(appDirectory) { @@ -163,3 +222,45 @@ function verifyEntryModuleDirectory(appDirectory) { throw new Error(`The specified path to app directory ${appDirectory} does not exist. Unable to find entry module.`); } } + + +function hasRootLevelPackage({ projectDir, packageName }) { + let hasRootLevelPackage; + try { + require.resolve(packageName, { paths: [projectDir] }); + hasRootLevelPackage = true; + } catch (e) { + hasRootLevelPackage = false; + } + + return hasRootLevelPackage; +} + +function replacePathInCompilerOptions({ compilerOptions, targetPath, replacementPath }) { + const paths = (compilerOptions && compilerOptions.paths) || {}; + for (const key in paths) { + if (paths.hasOwnProperty(key)) { + const pathsForPattern = paths[key]; + if (Array.isArray(pathsForPattern)) { + for (let i = 0; i < pathsForPattern.length; ++i) { + if (typeof pathsForPattern[i] === "string") { + pathsForPattern[i] = pathsForPattern[i].replace(targetPath, replacementPath); + } + } + } + } + } +} + +function ensurePathInCompilerOptions({ compilerOptions, sourcePath, destinationPath }) { + compilerOptions = compilerOptions || {}; + compilerOptions.paths = compilerOptions.paths || {}; + const paths = compilerOptions.paths; + if (paths[sourcePath]) { + if (Array.isArray(paths[sourcePath]) && paths[sourcePath].indexOf(destinationPath) === -1) { + paths[sourcePath].push(destinationPath); + } + } else { + paths[sourcePath] = [destinationPath]; + } +} diff --git a/jasmine-config/jasmine.json b/jasmine-config/jasmine.json index 8d3ecdc5..3d06fa01 100644 --- a/jasmine-config/jasmine.json +++ b/jasmine-config/jasmine.json @@ -3,7 +3,7 @@ "spec_files": [ "!node_modules/**/*.spec.js", "!demo/**/*.spec.js", - "./*.spec.js" + "./**/*.spec.js" ], "helpers": [ "jasmine-config/**/*.js" diff --git a/lib/after-prepare.js b/lib/after-prepare.js index 7138b402..333e60f7 100644 --- a/lib/after-prepare.js +++ b/lib/after-prepare.js @@ -12,7 +12,12 @@ module.exports = function (hookArgs) { release: hookArgs.prepareData.release }; - if (env.snapshot && shouldSnapshot(shouldSnapshotOptions)) { + if (env.snapshot && + shouldSnapshot(shouldSnapshotOptions) && + (!hookArgs.prepareData || + !hookArgs.prepareData.nativePrepare || + !hookArgs.prepareData.nativePrepare.skipNativePrepare)) { + installSnapshotArtefacts(hookArgs.prepareData.projectDir); } } diff --git a/lib/before-checkForChanges.js b/lib/before-checkForChanges.js index d456a1d3..fd81fd44 100644 --- a/lib/before-checkForChanges.js +++ b/lib/before-checkForChanges.js @@ -1,6 +1,11 @@ +const { shouldSnapshot, isWinOS } = require("./utils"); +const semver = require("semver"); + module.exports = function ($staticConfig, hookArgs) { - const majorVersionMatch = ($staticConfig.version || '').match(/^(\d+)\./); - const majorVersion = majorVersionMatch && majorVersionMatch[1] && +majorVersionMatch[1]; + const cliVersion = semver.parse($staticConfig.version); + const majorVersion = cliVersion && cliVersion.major; + const minorVersion = cliVersion && cliVersion.minor; + if (majorVersion && majorVersion < 6) { // check if we are using the bundle workflow or the legacy one. const isUsingBundleWorkflow = hookArgs && @@ -12,5 +17,17 @@ module.exports = function ($staticConfig, hookArgs) { const packageJsonData = require("../package.json") throw new Error(`The current version of ${packageJsonData.name} (${packageJsonData.version}) is not compatible with the used CLI: ${$staticConfig.version}. Please upgrade your NativeScript CLI version (npm i -g nativescript).`); } + } else { + const env = hookArgs.prepareData.env || {}; + const shouldSnapshotOptions = { + platform: hookArgs.prepareData.platform, + release: hookArgs.prepareData.release + }; + + const shouldSnapshotOnWin = env.snapshot && shouldSnapshot(shouldSnapshotOptions) && isWinOS(); + const isCLIWithoutWinSnapshotsSupport = majorVersion && majorVersion == 6 && minorVersion && minorVersion < 2; + if (shouldSnapshotOnWin && isCLIWithoutWinSnapshotsSupport) { + throw new Error(`In order to generate Snapshots on Windows, please upgrade your NativeScript CLI version (npm i -g nativescript).`); + } } } \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js index a527b24a..17b1eb33 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,18 +1,41 @@ const os = require("os"); +const { dirname } = require("path"); +const { existsSync, mkdirSync } = require("fs"); const { isAndroid } = require("../projectHelpers"); function shouldSnapshot(config) { const platformSupportsSnapshot = isAndroid(config.platform); - const osSupportsSnapshot = os.type() !== "Windows_NT"; - return config.release && platformSupportsSnapshot && osSupportsSnapshot; + return config.release && platformSupportsSnapshot; } function convertToUnixPath(relativePath) { return relativePath.replace(/\\/g, "/"); } +function isWinOS() { + return os.type() === "Windows_NT"; +} + +function warn(message) { + if (message) { + console.log(`\x1B[33;1m${message}\x1B[0m`); + } +} + +function ensureDirectoryExistence(filePath) { + var dir = dirname(filePath); + if (existsSync(dir)) { + return true; + } + ensureDirectoryExistence(dir); + mkdirSync(dir); +} + module.exports = { shouldSnapshot, - convertToUnixPath + convertToUnixPath, + isWinOS, + warn, + ensureDirectoryExistence }; diff --git a/load-application-css-angular.js b/load-application-css-angular.js index 314203c6..42e72167 100644 --- a/load-application-css-angular.js +++ b/load-application-css-angular.js @@ -3,5 +3,6 @@ const loadCss = require("./load-application-css"); module.exports = function() { loadCss(function() { global.registerModule("./app.css", () => require("~/app")); + global.registerModule("app.css", () => require("~/app")); }); } diff --git a/package.json b/package.json index 7421d2e5..c4b42a23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-webpack", - "version": "1.1.0", + "version": "1.5.1", "main": "index", "description": "", "homepage": "http://www.telerik.com", @@ -35,6 +35,7 @@ "prepare": "npm run tsc && npm run jasmine", "test": "npm run prepare", "jasmine": "jasmine --config=jasmine-config/jasmine.json", + "coverage": "nyc npm run test", "version": "rm package-lock.json && conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" }, "bin": { @@ -49,19 +50,22 @@ "@angular-devkit/core": "8.2.0", "clean-webpack-plugin": "~1.0.0", "copy-webpack-plugin": "~4.6.0", + "css": "2.2.1", "css-loader": "~2.1.1", - "extra-watch-webpack-plugin": "1.0.3", - "fork-ts-checker-webpack-plugin": "1.3.0", + "escape-string-regexp": "1.0.5", + "fork-ts-checker-webpack-plugin": "2.0.0", "global-modules-path": "2.0.0", "loader-utils": "^1.2.3", "minimatch": "3.0.4", "nativescript-hook": "0.2.4", - "nativescript-worker-loader": "~0.9.0", + "nativescript-worker-loader": "~0.11.0", + "properties-reader": "0.3.1", "proxy-lib": "0.4.0", "raw-loader": "~0.5.1", "request": "2.88.0", "resolve-url-loader": "~3.0.0", "sass-loader": "~7.1.0", + "sax": "^1.2.4", "schema-utils": "0.4.5", "semver": "^6.0.0", "shelljs": "0.6.0", @@ -77,18 +81,23 @@ "devDependencies": { "@angular/compiler": "8.2.0", "@angular/compiler-cli": "8.2.0", + "@istanbuljs/nyc-config-typescript": "^0.1.3", "@ngtools/webpack": "8.2.0", + "@types/css": "0.0.31", "@types/jasmine": "^3.3.7", "@types/loader-utils": "^1.1.3", "@types/node": "^10.12.12", "@types/proxyquire": "1.3.28", + "@types/sax": "^1.2.0", "@types/semver": "^6.0.0", "@types/webpack": "^4.4.34", "conventional-changelog-cli": "^1.3.22", "jasmine": "^3.2.0", "jasmine-spec-reporter": "^4.2.1", + "nyc": "^14.1.1", "proxyquire": "2.1.0", + "source-map-support": "^0.5.13", "tns-core-modules": "next", "typescript": "~3.5.3" } -} \ No newline at end of file +} diff --git a/plugins/GenerateNativeScriptEntryPointsPlugin.js b/plugins/GenerateNativeScriptEntryPointsPlugin.js index 9417be22..6f7177c3 100644 --- a/plugins/GenerateNativeScriptEntryPointsPlugin.js +++ b/plugins/GenerateNativeScriptEntryPointsPlugin.js @@ -1,5 +1,5 @@ const { convertToUnixPath } = require("../lib/utils"); -const { RawSource } = require("webpack-sources"); +const { RawSource, ConcatSource } = require("webpack-sources"); const { getPackageJson } = require("../projectHelpers"); const { SNAPSHOT_ENTRY_NAME } = require("./NativeScriptSnapshotPlugin"); const path = require("path"); @@ -83,7 +83,12 @@ exports.GenerateNativeScriptEntryPointsPlugin = (function () { return `require("./${depRelativePathUnix}");`; }).join(""); const currentEntryFileContent = compilation.assets[filePath].source(); - compilation.assets[filePath] = new RawSource(`${requireDeps}${currentEntryFileContent}`); + + if(compilation.assets[filePath] instanceof ConcatSource) { + compilation.assets[filePath].children.unshift(`${requireDeps}`); + } else { + compilation.assets[filePath] = new RawSource(`${requireDeps}${currentEntryFileContent}`); + } } }); } diff --git a/plugins/NativeScriptSnapshotPlugin/index.js b/plugins/NativeScriptSnapshotPlugin/index.js index 97cebd7e..58b14876 100644 --- a/plugins/NativeScriptSnapshotPlugin/index.js +++ b/plugins/NativeScriptSnapshotPlugin/index.js @@ -8,6 +8,7 @@ const { ANDROID_PROJECT_DIR, ANDROID_APP_PATH, } = require("../../androidProjectHelpers"); +const { ensureDirectoryExistence } = require("../../lib/utils"); const schema = require("./options.json"); const SNAPSHOT_ENTRY_NAME = "snapshot-entry"; @@ -57,6 +58,7 @@ exports.NativeScriptSnapshotPlugin = (function () { snapshotEntryContent += [...requireModules, ...internalRequireModules] .map(mod => `require('${mod}')`).join(";"); + ensureDirectoryExistence(snapshotEntryPath); writeFileSync(snapshotEntryPath, snapshotEntryContent, { encoding: "utf8" }); // add the module to the entry points to make sure it's content is evaluated @@ -68,7 +70,6 @@ exports.NativeScriptSnapshotPlugin = (function () { // ensure that the runtime is installed only in the snapshotted chunk webpackConfig.optimization.runtimeChunk = { name: SNAPSHOT_ENTRY_NAME }; } - NativeScriptSnapshotPlugin.getInternalRequireModules = function (webpackContext) { const packageJson = getPackageJson(webpackContext); return (packageJson && packageJson["android"] && packageJson["android"]["requireModules"]) || []; @@ -97,6 +98,11 @@ exports.NativeScriptSnapshotPlugin = (function () { NativeScriptSnapshotPlugin.prototype.generate = function (webpackChunks) { const options = this.options; + if (options.skipSnapshotTools) { + console.log(`Skipping snapshot tools.`); + return Promise.resolve(); + } + const inputFiles = webpackChunks.map(chunk => join(options.webpackConfig.output.path, chunk.files[0])); const preprocessedInputFile = join( this.options.projectRoot, @@ -113,6 +119,8 @@ exports.NativeScriptSnapshotPlugin = (function () { useLibs: options.useLibs, androidNdkPath: options.androidNdkPath, v8Version: options.v8Version, + snapshotInDocker: options.snapshotInDocker, + skipSnapshotTools: options.skipSnapshotTools }).then(() => { // Make the original files empty inputFiles.forEach(inputFile => diff --git a/plugins/NativeScriptSnapshotPlugin/options.json b/plugins/NativeScriptSnapshotPlugin/options.json index 513afd4e..22e08d60 100644 --- a/plugins/NativeScriptSnapshotPlugin/options.json +++ b/plugins/NativeScriptSnapshotPlugin/options.json @@ -5,8 +5,8 @@ "type": "string" }, "angular": { - "type": "boolean", - "default": false + "type": "boolean", + "default": false }, "chunk": { "type": "string" @@ -25,17 +25,13 @@ }, "targetArchs": { "type": "array", - "default": [ - "arm", - "arm64", - "ia32" - ], "items": { "type": "string", "enum": [ "arm", "arm64", - "ia32" + "ia32", + "ia64" ] } }, @@ -43,6 +39,13 @@ "type": "boolean", "default": false }, + "snapshotInDocker": { + "type": "boolean" + }, + "skipSnapshotTools": { + "type": "boolean", + "default": false + }, "v8Version": { "type": "string" }, @@ -60,4 +63,4 @@ "webpackConfig" ], "additionalProperties": false -} +} \ No newline at end of file diff --git a/plugins/PlatformFSPlugin.ts b/plugins/PlatformFSPlugin.ts index 5619ea4e..91daccac 100644 --- a/plugins/PlatformFSPlugin.ts +++ b/plugins/PlatformFSPlugin.ts @@ -8,7 +8,7 @@ export interface PlatformFSPluginOptions { platform?: string; /** - * A list of all platforms. By default it is `["ios", "android"]`. + * A list of all platforms. By default it is `["ios", "android", "desktop"]`. */ platforms?: string[]; @@ -18,6 +18,8 @@ export interface PlatformFSPluginOptions { ignore?: string[]; } +const internalPlatforms = ["ios", "android"]; + export class PlatformFSPlugin { protected readonly platform: string; protected readonly platforms: ReadonlyArray; @@ -26,7 +28,7 @@ export class PlatformFSPlugin { constructor({ platform, platforms, ignore }: PlatformFSPluginOptions) { this.platform = platform || ""; - this.platforms = platforms || ["ios", "android"]; + this.platforms = platforms || internalPlatforms; this.ignore = ignore || []; } @@ -58,6 +60,8 @@ export function mapFileSystem(args: MapFileSystemArgs): any { const fs = compiler.inputFileSystem; ignore = args.ignore || []; + const isExternal = internalPlatforms.indexOf(platform) === -1; + const minimatchFileFilters = ignore.map(pattern => { const minimatchFilter = minimatch.filter(pattern); return file => minimatchFilter(relative(context, file)); @@ -80,7 +84,7 @@ export function mapFileSystem(args: MapFileSystemArgs): any { return join(dir, name.substr(0, name.length - currentPlatformExt.length) + ext); } return file; - } + }; const isNotIgnored = file => !isIgnored(file); @@ -95,7 +99,28 @@ export function mapFileSystem(args: MapFileSystemArgs): any { function platformSpecificFile(file: string): string { const {dir, name, ext} = parseFile(file); - const platformFilePath = join(dir, `${name}.${platform}${ext}`); + let platformFilePath = join(dir, `${name}.${platform}${ext}`); + + if (isExternal && dir.indexOf("/@nativescript/core/") !== -1) { + let replacedPath; + try { + replacedPath = dir.replace( + /node_modules(\/[^/]+)?\/@nativescript\/core/, + `node_modules/nativescript-platform-${platform}` + ); + + platformFilePath = require.resolve(join(replacedPath, `${name}.${platform}${ext}`)); + } catch (e) { + if (replacedPath) { + if (ext === ".d") { + platformFilePath = undefined; + } else { + platformFilePath = join(replacedPath, `${name}${ext}`); + } + } + } + } + return platformFilePath; } diff --git a/plugins/WatchStateLoggerPlugin.ts b/plugins/WatchStateLoggerPlugin.ts index 7e5302d7..2767809d 100644 --- a/plugins/WatchStateLoggerPlugin.ts +++ b/plugins/WatchStateLoggerPlugin.ts @@ -29,15 +29,14 @@ export class WatchStateLoggerPlugin { console.log(messages.compilationComplete); } - let emittedFiles = Object + const emittedFiles = Object .keys(compilation.assets) .filter(assetKey => compilation.assets[assetKey].emitted); const chunkFiles = getChunkFiles(compilation); - process.send && process.send(messages.compilationComplete, error => null); // Send emitted files so they can be LiveSynced if need be - process.send && process.send({ emittedFiles, chunkFiles }, error => null); + process.send && process.send({ emittedFiles, chunkFiles, hash: compilation.hash }, error => null); }); } } diff --git a/snapshot/android/project-snapshot-generator-cli-ags-parser.js b/snapshot/android/project-snapshot-generator-cli-ags-parser.js index 76af75b1..6d3b9b61 100644 --- a/snapshot/android/project-snapshot-generator-cli-ags-parser.js +++ b/snapshot/android/project-snapshot-generator-cli-ags-parser.js @@ -9,6 +9,14 @@ module.exports = function parseProjectSnapshotGeneratorArgs() { result.useLibs = parseBool(result.useLibs); } + if (result.snapshotInDocker !== undefined) { + result.snapshotInDocker = parseBool(result.snapshotInDocker); + } + + if (result.skipSnapshotTools !== undefined) { + result.skipSnapshotTools = parseBool(result.skipSnapshotTools); + } + if (result.install !== undefined) { result.install = parseBool(result.install); } @@ -22,7 +30,7 @@ function parseJsonFromProcessArgs() { var currentKey = ""; var currentValue = ""; - args.forEach(function(value, index, array) { + args.forEach(function (value, index, array) { if (value.startsWith("--")) { // if is key addKeyAndValueToResult(currentKey, currentValue, result); currentKey = value.slice(2); diff --git a/snapshot/android/project-snapshot-generator.js b/snapshot/android/project-snapshot-generator.js index be5ef1f3..6d168149 100644 --- a/snapshot/android/project-snapshot-generator.js +++ b/snapshot/android/project-snapshot-generator.js @@ -1,30 +1,26 @@ -const { dirname, isAbsolute, join, resolve } = require("path"); -const { existsSync, readFileSync, writeFileSync } = require("fs"); +const { isAbsolute, join, resolve, sep } = require("path"); +const { readFileSync, writeFileSync } = require("fs"); const shelljs = require("shelljs"); const semver = require("semver"); const SnapshotGenerator = require("./snapshot-generator"); const { - CONSTANTS, - createDirectory, - getJsonFile, + CONSTANTS } = require("./utils"); -const { getPackageJson } = require("../../projectHelpers"); const { ANDROID_PROJECT_DIR, ANDROID_APP_PATH, ANDROID_CONFIGURATIONS_PATH, getAndroidRuntimeVersion, getAndroidV8Version, + getRuntimeNdkRevision, getMksnapshotParams } = require("../../androidProjectHelpers"); -const MIN_ANDROID_RUNTIME_VERSION = "3.0.0"; +// min version with settings.json file specifying the V8 version +const MIN_ANDROID_RUNTIME_VERSION = "5.2.1"; const VALID_ANDROID_RUNTIME_TAGS = Object.freeze(["next", "rc"]); -const V8_VERSIONS_FILE_NAME = "v8-versions.json"; -const V8_VERSIONS_URL = `https://raw.githubusercontent.com/NativeScript/android-runtime/master/${V8_VERSIONS_FILE_NAME}`; -const V8_VERSIONS_LOCAL_PATH = resolve(CONSTANTS.SNAPSHOT_TMP_DIR, V8_VERSIONS_FILE_NAME); const resolveRelativePath = (path) => { if (path) @@ -103,7 +99,7 @@ ProjectSnapshotGenerator.installSnapshotArtefacts = function (projectRoot) { // Copy the libs to the specified destination in the platforms folder shelljs.mkdir("-p", libsDestinationPath); - shelljs.cp("-R", join(buildPath, "ndk-build/libs") + "/", libsDestinationPath); + shelljs.cp("-R", join(buildPath, "ndk-build/libs") + sep, libsDestinationPath); } else { // useLibs = false const blobsSrcPath = join(buildPath, "snapshots/blobs"); @@ -111,12 +107,7 @@ ProjectSnapshotGenerator.installSnapshotArtefacts = function (projectRoot) { const appPackageJsonPath = join(appPath, "package.json"); // Copy the blobs in the prepared app folder - shelljs.cp("-R", blobsSrcPath + "/", resolve(appPath, "../snapshots")); - - /* - Rename TNSSnapshot.blob files to snapshot.blob files. The xxd tool uses the file name for the name of the static array. This is why the *.blob files are initially named TNSSnapshot.blob. After the xxd step, they must be renamed to snapshot.blob, because this is the filename that the Android runtime is looking for. - */ - shelljs.exec("find " + blobsDestinationPath + " -name '*.blob' -execdir mv {} snapshot.blob ';'"); + shelljs.cp("-R", blobsSrcPath + sep, resolve(appPath, "../snapshots")); // Update the package.json file const appPackageJson = shelljs.test("-e", appPackageJsonPath) ? JSON.parse(readFileSync(appPackageJsonPath, 'utf8')) : {}; @@ -126,73 +117,6 @@ ProjectSnapshotGenerator.installSnapshotArtefacts = function (projectRoot) { } } -const versionIsPrerelease = version => version.indexOf("-") > -1; -const v8VersionsFileExists = () => existsSync(V8_VERSIONS_LOCAL_PATH); -const saveV8VersionsFile = versionsMap => - writeFileSync(V8_VERSIONS_LOCAL_PATH, JSON.stringify(versionsMap)); -const readV8VersionsFile = () => JSON.parse(readFileSync(V8_VERSIONS_LOCAL_PATH)); -const fetchV8VersionsFile = () => - new Promise((resolve, reject) => { - getJsonFile(V8_VERSIONS_URL) - .then(versionsMap => { - createDirectory(dirname(V8_VERSIONS_LOCAL_PATH)); - saveV8VersionsFile(versionsMap); - return resolve(versionsMap); - }) - .catch(reject); - }); - -const findV8Version = (runtimeVersion, v8VersionsMap) => { - const runtimeRange = Object.keys(v8VersionsMap) - .find(range => semver.satisfies(runtimeVersion, range)); - - return v8VersionsMap[runtimeRange]; -} - -const getV8VersionsMap = runtimeVersion => - new Promise((resolve, reject) => { - if (!v8VersionsFileExists() || versionIsPrerelease(runtimeVersion)) { - fetchV8VersionsFile() - .then(versionsMap => resolve({ versionsMap, latest: true })) - .catch(reject); - } else { - const versionsMap = readV8VersionsFile(); - return resolve({ versionsMap, latest: false }); - } - }); - -ProjectSnapshotGenerator.prototype.getV8Version = function (generationOptions) { - return new Promise((resolve, reject) => { - const maybeV8Version = generationOptions.v8Version; - if (maybeV8Version) { - return resolve(maybeV8Version); - } - - // try to get the V8 Version from the settings.json file in android runtime folder - const runtimeV8Version = getAndroidV8Version(this.options.projectRoot); - if(runtimeV8Version) { - return resolve(runtimeV8Version); - } - - const runtimeVersion = getAndroidRuntimeVersion(this.options.projectRoot); - getV8VersionsMap(runtimeVersion) - .then(({ versionsMap, latest }) => { - const v8Version = findV8Version(runtimeVersion, versionsMap); - - if (!v8Version && !latest) { - fetchV8VersionsFile().then(latestVersionsMap => { - const version = findV8Version(runtimeVersion, latestVersionsMap) - return resolve(version); - }) - .catch(reject); - } else { - return resolve(v8Version); - } - }) - .catch(reject); - }); -} - ProjectSnapshotGenerator.prototype.validateAndroidRuntimeVersion = function () { const currentRuntimeVersion = getAndroidRuntimeVersion(this.options.projectRoot); @@ -209,6 +133,11 @@ ProjectSnapshotGenerator.prototype.validateAndroidRuntimeVersion = function () { } ProjectSnapshotGenerator.prototype.generate = function (generationOptions) { + if (generationOptions.skipSnapshotTools) { + console.log("Skipping snapshot tools."); + return Promise.resolve(); + } + generationOptions = generationOptions || {}; console.log("Running snapshot generation with the following arguments: "); @@ -219,49 +148,50 @@ ProjectSnapshotGenerator.prototype.generate = function (generationOptions) { shelljs.mkdir("-p", this.getBuildPath()); const snapshotToolsPath = resolveRelativePath(generationOptions.snapshotToolsPath) || CONSTANTS.SNAPSHOT_TMP_DIR; - const androidNdkPath = generationOptions.androidNdkPath || process.env.ANDROID_NDK_HOME; - console.log("Snapshot tools path: " + snapshotToolsPath); // Generate snapshots const generator = new SnapshotGenerator({ buildPath: this.getBuildPath() }); - const noV8VersionFoundMessage = `Cannot find suitable v8 version!`; - let shouldRethrow = false; - const mksnapshotParams = getMksnapshotParams(this.options.projectRoot); + const recommendedAndroidNdkRevision = getRuntimeNdkRevision(this.options.projectRoot); + const v8Version = generationOptions.v8Version || getAndroidV8Version(this.options.projectRoot); + if (!v8Version) { + throw new Error(noV8VersionFoundMessage); + } - return this.getV8Version(generationOptions).then(v8Version => { - shouldRethrow = true; - if (!v8Version) { - throw new Error(noV8VersionFoundMessage); - } + // NOTE: Order is important! Add new archs at the end of the array + const defaultTargetArchs = ["arm", "arm64", "ia32", "ia64"]; + const runtimeVersion = getAndroidRuntimeVersion(this.options.projectRoot); + if (runtimeVersion && semver.lt(semver.coerce(runtimeVersion), "6.0.2")) { + const indexOfIa64 = defaultTargetArchs.indexOf("ia64"); + // Before 6.0.2 version of Android runtime we supported only arm, arm64 and ia32. + defaultTargetArchs.splice(indexOfIa64, defaultTargetArchs.length - indexOfIa64); + } - const options = { - snapshotToolsPath, - targetArchs: generationOptions.targetArchs || ["arm", "arm64", "ia32"], - v8Version: generationOptions.v8Version || v8Version, - preprocessedInputFile: generationOptions.preprocessedInputFile, - useLibs: generationOptions.useLibs || false, - inputFiles: generationOptions.inputFiles || [join(this.options.projectRoot, "__snapshot.js")], - androidNdkPath, - mksnapshotParams: mksnapshotParams - }; - - return generator.generate(options).then(() => { - console.log("Snapshots build finished succesfully!"); - - if (generationOptions.install) { - ProjectSnapshotGenerator.cleanSnapshotArtefacts(this.options.projectRoot); - ProjectSnapshotGenerator.installSnapshotArtefacts(this.options.projectRoot); - console.log(generationOptions.useLibs ? - "Snapshot is included in the app as dynamically linked library (.so file)." : - "Snapshot is included in the app as binary .blob file. The more space-efficient option is to embed it in a dynamically linked library (.so file)."); - } - }); - }).catch(error => { - throw shouldRethrow ? - error : - new Error(`${noV8VersionFoundMessage} Original error: ${error.message || error}`); - }); + const options = { + snapshotToolsPath, + targetArchs: generationOptions.targetArchs || defaultTargetArchs, + v8Version: generationOptions.v8Version || v8Version, + preprocessedInputFile: generationOptions.preprocessedInputFile, + useLibs: generationOptions.useLibs || false, + inputFiles: generationOptions.inputFiles || [join(this.options.projectRoot, "__snapshot.js")], + androidNdkPath: generationOptions.androidNdkPath, + mksnapshotParams: mksnapshotParams, + snapshotInDocker: generationOptions.snapshotInDocker, + recommendedAndroidNdkRevision, + runtimeVersion + }; + + return generator.generate(options).then(() => { + console.log("Snapshots build finished succesfully!"); + + if (generationOptions.install) { + ProjectSnapshotGenerator.cleanSnapshotArtefacts(this.options.projectRoot); + ProjectSnapshotGenerator.installSnapshotArtefacts(this.options.projectRoot); + console.log(generationOptions.useLibs ? + "Snapshot is included in the app as dynamically linked library (.so file)." : + "Snapshot is included in the app as binary .blob file. The more space-efficient option is to embed it in a dynamically linked library (.so file)."); + } + });; } diff --git a/snapshot/android/snapshot-generator.js b/snapshot/android/snapshot-generator.js index 8c177809..eb055dcd 100644 --- a/snapshot/android/snapshot-generator.js +++ b/snapshot/android/snapshot-generator.js @@ -1,12 +1,16 @@ const fs = require("fs"); -const { dirname, join, EOL } = require("path"); -const os = require("os"); +const { dirname, relative, join, EOL } = require("path"); const child_process = require("child_process"); - +const { convertToUnixPath, warn } = require("../../lib/utils"); +const { isWindows } = require("./utils"); +const PropertiesReader = require('properties-reader'); +const semver = require("semver"); const shelljs = require("shelljs"); -const { createDirectory, downloadFile } = require("./utils"); +const { createDirectory, downloadFile, getHostOS, getHostOSArch, CONSTANTS, has32BitArch, isMacOSCatalinaOrHigher, isSubPath } = require("./utils"); +const SNAPSHOTS_DOCKER_IMAGE = "nativescript/v8-snapshot:latest"; +const SNAPSHOT_TOOLS_DIR_NAME = "mksnapshot-tools"; const NDK_BUILD_SEED_PATH = join(__dirname, "snapshot-generator-tools/ndk-build"); const BUNDLE_PREAMBLE_PATH = join(__dirname, "snapshot-generator-tools/bundle-preamble.js"); const BUNDLE_ENDING_PATH = join(__dirname, "snapshot-generator-tools/bundle-ending.js"); @@ -14,6 +18,8 @@ const INCLUDE_GRADLE_PATH = join(__dirname, "snapshot-generator-tools/include.gr const MKSNAPSHOT_TOOLS_DOWNLOAD_ROOT_URL = "https://raw.githubusercontent.com/NativeScript/mksnapshot-tools/production/"; const MKSNAPSHOT_TOOLS_DOWNLOAD_TIMEOUT = 60000; const SNAPSHOT_BLOB_NAME = "TNSSnapshot"; +const DOCKER_IMAGE_OS = "linux"; +const DOCKER_IMAGE_ARCH = "x64"; function shellJsExecuteInDir(dir, action) { const currentDir = shelljs.pwd(); @@ -25,17 +31,6 @@ function shellJsExecuteInDir(dir, action) { } } -function getHostOS() { - const hostOS = os.type().toLowerCase(); - if (hostOS.startsWith("darwin")) - return "darwin"; - if (hostOS.startsWith("linux")) - return "linux"; - if (hostOS.startsWith("win")) - return "win"; - return hostOS; -} - function SnapshotGenerator(options) { this.buildPath = options.buildPath || join(__dirname, "build"); } @@ -43,6 +38,24 @@ module.exports = SnapshotGenerator; SnapshotGenerator.SNAPSHOT_PACKAGE_NANE = "nativescript-android-snapshot"; +SnapshotGenerator.prototype.shouldSnapshotInDocker = function (hostOS, targetArchs, currentRuntimeVersion) { + let shouldSnapshotInDocker = false; + const minRuntimeWithoutMacOSSnapshotTools = "6.3.0"; + const generateInDockerMessage = "The snapshots will be generated in a docker container."; + if (hostOS === CONSTANTS.WIN_OS_NAME) { + console.log(`The V8 snapshot tools are not supported on Windows. ${generateInDockerMessage}`); + shouldSnapshotInDocker = true; + } else if (hostOS === CONSTANTS.MAC_OS_NAME && semver.gte(currentRuntimeVersion, minRuntimeWithoutMacOSSnapshotTools)) { + console.log(`Starting from Android Runtime 6.3.0, the Snapshot tools are no longer supported on macOS. ${generateInDockerMessage}`); + shouldSnapshotInDocker = true; + } else if (isMacOSCatalinaOrHigher() && has32BitArch(targetArchs)) { + console.log(`Starting from macOS Catalina, the 32-bit processes are no longer supported. ${generateInDockerMessage}`); + shouldSnapshotInDocker = true; + } + + return shouldSnapshotInDocker; +} + SnapshotGenerator.prototype.preprocessInputFiles = function (inputFiles, outputFile) { // Make some modifcations on the original bundle and save it on the specified path const bundlePreambleContent = fs.readFileSync(BUNDLE_PREAMBLE_PATH, "utf8"); @@ -53,7 +66,7 @@ SnapshotGenerator.prototype.preprocessInputFiles = function (inputFiles, outputF // Example: // (function() { // some code here - // })() + // })() // // sourceMapUrl...... // ** when we join without `;` here, the next IIFE is assumed as a function call to the result of the first IIFE // (function() { @@ -67,9 +80,26 @@ SnapshotGenerator.prototype.preprocessInputFiles = function (inputFiles, outputF const snapshotToolsDownloads = {}; -SnapshotGenerator.prototype.downloadMksnapshotTool = function (snapshotToolsPath, v8Version, targetArch) { - const hostOS = getHostOS(); - const mksnapshotToolRelativePath = join("mksnapshot-tools", "v8-v" + v8Version, hostOS + "-" + os.arch(), "mksnapshot-" + targetArch); +SnapshotGenerator.prototype.downloadMksnapshotTools = function (snapshotToolsPath, v8Version, targetArchs, snapshotInDocker) { + var toolsOS = ""; + var toolsArch = ""; + if (snapshotInDocker) { + toolsOS = DOCKER_IMAGE_OS; + toolsArch = DOCKER_IMAGE_ARCH; + } else { + toolsOS = getHostOS(); + toolsArch = getHostOSArch(); + } + + return Promise.all(targetArchs.map((arch) => { + return this.downloadMksnapshotTool(snapshotToolsPath, v8Version, arch, toolsOS, toolsArch).then(path => { + return { path, arch }; + }); + })); +} + +SnapshotGenerator.prototype.downloadMksnapshotTool = function (snapshotToolsPath, v8Version, targetArch, hostOS, hostArch) { + const mksnapshotToolRelativePath = join(SNAPSHOT_TOOLS_DIR_NAME, "v8-v" + v8Version, hostOS + "-" + hostArch, "mksnapshot-" + targetArch); const mksnapshotToolPath = join(snapshotToolsPath, mksnapshotToolRelativePath); if (fs.existsSync(mksnapshotToolPath)) return Promise.resolve(mksnapshotToolPath); @@ -101,77 +131,76 @@ SnapshotGenerator.prototype.convertToAndroidArchName = function (archName) { case "arm": return "armeabi-v7a"; case "arm64": return "arm64-v8a"; case "ia32": return "x86"; - case "x64": return "x64"; + case "ia64": return "x86_64"; default: return archName; } } -SnapshotGenerator.prototype.runMksnapshotTool = function (snapshotToolsPath, inputFile, v8Version, targetArchs, buildCSource, mksnapshotParams) { +SnapshotGenerator.prototype.generateSnapshots = function (snapshotToolsPath, inputFile, v8Version, targetArchs, buildCSource, mksnapshotParams, snapshotInDocker) { // Cleans the snapshot build folder shelljs.rm("-rf", join(this.buildPath, "snapshots")); + return this.downloadMksnapshotTools(snapshotToolsPath, v8Version, targetArchs, snapshotInDocker).then((localTools) => { + var shouldDownloadDockerTools = false; + if (!snapshotInDocker) { + snapshotInDocker = localTools.some(tool => !this.canUseSnapshotTool(tool.path)); + shouldDownloadDockerTools = snapshotInDocker; + } - const mksnapshotStdErrPath = join(this.buildPath, "mksnapshot-stderr.txt"); + if (shouldDownloadDockerTools) { + return this.downloadMksnapshotTools(snapshotToolsPath, v8Version, targetArchs, snapshotInDocker).then((dockerTools) => { + console.log(`Generating snapshots in a docker container.`); + return this.runMksnapshotTools(snapshotToolsPath, dockerTools, inputFile, mksnapshotParams, buildCSource, snapshotInDocker); + }); + } - return Promise.all(targetArchs.map((arch) => { - return this.downloadMksnapshotTool(snapshotToolsPath, v8Version, arch).then((currentArchMksnapshotToolPath) => { - if (!fs.existsSync(currentArchMksnapshotToolPath)) { - throw new Error("Can't find mksnapshot tool for " + arch + " at path " + currentArchMksnapshotToolPath); - } + return this.runMksnapshotTools(snapshotToolsPath, localTools, inputFile, mksnapshotParams, buildCSource, snapshotInDocker); + }); +} - const androidArch = this.convertToAndroidArchName(arch); - console.log("***** Generating snapshot for " + androidArch + " *****"); - // Generate .blob file - const currentArchBlobOutputPath = join(this.buildPath, "snapshots/blobs", androidArch); - shelljs.mkdir("-p", currentArchBlobOutputPath); - var params = "--profile_deserialization"; - if (mksnapshotParams) { - // Starting from android runtime 5.3.0, the parameters passed to mksnapshot are read from the settings.json file - params = mksnapshotParams; - } - const command = `${currentArchMksnapshotToolPath} ${inputFile} --startup_blob ${join(currentArchBlobOutputPath, `${SNAPSHOT_BLOB_NAME}.blob`)} ${params}`; - - return new Promise((resolve, reject) => { - const child = child_process.exec(command, { encoding: "utf8" }, (error, stdout, stderr) => { - const errorHeader = `Target architecture: ${androidArch}\n`; - let errorFooter = ``; - if (stderr.length || error) { - try { - require(inputFile); - } catch (e) { - errorFooter = `\nJavaScript execution error: ${e.stack}$`; - } - } - - if (stderr.length) { - const message = `${errorHeader}${stderr}${errorFooter}`; - reject(new Error(message)); - } else if (error) { - error.message = `${errorHeader}${error.message}${errorFooter}`; - reject(error); - } else { - console.log(stdout); - resolve(); - } - }) - }).then(() => { - // Generate .c file - if (buildCSource) { - const currentArchSrcOutputPath = join(this.buildPath, "snapshots/src", androidArch); - shelljs.mkdir("-p", currentArchSrcOutputPath); - shellJsExecuteInDir(currentArchBlobOutputPath, function () { - shelljs.exec(`xxd -i ${SNAPSHOT_BLOB_NAME}.blob > ${join(currentArchSrcOutputPath, `${SNAPSHOT_BLOB_NAME}.c`)}`); - }); - } +SnapshotGenerator.prototype.runMksnapshotTools = function (snapshotToolsBasePath, snapshotTools, inputFile, mksnapshotParams, buildCSource, snapshotInDocker) { + let currentSnapshotOperation = Promise.resolve(); + const canRunInParallel = !snapshotInDocker; + return Promise.all(snapshotTools.map((tool) => { + if (canRunInParallel) { + return this.runMksnapshotTool(tool, mksnapshotParams, inputFile, snapshotInDocker, snapshotToolsBasePath, buildCSource); + } else { + currentSnapshotOperation = currentSnapshotOperation.then(() => { + return this.runMksnapshotTool(tool, mksnapshotParams, inputFile, snapshotInDocker, snapshotToolsBasePath, buildCSource); }); - }); + + return currentSnapshotOperation; + } })).then(() => { console.log("***** Finished generating snapshots. *****"); }); } -SnapshotGenerator.prototype.buildSnapshotLibs = function (androidNdkBuildPath, targetArchs) { +SnapshotGenerator.prototype.canUseSnapshotTool = function (snapshotToolPath) { + try { + child_process.execSync(`${snapshotToolPath} --help`); + return true; + } + catch (error) { + console.log(`Unable to execute '${snapshotToolPath}' locally.Error message: '${error.message}'`); + return false; + } +} + +SnapshotGenerator.prototype.setupDocker = function () { + try { + child_process.execSync(`docker --version`); + } + catch (error) { + throw new Error(`Docker installation cannot be found. Install Docker and add it to your PATH in order to build snapshots.`); + } + + child_process.execSync(`docker pull ${SNAPSHOTS_DOCKER_IMAGE}`); +} + +SnapshotGenerator.prototype.buildSnapshotLibs = function (androidNdkPath, recommendedAndroidNdkRevision, targetArchs) { // Compile *.c files to produce *.so libraries with ndk-build tool + const androidNdkBuildPath = this.getAndroidNdkBuildPath(androidNdkPath, recommendedAndroidNdkRevision); const ndkBuildPath = join(this.buildPath, "ndk-build"); const androidArchs = targetArchs.map(arch => this.convertToAndroidArchName(arch)); console.log("Building native libraries for " + androidArchs.join()); @@ -185,6 +214,78 @@ SnapshotGenerator.prototype.buildSnapshotLibs = function (androidNdkBuildPath, t return join(ndkBuildPath, "libs"); } +SnapshotGenerator.prototype.getAndroidNdkBuildPath = function (androidNdkPath, recommendedAndroidNdkRevision) { + const ndkBuildExecutableName = isWindows() ? "ndk-build.cmd" : "ndk-build"; + let hasNdk = false; + // fallback for Android Runtime < 6.2.0 with the 6.1.0 value + recommendedAndroidNdkRevision = recommendedAndroidNdkRevision || "20.0.5594570"; + let androidNdkBuildPath = ""; + if (androidNdkPath) { + // specified by the user + const localNdkRevision = this.getAndroidNdkRevision(androidNdkPath); + androidNdkBuildPath = join(androidNdkPath, ndkBuildExecutableName); + if (!fs.existsSync(androidNdkBuildPath)) { + throw new Error(`The provided Android NDK path does not contain ${ndkBuildExecutableName} executable.`); + } else if (localNdkRevision !== recommendedAndroidNdkRevision) { + warn(this.getRecommendedNdkWarning(localNdkRevision, recommendedAndroidNdkRevision)); + } + + hasNdk = true; + console.log("Using Android NDK from webpack.config."); + } else { + if (process.env.ANDROID_NDK_HOME) { + // check ANDROID_NDK_HOME + const localNdkRevision = this.getAndroidNdkRevision(process.env.ANDROID_NDK_HOME); + androidNdkBuildPath = join(process.env.ANDROID_NDK_HOME, ndkBuildExecutableName); + if (fs.existsSync(androidNdkBuildPath)) { + hasNdk = true; + console.log("Using Android NDK from ANDROID_NDK_HOME."); + } + + if (localNdkRevision !== recommendedAndroidNdkRevision) { + warn(this.getRecommendedNdkWarning(localNdkRevision, recommendedAndroidNdkRevision)); + } + } + + if (!hasNdk) { + // available globally + androidNdkBuildPath = ndkBuildExecutableName; + try { + child_process.execSync(`${androidNdkBuildPath} --version`, { stdio: "ignore" }); + hasNdk = true; + console.log("Using Android NDK from PATH."); + console.log(`Cannot determine the version of the global Android NDK. The recommended versions is v${recommendedAndroidNdkRevision}`); + } catch (_) { + } + } + + if (!hasNdk) { + // installed in ANDROID_HOME + androidNdkBuildPath = join(process.env.ANDROID_HOME, "ndk", recommendedAndroidNdkRevision, ndkBuildExecutableName); + if (fs.existsSync(androidNdkBuildPath)) { + hasNdk = true; + console.log("Using Android NDK from ANDROID_HOME."); + } + } + } + + if (!hasNdk) { + throw new Error(`Android NDK v${recommendedAndroidNdkRevision} is not installed. Install it from Android Studio or download it and set ANDROID_NDK_HOME or add it to your PATH. You can find installation instructions in this article: https://developer.android.com/studio/projects/install-ndk#specific-version`); + } + + return androidNdkBuildPath; +} + +SnapshotGenerator.prototype.getAndroidNdkRevision = function (androidNdkPath) { + const ndkPropertiesFile = join(androidNdkPath, "source.properties"); + if (fs.existsSync(ndkPropertiesFile)) { + const properties = PropertiesReader(ndkPropertiesFile); + return properties.get("Pkg.Revision"); + } else { + return null; + } +} + SnapshotGenerator.prototype.buildIncludeGradle = function () { shelljs.cp(INCLUDE_GRADLE_PATH, join(this.buildPath, "include.gradle")); } @@ -199,21 +300,169 @@ SnapshotGenerator.prototype.generate = function (options) { console.log("***** Starting snapshot generation using V8 version: ", options.v8Version); this.preprocessInputFiles(options.inputFiles, preprocessedInputFile); + const hostOS = getHostOS(); + const snapshotInDocker = options.snapshotInDocker || this.shouldSnapshotInDocker(hostOS, options.targetArchs, options.runtimeVersion); // generates the actual .blob and .c files - return this.runMksnapshotTool( + return this.generateSnapshots( options.snapshotToolsPath, preprocessedInputFile, options.v8Version, options.targetArchs, options.useLibs, - options.mksnapshotParams + options.mksnapshotParams, + snapshotInDocker ).then(() => { this.buildIncludeGradle(); if (options.useLibs) { - const androidNdkBuildPath = options.androidNdkPath ? join(options.androidNdkPath, "ndk-build") : "ndk-build"; - this.buildSnapshotLibs(androidNdkBuildPath, options.targetArchs); + this.buildSnapshotLibs(options.androidNdkPath, options.recommendedAndroidNdkRevision, options.targetArchs); } return this.buildPath; }); } + +SnapshotGenerator.prototype.getSnapshotToolCommand = function (snapshotToolPath, inputFilePath, outputPath, toolParams) { + return `${snapshotToolPath} ${inputFilePath} --startup_blob ${outputPath} ${toolParams}`; +} + +SnapshotGenerator.prototype.getXxdCommand = function (srcOutputDir, xxdLocation) { + // https://github.com/NativeScript/docker-images/tree/master/v8-snapshot/bin + return `${xxdLocation || ""}xxd -i ${SNAPSHOT_BLOB_NAME}.blob > ${srcOutputDir}`; +} + +SnapshotGenerator.prototype.getPathInDocker = function (mappedLocalDir, mappedDockerDir, targetPath) { + if (!isSubPath(mappedLocalDir, targetPath)) { + throw new Error(`Cannot determine a docker path. '${targetPath}' should be inside '${mappedLocalDir}'`) + } + + const pathInDocker = join(mappedDockerDir, relative(mappedLocalDir, targetPath)); + + return convertToUnixPath(pathInDocker); +} + +SnapshotGenerator.prototype.handleSnapshotToolResult = function (error, stdout, stderr, inputFile, androidArch) { + let toolError = null; + const errorHeader = `Target architecture: ${androidArch}\n`; + let errorFooter = ``; + if ((stderr && stderr.length) || error) { + try { + require(inputFile); + } + catch (e) { + errorFooter = `\nJavaScript execution error: ${e.stack}$`; + } + } + + if (stderr && stderr.length) { + const message = `${errorHeader}${stderr}${errorFooter}`; + toolError = new Error(message); + } + else if (error) { + error.message = `${errorHeader}${error.message}${errorFooter}`; + toolError = error; + } else { + console.log(stdout); + } + + return toolError; +} + +SnapshotGenerator.prototype.copySnapshotTool = function (allToolsDir, targetTool, destinationDir) { + // we cannot mount the source tools folder as its not shared by default: + // docker: Error response from daemon: Mounts denied: + // The path /var/folders/h2/1yck52fx2mg7c790vhcw90s8087sk8/T/snapshot-tools/mksnapshot-tools + // is not shared from OS X and is not known to Docker. + const toolPathRelativeToAllToolsDir = relative(allToolsDir, targetTool); + const toolDestinationPath = join(destinationDir, toolPathRelativeToAllToolsDir) + createDirectory(dirname(toolDestinationPath)); + shelljs.cp(targetTool, toolDestinationPath); + + return toolDestinationPath; +} + +SnapshotGenerator.prototype.buildCSource = function (androidArch, blobInputDir, snapshotInDocker) { + const srcOutputDir = join(this.buildPath, "snapshots/src", androidArch); + createDirectory(srcOutputDir); + let command = ""; + if (snapshotInDocker) { + const blobsInputInDocker = `/blobs/${androidArch}` + const srcOutputDirInDocker = `/dist/src/${androidArch}`; + const outputPathInDocker = this.getPathInDocker(srcOutputDir, srcOutputDirInDocker, join(srcOutputDir, `${SNAPSHOT_BLOB_NAME}.c`)); + const buildCSourceCommand = this.getXxdCommand(outputPathInDocker, "/bin/"); + command = `docker run --rm -v "${blobInputDir}:${blobsInputInDocker}" -v "${srcOutputDir}:${srcOutputDirInDocker}" ${SNAPSHOTS_DOCKER_IMAGE} /bin/sh -c "cd ${blobsInputInDocker} && ${buildCSourceCommand}"`; + } + else { + command = this.getXxdCommand(join(srcOutputDir, `${SNAPSHOT_BLOB_NAME}.c`)); + } + shellJsExecuteInDir(blobInputDir, function () { + shelljs.exec(command); + }); +} + +SnapshotGenerator.prototype.getRecommendedNdkWarning = function (localNdkRevision, recommendedAndroidNdkRevision) { + if (localNdkRevision) { + return `The provided Android NDK is v${localNdkRevision} while the required one is v${recommendedAndroidNdkRevision}`; + } else { + return `The provided Android NDK version is different than the required one - v${recommendedAndroidNdkRevision}`; + } +} + +SnapshotGenerator.prototype.runMksnapshotTool = function (tool, mksnapshotParams, inputFile, snapshotInDocker, snapshotToolsPath, buildCSource) { + const toolPath = tool.path; + const androidArch = this.convertToAndroidArchName(tool.arch); + if (!fs.existsSync(toolPath)) { + throw new Error(`Can't find mksnapshot tool for ${androidArch} at path ${toolPath}`); + } + + const tempFolders = []; + return new Promise((resolve, reject) => { + console.log("***** Generating snapshot for " + androidArch + " *****"); + const inputFileDir = dirname(inputFile); + const blobOutputDir = join(this.buildPath, "snapshots/blobs", androidArch); + createDirectory(blobOutputDir); + const toolParams = mksnapshotParams || "--profile_deserialization"; + + let command = ""; + if (snapshotInDocker) { + this.setupDocker(); + const appDirInDocker = "/app"; + const blobOutputDirInDocker = `/dist/blobs/${androidArch}`; + const toolsTempFolder = join(inputFileDir, "tmp"); + tempFolders.push(toolsTempFolder); + const toolPathInAppDir = this.copySnapshotTool(snapshotToolsPath, toolPath, toolsTempFolder); + const toolPathInDocker = this.getPathInDocker(inputFileDir, appDirInDocker, toolPathInAppDir); + const inputFilePathInDocker = this.getPathInDocker(inputFileDir, appDirInDocker, inputFile); + const outputPathInDocker = this.getPathInDocker(blobOutputDir, blobOutputDirInDocker, join(blobOutputDir, `${SNAPSHOT_BLOB_NAME}.blob`)); + const toolCommandInDocker = this.getSnapshotToolCommand(toolPathInDocker, inputFilePathInDocker, outputPathInDocker, toolParams); + command = `docker run --rm -v "${inputFileDir}:${appDirInDocker}" -v "${blobOutputDir}:${blobOutputDirInDocker}" ${SNAPSHOTS_DOCKER_IMAGE} /bin/sh -c "${toolCommandInDocker}"`; + } else { + command = this.getSnapshotToolCommand(toolPath, inputFile, join(blobOutputDir, `${SNAPSHOT_BLOB_NAME}.blob`), toolParams); + } + + // Generate .blob file + child_process.exec(command, { encoding: "utf8" }, (error, stdout, stderr) => { + tempFolders.forEach(tempFolder => { + shelljs.rm("-rf", tempFolder); + }); + + const snapshotError = this.handleSnapshotToolResult(error, stdout, stderr, inputFile, androidArch); + if (snapshotError) { + return reject(snapshotError); + } + + return resolve(blobOutputDir); + }); + }).then((blobOutputDir) => { + // Generate .c file + if (buildCSource) { + this.buildCSource(androidArch, blobOutputDir, snapshotInDocker) + } + + /* + Rename TNSSnapshot.blob files to snapshot.blob files. The xxd tool uses the file name for the name of the static array. + This is why the *.blob files are initially named TNSSnapshot.blob. + After the xxd step, they must be renamed to snapshot.blob, because this is the filename that the Android runtime is looking for. + */ + shelljs.mv(join(blobOutputDir, `${SNAPSHOT_BLOB_NAME}.blob`), join(blobOutputDir, `snapshot.blob`)); + }); +} diff --git a/snapshot/android/utils.js b/snapshot/android/utils.js index 2b63ae28..947f0302 100644 --- a/snapshot/android/utils.js +++ b/snapshot/android/utils.js @@ -1,17 +1,69 @@ -const { chmodSync, createWriteStream, existsSync } = require("fs"); +const { chmodSync, createWriteStream } = require("fs"); const { tmpdir, EOL } = require("os"); -const { dirname, join } = require("path"); +const { join, relative, isAbsolute } = require("path"); +const os = require("os"); const { mkdir } = require("shelljs"); const { get } = require("request"); const { getProxySettings } = require("proxy-lib"); +const semver = require("semver"); const CONSTANTS = { SNAPSHOT_TMP_DIR: join(tmpdir(), "snapshot-tools"), + MAC_OS_NAME: "darwin", + WIN_OS_NAME: "win", + LINUX_OS_NAME: "linux" }; const createDirectory = dir => mkdir('-p', dir); +function getHostOS() { + const hostOS = os.type().toLowerCase(); + if (hostOS.startsWith(CONSTANTS.MAC_OS_NAME)) + return CONSTANTS.MAC_OS_NAME; + if (hostOS.startsWith(CONSTANTS.LINUX_OS_NAME)) + return CONSTANTS.LINUX_OS_NAME; + if (hostOS.startsWith(CONSTANTS.WIN_OS_NAME)) + return CONSTANTS.WIN_OS_NAME; + return hostOS; +} + +function getHostOSVersion() { + return os.release(); +} + +function getHostOSArch() { + return os.arch(); +} + +function has32BitArch(targetArchs) { + return (Array.isArray(targetArchs) && targetArchs.some(arch => arch === "arm" || arch === "ia32")) || + (targetArchs === "arm" || targetArchs === "ia32"); +} + +function isSubPath(parentPath, childPath) { + const relativePath = relative(parentPath, childPath); + + return relativePath === "" || + (relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath)); +} + +function isMacOSCatalinaOrHigher() { + let isCatalinaOrHigher = false; + const catalinaVersion = "19.0.0"; + const hostOS = getHostOS(); + if (hostOS === CONSTANTS.MAC_OS_NAME) { + const hostOSVersion = getHostOSVersion(); + isCatalinaOrHigher = semver.gte(hostOSVersion, catalinaVersion); + } + + return isCatalinaOrHigher; +} + +function isWindows() { + return getHostOS() === CONSTANTS.WIN_OS_NAME; +} + const downloadFile = (url, destinationFilePath, timeout) => new Promise((resolve, reject) => { getRequestOptions(url, timeout) @@ -64,6 +116,13 @@ const getRequestOptions = (url, timeout) => module.exports = { CONSTANTS, createDirectory, + has32BitArch, + getHostOS, + getHostOSVersion, + getHostOSArch, + isMacOSCatalinaOrHigher, downloadFile, getJsonFile, + isSubPath, + isWindows }; diff --git a/templates/tsconfig.tns.json b/templates/tsconfig.tns.json index 95f2ecee..9ce50ed9 100644 --- a/templates/tsconfig.tns.json +++ b/templates/tsconfig.tns.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig", "compilerOptions": { - "module": "es2015", + "module": "esNext", "moduleResolution": "node" } } diff --git a/templates/webpack.angular.js b/templates/webpack.angular.js index e6110ff0..f0b43bbe 100644 --- a/templates/webpack.angular.js +++ b/templates/webpack.angular.js @@ -7,6 +7,7 @@ const { nsReplaceBootstrap } = require("nativescript-dev-webpack/transformers/ns const { nsReplaceLazyLoader } = require("nativescript-dev-webpack/transformers/ns-replace-lazy-loader"); const { nsSupportHmrNg } = require("nativescript-dev-webpack/transformers/ns-support-hmr-ng"); const { getMainModulePath } = require("nativescript-dev-webpack/utils/ast-utils"); +const { getNoEmitOnErrorFromTSConfig, getCompilerOptionsFromTSConfig } = require("nativescript-dev-webpack/utils/tsconfig-utils"); const CleanWebpackPlugin = require("clean-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); @@ -17,12 +18,13 @@ const hashSalt = Date.now().toString(); module.exports = env => { // Add your custom Activities, Services and other Android app components here. - const appComponents = [ + const appComponents = env.appComponents || []; + appComponents.push(...[ "tns-core-modules/ui/frame", "tns-core-modules/ui/frame/activity", - ]; + ]); - const platform = env && (env.android && "android" || env.ios && "ios"); + const platform = env && (env.android && "android" || env.ios && "ios" || env.platform); if (!platform) { throw new Error("You need to provide a target platform!"); } @@ -50,16 +52,41 @@ module.exports = env => { hmr, // --env.hmr, unitTesting, // --env.unitTesting verbose, // --env.verbose + snapshotInDocker, // --env.snapshotInDocker + skipSnapshotTools, // --env.skipSnapshotTools + compileSnapshot // --env.compileSnapshot } = env; + const useLibs = compileSnapshot; const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; const externals = nsWebpack.getConvertedExternals(env.externals); const appFullPath = resolve(projectRoot, appPath); - const appResourcesFullPath = resolve(projectRoot, appResourcesPath); const tsConfigName = "tsconfig.tns.json"; + const tsConfigPath = join(__dirname, tsConfigName); + const hasRootLevelScopedModules = nsWebpack.hasRootLevelScopedModules({ projectDir: projectRoot }); + const hasRootLevelScopedAngular = nsWebpack.hasRootLevelScopedAngular({ projectDir: projectRoot }); + let coreModulesPackageName = "tns-core-modules"; + const alias = env.alias || {}; + alias['~'] = appFullPath; + + const compilerOptions = getCompilerOptionsFromTSConfig(tsConfigPath); + if (hasRootLevelScopedModules) { + coreModulesPackageName = "@nativescript/core"; + alias["tns-core-modules"] = coreModulesPackageName; + nsWebpack.processTsPathsForScopedModules({ compilerOptions }); + } + + if (hasRootLevelScopedAngular) { + alias["nativescript-angular"] = "@nativescript/angular"; + nsWebpack.processTsPathsForScopedAngular({ compilerOptions }); + } + + const appResourcesFullPath = resolve(projectRoot, appResourcesPath); const entryModule = `${nsWebpack.getEntryModule(appFullPath, platform)}.ts`; const entryPath = `.${sep}${entryModule}`; - const entries = { bundle: entryPath }; + const entries = env.entries || {}; + entries.bundle = entryPath; + const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("tns-core-modules") > -1); if (platform === "ios" && !areCoreModulesExternal) { entries["tns_modules/tns-core-modules/inspector_modules"] = "inspector_modules"; @@ -93,10 +120,11 @@ module.exports = env => { hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]), platformTransformers: ngCompilerTransformers.map(t => t(() => ngCompilerPlugin, resolve(appFullPath, entryModule), projectRoot)), mainPath: join(appFullPath, entryModule), - tsConfigPath: join(__dirname, tsConfigName), + tsConfigPath, skipCodeGeneration: !aot, sourceMap: !!isAnySourceMapEnabled, - additionalLazyModuleResources: additionalLazyModuleResources + additionalLazyModuleResources: additionalLazyModuleResources, + compilerOptions: { paths: compilerOptions.paths } }); let sourceMapFilename = nsWebpack.getSourceMapFilename(hiddenSourceMap, __dirname, dist); @@ -107,6 +135,8 @@ module.exports = env => { itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "build", "configurations", "nativescript-android-snapshot")}`); } + const noEmitOnErrorFromTSConfig = getNoEmitOnErrorFromTSConfig(join(projectRoot, tsConfigName)); + nsWebpack.processAppComponents(appComponents, platform); const config = { mode: production ? "production" : "development", @@ -134,14 +164,12 @@ module.exports = env => { extensions: [".ts", ".js", ".scss", ".css"], // Resolve {N} system modules from tns-core-modules modules: [ - resolve(__dirname, "node_modules/tns-core-modules"), + resolve(__dirname, `node_modules/${coreModulesPackageName}`), resolve(__dirname, "node_modules"), - "node_modules/tns-core-modules", + `node_modules/${coreModulesPackageName}`, "node_modules", ], - alias: { - '~': appFullPath - }, + alias, symlinks: true }, resolveLoader: { @@ -158,6 +186,7 @@ module.exports = env => { devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), optimization: { runtimeChunk: "single", + noEmitOnErrors: noEmitOnErrorFromTSConfig, splitChunks: { cacheGroups: { vendor: { @@ -220,19 +249,24 @@ module.exports = env => { { test: /\.html$|\.xml$/, use: "raw-loader" }, - // tns-core-modules reads the app.css and its imports using css-loader { test: /[\/|\\]app\.css$/, use: [ "nativescript-dev-webpack/style-hot-loader", - { loader: "css-loader", options: { url: false } } + { + loader: "nativescript-dev-webpack/css2json-loader", + options: { useForImports: true } + } ] }, { test: /[\/|\\]app\.scss$/, use: [ "nativescript-dev-webpack/style-hot-loader", - { loader: "css-loader", options: { url: false } }, + { + loader: "nativescript-dev-webpack/css2json-loader", + options: { useForImports: true } + }, "sass-loader" ] }, @@ -308,6 +342,9 @@ module.exports = env => { ], projectRoot, webpackConfig: config, + snapshotInDocker, + skipSnapshotTools, + useLibs })); } diff --git a/templates/webpack.config.spec.ts b/templates/webpack.config.spec.ts index 024461bd..8d62b062 100644 --- a/templates/webpack.config.spec.ts +++ b/templates/webpack.config.spec.ts @@ -1,7 +1,6 @@ import * as proxyquire from 'proxyquire'; import * as nsWebpackIndex from '../index'; import { join } from 'path'; -import { skipPartiallyEmittedExpressions } from 'typescript'; // With noCallThru enabled, `proxyquire` will not fall back to requiring the real module to populate properties that are not mocked. // This allows us to mock packages that are not available in node_modules. // In case you want to enable fallback for a specific object, just add `'@noCallThru': false`. @@ -30,9 +29,15 @@ const nativeScriptDevWebpack = { PlatformFSPlugin: EmptyClass, getAppPath: () => 'app', getEntryModule: () => 'EntryModule', + hasRootLevelScopedModules: () => false, + hasRootLevelScopedAngular: () => false, + processTsPathsForScopedModules: () => false, + processTsPathsForScopedAngular: () => false, getResolver: () => null, getConvertedExternals: nsWebpackIndex.getConvertedExternals, - getSourceMapFilename: nsWebpackIndex.getSourceMapFilename + getSourceMapFilename: nsWebpackIndex.getSourceMapFilename, + processAppComponents: nsWebpackIndex.processAppComponents, + getUserDefinedEntries: nsWebpackIndex.getUserDefinedEntries, }; const emptyObject = {}; @@ -46,6 +51,7 @@ const webpackConfigAngular = proxyquire('./webpack.angular', { 'nativescript-dev-webpack/transformers/ns-replace-lazy-loader': { nsReplaceLazyLoader: () => { return FakeLazyTransformerFlag } }, 'nativescript-dev-webpack/transformers/ns-support-hmr-ng': { nsSupportHmrNg: () => { return FakeHmrTransformerFlag } }, 'nativescript-dev-webpack/utils/ast-utils': { getMainModulePath: () => { return "fakePath"; } }, + 'nativescript-dev-webpack/utils/tsconfig-utils': { getNoEmitOnErrorFromTSConfig: () => { return false; }, getCompilerOptionsFromTSConfig: () => { return false; } }, 'nativescript-dev-webpack/plugins/NativeScriptAngularCompilerPlugin': { getAngularCompilerPlugin: () => { return AngularCompilerStub; } }, '@ngtools/webpack': { AngularCompilerPlugin: AngularCompilerStub @@ -56,6 +62,7 @@ const webpackConfigAngular = proxyquire('./webpack.angular', { const webpackConfigTypeScript = proxyquire('./webpack.typescript', { 'nativescript-dev-webpack': nativeScriptDevWebpack, 'nativescript-dev-webpack/nativescript-target': emptyObject, + 'nativescript-dev-webpack/utils/tsconfig-utils': { getNoEmitOnErrorFromTSConfig: () => { return false; }, getCompilerOptionsFromTSConfig: () => { return false; } }, 'terser-webpack-plugin': TerserJsStub }); @@ -354,6 +361,29 @@ describe('webpack.config.js', () => { expect(config.output.sourceMapFilename).toEqual(join("..", newSourceMapFolder, "[file].map")); }); }); + + describe(`alias for webpack.${type}.js (${platform})`, () => { + it('should add alias when @nativescript/core is at the root of node_modules', () => { + nativeScriptDevWebpack.hasRootLevelScopedModules = () => true; + nativeScriptDevWebpack.hasRootLevelScopedAngular = () => true; + const input = getInput({ platform }); + const config = webpackConfig(input); + expect(config.resolve.alias['tns-core-modules']).toBe('@nativescript/core'); + if (type === 'angular') { + expect(config.resolve.alias['nativescript-angular']).toBe('@nativescript/angular'); + } + }); + it('shouldn\'t add alias when @nativescript/core is not at the root of node_modules', () => { + nativeScriptDevWebpack.hasRootLevelScopedModules = () => false; + nativeScriptDevWebpack.hasRootLevelScopedAngular = () => false; + const input = getInput({ platform }); + const config = webpackConfig(input); + expect(config.resolve.alias['tns-core-modules']).toBeUndefined(); + if (type === 'angular') { + expect(config.resolve.alias['nativescript-angular']).toBeUndefined(); + } + }); + }); }); }); }); \ No newline at end of file diff --git a/templates/webpack.javascript.js b/templates/webpack.javascript.js index 7460dd2a..59360c38 100644 --- a/templates/webpack.javascript.js +++ b/templates/webpack.javascript.js @@ -12,12 +12,13 @@ const hashSalt = Date.now().toString(); module.exports = env => { // Add your custom Activities, Services and other android app components here. - const appComponents = [ + const appComponents = env.appComponents || []; + appComponents.push(...[ "tns-core-modules/ui/frame", "tns-core-modules/ui/frame/activity", - ]; + ]); - const platform = env && (env.android && "android" || env.ios && "ios"); + const platform = env && (env.android && "android" || env.ios && "ios" || env.platform); if (!platform) { throw new Error("You need to provide a target platform!"); } @@ -25,6 +26,10 @@ module.exports = env => { const platforms = ["ios", "android"]; const projectRoot = __dirname; + if (env.platform) { + platforms.push(env.platform); + } + // Default destination inside platforms//... const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); @@ -44,16 +49,31 @@ module.exports = env => { hmr, // --env.hmr, unitTesting, // --env.unitTesting, verbose, // --env.verbose + snapshotInDocker, // --env.snapshotInDocker + skipSnapshotTools, // --env.skipSnapshotTools + compileSnapshot // --env.compileSnapshot } = env; + const useLibs = compileSnapshot; const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; const externals = nsWebpack.getConvertedExternals(env.externals); const appFullPath = resolve(projectRoot, appPath); + const hasRootLevelScopedModules = nsWebpack.hasRootLevelScopedModules({ projectDir: projectRoot }); + let coreModulesPackageName = "tns-core-modules"; + const alias = env.alias || {}; + alias['~'] = appFullPath; + + if (hasRootLevelScopedModules) { + coreModulesPackageName = "@nativescript/core"; + alias["tns-core-modules"] = coreModulesPackageName; + } const appResourcesFullPath = resolve(projectRoot, appResourcesPath); const entryModule = nsWebpack.getEntryModule(appFullPath, platform); const entryPath = `.${sep}${entryModule}.js`; - const entries = { bundle: entryPath }; + const entries = env.entries || {}; + entries.bundle = entryPath; + const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("tns-core-modules") > -1); if (platform === "ios" && !areCoreModulesExternal) { entries["tns_modules/tns-core-modules/inspector_modules"] = "inspector_modules"; @@ -95,14 +115,12 @@ module.exports = env => { extensions: [".js", ".scss", ".css"], // Resolve {N} system modules from tns-core-modules modules: [ - resolve(__dirname, "node_modules/tns-core-modules"), + resolve(__dirname, `node_modules/${coreModulesPackageName}`), resolve(__dirname, "node_modules"), - "node_modules/tns-core-modules", + `node_modules/${coreModulesPackageName}`, "node_modules", ], - alias: { - '~': appFullPath - }, + alias, // resolve symlinks to symlinked modules symlinks: true }, @@ -121,6 +139,7 @@ module.exports = env => { devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), optimization: { runtimeChunk: "single", + noEmitOnErrors: true, splitChunks: { cacheGroups: { vendor: { @@ -190,13 +209,13 @@ module.exports = env => { { test: /\.css$/, - use: { loader: "css-loader", options: { url: false } } + use: "nativescript-dev-webpack/css2json-loader" }, { test: /\.scss$/, use: [ - { loader: "css-loader", options: { url: false } }, + "nativescript-dev-webpack/css2json-loader", "sass-loader" ] }, @@ -249,6 +268,9 @@ module.exports = env => { ], projectRoot, webpackConfig: config, + snapshotInDocker, + skipSnapshotTools, + useLibs })); } diff --git a/templates/webpack.typescript.js b/templates/webpack.typescript.js index 44a60a10..35c4fe65 100644 --- a/templates/webpack.typescript.js +++ b/templates/webpack.typescript.js @@ -3,6 +3,7 @@ const { join, relative, resolve, sep } = require("path"); const webpack = require("webpack"); const nsWebpack = require("nativescript-dev-webpack"); const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target"); +const { getNoEmitOnErrorFromTSConfig } = require("nativescript-dev-webpack/utils/tsconfig-utils"); const CleanWebpackPlugin = require("clean-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); @@ -13,12 +14,13 @@ const hashSalt = Date.now().toString(); module.exports = env => { // Add your custom Activities, Services and other Android app components here. - const appComponents = [ + const appComponents = env.appComponents || []; + appComponents.push(...[ "tns-core-modules/ui/frame", "tns-core-modules/ui/frame/activity", - ]; + ]); - const platform = env && (env.android && "android" || env.ios && "ios"); + const platform = env && (env.android && "android" || env.ios && "ios" || env.platform); if (!platform) { throw new Error("You need to provide a target platform!"); } @@ -26,6 +28,10 @@ module.exports = env => { const platforms = ["ios", "android"]; const projectRoot = __dirname; + if (env.platform) { + platforms.push(env.platform); + } + // Default destination inside platforms//... const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); @@ -45,16 +51,31 @@ module.exports = env => { hmr, // --env.hmr, unitTesting, // --env.unitTesting, verbose, // --env.verbose + snapshotInDocker, // --env.snapshotInDocker + skipSnapshotTools, // --env.skipSnapshotTools + compileSnapshot // --env.compileSnapshot } = env; + + const useLibs = compileSnapshot; const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; const externals = nsWebpack.getConvertedExternals(env.externals); const appFullPath = resolve(projectRoot, appPath); + const hasRootLevelScopedModules = nsWebpack.hasRootLevelScopedModules({ projectDir: projectRoot }); + let coreModulesPackageName = "tns-core-modules"; + const alias = env.alias || {}; + alias['~'] = appFullPath; + + if (hasRootLevelScopedModules) { + coreModulesPackageName = "@nativescript/core"; + alias["tns-core-modules"] = coreModulesPackageName; + } const appResourcesFullPath = resolve(projectRoot, appResourcesPath); const entryModule = nsWebpack.getEntryModule(appFullPath, platform); const entryPath = `.${sep}${entryModule}.ts`; - const entries = { bundle: entryPath }; + const entries = env.entries || {}; + entries.bundle = entryPath; const tsConfigPath = resolve(projectRoot, "tsconfig.tns.json"); @@ -71,6 +92,8 @@ module.exports = env => { itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "build", "configurations", "nativescript-android-snapshot")}`); } + const noEmitOnErrorFromTSConfig = getNoEmitOnErrorFromTSConfig(tsConfigPath); + nsWebpack.processAppComponents(appComponents, platform); const config = { mode: production ? "production" : "development", @@ -98,14 +121,12 @@ module.exports = env => { extensions: [".ts", ".js", ".scss", ".css"], // Resolve {N} system modules from tns-core-modules modules: [ - resolve(__dirname, "node_modules/tns-core-modules"), + resolve(__dirname, `node_modules/${coreModulesPackageName}`), resolve(__dirname, "node_modules"), - "node_modules/tns-core-modules", + `node_modules/${coreModulesPackageName}`, "node_modules", ], - alias: { - '~': appFullPath - }, + alias, // resolve symlinks to symlinked modules symlinks: true }, @@ -124,6 +145,7 @@ module.exports = env => { devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), optimization: { runtimeChunk: "single", + noEmitOnErrors: noEmitOnErrorFromTSConfig, splitChunks: { cacheGroups: { vendor: { @@ -183,7 +205,7 @@ module.exports = env => { }, ].filter(loader => !!loader) }, - + { test: /\.(ts|css|scss|html|xml)$/, use: "nativescript-dev-webpack/hmr/hot-loader" @@ -193,13 +215,13 @@ module.exports = env => { { test: /\.css$/, - use: { loader: "css-loader", options: { url: false } } + use: "nativescript-dev-webpack/css2json-loader" }, { test: /\.scss$/, use: [ - { loader: "css-loader", options: { url: false } }, + "nativescript-dev-webpack/css2json-loader", "sass-loader" ] }, @@ -253,6 +275,7 @@ module.exports = env => { tsconfig: tsConfigPath, async: false, useTypescriptIncrementalApi: true, + checkSyntacticErrors: true, memoryLimit: 4096 }) ], @@ -277,6 +300,9 @@ module.exports = env => { ], projectRoot, webpackConfig: config, + snapshotInDocker, + skipSnapshotTools, + useLibs })); } diff --git a/templates/webpack.vue.js b/templates/webpack.vue.js index 28bbfe9f..16339117 100644 --- a/templates/webpack.vue.js +++ b/templates/webpack.vue.js @@ -16,12 +16,13 @@ const hashSalt = Date.now().toString(); module.exports = env => { // Add your custom Activities, Services and other android app components here. - const appComponents = [ + const appComponents = env.appComponents || []; + appComponents.push(...[ "tns-core-modules/ui/frame", "tns-core-modules/ui/frame/activity", - ]; + ]); - const platform = env && (env.android && "android" || env.ios && "ios"); + const platform = env && (env.android && "android" || env.ios && "ios" || env.platform); if (!platform) { throw new Error("You need to provide a target platform!"); } @@ -29,6 +30,10 @@ module.exports = env => { const platforms = ["ios", "android"]; const projectRoot = __dirname; + if (env.platform) { + platforms.push(env.platform); + } + // Default destination inside platforms//... const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); @@ -47,19 +52,37 @@ module.exports = env => { hiddenSourceMap, // --env.hiddenSourceMap unitTesting, // --env.unitTesting verbose, // --env.verbose + snapshotInDocker, // --env.snapshotInDocker + skipSnapshotTools, // --env.skipSnapshotTools + compileSnapshot // --env.compileSnapshot } = env; + const useLibs = compileSnapshot; const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap; const externals = nsWebpack.getConvertedExternals(env.externals); const mode = production ? "production" : "development" const appFullPath = resolve(projectRoot, appPath); + const hasRootLevelScopedModules = nsWebpack.hasRootLevelScopedModules({ projectDir: projectRoot }); + let coreModulesPackageName = "tns-core-modules"; + const alias = env.alias || {}; + alias['~'] = appFullPath; + alias['@'] = appFullPath; + alias['vue'] = 'nativescript-vue'; + + if (hasRootLevelScopedModules) { + coreModulesPackageName = "@nativescript/core"; + alias["tns-core-modules"] = coreModulesPackageName; + } + const appResourcesFullPath = resolve(projectRoot, appResourcesPath); const entryModule = nsWebpack.getEntryModule(appFullPath, platform); const entryPath = `.${sep}${entryModule}`; - const entries = { bundle: entryPath }; + const entries = env.entries || {}; + entries.bundle = entryPath; + const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("tns-core-modules") > -1); if (platform === "ios" && !areCoreModulesExternal) { entries["tns_modules/tns-core-modules/inspector_modules"] = "inspector_modules"; @@ -102,16 +125,12 @@ module.exports = env => { extensions: [".vue", ".ts", ".js", ".scss", ".css"], // Resolve {N} system modules from tns-core-modules modules: [ - resolve(__dirname, "node_modules/tns-core-modules"), + resolve(__dirname, `node_modules/${coreModulesPackageName}`), resolve(__dirname, "node_modules"), - "node_modules/tns-core-modules", + `node_modules/${coreModulesPackageName}`, "node_modules", ], - alias: { - '~': appFullPath, - '@': appFullPath, - 'vue': 'nativescript-vue' - }, + alias, // resolve symlinks to symlinked modules symlinks: true, }, @@ -130,6 +149,7 @@ module.exports = env => { devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"), optimization: { runtimeChunk: "single", + noEmitOnErrors: true, splitChunks: { cacheGroups: { vendor: { @@ -190,8 +210,30 @@ module.exports = env => { }, ].filter(loader => Boolean(loader)), }, + { + test: /[\/|\\]app\.css$/, + use: [ + 'nativescript-dev-webpack/style-hot-loader', + { + loader: "nativescript-dev-webpack/css2json-loader", + options: { useForImports: true } + }, + ], + }, + { + test: /[\/|\\]app\.scss$/, + use: [ + 'nativescript-dev-webpack/style-hot-loader', + { + loader: "nativescript-dev-webpack/css2json-loader", + options: { useForImports: true } + }, + 'sass-loader', + ], + }, { test: /\.css$/, + exclude: /[\/|\\]app\.css$/, use: [ 'nativescript-dev-webpack/style-hot-loader', 'nativescript-dev-webpack/apply-css-loader.js', @@ -200,11 +242,12 @@ module.exports = env => { }, { test: /\.scss$/, + exclude: /[\/|\\]app\.scss$/, use: [ 'nativescript-dev-webpack/style-hot-loader', 'nativescript-dev-webpack/apply-css-loader.js', { loader: "css-loader", options: { url: false } }, - "sass-loader", + 'sass-loader', ], }, { @@ -296,6 +339,9 @@ module.exports = env => { ], projectRoot, webpackConfig: config, + snapshotInDocker, + skipSnapshotTools, + useLibs })); } diff --git a/utils/ast-utils.ts b/utils/ast-utils.ts index 88660b86..5c0b19f8 100644 --- a/utils/ast-utils.ts +++ b/utils/ast-utils.ts @@ -18,6 +18,7 @@ import { dirname, join, relative } from "path"; import * as ts from "typescript"; import { readFileSync, existsSync } from "fs"; import { collectDeepNodes } from "@ngtools/webpack/src/transformers"; +import { getCompilerOptionsFromTSConfig } from "./tsconfig-utils"; export function getMainModulePath(entryFilePath: string, tsConfigName: string) { try { @@ -43,23 +44,13 @@ export function getMainModulePath(entryFilePath: string, tsConfigName: string) { function tsResolve(moduleName: string, containingFilePath: string, tsConfigName: string) { let result = moduleName; try { - const parseConfigFileHost: ts.ParseConfigFileHost = { - getCurrentDirectory: ts.sys.getCurrentDirectory, - useCaseSensitiveFileNames: false, - readDirectory: ts.sys.readDirectory, - fileExists: ts.sys.fileExists, - readFile: ts.sys.readFile, - onUnRecoverableConfigFileDiagnostic: undefined - }; - - const tsConfig = ts.getParsedCommandLineOfConfigFile(tsConfigName, ts.getDefaultCompilerOptions(), parseConfigFileHost); - - const compilerOptions: ts.CompilerOptions = tsConfig.options || ts.getDefaultCompilerOptions(); const moduleResolutionHost: ts.ModuleResolutionHost = { fileExists: ts.sys.fileExists, readFile: ts.sys.readFile }; + const compilerOptions = getCompilerOptionsFromTSConfig(tsConfigName); + const resolutionResult = ts.resolveModuleName(moduleName, containingFilePath, compilerOptions, moduleResolutionHost); if (resolutionResult && resolutionResult.resolvedModule && resolutionResult.resolvedModule.resolvedFileName) { diff --git a/utils/tsconfig-utils.ts b/utils/tsconfig-utils.ts new file mode 100644 index 00000000..5b8bbf13 --- /dev/null +++ b/utils/tsconfig-utils.ts @@ -0,0 +1,25 @@ +import * as ts from "typescript"; + +export function getCompilerOptionsFromTSConfig(tsConfigPath: string): ts.CompilerOptions { + const parseConfigFileHost: ts.ParseConfigFileHost = { + getCurrentDirectory: ts.sys.getCurrentDirectory, + useCaseSensitiveFileNames: false, + readDirectory: ts.sys.readDirectory, + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + onUnRecoverableConfigFileDiagnostic: undefined + }; + + const tsConfig = ts.getParsedCommandLineOfConfigFile(tsConfigPath, ts.getDefaultCompilerOptions(), parseConfigFileHost); + + const compilerOptions: ts.CompilerOptions = tsConfig.options || ts.getDefaultCompilerOptions(); + + return compilerOptions; +} + +export function getNoEmitOnErrorFromTSConfig(tsConfigPath: string): boolean { + const compilerOptions = getCompilerOptionsFromTSConfig(tsConfigPath); + const noEmitOnError = !!compilerOptions.noEmitOnError; + + return noEmitOnError; +} \ No newline at end of file diff --git a/xml-namespace-loader.spec.ts b/xml-namespace-loader.spec.ts new file mode 100644 index 00000000..80ab872f --- /dev/null +++ b/xml-namespace-loader.spec.ts @@ -0,0 +1,315 @@ +import xmlNsLoader from "./xml-namespace-loader"; +import { convertSlashesInPath } from "./projectHelpers"; + +const CODE_FILE = ` + + + + + + + + + + +`; + +interface TestSetup { + resolveMap: { [path: string]: string }, + expectedDeps: string[], + expectedRegs: { name: string, path: string }[], + ignore?: RegExp, + assureNoDeps?: boolean, + expectError?: boolean, + expectWarnings?: number +} + +function getContext( + done: DoneFn, + { resolveMap, expectedDeps, expectedRegs, assureNoDeps, ignore, expectError, expectWarnings }: TestSetup) { + const actualDeps: string[] = []; + const actualWarnings: Error[] =[] + let callbackCalled = false; + + const loaderContext = { + rootContext: "app", + context: "app/component", + async: () => (error, source: string) => { + if (callbackCalled) { + done.fail("Callback called more than once!"); + } + callbackCalled = true; + + expectedDeps.forEach(expectedDep => expect(actualDeps).toContain(expectedDep)); + + expectedRegs.forEach(({ name, path }) => { + const regCode = `global.registerModule("${name}", function() { return require("${path}"); });`; + expect(source).toContain(regCode); + }) + + if (assureNoDeps) { + expect(actualDeps.length).toBe(0); + expect(source).not.toContain("global.registerModule"); + } + + if(expectWarnings){ + expect(actualWarnings.length).toEqual(expectWarnings); + } + + if (error && !expectError) { + done.fail(error) + } else if (!error && expectError) { + done.fail("Error expected here") + } else { + done(); + } + }, + resolve: (context: string, request: string, callback: (err: Error, result: string) => void) => { + request = convertSlashesInPath(request); + if (resolveMap[request]) { + callback(undefined, resolveMap[request]); + } else { + callback(new Error(`Module ${request} not found`), undefined); + } + }, + addDependency: (dep: string) => { + actualDeps.push(dep); + }, + emitWarning: (err: Error) => { + actualWarnings.push(err); + }, + query: { ignore } + } + + return loaderContext; +} + +describe("XmlNamespaceLoader", () => { + it("with namespace pointing to files", (done) => { + const resolveMap = { + "app/nativescript-ui-chart": "app/nativescript-ui-chart.js", + "app/nativescript-ui-chart.xml": "app/nativescript-ui-chart.xml", + "app/nativescript-ui-chart.css": "app/nativescript-ui-chart.css", + }; + + const expectedDeps = [ + "app/nativescript-ui-chart.js", + "app/nativescript-ui-chart.xml", + "app/nativescript-ui-chart.css", + ]; + + const expectedRegs = [ + { name: "nativescript-ui-chart", path: "app/nativescript-ui-chart.js" }, + { name: "nativescript-ui-chart/RadCartesianChart", path: "app/nativescript-ui-chart.js" }, + { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart.xml" }, + { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart.css" }, + ]; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs }); + + xmlNsLoader.call(loaderContext, CODE_FILE); + }) + + it("with namespace/elementName pointing to files (with package.json)", (done) => { + const resolveMap = { + "app/nativescript-ui-chart": "app/nativescript-ui-chart/RadCartesianChart.js", //simulate package.json + "app/nativescript-ui-chart/RadCartesianChart": "app/nativescript-ui-chart/RadCartesianChart.js", + "app/nativescript-ui-chart/RadCartesianChart.xml": "app/nativescript-ui-chart/RadCartesianChart.xml", + "app/nativescript-ui-chart/RadCartesianChart.css": "app/nativescript-ui-chart/RadCartesianChart.css", + } + + const expectedDeps = [ + "app/nativescript-ui-chart/RadCartesianChart.js", + "app/nativescript-ui-chart/RadCartesianChart.xml", + "app/nativescript-ui-chart/RadCartesianChart.css", + ]; + + const expectedRegs = [ + { name: "nativescript-ui-chart", path: "app/nativescript-ui-chart/RadCartesianChart.js" }, + { name: "nativescript-ui-chart/RadCartesianChart", path: "app/nativescript-ui-chart/RadCartesianChart.js" }, + { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart/RadCartesianChart.xml" }, + { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart/RadCartesianChart.css" }, + ]; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }) + + it("with namespace/elementName pointing to files", (done) => { + const resolveMap = { + "app/nativescript-ui-chart/RadCartesianChart": "app/nativescript-ui-chart/RadCartesianChart.js", + "app/nativescript-ui-chart/RadCartesianChart.xml": "app/nativescript-ui-chart/RadCartesianChart.xml", + "app/nativescript-ui-chart/RadCartesianChart.css": "app/nativescript-ui-chart/RadCartesianChart.css", + } + + const expectedDeps = [ + "app/nativescript-ui-chart/RadCartesianChart.js", + "app/nativescript-ui-chart/RadCartesianChart.xml", + "app/nativescript-ui-chart/RadCartesianChart.css", + ]; + + const expectedRegs = [ + { name: "nativescript-ui-chart", path: "app/nativescript-ui-chart/RadCartesianChart.js" }, + { name: "nativescript-ui-chart/RadCartesianChart", path: "app/nativescript-ui-chart/RadCartesianChart.js" }, + { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart/RadCartesianChart.xml" }, + { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart/RadCartesianChart.css" }, + ]; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }) + + it("with namespace/elementName pointing to files - only XML and CSS", (done) => { + const resolveMap = { + "app/nativescript-ui-chart/RadCartesianChart.xml": "app/nativescript-ui-chart/RadCartesianChart.xml", + "app/nativescript-ui-chart/RadCartesianChart.css": "app/nativescript-ui-chart/RadCartesianChart.css", + } + + const expectedDeps = [ + "app/nativescript-ui-chart/RadCartesianChart.xml", + "app/nativescript-ui-chart/RadCartesianChart.css", + ]; + + const expectedRegs = [ + { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart/RadCartesianChart.xml" }, + { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart/RadCartesianChart.css" }, + ]; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }) + + it("with plugin path", (done) => { + const resolveMap = { + "nativescript-ui-chart": "node_module/nativescript-ui-chart/ui-chart.js", + } + + const expectedDeps = [ + ]; + + const expectedRegs = [ + { name: "nativescript-ui-chart", path: "nativescript-ui-chart" }, + { name: "nativescript-ui-chart/RadCartesianChart", path: "nativescript-ui-chart" }, + ]; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs }); + xmlNsLoader.call(loaderContext, CODE_FILE); + }) + + it("with ignored namespace should not add deps or register calls", (done) => { + const resolveMap = { + "app/nativescript-ui-chart": "app/nativescript-ui-chart.js", + "app/nativescript-ui-chart.xml": "app/nativescript-ui-chart.xml", + "app/nativescript-ui-chart.css": "app/nativescript-ui-chart.css", + }; + const expectedDeps = []; + const expectedRegs = []; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, ignore: /nativescript\-ui\-chart/, assureNoDeps: true }); + + xmlNsLoader.call(loaderContext, CODE_FILE); + }) + + it("with XML declaration and Doctype does not fail", (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ` + + + + `; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, assureNoDeps: true }); + + xmlNsLoader.call(loaderContext, testXml); + }) + it("with invalid XML fails", (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ``; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, expectError: true }); + + xmlNsLoader.call(loaderContext, testXml); + }) + + it("doesn't throw with ios and android platform namespaces", (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ` + + + + `; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, assureNoDeps: true }); + + xmlNsLoader.call(loaderContext, testXml); + }) + + it("doesn't throw with ios and android platform namespaces", (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ` + + + + + + `; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, assureNoDeps: true }); + + xmlNsLoader.call(loaderContext, testXml); + }) + + it("throws with unbound namespace namespaces", (done) => { + const resolveMap = {}; + const expectedDeps = []; + const expectedRegs = []; + + const testXml = ` + + + + `; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, expectError: true }); + + xmlNsLoader.call(loaderContext, testXml); + }) + + + it("with '&&', '||', '<=' and '>=' in binding expression, emits warnings, but does not fail", (done) => { + const resolveMap = { + "nativescript-ui-chart": "node_module/nativescript-ui-chart/ui-chart.js", + } + + const expectedDeps = []; + + const expectedRegs = [ + { name: "nativescript-ui-chart", path: "nativescript-ui-chart" }, + { name: "nativescript-ui-chart/RadCartesianChart", path: "nativescript-ui-chart" }, + ]; + + const testXml = ` + + + + + + `; + + const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, expectWarnings: 1 }); + + xmlNsLoader.call(loaderContext, testXml); + }) +}); diff --git a/xml-namespace-loader.js b/xml-namespace-loader.ts similarity index 59% rename from xml-namespace-loader.js rename to xml-namespace-loader.ts index e1294953..f3a16404 100644 --- a/xml-namespace-loader.js +++ b/xml-namespace-loader.ts @@ -1,22 +1,34 @@ -const { parse, relative, join, basename, extname } = require("path"); -const { promisify } = require('util'); -const { convertSlashesInPath } = require("./projectHelpers"); +import { parse, join } from "path"; +import { promisify } from "util"; +import { loader } from "webpack"; +import { parser, QualifiedTag } from "sax"; -module.exports = function (source, map) { - this.value = source; +import { convertSlashesInPath } from "./projectHelpers"; + +interface NamespaceEntry { + name: string; + path: string +} + +const loader: loader.Loader = function (source: string, map) { const { ignore } = this.query; + + let callbackCalled = false; const callback = this.async(); + const callbackWrapper = (error?: Error, content?: string, map?: any) => { + if (!callbackCalled) { + callbackCalled = true; + callback(error, content, map); + } + } - const { XmlParser } = require("tns-core-modules/xml"); const resolvePromise = promisify(this.resolve); - const promises = []; + const promises: Promise[] = []; + const namespaces: NamespaceEntry[] = []; - const namespaces = []; - const parser = new XmlParser((event) => { - const { namespace, elementName } = event; + const handleOpenTag = (namespace: string, elementName: string) => { const moduleName = `${namespace}/${elementName}`; - if ( namespace && !namespace.startsWith("http") && @@ -55,7 +67,7 @@ module.exports = function (source, map) { promises.push(resolvePromise(this.context, localNamespacePath) .then(path => pathResolved(path)) .catch(() => { - return promise = resolvePromise(this.context, localModulePath) + return resolvePromise(this.context, localModulePath) .then(path => pathResolved(path)) .catch(() => { return Promise.all([ @@ -81,17 +93,40 @@ module.exports = function (source, map) { }) ); } - }, undefined, true); + } + + const saxParser = parser(true, { xmlns: true }); + + // Register ios and android prefixes as namespaces to avoid "unbound xml namespace" errors + (saxParser).ns["ios"] = "http://schemas.nativescript.org/tns.xsd"; + (saxParser).ns["android"] = "http://schemas.nativescript.org/tns.xsd"; + (saxParser).ns["desktop"] = "http://schemas.nativescript.org/tns.xsd"; + (saxParser).ns["web"] = "http://schemas.nativescript.org/tns.xsd"; + + saxParser.onopentag = (node: QualifiedTag) => { handleOpenTag(node.uri, node.local); }; + saxParser.onerror = (err) => { + // Do only warning about invalid character "&"" for back-compatibility + // as it is common to use it in a binding expression + if (err && + err.message.indexOf("Invalid character") >= 0 && + err.message.indexOf("Char: &") >= 0) { + this.emitWarning(err) + } else { + callbackWrapper(err); + } - parser.parse(source); + saxParser.error = null; + }; + saxParser.write(source).close(); Promise.all(promises).then(() => { - const moduleRegisters = namespaces - .map(convertPath) - .map(n => - `global.registerModule("${n.name}", function() { return require("${n.path}"); });` - ) - .join(""); + const distinctNamespaces = new Map(); + namespaces.forEach(({ name, path }) => distinctNamespaces.set(name, convertSlashesInPath(path))); + + const moduleRegisters: string[] = []; + distinctNamespaces.forEach((path, name) => { + moduleRegisters.push(`global.registerModule("${name}", function() { return require("${path}"); });\n`); + }); // escape special whitespace characters // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Issue_with_plain_JSON.stringify_for_use_as_JavaScript @@ -99,16 +134,12 @@ module.exports = function (source, map) { .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029'); - const wrapped = `${moduleRegisters}\nmodule.exports = ${json}`; + const wrapped = `${moduleRegisters.join("")}\nmodule.exports = ${json}`; - callback(null, wrapped, map); + callbackWrapper(null, wrapped, map); }).catch((err) => { - callback(err); + callbackWrapper(err); }) - } -function convertPath(obj) { - obj.path = convertSlashesInPath(obj.path); - return obj; -} +export default loader;