diff --git a/.travis.yml b/.travis.yml index 71e3295d54..5e4ee5d250 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,12 +8,16 @@ node_js: - '6' git: submodules: true +install: +- npm install --ignore-scripts before_script: +- gem install xcodeproj +- gem install cocoapods - npm install grunt - node_modules/.bin/grunt enableScripts:false - grunt rebuild -- ./bin/nativescript error-reporting disable # force ~/.local dir creation -- some tests rely on it -- ./bin/nativescript usage-reporting disable +- "./bin/nativescript error-reporting disable" +- "./bin/nativescript usage-reporting disable" - npm test - node_modules/.bin/grunt enableScripts:true script: @@ -26,27 +30,27 @@ after_success: before_deploy: - node .travis/add-publishConfig.js $TRAVIS_BRANCH deploy: - - provider: s3 - bucket: nativescript-ci - access_key_id: AKIAIYSWYOZRFLVKPCTQ - secret_access_key: - secure: THGlblH9XdRcTQMc3jm4kpwCB3myl8MGB3v9XjB5ObK4gqxUxuPi6e158LEG9Dgb730MGEYtaAjc9OneH59WAjQOrdcf3GXiGKOiCYzGYZLqVE4pjNDuxHaVGOj7mso4TzMinMCaDSQajTvadCfVmXqgT6p9eSXkiV3V2d2DN6c= - skip_cleanup: true - local-dir: s3-publish - upload-dir: build_result - on: - branch: master - - provider: npm - skip_cleanup: true - email: nativescript@telerik.com - on: - branch: master - api_key: - secure: KzzsvF3eA3j4gRQa8tO//+XWNSR3XiX8Sa18o3PyKyG9/cBZ6PQ3Te74cNS1C3ZiLUOgs5dWA6/TmRVPci4XjvFaWo/B6e2fuVSl5H94Od99bkeBHJsbLSEkLN4ClV/YbGuyKgA5Q2yIFt6p2EJjL90RjbbIk7I4YuyG2Mo3j0Q= - - provider: npm - skip_cleanup: true - email: nativescript@telerik.com - on: - branch: release - api_key: - secure: KzzsvF3eA3j4gRQa8tO//+XWNSR3XiX8Sa18o3PyKyG9/cBZ6PQ3Te74cNS1C3ZiLUOgs5dWA6/TmRVPci4XjvFaWo/B6e2fuVSl5H94Od99bkeBHJsbLSEkLN4ClV/YbGuyKgA5Q2yIFt6p2EJjL90RjbbIk7I4YuyG2Mo3j0Q= +- provider: s3 + access_key_id: AKIAJL6X6724CSX64X3Q + secret_access_key: + secure: a0T/2S+/rkRJqEotWPAr1VELA3k5TGyRw6VmXgBQnkirc6H0Pfu0P2DY8iriO7pnTPDCPAskdBCuk6t+RYw/OCrGDzFPApnAQ7t3tksKPr2bGYqh2HVqbFKZyEbNjzwsgxn7cmLPo936ZTHP2muQItEI3o9Zh9EZ5XHtv0Maw0k= + bucket: nativescript-ci + skip_cleanup: true + local-dir: s3-publish + upload-dir: build_result + on: + branch: master +- provider: npm + skip_cleanup: true + email: nativescript@telerik.com + on: + branch: master + api_key: + secure: "g7Bpo7zX9kHaX8BcrnT/6S9/uuscAb2t+5Zr6okHCTaJXuLGOvzeV9KLFRyKKn93/o6sPlRIVA9welsYhUhdIlOUKz3jlPzejoaURhEY3xFrDWX29beho1Q88/AM5idGtosyElxvpw435WYeu/JlAu3DoYtCQavNXeEz5dY8cY0=" +- provider: npm + skip_cleanup: true + email: nativescript@telerik.com + on: + branch: release + api_key: + secure: "g7Bpo7zX9kHaX8BcrnT/6S9/uuscAb2t+5Zr6okHCTaJXuLGOvzeV9KLFRyKKn93/o6sPlRIVA9welsYhUhdIlOUKz3jlPzejoaURhEY3xFrDWX29beho1Q88/AM5idGtosyElxvpw435WYeu/JlAu3DoYtCQavNXeEz5dY8cY0=" diff --git a/.travis/add-publishConfig.js b/.travis/add-publishConfig.js index e11c0dd434..306595c06a 100644 --- a/.travis/add-publishConfig.js +++ b/.travis/add-publishConfig.js @@ -1,25 +1,21 @@ #!/usr/bin/env node -var fsModule = require("fs"); +const fsModule = require("fs"); +const path = "./package.json"; +const fileOptions = {encoding: "utf-8"}; +const content = fsModule.readFileSync(path, fileOptions); -// Adds a publishConfig section to the package.json file -// and sets a tag to it - -var path = "./package.json"; -var fileOptions = {encoding: "utf-8"}; -var content = fsModule.readFileSync(path, fileOptions); - -var packageDef = JSON.parse(content); +const packageDef = JSON.parse(content); if (!packageDef.publishConfig) { packageDef.publishConfig = {}; } -var branch = process.argv[2]; +const branch = process.argv[2]; if (!branch) { console.log("Please pass the branch name as an argument!"); process.exit(1); } packageDef.publishConfig.tag = branch === "release" ? "rc" : "next"; -var newContent = JSON.stringify(packageDef, null, " "); +const newContent = JSON.stringify(packageDef, null, " "); fsModule.writeFileSync(path, newContent, fileOptions); diff --git a/.vscode/launch.json b/.vscode/launch.json index acf4559e49..88a6442f47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -67,8 +67,19 @@ { "type": "node", "request": "attach", - "name": "Attach to Process", - "port": 5858, + "name": "Attach to Broker Process", + // In case you want to debug Analytics Broker process, add `--debug-brk=9897` (or --inspect-brk=9897) when spawning analytics-broker-process. + "port": 9897, + "sourceMaps": true + }, + + { + "type": "node", + "request": "attach", + "name": "Attach to Eqatec Process", + // In case you want to debug Eqatec Analytics process, add `--debug-brk=9855` (or --inspect-brk=9855) when spawning eqatec-analytics-process. + // NOTE: Ensure you set it only for one of the analytics processes. + "port": 9855, "sourceMaps": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f25cbcc6..ee07c39dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,113 @@ NativeScript CLI Changelog ================ +3.2.0 - RC.1 (2017, August 29) +== + +### Fixed +* [Fixed #3073](https://github.com/NativeScript/nativescript-cli/issues/3073): Saving two platform-specific files during LiveSync causes an exception +* [Fixed #3054](https://github.com/NativeScript/nativescript-cli/issues/3054): `tns prepare ` fails with Unhandled promise rejection +* [Fixed #3048](https://github.com/NativeScript/nativescript-cli/issues/3048): Fixed setup script for Windows +* [Fixed #3046](https://github.com/NativeScript/nativescript-cli/issues/3046): Export fails for Xcode 9 beta 5 +* [Fixed #3026](https://github.com/NativeScript/nativescript-cli/issues/3026): Fixed scripts for local installation of NativeScript on macOS +* [Fixed #3021](https://github.com/NativeScript/nativescript-cli/issues/3021): If multiple devices from the same platform are connected `tns debug --start` should ask you which of them to use +* [Fixed #3020](https://github.com/NativeScript/nativescript-cli/issues/3020): iOS Archive Export unexpected behavior when using AdHoc or AppStore provisioning +* [Fixed #3007](https://github.com/NativeScript/nativescript-cli/issues/3007): Application hangs on iOS during LiveSync +* [Fixed #3006](https://github.com/NativeScript/nativescript-cli/issues/3006): Add help for --provision option +* [Fixed #2952](https://github.com/NativeScript/nativescript-cli/issues/2952): Do not select automatically on which Android device to start debugging +* [Fixed #2946](https://github.com/NativeScript/nativescript-cli/issues/2946): Can't run iOS app on case-sensitive filesystem on macOS +* [Fixed #2934](https://github.com/NativeScript/nativescript-cli/issues/2934): Running `tns build android --release ...` uses *.debug.* files in build output +* [Fixed #2888](https://github.com/NativeScript/nativescript-cli/issues/2888): Build in release mode for iOS doesn't seem to set the production mode for APN +* [Fixed #2825](https://github.com/NativeScript/nativescript-cli/issues/2825): LiveSync won't work if appId doesn't match - no warning/error +* [Fixed #2818](https://github.com/NativeScript/nativescript-cli/issues/2818): Exception and stack trace is not shown in terminal for Android +* [Fixed #2810](https://github.com/NativeScript/nativescript-cli/issues/2810): Cannot read property 'match' of null error when installing nativescript cli +* [Fixed #2728](https://github.com/NativeScript/nativescript-cli/issues/2728): `tns run ios --device fakeID` starts iOS Simulator +* [Fixed #2716](https://github.com/NativeScript/nativescript-cli/issues/2716): Webpack issues when build in release mode +* [Fixed #2657](https://github.com/NativeScript/nativescript-cli/issues/2657): `tns run android/ios` does not remove folders correctly +* [Fixed #2515](https://github.com/NativeScript/nativescript-cli/issues/2515): CLI captures logs from Chrome and Cordova apps +* [Fixed #2501](https://github.com/NativeScript/nativescript-cli/issues/2501): Manual signing with distribution provisioning profile fails with NS 2.5 + +3.1.3 (2017, July 25) +== + +### New + +* [Implemented #2470](https://github.com/NativeScript/nativescript-cli/issues/2470): Establish a recommended workflow for simultaneous Android & iOS development - use `tns run` command. +* [Implemented #2873](https://github.com/NativeScript/nativescript-cli/issues/2873): Add official support for Node 8. +* [Implemented #2894](https://github.com/NativeScript/nativescript-cli/issues/2894): Support for Android 8 (API-26) and Android Build Tools 26.0.0. + +### Fixed +* [Fixed #2361](https://github.com/NativeScript/nativescript-cli/issues/2361): 'iTunes is not installed...' on Windows when using `tns run android`. +* [Fixed #2870](https://github.com/NativeScript/nativescript-cli/issues/2870): CLI can not create projects with custom templates when npm 5 is used . +* [Fixed #2871](https://github.com/NativeScript/nativescript-cli/issues/2871): CLI can not add platform from local tgz package when npm 5 is used . +* [Fixed #2889](https://github.com/NativeScript/nativescript-cli/issues/2889): `tns prepare ios --provision` starts simulator. +* [Fixed #2936](https://github.com/NativeScript/nativescript-cli/issues/2936): CFBundleURLTypes cannot be overridden from a plugin. +* [Fixed #2941](https://github.com/NativeScript/nativescript-cli/issues/2941): Duplicate console logs with LiveSync in 3.1. +* [Fixed #2965](https://github.com/NativeScript/nativescript-cli/issues/2965): Unmet peerDependencies break adding of platform. +* [Fixed #2966](https://github.com/NativeScript/nativescript-cli/issues/2966): Improve selection of device/emulator for debugging. +* [Fixed #2975](https://github.com/NativeScript/nativescript-cli/issues/2975): CLI Process hangs on native build failure. +* [Fixed #2986](https://github.com/NativeScript/nativescript-cli/issues/2986): Preparing a project for a platform causes changes for the other platform. +* [Fixed #2988](https://github.com/NativeScript/nativescript-cli/issues/2988): CLI fails with EPIPE during `$ tns run ios`. + +3.1.2 (2017, July 06) +== + +### Fixed +* [Fixed #2950](https://github.com/NativeScript/nativescript-cli/issues/2950): Unable to provide user input on postinstall of plugin + +3.1.1 (2017, June 28) +== + +### Fixed +* [Fixed #2879](https://github.com/NativeScript/nativescript-cli/issues/2879): Livesync does not apply changes in CSS files on physical iOS devices +* [Fixed #2892](https://github.com/NativeScript/nativescript-cli/issues/2892): Not copying the CFBundleURLTypes from the plist file to the project +* [Fixed #2916](https://github.com/NativeScript/nativescript-cli/issues/2916): If no device or emulator is attached `tns debug android` kills the commandline process and doesn't start an emulator +* [Fixed #2923](https://github.com/NativeScript/nativescript-cli/issues/2923): Fix asking for user email on postinstall +* [Fixed #2929](https://github.com/NativeScript/nativescript-cli/issues/2929): Android release builds with webpack disregards plugin's gradle dependencies. + + +3.1.0 (2017, June 22) +== + +### Fixed + +* [Fixed #2905](https://github.com/NativeScript/nativescript-cli/issues/2905): Doctor command fails when ANDROID_HOME is not set +* [Fixed #2874](https://github.com/NativeScript/nativescript-cli/issues/2874): Unable to build and deploy app to iTunes: Unable to connect to iTunes Connect +* [Fixed #2856](https://github.com/NativeScript/nativescript-cli/issues/2856): DevDependencies' dependencies are added to native project +* [Fixed #2860](https://github.com/NativeScript/nativescript-cli/issues/2860): `tns run ios` fails on iOS devices after rebuilding application in the process +* [Fixed #2850](https://github.com/NativeScript/nativescript-cli/issues/2850): Document properly the "Emulate Options" +* [Fixed #2757](https://github.com/NativeScript/nativescript-cli/issues/2757): `tns build ios --forDevice --path TestApp` start simulator +* [Fixed #2720](https://github.com/NativeScript/nativescript-cli/issues/2720): Livesync error with webstorm temporary files +* [Fixed #2716](https://github.com/NativeScript/nativescript-cli/issues/2716): Web pack issues when build in release mode +* [Fixed #2501](https://github.com/NativeScript/nativescript-cli/issues/2501): Manual signing with distribution provisioning profile fails with NS 2.5 +* [Fixed #2446](https://github.com/NativeScript/nativescript-cli/issues/2446): tns run--device NonexistentName retunrs different messages for ios and android +* [Fixed #1358](https://github.com/NativeScript/nativescript-cli/issues/1358): Webstorm Ubuntu .bash_profile +* [Fixed #521](https://github.com/NativeScript/nativescript-cli/issues/521): EPERM error with .local/share directory after installing CLI + +### Deprecated + +* [Implemented #2329](https://github.com/NativeScript/nativescript-cli/issues/2329): Remove the emulate command + +3.0.3 (2017, June 1) +== + +### Fixed + +* [Fix #2855](https://github.com/NativeScript/nativescript-cli/issues/2855): iOS inspector not installed with npm 5 + +3.0.2 (2017, May 30) +== + +### Fixed + +* Removed restart of the App if HTML/CSS file was modified. The issue is fixed in the Modules **3.0.1** and we can again just refresh the app on change. +* [Fix #2852](https://github.com/NativeScript/nativescript-cli/pull/2852): Fix prepare for android when building with webpack + 3.0.1 (2017, May 11) == ### Fixed -* [Fix #2780](https://github.com/NativeScript/nativescript-cli/issues/2780): CLI tool doesn't restart app if HTML/CSS file was modified * [Fix #2732](https://github.com/NativeScript/nativescript-cli/issues/2732): Livesync crashes app every OTHER time on iOS with 3.0.0-rc.2 * [Fix #2764](https://github.com/NativeScript/nativescript-cli/issues/2764): Error when executing "tns run ios" with 3.0 on a project that is located in a directory path with "spaces" @@ -30,7 +131,7 @@ NativeScript CLI Changelog * [Fixed #2661](https://github.com/NativeScript/nativescript-cli/issues/2661): Adding new files during livesync doesn't succeed on iOS Devices * [Fixed #2650](https://github.com/NativeScript/nativescript-cli/issues/2650): Release Build Android error: gradlew.bat failed with exit code 1 When Path contains Space * [Fixed #2125](https://github.com/NativeScript/nativescript-cli/issues/2125): NativeScript setup script fails on Mac -* [Fixed #2697](https://github.com/NativeScript/nativescript-cli/issues/2697): App_Resources being copied into app RAW +* [Fixed #2697](https://github.com/NativeScript/nativescript-cli/issues/2697): App_Resources being copied into app RAW 3.0.0-RC.1 (2017, March 29) == @@ -40,19 +141,19 @@ NativeScript CLI Changelog * [Implemented #2170](https://github.com/NativeScript/nativescript-cli/issues/2170): CLI should report adb install errors * [Implemented #2204](https://github.com/NativeScript/nativescript-cli/issues/2204): Remove fibers from CLI and use async/await * [Implemented #2349](https://github.com/NativeScript/nativescript-cli/issues/2349): Command "emulate" and options "--emulator/--device/--avd" are a bit confusing to use -* [Implemented #2513](https://github.com/NativeScript/nativescript-cli/issues/2513): Allow templates to omit App_Resources +* [Implemented #2513](https://github.com/NativeScript/nativescript-cli/issues/2513): Allow templates to omit App_Resources * [Implemented #2633](https://github.com/NativeScript/nativescript-cli/issues/2633): Deprecate `tns livesync` command ### Fixed -* [Fixed #2225](https://github.com/NativeScript/nativescript-cli/issues/2225): The tns debug ios command hangs on launch screen +* [Fixed #2225](https://github.com/NativeScript/nativescript-cli/issues/2225): The tns debug ios command hangs on launch screen * [Fixed #2357](https://github.com/NativeScript/nativescript-cli/issues/2357): --copy-to option is broken * [Fixed #2446](https://github.com/NativeScript/nativescript-cli/issues/2446): tns emulate --device NonexistentName retunrs different messages for ios and android * [Fixed #2486](https://github.com/NativeScript/nativescript-cli/issues/2486): tns run android (without started emulator/connected device) fails to start app * [Fixed #2487](https://github.com/NativeScript/nativescript-cli/issues/2487): Exception: The plugin tns-android@2.5.0 is already installed * [Fixed #2496](https://github.com/NativeScript/nativescript-cli/issues/2496): `tns platform clean android` does not maintain the same version of the runtimes * [Fixed #2511](https://github.com/NativeScript/nativescript-cli/issues/2511): Second run of `tns run android --release` does not respect changes in application code -* [Fixed #2557](https://github.com/NativeScript/nativescript-cli/issues/2557): 2.5.1 iOS incremental build generates inconsistent binary +* [Fixed #2557](https://github.com/NativeScript/nativescript-cli/issues/2557): 2.5.1 iOS incremental build generates inconsistent binary * [Fixed #2559](https://github.com/NativeScript/nativescript-cli/issues/2559): `tns init` fails, `ins init --force` produce invalid app * [Fixed #2560](https://github.com/NativeScript/nativescript-cli/issues/2560): `tns run` should do full prepare/rebuild if something in App_Resources is changes * [Fixed #2561](https://github.com/NativeScript/nativescript-cli/issues/2561): Fix prepare process terminates if passing too many arguments to a new node process @@ -80,8 +181,8 @@ NativeScript CLI Changelog ### Fixed * [Fixed #2476](https://github.com/NativeScript/nativescript-cli/issues/2476): `tns run ios` unable to sync files on iOS Simulator -* [Fixed #2491](https://github.com/NativeScript/nativescript-cli/issues/2491): "ERROR Error: Could not find module" after 2.5 upgrade -* [Fixed #2472](https://github.com/NativeScript/nativescript-cli/issues/2472): Livesync watches .DS_Store files +* [Fixed #2491](https://github.com/NativeScript/nativescript-cli/issues/2491): "ERROR Error: Could not find module" after 2.5 upgrade +* [Fixed #2472](https://github.com/NativeScript/nativescript-cli/issues/2472): Livesync watches .DS_Store files * [Fixed #2469](https://github.com/NativeScript/nativescript-cli/issues/2469): `tns run` can get stuck in a loop since 2.5 * [Fixed #2512](https://github.com/NativeScript/nativescript-cli/issues/2512): Run should not watch hidden files * [Fixed #2521](https://github.com/NativeScript/nativescript-cli/issues/2521): Enable requesting user input during plugin installation @@ -94,7 +195,7 @@ NativeScript CLI Changelog * [Implemented #2276](https://github.com/NativeScript/nativescript-cli/issues/2276): Support plugin development when using the watch option * [Implemented #2260]( https://github.com/NativeScript/nativescript-cli/issues/2260): Deprecate support for Node.js 4.x feature * [Implemented #2112]( https://github.com/NativeScript/nativescript-cli/issues/2112): Update webpack plugin to run as a npm script feature -* [Implemented #1906]( https://github.com/NativeScript/nativescript-cli/issues/1906): TNS Doctor Android tools detection feature +* [Implemented #1906]( https://github.com/NativeScript/nativescript-cli/issues/1906): TNS Doctor Android tools detection feature * [Implemented #1845](https://github.com/NativeScript/nativescript-cli/issues/1845): Remove NPM as a dependency, use the NPM installed on users' machine feature ### Fixed @@ -102,7 +203,7 @@ NativeScript CLI Changelog **Install and setup issues** * [Fixed #2302](https://github.com/NativeScript/nativescript-cli/issues/2302): App runs in Xcode and CLI but not on test flight -* [Fixed #1922](https://github.com/NativeScript/nativescript-cli/issues/1922): TNS doctor does not detect if JDK is not installed and throws an error +* [Fixed #1922](https://github.com/NativeScript/nativescript-cli/issues/1922): TNS doctor does not detect if JDK is not installed and throws an error * [Fixed #2312](https://github.com/NativeScript/nativescript-cli/issues/2312): Creating app w/ nativescript@next is not deploying App_Resources folder * [Fixed #2308](https://github.com/NativeScript/nativescript-cli/issues/2308): Using nativescript@next brings multiple libraries into node_modules * [Fixed #2301](https://github.com/NativeScript/nativescript-cli/issues/2301): Rework logic of handling 2 package.json files after tns create @@ -113,8 +214,8 @@ NativeScript CLI Changelog * [Fixed #2213](https://github.com/NativeScript/nativescript-cli/issues/2213): When switching build configurations plugin platform files are copied into assets * [Fixed #2177](https://github.com/NativeScript/nativescript-cli/issues/2177): Prepare does not flatten scoped dependencies -* [Fixed #2150](https://github.com/NativeScript/nativescript-cli/issues/2150): TNS run command broken with using --watch -* [Fixed #1740](https://github.com/NativeScript/nativescript-cli/issues/1740): Dev Dependencies (like Gulp, etc) getting built and build is failing because of which Gulp * [Fixed #](): integration is not working currently. +* [Fixed #2150](https://github.com/NativeScript/nativescript-cli/issues/2150): TNS run command broken with using --watch +* [Fixed #1740](https://github.com/NativeScript/nativescript-cli/issues/1740): Dev Dependencies (like Gulp, etc) getting built and build is failing because of which Gulp * [Fixed #](): integration is not working currently. * [Fixed #2270](https://github.com/NativeScript/nativescript-cli/issues/2270): App is not rebuilt after removing plugin with native dependencies bug * [Fixed #2253](https://github.com/NativeScript/nativescript-cli/issues/2253): Prepare command with bundle option doesn't copy files @@ -127,7 +228,7 @@ NativeScript CLI Changelog **Emulate/ Device issues** -* [Fixed #1522](https://github.com/NativeScript/nativescript-cli/issues/1522): Cannot specify emulator device while debugging +* [Fixed #1522](https://github.com/NativeScript/nativescript-cli/issues/1522): Cannot specify emulator device while debugging * [Fixed #2345](https://github.com/NativeScript/nativescript-cli/issues/2345): Option --device {DeviceName} not working for emulate/run/debug/deploy bug * [Fixed #2344](https://github.com/NativeScript/nativescript-cli/issues/2344): Emulate command not working for iOS @@ -150,18 +251,18 @@ NativeScript CLI Changelog * [Fixed #2228](https://github.com/NativeScript/nativescript-cli/issues/2228): Homebrew as root is no longer supported * [Fixed #2227]( https://github.com/NativeScript/nativescript-cli/issues/2227): Remove use of lib/iOS folder for native plugins * [Fixed #282]( https://github.com/NativeScript/nativescript-cli/issues/282): "tns platform add android --path TNSApp --symlink" does not work on Windows -* [Fixed #1802](https://github.com/NativeScript/nativescript-cli/issues/1802): semver copied to platforms/android even though it's a dev dependency +* [Fixed #1802](https://github.com/NativeScript/nativescript-cli/issues/1802): semver copied to platforms/android even though it's a dev dependency 2.4.2 (2016, December 8) == ### Fixed -* [Fixed #2332](https://github.com/NativeScript/nativescript-cli/pull/2332): Fixed email registration process +* [Fixed #2332](https://github.com/NativeScript/nativescript-cli/pull/2332): Fixed email registration process 2.4.1 (2016, December 4) == ### Fixed -* [Fixed #2200](https://github.com/NativeScript/nativescript-cli/pull/2200): TypeScript files don't show in {N} debugger. -* [Fixed #2273](https://github.com/NativeScript/nativescript-cli/pull/2273): Livesync should continue to work after app crash +* [Fixed #2200](https://github.com/NativeScript/nativescript-cli/pull/2200): TypeScript files don't show in {N} debugger. +* [Fixed #2273](https://github.com/NativeScript/nativescript-cli/pull/2273): Livesync should continue to work after app crash 2.4.0 (2016, November 16) == @@ -224,7 +325,7 @@ NativeScript CLI Changelog ### New * [Implemented #1959](https://github.com/NativeScript/nativescript-cli/issues/1959): XCode8/iOS10 support -* [Implemented #1948](https://github.com/NativeScript/nativescript-cli/issues/1948): npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue +* [Implemented #1948](https://github.com/NativeScript/nativescript-cli/issues/1948): npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue * [Implemented #1944](https://github.com/NativeScript/nativescript-cli/issues/1944): Enhancement: tns plugin INSTALL alias * [Implemented #1565](https://github.com/NativeScript/nativescript-cli/issues/1565): Livesync debugging support * [Implemented #526](https://github.com/NativeScript/nativescript-cli/issues/526): Update plugin command diff --git a/CocoaPods.md b/CocoaPods.md deleted file mode 100644 index 08ec9ce849..0000000000 --- a/CocoaPods.md +++ /dev/null @@ -1,102 +0,0 @@ -# Using CocoaPods - -When you develop for iOS, you can quickly add third-party libraries to your NativeScript projects via the [CocoaPods](https://cocoapods.org/) dependency manager. - -To work with such libraries, you need to wrap them as a custom NativeScript plugin and add them to your project. - - * [Install CocoaPods](#install-cocoapods) - * [Create CLI Project](#create-cli-project) - * [Wrap the Library as NativeScript Plugin](#wrap-the-library-as-nativescript-plugin) - * [Build the Project](#build-the-project) - -## Install CocoaPods -You need to install CocoaPods. If you haven't yet, you can do so by running: - -```bash -$ sudo gem install cocoapods -``` -> **NOTE:** The minimum required version of CocoaPods is 1.0.0. - -To check your current version, run the following command. - -```bash -$ pod --version -``` - -To update CocoaPods, just run the installation command again. - -``` -sudo gem install cocoapods -``` - -## Create CLI Project -To start, create a project and add the iOS platform. - -```bash -$ tns create MYCocoaPodsApp -$ cd MYCocoaPodsApp -$ tns platform add ios -``` - -## Wrap the Library as NativeScript Plugin - -For more information about working with NativeScript plugins, click [here](PLUGINS.md). - -```bash -cd .. -mkdir my-plugin -cd my-plugin -``` - -Create a `package.json` file with the following content: - -```json -{ - "name": "my-plugin", - "version": "0.0.1", - "nativescript": { - "platforms": { - "ios": "1.3.0" - } - } -} -``` - -Create a [Podfile](https://guides.cocoapods.org/syntax/podfile.html) which describes the dependency to the library that you want to use. Move it to the `platforms/ios` folder. - -``` -my-plugin/ -├── package.json -└── platforms/ - └── ios/ - └── Podfile -``` - -Podfile: -``` -pod 'GoogleMaps' -``` - -## Install the Plugin - -Next, install the plugin: - -```bash -tns plugin add ../my-plugin -``` - -> **NOTE:** Installing CocoaPods sets the deployment target of your app to iOS 8, if not already set to iOS 8 or later. This change is required because CocoaPods are installed as shared frameworks to ensure that all symbols are available at runtime. - -## Build the Project - -```bash -tns build ios -``` - -This modifies the `MYCocoaPodsApp.xcodeproj` and creates a workspace with the same name. - -> **IMPORTANT:** You will no longer be able to run the `xcodeproj` alone. NativeScript CLI will build the newly created workspace and produce the correct package. - -## Troubleshooting - -In case of post-build linker errors, you might need to resolve missing dependencies to native frameworks required by the installed CocoaPod. For more information about how to create the required links, see the [build.xcconfig specification](PLUGINS.md#buildxcconfig-specification). diff --git a/Gruntfile.js b/Gruntfile.js index 0cf68a12d2..819193adb7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,8 +1,9 @@ -var now = new Date().toISOString(); +const childProcess = require("child_process"); +const now = new Date().toISOString(); function shallowCopy(obj) { var result = {}; - Object.keys(obj).forEach(function(key) { + Object.keys(obj).forEach(function (key) { result[key] = obj[key]; }); return result; @@ -11,10 +12,10 @@ function shallowCopy(obj) { var travis = process.env["TRAVIS"]; var buildNumber = process.env["PACKAGE_VERSION"] || process.env["BUILD_NUMBER"] || "non-ci"; -module.exports = function(grunt) { +module.exports = function (grunt) { var path = require("path"); var commonLibNodeModules = path.join("lib", "common", "node_modules"); - if(require("fs").existsSync(commonLibNodeModules)) { + if (require("fs").existsSync(commonLibNodeModules)) { grunt.file.delete(commonLibNodeModules); } grunt.file.write(path.join("lib", "common", ".d.ts"), ""); @@ -50,17 +51,6 @@ module.exports = function(grunt) { }, }, - tslint: { - build: { - files: { - src: ["lib/**/*.ts", "test/**/*.ts", "!lib/common/node_modules/**/*.ts", "!lib/common/messages/**/*.ts", "lib/common/test/unit-tests/**/*.ts", "definitions/**/*.ts", "!lib/**/*.d.ts" , "!test/**/*.d.ts"] - }, - options: { - configuration: grunt.file.readJSON("./tslint.json") - } - } - }, - watch: { devall: { files: ["lib/**/*.ts", 'test/**/*.ts', "!lib/common/node_modules/**/*.ts", "!lib/common/messages/**/*.ts"], @@ -96,7 +86,7 @@ module.exports = function(grunt) { command: "npm pack", options: { execOptions: { - env: (function() { + env: (function () { var env = shallowCopy(process.env); env["NATIVESCRIPT_SKIP_POSTINSTALL_TASKS"] = "1"; return env; @@ -143,9 +133,8 @@ module.exports = function(grunt) { grunt.loadNpmTasks("grunt-contrib-watch"); grunt.loadNpmTasks("grunt-shell"); grunt.loadNpmTasks("grunt-ts"); - grunt.loadNpmTasks("grunt-tslint"); - grunt.registerTask("set_package_version", function(version) { + grunt.registerTask("set_package_version", function (version) { var buildVersion = version !== undefined ? version : buildNumber; if (process.env["BUILD_CAUSE_GHPRBCAUSE"]) { buildVersion = "PR" + buildVersion; @@ -154,8 +143,8 @@ module.exports = function(grunt) { var packageJson = grunt.file.readJSON("package.json"); var versionParts = packageJson.version.split("-"); if (process.env["RELEASE_BUILD"]) { -// HACK - excluded until 1.0.0 release or we refactor our project infrastructure (whichever comes first) -// packageJson.version = versionParts[0]; + // HACK - excluded until 1.0.0 release or we refactor our project infrastructure (whichever comes first) + // packageJson.version = versionParts[0]; } else { versionParts[1] = buildVersion; packageJson.version = versionParts.join("-"); @@ -163,7 +152,11 @@ module.exports = function(grunt) { grunt.file.write("package.json", JSON.stringify(packageJson, null, " ")); }); - grunt.registerTask("enableScripts", function(enable) { + grunt.registerTask("tslint:build", function (version) { + childProcess.execSync("npm run tslint", { stdio: "inherit" }); + }); + + grunt.registerTask("enableScripts", function (enable) { var enableTester = /false/i; var newScriptsAttr = !enableTester.test(enable) ? "scripts" : "skippedScripts"; var packageJson = grunt.file.readJSON("package.json"); diff --git a/LICENSE b/LICENSE index 2c63567572..dda4f06e7e 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2015-2016 Telerik AD + Copyright (c) 2015-2017 Telerik AD Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +198,4 @@ Apache License distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/PLUGINS.md b/PLUGINS.md deleted file mode 100644 index 4a4be475a9..0000000000 --- a/PLUGINS.md +++ /dev/null @@ -1,359 +0,0 @@ -Plugins -========= - -Starting with NativeScript CLI 1.1.0, you can develop or use plugins in your NativeScript projects. - -* [What Are NativeScript Plugins](#what-are-nativescript-plugins) -* [Where Can You Find NativeScript Plugins](#where-can-you-find-nativescript-plugins) -* [Create a Plugin](#create-a-plugin) - * [Directory Structure](#directory-structure) - * [`package.json` Specification](#packagejson-specification) - * [`include.gradle` Specification](#includegradle-specification) - * [`build.xcconfig` Specification](#buildxcconfig-specification) -* [Install a Plugin](#install-a-plugin) - * [Valid Plugin Sources](#valid-plugin-sources) - * [Installation Specifics](#installation-specifics) - * [Manual Steps After Installation](#manual-steps-after-installation) -* [Use a Plugin](#use-a-plugin) -* [Remove a Plugin](#remove-a-plugin) - * [Removal Specifics](#removal-specifics) - * [Manual Steps After Removal](#manual-steps-after-removal) - -## What Are NativeScript Plugins - -A NativeScript plugin is any npm package, published or not, that exposes a native API via JavaScript and consists of the following elements. - -* A `package.json` file which contains the following metadata: name, version, supported runtime versions, dependencies and others. For more information, see the [`package.json` Specification](#packagejson-specification) section. -* One or more CommonJS modules that expose a native API via a unified JavaScript API. For more information about Common JS modules, see the [CommonJS Wiki](http://wiki.commonjs.org/wiki/CommonJS). -* (Optional) `AndroidManifest.xml` and `Info.plist` which describe the permissions, features or other configurations required or used by your app for Android and iOS, respectively. -* (Optional) Native Android libraries and the native Android `include.gradle` configuration file which describes the native dependencies. For more information, see the [`include.gradle` Specification](#includegradle-specification) section. -* (Optional) Native iOS libraries and the native `build.xcconfig` configuration file which describes the native dependencies. For more information, see the [`build.xcconfig` Specification](#buildxcconfig-specification) section. - -The plugin must have the directory structure, described in the [Directory Structure](#directory-structure) section. - -## Where Can You Find NativeScript Plugins - -You can find a list of Telerik-verified NativeScript plugins on the [Telerik Verified Plugin Marketplace](http://plugins.telerik.com/nativescript), and a list of community-written NativeScript plugins by [searching for “nativescript” on npm](https://www.npmjs.com/search?q=nativescript). - -## Create a Plugin - -If the NativeScript framework does not expose a native API that you need, you can develop a plugin which exposes the required functionality. When you develop a plugin, keep in mind the following requirements. - -* The plugin must be a valid npm package. -* The plugin must expose a built-in native API or a native API available via custom native libraries. -* The plugin must be written in JavaScript and must comply with the CommonJS specification. If you are using a transpiler, make sure to include the transpiled JavaScript files in your plugin. -* The plugin directory structure must comply with the specification described below. -* The plugin must contain a valid `package.json` which complies with the specification described below. -* If the plugin requires any permissions, features or other configuration specifics, it must contain `AndroidManifest.xml` or `Info.plist` file which describe them. -* If the plugin depends on native libraries, it must contain a valid `include.gradle` or `build.xcconfig` file, which describes the dependencies. - -### Directory Structure - -NativeScript plugins which consist of one CommonJS module might have the following directory structure. - -``` -my-plugin/ -├── index.js -├── package.json -└── platforms/ - ├── android/ - │ ├── res/ - │ └── AndroidManifest.xml - └── ios/ - └── Info.plist -``` - -NativeScript plugins which consist of multiple CommonJS modules might have the following directory structure. - -``` -my-plugin/ -├── index.js -├── package.json -├── MyModule1/ -│ ├── index1.js -│ └── package.json -├── MyModule2/ -│ ├── index2.js -│ └── package.json -└── platforms/ - ├── android/ - │ ├── AndroidManifest.xml - │ └── res/ - └── ios/ - └── Info.plist -``` - -* `index.js`: This file is the CommonJS module which exposes the native API. You can use platform-specific `*.platform.js` files. For example: `index.ios.js` and `index.android.js`. During the plugin installation, the NativeScript CLI will copy the platform resources to the `tns_modules` subdirectory in the correct platform destination in the `platforms` directory of your project.
Alternatively, you can give any name to this CommonJS module. In this case, however, you need to point to this file by setting the `main` key in the `package.json` for the plugin. For more information, see [Folders as Modules](https://nodejs.org/api/modules.html#modules_folders_as_modules). -* `package.json`: This file contains the metadata for your plugin. It sets the supported runtimes, the plugin name and version and any dependencies. The `package.json` specification is described in detail below. -* `platforms\android\AndroidManifest.xml`: This file describes any specific configuration changes required for your plugin to work. For example: required permissions. For more information about the format of `AndroidManifest.xml`, see [App Manifest](http://developer.android.com/guide/topics/manifest/manifest-intro.html).
During build, gradle will merge the plugin `AndroidManifest.xml` with the `AndroidManifest.xml` for your project. The NativeScript CLI will not resolve any contradicting or duplicate entries during the merge. After the plugin is installed, you need to manually resolve such issues. -* `platforms\android\res`: (Optional) This directory contains resources declared by the `AndroidManifest.xml` file. You can look at the folder structure [here](http://developer.android.com/guide/topics/resources/providing-resources.html#ResourceTypes). -* `platforms\ios\Info.plist`: This file describes any specific configuration changes required for your plugin to work. For example, required permissions. For more information about the format of `Info.plist`, see [About Information Property List Files](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html).
During the plugin installation, the NativeScript CLI will merge the plugin `Info.plist` with the `Info.plist` for your project. The NativeScript CLI will not resolve any contradicting or duplicate entries during the merge. After the plugin is installed, you need to manually resolve such issues. - -NativeScript plugins which contain both native Android and iOS libraries might have the following directory structure. - -``` -my-plugin/ -├── ... -└── platforms/ - ├── android/ - │ ├── res/ - │ ├── MyLibrary.jar - │ ├── MyLibrary.aar - │ ├── include.gradle - │ └── AndroidManifest.xml - └── ios/ - ├── MyiOSFramework.framework - ├── build.xcconfig - ├── Podfile - ├── Info.plist - ├── MyStaticiOSLibrary.a - └── include/ - └── MyStaticiOSLibrary/ - └── ... -``` - -* `platforms\android`: This directory contains any native Android libraries packaged as `*.jar` and `*.aar` packages. These native libraries can reside in the root of this directory or in a user-created sub-directory. During the plugin installation, the NativeScript CLI will configure the Android project in `platforms\android` to work with the plugin. -* `platforms\android\res`: (Optional) This directory contains resources declared by the `AndroidManifest.xml` file. You can look at the folder structure [here](http://developer.android.com/guide/topics/resources/providing-resources.html#ResourceTypes). -* `platforms\android\include.gradle`: This file modifies the native Android configuration of your NativeScript project such as native dependencies, build types and configurations. For more information about the format of `include.gradle`, see [`include.gradle` file](#includegradle-specification). -* `platforms\ios`: This directory contains native dynamic iOS Cocoa Touch Frameworks (`.framework`) and Cocoa Touch Static Libraries (`.a`). During the plugin installation, the NativeScript CLI will copy these files to `lib\iOS` in your project and will configure the iOS project in `platforms\ios` to work with the libraries. If the library is written in Swift, only APIs exposed to Objective-C are exposed to NativeScript. In case the plugin contains a Cocoa Touch Static Library (`.a`), you must place all public headers (`.h`) under `include\\`. Make sure that the static libraries are built at least for the following processor architectures - armv7, arm64, i386. -* `platforms\ios\build.xcconfig`: This file modifies the native iOS configuration of your NativeScript project such as native dependencies and configurations. For more information about the format of `build.xcconfig`, see [`build.xcconfig` file](#buildxcconfig-specification). -* `platforms\ios\Podfile`: This file describes the dependency to the library that you want to use. For more information, see [the CocoaPods article](CocoaPods.md). - -### Package.json Specification - -Every NativeScript plugin should contain a valid `package.json` file in its root. This `package.json` file must meet the following requirements. - -* It must comply with the [npm specification](https://docs.npmjs.com/files/package.json).
The `package.json` must contain at least `name` and `version` pairs. You will later use the plugin in your code by requiring it by its `name`. -* It must contain a `nativescript` section which describes the supported NativeScript runtimes and their versions. This section can be empty. If you want to define supported platforms and runtimes, you can nest a `platforms` section. In this `platforms` section, you can nest `ios` and `android` key-value pairs. The values in these pairs must be valid runtime versions or ranges of values specified by a valid semver(7) syntax. -* If the plugin depends on other npm modules, it must contain a `dependencies` section as described [here](https://docs.npmjs.com/files/package.json#dependencies).
The NativeScript CLI will resolve the dependencies during the plugin installation. - -#### Package.json Example - -The following is an example of a `package.json` file for a NativeScript plugin which supports the 1.0.0 version of the iOS runtime and the 1.1.0 version of the Android runtime. - -```JSON -{ - "name": "myplugin", - "version": "0.0.1", - "nativescript": { - "platforms": { - "ios": "1.0.0", - "android": "1.1.0" - } - } -} -``` - -### Include.gradle Specification - -Every NativeScript plugin, which contains native Android dependencies, should also contain a valid `include.gradle` file in the root of its `platforms\android` directory. This `include.gradle` file must meet the following requirements. - -* It must contain its own [configuration](http://developer.android.com/tools/building/configuring-gradle.html). -* It might contain native dependencies required to build the plugin properly. -* Any native dependencies should be available in [jcenter](https://bintray.com/bintray/jcenter) or from the Android SDK installed on your machine. - -> **IMPORTANT:** If you don't have an `include.gradle` file, at build time, gradle will create a default one containing all default elements. - -#### Include.gradle Example -```gradle -//default elements -android { - productFlavors { - "my-plugin" { - dimension "my-plugin" - } - } -} - -//optional elements -dependencies { - compile "groupName:pluginName:ver" -} -``` - -### Build.xcconfig Specification -Every NativeScript plugin, which contains native iOS dependencies, can also contain a [valid](https://pewpewthespells.com/blog/xcconfig_guide.html) `build.xcconfig` file in the root of its `platforms\ios` directory. This `build.xcconfig` file might contain native dependencies required to build the plugin properly. - -#### Build.xcconfig Example -``` -OTHER_LDFLAGS = $(inherited) -framework "QuartzCore" -l"sqlite3" -``` - -## Install a Plugin - -To install a plugin for your project, inside your project, run the following command. - -```Shell -tns plugin add -``` - -### Valid Plugin Sources - -You can specify a plugin by name in the npm registry, local path or URL. The following are valid values for the `` attribute. - -* A `` or `@` for plugins published in the npm registry. -* A `` to the directory which contains the plugin files and its `package.json` file. -* A `` to a `.tar.gz` archive containing a directory with the plugin and its `package.json` file. -* A `` which resolves to a `.tar.gz` archive containing a directory with the plugin and its `package.json` file. -* A `` which resolves to a `.tar.gz` archive containing a directory with the plugin and its `package.json` file. - -### Installation Specifics - -The installation of a NativeScript plugin mimics the installation of an npm module. - -The NativeScript CLI takes the plugin and installs it to the `node_modules` directory in the root of your project. During this process, the NativeScript CLI resolves any dependencies described in the plugin `package.json` file and adds the plugin to the project `package.json` file in the project root. - -If the NativeScript CLI detects any native iOS libraries in the plugin, it copies the library files to the `lib\ios` folder in your project and configures the iOS-specific projects in `platforms\ios` to work with the library. - -Next, the NativeScript CLI runs a partial `prepare` operation for the plugin for all platforms configured for the project. During this operation, the CLI copies only the plugin to the `tns_modules` subdirectories in the `platforms\android` and `platforms\ios` directories in your project. If your plugin contains platform-specific `JS` files, the CLI copies them to the respective platform subdirectory and renames them by removing the platform modifier. - -> **TIP:** If you have not configured any platforms, when you run `$ tns platform add`, the NativeScript CLI will automatically prepare all installed plugins for the newly added platform. - -Finally, the CLI merges the plugin `Info.plist` file with `platforms\ios\Info.plist` in your project. The plugin `AndroidManifest.xml` will be merged with `platforms\android\AndroidManifest.xml` later, at build time. - -> **IMPORTANT:** Currently, the merging of the platform configuration files does not resolve any contradicting or duplicate entries. - -#### AndroidManifest.xml Merge Example - -The following is an example of a plugin `AndroidManifest`, project `AndroidManifest.xml` and the resulting merged file after the plugin installation. - -**The Plugin Manifest** - -```XML - - - - - - - - - - - - - - - -``` - -**The Project Manifest Located in `platforms\android\`** - -```XML - - - - - - - - - - - - - - - - - - - - -``` - -**The Merged Manifest Located in `platforms\android\`** - -```XML - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### Manual Steps After Installation - -After the installation is complete, you need to open `platforms\android\AndroidManifest.xml` and `platforms\ios\Info.plist` in your project and inspect them for duplicate or contradicting entries. Make sure to preserve the settings required by the plugin. Otherwise, your app might not build or it might not work as expected, when deployed on device. - -## Use a Plugin - -To use a plugin inside your project, you need to add a `require` in your app. - -```JavaScript -var myPlugin = require("myplugin"); -``` - -This will look for a `myplugin` module with a valid `package.json` file in the `tns_modules` directory. Note that you must require the plugin with the value for the `name` key in the plugin `package.json` file. - -## Remove a Plugin - -To remove a plugin from your project, inside your project, run the following command. - -```Shell -tns plugin remove -``` - -You must specify the plugin by the value for the `name` key in the plugin `package.json` file. - -### Removal Specifics - -The removal of a NativeScript plugin mimics the removal of an npm module. - -The NativeScript CLI removes any plugin files from the `node_modules` directory in the root of your project. During this process, the NativeScript CLI removes any dependencies described in the plugin `package.json` file and removes the plugin from the project `package.json` file in the project root. - -> **IMPORTANT:** For iOS, this operation does not remove files from the `platforms\ios` directories and native iOS libraries, and does not unmerge the `Info.plist` file. For Android, this operation unmerges the `AndroidManifest.xml` file and takes care of removing any plugin files located in `platforms\android`. - -### Manual Steps After Removal - -After the plugin removal is complete, make sure to remove any leftover native iOS library files from the `lib\ios` directory in the root of the project. Update the iOS-specific projects in `platforms\ios` to remove any dependencies on the removed native libraries. - -Next, you need to run the following command. - -```Shell -tns prepare -``` - -Make sure to run the command for all platforms configured for the project. During this operation, the NativeScript CLI will remove any leftover plugin files from your `platforms\ios` directory. - -> **TIP:** Instead of `$ tns prepare` you can run `$ tns build`, `$ tns run`, `$ tns deploy` or `$ tns emulate`. All these commands run `$ tns prepare`. - -Next, open your `platforms\ios\Info.plist` file and remove any leftover entries from the plugin `Info.plist` file. - -Finally, make sure to update your code not to use the uninstalled plugin. diff --git a/PublicAPI.md b/PublicAPI.md index 0434108d54..206287aee4 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -7,6 +7,36 @@ This document describes all methods that can be invoked when NativeScript CLI is const tns = require("nativescript"); ``` +# Contents +* [projectService](#projectservice) + * [createProject](#createproject) + * [isValidNativeScriptProject](#isvalidnativescriptproject) +* [extensibilityService](#extensibilityservice) + * [installExtension](#installextension) + * [uninstallExtension](#uninstallextension) + * [getInstalledExtensions](#getinstalledextensions) + * [loadExtensions](#loadextensions) +* [settingsService](#settingsservice) + * [setSettings](#setsettings) +* [npm](#npm) + * [install](#install) + * [uninstall](#uninstall) + * [search](#search) + * [view](#view) +* [analyticsService](#analyticsservice) + * [startEqatecMonitor](#starteqatecmonitor) +* [debugService](#debugservice) + * [debug](#debug) +* [liveSyncService](#livesyncservice) + * [liveSync](#livesync) + * [stopLiveSync](#stopLiveSync) + * [enableDebugging](#enableDebugging) + * [attachDebugger](#attachDebugger) + * [disableDebugging](#disableDebugging) + * [getLiveSyncDeviceDescriptors](#getLiveSyncDeviceDescriptors) + * [events](#events) + + ## Module projectService `projectService` modules allow you to create new NativeScript application. @@ -94,12 +124,12 @@ interface IExtensionData { ``` ### installExtension -Installs specified extension and loads it in the current process, so the functionality that it adds can be used immediately. +Installs specified extension. * Definition: ```TypeScript /** - * Installs and loads specified extension. + * Installs a specified extension. * @param {string} extensionName Name of the extension to be installed. It may contain version as well, i.e. myPackage, myPackage@1.0.0, myPackage.tgz, https://github.com/myOrganization/myPackage/tarball/master, https://github.com/myOrganization/myPackage etc. * @returns {Promise} Information about installed extensions. */ @@ -180,9 +210,655 @@ for (let promise of loadExtensionsPromises) { } ``` +### loadExtension +Loads a specified extension. + +* Definition +```TypeScript +/** + * Loads a single extension, so its methods and commands can be used from CLI. + * @param {string} extensionName Name of the extension to be installed. It may contain version as well, i.e. myPackage, myPackage@1.0.0 + * A Promise is returned. It will be rejected in case the extension cannot be loaded. + * @returns {Promise} Promise, resolved with IExtensionData. + */ +loadExtension(extensionName: string): Promise; +``` + +* Usage: +```JavaScript +tns.extensibilityService.loadExtension("my-extension") + .then(extensionData => console.log(`Loaded extension: ${extensionData.extensionName}.`), + err => { + console.log(`Failed to load extension: ${err.extensionName}`); + console.log(err); + }); +} +``` + +## settingsService +`settingsService` module provides a way to configure various settings. + +### setSettings +Used to set various settings in order to modify the behavior of some methods. +* Auxiliary interfaces: +```TypeScript +/** + * Describes configuration settings that modify the behavior of some methods. + */ +interface IConfigurationSettings { + /** + * This string will be used when constructing the UserAgent http header. + * @type {string} + */ + userAgentName: string; +} +``` + +* Definition: +```TypeScript +/** + * Describes service used to confugure various settings. + */ +interface ISettingsService { + /** + * Used to set various settings in order to modify the behavior of some methods. + * @param {IConfigurationSettings} settings Settings which will modify the behaviour of some methods. + * @returns {void} + */ + setSettings(settings: IConfigurationSettings): void; +} +``` + +* Usage: +```JavaScript +tns.settingsService.setSettings({ userAgentName: "myUserAgent" }); +``` + +## npm +`npm` module provides a way to interact with npm specifically the use of install, uninstall, search and view commands. + +### install +Installs specified package. Note that you can use the third argument in order to pass different options to the installation like `ignore-scripts`, `save` or `save-exact` which work exactly like they would if you would execute npm from the command line and pass them as `--` flags. +* Auxiliary interfaces: +```TypeScript +/** + * Describes information about installed package. + */ +interface INpmInstallResultInfo { + /** + * Installed package's name. + * @type {string} + */ + name: string; + /** + * Installed package's version. + * @type {string} + */ + version: string; + /** + * The original output that npm CLI produced upon installation. + * @type {INpmInstallCLIResult} + */ + originalOutput: INpmInstallCLIResult; +} +``` + +* Definition: +```TypeScript +/** + * Installs dependency + * @param {string} packageName The name of the dependency - can be a path, a url or a string. + * @param {string} pathToSave The destination of the installation. + * @param {IDictionary} config Additional options that can be passed to manipulate installation. + * @return {Promise} Information about installed package. +*/ +install(packageName: string, pathToSave: string, config: IDictionary): Promise; +``` + +* Usage: +```JavaScript +tns.npm.install("lodash", "/tmp/myProject", { save: true }).then(result => { + console.log(`${result.name} installed successfully`); +}, err => { + console.log("An error occurred during installation", err); +}); +``` + +### uninstall +Uninstalls a specified package. + +* Definition: +```TypeScript +/** + * Uninstalls a dependency + * @param {string} packageName The name of the dependency. + * @param {IDictionary} config Additional options that can be passed to manipulate uninstallation. + * @param {string} path The destination of the uninstallation. + * @return {Promise} The output of the uninstallation. +*/ +uninstall(packageName: string, config?: IDictionary, path?: string): Promise; +``` + +* Usage: +```JavaScript +tns.npm.uninstall("lodash", "/tmp/myProject", { save: true }).then(output => { + console.log(`Uninstalled successfully, output: ${output}`); +}, err => { + console.log("An error occurred during uninstallation", err); +}); +``` + +### search +Searches for a package using keywords. + +* Definition: +```TypeScript +/** + * Searches for a package. + * @param {string[]} filter Keywords with which to perform the search. + * @param {IDictionary} config Additional options that can be passed to manipulate search. + * @return {Promise} The output of the uninstallation. + */ +search(filter: string[], config: IDictionary): Promise; +``` + +* Usage: +```JavaScript +tns.npm.search(["nativescript", "cloud"], { silent: true }).then(output => { + console.log(`Found: ${output}`); +}, err => { + console.log("An error occurred during searching", err); +}); +``` + +### view +Provides information about a given package. + +* Definition +```TypeScript +/** + * Provides information about a given package. + * @param {string} packageName The name of the package. + * @param {IDictionary} config Additional options that can be passed to manipulate view. + * @return {Promise} Object, containing information about the package. + */ +view(packageName: string, config: Object): Promise; +``` + +* Usage: +```JavaScript +tns.npm.view(["nativescript"], {}).then(result => { + console.log(`${result.name}'s latest version is ${result["dist-tags"].latest}`); +}, err => { + console.log("An error occurred during viewing", err); +}); +``` + +## debugService +Provides methods for debugging applications on devices. The service is also event emitter, that raises the following events: +* `connectionError` event - this event is raised when the debug operation cannot start on iOS device. The causes can be: + * Application is not running on the specified iOS Device. + * Application is not built in debug configuration on the specified iOS device. + The event is raised with the following information: +```TypeScript +{ + /** + * Device identifier on which the debug process cannot start. + */ + deviceId: string; + + /** + * The error message. + */ + message: string; + + /** + * Code of the error. + */ + code: number +} +``` + +* Usage: +```JavaScript +tns.debugService.on("connectionError", errorData => { + console.log(`Unable to start debug operation on device ${errorData.deviceId}. Error is: ${errorData.message}.`); +}); +``` + +### debug +The `debug` method allows starting a debug operation for specified application on a specific device. The method returns a Promise, which is resolved with a url. The url should be opened in Chrome DevTools in order to debug the application. + +The returned Promise will be rejected in case any error occurs. It will also be rejected in case: +1. Specified deviceIdentifier is not found in current list of attached devices. +1. The device, specified as deviceIdentifier is connected but not trusted. +1. The specified application is not installed on the device. +1. Trying to debug applications on connected iOS device on Linux. +1. In case the application is not running on the specified device. +1. In case the installed application is not built in debug configuration. + +* Definition: +```TypeScript +/** + * Starts debug operation based on the specified debug data. + * @param {IDebugData} debugData Describes information for device and application that will be debugged. + * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. + * @returns {Promise} Device Identifier, full url and port where the frontend client can be connected. + */ +debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; +``` + +The type of arguments that you can pass are described below: +```TypeScript +/** + * Describes information for starting debug process. + */ +interface IDebugData { + /** + * Id of the device on which the debug process will be started. + */ + deviceIdentifier: string; + + /** + * Application identifier of the app that it will be debugged. + */ + applicationIdentifier: string; + + /** + * Path to .app built for iOS Simulator. + */ + pathToAppPackage?: string; + + /** + * The name of the application, for example `MyProject`. + */ + projectName?: string; + + /** + * Path to project. + */ + projectDir?: string; +} + +/** + * Describes all options that define the behavior of debug. + */ +interface IDebugOptions { + /** + * Defines if bundled Chrome DevTools should be used or specific commit. + * Default value is true for Android and false for iOS. + */ + useBundledDevTools?: boolean; + + /** + * Defines if https://chrome-devtools-frontend.appspot.com should be used instead of chrome-devtools://devtools + * In case it is passed, the value of `useBundledDevTools` is disregarded. + * Default value is false. + */ + useHttpUrl?: boolean; + + /** + * Defines the commit that will be used in cases where remote protocol is required. + * For Android this is the case when useHttpUrl is set to true or useBundledDevTools is set to false. + * For iOS the value is used by default and when useHttpUrl is set to true. + * Default value is 02e6bde1bbe34e43b309d4ef774b1168d25fd024 which corresponds to 55.0.2883 Chrome version + */ + devToolsCommit?: string; + + /** + * Defines if Chrome DevTools should be used for debugging. + */ + chrome?: boolean; + + /** + * Defines if thе application is already started on device. + */ + start?: boolean; +} +``` + +* Usage: +```JavaScript +tns.debugService.on("connectionError", errorData => { + console.log(`Unable to start debug operation on device ${errorData.deviceId}. Error is: ${errorData.message}.`); +}); + +const debugData = { + deviceIdentifier: "4df18f307d8a8f1b", + applicationIdentifier: "com.telerik.app1", + projectName: "app1", + projectDir: "/Users/myUser/app1" +}; + +const debugOptions = { + useBundledDevTools: true +}; + +tns.debugService.debug(debugData, debugOptions) + .then(debugInfo => console.log(`Open the following url in Chrome DevTools: ${debugInfo.url}, port is: ${debugInfo.port} and deviceIdentifier is: ${debugInfo.deviceIdentifier}`)) + .catch(err => console.log(`Unable to start debug operation, reason: ${err.message}.`)); +``` + +## liveSyncService +Used to LiveSync changes on devices. The operation can be started for multiple devices and stopped for each of them. During LiveSync operation, the service will emit different events based on the action that's executing. + +### liveSync +Starts a LiveSync operation for specified devices. During the operation, application may have to be rebuilt (for example in case a change in App_Resources is detected). +By default the LiveSync operation will start file system watcher for `/app` directory and any change in it will trigger a LiveSync operation. +After calling the method once, you can add new devices to the same LiveSync operation by calling the method again with the new device identifiers. + +> NOTE: Consecutive calls to `liveSync` method for the same project will execute the initial sync (deploy and fullSync) only for new device identifiers. So in case the first call is for devices with ids [ 'A' , 'B' ] and the second one is for devices with ids [ 'B', 'C' ], the initial sync will be executed only for device with identifier 'C'. + +> NOTE: In case a consecutive call to `liveSync` method requires change in the pattern for watching files (i.e. `liveSyncData.syncAllFiles` option has changed), current watch operation will be stopped and a new one will be started. + +> NOTE: In case `debugggingEnabled` is set to `true` in a deviceDescriptor, debugging will initially be enabled for that device and a debugger will be attached after a successful livesync operation. + +* Definition +```TypeScript +/** + * Starts LiveSync operation by rebuilding the application if necessary and starting watcher. + * @param {ILiveSyncDeviceInfo[]} deviceDescriptors Describes each device for which we would like to sync the application - identifier, outputPath and action to rebuild the app. + * @param {ILiveSyncInfo} liveSyncData Describes the LiveSync operation - for which project directory is the operation and other settings. + * @returns {Promise} + */ +liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise; +``` + +* Usage: +```JavaScript +const projectDir = "myProjectDir"; +const androidDeviceDescriptor = { + identifier: "4df18f307d8a8f1b", + buildAction: () => { + return tns.localBuildService.build("Android", { projectDir, bundle: false, release: false, buildForDevice: true }); + }, + outputPath: null +}; + +const iOSDeviceDescriptor = { + identifier: "12318af23ebc0e25", + buildAction: () => { + return tns.localBuildService.build("iOS", { projectDir, bundle: false, release: false, buildForDevice: true }); + }, + outputPath: null +}; + +const liveSyncData = { + projectDir, + skipWatcher: false, + watchAllFiles: false, + useLiveEdit: false +}; + +tns.liveSyncService.liveSync([ androidDeviceDescriptor, iOSDeviceDescriptor ], liveSyncData) + .then(() => { + console.log("LiveSync operation started."); + }, err => { + console.log("An error occurred during LiveSync", err); + }); +``` + +### stopLiveSync +Stops LiveSync operation. In case deviceIdentifires are passed, the operation will be stopped only for these devices. + +* Definition +```TypeScript +/** + * Stops LiveSync operation for specified directory. + * @param {string} projectDir The directory for which to stop the operation. + * @param {string[]} @optional deviceIdentifiers Device ids for which to stop the application. In case nothing is passed, LiveSync operation will be stopped for all devices. + * @returns {Promise} + */ +stopLiveSync(projectDir: string, deviceIdentifiers?: string[]): Promise; +``` + +* Usage +```JavaScript +const projectDir = "myProjectDir"; +const deviceIdentifiers = [ "4df18f307d8a8f1b", "12318af23ebc0e25" ]; +tns.liveSyncService.stopLiveSync(projectDir, deviceIdentifiers) + .then(() => { + console.log("LiveSync operation stopped."); + }, err => { + console.log("An error occurred during stopage.", err); + }); +``` + +### enableDebugging +Enables debugging during a LiveSync operation. This method will try to attach a debugger to the application. Note that `userInteractionNeeded` event may be raised. Additional details about the arguments can be seen [here](https://github.com/NativeScript/nativescript-cli/blob/master/lib/definitions/livesync.d.ts). + +* Definition +```TypeScript +/** +* Enables debugging for the specified devices +* @param {IEnableDebuggingDeviceOptions[]} deviceOpts Settings used for enabling debugging for each device. +* @param {IDebuggingAdditionalOptions} enableDebuggingOptions Settings used for enabling debugging. +* @returns {Promise[]} Array of promises for each device. +*/ +enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], enableDebuggingOptions: IDebuggingAdditionalOptions): Promise[]; +``` + +* Usage +```JavaScript +const projectDir = "/tmp/myProject"; +const liveSyncData = { projectDir }; +const devices = [androidDeviceDescriptor, iOSDeviceDescriptor]; +tns.liveSyncService.liveSync(devices, liveSyncData) + .then(() => { + console.log("LiveSync operation started."); + devices.forEach(device => { + tns.liveSyncService.enableDebugging([{ + deviceIdentifier: device.identifier + }], { projectDir }); + }); + }); +``` + +### attachDebugger +Attaches a debugger to the specified device. Additional details about the argument can be seen [here](https://github.com/NativeScript/nativescript-cli/blob/master/lib/definitions/livesync.d.ts). + +* Definition +```TypeScript +/** +* Attaches a debugger to the specified device. +* @param {IAttachDebuggerOptions} settings Settings used for controling the attaching process. +* @returns {Promise} +*/ +attachDebugger(settings: IAttachDebuggerOptions): Promise; +``` + +* Usage +```JavaScript +tns.liveSyncService.on("userInteractionNeeded", data => { + console.log("Please restart the app manually"); + return tns.liveSyncService.attachDebugger(data); +}); +``` + +### disableDebugging +Disables debugging during a LiveSync operation. This method will try to detach a debugger from the application. Additional details about the arguments can be seen [here](https://github.com/NativeScript/nativescript-cli/blob/master/lib/definitions/livesync.d.ts). + +* Definition +```TypeScript +/** +* Disables debugging for the specified devices +* @param {IDisableDebuggingDeviceOptions[]} deviceOptions Settings used for disabling debugging for each device. +* @param {IDebuggingAdditionalOptions} debuggingAdditionalOptions Settings used for disabling debugging. +* @returns {Promise[]} Array of promises for each device. +*/ +disableDebugging(deviceOptions: IDisableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[]; +``` + +* Usage +```JavaScript +const projectDir = "/tmp/myProject"; +const liveSyncData = { projectDir }; +const devices = [androidDeviceDescriptor, iOSDeviceDescriptor]; +tns.liveSyncService.liveSync(devices, liveSyncData) + .then(() => { + console.log("LiveSync operation started."); + devices.forEach(device => { + tns.liveSyncService.enableDebugging([{ + deviceIdentifier: device.identifier + }], { projectDir }); + setTimeout(() => { + tns.liveSyncService.disableDebugging([{ + deviceIdentifier: device.identifier + }], { projectDir }); + }, 1000 * 30); + }); + }); +``` + +### getLiveSyncDeviceDescriptors +Gives information for currently running LiveSync operation and parameters used to start it on each device. + +* Definition +```TypeScript +/** + * Returns the device information for current LiveSync operation of specified project. + * In case LiveSync has been started on many devices, but stopped for some of them at a later point, + * calling the method after that will return information only for devices for which LiveSync operation is in progress. + * @param {string} projectDir The path to project for which the LiveSync operation is executed + * @returns {ILiveSyncDeviceInfo[]} Array of elements describing parameters used to start LiveSync on each device. +*/ +getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[]; +``` + +* Usage +```JavaScript +const projectDir = "myProjectDir"; +const deviceIdentifiers = [ "4df18f307d8a8f1b", "12318af23ebc0e25" ]; +const currentlyRunningDescriptors = tns.liveSyncService.getLiveSyncDeviceDescriptors(projectDir); +console.log(`LiveSync for ${projectDir} is currently running on the following devices: ${currentlyRunningDescriptors.map(descriptor => descriptor.identifier)}`); +``` + +### Events +`liveSyncService` raises several events in order to provide information for current state of the operation. +* liveSyncStarted - raised whenever CLI starts a LiveSync operation for specific device. When `liveSync` method is called, the initial LiveSync operation will emit `liveSyncStarted` for each specified device. After that the event will be emitted only in case when liveSync method is called again with different device instances. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncStarted", data => { + console.log(`Started LiveSync on ${data.deviceIdentifier} for ${data.applicationIdentifier}.`); +}); +``` + +* liveSyncExecuted - raised whenever CLI finishes a LiveSync operation for specific device. When `liveSync` method is called, the initial LiveSync operation will emit `liveSyncExecuted` for each specified device once it finishes the operation. After that the event will be emitted whenever a change is detected (in case file system watcher is started) and the LiveSync operation is executed for each device. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; + /** + * Full paths to files synced during the operation. In case the `syncedFiles.length` is 0, the operation is "fullSync" (i.e. all project files are synced). + */ + syncedFiles: string[]; + isFullSync: boolean; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncExecuted", data => { + console.log(`Executed LiveSync on ${data.deviceIdentifier} for ${data.applicationIdentifier}. Uploaded files are: ${data.syncedFiles.join(" ")}.`); +}); +``` + +* liveSyncStopped - raised when LiveSync operation is stopped. The event will be raised when the operation is stopped for each device and will be raised when the whole operation is stopped. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + /** + * Passed only when the LiveSync operation is stopped for a specific device. In case it is not passed, the whole LiveSync operation is stopped. + */ + deviceIdentifier?: string; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncStopped", data => { + if (data.deviceIdentifier) { + console.log(`Stopped LiveSync on ${data.deviceIdentifier} for ${data.projectDir}.`); + } else { + console.log(`Stopped LiveSync for ${data.projectDir}.`); + } +}); +``` + +* liveSyncError - raised whenever an error is detected during LiveSync operation. The event is raised for specific device. Once an error is detected, the event will be raised and the LiveSync operation will be stopped for this device, i.e. `liveSyncStopped` event will be raised for it. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; + error: Error; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("liveSyncError", data => { + console.log(`Error detected during LiveSync on ${data.deviceIdentifier} for ${data.projectDir}. Error: ${data.error.message}.`); +}); +``` + +* notify - raised when LiveSync operation has some data that is important for the user. The event is raised for specific device. The event is raised with the following data: +```TypeScript +{ + projectDir: string; + deviceIdentifier: string; + applicationIdentifier: string; + notification: string; +} +``` + +Example: +```JavaScript +tns.liveSyncService.on("notify", data => { + console.log(`Notification: ${data.notification} for LiveSync operation on ${data.deviceIdentifier} for ${data.projectDir}. `); +}); +``` + +* userInteractionNeeded - raised whenever CLI needs to restart an application but cannot so the user has to restart it manually. The event is raised with an object, which can later be passed to `attachDebugger` method of `liveSyncService`: + +Example: +```JavaScript +tns.liveSyncService.on("userInteractionNeeded", data => { + console.log("Please restart the app manually"); + return tns.liveSyncService.attachDebugger(data); +}); +``` + +* debuggerAttached - raised whenever CLI attaches the backend debugging socket and a frontend debugging client may be attached. The event is raised with an object containing the device's identifier, url for debugging and port + +Example: +```JavaScript +tns.liveSyncService.on("debuggerAttached", debugInfo => { + console.log(`Backend client connected, frontend client may be connected at ${debugInfo.url} to debug app on device ${debugInfo.deviceIdentifier}. Port is: ${debugInfo.port}`); +}); +``` + +* debuggerDetached - raised whenever CLI detaches the backend debugging socket. The event is raised with an object of the `IDebugInformation` type: + +Example: +```JavaScript +tns.liveSyncService.on("debuggerDetached", debugInfo => { + console.log(`Detached debugger for device with id ${debugInfo.deviceIdentifier}`); +}); +``` + ## How to add a new method to Public API CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification. For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`. More information how to add a method to public API is available [here](https://github.com/telerik/mobile-cli-lib#how-to-make-a-method-public). -After that add each method that you've exposed to the tests in `tests/nativescript-cli-lib.ts` file. There you'll find an object describing each publicly available module and the methods that you can call. +After that add each method that you've exposed to the tests in `tests/nativescript-cli-lib.ts` file. There you'll find an object describing each publicly available module and the methods that you can call. \ No newline at end of file diff --git a/README.md b/README.md index 88fb5de239..06f236e542 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ The NativeScript CLI lets you create, build, and deploy NativeScript-based proje * [Prepare for Build](#prepare-for-build) * [Build Your Project](#build-your-project) * [Deploy Your Project](#deploy-your-project) - * [Emulate Your Project](#emulate-your-project) * [Run Your Project](#run-your-project) * [Extending the CLI](#extending-the-cli) * [Troubleshooting](#troubleshooting) @@ -112,7 +111,6 @@ Quick Start * [Prepare for Build](#prepare-for-build) * [Build Your Project](#build-your-project) * [Deploy Your Project](#deploy-your-project) -* [Emulate Your Project](#emulate-your-project) * [Run Your Project](#run-your-project) ## The Commands @@ -259,6 +257,12 @@ The NativeScript CLI respects any platform configuration files placed inside `ap Additionaly, you can modify `app/App_Resources/build.xcconfig` and `app/App_Resources/app.gradle` for adding/removing additional build properties for iOS and Android, respectively. +### Modifying Entitlements File (iOS only) + +To specify which Capabilities are required by your App - Maps, Push Notifications, Wallet and etc. you can add or edit the `app.entitlements` file placed inside `app/App_Resources/iOS`. When building the project, the default `app/App_Resources/iOS/app.entitlements` file gets merged with all Plugins entitlement files and a new `yourAppName.entitlements` is created in the platforms directory. The path would be `app/platforms/ios//.entitlements` and will be linked in the `build.xcconfig` file. + +You can always override the generated entitlements file, by pointing to your own entitlements file by setting the `CODE_SIGN_ENTITLEMENTS` property in the `app/App_Resources/iOS/build.xcconfig` file. + [Back to Top][1] ## Prepare for Build @@ -377,7 +381,7 @@ tns doctor This command prints warnings about current configuration issues and provides basic information about how to resolve them. -If addressing the configuration issues does not resolve your problem, you can [report an issue](https://github.com/NativeScript/nativescript-cli/blob/master/CONTRIBUTING.md#report-an-issue) or [post in the NativeScript page in Google Groups](https://groups.google.com/forum/#!forum/nativescript). +If addressing the configuration issues does not resolve your problem, you can [report an issue](https://github.com/NativeScript/nativescript-cli/blob/master/CONTRIBUTING.md#report-an-issue) or [post in the NativeScript forums](https://discourse.nativescript.org/). [Back to Top][1] diff --git a/docs/man_pages/device/device-android.md b/docs/man_pages/device/device-android.md index 194b6280f9..2394853e16 100644 --- a/docs/man_pages/device/device-android.md +++ b/docs/man_pages/device/device-android.md @@ -8,6 +8,7 @@ General | `$ tns device android [--timeout ]` Lists all recognized connected Android physical and running virtual devices with serial number and index. <% if(isHtml) { %>If a connected Android device is not shown in the list, make sure that you have installed the required Android USB drivers on your system and that USB debugging is enabled on the device.<% } %> ### Options +* `--available-devices` - Lists all available emulators for Android. * `--timeout` - Sets the time in milliseconds for the operation to search for connected devices before completing. If not set, the default value is 4000. <% if(isHtml) { %>The operation will continue to wait and listen for newly connected devices and will list them after the specified time expires. ### Related Commands diff --git a/docs/man_pages/device/device-ios.md b/docs/man_pages/device/device-ios.md index 1269f967ee..f219c96546 100644 --- a/docs/man_pages/device/device-ios.md +++ b/docs/man_pages/device/device-ios.md @@ -11,6 +11,7 @@ Lists all recognized connected iOS devices with serial number and index. <% if((isConsole && (isWindows || isMacOS)) || isHtml) { %> ### Options +* `--available-devices` - Lists all available emulators for iOS. * `--timeout` - Sets the time in milliseconds for the operation to search for connected devices before completing. If not set, the default value is 4000. <% } %><% if(isHtml) { %>The operation will continue to wait and listen for newly connected devices and will list them after the specified time expires. ### Command Limitations diff --git a/docs/man_pages/device/device.md b/docs/man_pages/device/device.md index 22cdee2814..fe4b30467f 100644 --- a/docs/man_pages/device/device.md +++ b/docs/man_pages/device/device.md @@ -20,6 +20,10 @@ Lists all recognized connected Android <% if(isWindows || isMacOS) { %>or iOS de * You can run `$ tns device ios` on Windows and OS X systems. +### Aliases + +* You can use `$ tns devices` as an alias for `$ tns device`. + ### Related Commands Command | Description diff --git a/docs/man_pages/general/proxy-set.md b/docs/man_pages/general/proxy-set.md index 3ab89f331b..94b2d2735f 100644 --- a/docs/man_pages/general/proxy-set.md +++ b/docs/man_pages/general/proxy-set.md @@ -20,6 +20,9 @@ Sets proxy settings. ### Command Limitations * You can set credentials only on Windows Systems. +* Proxy settings for npm and (Android) Gradle need to be set separately. + * configuring `npm` proxy - https://docs.npmjs.com/misc/config#https-proxy + * (Android) configuring Gradle proxy - set global configuration in the user directory - _/.gradle/gradle.properties_ - https://docs.gradle.org/3.3/userguide/build_environment.html#sec:accessing_the_web_via_a_proxy ### Related Commands diff --git a/docs/man_pages/index.md b/docs/man_pages/index.md index 5a26c9affa..47da22740f 100644 --- a/docs/man_pages/index.md +++ b/docs/man_pages/index.md @@ -29,7 +29,6 @@ Command | Description [prepare ``](project/configuration/prepare.html) | Copies relevant content from the app directory to the subdirectory for the selected target platform to let you build the project. [build ``](project/testing/build.html) | Builds the project for the selected target platform and produces an application package or an emulator package. [deploy ``](project/testing/deploy.html) | Deploys the project to a connected physical or virtual device. -[emulate ``](project/testing/emulate.html) | Deploys the project in the native emulator for the selected target platform. [run ``](project/testing/run.html) | Runs your project on a connected device or in the native emulator, if configured. [debug ``](project/testing/debug.html) | Debugs your project on a connected physical or virtual device. [test init](project/testing/test-init.html) | Configures your project for unit testing with a selected framework. diff --git a/docs/man_pages/project/testing/build-android.md b/docs/man_pages/project/testing/build-android.md index 0226025829..5acd4ca6f4 100644 --- a/docs/man_pages/project/testing/build-android.md +++ b/docs/man_pages/project/testing/build-android.md @@ -34,9 +34,6 @@ Command | Description [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. diff --git a/docs/man_pages/project/testing/build-ios.md b/docs/man_pages/project/testing/build-ios.md index dfd914a625..de0ee38850 100644 --- a/docs/man_pages/project/testing/build-ios.md +++ b/docs/man_pages/project/testing/build-ios.md @@ -3,7 +3,7 @@ build ios Usage | Synopsis ---|--- -General | `$ tns build ios [--for-device] [--release] [--copy-to ]` +General | `$ tns build ios [--for-device] [--release] [--copy-to ] [--provision []]` Builds the project for iOS and produces an `APP` or `IPA` that you can manually deploy in the iOS Simulator or on device, respectively. @@ -15,6 +15,8 @@ Builds the project for iOS and produces an `APP` or `IPA` that you can manually * `--release` - If set, produces a release build. Otherwise, produces a debug build. * `--for-device` - If set, produces an application package that you can deploy on device. Otherwise, produces a build that you can run only in the native iOS Simulator. * `--copy-to` - Specifies the file path where the built `.ipa` will be copied. If it points to a non-existent directory, it will be created. If the specified value is directory, the original file name will be used. +* `--team-id` - If used without parameter, lists all team names and ids. If used with team name or id, it will switch to automatic signing mode and configure the .xcodeproj file of your app. In this case .xcconfig should not contain any provisioning/team id flags. This team id will be further used for codesigning the app. For Xcode 9.0+, xcodebuild will be allowed to update and modify automatically managed provisioning profiles. +* `--provision` - If used without parameter, lists all eligible provisioning profiles. If used with UUID or name of your provisioning profile, it will switch to manual signing mode and configure the .xcodeproj file of your app. In this case xcconfig should not contain any provisioning/team id flags. This provisioning profile will be further used for codesigning the app. <% } %> <% if(isHtml) { %> ### Command Limitations @@ -33,9 +35,6 @@ Command | Description [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. diff --git a/docs/man_pages/project/testing/build.md b/docs/man_pages/project/testing/build.md index a53ed9d2ee..6d7eb33f32 100644 --- a/docs/man_pages/project/testing/build.md +++ b/docs/man_pages/project/testing/build.md @@ -29,9 +29,6 @@ Command | Description [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. diff --git a/docs/man_pages/project/testing/debug-android.md b/docs/man_pages/project/testing/debug-android.md index 0020bb9586..c1c2021fd3 100644 --- a/docs/man_pages/project/testing/debug-android.md +++ b/docs/man_pages/project/testing/debug-android.md @@ -3,54 +3,46 @@ debug android Usage | Synopsis ---|--- -Deploy on device, run the app start Chrome DevTools, and attach the debugger | `$ tns debug android` -Deploy on device, run the app and stop at the first code statement | `$ tns debug android --debug-brk [--device ] [--debug-port ] [--timeout ]` -Deploy in the native emulator, run the app and stop at the first code statement | `$ tns debug android --debug-brk --emulator [] [--timeout ]` -Attach the debug tools to a running app on device | `$ tns debug android --start [--device ] [--debug-port ] [--timeout ]` -Attach the debug tools to a running app in the native emulator | `$ tns debug android --start --emulator [] [--timeout ]` -Detach the debug tools | `$ tns debug android --stop` +Deploy on device/emulator, run the app, follow generated link to use in Chrome Developer Tools, and attach the debugger | `$ tns debug android` +Deploy on device/emulator, run the app and stop at the first code statement | `$ tns debug android --debug-brk [--device ] [--timeout ]` +Deploy in the native emulator, run the app and stop at the first code statement | `$ tns debug android --debug-brk --emulator [--timeout ]` +Attach the debug tools to a running app on device/emulator | `$ tns debug android --start [--device ] [--timeout ]` +Attach the debug tools to a running app in the native emulator | `$ tns debug android --start --emulator [--timeout ]` Prepares, builds and deploys the project when necessary. Debugs your project on a connected device or emulator. -While debugging, prints the output from the application in the console and watches for changes in your code. Once a change is detected, it synchronizes the change with all selected devices and restarts/refreshes the application. +While debugging, prints the output from the application in the console and watches for changes in your code. Once a change is detected, it synchronizes the change with the selected device and restarts the application. ### Options -* `--device` - Specifies a connected device on which to debug the app. -* `--emulator` - Specifies that you want to debug the app in the native Android emulator from the Android SDK. -* `--debug-brk` - Prepares, builds and deploys the application package on a device or in an emulator, launches the Chrome DevTools of your Chrome browser and stops at the first code statement. +* `--device` - Specifies a connected device/emulator on which to debug the app. +* `--emulator` - Specifies that you want to debug the app in the native Android emulator. +* `--debug-brk` - Prepares, builds and deploys the application package on a device/emulator, generates a link for Chrome Developer Tools and stops at the first code statement. * `--start` - Attaches the debug tools to a deployed and running app. -* `--stop` - Detaches the debug tools. -* `--debug-port` - Sets a new port on which to attach the debug tools. -* `--timeout` - Sets the number of seconds that the NativeScript CLI will wait for the debugger to boot. If not set, the default timeout is 90 seconds. +* `--timeout` - Sets the number of seconds that the NativeScript CLI will wait for the emulator/device to boot. If not set, the default timeout is 90 seconds. * `--no-watch` - If set, changes in your code will not be reflected during the execution of this command. * `--clean` - If set, forces rebuilding the native application. ### Attributes -* `` is the index or name of the target device as listed by `$ tns device` -* `` is an accessible port on the device to which you want to attach the debugging tools. -* `` is any valid combination of options as listed by `$ tns help emulate android` +* `` is the device identifier or name of the target device as listed by `$ tns device android` <% if(isHtml) { %> ### Prerequisites -* You must have Chrome installed on your system.
If you are using a non-standard named Chrome app on an OS X system (for example, a nightly Canary update), you need to set this name in the `ANDROID_DEBUG_UI_MAC` setting in the NativeScript [config.json](file:///<%= #{config.getConfigPath(config)} %>). +* You must have Chrome installed on your system.
+ ### Related Commands Command | Description ----------|---------- -[build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. -[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. [build](build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. -[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. +[build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. +[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. -[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. -[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +[device](../../device/device.html) | Lists all connected devices/emulators. +[device android](../../device/device-android.html) | Lists all connected devices/emulators for android. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. +[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [test init](test-init.html) | Configures your project for unit testing with a selected framework. [test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. -[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. <% } %> \ No newline at end of file diff --git a/docs/man_pages/project/testing/debug-ios.md b/docs/man_pages/project/testing/debug-ios.md index 59766b276e..cd0fb05da2 100644 --- a/docs/man_pages/project/testing/debug-ios.md +++ b/docs/man_pages/project/testing/debug-ios.md @@ -3,14 +3,14 @@ debug ios Usage | Synopsis ---|--- -Deploy on device, run the app, start Safari Web Inspector and attach the debugger | `$ tns debug ios` -Deploy on device, run the app and stop at the first code statement | `$ tns debug ios --debug-brk [--device ] [--no-client]` -Deploy in the iOS Simulator, run the app and stop at the first code statement | `$ tns debug ios --debug-brk --emulator [] [--no-client]` -Attach the debug tools to a running app on device | `$ tns debug ios --start [--device ] [--no-client]` -Attach the debug tools to a running app in the iOS Simulator | `$ tns debug ios --start --emulator [] [--no-client]` +Deploy on device/simulator, run the app, start Safari Web Inspector and attache the debugger | `$ tns debug ios` +Deploy on device/simulator, run the app and stop at the first code statement | `$ tns debug ios --debug-brk [--device ] [--no-client]` +Deploy in the iOS simulator, run the app and stop at the first code statement | `$ tns debug ios --debug-brk --emulator [--no-client]` +Attach the debug tools to a running app on specified device or simulator| `$ tns debug ios --start [--device ] [--no-client]` +Attach the debug tools to a running app in the iOS simulator | `$ tns debug ios --start --emulator [--no-client]` -Prepares, builds and deploys the project when necessary. Debugs your project on a connected device or in the iOS Simulator. <% if(isHtml) { %>Any debugging traffic is forwarded on port 8080 from the device to the local machine.<% } %> -While debugging, prints the output from the application in the console and watches for changes in your code. Once a change is detected, it synchronizes the change with all selected devices and restarts/refreshes the application. +Prepares, builds and deploys the project when necessary. Debugs your project on a connected device or in the iOS simulator. <% if(isHtml) { %>Any debugging traffic is forwarded to port 8080( or the next available one) from the device to the local machine.<% } %> +While debugging, prints the output from the application in the console and watches for changes in your code. Once a change is detected, it synchronizes the change with the selected device and restarts the application. <% if(isConsole && (isWindows || isLinux)) { %>WARNING: You can run this command only on OS X systems. To view the complete help for this command, run `$ tns help debug ios`<% } %> @@ -18,23 +18,23 @@ While debugging, prints the output from the application in the console and watch <% if(isHtml) { %>> <% } %>IMPORTANT: Before building for iOS device, verify that you have configured a valid pair of certificate and provisioning profile on your OS X system. <% if(isHtml) { %>For more information, see [Obtaining Signing Identities and Downloading Provisioning Profiles](https://developer.apple.com/library/mac/recipes/xcode_help-accounts_preferences/articles/obtain_certificates_and_provisioning_profiles.html).<% } %> ### Options -* `--device` - Specifies a connected device on which to run the app. +* `--device` - Specifies a connected device or iOS simulator on which to run the app. * `--emulator` - Indicates that you want to debug your app in the iOS simulator. -* `--debug-brk` - Prepares, builds and deploys the application package on a device or in an emulator, runs the app, launches the developer tools of your Safari browser and stops at the first code statement. +* `--debug-brk` - Prepares, builds and deploys the application package on a device or in a simulator, runs the app, launches the developer tools of your Safari browser and stops at the first code statement. * `--start` - Attaches the debug tools to a deployed and running app and launches the developer tools of your Safari browser. -* `--no-client` - If set, the NativeScript CLI attaches the debug tools but does not launch the developer tools in Safari. -* `--timeout` - Sets the number of seconds that NativeScript CLI will wait for the debugger to boot. If not set, the default timeout is 90 seconds. +* `--no-client` - If set, the NativeScript CLI attaches the debug tools but does not launch the developer tools in Safari. Could be used on already started Safari Web Inspector. +* `--timeout` - Sets the number of seconds that NativeScript CLI will wait for the simulator/device to boot. If not set, the default timeout is 90 seconds. * `--no-watch` - If set, changes in your code will not be reflected during the execution of this command. * `--clean` - If set, forces rebuilding the native application. +* `--chrome` - Allows debugging in Chrome Developer Tools. If set Safari Web Inspector is not started and debugging is attached to Chrome Developer Tools. ### Attributes -* `` is the index or name of the target device as listed by `$ tns device` -* `` is any valid combination of options as listed by `$ tns help emulate ios` +* `` is the device identifier of the target device as listed by `$ tns device ios` <% } %> <% if(isHtml) { %> ### Prerequisite -* If you want to debug in the iOS Simulator, you must have Xcode 6 or later installed on your system. +* If you want to debug in the iOS simulator, you must have Xcode 6 or later installed on your system. ### Command Limitations @@ -44,19 +44,15 @@ While debugging, prints the output from the application in the console and watch Command | Description ----------|---------- -[build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. -[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. [build](build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. -[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. +[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS simulator or on device, respectively. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. +[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. -[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. -[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. +[device](../../device/device.html) | Lists all connected devices/emulators. +[device ios](../../device/device-ios.html) | Lists all connected devices/simulators for iOS. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. +[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS simulator, if configured. [test init](test-init.html) | Configures your project for unit testing with a selected framework. -[test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. -[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. +[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS simulator. <% } %> \ No newline at end of file diff --git a/docs/man_pages/project/testing/debug.md b/docs/man_pages/project/testing/debug.md index f0d8d6bde0..f9a7123d0f 100644 --- a/docs/man_pages/project/testing/debug.md +++ b/docs/man_pages/project/testing/debug.md @@ -5,12 +5,12 @@ Usage | Synopsis ---|--- <% if((isConsole && isMacOS) || isHtml) { %>General | `$ tns debug `<% } %><% if(isConsole && (isLinux || isWindows)) { %>General | `$ tns debug android`<% } %> -Debugs your project on a connected device or in a native emulator. <% if(isMacOS) { %>You must specify the target platform on which you want to debug.<% } %> The command will prepare, build and deploy the app when necessary. By default listens for changes in your code, synchronizes those changes and refreshes the selected device. +Debugs your project on a connected device or in a native emulator. <% if(isMacOS) { %>You must specify the target platform on which you want to debug.<% } %> The command will prepare, build and deploy the app when necessary. By default listens for changes in your code, synchronizes those changes and restarts the app on the targeted device. <% if((isConsole && isMacOS) || isHtml) { %>### Attributes -`` is the target mobile platform for which you want to debug your project. You can set the following target platforms. +`` is the target mobile platform for which you want to debug your project. You can set the following target platforms: * `android` - Debugs your project on a connected Android device or Android emulator. -* `ios` - Debugs your project on a connected iOS device or in a native iOS emulator.<% } %> +* `ios` - Debugs your project on a connected iOS device or in a native iOS simulator.<% } %> <% if(isHtml) { %> ### Command Limitations @@ -27,9 +27,6 @@ Command | Description [debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. diff --git a/docs/man_pages/project/testing/deploy.md b/docs/man_pages/project/testing/deploy.md index 7338bbb0fd..4bb3339a90 100644 --- a/docs/man_pages/project/testing/deploy.md +++ b/docs/man_pages/project/testing/deploy.md @@ -44,9 +44,6 @@ Command | Description [debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. diff --git a/docs/man_pages/project/testing/emulate-android.md b/docs/man_pages/project/testing/emulate-android.md deleted file mode 100644 index f5fbcd646b..0000000000 --- a/docs/man_pages/project/testing/emulate-android.md +++ /dev/null @@ -1,59 +0,0 @@ -emulate android -========== - -Usage | Synopsis ----|--- -Run in the native emulator | `$ tns emulate android [--device ] [--path ] [--timeout ] [--key-store-path --key-store-password --key-store-alias --key-store-alias-password ] [--release] [--justlaunch]` -Run in the default Android virtual device or in a currently running emulator | `$ tns emulate android [--path ] [--timeout ] [--key-store-path --key-store-password --key-store-alias --key-store-alias-password ] [--release] [--justlaunch]` - -Builds the specified project and runs it in the native emulator from the Android SDK. While your app is running, prints the output from the application in the console.<% if(isHtml) { %>If you do not select an Android virtual device (AVD) with the `--device` option, your app runs in the default AVD or a currently running emulator, if any. <% } %> - -### Options -* `--available-devices` - Lists all available emulators for Android. -* `--no-watch` - If set, changes in your code will not be reflected during the execution of this command -* `--path` - Specifies the directory that contains the project. If not specified, the project is searched for in the current directory and all directories above it. -* `--device` - Sets the Android virtual device on which you want to run your app. You can set only one device at a time. -* `--timeout` - Sets the number of seconds that the NativeScript CLI will wait for the virtual device to boot before quitting the operation and releasing the console. If not set, the default timeout is 120 seconds. To wait indefinitely, set 0. -* `--release` - If set, produces a release build. Otherwise, produces a debug build. When set, you must also specify the `--key-store-*` options. -* `--key-store-path` - Specifies the file path to the keystore file (P12) which you want to use to code sign your APK. You can use the `--key-store-*` options along with `--release` to produce a signed release build. You need to specify all `--key-store-*` options. -* `--key-store-password` - Provides the password for the keystore file specified with --key-store-path. You can use the `--key-store-*` options along with --release to produce a signed release build. You need to specify all `--key-store-*` options. -* `--key-store-alias` - Provides the alias for the keystore file specified with `--key-store-path`. You can use the `--key-store-*` options along with `--release` to produce a signed release build. You need to specify all `--key-store-*` options. -* `--key-store-alias-password` - Provides the password for the alias specified with `--key-store-alias-password`. You can use the `--key-store-*` options along with `--release` to produce a signed release build. You need to specify all `--key-store-*` options. -* `--justlaunch` - If set, does not print the application output in the console. -* `--clean` - If set, forces rebuilding the native application. - -### Attributes -* `` is the name of the Android virtual device that you want to use as listed by `$ android list avd` - -<% if(isHtml) { %> -### Prerequisites -Before running your app in the Android emulator from the Android SDK, verify that your system meets the following requirements. -* Verify that you have installed the Android SDK. -* Verify that you have added the following Android SDK directories to the `PATH` environment variable: - * `platform-tools` - * `tools` - -### Command Limitations - -* When the `--release` flag is set, you must also specify all `--key-store-*` options. - -### Related Commands - -Command | Description -----------|---------- -[build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. -[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. -[build](build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. -[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. -[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. -[debug](debug.html) | Debugs your project on a connected device or in a native emulator. -[deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. -[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. -[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. -[run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. -[test init](test-init.html) | Configures your project for unit testing with a selected framework. -[test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. -[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. -<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/testing/emulate-ios.md b/docs/man_pages/project/testing/emulate-ios.md deleted file mode 100644 index ad73948968..0000000000 --- a/docs/man_pages/project/testing/emulate-ios.md +++ /dev/null @@ -1,53 +0,0 @@ -emulate ios -========== - -Usage | Synopsis ----|--- -General | `$ tns emulate ios [--path ] [--device ] [--available-devices] [--release] [--timeout]` - -Prepares, builds and deploys the specified project. Runs it in the native iOS Simulator. While your app is running, prints the output from the application in the console and watches for changes in your code. Once a change is detected, it synchronizes the change with all selected devices and restarts/refreshes the application. - -<% if(isConsole && (isLinux || isWindows)) { %>WARNING: You can run this command only on OS X systems. To view the complete help for this command, run `$ tns help emulate ios`<% } %> - -<% if((isConsole && isMacOS) || isHtml) { %>### Options -* `--available-devices` - Lists all available emulators for the current Xcode. -* `--no-watch` - If set, changes in your code will not be reflected during the execution of this command. -* `--release` - If set, produces a release build. Otherwise, produces a debug build. -* `--path` - Specifies the directory that contains the project. If not specified, the project is searched for in the current directory and all directories above it. -* `--device` - Specifies the name of the iOS Simulator device on which you want to run your app. To list the available iOS Simulator devices, run `$ tns emulate ios --available-devices` -* `--timeout` - Sets the number of seconds that the NativeScript CLI will wait for the iOS Simulator to start before quitting the operation and releasing the console. The value must be a positive integer. If not set, the default timeout is 90 seconds. -* `--justlaunch` - If set, does not print the application output in the console. -* `--clean` - If set, forces rebuilding the native application. - -### Attributes -* `` is the name of the iOS Simulator device on which you want to run your app as listed by `$ tns emulate ios --available-devices`<% } %> - -<% if(isHtml) { %> -### Prerequisites -Before running the iOS Simulator, verify that you have met the following requirements. -* You have installed Xcode. The version of Xcode must be compatible with the ios-sim-portable npm package on which the NativeScript CLI depends. For more information, visit [https://www.npmjs.org/package/ios-sim-portable](https://www.npmjs.org/package/ios-sim-portable). - -### Command Limitations - -* You can run `$ tns emulate ios` only on OS X systems. - -### Related Commands - -Command | Description -----------|---------- -[build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. -[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. -[build](build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. -[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. -[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. -[debug](debug.html) | Debugs your project on a connected device or in a native emulator. -[deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate](emulate.html) | You must run the emulate command with a related command. -[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. -[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. -[run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. -[test init](test-init.html) | Configures your project for unit testing with a selected framework. -[test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. -[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. -<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/testing/emulate.md b/docs/man_pages/project/testing/emulate.md deleted file mode 100644 index 52ea563ed9..0000000000 --- a/docs/man_pages/project/testing/emulate.md +++ /dev/null @@ -1,35 +0,0 @@ -emulate -========== - -Usage | Synopsis ----|--- -<% if((isConsole && isMacOS) || isHtml) { %>General | `$ tns emulate `<% } %><% if(isConsole && (isLinux || isWindows)) { %>General | `$ tns emulate android`<% } %> - -Runs the project in the native emulator for the selected target platform. <% if(isMacOS) { %>You must specify the target platform for which you want to build your project.<% } %> The command will prepare, build and deploy the app when necessary. By default listens for changes in your code, synchronizes those changes and refreshes all selected emulators. - -<% if((isConsole && isMacOS) || isHtml) { %>### Attributes -`` is the target mobile platform for which you want to emulate your project. You can set the following target platforms. -* `android` - Builds the specified project and runs it in the native Android emulator. -* `ios` - Builds the specified project and runs it in the native iOS Simulator.<% } %> - -<% if(isHtml) { %> -### Related Commands - -Command | Description -----------|---------- -[build android](build-android.html) | Builds the project for Android and produces an APK that you can manually deploy on device or in the native emulator. -[build ios](build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively. -[build](build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator. -[debug android](debug-android.html) | Debugs your project on a connected Android device or in a native emulator. -[debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. -[debug](debug.html) | Debugs your project on a connected device or in a native emulator. -[deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. -[run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. -[run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. -[test init](test-init.html) | Configures your project for unit testing with a selected framework. -[test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. -[test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. -<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/testing/run-android.md b/docs/man_pages/project/testing/run-android.md index 805e55f866..a02135b7b5 100644 --- a/docs/man_pages/project/testing/run-android.md +++ b/docs/man_pages/project/testing/run-android.md @@ -48,9 +48,6 @@ Command | Description [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. [test init](test-init.html) | Configures your project for unit testing with a selected framework. diff --git a/docs/man_pages/project/testing/run-ios.md b/docs/man_pages/project/testing/run-ios.md index 191bfd9532..c23d638259 100644 --- a/docs/man_pages/project/testing/run-ios.md +++ b/docs/man_pages/project/testing/run-ios.md @@ -47,9 +47,6 @@ Command | Description [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run](run.html) | Runs your project on a connected device or in the native emulator for the selected platform. [test init](test-init.html) | Configures your project for unit testing with a selected framework. diff --git a/docs/man_pages/project/testing/run.md b/docs/man_pages/project/testing/run.md index 1afbff00a3..851cf43611 100644 --- a/docs/man_pages/project/testing/run.md +++ b/docs/man_pages/project/testing/run.md @@ -3,19 +3,24 @@ run Usage | Synopsis ---|--- -<% if((isConsole && isMacOS) || isHtml) { %>General | `$ tns run `<% } %><% if(isConsole && (isLinux || isWindows)) { %>General | `$ tns run android`<% } %> +Run on all connected devices | `$ tns run [--release] [--justlaunch]` +Run on a selected connected device or running emulator. Will start emulator with specified `Device Identifier`, if not already running. | `$ tns run --device [--release] [--justlaunch]` -Runs your project on all connected devices or in native emulators for the selected platform.<% if(isMacOS) { %> You must specify the target platform on which you want to run your project.<% } %><% if(isConsole && (isLinux || isWindows)) { %>You must run `$ tns run android`<% } %> The command will prepare, build and deploy the app when necessary. By default listens for changes in your code, synchronizes those changes and refreshes all selected devices. +Runs your project on all connected devices or in native emulators for the selected platform.<% if(isConsole && (isLinux || isWindows)) { %>The command will work with all currently running Android devices and emulators.<% } %> The command will prepare, build and deploy the app when necessary. By default listens for changes in your code, synchronizes those changes and refreshes all selected devices. -<% if((isConsole && isMacOS) || isHtml) { %>### Attributes -`` is the target mobile platform on which you want to run your project. You can set the following target platforms. -* `android` - Runs your project on a connected Android device, in the native emulator. -* `ios` - Runs your project on a connected iOS device or in the iOS Simulator.<% } %> +### Options +* `--justlaunch` - If set, does not print the application output in the console. +* `--release` - If set, produces a release build. Otherwise, produces a debug build. +* `--device` - Specifies a connected device/emulator to start and run the app. + +### Attributes +* `` is the index or `Device Identifier` of the target device as listed by `$ tns device --available-devices` <% if(isHtml) { %> ### Command Limitations -* You can run `$ tns run ios` only on OS X systems. +* The command will work with all connected devices and running emulators on macOS. On Windows and Linux the command will work with Android devices only. +* In case a platform is not specified and there's no running devices and emulators, the command will fail. ### Related Commands @@ -30,12 +35,9 @@ Command | Description [debug ios](debug-ios.html) | Debugs your project on a connected iOS device or in a native emulator. [debug](debug.html) | Debugs your project on a connected device or in a native emulator. [deploy](deploy.html) | Builds and deploys the project to a connected physical or virtual device. -[emulate android](emulate-android.html) | Builds the specified project and runs it in a native Android emulator. -[emulate ios](emulate-ios.html) | Builds the specified project and runs it in the native iOS Simulator. -[emulate](emulate.html) | You must run the emulate command with a related command. [run android](run-android.html) | Runs your project on a connected Android device or in a native Android emulator, if configured. [run ios](run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured. [test init](test-init.html) | Configures your project for unit testing with a selected framework. [test android](test-android.html) | Runs the tests in your project on Android devices or native emulators. [test ios](test-ios.html) | Runs the tests in your project on iOS devices or the iOS Simulator. -<% } %> \ No newline at end of file +<% } %> diff --git a/lib/android-tools-info.ts b/lib/android-tools-info.ts index e519dab0ec..020b1b3dcd 100644 --- a/lib/android-tools-info.ts +++ b/lib/android-tools-info.ts @@ -5,7 +5,7 @@ import { cache } from "./common/decorators"; export class AndroidToolsInfo implements IAndroidToolsInfo { private static ANDROID_TARGET_PREFIX = "android"; - private static SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22", "android-23", "android-24", "android-25"]; + private static SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22", "android-23", "android-24", "android-25", "android-26"]; private static MIN_REQUIRED_COMPILE_TARGET = 22; private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=23"; private static VERSION_REGEX = /((\d+\.){2}\d+)/; @@ -29,7 +29,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { @cache() public getToolsInfo(): IAndroidToolsInfoData { if (!this.toolsInfo) { - let infoData: IAndroidToolsInfoData = Object.create(null); + const infoData: IAndroidToolsInfoData = Object.create(null); infoData.androidHomeEnvVar = this.androidHome; infoData.compileSdkVersion = this.getCompileSdkVersion(); infoData.buildToolsVersion = this.getBuildToolsVersion(); @@ -46,8 +46,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { public validateInfo(options?: { showWarningsAsErrors: boolean, validateTargetSdk: boolean }): boolean { let detectedErrors = false; this.showWarningsAsErrors = options && options.showWarningsAsErrors; - let toolsInfoData = this.getToolsInfo(); - let isAndroidHomeValid = this.validateAndroidHomeEnvVariable(); + const toolsInfoData = this.getToolsInfo(); + const isAndroidHomeValid = this.validateAndroidHomeEnvVariable(); if (!toolsInfoData.compileSdkVersion) { this.printMessage(`Cannot find a compatible Android SDK for compilation. To be able to build for Android, install Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later.`, `Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage your Android SDK versions.`); @@ -55,8 +55,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } if (!toolsInfoData.buildToolsVersion) { - let buildToolsRange = this.getBuildToolsRange(); - let versionRangeMatches = buildToolsRange.match(/^.*?([\d\.]+)\s+.*?([\d\.]+)$/); + const buildToolsRange = this.getBuildToolsRange(); + const versionRangeMatches = buildToolsRange.match(/^.*?([\d\.]+)\s+.*?([\d\.]+)$/); let message = `You can install any version in the following range: '${buildToolsRange}'.`; // Improve message in case buildToolsRange is something like: ">=22.0.0 <=22.0.0" - same numbers on both sides @@ -83,11 +83,11 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } if (options && options.validateTargetSdk) { - let targetSdk = toolsInfoData.targetSdkVersion; - let newTarget = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${targetSdk}`; + const targetSdk = toolsInfoData.targetSdkVersion; + const newTarget = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${targetSdk}`; if (!_.includes(AndroidToolsInfo.SUPPORTED_TARGETS, newTarget)) { - let supportedVersions = AndroidToolsInfo.SUPPORTED_TARGETS.sort(); - let minSupportedVersion = this.parseAndroidSdkString(_.first(supportedVersions)); + const supportedVersions = AndroidToolsInfo.SUPPORTED_TARGETS.sort(); + const minSupportedVersion = this.parseAndroidSdkString(_.first(supportedVersions)); if (targetSdk && (targetSdk < minSupportedVersion)) { this.printMessage(`The selected Android target SDK ${newTarget} is not supported. You must target ${minSupportedVersion} or later.`); @@ -107,10 +107,10 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { this.showWarningsAsErrors = options.showWarningsAsErrors; } - let additionalMessage = "You will not be able to build your projects for Android." + EOL + const additionalMessage = "You will not be able to build your projects for Android." + EOL + "To be able to build for Android, verify that you have installed The Java Development Kit (JDK) and configured it according to system requirements as" + EOL + " described in " + this.$staticConfig.SYS_REQUIREMENTS_LINK; - let matchingVersion = (installedJavaVersion || "").match(AndroidToolsInfo.VERSION_REGEX); + const matchingVersion = (installedJavaVersion || "").match(AndroidToolsInfo.VERSION_REGEX); if (matchingVersion && matchingVersion[1]) { if (semver.lt(matchingVersion[1], AndroidToolsInfo.MIN_JAVA_VERSION)) { hasProblemWithJavaVersion = true; @@ -126,7 +126,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { public async getPathToAdbFromAndroidHome(): Promise { if (this.androidHome) { - let pathToAdb = path.join(this.androidHome, "platform-tools", "adb"); + const pathToAdb = path.join(this.androidHome, "platform-tools", "adb"); try { await this.$childProcess.execFile(pathToAdb, ["help"]); return pathToAdb; @@ -212,19 +212,19 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private getCompileSdkVersion(): number { if (!this.selectedCompileSdk) { - let userSpecifiedCompileSdk = this.$options.compileSdk; + const userSpecifiedCompileSdk = this.$options.compileSdk; if (userSpecifiedCompileSdk) { - let installedTargets = this.getInstalledTargets(); - let androidCompileSdk = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${userSpecifiedCompileSdk}`; + const installedTargets = this.getInstalledTargets(); + const androidCompileSdk = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${userSpecifiedCompileSdk}`; if (!_.includes(installedTargets, androidCompileSdk)) { this.$errors.failWithoutHelp(`You have specified '${userSpecifiedCompileSdk}' for compile sdk, but it is not installed on your system.`); } this.selectedCompileSdk = userSpecifiedCompileSdk; } else { - let latestValidAndroidTarget = this.getLatestValidAndroidTarget(); + const latestValidAndroidTarget = this.getLatestValidAndroidTarget(); if (latestValidAndroidTarget) { - let integerVersion = this.parseAndroidSdkString(latestValidAndroidTarget); + const integerVersion = this.parseAndroidSdkString(latestValidAndroidTarget); if (integerVersion && integerVersion >= AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET) { this.selectedCompileSdk = integerVersion; @@ -237,7 +237,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } private getTargetSdk(): number { - let targetSdk = this.$options.sdk ? parseInt(this.$options.sdk) : this.getCompileSdkVersion(); + const targetSdk = this.$options.sdk ? parseInt(this.$options.sdk) : this.getCompileSdkVersion(); this.$logger.trace(`Selected targetSdk is: ${targetSdk}`); return targetSdk; } @@ -245,12 +245,12 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private getMatchingDir(pathToDir: string, versionRange: string): string { let selectedVersion: string; if (this.$fs.exists(pathToDir)) { - let subDirs = this.$fs.readDirectory(pathToDir); + const subDirs = this.$fs.readDirectory(pathToDir); this.$logger.trace(`Directories found in ${pathToDir} are ${subDirs.join(", ")}`); - let subDirsVersions = subDirs + const subDirsVersions = subDirs .map(dirName => { - let dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX); + const dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX); if (dirNameGroups) { return dirNameGroups[1]; } @@ -259,7 +259,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { }) .filter(dirName => !!dirName); this.$logger.trace(`Versions found in ${pathToDir} are ${subDirsVersions.join(", ")}`); - let version = semver.maxSatisfying(subDirsVersions, versionRange); + const version = semver.maxSatisfying(subDirsVersions, versionRange); if (version) { selectedVersion = _.find(subDirs, dir => dir.indexOf(version) !== -1); } @@ -275,8 +275,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private getBuildToolsVersion(): string { let buildToolsVersion: string; if (this.androidHome) { - let pathToBuildTools = path.join(this.androidHome, "build-tools"); - let buildToolsRange = this.getBuildToolsRange(); + const pathToBuildTools = path.join(this.androidHome, "build-tools"); + const buildToolsRange = this.getBuildToolsRange(); buildToolsVersion = this.getMatchingDir(pathToBuildTools, buildToolsRange); } @@ -284,7 +284,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } private getAppCompatRange(): string { - let compileSdkVersion = this.getCompileSdkVersion(); + const compileSdkVersion = this.getCompileSdkVersion(); let requiredAppCompatRange: string; if (compileSdkVersion) { requiredAppCompatRange = `>=${compileSdkVersion} <${compileSdkVersion + 1}`; @@ -295,9 +295,9 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private getAndroidSupportRepositoryVersion(): string { let selectedAppCompatVersion: string; - let requiredAppCompatRange = this.getAppCompatRange(); + const requiredAppCompatRange = this.getAppCompatRange(); if (this.androidHome && requiredAppCompatRange) { - let pathToAppCompat = path.join(this.androidHome, "extras", "android", "m2repository", "com", "android", "support", "appcompat-v7"); + const pathToAppCompat = path.join(this.androidHome, "extras", "android", "m2repository", "com", "android", "support", "appcompat-v7"); selectedAppCompatVersion = this.getMatchingDir(pathToAppCompat, requiredAppCompatRange); } @@ -306,7 +306,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } private getLatestValidAndroidTarget(): string { - let installedTargets = this.getInstalledTargets(); + const installedTargets = this.getInstalledTargets(); return _.findLast(AndroidToolsInfo.SUPPORTED_TARGETS.sort(), supportedTarget => _.includes(installedTargets, supportedTarget)); } @@ -317,11 +317,12 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { @cache() private getInstalledTargets(): string[] { let installedTargets: string[] = []; - const pathToInstalledTargets = path.join(this.androidHome, "platforms"); - if (this.$fs.exists(pathToInstalledTargets)) { - installedTargets = this.$fs.readDirectory(pathToInstalledTargets); + if (this.androidHome) { + const pathToInstalledTargets = path.join(this.androidHome, "platforms"); + if (this.$fs.exists(pathToInstalledTargets)) { + installedTargets = this.$fs.readDirectory(pathToInstalledTargets); + } } - this.$logger.trace("Installed Android Targets are: ", installedTargets); return installedTargets; diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index cb26b7ef7d..6d2c0cf6b1 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -9,8 +9,10 @@ $injector.require("projectData", "./project-data"); $injector.require("projectDataService", "./services/project-data-service"); $injector.requirePublic("projectService", "./services/project-service"); $injector.require("androidProjectService", "./services/android-project-service"); +$injector.require("iOSEntitlementsService", "./services/ios-entitlements-service"); $injector.require("iOSProjectService", "./services/ios-project-service"); $injector.require("iOSProvisionService", "./services/ios-provision-service"); +$injector.require("xCConfigService", "./services/xcconfig-service"); $injector.require("cocoapodsService", "./services/cocoapods-service"); @@ -22,12 +24,15 @@ $injector.require("platformsData", "./platforms-data"); $injector.require("platformService", "./services/platform-service"); $injector.require("debugDataService", "./services/debug-data-service"); +$injector.requirePublicClass("debugService", "./services/debug-service"); $injector.require("iOSDebugService", "./services/ios-debug-service"); $injector.require("androidDebugService", "./services/android-debug-service"); $injector.require("userSettingsService", "./services/user-settings-service"); $injector.require("analyticsSettingsService", "./services/analytics-settings-service"); -$injector.require("analyticsService", "./services/analytics-service"); +$injector.require("analyticsService", "./services/analytics/analytics-service"); +$injector.require("eqatecAnalyticsProvider", "./services/analytics/eqatec-analytics-provider"); +$injector.require("googleAnalyticsProvider", "./services/analytics/google-analytics-provider"); $injector.require("emulatorSettingsService", "./services/emulator-settings-service"); @@ -37,6 +42,7 @@ $injector.requireCommand("platform|*list", "./commands/list-platforms"); $injector.requireCommand("platform|add", "./commands/add-platform"); $injector.requireCommand("platform|remove", "./commands/remove-platform"); $injector.requireCommand("platform|update", "./commands/update-platform"); +$injector.requireCommand("run|*all", "./commands/run"); $injector.requireCommand("run|ios", "./commands/run"); $injector.requireCommand("run|android", "./commands/run"); @@ -49,8 +55,6 @@ $injector.requireCommand("clean-app|android", "./commands/clean-app"); $injector.requireCommand("build|ios", "./commands/build"); $injector.requireCommand("build|android", "./commands/build"); $injector.requireCommand("deploy", "./commands/deploy"); -$injector.requireCommand("emulate|android", "./commands/emulate"); -$injector.requireCommand("emulate|ios", "./commands/emulate"); $injector.require("testExecutionService", "./services/test-execution-service"); $injector.requireCommand("dev-test|android", "./commands/test"); @@ -65,16 +69,14 @@ $injector.requireCommand("appstore|upload", "./commands/appstore-upload"); $injector.requireCommand("publish|ios", "./commands/appstore-upload"); $injector.require("itmsTransporterService", "./services/itmstransporter-service"); -$injector.require("npm", "./node-package-manager"); +$injector.requirePublic("npm", "./node-package-manager"); $injector.require("npmInstallationManager", "./npm-installation-manager"); -$injector.require("lockfile", "./lockfile"); $injector.require("dynamicHelpProvider", "./dynamic-help-provider"); $injector.require("mobilePlatformsCapabilities", "./mobile-platforms-capabilities"); $injector.require("commandsServiceProvider", "./providers/commands-service-provider"); $injector.require("deviceAppDataProvider", "./providers/device-app-data-provider"); $injector.require("deviceLogProvider", "./common/mobile/device-log-provider"); -$injector.require("liveSyncProvider", "./providers/livesync-provider"); $injector.require("projectFilesProvider", "./providers/project-files-provider"); $injector.require("nodeModulesBuilder", "./tools/node-modules/node-modules-builder"); @@ -99,14 +101,15 @@ $injector.require("infoService", "./services/info-service"); $injector.requireCommand("info", "./commands/info"); $injector.require("androidToolsInfo", "./android-tools-info"); +$injector.require("devicePathProvider", "./device-path-provider"); $injector.requireCommand("platform|clean", "./commands/platform-clean"); +$injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service"); +$injector.require("liveSyncCommandHelper", "./services/livesync/livesync-command-helper"); +$injector.require("androidLiveSyncService", "./services/livesync/android-livesync-service"); +$injector.require("iOSLiveSyncService", "./services/livesync/ios-livesync-service"); $injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); // The name is used in https://github.com/NativeScript/nativescript-dev-typescript -$injector.require("iosLiveSyncServiceLocator", "./services/livesync/ios-device-livesync-service"); -$injector.require("androidLiveSyncServiceLocator", "./services/livesync/android-device-livesync-service"); -$injector.require("platformLiveSyncService", "./services/livesync/platform-livesync-service"); - $injector.require("sysInfo", "./sys-info"); $injector.require("iOSNotificationService", "./services/ios-notification-service"); @@ -124,6 +127,9 @@ $injector.require("projectChangesService", "./services/project-changes-service") $injector.require("emulatorPlatformService", "./services/emulator-platform-service"); +$injector.require("pbxprojDomXcode", "./node/pbxproj-dom-xcode"); +$injector.require("xcode", "./node/xcode"); + $injector.require("staticConfig", "./config"); $injector.require("requireService", "./services/require-service"); @@ -132,3 +138,6 @@ $injector.requireCommand("extension|*list", "./commands/extensibility/list-exten $injector.requireCommand("extension|install", "./commands/extensibility/install-extension"); $injector.requireCommand("extension|uninstall", "./commands/extensibility/uninstall-extension"); $injector.requirePublic("extensibilityService", "./services/extensibility-service"); + +$injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder"); +$injector.require("subscriptionService", "./services/subscription-service"); diff --git a/lib/commands/add-platform.ts b/lib/commands/add-platform.ts index b41fccf62c..5a597c5d6f 100644 --- a/lib/commands/add-platform.ts +++ b/lib/commands/add-platform.ts @@ -4,12 +4,13 @@ export class AddPlatformCommand implements ICommand { constructor(private $options: IOptions, private $platformService: IPlatformService, private $projectData: IProjectData, + private $platformsData: IPlatformsData, private $errors: IErrors) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - await this.$platformService.addPlatforms(args, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }, this.$options.frameworkPath); + await this.$platformService.addPlatforms(args, this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); } public async canExecute(args: string[]): Promise { @@ -17,7 +18,12 @@ export class AddPlatformCommand implements ICommand { this.$errors.fail("No platform specified. Please specify a platform to add"); } - _.each(args, arg => this.$platformService.validatePlatform(arg, this.$projectData)); + for (const arg of args) { + this.$platformService.validatePlatform(arg, this.$projectData); + const platformData = this.$platformsData.getPlatformData(arg, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + } return true; } diff --git a/lib/commands/appstore-list.ts b/lib/commands/appstore-list.ts index 2ba7b8a435..6313a2f64b 100644 --- a/lib/commands/appstore-list.ts +++ b/lib/commands/appstore-list.ts @@ -7,11 +7,21 @@ export class ListiOSApps implements ICommand { constructor(private $injector: IInjector, private $itmsTransporterService: IITMSTransporterService, private $logger: ILogger, - private $prompter: IPrompter) { } + private $projectData: IProjectData, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $platformService: IPlatformService, + private $errors: IErrors, + private $prompter: IPrompter) { + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { - let username = args[0], - password = args[1]; + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } + + let username = args[0]; + let password = args[1]; if (!username) { username = await this.$prompter.getString("Apple ID", { allowEmpty: false }); @@ -21,12 +31,12 @@ export class ListiOSApps implements ICommand { password = await this.$prompter.getPassword("Apple ID password"); } - let iOSApplications = await this.$itmsTransporterService.getiOSApplications({ username, password }); + const iOSApplications = await this.$itmsTransporterService.getiOSApplications({ username, password }); if (!iOSApplications || !iOSApplications.length) { this.$logger.out("Seems you don't have any applications yet."); } else { - let table: any = createTable(["Application Name", "Bundle Identifier", "Version"], iOSApplications.map(element => { + const table: any = createTable(["Application Name", "Bundle Identifier", "Version"], iOSApplications.map(element => { return [element.name, element.bundleId, element.version]; })); diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 89fa327030..9968d0faf3 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -7,7 +7,6 @@ export class PublishIOS implements ICommand { new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)]; constructor(private $errors: IErrors, - private $hostInfo: IHostInfo, private $injector: IInjector, private $itmsTransporterService: IITMSTransporterService, private $logger: ILogger, @@ -15,8 +14,8 @@ export class PublishIOS implements ICommand { private $options: IOptions, private $prompter: IPrompter, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { - this.$projectData.initializeProjectData(); - } + this.$projectData.initializeProjectData(); + } private get $platformsData(): IPlatformsData { return this.$injector.resolve("platformsData"); @@ -29,12 +28,12 @@ export class PublishIOS implements ICommand { } public async execute(args: string[]): Promise { - let username = args[0], - password = args[1], - mobileProvisionIdentifier = args[2], - codeSignIdentity = args[3], - teamID = this.$options.teamId, - ipaFilePath = this.$options.ipa ? path.resolve(this.$options.ipa) : null; + let username = args[0]; + let password = args[1]; + const mobileProvisionIdentifier = args[2]; + const codeSignIdentity = args[3]; + const teamID = this.$options.teamId; + let ipaFilePath = this.$options.ipa ? path.resolve(this.$options.ipa) : null; if (!username) { username = await this.$prompter.getString("Apple ID", { allowEmpty: false }); @@ -55,11 +54,11 @@ export class PublishIOS implements ICommand { this.$options.release = true; if (!ipaFilePath) { - let platform = this.$devicePlatformsConstants.iOS; + const platform = this.$devicePlatformsConstants.iOS; // No .ipa path provided, build .ipa on out own. const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; if (mobileProvisionIdentifier || codeSignIdentity) { - let iOSBuildConfig: IBuildConfig = { + const iOSBuildConfig: IBuildConfig = { projectDir: this.$options.path, release: this.$options.release, device: this.$options.device, @@ -71,20 +70,20 @@ export class PublishIOS implements ICommand { }; this.$logger.info("Building .ipa with the selected mobile provision and/or certificate."); // This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here. - await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, this.$options); await this.$platformService.buildPlatform(platform, iOSBuildConfig, this.$projectData); ipaFilePath = this.$platformService.lastOutputPath(platform, iOSBuildConfig, this.$projectData); } else { this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission."); - await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, this.$options); - let platformData = this.$platformsData.getPlatformData(platform, this.$projectData); - let iOSProjectService = platformData.platformProjectService; + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const iOSProjectService = platformData.platformProjectService; - let archivePath = await iOSProjectService.archive(this.$projectData); + const archivePath = await iOSProjectService.archive(this.$projectData); this.$logger.info("Archive at: " + archivePath); - let exportPath = await iOSProjectService.exportArchive(this.$projectData, { archivePath, teamID }); + const exportPath = await iOSProjectService.exportArchive(this.$projectData, { archivePath, teamID, provision: mobileProvisionIdentifier || this.$options.provision }); this.$logger.info("Export at: " + exportPath); ipaFilePath = exportPath; @@ -100,8 +99,8 @@ export class PublishIOS implements ICommand { } public async canExecute(args: string[]): Promise { - if (!this.$hostInfo.isDarwin) { - this.$errors.failWithoutHelp("This command is only available on Mac OS X."); + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } return true; diff --git a/lib/commands/build.ts b/lib/commands/build.ts index d8107e9cb7..f03710a2b6 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -1,15 +1,17 @@ export class BuildCommandBase { constructor(protected $options: IOptions, + protected $errors: IErrors, protected $projectData: IProjectData, protected $platformsData: IPlatformsData, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, protected $platformService: IPlatformService) { this.$projectData.initializeProjectData(); } public async executeCore(args: string[]): Promise { - let platform = args[0].toLowerCase(); + const platform = args[0].toLowerCase(); const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, this.$options); this.$options.clean = true; const buildConfig: IBuildConfig = { buildForDevice: this.$options.forDevice, @@ -29,16 +31,24 @@ export class BuildCommandBase { this.$platformService.copyLastOutput(platform, this.$options.copyTo, buildConfig, this.$projectData); } } + + protected validatePlatform(platform: string): void { + if (!this.$platformService.isPlatformSupportedForOS(platform, this.$projectData)) { + this.$errors.fail(`Applications for platform ${platform} can not be built on this OS`); + } + } } export class BuildIosCommand extends BuildCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; constructor(protected $options: IOptions, + $errors: IErrors, $projectData: IProjectData, $platformsData: IPlatformsData, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $platformService: IPlatformService) { - super($options, $projectData, $platformsData, $platformService); + super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService); } public async execute(args: string[]): Promise { @@ -46,7 +56,8 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand { } public canExecute(args: string[]): Promise { - return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + super.validatePlatform(this.$devicePlatformsConstants.iOS); + return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } @@ -56,11 +67,12 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; constructor(protected $options: IOptions, + protected $errors: IErrors, $projectData: IProjectData, $platformsData: IPlatformsData, - private $errors: IErrors, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $platformService: IPlatformService) { - super($options, $projectData, $platformsData, $platformService); + super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService); } public async execute(args: string[]): Promise { @@ -68,10 +80,16 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { } public async canExecute(args: string[]): Promise { + super.validatePlatform(this.$devicePlatformsConstants.Android); if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + + const platformData = this.$platformsData.getPlatformData(this.$devicePlatformsConstants.Android, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + + return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } } diff --git a/lib/commands/clean-app.ts b/lib/commands/clean-app.ts index 699846b120..b32718804d 100644 --- a/lib/commands/clean-app.ts +++ b/lib/commands/clean-app.ts @@ -1,46 +1,64 @@ -export class CleanAppCommandBase { +export class CleanAppCommandBase implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + protected platform: string; + constructor(protected $options: IOptions, protected $projectData: IProjectData, - private $platformService: IPlatformService) { - this.$projectData.initializeProjectData(); - } + protected $platformService: IPlatformService, + protected $errors: IErrors, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $platformsData: IPlatformsData) { + + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { - let platform = args[0].toLowerCase(); const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - return this.$platformService.cleanDestinationApp(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + return this.$platformService.cleanDestinationApp(this.platform.toLowerCase(), appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, this.$options); + } + + public async canExecute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.platform, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.platform} can not be built on this OS`); + } + + const platformData = this.$platformsData.getPlatformData(this.platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + return true; } } export class CleanAppIosCommand extends CleanAppCommandBase implements ICommand { constructor(protected $options: IOptions, - private $platformsData: IPlatformsData, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $platformsData: IPlatformsData, + protected $errors: IErrors, $platformService: IPlatformService, $projectData: IProjectData) { - super($options, $projectData, $platformService); + super($options, $projectData, $platformService, $errors, $devicePlatformsConstants, $platformsData); } - public allowedParameters: ICommandParameter[] = []; - - public async execute(args: string[]): Promise { - return super.execute([this.$platformsData.availablePlatforms.iOS]); + protected get platform(): string { + return this.$devicePlatformsConstants.iOS; } } $injector.registerCommand("clean-app|ios", CleanAppIosCommand); export class CleanAppAndroidCommand extends CleanAppCommandBase implements ICommand { - public allowedParameters: ICommandParameter[] = []; - constructor(protected $options: IOptions, - private $platformsData: IPlatformsData, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $platformsData: IPlatformsData, + protected $errors: IErrors, $platformService: IPlatformService, $projectData: IProjectData) { - super($options, $projectData, $platformService); + super($options, $projectData, $platformService, $errors, $devicePlatformsConstants, $platformsData); } - public async execute(args: string[]): Promise { - return super.execute([this.$platformsData.availablePlatforms.Android]); + protected get platform(): string { + return this.$devicePlatformsConstants.Android; } } diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 2a026f0bb0..28be7548f7 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -1,103 +1,145 @@ -import { EOL } from "os"; +import { CONNECTED_STATUS } from "../common/constants"; +import { isInteractive } from "../common/helpers"; +import { cache } from "../common/decorators"; +import { DebugCommandErrors } from "../constants"; -export abstract class DebugPlatformCommand implements ICommand { +export class DebugPlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; - constructor(private debugService: IPlatformDebugService, - private $devicesService: Mobile.IDevicesService, - private $injector: IInjector, - private $config: IConfiguration, - private $usbLiveSyncService: ILiveSyncService, - private $debugDataService: IDebugDataService, + constructor(private platform: string, + private $debugService: IDebugService, + protected $devicesService: Mobile.IDevicesService, protected $platformService: IPlatformService, protected $projectData: IProjectData, protected $options: IOptions, protected $platformsData: IPlatformsData, - protected $logger: ILogger) { - this.$projectData.initializeProjectData(); + protected $logger: ILogger, + protected $errors: IErrors, + private $debugDataService: IDebugDataService, + private $liveSyncService: IDebugLiveSyncService, + private $prompter: IPrompter, + private $liveSyncCommandHelper: ILiveSyncCommandHelper) { } public async execute(args: string[]): Promise { - const debugOptions = this.$options; - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - platformTemplate: this.$options.platformTemplate, - projectDir: this.$options.path, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId - }; + const debugOptions = _.cloneDeep(this.$options.argv); - let debugData = this.$debugDataService.createDebugData(this.$projectData, this.$options); + const debugData = this.$debugDataService.createDebugData(this.$projectData, this.$options); await this.$platformService.trackProjectType(this.$projectData); + const selectedDeviceForDebug = await this.getDeviceForDebug(); + debugData.deviceIdentifier = selectedDeviceForDebug.deviceInfo.identifier; if (this.$options.start) { - return this.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); + await this.$liveSyncService.printDebugInformation(await this.$debugService.debug(debugData, debugOptions)); + return; } - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; + await this.$devicesService.detectCurrentlyAttachedDevices({ shouldReturnImmediateResult: false, platform: this.platform }); - await this.$platformService.deployPlatform(this.$devicesService.platform, appFilesUpdaterOptions, deployOptions, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); - this.$config.debugLivesync = true; - let applicationReloadAction = async (deviceAppData: Mobile.IDeviceAppData): Promise => { - let projectData: IProjectData = this.$injector.resolve("projectData"); + await this.$liveSyncCommandHelper.executeLiveSyncOperation([selectedDeviceForDebug], this.platform, { + [selectedDeviceForDebug.deviceInfo.identifier]: true + }); + } - await this.debugService.debugStop(); + public async getDeviceForDebug(): Promise { + if (this.$options.forDevice && this.$options.emulator) { + this.$errors.fail(DebugCommandErrors.UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR); + } - let applicationId = deviceAppData.appIdentifier; - await deviceAppData.device.applicationManager.stopApplication(applicationId, projectData.projectName); + await this.$devicesService.detectCurrentlyAttachedDevices({ platform: this.platform, shouldReturnImmediateResult: false }); - const buildConfig: IBuildConfig = _.merge({ buildForDevice: !deviceAppData.device.isEmulator }, deployOptions); - debugData.pathToAppPackage = this.$platformService.lastOutputPath(this.debugService.platform, buildConfig, projectData); + if (this.$options.device) { + const device = await this.$devicesService.getDevice(this.$options.device); + return device; + } - this.printDebugInformation(await this.debugService.debug(debugData, debugOptions)); - }; + // Now let's take data for each device: + const availableDevicesAndEmulators = this.$devicesService.getDeviceInstances() + .filter(d => d.deviceInfo.status === CONNECTED_STATUS && (!this.platform || d.deviceInfo.platform.toLowerCase() === this.platform.toLowerCase())); + + const selectedDevices = availableDevicesAndEmulators.filter(d => this.$options.emulator ? d.isEmulator : (this.$options.forDevice ? !d.isEmulator : true)); + + if (selectedDevices.length > 1) { + if (isInteractive()) { + const choices = selectedDevices.map(e => `${e.deviceInfo.identifier} - ${e.deviceInfo.displayName}`); + + const selectedDeviceString = await this.$prompter.promptForChoice("Select device for debugging", choices); + + const selectedDevice = _.find(selectedDevices, d => `${d.deviceInfo.identifier} - ${d.deviceInfo.displayName}` === selectedDeviceString); + return selectedDevice; + } else { + const sortedInstances = _.sortBy(selectedDevices, e => e.deviceInfo.version); + const emulators = sortedInstances.filter(e => e.isEmulator); + const devices = sortedInstances.filter(d => !d.isEmulator); + let selectedInstance: Mobile.IDevice; + + if (this.$options.emulator || this.$options.forDevice) { + // When --emulator or --forDevice is passed, the instances are already filtered + // So we are sure we have exactly the type we need and we can safely return the last one (highest OS version). + selectedInstance = _.last(sortedInstances); + } else { + if (emulators.length) { + selectedInstance = _.last(emulators); + } else { + selectedInstance = _.last(devices); + } + } + + this.$logger.warn(`Multiple devices/emulators found. Starting debugger on ${selectedInstance.deviceInfo.identifier}. ` + + "If you want to debug on specific device/emulator, you can specify it with --device option."); + + return selectedInstance; + } + } else if (selectedDevices.length === 1) { + return _.head(selectedDevices); + } - return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, this.$projectData, applicationReloadAction); + this.$errors.failWithoutHelp(DebugCommandErrors.NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS); } public async canExecute(args: string[]): Promise { - await this.$devicesService.initialize({ platform: this.debugService.platform, deviceId: this.$options.device }); - // Start emulator if --emulator is selected or no devices found. - if (this.$options.emulator || this.$devicesService.deviceCount === 0) { - return true; + if (!this.$platformService.isPlatformSupportedForOS(this.platform, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.platform} can not be built on this OS`); } - if (this.$devicesService.deviceCount > 1) { - // Starting debugger on emulator. - this.$options.emulator = true; - - this.$logger.warn("Multiple devices found! Starting debugger on emulator. If you want to debug on specific device please select device with --device option.".yellow.bold); + if (this.$options.release) { + this.$errors.fail("--release flag is not applicable to this command"); } - return true; - } + const platformData = this.$platformsData.getPlatformData(this.platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); - protected printDebugInformation(information: string[]): void { - _.each(information, i => { - this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${i}${EOL}`.cyan); + await this.$devicesService.initialize({ + platform: this.platform, + deviceId: this.$options.device, + emulator: this.$options.emulator, + skipDeviceDetectionInterval: true }); + + return true; } } -export class DebugIOSCommand extends DebugPlatformCommand { - constructor(protected $logger: ILogger, - $iOSDebugService: IPlatformDebugService, - $devicesService: Mobile.IDevicesService, - $injector: IInjector, - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $config: IConfiguration, - $usbLiveSyncService: ILiveSyncService, - $debugDataService: IDebugDataService, - $platformService: IPlatformService, - $options: IOptions, - $projectData: IProjectData, - $platformsData: IPlatformsData, +export class DebugIOSCommand implements ICommand { + + @cache() + private get debugPlatformCommand(): DebugPlatformCommand { + return this.$injector.resolve(DebugPlatformCommand, { platform: this.platform }); + } + + public allowedParameters: ICommandParameter[] = []; + + constructor(protected $errors: IErrors, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $platformService: IPlatformService, + private $options: IOptions, + private $injector: IInjector, + private $projectData: IProjectData, + private $platformsData: IPlatformsData, $iosDeviceOperations: IIOSDeviceOperations) { - super($iOSDebugService, $devicesService, $injector, $config, $usbLiveSyncService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger); + this.$projectData.initializeProjectData(); // Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket. // In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket. // That's why the `$ tns debug ios --justlaunch` command will not release the terminal. @@ -105,38 +147,50 @@ export class DebugIOSCommand extends DebugPlatformCommand { $iosDeviceOperations.setShouldDispose(false); } - public async canExecute(args: string[]): Promise { - return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + public execute(args: string[]): Promise { + return this.debugPlatformCommand.execute(args); } - protected printDebugInformation(information: string[]): void { - if (this.$options.chrome) { - super.printDebugInformation(information); + public async canExecute(args: string[]): Promise { + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } + + return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } + + public platform = this.$devicePlatformsConstants.iOS; } $injector.registerCommand("debug|ios", DebugIOSCommand); -export class DebugAndroidCommand extends DebugPlatformCommand { - constructor($logger: ILogger, - $androidDebugService: IPlatformDebugService, - $devicesService: Mobile.IDevicesService, - $injector: IInjector, - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - $config: IConfiguration, - $usbLiveSyncService: ILiveSyncService, - $debugDataService: IDebugDataService, - $platformService: IPlatformService, - $options: IOptions, - $projectData: IProjectData, - $platformsData: IPlatformsData) { - super($androidDebugService, $devicesService, $injector, $config, $usbLiveSyncService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger); +export class DebugAndroidCommand implements ICommand { + + @cache() + private get debugPlatformCommand(): DebugPlatformCommand { + return this.$injector.resolve(DebugPlatformCommand, { platform: this.platform }); } + public allowedParameters: ICommandParameter[] = []; + + constructor(protected $errors: IErrors, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $platformService: IPlatformService, + private $options: IOptions, + private $injector: IInjector, + private $projectData: IProjectData, + private $platformsData: IPlatformsData) { + this.$projectData.initializeProjectData(); + } + + public execute(args: string[]): Promise { + return this.debugPlatformCommand.execute(args); + } public async canExecute(args: string[]): Promise { - return await super.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } + + public platform = this.$devicePlatformsConstants.Android; } $injector.registerCommand("debug|android", DebugAndroidCommand); diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index 66bb49355a..3fc90bc5c7 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -6,7 +6,8 @@ export class DeployOnDeviceCommand implements ICommand { private $options: IOptions, private $projectData: IProjectData, private $errors: IErrors, - private $mobileHelper: Mobile.IMobileHelper) { + private $mobileHelper: Mobile.IMobileHelper, + private $platformsData: IPlatformsData) { this.$projectData.initializeProjectData(); } @@ -27,7 +28,7 @@ export class DeployOnDeviceCommand implements ICommand { keyStorePassword: this.$options.keyStorePassword, keyStorePath: this.$options.keyStorePath }; - return this.$platformService.deployPlatform(args[0], appFilesUpdaterOptions, deployOptions, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + return this.$platformService.deployPlatform(args[0], appFilesUpdaterOptions, deployOptions, this.$projectData, this.$options); } public async canExecute(args: string[]): Promise { @@ -43,7 +44,11 @@ export class DeployOnDeviceCommand implements ICommand { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return this.$platformService.validateOptions(this.$options.provision, this.$projectData, args[0]); + const platformData = this.$platformsData.getPlatformData(args[0], this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + + return this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, args[0]); } } diff --git a/lib/commands/emulate.ts b/lib/commands/emulate.ts deleted file mode 100644 index bdc9429723..0000000000 --- a/lib/commands/emulate.ts +++ /dev/null @@ -1,68 +0,0 @@ -export class EmulateCommandBase { - constructor(private $options: IOptions, - private $projectData: IProjectData, - private $logger: ILogger, - private $platformService: IPlatformService) { - this.$projectData.initializeProjectData(); - } - - public async executeCore(args: string[]): Promise { - this.$logger.warn(`Emulate command is deprecated and will soon be removed. Please use "tns run " instead. All options available for "tns emulate" are present in "tns run" command. To run on all available emulators, use "tns run --emulator".`); - this.$options.emulator = true; - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - const emulateOptions: IEmulatePlatformOptions = { - avd: this.$options.avd, - clean: this.$options.clean, - device: this.$options.device, - release: this.$options.release, - emulator: this.$options.emulator, - projectDir: this.$options.path, - justlaunch: this.$options.justlaunch, - availableDevices: this.$options.availableDevices, - platformTemplate: this.$options.platformTemplate, - provision: this.$options.provision, - teamId: this.$options.teamId, - keyStoreAlias: this.$options.keyStoreAlias, - keyStoreAliasPassword: this.$options.keyStoreAliasPassword, - keyStorePassword: this.$options.keyStorePassword, - keyStorePath: this.$options.keyStorePath - }; - return this.$platformService.emulatePlatform(args[0], appFilesUpdaterOptions, emulateOptions, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); - } -} - -export class EmulateIosCommand extends EmulateCommandBase implements ICommand { - public allowedParameters: ICommandParameter[] = []; - - constructor($options: IOptions, - $projectData: IProjectData, - $logger: ILogger, - $platformService: IPlatformService, - private $platformsData: IPlatformsData) { - super($options, $projectData, $logger, $platformService); - } - - public async execute(args: string[]): Promise { - return this.executeCore([this.$platformsData.availablePlatforms.iOS]); - } -} - -$injector.registerCommand("emulate|ios", EmulateIosCommand); - -export class EmulateAndroidCommand extends EmulateCommandBase implements ICommand { - constructor($options: IOptions, - $projectData: IProjectData, - $logger: ILogger, - $platformService: IPlatformService, - private $platformsData: IPlatformsData) { - super($options, $projectData, $logger, $platformService); - } - - public allowedParameters: ICommandParameter[] = []; - - public async execute(args: string[]): Promise { - return this.executeCore([this.$platformsData.availablePlatforms.Android]); - } -} - -$injector.registerCommand("emulate|android", EmulateAndroidCommand); diff --git a/lib/commands/extensibility/install-extension.ts b/lib/commands/extensibility/install-extension.ts index 91bea2a76a..08513d67bc 100644 --- a/lib/commands/extensibility/install-extension.ts +++ b/lib/commands/extensibility/install-extension.ts @@ -6,6 +6,9 @@ export class InstallExtensionCommand implements ICommand { public async execute(args: string[]): Promise { const extensionData = await this.$extensibilityService.installExtension(args[0]); this.$logger.info(`Successfully installed extension ${extensionData.extensionName}.`); + + await this.$extensibilityService.loadExtension(extensionData.extensionName); + this.$logger.info(`Successfully loaded extension ${extensionData.extensionName}.`); } allowedParameters: ICommandParameter[] = [this.$stringParameterBuilder.createMandatoryParameter("You have to provide a valid name for extension that you want to install.")]; diff --git a/lib/commands/install.ts b/lib/commands/install.ts index a39423ec9c..69be170ea7 100644 --- a/lib/commands/install.ts +++ b/lib/commands/install.ts @@ -14,8 +14,8 @@ export class InstallCommand implements ICommand { private $fs: IFileSystem, private $stringParameter: ICommandParameter, private $npm: INodePackageManager) { - this.$projectData.initializeProjectData(); - } + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { return args[0] ? this.installModule(args[0]) : this.installProjectDependencies(); @@ -26,12 +26,15 @@ export class InstallCommand implements ICommand { await this.$pluginsService.ensureAllDependenciesAreInstalled(this.$projectData); - for (let platform of this.$platformsData.platformsNames) { - let platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + for (const platform of this.$platformsData.platformsNames) { + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); const frameworkPackageData = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName); if (frameworkPackageData && frameworkPackageData.version) { try { - await this.$platformService.addPlatforms([`${platform}@${frameworkPackageData.version}`], this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }, this.$options.frameworkPath); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + + await this.$platformService.addPlatforms([`${platform}@${frameworkPackageData.version}`], this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); } catch (err) { error = `${error}${EOL}${err}`; } @@ -44,14 +47,20 @@ export class InstallCommand implements ICommand { } private async installModule(moduleName: string): Promise { - let projectDir = this.$projectData.projectDir; + const projectDir = this.$projectData.projectDir; - let devPrefix = 'nativescript-dev-'; + const devPrefix = 'nativescript-dev-'; if (!this.$fs.exists(moduleName) && moduleName.indexOf(devPrefix) !== 0) { moduleName = devPrefix + moduleName; } - await this.$npm.install(moduleName, projectDir, { 'save-dev': true }); + await this.$npm.install(moduleName, projectDir, { + 'save-dev': true, + disableNpmInstall: this.$options.disableNpmInstall, + frameworkPath: this.$options.frameworkPath, + ignoreScripts: this.$options.ignoreScripts, + path: this.$options.path + }); } } diff --git a/lib/commands/list-platforms.ts b/lib/commands/list-platforms.ts index a90326b717..7210609be8 100644 --- a/lib/commands/list-platforms.ts +++ b/lib/commands/list-platforms.ts @@ -10,10 +10,10 @@ export class ListPlatformsCommand implements ICommand { } public async execute(args: string[]): Promise { - let installedPlatforms = this.$platformService.getInstalledPlatforms(this.$projectData); + const installedPlatforms = this.$platformService.getInstalledPlatforms(this.$projectData); if (installedPlatforms.length > 0) { - let preparedPlatforms = this.$platformService.getPreparedPlatforms(this.$projectData); + const preparedPlatforms = this.$platformService.getPreparedPlatforms(this.$projectData); if (preparedPlatforms.length > 0) { this.$logger.out("The project is prepared for: ", helpers.formatListOfNames(preparedPlatforms, "and")); } else { @@ -22,7 +22,7 @@ export class ListPlatformsCommand implements ICommand { this.$logger.out("Installed platforms: ", helpers.formatListOfNames(installedPlatforms, "and")); } else { - let formattedPlatformsList = helpers.formatListOfNames(this.$platformService.getAvailablePlatforms(this.$projectData), "and"); + const formattedPlatformsList = helpers.formatListOfNames(this.$platformService.getAvailablePlatforms(this.$projectData), "and"); this.$logger.out("Available platforms for this OS: ", formattedPlatformsList); this.$logger.out("No installed platforms found. Use $ tns platform add"); } diff --git a/lib/commands/platform-clean.ts b/lib/commands/platform-clean.ts index 579e4b1040..5542372de9 100644 --- a/lib/commands/platform-clean.ts +++ b/lib/commands/platform-clean.ts @@ -4,12 +4,13 @@ export class CleanCommand implements ICommand { constructor(private $options: IOptions, private $projectData: IProjectData, private $platformService: IPlatformService, - private $errors: IErrors) { - this.$projectData.initializeProjectData(); - } + private $errors: IErrors, + private $platformsData: IPlatformsData) { + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { - await this.$platformService.cleanPlatforms(args, this.$options.platformTemplate, this.$projectData, {provision: this.$options.provision, sdk: this.$options.sdk }); + await this.$platformService.cleanPlatforms(args, this.$options.platformTemplate, this.$projectData, this.$options); } public async canExecute(args: string[]): Promise { @@ -17,7 +18,13 @@ export class CleanCommand implements ICommand { this.$errors.fail("No platform specified. Please specify a platform to clean"); } - _.each(args, arg => this.$platformService.validatePlatformInstalled(arg, this.$projectData)); + for (const platform of args) { + this.$platformService.validatePlatformInstalled(platform, this.$projectData); + + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + } return true; } diff --git a/lib/commands/plugin/add-plugin.ts b/lib/commands/plugin/add-plugin.ts index ed9fd44d3d..a2830b007d 100644 --- a/lib/commands/plugin/add-plugin.ts +++ b/lib/commands/plugin/add-plugin.ts @@ -16,8 +16,8 @@ export class AddPluginCommand implements ICommand { this.$errors.fail("You must specify plugin name."); } - let installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); - let pluginName = args[0].toLowerCase(); + const installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + const pluginName = args[0].toLowerCase(); if (_.some(installedPlugins, (plugin: IPluginData) => plugin.name.toLowerCase() === pluginName)) { this.$errors.failWithoutHelp(`Plugin "${pluginName}" is already installed.`); } diff --git a/lib/commands/plugin/list-plugins.ts b/lib/commands/plugin/list-plugins.ts index cc5d52e480..f9afa9c316 100644 --- a/lib/commands/plugin/list-plugins.ts +++ b/lib/commands/plugin/list-plugins.ts @@ -10,19 +10,19 @@ export class ListPluginsCommand implements ICommand { } public async execute(args: string[]): Promise { - let installedPlugins: IPackageJsonDepedenciesResult = this.$pluginsService.getDependenciesFromPackageJson(this.$projectData.projectDir); + const installedPlugins: IPackageJsonDepedenciesResult = this.$pluginsService.getDependenciesFromPackageJson(this.$projectData.projectDir); - let headers: string[] = ["Plugin", "Version"]; - let dependenciesData: string[][] = this.createTableCells(installedPlugins.dependencies); + const headers: string[] = ["Plugin", "Version"]; + const dependenciesData: string[][] = this.createTableCells(installedPlugins.dependencies); - let dependenciesTable: any = createTable(headers, dependenciesData); + const dependenciesTable: any = createTable(headers, dependenciesData); this.$logger.out("Dependencies:"); this.$logger.out(dependenciesTable.toString()); if (installedPlugins.devDependencies && installedPlugins.devDependencies.length) { - let devDependenciesData: string[][] = this.createTableCells(installedPlugins.devDependencies); + const devDependenciesData: string[][] = this.createTableCells(installedPlugins.devDependencies); - let devDependenciesTable: any = createTable(headers, devDependenciesData); + const devDependenciesTable: any = createTable(headers, devDependenciesData); this.$logger.out("Dev Dependencies:"); this.$logger.out(devDependenciesTable.toString()); @@ -30,8 +30,8 @@ export class ListPluginsCommand implements ICommand { this.$logger.out("There are no dev dependencies."); } - let viewDependenciesCommand: string = "npm view grep dependencies".cyan.toString(); - let viewDevDependenciesCommand: string = "npm view grep devDependencies".cyan.toString(); + const viewDependenciesCommand: string = "npm view grep dependencies".cyan.toString(); + const viewDevDependenciesCommand: string = "npm view grep devDependencies".cyan.toString(); this.$logger.warn("NOTE:"); this.$logger.warn(`If you want to check the dependencies of installed plugin use ${viewDependenciesCommand}`); diff --git a/lib/commands/plugin/remove-plugin.ts b/lib/commands/plugin/remove-plugin.ts index 9bd07e9a28..9edd83df22 100644 --- a/lib/commands/plugin/remove-plugin.ts +++ b/lib/commands/plugin/remove-plugin.ts @@ -20,14 +20,14 @@ export class RemovePluginCommand implements ICommand { let pluginNames: string[] = []; try { // try installing the plugins, so we can get information from node_modules about their native code, libs, etc. - let installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + const installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); pluginNames = installedPlugins.map(pl => pl.name); } catch (err) { this.$logger.trace("Error while installing plugins. Error is:", err); pluginNames = _.keys(this.$projectData.dependencies); } - let pluginName = args[0].toLowerCase(); + const pluginName = args[0].toLowerCase(); if (!_.some(pluginNames, name => name.toLowerCase() === pluginName)) { this.$errors.failWithoutHelp(`Plugin "${pluginName}" is not installed.`); } diff --git a/lib/commands/plugin/update-plugin.ts b/lib/commands/plugin/update-plugin.ts index b0d03d1941..d0b9db4f83 100644 --- a/lib/commands/plugin/update-plugin.ts +++ b/lib/commands/plugin/update-plugin.ts @@ -9,11 +9,11 @@ export class UpdatePluginCommand implements ICommand { let pluginNames = args; if (!pluginNames || args.length === 0) { - let installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + const installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); pluginNames = installedPlugins.map(p => p.name); } - for (let pluginName of pluginNames) { + for (const pluginName of pluginNames) { await this.$pluginsService.remove(pluginName, this.$projectData); await this.$pluginsService.add(pluginName, this.$projectData); } @@ -24,10 +24,10 @@ export class UpdatePluginCommand implements ICommand { return true; } - let installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); - let installedPluginNames: string[] = installedPlugins.map(pl => pl.name); + const installedPlugins = await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + const installedPluginNames: string[] = installedPlugins.map(pl => pl.name); - let pluginName = args[0].toLowerCase(); + const pluginName = args[0].toLowerCase(); if (!_.some(installedPluginNames, name => name.toLowerCase() === pluginName)) { this.$errors.failWithoutHelp(`Plugin "${pluginName}" is not installed.`); } diff --git a/lib/commands/post-install.ts b/lib/commands/post-install.ts index 87263def0c..779703e5c3 100644 --- a/lib/commands/post-install.ts +++ b/lib/commands/post-install.ts @@ -1,15 +1,8 @@ import { PostInstallCommand } from "../common/commands/post-install"; -import * as emailValidator from "email-validator"; -import * as queryString from "querystring"; -import * as helpers from "../common/helpers"; export class PostInstallCliCommand extends PostInstallCommand { - private logger: ILogger; - constructor($fs: IFileSystem, - private $httpClient: Server.IHttpClient, - private $prompter: IPrompter, - private $userSettingsService: IUserSettingsService, + private $subscriptionService: ISubscriptionService, $staticConfig: Config.IStaticConfig, $commandsService: ICommandsService, $htmlHelpService: IHtmlHelpService, @@ -18,63 +11,12 @@ export class PostInstallCliCommand extends PostInstallCommand { $analyticsService: IAnalyticsService, $logger: ILogger) { super($fs, $staticConfig, $commandsService, $htmlHelpService, $options, $doctorService, $analyticsService, $logger); - this.logger = $logger; } public async execute(args: string[]): Promise { await super.execute(args); - if (await this.shouldAskForEmail()) { - this.logger.out("Leave your e-mail address here to subscribe for NativeScript newsletter and product updates, tips and tricks:"); - let email = await this.getEmail("(press Enter for blank)"); - await this.$userSettingsService.saveSetting("EMAIL_REGISTERED", true); - await this.sendEmail(email); - } - } - - private async shouldAskForEmail(): Promise { - return helpers.isInteractive() && await process.env.CLI_NOPROMPT !== "1" && !this.$userSettingsService.getSettingValue("EMAIL_REGISTERED"); - } - - private async getEmail(prompt: string, options?: IPrompterOptions): Promise { - let schema: IPromptSchema = { - message: prompt, - type: "input", - name: "inputEmail", - validate: (value: any) => { - if (value === "" || emailValidator.validate(value)) { - return true; - } - return "Please provide a valid e-mail or simply leave it blank."; - }, - default: options && options.defaultAction - }; - - let result = await this.$prompter.get([schema]); - return result.inputEmail; - } - - private async sendEmail(email: string): Promise { - if (email) { - let postData = queryString.stringify({ - 'elqFormName': "dev_uins_cli", - 'elqSiteID': '1325', - 'emailAddress': email, - 'elqCookieWrite': '0' - }); - - let options = { - url: 'https://s1325.t.eloqua.com/e/f2', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length - }, - body: postData - }; - - await this.$httpClient.httpRequest(options); - } + await this.$subscriptionService.subscribeForNewsletter(); } } diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index 648771aba3..489ebcd358 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -4,17 +4,26 @@ export class PrepareCommand implements ICommand { constructor(private $options: IOptions, private $platformService: IPlatformService, private $projectData: IProjectData, - private $platformCommandParameter: ICommandParameter) { - this.$projectData.initializeProjectData(); - } + private $platformCommandParameter: ICommandParameter, + private $platformsData: IPlatformsData) { + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - await this.$platformService.preparePlatform(args[0], appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + await this.$platformService.preparePlatform(args[0], appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, this.$options); } public async canExecute(args: string[]): Promise { - return await this.$platformCommandParameter.validate(args[0]) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, args[0]); + const platform = args[0]; + const result = await this.$platformCommandParameter.validate(platform) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); + if (result) { + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + } + + return result; } } diff --git a/lib/commands/remove-platform.ts b/lib/commands/remove-platform.ts index 2432525753..e6132e14bf 100644 --- a/lib/commands/remove-platform.ts +++ b/lib/commands/remove-platform.ts @@ -3,9 +3,10 @@ export class RemovePlatformCommand implements ICommand { constructor(private $platformService: IPlatformService, private $projectData: IProjectData, - private $errors: IErrors) { - this.$projectData.initializeProjectData(); - } + private $errors: IErrors, + private $platformsData: IPlatformsData) { + this.$projectData.initializeProjectData(); + } public execute(args: string[]): Promise { return this.$platformService.removePlatforms(args, this.$projectData); @@ -16,7 +17,12 @@ export class RemovePlatformCommand implements ICommand { this.$errors.fail("No platform specified. Please specify a platform to remove"); } - _.each(args, arg => this.$platformService.validatePlatformInstalled(arg, this.$projectData)); + for (const platform of args) { + this.$platformService.validatePlatformInstalled(platform, this.$projectData); + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + } return true; } diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 753c90e7fa..505552f685 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -1,96 +1,145 @@ -export class RunCommandBase { +import { ERROR_NO_VALID_SUBCOMMAND_FORMAT } from "../common/constants"; +import { cache } from "../common/decorators"; + +export class RunCommandBase implements ICommand { + + public platform: string; constructor(protected $platformService: IPlatformService, - protected $usbLiveSyncService: ILiveSyncService, protected $projectData: IProjectData, protected $options: IOptions, - protected $emulatorPlatformService: IEmulatorPlatformService) { - this.$projectData.initializeProjectData(); + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + protected $devicesService: Mobile.IDevicesService, + protected $platformsData: IPlatformsData, + private $hostInfo: IHostInfo, + private $liveSyncCommandHelper: ILiveSyncCommandHelper + ) { } + + public allowedParameters: ICommandParameter[] = []; + public async execute(args: string[]): Promise { + return this.executeCore(args); + } + + public async canExecute(args: string[]): Promise { + if (args.length) { + this.$errors.fail(ERROR_NO_VALID_SUBCOMMAND_FORMAT, "run"); } - public async executeCore(args: string[]): Promise { + this.$projectData.initializeProjectData(); + this.platform = args[0] || this.platform; - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - const deployOptions: IDeployPlatformOptions = { - clean: this.$options.clean, - device: this.$options.device, - emulator: this.$options.emulator, - projectDir: this.$options.path, - platformTemplate: this.$options.platformTemplate, - release: this.$options.release, - provision: this.$options.provision, - teamId: this.$options.teamId, - keyStoreAlias: this.$options.keyStoreAlias, - keyStoreAliasPassword: this.$options.keyStoreAliasPassword, - keyStorePassword: this.$options.keyStorePassword, - keyStorePath: this.$options.keyStorePath - }; - - await this.$platformService.deployPlatform(args[0], appFilesUpdaterOptions, deployOptions, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + if (!this.platform && !this.$hostInfo.isDarwin) { + this.platform = this.$devicePlatformsConstants.Android; + } - if (this.$options.bundle) { - this.$options.watch = false; + const availablePlatforms = this.$liveSyncCommandHelper.getPlatformsForOperation(this.platform); + for (const platform of availablePlatforms) { + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); } - if (this.$options.release) { - const deployOpts: IRunPlatformOptions = { - device: this.$options.device, - emulator: this.$options.emulator, - justlaunch: this.$options.justlaunch, - }; + return true; + } - await this.$platformService.startApplication(args[0], deployOpts, this.$projectData.projectId); - return this.$platformService.trackProjectType(this.$projectData); + public async executeCore(args: string[]): Promise { + if (this.$options.bundle) { + this.$options.watch = false; } - return this.$usbLiveSyncService.liveSync(args[0], this.$projectData); + await this.$devicesService.initialize({ + deviceId: this.$options.device, + platform: this.platform, + emulator: this.$options.emulator, + skipDeviceDetectionInterval: true, + skipInferPlatform: !this.platform + }); + + await this.$devicesService.detectCurrentlyAttachedDevices({ shouldReturnImmediateResult: false, platform: this.platform }); + let devices = this.$devicesService.getDeviceInstances(); + devices = devices.filter(d => !this.platform || d.deviceInfo.platform.toLowerCase() === this.platform.toLowerCase()); + await this.$liveSyncCommandHelper.executeLiveSyncOperation(devices, this.platform); } } -export class RunIosCommand extends RunCommandBase implements ICommand { +$injector.registerCommand("run|*all", RunCommandBase); + +export class RunIosCommand implements ICommand { + + @cache() + private get runCommand(): RunCommandBase { + const runCommand = this.$injector.resolve(RunCommandBase); + runCommand.platform = this.platform; + return runCommand; + } + public allowedParameters: ICommandParameter[] = []; + public get platform(): string { + return this.$devicePlatformsConstants.iOS; + } - constructor($platformService: IPlatformService, - private $platformsData: IPlatformsData, - $usbLiveSyncService: ILiveSyncService, - $projectData: IProjectData, - $options: IOptions, - $emulatorPlatformService: IEmulatorPlatformService) { - super($platformService, $usbLiveSyncService, $projectData, $options, $emulatorPlatformService); + constructor(protected $platformsData: IPlatformsData, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + private $injector: IInjector, + private $platformService: IPlatformService, + private $projectData: IProjectData, + private $options: IOptions) { } public async execute(args: string[]): Promise { - return this.executeCore([this.$platformsData.availablePlatforms.iOS]); + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); + } + + return this.runCommand.executeCore(args); } public async canExecute(args: string[]): Promise { - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return await this.runCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } $injector.registerCommand("run|ios", RunIosCommand); -export class RunAndroidCommand extends RunCommandBase implements ICommand { +export class RunAndroidCommand implements ICommand { + + @cache() + private get runCommand(): RunCommandBase { + const runCommand = this.$injector.resolve(RunCommandBase); + runCommand.platform = this.platform; + return runCommand; + } + public allowedParameters: ICommandParameter[] = []; + public get platform(): string { + return this.$devicePlatformsConstants.Android; + } - constructor($platformService: IPlatformService, - private $platformsData: IPlatformsData, - $usbLiveSyncService: ILiveSyncService, - $projectData: IProjectData, - $options: IOptions, - $emulatorPlatformService: IEmulatorPlatformService, - private $errors: IErrors) { - super($platformService, $usbLiveSyncService, $projectData, $options, $emulatorPlatformService); + constructor(protected $platformsData: IPlatformsData, + protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + protected $errors: IErrors, + private $injector: IInjector, + private $platformService: IPlatformService, + private $projectData: IProjectData, + private $options: IOptions) { } public async execute(args: string[]): Promise { - return this.executeCore([this.$platformsData.availablePlatforms.Android]); + return this.runCommand.executeCore(args); } public async canExecute(args: string[]): Promise { + await this.runCommand.canExecute(args); + + if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.Android, this.$projectData)) { + this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.Android} can not be built on this OS`); + } + if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } } diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 69736ac8a7..2cf77223a5 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -17,25 +17,29 @@ class TestInitCommand implements ICommand { private $resources: IResourceLoader, private $pluginsService: IPluginsService, private $logger: ILogger) { - this.$projectData.initializeProjectData(); - } + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { - let projectDir = this.$projectData.projectDir; + const projectDir = this.$projectData.projectDir; - let frameworkToInstall = this.$options.framework || + const frameworkToInstall = this.$options.framework || await this.$prompter.promptForChoice('Select testing framework:', TESTING_FRAMEWORKS); if (TESTING_FRAMEWORKS.indexOf(frameworkToInstall) === -1) { this.$errors.fail(`Unknown or unsupported unit testing framework: ${frameworkToInstall}`); } - let dependencies = this.frameworkDependencies[frameworkToInstall] || []; - let modulesToInstall = ['karma', 'karma-' + frameworkToInstall, 'karma-nativescript-launcher'].concat(dependencies.map(f => 'karma-' + f)); + const dependencies = this.frameworkDependencies[frameworkToInstall] || []; + const modulesToInstall = ['karma', 'karma-' + frameworkToInstall, 'karma-nativescript-launcher'].concat(dependencies.map(f => 'karma-' + f)); - for (let mod of modulesToInstall) { + for (const mod of modulesToInstall) { await this.$npm.install(mod, projectDir, { 'save-dev': true, optional: false, + disableNpmInstall: this.$options.disableNpmInstall, + frameworkPath: this.$options.frameworkPath, + ignoreScripts: this.$options.ignoreScripts, + path: this.$options.path }); const modulePath = path.join(projectDir, "node_modules", mod); @@ -43,14 +47,18 @@ class TestInitCommand implements ICommand { const modulePackageJsonContent = this.$fs.readJson(modulePackageJsonPath); const modulePeerDependencies = modulePackageJsonContent.peerDependencies || {}; - for (let peerDependency in modulePeerDependencies) { - let dependencyVersion = modulePeerDependencies[peerDependency] || "*"; + for (const peerDependency in modulePeerDependencies) { + const dependencyVersion = modulePeerDependencies[peerDependency] || "*"; // catch errors when a peerDependency is already installed // e.g karma is installed; karma-jasmine depends on karma and will try to install it again try { await this.$npm.install(`${peerDependency}@${dependencyVersion}`, projectDir, { - 'save-dev': true + 'save-dev': true, + disableNpmInstall: false, + frameworkPath: this.$options.frameworkPath, + ignoreScripts: this.$options.ignoreScripts, + path: this.$options.path }); } catch (e) { this.$logger.error(e.message); @@ -60,7 +68,7 @@ class TestInitCommand implements ICommand { await this.$pluginsService.add('nativescript-unit-test-runner', this.$projectData); - let testsDir = path.join(projectDir, 'app/tests'); + const testsDir = path.join(projectDir, 'app/tests'); let shouldCreateSampleTests = true; if (this.$fs.exists(testsDir)) { this.$logger.info('app/tests/ directory already exists, will not create an example test project.'); @@ -69,8 +77,8 @@ class TestInitCommand implements ICommand { this.$fs.ensureDirectoryExists(testsDir); - let karmaConfTemplate = this.$resources.readText('test/karma.conf.js'); - let karmaConf = _.template(karmaConfTemplate)({ + const karmaConfTemplate = this.$resources.readText('test/karma.conf.js'); + const karmaConf = _.template(karmaConfTemplate)({ frameworks: [frameworkToInstall].concat(dependencies) .map(fw => `'${fw}'`) .join(', ') @@ -78,7 +86,7 @@ class TestInitCommand implements ICommand { this.$fs.writeFile(path.join(projectDir, 'karma.conf.js'), karmaConf); - let exampleFilePath = this.$resources.resolvePath(`test/example.${frameworkToInstall}.js`); + const exampleFilePath = this.$resources.resolvePath(`test/example.${frameworkToInstall}.js`); if (shouldCreateSampleTests && this.$fs.exists(exampleFilePath)) { this.$fs.copyFile(exampleFilePath, path.join(testsDir, 'example.js')); diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 9e6f197120..b990a02d44 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -1,9 +1,13 @@ +import * as helpers from "../common/helpers"; + function RunTestCommandFactory(platform: string) { return function RunTestCommand( + $options: IOptions, $testExecutionService: ITestExecutionService, $projectData: IProjectData) { $projectData.initializeProjectData(); - this.execute = (args: string[]): Promise => $testExecutionService.startTestRunner(platform, $projectData); + const projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: $options.release }); + this.execute = (args: string[]): Promise => $testExecutionService.startTestRunner(platform, $projectData, projectFilesConfig); this.allowedParameters = []; }; } @@ -12,9 +16,10 @@ $injector.registerCommand("dev-test|android", RunTestCommandFactory('android')); $injector.registerCommand("dev-test|ios", RunTestCommandFactory('iOS')); function RunKarmaTestCommandFactory(platform: string) { - return function RunKarmaTestCommand($testExecutionService: ITestExecutionService, $projectData: IProjectData) { + return function RunKarmaTestCommand($options: IOptions, $testExecutionService: ITestExecutionService, $projectData: IProjectData) { $projectData.initializeProjectData(); - this.execute = (args: string[]): Promise => $testExecutionService.startKarmaServer(platform, $projectData); + const projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: $options.release }); + this.execute = (args: string[]): Promise => $testExecutionService.startKarmaServer(platform, $projectData, projectFilesConfig); this.allowedParameters = []; }; } diff --git a/lib/commands/update-platform.ts b/lib/commands/update-platform.ts index 5ff02b6509..7dc8c1dae3 100644 --- a/lib/commands/update-platform.ts +++ b/lib/commands/update-platform.ts @@ -4,12 +4,13 @@ export class UpdatePlatformCommand implements ICommand { constructor(private $options: IOptions, private $projectData: IProjectData, private $platformService: IPlatformService, - private $errors: IErrors) { + private $errors: IErrors, + private $platformsData: IPlatformsData) { this.$projectData.initializeProjectData(); } public async execute(args: string[]): Promise { - await this.$platformService.updatePlatforms(args, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); + await this.$platformService.updatePlatforms(args, this.$options.platformTemplate, this.$projectData, this.$options); } public async canExecute(args: string[]): Promise { @@ -17,7 +18,13 @@ export class UpdatePlatformCommand implements ICommand { this.$errors.fail("No platform specified. Please specify platforms to update."); } - _.each(args, arg => this.$platformService.validatePlatform(arg.split("@")[0], this.$projectData)); + for (const arg of args) { + const platform = arg.split("@")[0]; + this.$platformService.validatePlatform(platform, this.$projectData); + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + } return true; } diff --git a/lib/commands/update.ts b/lib/commands/update.ts index 49c3eee4ea..2669740cca 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -12,19 +12,19 @@ export class UpdateCommand implements ICommand { private $projectDataService: IProjectDataService, private $fs: IFileSystem, private $logger: ILogger) { - this.$projectData.initializeProjectData(); - } + this.$projectData.initializeProjectData(); + } public async execute(args: string[]): Promise { - let folders = ["lib", "hooks", "platforms", "node_modules"]; - let tmpDir = path.join(this.$projectData.projectDir, ".tmp_backup"); + const folders = ["lib", "hooks", "platforms", "node_modules"]; + const tmpDir = path.join(this.$projectData.projectDir, ".tmp_backup"); try { shelljs.rm("-fr", tmpDir); shelljs.mkdir(tmpDir); shelljs.cp(path.join(this.$projectData.projectDir, "package.json"), tmpDir); - for (let folder of folders) { - let folderToCopy = path.join(this.$projectData.projectDir, folder); + for (const folder of folders) { + const folderToCopy = path.join(this.$projectData.projectDir, folder); if (this.$fs.exists(folderToCopy)) { shelljs.cp("-rf", folderToCopy, tmpDir); } @@ -38,10 +38,10 @@ export class UpdateCommand implements ICommand { await this.executeCore(args, folders); } catch (error) { shelljs.cp("-f", path.join(tmpDir, "package.json"), this.$projectData.projectDir); - for (let folder of folders) { + for (const folder of folders) { shelljs.rm("-rf", path.join(this.$projectData.projectDir, folder)); - let folderToCopy = path.join(tmpDir, folder); + const folderToCopy = path.join(tmpDir, folder); if (this.$fs.exists(folderToCopy)) { shelljs.cp("-fr", folderToCopy, this.$projectData.projectDir); @@ -55,16 +55,24 @@ export class UpdateCommand implements ICommand { } public async canExecute(args: string[]): Promise { + for (const arg of args) { + const platform = arg.split("@")[0]; + this.$platformService.validatePlatformInstalled(platform, this.$projectData); + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); + } + return args.length < 2 && this.$projectData.projectDir !== ""; } private async executeCore(args: string[], folders: string[]): Promise { let platforms = this.$platformService.getInstalledPlatforms(this.$projectData); - let availablePlatforms = this.$platformService.getAvailablePlatforms(this.$projectData); - let packagePlatforms: string[] = []; + const availablePlatforms = this.$platformService.getAvailablePlatforms(this.$projectData); + const packagePlatforms: string[] = []; - for (let platform of availablePlatforms) { - let platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + for (const platform of availablePlatforms) { + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); const platformVersion = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName); if (platformVersion) { packagePlatforms.push(platform); @@ -76,19 +84,19 @@ export class UpdateCommand implements ICommand { await this.$pluginsService.remove("tns-core-modules", this.$projectData); await this.$pluginsService.remove("tns-core-modules-widgets", this.$projectData); - for (let folder of folders) { + for (const folder of folders) { shelljs.rm("-fr", folder); } platforms = platforms.concat(packagePlatforms); if (args.length === 1) { - for (let platform of platforms) { - await this.$platformService.addPlatforms([platform + "@" + args[0]], this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }, this.$options.frameworkPath); + for (const platform of platforms) { + await this.$platformService.addPlatforms([platform + "@" + args[0]], this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); } await this.$pluginsService.add("tns-core-modules@" + args[0], this.$projectData); } else { - await this.$platformService.addPlatforms(platforms, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk }, this.$options.frameworkPath); + await this.$platformService.addPlatforms(platforms, this.$options.platformTemplate, this.$projectData, this.$options, this.$options.frameworkPath); await this.$pluginsService.add("tns-core-modules", this.$projectData); } diff --git a/lib/common b/lib/common index ebfdf06c65..6522b37bc2 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit ebfdf06c656604551bfd4d261185f10cb7c566af +Subproject commit 6522b37bc2beadfccb98f9f1ca9002c7046650b4 diff --git a/lib/config.ts b/lib/config.ts index 68e9409bca..4b6b21c942 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -8,7 +8,6 @@ export class Configuration extends ConfigBase implements IConfiguration { // Use TYPESCRIPT_COMPILER_OPTIONS = {}; ANDROID_DEBUG_UI: string = null; USE_POD_SANDBOX: boolean = false; - debugLivesync: boolean = false; /*don't require logger and everything that has logger as dependency in config.js due to cyclic dependency*/ constructor(protected $fs: IFileSystem) { @@ -24,6 +23,7 @@ export class StaticConfig extends StaticConfigBase implements IStaticConfig { public CLIENT_NAME = "tns"; public CLIENT_NAME_ALIAS = "NativeScript"; public ANALYTICS_API_KEY = "5752dabccfc54c4ab82aea9626b7338e"; + public ANALYTICS_EXCEPTIONS_API_KEY = "35478fe7de68431399e96212540a3d5d"; public TRACK_FEATURE_USAGE_SETTING_NAME = "TrackFeatureUsage"; public ERROR_REPORT_SETTING_NAME = "TrackExceptions"; public ANALYTICS_INSTALLATION_ID_SETTING_NAME = "AnalyticsInstallationID"; @@ -73,12 +73,12 @@ export class StaticConfig extends StaticConfigBase implements IStaticConfig { } public get PATH_TO_BOOTSTRAP(): string { - return path.join(__dirname, "bootstrap"); + return path.join(__dirname, "bootstrap.js"); } public async getAdbFilePath(): Promise { if (!this._adbFilePath) { - let androidToolsInfo: IAndroidToolsInfo = this.$injector.resolve("androidToolsInfo"); + const androidToolsInfo: IAndroidToolsInfo = this.$injector.resolve("androidToolsInfo"); this._adbFilePath = await androidToolsInfo.getPathToAdbFromAndroidHome() || await super.getAdbFilePath(); } diff --git a/lib/constants.ts b/lib/constants.ts index 21a4488697..6405df9471 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -16,12 +16,21 @@ export const TEST_RUNNER_NAME = "nativescript-unit-test-runner"; export const LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"]; export const XML_FILE_EXTENSION = ".xml"; export const PLATFORMS_DIR_NAME = "platforms"; +export const CODE_SIGN_ENTITLEMENTS = "CODE_SIGN_ENTITLEMENTS"; +export const AWAIT_NOTIFICATION_TIMEOUT_SECONDS = 9; export class PackageVersion { static NEXT = "next"; static LATEST = "latest"; } +const liveSyncOperation = "LiveSync Operation"; +export class LiveSyncTrackActionNames { + static LIVESYNC_OPERATION = liveSyncOperation; + static LIVESYNC_OPERATION_BUILD = `${liveSyncOperation} - Build`; + static DEVICE_INFO = `Device Info for ${liveSyncOperation}`; +} + export const PackageJsonKeysToKeep: Array = ["name", "main", "android", "version"]; export class SaveOptions { @@ -65,9 +74,50 @@ class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationTyp } export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass(); - +export class LiveSyncPaths { + static SYNC_DIR_NAME = "sync"; + static REMOVEDSYNC_DIR_NAME = "removedsync"; + static FULLSYNC_DIR_NAME = "fullsync"; + static IOS_DEVICE_PROJECT_ROOT_PATH = "Library/Application Support/LiveSync"; + static IOS_DEVICE_SYNC_ZIP_PATH = "Library/Application Support/LiveSync/sync.zip"; +} export const ANGULAR_NAME = "angular"; export const TYPESCRIPT_NAME = "typescript"; export const BUILD_OUTPUT_EVENT_NAME = "buildOutput"; export const CONNECTION_ERROR_EVENT_NAME = "connectionError"; +export const USER_INTERACTION_NEEDED_EVENT_NAME = "userInteractionNeeded"; +export const DEBUGGER_ATTACHED_EVENT_NAME = "debuggerAttached"; +export const DEBUGGER_DETACHED_EVENT_NAME = "debuggerDetached"; export const VERSION_STRING = "version"; +export const INSPECTOR_CACHE_DIRNAME = "ios-inspector"; +export const POST_INSTALL_COMMAND_NAME = "post-install-cli"; + +export class DebugCommandErrors { + public static UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR = "The options --for-device and --emulator cannot be used simultaneously. Please use only one of them."; + public static NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS = "Unable to find device or emulator for specified options."; + public static UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING = "Unsupported device OS for debugging"; +} + +export const enum NativePlatformStatus { + requiresPlatformAdd = "1", + requiresPrepare = "2", + alreadyPrepared = "3" +} + +export const enum DebugTools { + Chrome = "Chrome", + Inspector = "Inspector" +} + +export const enum TrackActionNames { + Build = "Build", + CreateProject = "Create project", + Debug = "Debug", + Deploy = "Deploy", + LiveSync = "LiveSync" +} + +export const enum BuildStates { + Clean = "Clean", + Incremental = "Incremental" +} diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 9746e88b14..c6f18218d7 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -1,8 +1,37 @@ interface INodePackageManager { - install(packageName: string, pathToSave: string, config?: any): Promise; - uninstall(packageName: string, config?: any, path?: string): Promise; + /** + * Installs dependency + * @param {string} packageName The name of the dependency - can be a path, a url or a string. + * @param {string} pathToSave The destination of the installation. + * @param {INodePackageManagerInstallOptions} config Additional options that can be passed to manipulate installation. + * @return {Promise} Information about installed package. + */ + install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise; + + /** + * Uninstalls a dependency + * @param {string} packageName The name of the dependency. + * @param {IDictionary} config Additional options that can be passed to manipulate uninstallation. + * @param {string} path The destination of the uninstallation. + * @return {Promise} The output of the uninstallation. + */ + uninstall(packageName: string, config?: IDictionary, path?: string): Promise; + + /** + * Provides information about a given package. + * @param {string} packageName The name of the package. + * @param {IDictionary} config Additional options that can be passed to manipulate view. + * @return {Promise} Object, containing information about the package. + */ view(packageName: string, config: Object): Promise; - search(filter: string[], config: any): Promise; + + /** + * Searches for a package. + * @param {string[]} filter Keywords with which to perform the search. + * @param {IDictionary} config Additional options that can be passed to manipulate search. + * @return {Promise} The output of the uninstallation. + */ + search(filter: string[], config: IDictionary): Promise; } interface INpmInstallationManager { @@ -13,18 +42,268 @@ interface INpmInstallationManager { getInspectorFromCache(inspectorNpmPackageName: string, projectDir: string): Promise; } +/** + * Describes options that can be passed to manipulate package installation. + */ +interface INodePackageManagerInstallOptions extends INpmInstallConfigurationOptions, IDictionary { + /** + * Destination of the installation. + * @type {string} + * @optional + */ + path?: string; +} + +/** + * Describes information about dependency packages. + */ +interface INpmDependencyInfo { + /** + * Dependency name. + */ + [key: string]: { + /** + * Dependency version. + * @type {string} + */ + version: string; + /** + * How was the dependency resolved. For example: lodash@latest or underscore@>=1.8.3 <2.0.0 + * @type {string} + */ + from: string; + /** + * Where was the dependency resolved from. For example: https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz + * @type {string} + */ + resolved: string; + /** + * Dependencies of the dependency. + * @type {INpmDependencyInfo} + */ + dependencies?: INpmDependencyInfo; + /** + * Set to true when the dependency is invalid. + * @type {boolean} + */ + invalid?: boolean; + /** + * If invalid is set to true this will contain errors which make the dependency invalid. + */ + problems?: string[]; + } +} + +/** + * Describes information about peer dependency packages. + */ +interface INpmPeerDependencyInfo { + required: { + /** + * Id used in package.json - for example: zone.js@^0.8.4 + * @type {string} + */ + _id: string; + /** + * Dependency name. + * @type {string} + */ + name: string; + /** + * Dependency version. + * @type {string} + */ + version: string; + /** + * If peerMissing below is set to true this will contain information about missing peers. + */ + peerMissing?: [ + { + /** + * The id of the package which requires the unmet peer dependency. + * @type {string} + */ + requiredBy: string; + /** + * The id of the unmet peer dependency. + * @type {string} + */ + requires: string; + } + ]; + /** + * Dependencies of the dependency. + * @type {INpmDependencyInfo} + */ + dependencies: INpmDependencyInfo; + /** + * Whether the dependency was found or not. + * @type {boolean} + */ + _found: boolean; + }; + /** + * Set to true if peer dependency unmet. + * @type {boolean} + */ + peerMissing: boolean; +} + +/** + * Describes information about dependency update packages. + */ +interface INpm5DependencyInfo { + /** + * Npm action type. + * @type {string} + */ + action: string; + /** + * Dependency name. + * @type {string} + */ + name: string; + /** + * Dependency version. + * @type {string} + */ + version: string; + /** + * Destination of the installation. + * @type {string} + */ + path: string; + /** + * Dependency previous version. + * @type {string} + */ + previousVersion: string; +} + +/** + * Describes information returned by the npm CLI upon calling install with --json flag. + */ +interface INpmInstallCLIResult { + /** + * The name of the destination package. Note that this is not the installed package. + * @type {string} + */ + name: string; + /** + * The version of the destination package. Note that this is not the installed package. + * @type {string} + */ + version: string; + /** + * Installed dependencies. Note that whenever installing a particular dependency it is listed as the first key and after it any number of peer dependencies may follow. + * Whenever installing npm prints the information by reversing the tree of operations and because the initial dependency was installed last it is listed first. + * @type {INpmDependencyInfo | INpmPeerDependencyInfo} + */ + dependencies: INpmDependencyInfo | INpmPeerDependencyInfo; + /** + * Describes problems that might have occurred during installation. For example missing peer dependencies. + */ + problems?: string[]; +} + +/** + * Describes information returned by the npm 5 CLI upon calling install with --json flag. + */ +interface INpm5InstallCliResult { + /** + * Added dependencies. Note that whenever add a particular dependency with npm 5 it is listed inside of array with key "Added". + * @type {INpmDependencyUpdateInfo[]} + */ + added: INpm5DependencyInfo[]; + /** + * Removed dependencies. Note that whenever remove a particular dependency with npm 5 it is listed inside of array with key "removed". + * @type {INpmDependencyUpdateInfo[]} + */ + removed: INpm5DependencyInfo[]; + /** + * Updated dependencies. Note that whenever update a particular dependency with npm 5 it is listed inside of array with key "updated". + * @type {INpmDependencyUpdateInfo[]} + */ + updated: INpm5DependencyInfo[]; + /** + * Moved dependencies. Note that whenever move a particular dependency with npm 5 it is listed inside of array with key "moved". + * @type {INpmDependencyUpdateInfo[]} + */ + moved: INpm5DependencyInfo[]; + /** + * Failed dependencies. Note that whenever use npm 5 and the operation over particular dependency fail it is listed inside of array with key "failed". + * @type {INpmDependencyUpdateInfo[]} + */ + failed: INpm5DependencyInfo[]; + /** + * Warnings. Note that whenever use npm 5 and the operation over particular dependency have warnings they are listed inside of array with key "warnings". + * @type {INpmDependencyUpdateInfo[]} + */ + warnings: INpm5DependencyInfo[]; + /** + *Time elapsed. + * @type {Number} + */ + elapsed: Number +} + +/** + * Describes information about installed package. + */ +interface INpmInstallResultInfo { + /** + * Installed package's name. + * @type {string} + */ + name: string; + /** + * Installed package's version. + * @type {string} + */ + version: string; + /** + * The original output that npm CLI produced upon installation. + * @type {INpmInstallCLIResult} + */ + originalOutput?: INpmInstallCLIResult | INpm5InstallCliResult; +} + interface INpmInstallOptions { pathToSave?: string; version?: string; dependencyType?: string; } +/** + * Describes npm package installed in node_modules. + */ interface IDependencyData { + /** + * The name of the package. + */ name: string; - version: string; - nativescript: any; - dependencies?: IStringDictionary; - devDependencies?: IStringDictionary; + + /** + * The full path where the package is installed. + */ + directory: string; + + /** + * The depth inside node_modules dir, where the package is located. + * The /node_modules/ is level 0. + * Level 1 is /node_modules//node_modules, etc. + */ + depth: number; + + /** + * Describes the `nativescript` key in package.json of a dependency. + */ + nativescript?: any; + + /** + * Dependencies of the current module. + */ + dependencies?: string[]; } interface IStaticConfig extends Config.IStaticConfig { } @@ -32,7 +311,6 @@ interface IStaticConfig extends Config.IStaticConfig { } interface IConfiguration extends Config.IConfig { ANDROID_DEBUG_UI: string; USE_POD_SANDBOX: boolean; - debugLivesync: boolean; } interface IApplicationPackage { @@ -40,47 +318,10 @@ interface IApplicationPackage { time: Date; } -interface ILockFile { - lock(): void; - unlock(): void; - check(): boolean; -} - interface IOpener { open(target: string, appname: string): void; } -interface ILiveSyncService { - liveSync(platform: string, projectData: IProjectData, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise; -} - -interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase { - /** - * Refreshes the application's content on a device - * @param {Mobile.IDeviceAppData} deviceAppData Information about the application and the device. - * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. - * @param {boolean} forceExecuteFullSync If this is passed a full LiveSync is performed instead of an incremental one. - * @param {IProjectData} projectData Project data. - * @return {Promise} - */ - refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean, projectData: IProjectData): Promise; - /** - * Removes specified files from a connected device - * @param {string} appIdentifier Application identifier. - * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. - * @param {string} projectId Project identifier - for example org.nativescript.livesync. - * @return {Promise} - */ - removeFiles(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise; - afterInstallApplicationAction?(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise; -} - -interface IPlatformLiveSyncService { - fullSync(projectData: IProjectData, postAction?: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise): Promise; - partialSync(event: string, filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise; - refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], isFullSync: boolean, projectData: IProjectData): Promise; -} - interface IBundle { bundle: boolean; } @@ -98,7 +339,7 @@ interface IClean { } interface IProvision { - provision: any; + provision: string; } interface ITeamIdentifier { @@ -112,7 +353,28 @@ interface IAndroidReleaseOptions { keyStorePath?: string; } -interface IOptions extends ICommonOptions, IBundle, IPlatformTemplate, IEmulator, IClean, IProvision, ITeamIdentifier, IAndroidReleaseOptions { +interface INpmInstallConfigurationOptionsBase { + frameworkPath: string; + ignoreScripts: boolean; //npm flag +} + +interface INpmInstallConfigurationOptions extends INpmInstallConfigurationOptionsBase { + disableNpmInstall: boolean; +} + +interface ICreateProjectOptions extends INpmInstallConfigurationOptionsBase { + pathToTemplate?: string; +} + +interface IDebugInformation extends IPort, Mobile.IDeviceIdentifier { + url: string; +} + +interface IPort { + port: Number; +} + +interface IOptions extends ICommonOptions, IBundle, IPlatformTemplate, IEmulator, IClean, IProvision, ITeamIdentifier, IAndroidReleaseOptions, INpmInstallConfigurationOptions, IPort { all: boolean; client: boolean; compileSdk: number; @@ -121,15 +383,11 @@ interface IOptions extends ICommonOptions, IBundle, IPlatformTemplate, IEmulator forDevice: boolean; framework: string; frameworkName: string; - frameworkPath: string; frameworkVersion: string; - ignoreScripts: boolean; //npm flag - disableNpmInstall: boolean; ipa: string; tsc: boolean; ng: boolean; androidTypings: boolean; - port: Number; production: boolean; //npm flag sdk: string; syncAllFiles: boolean; @@ -147,14 +405,10 @@ interface IDeviceEmulator extends IEmulator, IDeviceIdentifier { } interface IRunPlatformOptions extends IJustLaunch, IDeviceEmulator { } -interface IDeployPlatformOptions extends IAndroidReleaseOptions, IPlatformTemplate, IRelease, IClean, IDeviceEmulator, IProvision, ITeamIdentifier { - projectDir: string; +interface IDeployPlatformOptions extends IAndroidReleaseOptions, IPlatformTemplate, IRelease, IClean, IDeviceEmulator, IProvision, ITeamIdentifier, IProjectDir { forceInstall?: boolean; } -interface IEmulatePlatformOptions extends IJustLaunch, IDeployPlatformOptions, IAvailableDevices, IAvd { -} - interface IUpdatePlatformOptions extends IPlatformTemplate { currentVersion: string; newVersion: string; @@ -302,11 +556,6 @@ interface IiOSNotification { getAttachAvailable(projectId: string): string; } -interface IiOSNotificationService { - awaitNotification(deviceIdentifier: string, socket: number, timeout: number): Promise; - postNotification(deviceIdentifier: string, notification: string, commandType?: string): Promise; -} - interface IiOSSocketRequestExecutor { executeLaunchRequest(deviceIdentifier: string, timeout: number, readyForAttachTimeout: number, projectId: string, shouldBreak?: boolean): Promise; executeAttachRequest(device: Mobile.IiOSDevice, timeout: number, projectId: string): Promise; @@ -354,10 +603,9 @@ interface IVersionsService { getRuntimesVersions(): Promise; /** - * Gets versions information about the nativescript components with new. - * @return {Promise} The version information. + * Checks version information about the nativescript components and prints available updates if any. */ - getComponentsForUpdate(): Promise; + checkComponentsForUpdate(): Promise; /** * Gets versions information about all nativescript components. diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index 752b2ad762..beed61b239 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -1,31 +1,147 @@ -interface IDebugData { - deviceIdentifier: string; +/** + * Describes information for starting debug process. + */ +interface IDebugData extends Mobile.IDeviceIdentifier { + /** + * Application identifier of the app that it will be debugged. + */ applicationIdentifier: string; + + /** + * Path to .app built for iOS Simulator. + */ pathToAppPackage?: string; + + /** + * The name of the application, for example `MyProject`. + */ projectName?: string; + + /** + * Path to project. + */ projectDir?: string; } +/** + * Describes all options that define the behavior of debug. + */ interface IDebugOptions { + /** + * Defines if Chrome DevTools should be used for debugging. + */ chrome?: boolean; + + /** + * Defines if thе application is already started on device. + */ start?: boolean; + + /** + * Defines if we should stop the currently running debug process. + */ stop?: boolean; + + /** + * Defines if debug process is for emulator (not for real device). + */ emulator?: boolean; + + /** + * Defines if the debug process should break on the first line. + */ debugBrk?: boolean; + + /** + * Defines if the debug process will not have a client attached (i.e. the process will be started, but NativeScript Inspector will not be started and it will not attach to the running debug process). + */ client?: boolean; + + /** + * Defines if the process will watch for further changes in the project and transferrs them to device immediately, resulting in restar of the debug process. + */ justlaunch?: boolean; + + /** + * Defines if bundled Chrome DevTools should be used or specific commit. + * Default value is true for Android and false for iOS. + */ + useBundledDevTools?: boolean; + + /** + * Defines if https://chrome-devtools-frontend.appspot.com should be used instead of chrome-devtools://devtools + * In case it is passed, the value of `useBundledDevTools` is disregarded. + * Default value is false. + */ + useHttpUrl?: boolean; + + /** + * Defines the commit that will be used in cases where remote protocol is required. + * For Android this is the case when useHttpUrl is set to true or useBundledDevTools is set to false. + * For iOS the value is used by default and when useHttpUrl is set to true. + * Default value is 02e6bde1bbe34e43b309d4ef774b1168d25fd024 which corresponds to 55.0.2883 Chrome version + */ + devToolsCommit?: string; } +/** + * Describes methods to create debug data object used by other methods. + */ interface IDebugDataService { - createDebugData(projectData: IProjectData, options: IOptions): IDebugData; + /** + * Creates the debug data based on specified options. + * @param {IProjectData} projectData The data describing project that will be debugged. + * @param {IOptions} options The options based on which debugData will be created + * @returns {IDebugData} Data describing the required information for starting debug process. + */ + createDebugData(projectData: IProjectData, options: IDeviceIdentifier): IDebugData; } -interface IDebugService extends NodeJS.EventEmitter { - debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; +/** + * Describes methods for debug operation. + */ +interface IDebugServiceBase extends NodeJS.EventEmitter { + /** + * Starts debug operation based on the specified debug data. + * @param {IDebugData} debugData Describes information for device and application that will be debugged. + * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. + * @returns {Promise} Device Identifier, full url and port where the frontend client can be connected. + */ + debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; } -interface IPlatformDebugService extends IDebugService { +interface IDebugService extends IDebugServiceBase { + /** + * Stops debug operation for a specific device. + * @param {string} deviceIdentifier Identifier of the device fo which debugging will be stopped. + * @returns {Promise} + */ + debugStop(deviceIdentifier: string): Promise; +} + +/** + * Describes actions required for debugging on specific platform (Android or iOS). + */ +interface IPlatformDebugService extends IPlatform, NodeJS.EventEmitter { + /** + * Starts debug operation. + * @param {IDebugData} debugData Describes information for device and application that will be debugged. + * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. + * @returns {Promise} + */ debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise; - debugStop(): Promise - platform: string; + + /** + * Stops debug operation. + * @returns {Promise} + */ + debugStop(): Promise; + + /** + * Starts debug operation based on the specified debug data. + * @param {IDebugData} debugData Describes information for device and application that will be debugged. + * @param {IDebugOptions} debugOptions Describe possible options to modify the behaivor of the debug operation, for example stop on the first line. + * @returns {Promise} Full url where the frontend client may be connected. + */ + debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; } diff --git a/lib/definitions/emulator-platform-service.d.ts b/lib/definitions/emulator-platform-service.d.ts index 259bbabbb2..e088a2efc8 100644 --- a/lib/definitions/emulator-platform-service.d.ts +++ b/lib/definitions/emulator-platform-service.d.ts @@ -1,7 +1,6 @@ -interface IEmulatorInfo { +interface IEmulatorInfo extends IPlatform { name: string; version: string; - platform: string; id: string; type: string; isRunning?: boolean; diff --git a/lib/definitions/extensibility.d.ts b/lib/definitions/extensibility.d.ts index 3aa9dba5cd..90d2916ef9 100644 --- a/lib/definitions/extensibility.d.ts +++ b/lib/definitions/extensibility.d.ts @@ -13,7 +13,7 @@ interface IExtensionData { */ interface IExtensibilityService { /** - * Installs and loads specified extension. + * Installs a specified extension. * @param {string} extensionName Name of the extension to be installed. It may contain version as well, i.e. myPackage, myPackage@1.0.0, * myPackage.tgz, https://github.com/myOrganization/myPackage/tarball/master, https://github.com/myOrganization/myPackage, etc. * @returns {Promise} Information about installed extensions. @@ -35,6 +35,14 @@ interface IExtensibilityService { */ loadExtensions(): Promise[]; + /** + * Loads a single extension, so its methods and commands can be used from CLI. + * @param {string} extensionName Name of the extension to be installed. It may contain version as well, i.e. myPackage, myPackage@1.0.0 + * A Promise is returned. It will be rejected in case the extension cannot be loaded. + * @returns {Promise} Promise, resolved with IExtensionData. + */ + loadExtension(extensionName: string): Promise; + /** * Gets information about installed dependencies - names and versions. * @returns {IStringDictionary} diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts new file mode 100644 index 0000000000..19b73bcd11 --- /dev/null +++ b/lib/definitions/livesync.d.ts @@ -0,0 +1,358 @@ +// This interface is a mashup of NodeJS' along with Chokidar's event watchers +interface IFSWatcher extends NodeJS.EventEmitter { + // from fs.FSWatcher + close(): void; + + /** + * events.EventEmitter + * 1. change + * 2. error + */ + addListener(event: string, listener: Function): this; + addListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + addListener(event: "error", listener: (code: number, signal: string) => void): this; + + on(event: string, listener: Function): this; + on(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + on(event: "error", listener: (code: number, signal: string) => void): this; + + once(event: string, listener: Function): this; + once(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + once(event: "error", listener: (code: number, signal: string) => void): this; + + prependListener(event: string, listener: Function): this; + prependListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + prependListener(event: "error", listener: (code: number, signal: string) => void): this; + + prependOnceListener(event: string, listener: Function): this; + prependOnceListener(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this; + prependOnceListener(event: "error", listener: (code: number, signal: string) => void): this; + + // From chokidar FSWatcher + + /** + * Add files, directories, or glob patterns for tracking. Takes an array of strings or just one + * string. + */ + add(paths: string | string[]): void; + + /** + * Stop watching files, directories, or glob patterns. Takes an array of strings or just one + * string. + */ + unwatch(paths: string | string[]): void; + + /** + * Returns an object representing all the paths on the file system being watched by this + * `FSWatcher` instance. The object's keys are all the directories (using absolute paths unless + * the `cwd` option was used), and the values are arrays of the names of the items contained in + * each directory. + */ + getWatched(): IDictionary; + + /** + * Removes all listeners from watched files. + */ + close(): void; +} + +interface ILiveSyncProcessInfo { + timer: NodeJS.Timer; + watcherInfo: { + watcher: IFSWatcher, + pattern: string | string[] + }; + actionsChain: Promise; + isStopped: boolean; + deviceDescriptors: ILiveSyncDeviceInfo[]; + currentSyncAction: Promise; +} + +interface IOptionalOutputPath { + /** + * Path where the build result is located (directory containing .ipa, .apk or .zip). + * This is required for initial checks where LiveSync will skip the rebuild in case there's already a build result and no change requiring rebuild is made since then. + * In case it is not passed, the default output for local builds will be used. + */ + outputPath?: string; +} + +/** + * Describes information for LiveSync on a device. + */ +interface ILiveSyncDeviceInfo extends IOptionalOutputPath { + /** + * Device identifier. + */ + identifier: string; + + /** + * Action that will rebuild the application. The action must return a Promise, which is resolved with at path to build artifact. + * @returns {Promise} Path to build artifact (.ipa, .apk or .zip). + */ + buildAction: () => Promise; + + /** + * Whether to skip preparing the native platform. + */ + skipNativePrepare?: boolean; + + /** + * Whether debugging has been enabled for this device or not + */ + debugggingEnabled?: boolean; + + /** + * Describes options specific for each platform, like provision for iOS, target sdk for Android, etc. + */ + platformSpecificOptions?: IPlatformOptions; +} + +/** + * Describes a LiveSync operation. + */ +interface ILiveSyncInfo extends IProjectDir, IOptionalDebuggingOptions { + /** + * Defines if the watcher should be skipped. If not passed, fs.Watcher will be started. + */ + skipWatcher?: boolean; + + /** + * Defines if all project files should be watched for changes. In case it is not passed, only `app` dir of the project will be watched for changes. + * In case it is set to true, the package.json of the project and node_modules directory will also be watched, so any change there will be transferred to device(s). + */ + watchAllFiles?: boolean; + + /** + * Defines if the liveEdit functionality should be used, i.e. LiveSync of .js files without restart. + * NOTE: Currently this is available only for iOS. + */ + useLiveEdit?: boolean; + + /** + * Forces a build before the initial livesync. + */ + clean?: boolean; +} + +interface ILatestAppPackageInstalledSettings extends IDictionary> { /* empty */ } + +interface IIsEmulator { + isEmulator: boolean; +} + +interface ILiveSyncBuildInfo extends IIsEmulator, IPlatform { + pathToBuildItem: string; +} + +/** + * Desribes object that can be passed to ensureLatestAppPackageIsInstalledOnDevice method. + */ +interface IEnsureLatestAppPackageIsInstalledOnDeviceOptions { + device: Mobile.IDevice; + preparedPlatforms: string[]; + rebuiltInformation: ILiveSyncBuildInfo[]; + projectData: IProjectData; + deviceBuildInfoDescriptor: ILiveSyncDeviceInfo; + settings: ILatestAppPackageInstalledSettings; + liveSyncData?: ILiveSyncInfo; + modifiedFiles?: string[]; +} + +/** + * Describes the action that has been executed during ensureLatestAppPackageIsInstalledOnDevice execution. + */ +interface IAppInstalledOnDeviceResult { + /** + * Defines if the app has been installed on device from the ensureLatestAppPackageIsInstalledOnDevice method. + */ + appInstalled: boolean; +} + +/** + * Describes LiveSync operations. + */ +interface ILiveSyncService { + /** + * Starts LiveSync operation by rebuilding the application if necessary and starting watcher. + * @param {ILiveSyncDeviceInfo[]} deviceDescriptors Describes each device for which we would like to sync the application - identifier, outputPath and action to rebuild the app. + * @param {ILiveSyncInfo} liveSyncData Describes the LiveSync operation - for which project directory is the operation and other settings. + * @returns {Promise} + */ + liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise; + + /** + * Stops LiveSync operation for specified directory. + * @param {string} projectDir The directory for which to stop the operation. + * @param {string[]} @optional deviceIdentifiers Device ids for which to stop the application. In case nothing is passed, LiveSync operation will be stopped for all devices. + * @param { shouldAwaitAllActions: boolean } @optional stopOptions Specifies whether we should await all actions. + * @returns {Promise} + */ + stopLiveSync(projectDir: string, deviceIdentifiers?: string[], stopOptions?: { shouldAwaitAllActions: boolean }): Promise; + + /** + * Returns the device information for current LiveSync operation of specified project. + * In case LiveSync has been started on many devices, but stopped for some of them at a later point, + * calling the method after that will return information only for devices for which LiveSync operation is in progress. + * @param {string} projectDir The path to project for which the LiveSync operation is executed + * @returns {ILiveSyncDeviceInfo[]} Array of elements describing parameters used to start LiveSync on each device. + */ + getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[]; +} + +/** + * Describes LiveSync operations while debuggging. + */ +interface IDebugLiveSyncService extends ILiveSyncService { + /** + * Prints debug information. + * @param {IDebugInformation} debugInformation Information to be printed. + * @returns {IDebugInformation} Full url and port where the frontend client can be connected. + */ + printDebugInformation(debugInformation: IDebugInformation): IDebugInformation; + + /** + * Enables debugging for the specified devices + * @param {IEnableDebuggingDeviceOptions[]} deviceOpts Settings used for enabling debugging for each device. + * @param {IDebuggingAdditionalOptions} enableDebuggingOptions Settings used for enabling debugging. + * @returns {Promise[]} Array of promises for each device. + */ + enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], enableDebuggingOptions: IDebuggingAdditionalOptions): Promise[]; + + /** + * Disables debugging for the specified devices + * @param {IDisableDebuggingDeviceOptions[]} deviceOptions Settings used for disabling debugging for each device. + * @param {IDebuggingAdditionalOptions} debuggingAdditionalOptions Settings used for disabling debugging. + * @returns {Promise[]} Array of promises for each device. + */ + disableDebugging(deviceOptions: IDisableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[]; + + /** + * Attaches a debugger to the specified device. + * @param {IAttachDebuggerOptions} settings Settings used for controling the attaching process. + * @returns {Promise} Full url and port where the frontend client can be connected. + */ + attachDebugger(settings: IAttachDebuggerOptions): Promise; +} + +/** + * Describes additional debugging settings. + */ +interface IDebuggingAdditionalOptions extends IProjectDir { } + +/** + * Describes settings used when disabling debugging. + */ +interface IDisableDebuggingDeviceOptions extends Mobile.IDeviceIdentifier { } + +interface IOptionalDebuggingOptions { + /** + * Optional debug options - can be used to control the start of a debug process. + */ + debugOptions?: IDebugOptions; +} + +/** + * Describes settings used when enabling debugging. + */ +interface IEnableDebuggingDeviceOptions extends Mobile.IDeviceIdentifier, IOptionalDebuggingOptions { } + +/** + * Describes settings passed to livesync service in order to control event emitting during refresh application. + */ +interface IShouldSkipEmitLiveSyncNotification { + /** + * Whether to skip emitting an event during refresh. Default is false. + */ + shouldSkipEmitLiveSyncNotification: boolean; +} + +/** + * Describes settings used for attaching a debugger. + */ +interface IAttachDebuggerOptions extends IDebuggingAdditionalOptions, IEnableDebuggingDeviceOptions, IIsEmulator, IPlatform, IOptionalOutputPath { +} + +interface ILiveSyncWatchInfo { + projectData: IProjectData; + filesToRemove: string[]; + filesToSync: string[]; + isReinstalled: boolean; + syncAllFiles: boolean; + useLiveEdit?: boolean; +} + +interface ILiveSyncResultInfo { + modifiedFilesData: Mobile.ILocalToDevicePathData[]; + isFullSync: boolean; + deviceAppData: Mobile.IDeviceAppData; + useLiveEdit?: boolean; +} + +interface IFullSyncInfo { + projectData: IProjectData; + device: Mobile.IDevice; + watch: boolean; + syncAllFiles: boolean; + useLiveEdit?: boolean; +} + +interface IPlatformLiveSyncService { + fullSync(syncInfo: IFullSyncInfo): Promise; + liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise; + refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise; +} + +interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase { + /** + * Refreshes the application's content on a device + * @param {Mobile.IDeviceAppData} deviceAppData Information about the application and the device. + * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. + * @param {boolean} forceExecuteFullSync If this is passed a full LiveSync is performed instead of an incremental one. + * @param {IProjectData} projectData Project data. + * @return {Promise} + */ + refreshApplication(projectData: IProjectData, + liveSyncInfo: ILiveSyncResultInfo): Promise; + + /** + * Removes specified files from a connected device + * @param {Mobile.IDeviceAppData} deviceAppData Data about device and app. + * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. + * @return {Promise} + */ + removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; +} + +interface IAndroidNativeScriptDeviceLiveSyncService { + /** + * Retrieves the android device's hash service. + * @param {string} appIdentifier Application identifier. + * @return {Promise} The hash service + */ + getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService; +} + +interface IDeviceProjectRootOptions { + appIdentifier: string; + getDirname?: boolean; + syncAllFiles?: boolean; + watch?: boolean; +} + +interface IDevicePathProvider { + getDeviceProjectRootPath(device: Mobile.IDevice, options: IDeviceProjectRootOptions): Promise; + getDeviceSyncZipPath(device: Mobile.IDevice): string; +} + +interface ILiveSyncCommandHelper { + /** + * Method sets up configuration, before calling livesync and expects that devices are already discovered. + * @param {Mobile.IDevice[]} devices List of discovered devices + * @param {string} platform The platform for which the livesync will be ran + * @param {IDictionary} deviceDebugMap @optional A map representing devices which have debugging enabled initially. + * @returns {Promise} + */ + executeLiveSyncOperation(devices: Mobile.IDevice[], platform: string, deviceDebugMap?: IDictionary): Promise; + getPlatformsForOperation(platform: string): string[]; +} diff --git a/lib/definitions/lockfile.d.ts b/lib/definitions/lockfile.d.ts deleted file mode 100644 index af62919fee..0000000000 --- a/lib/definitions/lockfile.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare module "lockfile" { - export function lock(lockFilename: string, lockParams: ILockParams, callback: (err: Error) => void): void; - export function lockSync(lockFilename: string, lockParams: ILockSyncParams): void; - export function unlock(lockFilename: string, callback: (err: Error) => void): void; - export function unlockSync(lockFilename: string): void; - export function check(lockFilename: string, lockParams: ILockParams, callback: (err: Error, isLocked: boolean) => void): boolean; - export function checkSync(path: string, opts: Options): boolean; - - export interface Options { - wait?: number; - stale?: number; - retries?: number; - retryWait?: number; - } - - interface ILockSyncParams { - retries: number; - stale: number; - } - - interface ILockParams extends ILockSyncParams { - retryWait: number; - } -} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 638d675f7c..535680ba60 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -1,7 +1,7 @@ interface IPlatformService extends NodeJS.EventEmitter { - cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, framework?: string): Promise; + cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, framework?: string): Promise; - addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, frameworkPath?: string): Promise; + addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, frameworkPath?: string): Promise; /** * Gets list of all installed platforms (the ones for which /platforms/ exists). @@ -32,7 +32,7 @@ interface IPlatformService extends NodeJS.EventEmitter { */ removePlatforms(platforms: string[], projectData: IProjectData): Promise; - updatePlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; + updatePlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions): Promise; /** * Ensures that the specified platform and its dependencies are installed. @@ -43,11 +43,11 @@ interface IPlatformService extends NodeJS.EventEmitter { * @param {IAppFilesUpdaterOptions} appFilesUpdaterOptions Options needed to instantiate AppFilesUpdater class. * @param {string} platformTemplate The name of the platform template. * @param {IProjectData} projectData DTO with information about the project. - * @param {IPlatformSpecificData} platformSpecificData Platform specific data required for project preparation. + * @param {IAddPlatformCoreOptions} config Options required for project preparation/creation. * @param {Array} filesToSync Files about to be synced to device. * @returns {boolean} true indicates that the platform was prepared. */ - preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, filesToSync?: Array): Promise; + preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, filesToSync?: Array, nativePrepare?: INativePrepare): Promise; /** * Determines whether a build is necessary. A build is necessary when one of the following is true: @@ -55,10 +55,11 @@ interface IPlatformService extends NodeJS.EventEmitter { * - the .nsbuildinfo file in product folder points to an old prepare. * @param {string} platform The platform to build. * @param {IProjectData} projectData DTO with information about the project. - * @param {IBuildConfig} buildConfig Indicates whether the build is for device or emulator. + * @param {IBuildConfig} @optional buildConfig Indicates whether the build is for device or emulator. + * @param {string} @optional outputPath Directory containing build information and artifacts. * @returns {boolean} true indicates that the platform should be build. */ - shouldBuild(platform: string, projectData: IProjectData, buildConfig?: IBuildConfig): Promise; + shouldBuild(platform: string, projectData: IProjectData, buildConfig?: IBuildConfig, outputPath?: string): Promise; /** * Builds the native project for the specified platform for device or emulator. @@ -77,9 +78,10 @@ interface IPlatformService extends NodeJS.EventEmitter { * - the .nsbuildinfo file located in application root folder is different than the local .nsbuildinfo file * @param {Mobile.IDevice} device The device where the application should be installed. * @param {IProjectData} projectData DTO with information about the project. + * @param {string} @optional outputPath Directory containing build information and artifacts. * @returns {Promise} true indicates that the application should be installed. */ - shouldInstall(device: Mobile.IDevice, projectData: IProjectData): Promise; + shouldInstall(device: Mobile.IDevice, projectData: IProjectData, outputPath?: string): Promise; /** * Installs the application on specified device. @@ -87,17 +89,19 @@ interface IPlatformService extends NodeJS.EventEmitter { * * .nsbuildinfo is not persisted when building for release. * @param {Mobile.IDevice} device The device where the application should be installed. * @param {IRelease} options Whether the application was built in release configuration. + * @param {string} @optional pathToBuiltApp Path to build artifact. + * @param {string} @optional outputPath Directory containing build information and artifacts. * @param {IProjectData} projectData DTO with information about the project. * @returns {void} */ - installApplication(device: Mobile.IDevice, options: IRelease, projectData: IProjectData): Promise; + installApplication(device: Mobile.IDevice, options: IRelease, projectData: IProjectData, pathToBuiltApp?: string, outputPath?: string): Promise; /** * Gets first chance to validate the options provided as command line arguments. * If no platform is provided or a falsy (null, undefined, "", false...) platform is provided, * the options will be validated for all available platforms. */ - validateOptions(provision: any, projectData: IProjectData, platform?: string): Promise; + validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string): Promise; /** * Executes prepare, build and installOnPlatform when necessary to ensure that the latest version of the app is installed on specified platform. @@ -106,10 +110,10 @@ interface IPlatformService extends NodeJS.EventEmitter { * @param {IAppFilesUpdaterOptions} appFilesUpdaterOptions Options needed to instantiate AppFilesUpdater class. * @param {IDeployPlatformOptions} deployOptions Various options that can manage the deploy operation. * @param {IProjectData} projectData DTO with information about the project. - * @param {any} platformSpecificData Platform specific data required for project preparation. + * @param {IAddPlatformCoreOptions} config Options required for project preparation/creation. * @returns {void} */ - deployPlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, deployOptions: IDeployPlatformOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; + deployPlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, deployOptions: IDeployPlatformOptions, projectData: IProjectData, config: IPlatformOptions): Promise; /** * Runs the application on specified platform. Assumes that the application is already build and installed. Fails if this is not true. @@ -120,41 +124,39 @@ interface IPlatformService extends NodeJS.EventEmitter { */ startApplication(platform: string, runOptions: IRunPlatformOptions, projectId: string): Promise; - /** - * The emulate command. In addition to `run --emulator` command, it handles the `--available-devices` option to show the available devices. - * @param {string} platform The platform to emulate. - * @param {IAppFilesUpdaterOptions} appFilesUpdaterOptions Options needed to instantiate AppFilesUpdater class. - * @param {IEmulatePlatformOptions} emulateOptions Various options that can manage the emulate operation. - * @param {IProjectData} projectData DTO with information about the project. - * @param {any} platformSpecificData Platform specific data required for project preparation. - * @returns {void} - */ - emulatePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, emulateOptions: IEmulatePlatformOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; - - cleanDestinationApp(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; + cleanDestinationApp(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions): Promise; validatePlatformInstalled(platform: string, projectData: IProjectData): void; /** * Ensures the passed platform is a valid one (from the supported ones) - * and that it can be built on the current OS */ validatePlatform(platform: string, projectData: IProjectData): void; + /** + * Checks whether passed platform can be built on the current OS + * @param {string} platform The mobile platform. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {boolean} Whether the platform is supported for current OS or not. + */ + isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean; + /** * Returns information about the latest built application for device in the current project. * @param {IPlatformData} platformData Data describing the current platform. * @param {IBuildConfig} buildConfig Defines if the build is for release configuration. + * @param {string} @optional outputPath Directory that should contain the build artifact. * @returns {IApplicationPackage} Information about latest built application. */ - getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage; + getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage; /** * Returns information about the latest built application for simulator in the current project. * @param {IPlatformData} platformData Data describing the current platform. * @param {IBuildConfig} buildConfig Defines if the build is for release configuration. + * @param {string} @optional outputPath Directory that should contain the build artifact. * @returns {IApplicationPackage} Information about latest built application. */ - getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage; + getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage; /** * Copies latest build output to a specified location. @@ -171,9 +173,10 @@ interface IPlatformService extends NodeJS.EventEmitter { * @param {string} platform Mobile platform - Android, iOS. * @param {IBuildConfig} buildConfig Defines if the searched artifact should be for simulator and is it built for release. * @param {IProjectData} projectData DTO with information about the project. + * @param {string} @optional outputPath Directory that should contain the build artifact. * @returns {string} The path to latest built artifact. */ - lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): string; + lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData, outputPath?: string): string; /** * Reads contents of a file on device. @@ -199,37 +202,43 @@ interface IPlatformService extends NodeJS.EventEmitter { * @returns {Promise} */ trackActionForPlatform(actionData: ITrackPlatformAction): Promise; + + /** + * Saves build information in a proprietary file. + * @param {string} platform The build platform. + * @param {string} projectDir The project's directory. + * @param {string} buildInfoFileDirname The directory where the build file should be written to. + * @returns {void} + */ + saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void } +interface IPlatformOptions extends IPlatformSpecificData, ICreateProjectOptions { } + /** * Platform specific data required for project preparation. */ -interface IPlatformSpecificData { +interface IPlatformSpecificData extends IProvision, ITeamIdentifier { /** - * UUID of the provisioning profile used in iOS project preparation. + * Target SDK for Android. */ - provision: any; + sdk: string; /** - * Target SDK for Android. + * Data from mobileProvision. */ - sdk: string; + mobileProvisionData?: any; } /** * Describes information that will be tracked for specific action related for platforms - build, livesync, etc. */ -interface ITrackPlatformAction { +interface ITrackPlatformAction extends IPlatform { /** * Name of the action. */ action: string; - /** - * Platform for which the action will be executed. - */ - platform: string; - /** * Defines if the action is for device or emulator. */ @@ -268,12 +277,13 @@ interface IPlatformsData { } interface INodeModulesBuilder { - prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise; + prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; + prepareJSNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; cleanNodeModules(absoluteOutputPath: string, platform: string): void; } interface INodeModulesDependenciesBuilder { - getProductionDependencies(projectPath: string): void; + getProductionDependencies(projectPath: string): IDependencyData[]; } interface IBuildInfo { diff --git a/lib/definitions/plugins.d.ts b/lib/definitions/plugins.d.ts index aed9cb69c5..ded728e3cb 100644 --- a/lib/definitions/plugins.d.ts +++ b/lib/definitions/plugins.d.ts @@ -1,10 +1,10 @@ interface IPluginsService { add(plugin: string, projectData: IProjectData): Promise; // adds plugin by name, github url, local path and et. remove(pluginName: string, projectData: IProjectData): Promise; // removes plugin only by name - getAvailable(filter: string[]): Promise>; // gets all available plugins - prepare(pluginData: IDependencyData, platform: string, projectData: IProjectData): Promise; + prepare(pluginData: IDependencyData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; getAllInstalledPlugins(projectData: IProjectData): Promise; ensureAllDependenciesAreInstalled(projectData: IProjectData): Promise; + preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): void /** * Returns all dependencies and devDependencies from pacakge.json file. @@ -13,6 +13,8 @@ interface IPluginsService { */ getDependenciesFromPackageJson(projectDir: string): IPackageJsonDepedenciesResult; validate(platformData: IPlatformData, projectData: IProjectData): Promise; + preparePluginNativeCode(pluginData: IPluginData, platform: string, projectData: IProjectData): Promise; + convertToPluginData(cacheData: any, projectDir: string): IPluginData; } interface IPackageJsonDepedenciesResult { diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index cd5bae3cfa..0f2143102a 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -1,30 +1,43 @@ -interface IPrepareInfo { +interface IPrepareInfo extends IAddedNativePlatform { time: string; bundle: boolean; release: boolean; + projectFileHash: string; changesRequireBuild: boolean; changesRequireBuildTime: string; - iOSProvisioningProfileUUID?: string; } -interface IProjectChangesInfo { +interface IProjectChangesInfo extends IAddedNativePlatform { appFilesChanged: boolean; appResourcesChanged: boolean; modulesChanged: boolean; configChanged: boolean; packageChanged: boolean; nativeChanged: boolean; - hasChanges: boolean; - changesRequireBuild: boolean; + signingChanged: boolean; + + readonly hasChanges: boolean; + readonly changesRequireBuild: boolean; + readonly changesRequirePrepare: boolean; } -interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision {} +interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision, ITeamIdentifier { + nativePlatformStatus?: "1" | "2" | "3"; +} interface IProjectChangesService { - checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): IProjectChangesInfo; + checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): Promise; getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo; savePrepareInfo(platform: string, projectData: IProjectData): void; getPrepareInfoFilePath(platform: string, projectData: IProjectData): string; + setNativePlatformStatus(platform: string, projectData: IProjectData, nativePlatformStatus: IAddedNativePlatform): void; currentChanges: IProjectChangesInfo; -} \ No newline at end of file +} + +/** + * NativePlatformStatus.requiresPlatformAdd | NativePlatformStatus.requiresPrepare | NativePlatformStatus.alreadyPrepared + */ +interface IAddedNativePlatform { + nativePlatformStatus: "1" | "2" | "3"; +} diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index e6f6a5a281..e88532323e 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -53,8 +53,7 @@ interface IProjectService { isValidNativeScriptProject(pathToProject?: string): boolean; } -interface IProjectData { - projectDir: string; +interface IProjectData extends IProjectDir { projectName: string; platformsDir: string; projectFilePath: string; @@ -69,7 +68,7 @@ interface IProjectData { * @param {string} projectDir Project root directory. * @returns {void} */ - initializeProjectData(projectDir? :string): void; + initializeProjectData(projectDir?: string): void; } interface IProjectDataService { @@ -105,6 +104,8 @@ interface IProjectDataService { * @returns {void} */ removeDependency(projectDir: string, dependencyName: string): void; + + getProjectData(projectDir: string): IProjectData; } /** @@ -130,8 +131,11 @@ interface IBuildForDevice { buildForDevice: boolean; } -interface IBuildConfig extends IAndroidBuildOptionsSettings, IiOSBuildConfig { - projectDir: string; +interface INativePrepare { + skipNativePrepare: boolean; +} + +interface IBuildConfig extends IAndroidBuildOptionsSettings, IiOSBuildConfig, IProjectDir { clean?: boolean; architectures?: string[]; buildOutputStdio?: string; @@ -149,16 +153,12 @@ interface IiOSBuildConfig extends IBuildForDevice, IDeviceIdentifier, IProvision * Code sign identity used for build. If not set iPhone Developer is used as a default when building for device. */ codeSignIdentity?: string; - /** - * Team identifier. - */ - teamIdentifier?: string; } interface IPlatformProjectService extends NodeJS.EventEmitter { getPlatformData(projectData: IProjectData): IPlatformData; validate(projectData: IProjectData): Promise; - createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, pathToTemplate?: string): Promise; + createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise; interpolateData(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise; interpolateConfigurationFile(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): void; @@ -176,7 +176,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter { * @param {any} provision UUID of the provisioning profile used in iOS option validation. * @returns {void} */ - validateOptions(projectId?: string, provision?: true | string): Promise; + validateOptions(projectId?: string, provision?: true | string, teamId?: true | string): Promise; validatePlugins(projectData: IProjectData): Promise; @@ -225,7 +225,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter { removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise; afterPrepareAllPlugins(projectData: IProjectData): Promise; - beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDictionary): Promise; + beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise; /** * Gets the path wheren App_Resources should be copied. @@ -258,6 +258,12 @@ interface IPlatformProjectService extends NodeJS.EventEmitter { * @returns {void} */ cleanProject(projectRoot: string, projectData: IProjectData): Promise + + /** + * Check the current state of the project, and validate against the options. + * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. + */ + checkForChanges(changeset: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise; } interface IAndroidProjectPropertiesManager { @@ -267,8 +273,8 @@ interface IAndroidProjectPropertiesManager { } interface ITestExecutionService { - startTestRunner(platform: string, projectData: IProjectData): Promise; - startKarmaServer(platform: string, projectData: IProjectData): Promise; + startTestRunner(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; + startKarmaServer(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise; } /** diff --git a/lib/definitions/simple-plist.d.ts b/lib/definitions/simple-plist.d.ts new file mode 100644 index 0000000000..adc559fc81 --- /dev/null +++ b/lib/definitions/simple-plist.d.ts @@ -0,0 +1,4 @@ +declare module "simple-plist" { + export function readFile(filePath: string, callback?:(err: Error, obj: any) => void): void; + export function readFileSync(filePath: string): any; +} diff --git a/lib/definitions/subscription-service.d.ts b/lib/definitions/subscription-service.d.ts new file mode 100644 index 0000000000..4e4d0e79d1 --- /dev/null +++ b/lib/definitions/subscription-service.d.ts @@ -0,0 +1,11 @@ +/** + * Describes methods for subscribing to different NativeScript campaigns. + */ +interface ISubscriptionService { + /** + * Subscribes users for NativeScript's newsletter by asking them for their email. + * In case we've already asked the user for his email, this method will do nothing. + * @returns {Promise} + */ + subscribeForNewsletter(): Promise; +} diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts new file mode 100644 index 0000000000..dba30ee452 --- /dev/null +++ b/lib/device-path-provider.ts @@ -0,0 +1,44 @@ +import { fromWindowsRelativePathToUnix } from "./common/helpers"; +import { APP_FOLDER_NAME, LiveSyncPaths } from "./constants"; +import { AndroidDeviceLiveSyncService } from "./services/livesync/android-device-livesync-service"; +import * as path from "path"; + +export class DevicePathProvider implements IDevicePathProvider { + constructor(private $mobileHelper: Mobile.IMobileHelper, + private $injector: IInjector, + private $iOSSimResolver: Mobile.IiOSSimResolver) { + } + + public async getDeviceProjectRootPath(device: Mobile.IDevice, options: IDeviceProjectRootOptions): Promise { + let projectRoot = ""; + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { + if (device.isEmulator) { + const applicationPath = this.$iOSSimResolver.iOSSim.getApplicationPath(device.deviceInfo.identifier, options.appIdentifier); + projectRoot = path.join(applicationPath); + } else { + projectRoot = LiveSyncPaths.IOS_DEVICE_PROJECT_ROOT_PATH; + } + + if (!options.getDirname) { + projectRoot = path.join(projectRoot, APP_FOLDER_NAME); + } + } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { + projectRoot = `/data/local/tmp/${options.appIdentifier}`; + if (!options.getDirname) { + const deviceLiveSyncService = this.$injector.resolve(AndroidDeviceLiveSyncService, { _device: device }); + const hashService = deviceLiveSyncService.getDeviceHashService(options.appIdentifier); + const hashFile = options.syncAllFiles ? null : await hashService.doesShasumFileExistsOnDevice(); + const syncFolderName = options.watch || hashFile ? LiveSyncPaths.SYNC_DIR_NAME : LiveSyncPaths.FULLSYNC_DIR_NAME; + projectRoot = path.join(projectRoot, syncFolderName); + } + } + + return fromWindowsRelativePathToUnix(projectRoot); + } + + public getDeviceSyncZipPath(device: Mobile.IDevice): string { + return this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform) && !device.isEmulator ? LiveSyncPaths.IOS_DEVICE_SYNC_ZIP_PATH : undefined; + } +} + +$injector.register("devicePathProvider", DevicePathProvider); diff --git a/lib/device-sockets/ios/packet-stream.ts b/lib/device-sockets/ios/packet-stream.ts index accbbfb9ca..7a4fbbe2f6 100644 --- a/lib/device-sockets/ios/packet-stream.ts +++ b/lib/device-sockets/ios/packet-stream.ts @@ -12,14 +12,14 @@ export class PacketStream extends stream.Transform { while (packet.length > 0) { if (!this.buffer) { // read length - let length = packet.readInt32BE(0); + const length = packet.readInt32BE(0); this.buffer = new Buffer(length); this.offset = 0; packet = packet.slice(4); } packet.copy(this.buffer, this.offset); - let copied = Math.min(this.buffer.length - this.offset, packet.length); + const copied = Math.min(this.buffer.length - this.offset, packet.length); this.offset += copied; packet = packet.slice(copied); diff --git a/lib/device-sockets/ios/socket-proxy-factory.ts b/lib/device-sockets/ios/socket-proxy-factory.ts index 45f32abf8b..33a75b7b03 100644 --- a/lib/device-sockets/ios/socket-proxy-factory.ts +++ b/lib/device-sockets/ios/socket-proxy-factory.ts @@ -9,7 +9,6 @@ import temp = require("temp"); export class SocketProxyFactory extends EventEmitter implements ISocketProxyFactory { constructor(private $logger: ILogger, private $errors: IErrors, - private $config: IConfiguration, private $options: IOptions, private $net: INet) { super(); @@ -20,7 +19,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); - let server = net.createServer({ + const server = net.createServer({ allowHalfOpen: true }); @@ -29,7 +28,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact frontendSocket.on("end", () => { this.$logger.info('Frontend socket closed!'); - if (!(this.$config.debugLivesync && this.$options.watch)) { + if (!this.$options.watch) { process.exit(0); } }); @@ -39,7 +38,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact backendSocket.on("end", () => { this.$logger.info("Backend socket closed!"); - if (!(this.$config.debugLivesync && this.$options.watch)) { + if (!this.$options.watch) { process.exit(0); } }); @@ -64,7 +63,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact }); }); - let socketFileLocation = temp.path({ suffix: ".sock" }); + const socketFileLocation = temp.path({ suffix: ".sock" }); server.listen(socketFileLocation); if (!this.$options.client) { this.$logger.info("socket-file-location: " + socketFileLocation); @@ -75,7 +74,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact public async createWebSocketProxy(factory: () => Promise): Promise { // NOTE: We will try to provide command line options to select ports, at least on the localhost. - const localPort = await this.$net.getAvailablePortInRange(8080); + const localPort = await this.$net.getFreePort(); this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n"); @@ -84,7 +83,7 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact // We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it // in the connection callback. - let server = new ws.Server({ + const server = new ws.Server({ port: localPort, verifyClient: async (info: any, callback: Function) => { this.$logger.info("Frontend client connected."); @@ -103,27 +102,37 @@ export class SocketProxyFactory extends EventEmitter implements ISocketProxyFact } }); server.on("connection", (webSocket) => { - let encoding = "utf16le"; + const encoding = "utf16le"; - let deviceSocket: net.Socket = (webSocket.upgradeReq)["__deviceSocket"]; - let packets = new PacketStream(); + const deviceSocket: net.Socket = (webSocket.upgradeReq)["__deviceSocket"]; + const packets = new PacketStream(); deviceSocket.pipe(packets); packets.on("data", (buffer: Buffer) => { webSocket.send(buffer.toString(encoding)); }); + webSocket.on("error", err => { + this.$logger.trace("Error on debugger websocket", err); + }); + + deviceSocket.on("error", err => { + this.$logger.trace("Error on debugger deviceSocket", err); + }); + webSocket.on("message", (message, flags) => { - let length = Buffer.byteLength(message, encoding); - let payload = new Buffer(length + 4); + const length = Buffer.byteLength(message, encoding); + const payload = new Buffer(length + 4); payload.writeInt32BE(length, 0); payload.write(message, 4, length, encoding); deviceSocket.write(payload); }); - deviceSocket.on("end", () => { + deviceSocket.on("close", () => { this.$logger.info("Backend socket closed!"); - process.exit(0); + if (!this.$options.watch) { + process.exit(0); + } }); webSocket.on("close", () => { diff --git a/lib/dynamic-help-provider.ts b/lib/dynamic-help-provider.ts index 84aa255f27..ab8c43d3b0 100644 --- a/lib/dynamic-help-provider.ts +++ b/lib/dynamic-help-provider.ts @@ -6,7 +6,7 @@ export class DynamicHelpProvider implements IDynamicHelpProvider { } public getLocalVariables(options: { isHtml: boolean }): IDictionary { - let localVariables: IDictionary = { + const localVariables: IDictionary = { constants: constants }; return localVariables; diff --git a/lib/lockfile.ts b/lib/lockfile.ts deleted file mode 100644 index d9e0082274..0000000000 --- a/lib/lockfile.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as lockfile from "lockfile"; -import * as path from "path"; - -export class LockFile implements ILockFile { - private lockFilePath: string; - - constructor(private $options: IOptions) { - this.lockFilePath = path.join(this.$options.profileDir, ".lock"); - } - - private static LOCK_EXPIRY_PERIOD_SEC = 180; - private static LOCK_PARAMS = { - retryWait: 100, - retries: LockFile.LOCK_EXPIRY_PERIOD_SEC * 10, - stale: LockFile.LOCK_EXPIRY_PERIOD_SEC * 1000 - }; - - public lock(): void { - lockfile.lockSync(this.lockFilePath, LockFile.LOCK_PARAMS); - } - - public unlock(): void { - lockfile.unlockSync(this.lockFilePath); - } - - public check(): boolean { - return lockfile.checkSync(this.lockFilePath, LockFile.LOCK_PARAMS); - } -} - -$injector.register("lockfile", LockFile); diff --git a/lib/nativescript-cli-lib-bootstrap.ts b/lib/nativescript-cli-lib-bootstrap.ts index 505c1a76fc..1e545239e3 100644 --- a/lib/nativescript-cli-lib-bootstrap.ts +++ b/lib/nativescript-cli-lib-bootstrap.ts @@ -9,9 +9,8 @@ $injector.requirePublic("companionAppsService", "./common/appbuilder/services/li $injector.requirePublicClass("deviceEmitter", "./common/appbuilder/device-emitter"); $injector.requirePublicClass("deviceLogProvider", "./common/appbuilder/device-log-provider"); $injector.requirePublicClass("localBuildService", "./services/local-build-service"); -$injector.requirePublicClass("debugService", "./services/debug-service"); -$injector.require("iOSLogFilter", "./common/mobile/ios/ios-log-filter"); // We need this because some services check if (!$options.justlaunch) to start the device log after some operation. // We don't want this behaviour when the CLI is required as library. $injector.resolve("options").justlaunch = true; +$injector.resolve("staticConfig").disableAnalytics = true; diff --git a/lib/node-package-manager.ts b/lib/node-package-manager.ts index 45591a8122..60ca9c9d54 100644 --- a/lib/node-package-manager.ts +++ b/lib/node-package-manager.ts @@ -1,46 +1,71 @@ import * as path from "path"; +import { exported } from "./common/decorators"; +import { isInteractive } from "./common/helpers"; export class NodePackageManager implements INodePackageManager { + private static SCOPED_DEPENDENCY_REGEXP = /^(@.+?)(?:@(.+?))?$/; + private static DEPENDENCY_REGEXP = /^(.+?)(?:@(.+?))?$/; + constructor(private $fs: IFileSystem, private $hostInfo: IHostInfo, private $errors: IErrors, private $childProcess: IChildProcess, - private $logger: ILogger, - private $options: IOptions) { } + private $logger: ILogger) { } - public async install(packageName: string, pathToSave: string, config?: any): Promise { - if (this.$options.disableNpmInstall) { + @exported("npm") + public async install(packageName: string, pathToSave: string, config: INodePackageManagerInstallOptions): Promise { + if (config.disableNpmInstall) { return; } - if (this.$options.ignoreScripts) { - config = config || {}; + if (config.ignoreScripts) { config["ignore-scripts"] = true; } - let packageJsonPath = path.join(pathToSave, "package.json"); - let jsonContentBefore = this.$fs.readJson(packageJsonPath); - let dependenciesBefore = _.keys(jsonContentBefore.dependencies).concat(_.keys(jsonContentBefore.devDependencies)); + const packageJsonPath = path.join(pathToSave, "package.json"); + const jsonContentBefore = this.$fs.readJson(packageJsonPath); - let flags = this.getFlagsString(config, true); + const flags = this.getFlagsString(config, true); let params = ["install"]; - if (packageName !== pathToSave) { - params.push(packageName); //because npm install ${pwd} on mac tries to install itself as a dependency (windows and linux have no such issues) + const isInstallingAllDependencies = packageName === pathToSave; + if (!isInstallingAllDependencies) { + params.push(packageName); } + params = params.concat(flags); - let pwd = pathToSave; + const cwd = pathToSave; + // Npm creates `etc` directory in installation dir when --prefix is passed + // https://github.com/npm/npm/issues/11486 + // we should delete it if it was created because of us + const etcDirectoryLocation = path.join(cwd, "etc"); + const etcExistsPriorToInstallation = this.$fs.exists(etcDirectoryLocation); + //TODO: plamen5kov: workaround is here for a reason (remove whole file later) - if (this.$options.path) { + if (config.path) { let relativePathFromCwdToSource = ""; - if (this.$options.frameworkPath) { - relativePathFromCwdToSource = path.relative(this.$options.frameworkPath, pathToSave); + if (config.frameworkPath) { + relativePathFromCwdToSource = path.relative(config.frameworkPath, pathToSave); if (this.$fs.exists(relativePathFromCwdToSource)) { packageName = relativePathFromCwdToSource; } } } + try { - let spawnResult: ISpawnResult = await this.$childProcess.spawnFromEvent(this.getNpmExecutableName(), params, "close", { cwd: pwd, stdio: "inherit" }); - this.$logger.out(spawnResult.stdout); + const spawnResult: ISpawnResult = await this.getNpmInstallResult(params, cwd); + + // Whenever calling npm install without any arguments (hence installing all dependencies) no output is emitted on stdout + // Luckily, whenever you call npm install to install all dependencies chances are you won't need the name/version of the package you're installing because there is none. + if (isInstallingAllDependencies) { + return null; + } + + params = params.concat(["--json", "--dry-run", "--prefix", cwd]); + // After the actual install runs successfully execute a dry-run in order to get information about the package. + // We cannot use the actual install with --json to get the information because of post-install scripts which may print on stdout + // dry-run install is quite fast when the dependencies are already installed even for many dependencies (e.g. angular) so we can live with this approach + // We need the --prefix here because without it no output is emitted on stdout because all the dependencies are already installed. + const spawnNpmDryRunResult = await this.$childProcess.spawnFromEvent(this.getNpmExecutableName(), params, "close"); + return this.parseNpmInstallResult(spawnNpmDryRunResult.stdout, spawnResult.stdout, packageName); } catch (err) { if (err.message && err.message.indexOf("EPEERINVALID") !== -1) { // Not installed peer dependencies are treated by npm 2 as errors, but npm 3 treats them as warnings. @@ -52,58 +77,35 @@ export class NodePackageManager implements INodePackageManager { this.$fs.writeJson(packageJsonPath, jsonContentBefore); throw err; } + } finally { + if (!etcExistsPriorToInstallation) { + this.$fs.deleteDirectory(etcDirectoryLocation); + } } - - let jsonContentAfter = this.$fs.readJson(path.join(pathToSave, "package.json")); - let dependenciesAfter = _.keys(jsonContentAfter.dependencies).concat(_.keys(jsonContentAfter.devDependencies)); - - /** This diff is done in case the installed pakcage is a URL address, a path to local directory or a .tgz file - * in these cases we don't have the package name and we can't rely on "npm install --json"" option - * to get the project name because we have to parse the output from the stdout and we have no controll over it (so other messages may be mangled in stdout) - * The solution is to compare package.json project dependencies before and after install and get the name of the installed package, - * even if it's installed through local path or URL. If command installes more than one package, only the package originally installed is returned. - */ - let dependencyDiff = _(jsonContentAfter.dependencies) - .omitBy((val: string, key: string) => jsonContentBefore && jsonContentBefore.dependencies && jsonContentBefore.dependencies[key] && jsonContentBefore.dependencies[key] === val) - .keys() - .value(); - - let devDependencyDiff = _(jsonContentAfter.devDependencies) - .omitBy((val: string, key: string) => jsonContentBefore && jsonContentBefore.devDependencies && jsonContentBefore.devDependencies[key] && jsonContentBefore.devDependencies[key] === val) - .keys() - .value(); - - let diff = dependencyDiff.concat(devDependencyDiff); - - if (diff.length <= 0 && dependenciesBefore.length === dependenciesAfter.length && packageName !== pathToSave) { - this.$logger.warn(`The plugin ${packageName} is already installed`); - } - if (diff.length <= 0 && dependenciesBefore.length !== dependenciesAfter.length) { - this.$logger.warn(`Couldn't install package ${packageName} correctly`); - } - - return diff; } - public async uninstall(packageName: string, config?: any, path?: string): Promise { - let flags = this.getFlagsString(config, false); + @exported("npm") + public async uninstall(packageName: string, config?: any, path?: string): Promise { + const flags = this.getFlagsString(config, false); return this.$childProcess.exec(`npm uninstall ${packageName} ${flags}`, { cwd: path }); } - public async search(filter: string[], config: any): Promise { - let args = (([filter] || [])).concat(config.silent); - return this.$childProcess.exec(`npm search ${args.join(" ")}`); + @exported("npm") + public async search(filter: string[], config: any): Promise { + const flags = this.getFlagsString(config, false); + return this.$childProcess.exec(`npm search ${filter.join(" ")} ${flags}`); } + @exported("npm") public async view(packageName: string, config: Object): Promise { const wrappedConfig = _.extend({}, config, { json: true }); // always require view response as JSON - let flags = this.getFlagsString(wrappedConfig, false); + const flags = this.getFlagsString(wrappedConfig, false); let viewResult: any; try { viewResult = await this.$childProcess.exec(`npm view ${packageName} ${flags}`); } catch (e) { - this.$errors.failWithoutHelp(e); + this.$errors.failWithoutHelp(e.message); } return JSON.parse(viewResult); } @@ -119,8 +121,8 @@ export class NodePackageManager implements INodePackageManager { } private getFlagsString(config: any, asArray: boolean): any { - let array: Array = []; - for (let flag in config) { + const array: Array = []; + for (const flag in config) { if (flag === "global") { array.push(`--${flag}`); array.push(`${config[flag]}`); @@ -138,6 +140,135 @@ export class NodePackageManager implements INodePackageManager { return array.join(" "); } + + private parseNpmInstallResult(npmDryRunInstallOutput: string, npmInstallOutput: string, userSpecifiedPackageName: string): INpmInstallResultInfo { + // TODO: Add tests for this functionality + try { + const originalOutput: INpmInstallCLIResult | INpm5InstallCliResult = JSON.parse(npmDryRunInstallOutput); + const npm5Output = originalOutput; + const npmOutput = originalOutput; + let name: string; + _.forOwn(npmOutput.dependencies, (peerDependency: INpmPeerDependencyInfo, key: string) => { + if (!peerDependency.required && !peerDependency.peerMissing) { + name = key; + return false; + } + }); + + // Npm 5 return different object after performing `npm install --dry-run`. + // Considering that the dependency is already installed we should + // find it in the `updated` key as a first element of the array. + if (!name && npm5Output.updated) { + const updatedDependency = npm5Output.updated[0]; + return { + name: updatedDependency.name, + originalOutput, + version: updatedDependency.version + }; + } + const dependency = _.pick(npmOutput.dependencies, name); + return { + name, + originalOutput, + version: dependency[name].version + }; + } catch (err) { + this.$logger.trace(`Unable to parse result of npm --dry-run operation. Output is: ${npmDryRunInstallOutput}.`); + this.$logger.trace("Now we'll try to parse the real output of npm install command."); + + const npmOutputMatchRegExp = /^.--\s+(?!UNMET)(.*)@((?:\d+\.){2}\d+)/m; + const match = npmInstallOutput.match(npmOutputMatchRegExp); + if (match) { + return { + name: match[1], + version: match[2] + }; + } + } + + this.$logger.trace("Unable to get information from npm installation, trying to return value specified by user."); + return this.getDependencyInformation(userSpecifiedPackageName); + } + + private getDependencyInformation(dependency: string): INpmInstallResultInfo { + const scopeDependencyMatch = dependency.match(NodePackageManager.SCOPED_DEPENDENCY_REGEXP); + let name: string = null; + let version: string = null; + + if (scopeDependencyMatch) { + name = scopeDependencyMatch[1]; + version = scopeDependencyMatch[2]; + } else { + const matches = dependency.match(NodePackageManager.DEPENDENCY_REGEXP); + if (matches) { + name = matches[1]; + version = matches[2]; + } + } + + return { + name, + version + }; + } + + private async getNpmInstallResult(params: string[], cwd: string): Promise { + return new Promise((resolve, reject) => { + const npmExecutable = this.getNpmExecutableName(); + const stdioValue = isInteractive() ? "inherit" : "pipe"; + + const childProcess = this.$childProcess.spawn(npmExecutable, params, { cwd, stdio: stdioValue }); + + let isFulfilled = false; + let capturedOut = ""; + let capturedErr = ""; + + if (childProcess.stdout) { + childProcess.stdout.on("data", (data: string) => { + this.$logger.write(data.toString()); + capturedOut += data; + }); + } + + if (childProcess.stderr) { + childProcess.stderr.on("data", (data: string) => { + capturedErr += data; + }); + } + + childProcess.on("close", (arg: any) => { + const exitCode = typeof arg === "number" ? arg : arg && arg.code; + + if (exitCode === 0) { + isFulfilled = true; + const result = { + stdout: capturedOut, + stderr: capturedErr, + exitCode + }; + + resolve(result); + } else { + let errorMessage = `Command ${npmExecutable} ${params && params.join(" ")} failed with exit code ${exitCode}`; + if (capturedErr) { + errorMessage += ` Error output: \n ${capturedErr}`; + } + + if (!isFulfilled) { + isFulfilled = true; + reject(new Error(errorMessage)); + } + } + }); + + childProcess.on("error", (err: Error) => { + if (!isFulfilled) { + isFulfilled = true; + reject(err); + } + }); + }); + } } $injector.register("npm", NodePackageManager); diff --git a/lib/node/pbxproj-dom-xcode.ts b/lib/node/pbxproj-dom-xcode.ts new file mode 100644 index 0000000000..f7bb3a80d7 --- /dev/null +++ b/lib/node/pbxproj-dom-xcode.ts @@ -0,0 +1,7 @@ +import * as pbxprojDomXcodeModule from "pbxproj-dom/xcode"; + +declare global { + type IPbxprojDomXcode = typeof pbxprojDomXcodeModule; +} + +$injector.register("pbxprojDomXcode", pbxprojDomXcodeModule); diff --git a/lib/node/xcode.ts b/lib/node/xcode.ts new file mode 100644 index 0000000000..0e306d8a1a --- /dev/null +++ b/lib/node/xcode.ts @@ -0,0 +1,11 @@ +import * as xcode from "xcode"; + +declare global { + type IXcode = typeof xcode; + export namespace IXcode { + export type project = typeof xcode.project; + export interface Options extends xcode.Options {} // tslint:disable-line + } +} + +$injector.register("xcode", xcode); diff --git a/lib/npm-installation-manager.ts b/lib/npm-installation-manager.ts index 1fc8015910..0911fcecd5 100644 --- a/lib/npm-installation-manager.ts +++ b/lib/npm-installation-manager.ts @@ -20,54 +20,73 @@ export class NpmInstallationManager implements INpmInstallationManager { } public async getLatestCompatibleVersion(packageName: string): Promise { + const configVersion = this.$staticConfig.version; + const isPreReleaseVersion = semver.prerelease(configVersion) !== null; + let cliVersionRange = `~${semver.major(configVersion)}.${semver.minor(configVersion)}.0`; + if (isPreReleaseVersion) { + // if the user has some 0-19 pre-release version, include pre-release versions in the search query. + cliVersionRange = `~${configVersion}`; + } - let cliVersionRange = `~${this.$staticConfig.version}`; - let latestVersion = await this.getLatestVersion(packageName); + const latestVersion = await this.getLatestVersion(packageName); if (semver.satisfies(latestVersion, cliVersionRange)) { return latestVersion; } - let data = await this.$npm.view(packageName, { "versions": true }); - return semver.maxSatisfying(data, cliVersionRange) || latestVersion; + const data = await this.$npm.view(packageName, { "versions": true }); + + const maxSatisfying = semver.maxSatisfying(data, cliVersionRange); + return maxSatisfying || latestVersion; } public async install(packageName: string, projectDir: string, opts?: INpmInstallOptions): Promise { try { - let packageToInstall = this.$options.frameworkPath || packageName; - let pathToSave = projectDir; - let version = (opts && opts.version) || null; - let dependencyType = (opts && opts.dependencyType) || null; + const packageToInstall = this.$options.frameworkPath || packageName; + const pathToSave = projectDir; + const version = (opts && opts.version) || null; + const dependencyType = (opts && opts.dependencyType) || null; return await this.installCore(packageToInstall, pathToSave, version, dependencyType); } catch (error) { this.$logger.debug(error); - throw new Error(error); + throw error; } } public async getInspectorFromCache(inspectorNpmPackageName: string, projectDir: string): Promise { - let inspectorPath = path.join(projectDir, "node_modules", inspectorNpmPackageName); + const inspectorPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, inspectorNpmPackageName); // local installation takes precedence over cache if (!this.inspectorAlreadyInstalled(inspectorPath)) { - let cachepath = (await this.$childProcess.exec("npm get cache")).trim(); - let version = await this.getLatestCompatibleVersion(inspectorNpmPackageName); - let pathToPackageInCache = path.join(cachepath, inspectorNpmPackageName, version); - let pathToUnzippedInspector = path.join(pathToPackageInCache, "package"); + const cachePath = path.join(this.$options.profileDir, constants.INSPECTOR_CACHE_DIRNAME); + this.prepareCacheDir(cachePath); + const pathToPackageInCache = path.join(cachePath, constants.NODE_MODULES_FOLDER_NAME, inspectorNpmPackageName); if (!this.$fs.exists(pathToPackageInCache)) { - await this.$childProcess.exec(`npm cache add ${inspectorNpmPackageName}@${version}`); - let inspectorTgzPathInCache = path.join(pathToPackageInCache, "package.tgz"); - await this.$childProcess.exec(`tar -xf ${inspectorTgzPathInCache} -C ${pathToPackageInCache}`); - await this.$childProcess.exec(`npm install --prefix ${pathToUnzippedInspector}`); + const version = await this.getLatestCompatibleVersion(inspectorNpmPackageName); + await this.$childProcess.exec(`npm install ${inspectorNpmPackageName}@${version} --prefix ${cachePath}`); } + this.$logger.out("Using inspector from cache."); - return pathToUnzippedInspector; + return pathToPackageInCache; } + return inspectorPath; } + private prepareCacheDir(cacheDirName: string): void { + this.$fs.ensureDirectoryExists(cacheDirName); + + const cacheDirPackageJsonLocation = path.join(cacheDirName, constants.PACKAGE_JSON_FILE_NAME); + if (!this.$fs.exists(cacheDirPackageJsonLocation)) { + this.$fs.writeJson(cacheDirPackageJsonLocation, { + name: constants.INSPECTOR_CACHE_DIRNAME, + version: "0.1.0" + }); + } + } + private inspectorAlreadyInstalled(pathToInspector: string): Boolean { if (this.$fs.exists(pathToInspector)) { return true; @@ -80,35 +99,37 @@ export class NpmInstallationManager implements INpmInstallationManager { if (this.$fs.exists(possiblePackageName)) { packageName = possiblePackageName; } - if (packageName.indexOf(".tgz") >= 0) { - version = null; - } + // check if the packageName is url or local file and if it is, let npm install deal with the version - if (this.isURL(packageName) || this.$fs.exists(packageName)) { + if (this.isURL(packageName) || this.$fs.exists(packageName) || this.isTgz(packageName)) { version = null; } else { version = version || await this.getLatestCompatibleVersion(packageName); } - let installedModuleNames = await this.npmInstall(packageName, pathToSave, version, dependencyType); - let installedPackageName = installedModuleNames[0]; + const installResultInfo = await this.npmInstall(packageName, pathToSave, version, dependencyType); + const installedPackageName = installResultInfo.name; - let pathToInstalledPackage = path.join(pathToSave, "node_modules", installedPackageName); + const pathToInstalledPackage = path.join(pathToSave, "node_modules", installedPackageName); return pathToInstalledPackage; } - private isURL(str: string) { - let urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$'; - let url = new RegExp(urlRegex, 'i'); + private isTgz(packageName: string): boolean { + return packageName.indexOf(".tgz") >= 0; + } + + private isURL(str: string): boolean { + const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$'; + const url = new RegExp(urlRegex, 'i'); return str.length < 2083 && url.test(str); } - private async npmInstall(packageName: string, pathToSave: string, version: string, dependencyType: string): Promise { + private async npmInstall(packageName: string, pathToSave: string, version: string, dependencyType: string): Promise { this.$logger.out("Installing ", packageName); packageName = packageName + (version ? `@${version}` : ""); - let npmOptions: any = { silent: true, "save-exact": true }; + const npmOptions: any = { silent: true, "save-exact": true }; if (dependencyType) { npmOptions[dependencyType] = true; @@ -122,7 +143,7 @@ export class NpmInstallationManager implements INpmInstallationManager { * because npm view doens't work with those */ private async getVersion(packageName: string, version: string): Promise { - let data: any = await this.$npm.view(packageName, { "dist-tags": true }); + const data: any = await this.$npm.view(packageName, { "dist-tags": true }); this.$logger.trace("Using version %s. ", data[version]); return data[version]; diff --git a/lib/options.ts b/lib/options.ts index 7f71b3dcbb..4d73db3e4c 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -2,7 +2,7 @@ import * as commonOptionsLibPath from "./common/options"; import * as osenv from "osenv"; import * as path from "path"; -let OptionType = commonOptionsLibPath.OptionType; +const OptionType = commonOptionsLibPath.OptionType; export class Options extends commonOptionsLibPath.OptionsBase { constructor($errors: IErrors, @@ -34,7 +34,7 @@ export class Options extends commonOptionsLibPath.OptionsBase { androidTypings: { type: OptionType.Boolean }, bundle: { type: OptionType.Boolean }, all: { type: OptionType.Boolean }, - teamId: { type: OptionType.String }, + teamId: { type: OptionType.Object }, syncAllFiles: { type: OptionType.Boolean, default: false }, liveEdit: { type: OptionType.Boolean }, chrome: { type: OptionType.Boolean }, @@ -48,7 +48,7 @@ export class Options extends commonOptionsLibPath.OptionsBase { // I guess we can remove this code after some grace period, say after 1.7 is out if ($hostInfo.isWindows) { try { - let shelljs = require("shelljs"), + const shelljs = require("shelljs"), oldSettings = path.join(process.env.LocalAppData, ".nativescript-cli", "user-settings.json"), newSettings = path.join(process.env.AppData, ".nativescript-cli", "user-settings.json"); if (shelljs.test("-e", oldSettings) && !shelljs.test("-e", newSettings)) { @@ -60,7 +60,7 @@ export class Options extends commonOptionsLibPath.OptionsBase { } } - let that = (this); + const that = (this); // if justlaunch is set, it takes precedence over the --watch flag and the default true value if (that.justlaunch) { that.watch = false; diff --git a/lib/platforms-data.ts b/lib/platforms-data.ts index 2e12d32eec..7af6a383f1 100644 --- a/lib/platforms-data.ts +++ b/lib/platforms-data.ts @@ -15,7 +15,13 @@ export class PlatformsData implements IPlatformsData { } public getPlatformData(platform: string, projectData: IProjectData): IPlatformData { - return this.platformsData[platform.toLowerCase()] && this.platformsData[platform.toLowerCase()].getPlatformData(projectData); + const platformKey = platform && _.first(platform.toLowerCase().split("@")); + let platformData: IPlatformData; + if (platformKey) { + platformData = this.platformsData[platformKey] && this.platformsData[platformKey].getPlatformData(projectData); + } + + return platformData; } public get availablePlatforms(): any { diff --git a/lib/providers/device-app-data-provider.ts b/lib/providers/device-app-data-provider.ts deleted file mode 100644 index 1ee89f013f..0000000000 --- a/lib/providers/device-app-data-provider.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as deviceAppDataBaseLib from "../common/mobile/device-app-data/device-app-data-base"; -import * as path from "path"; -import { AndroidDeviceHashService } from "../common/mobile/android/android-device-hash-service"; -import { DeviceAndroidDebugBridge } from "../common/mobile/android/device-android-debug-bridge"; - -const SYNC_DIR_NAME = "sync"; -const FULLSYNC_DIR_NAME = "fullsync"; - -export class IOSAppIdentifier extends deviceAppDataBaseLib.DeviceAppDataBase implements Mobile.IDeviceAppData { - private static DEVICE_PROJECT_ROOT_PATH = "Library/Application Support/LiveSync/app"; - private _deviceProjectRootPath: string = null; - - constructor(_appIdentifier: string, - public device: Mobile.IDevice, - public platform: string, - private $iOSSimResolver: Mobile.IiOSSimResolver) { - super(_appIdentifier); - } - - public async getDeviceProjectRootPath(): Promise { - if (!this._deviceProjectRootPath) { - if (this.device.isEmulator) { - let applicationPath = this.$iOSSimResolver.iOSSim.getApplicationPath(this.device.deviceInfo.identifier, this.appIdentifier); - this._deviceProjectRootPath = path.join(applicationPath, "app"); - } else { - this._deviceProjectRootPath = IOSAppIdentifier.DEVICE_PROJECT_ROOT_PATH; - } - } - - return this._getDeviceProjectRootPath(this._deviceProjectRootPath); - } - - public get deviceSyncZipPath(): string { - if (this.device.isEmulator) { - return undefined; - } else { - return "Library/Application Support/LiveSync/sync.zip"; - } - } - - public async isLiveSyncSupported(): Promise { - return true; - } -} - -export class AndroidAppIdentifier extends deviceAppDataBaseLib.DeviceAppDataBase implements Mobile.IDeviceAppData { - constructor(_appIdentifier: string, - public device: Mobile.IDevice, - public platform: string, - private $options: IOptions, - private $injector: IInjector) { - super(_appIdentifier); - } - - private _deviceProjectRootPath: string; - - public async getDeviceProjectRootPath(): Promise { - if (!this._deviceProjectRootPath) { - let syncFolderName = await this.getSyncFolderName(); - this._deviceProjectRootPath = `/data/local/tmp/${this.appIdentifier}/${syncFolderName}`; - } - - return this._deviceProjectRootPath; - } - - public async isLiveSyncSupported(): Promise { - return true; - } - - private async getSyncFolderName(): Promise { - let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - let deviceHashService: AndroidDeviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: adb, appIdentifier: this.appIdentifier }); - let hashFile = this.$options.force ? null : await deviceHashService.doesShasumFileExistsOnDevice(); - return this.$options.watch || hashFile ? SYNC_DIR_NAME : FULLSYNC_DIR_NAME; - } -} - -export class DeviceAppDataProvider implements Mobile.IDeviceAppDataProvider { - public createFactoryRules(): IDictionary { - return { - iOS: { - vanilla: IOSAppIdentifier - }, - Android: { - vanilla: AndroidAppIdentifier - } - }; - } -} -$injector.register("deviceAppDataProvider", DeviceAppDataProvider); diff --git a/lib/providers/livesync-provider.ts b/lib/providers/livesync-provider.ts deleted file mode 100644 index 88034a2323..0000000000 --- a/lib/providers/livesync-provider.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as path from "path"; -import * as temp from "temp"; - -export class LiveSyncProvider implements ILiveSyncProvider { - constructor(private $androidLiveSyncServiceLocator: { factory: Function }, - private $iosLiveSyncServiceLocator: { factory: Function }, - private $platformService: IPlatformService, - private $platformsData: IPlatformsData, - private $logger: ILogger, - private $childProcess: IChildProcess, - private $options: IOptions) { } - - private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml"]; - - private deviceSpecificLiveSyncServicesCache: IDictionary = {}; - public get deviceSpecificLiveSyncServices(): IDictionary { - return { - android: (_device: Mobile.IDevice, $injector: IInjector) => { - if (!this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]) { - this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier] = $injector.resolve(this.$androidLiveSyncServiceLocator.factory, { _device: _device }); - } - - return this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]; - }, - ios: (_device: Mobile.IDevice, $injector: IInjector) => { - if (!this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]) { - this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier] = $injector.resolve(this.$iosLiveSyncServiceLocator.factory, { _device: _device }); - } - - return this.deviceSpecificLiveSyncServicesCache[_device.deviceInfo.identifier]; - } - }; - } - - public async buildForDevice(device: Mobile.IDevice, projectData: IProjectData): Promise { - let buildConfig: IBuildConfig = { - buildForDevice: !device.isEmulator, - projectDir: this.$options.path, - release: this.$options.release, - teamId: this.$options.teamId, - device: this.$options.device, - provision: this.$options.provision, - }; - - await this.$platformService.buildPlatform(device.deviceInfo.platform, buildConfig, projectData); - let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); - if (device.isEmulator) { - return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; - } - - return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; - } - - public async preparePlatformForSync(platform: string, provision: any, projectData: IProjectData): Promise { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, { provision: provision, sdk: this.$options.sdk }); - } - - public canExecuteFastSync(filePath: string, projectData: IProjectData, platform: string): boolean { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let fastSyncFileExtensions = LiveSyncProvider.FAST_SYNC_FILE_EXTENSIONS.concat(platformData.fastLivesyncFileExtensions); - return _.includes(fastSyncFileExtensions, path.extname(filePath)); - } - - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { - if (deviceAppData.platform.toLowerCase() === "android" || !deviceAppData.deviceSyncZipPath || !isFullSync) { - await deviceAppData.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); - } else { - temp.track(); - let tempZip = temp.path({ prefix: "sync", suffix: ".zip" }); - this.$logger.trace("Creating zip file: " + tempZip); - - if (this.$options.syncAllFiles) { - await this.$childProcess.spawnFromEvent("zip", ["-r", "-0", tempZip, "app"], "close", { cwd: path.dirname(projectFilesPath) }); - } else { - this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); - await this.$childProcess.spawnFromEvent("zip", ["-r", "-0", tempZip, "app", "-x", "app/tns_modules/*"], "close", { cwd: path.dirname(projectFilesPath) }); - } - - deviceAppData.device.fileSystem.transferFiles(deviceAppData, [{ - getLocalPath: () => tempZip, - getDevicePath: () => deviceAppData.deviceSyncZipPath, - getRelativeToProjectBasePath: () => "../sync.zip", - deviceProjectRootPath: await deviceAppData.getDeviceProjectRootPath() - }]); - } - } -} -$injector.register("liveSyncProvider", LiveSyncProvider); diff --git a/lib/providers/project-files-provider.ts b/lib/providers/project-files-provider.ts index afe422d537..89917e1da0 100644 --- a/lib/providers/project-files-provider.ts +++ b/lib/providers/project-files-provider.ts @@ -12,25 +12,25 @@ export class ProjectFilesProvider extends ProjectFilesProviderBase { private static INTERNAL_NONPROJECT_FILES = [ "**/*.ts" ]; - public mapFilePath(filePath: string, platform: string, projectData: IProjectData): string { - let platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); - let parsedFilePath = this.getPreparedFilePath(filePath); + public mapFilePath(filePath: string, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): string { + const platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + const parsedFilePath = this.getPreparedFilePath(filePath, projectFilesConfig); let mappedFilePath = ""; if (parsedFilePath.indexOf(constants.NODE_MODULES_FOLDER_NAME) > -1) { - let relativePath = path.relative(path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME), parsedFilePath); + const relativePath = path.relative(path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME), parsedFilePath); mappedFilePath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, constants.TNS_MODULES_FOLDER_NAME, relativePath); } else { mappedFilePath = path.join(platformData.appDestinationDirectoryPath, path.relative(projectData.projectDir, parsedFilePath)); } - let appResourcesDirectoryPath = path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME); - let platformSpecificAppResourcesDirectoryPath = path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName); + const appResourcesDirectoryPath = path.join(constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME); + const platformSpecificAppResourcesDirectoryPath = path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName); if (parsedFilePath.indexOf(appResourcesDirectoryPath) > -1 && parsedFilePath.indexOf(platformSpecificAppResourcesDirectoryPath) === -1) { return null; } if (parsedFilePath.indexOf(platformSpecificAppResourcesDirectoryPath) > -1) { - let appResourcesRelativePath = path.relative(path.join(projectData.projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, + const appResourcesRelativePath = path.relative(path.join(projectData.projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, platformData.normalizedPlatformName), parsedFilePath); mappedFilePath = path.join(platformData.platformProjectService.getAppResourcesDestinationDirectoryPath(projectData), appResourcesRelativePath); } diff --git a/lib/services/analytics-service.ts b/lib/services/analytics-service.ts deleted file mode 100644 index e37ef56004..0000000000 --- a/lib/services/analytics-service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AnalyticsServiceBase } from "../common/services/analytics-service-base"; - -export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService { - private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; - - constructor(protected $logger: ILogger, - protected $options: IOptions, - $staticConfig: Config.IStaticConfig, - $prompter: IPrompter, - $userSettingsService: UserSettings.IUserSettingsService, - $analyticsSettingsService: IAnalyticsSettingsService, - $progressIndicator: IProgressIndicator, - $osInfo: IOsInfo) { - super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo); - } - - protected async checkConsentCore(trackFeatureUsage: boolean): Promise { - await this.restartEqatecMonitor(AnalyticsService.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY); - await super.checkConsentCore(trackFeatureUsage); - - // Stop the monitor, so correct API_KEY will be used when features are tracked. - this.tryStopEqatecMonitor(); - } -} - -$injector.register("analyticsService", AnalyticsService); diff --git a/lib/services/analytics-settings-service.ts b/lib/services/analytics-settings-service.ts index ef21d53a36..6ca50db27e 100644 --- a/lib/services/analytics-settings-service.ts +++ b/lib/services/analytics-settings-service.ts @@ -1,7 +1,6 @@ import { createGUID } from "../common/helpers"; class AnalyticsSettingsService implements IAnalyticsSettingsService { - private static SESSIONS_STARTED_OBSOLETE_KEY = "SESSIONS_STARTED"; private static SESSIONS_STARTED_KEY_PREFIX = "SESSIONS_STARTED_"; constructor(private $userSettingsService: UserSettings.IUserSettingsService, @@ -12,16 +11,12 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { return true; } - public async getUserId(): Promise { - let currentUserId = await this.$userSettingsService.getSettingValue("USER_ID"); - if (!currentUserId) { - currentUserId = createGUID(false); - - this.$logger.trace(`Setting new USER_ID: ${currentUserId}.`); - await this.$userSettingsService.saveSetting("USER_ID", currentUserId); - } + public getUserId(): Promise { + return this.getSettingValueOrDefault("USER_ID"); + } - return currentUserId; + public getClientId(): Promise { + return this.getSettingValueOrDefault(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME); } public getClientName(): string { @@ -33,14 +28,8 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { } public async getUserSessionsCount(projectName: string): Promise { - let oldSessionCount = await this.$userSettingsService.getSettingValue(AnalyticsSettingsService.SESSIONS_STARTED_OBSOLETE_KEY); - - if (oldSessionCount) { - // remove the old property for sessions count - await this.$userSettingsService.removeSetting(AnalyticsSettingsService.SESSIONS_STARTED_OBSOLETE_KEY); - } - - return await this.$userSettingsService.getSettingValue(this.getSessionsProjectKey(projectName)) || oldSessionCount || 0; + const sessionsCountForProject = await this.$userSettingsService.getSettingValue(this.getSessionsProjectKey(projectName)); + return sessionsCountForProject || 0; } public async setUserSessionsCount(count: number, projectName: string): Promise { @@ -50,5 +39,17 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { private getSessionsProjectKey(projectName: string): string { return `${AnalyticsSettingsService.SESSIONS_STARTED_KEY_PREFIX}${projectName}`; } + + private async getSettingValueOrDefault(settingName: string): Promise { + let guid = await this.$userSettingsService.getSettingValue(settingName); + if (!guid) { + guid = createGUID(false); + + this.$logger.trace(`Setting new ${settingName}: ${guid}.`); + await this.$userSettingsService.saveSetting(settingName, guid); + } + + return guid; + } } $injector.register("analyticsSettingsService", AnalyticsSettingsService); diff --git a/lib/services/analytics/analytics-broker-process.ts b/lib/services/analytics/analytics-broker-process.ts new file mode 100644 index 0000000000..02b806aa75 --- /dev/null +++ b/lib/services/analytics/analytics-broker-process.ts @@ -0,0 +1,62 @@ +// NOTE: This file is used to track data in a separate process. +// The instances here are not shared with the ones in main CLI process. +import * as fs from "fs"; +import { AnalyticsBroker } from "./analytics-broker"; + +const pathToBootstrap = process.argv[2]; +if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { + throw new Error("Invalid path to bootstrap."); +} + +// After requiring the bootstrap we can use $injector +require(pathToBootstrap); + +const analyticsBroker = $injector.resolve(AnalyticsBroker, { pathToBootstrap }); +let trackingQueue: Promise = Promise.resolve(); + +let sentFinishMsg = false; +let receivedFinishMsg = false; + +const sendDataForTracking = async (data: ITrackingInformation) => { + trackingQueue = trackingQueue.then(() => analyticsBroker.sendDataForTracking(data)); + await trackingQueue; +}; + +const finishTracking = async (data?: ITrackingInformation) => { + if (!sentFinishMsg) { + sentFinishMsg = true; + + data = data || { type: TrackingTypes.Finish }; + const action = async () => { + await sendDataForTracking(data); + process.disconnect(); + }; + + if (receivedFinishMsg) { + await action(); + } else { + // In case we've got here without receiving "finish" message from parent (receivedFinishMsg is false) + // there might be various reasons, but most probably the parent is dead. + // However, there's no guarantee that we've received all messages. So wait some time before sending finish message to children. + setTimeout(async () => { + await action(); + }, 1000); + } + } +}; + +process.on("message", async (data: ITrackingInformation) => { + if (data.type === TrackingTypes.Finish) { + receivedFinishMsg = true; + await finishTracking(data); + return; + } + + await sendDataForTracking(data); +}); + +process.on("disconnect", async () => { + await finishTracking(); +}); + +process.send(AnalyticsMessages.BrokerReadyToReceive); diff --git a/lib/services/analytics/analytics-broker.ts b/lib/services/analytics/analytics-broker.ts new file mode 100644 index 0000000000..25d4bf1c3f --- /dev/null +++ b/lib/services/analytics/analytics-broker.ts @@ -0,0 +1,48 @@ +import { cache } from "../../common/decorators"; + +export class AnalyticsBroker implements IAnalyticsBroker { + + @cache() + private async getEqatecAnalyticsProvider(): Promise { + return this.$injector.resolve("eqatecAnalyticsProvider"); + } + + @cache() + private async getGoogleAnalyticsProvider(): Promise { + const clientId = await this.$analyticsSettingsService.getClientId(); + return this.$injector.resolve("googleAnalyticsProvider", { clientId }); + } + + constructor(private $analyticsSettingsService: IAnalyticsSettingsService, + private $injector: IInjector) { } + + public async sendDataForTracking(trackInfo: ITrackingInformation): Promise { + try { + const eqatecProvider = await this.getEqatecAnalyticsProvider(); + const googleProvider = await this.getGoogleAnalyticsProvider(); + + switch (trackInfo.type) { + case TrackingTypes.Exception: + await eqatecProvider.trackError(trackInfo); + break; + case TrackingTypes.Feature: + await eqatecProvider.trackInformation(trackInfo); + break; + case TrackingTypes.AcceptTrackFeatureUsage: + await eqatecProvider.acceptFeatureUsageTracking(trackInfo); + break; + case TrackingTypes.GoogleAnalyticsData: + await googleProvider.trackHit(trackInfo); + break; + case TrackingTypes.Finish: + await eqatecProvider.finishTracking(); + break; + default: + throw new Error(`Invalid tracking type: ${trackInfo.type}`); + } + } catch (err) { + // So, lets ignore the error for now until we find out what to do with it. + } + } + +} diff --git a/lib/services/analytics/analytics-constants.ts b/lib/services/analytics/analytics-constants.ts new file mode 100644 index 0000000000..5aff79435c --- /dev/null +++ b/lib/services/analytics/analytics-constants.ts @@ -0,0 +1,14 @@ +/** + * Defines messages used in communication between CLI's process and analytics subprocesses. + */ +const enum AnalyticsMessages { + /** + * Analytics Broker is initialized and is ready to receive information for tracking. + */ + BrokerReadyToReceive = "BrokerReadyToReceive", + + /** + * Eqatec Analytics process is initialized and is ready to receive information for tracking. + */ + EqatecAnalyticsReadyToReceive = "EqatecAnalyticsReadyToReceive" +} diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts new file mode 100644 index 0000000000..a999e1a96d --- /dev/null +++ b/lib/services/analytics/analytics-service.ts @@ -0,0 +1,198 @@ +import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; +import { ChildProcess } from "child_process"; +import * as path from "path"; +import { cache } from "../../common/decorators"; +import { isInteractive } from '../../common/helpers'; +import { DeviceTypes, AnalyticsClients } from "../../common/constants"; + +export class AnalyticsService extends AnalyticsServiceBase { + private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000; + private brokerProcess: ChildProcess; + + constructor(protected $logger: ILogger, + protected $options: IOptions, + $staticConfig: Config.IStaticConfig, + $prompter: IPrompter, + $userSettingsService: UserSettings.IUserSettingsService, + $analyticsSettingsService: IAnalyticsSettingsService, + $osInfo: IOsInfo, + private $childProcess: IChildProcess, + private $processService: IProcessService, + private $projectDataService: IProjectDataService, + private $mobileHelper: Mobile.IMobileHelper) { + super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo); + } + + public track(featureName: string, featureValue: string): Promise { + const data: IFeatureTrackingInformation = { + type: TrackingTypes.Feature, + featureName: featureName, + featureValue: featureValue + }; + + return this.sendInfoForTracking(data, this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); + } + + public trackException(exception: any, message: string): Promise { + const data: IExceptionsTrackingInformation = { + type: TrackingTypes.Exception, + exception, + message + }; + + return this.sendInfoForTracking(data, this.$staticConfig.ERROR_REPORT_SETTING_NAME); + } + + public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise { + this.sendMessageToBroker({ + type: TrackingTypes.AcceptTrackFeatureUsage, + acceptTrackFeatureUsage: settings.acceptTrackFeatureUsage + }); + } + + public async trackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise { + await this.initAnalyticsStatuses(); + + if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) { + gaSettings.customDimensions = gaSettings.customDimensions || {}; + gaSettings.customDimensions[GoogleAnalyticsCustomDimensions.client] = this.$options.analyticsClient || (isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.Unknown); + + const googleAnalyticsData: IGoogleAnalyticsTrackingInformation = _.merge({ type: TrackingTypes.GoogleAnalyticsData, category: AnalyticsClients.Cli }, gaSettings); + return this.sendMessageToBroker(googleAnalyticsData); + } + } + + public async trackEventActionInGoogleAnalytics(data: IEventActionData): Promise { + const device = data.device; + const platform = device ? device.deviceInfo.platform : data.platform; + const normalizedPlatform = platform ? this.$mobileHelper.normalizePlatformName(platform) : platform; + const isForDevice = device ? !device.isEmulator : data.isForDevice; + + let label: string = ""; + label = this.addDataToLabel(label, normalizedPlatform); + + // In some cases (like in case action is Build and platform is Android), we do not know if the deviceType is emulator or device. + // Just exclude the device_type in this case. + if (isForDevice !== null) { + const deviceType = isForDevice ? DeviceTypes.Device : (this.$mobileHelper.isAndroidPlatform(platform) ? DeviceTypes.Emulator : DeviceTypes.Simulator); + label = this.addDataToLabel(label, deviceType); + } + + if (device) { + label = this.addDataToLabel(label, device.deviceInfo.version); + } + + if (data.additionalData) { + label = this.addDataToLabel(label, data.additionalData); + } + + const customDimensions: IStringDictionary = {}; + if (data.projectDir) { + const projectData = this.$projectDataService.getProjectData(data.projectDir); + customDimensions[GoogleAnalyticsCustomDimensions.projectType] = projectData.projectType; + } + + const googleAnalyticsEventData: IGoogleAnalyticsEventData = { + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: data.action, + label, + customDimensions + }; + + this.$logger.trace("Will send the following information to Google Analytics:", googleAnalyticsEventData); + + await this.trackInGoogleAnalytics(googleAnalyticsEventData); + } + + public dispose(): void { + if (this.brokerProcess && this.shouldDisposeInstance) { + this.brokerProcess.disconnect(); + } + } + + private addDataToLabel(label: string, newData: string): string { + if (newData && label) { + return `${label}_${newData}`; + } + + return label || newData || ""; + } + + @cache() + private getAnalyticsBroker(): Promise { + return new Promise((resolve, reject) => { + const broker = this.$childProcess.spawn("node", + [ + path.join(__dirname, "analytics-broker-process.js"), + this.$staticConfig.PATH_TO_BOOTSTRAP + ], + { + stdio: ["ignore", "ignore", "ignore", "ipc"], + detached: true + } + ); + + broker.unref(); + + let isSettled = false; + + const timeoutId = setTimeout(() => { + if (!isSettled) { + reject(new Error("Unable to start Analytics Broker process.")); + } + }, AnalyticsService.ANALYTICS_BROKER_START_TIMEOUT); + + broker.on("error", (err: Error) => { + clearTimeout(timeoutId); + + if (!isSettled) { + isSettled = true; + reject(err); + } + }); + + broker.on("message", (data: any) => { + if (data === AnalyticsMessages.BrokerReadyToReceive) { + clearTimeout(timeoutId); + + if (!isSettled) { + isSettled = true; + + this.$processService.attachToProcessExitSignals(this, () => { + broker.send({ + type: TrackingTypes.Finish + }); + }); + + this.brokerProcess = broker; + + resolve(broker); + } + } + }); + }); + } + + private async sendInfoForTracking(trackingInfo: ITrackingInformation, settingName: string): Promise { + await this.initAnalyticsStatuses(); + + if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[settingName] === AnalyticsStatus.enabled) { + return this.sendMessageToBroker(trackingInfo); + } + } + + private async sendMessageToBroker(message: ITrackingInformation): Promise { + const broker = await this.getAnalyticsBroker(); + return new Promise((resolve, reject) => { + if (broker && broker.connected) { + try { + broker.send(message, resolve); + } catch (err) { + this.$logger.trace("Error while trying to send message to broker:", err); + } + } + }); + } +} + +$injector.register("analyticsService", AnalyticsService); diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts new file mode 100644 index 0000000000..ecb51c8b1f --- /dev/null +++ b/lib/services/analytics/analytics.d.ts @@ -0,0 +1,97 @@ +/** + * Describes if the user allows to be tracked. + */ +interface IAcceptUsageReportingInformation extends ITrackingInformation { + /** + * The answer of the question if user allows us to track them. + */ + acceptTrackFeatureUsage: boolean; +} + +/** + * Describes information used for tracking feature. + */ +interface IFeatureTrackingInformation extends ITrackingInformation { + /** + * The name of the feature that should be tracked. + */ + featureName: string; + + /** + * Value of the feature that should be tracked. + */ + featureValue: string; +} + +/** + * Describes information for exception that should be tracked. + */ +interface IExceptionsTrackingInformation extends ITrackingInformation { + /** + * The exception that should be tracked. + */ + exception: Error; + + /** + * The message of the error that should be tracked. + */ + message: string; +} + +/** + * Describes the broker used to pass information to all analytics providers. + */ +interface IAnalyticsBroker { + /** + * Sends the specified tracking information to all providers. + * @param {ITrackingInformation} trackInfo The information that should be passed to all providers. + * @returns {Promise} + */ + sendDataForTracking(trackInfo: ITrackingInformation): Promise; +} + +/** + * Describes analytics provider used for tracking in a specific Analytics Service. + */ +interface IAnalyticsProvider { + /** + * Sends exception for tracking in the analytics service provider. + * @param {IExceptionsTrackingInformation} trackInfo The information for exception that should be tracked. + * @returns {Promise} + */ + trackError(trackInfo: IExceptionsTrackingInformation): Promise; + + /** + * Sends feature for tracking in the analytics service provider. + * @param {IFeatureTrackingInformation} trackInfo The information for feature that should be tracked. + * @returns {Promise} + */ + trackInformation(trackInfo: IFeatureTrackingInformation): Promise; + + /** + * Sends information if user accepts to be tracked. + * @param {IAcceptUsageReportingInformation} trackInfo The information, containing user's answer if they allow to be tracked. + * @returns {Promise} + */ + acceptFeatureUsageTracking(data: IAcceptUsageReportingInformation): Promise; + + /** + * Waits for execution of all pending requests and finishes tracking operation + * @returns {Promise} + */ + finishTracking(): Promise; +} + +interface IGoogleAnalyticsTrackingInformation extends IGoogleAnalyticsData, ITrackingInformation { } + +/** + * Describes methods required to track in Google Analytics. + */ +interface IGoogleAnalyticsProvider { + /** + * Tracks hit types. + * @param {IGoogleAnalyticsData} data Data that has to be tracked. + * @returns {Promise} + */ + trackHit(data: IGoogleAnalyticsData): Promise; +} diff --git a/lib/services/analytics/eqatec-analytics-provider.ts b/lib/services/analytics/eqatec-analytics-provider.ts new file mode 100644 index 0000000000..e2b2f43163 --- /dev/null +++ b/lib/services/analytics/eqatec-analytics-provider.ts @@ -0,0 +1,60 @@ +import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; + +export class EqatecAnalyticsProvider extends AnalyticsServiceBase implements IAnalyticsProvider { + private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; + private static NEW_PROJECT_ANALYTICS_API_KEY = "b40f24fcb4f94bccaf64e4dc6337422e"; + + protected featureTrackingAPIKeys: string[] = [ + this.$staticConfig.ANALYTICS_API_KEY, + EqatecAnalyticsProvider.NEW_PROJECT_ANALYTICS_API_KEY + ]; + + protected acceptUsageReportingAPIKeys: string[] = [ + EqatecAnalyticsProvider.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY + ]; + + constructor(protected $logger: ILogger, + protected $options: IOptions, + $staticConfig: Config.IStaticConfig, + $prompter: IPrompter, + $userSettingsService: UserSettings.IUserSettingsService, + $analyticsSettingsService: IAnalyticsSettingsService, + $osInfo: IOsInfo) { + super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo); + } + + public async trackInformation(data: IFeatureTrackingInformation): Promise { + try { + await this.trackFeatureCore(`${data.featureName}.${data.featureValue}`); + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); + } + } + + public async trackError(data: IExceptionsTrackingInformation): Promise { + try { + await this.trackException(data.exception, data.message); + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); + } + } + + public async acceptFeatureUsageTracking(data: IAcceptUsageReportingInformation): Promise { + try { + await this.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: data.acceptTrackFeatureUsage }); + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); + } + } + + public async finishTracking(): Promise { + this.tryStopEqatecMonitors(); + } + + public dispose(): void { + // Intentionally left blank. + } + +} + +$injector.register("eqatecAnalyticsProvider", EqatecAnalyticsProvider); diff --git a/lib/services/analytics/google-analytics-custom-dimensions.ts b/lib/services/analytics/google-analytics-custom-dimensions.ts new file mode 100644 index 0000000000..9e7c1d7007 --- /dev/null +++ b/lib/services/analytics/google-analytics-custom-dimensions.ts @@ -0,0 +1,8 @@ +const enum GoogleAnalyticsCustomDimensions { + cliVersion = "cd1", + projectType = "cd2", + clientID = "cd3", + sessionID = "cd4", + client = "cd5", + nodeVersion = "cd6" +} diff --git a/lib/services/analytics/google-analytics-provider.ts b/lib/services/analytics/google-analytics-provider.ts new file mode 100644 index 0000000000..1bb8d9fe13 --- /dev/null +++ b/lib/services/analytics/google-analytics-provider.ts @@ -0,0 +1,120 @@ +import * as uuid from "uuid"; +import * as ua from "universal-analytics"; +import { AnalyticsClients } from "../../common/constants"; + +export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { + private static GA_TRACKING_ID = "UA-111455-44"; + private currentPage: string; + + constructor(private clientId: string, + private $staticConfig: IStaticConfig, + private $hostInfo: IHostInfo, + private $osInfo: IOsInfo) { + } + + public async trackHit(trackInfo: IGoogleAnalyticsData): Promise { + const visitor = ua({ + tid: GoogleAnalyticsProvider.GA_TRACKING_ID, + cid: this.clientId, + headers: { + ["User-Agent"]: this.getUserAgentString() + } + }); + + this.setCustomDimensions(visitor, trackInfo.customDimensions); + + switch (trackInfo.googleAnalyticsDataType) { + case GoogleAnalyticsDataType.Page: + await this.trackPageView(visitor, trackInfo); + break; + case GoogleAnalyticsDataType.Event: + await this.trackEvent(visitor, trackInfo); + break; + } + } + + private setCustomDimensions(visitor: ua.Visitor, customDimensions: IStringDictionary): void { + const defaultValues: IStringDictionary = { + [GoogleAnalyticsCustomDimensions.cliVersion]: this.$staticConfig.version, + [GoogleAnalyticsCustomDimensions.nodeVersion]: process.version, + [GoogleAnalyticsCustomDimensions.clientID]: this.clientId, + [GoogleAnalyticsCustomDimensions.projectType]: null, + [GoogleAnalyticsCustomDimensions.sessionID]: uuid.v4(), + [GoogleAnalyticsCustomDimensions.client]: AnalyticsClients.Unknown + }; + + customDimensions = _.merge(defaultValues, customDimensions); + + _.each(customDimensions, (value, key) => { + visitor.set(key, value); + }); + } + + private trackEvent(visitor: ua.Visitor, trackInfo: IGoogleAnalyticsEventData): Promise { + return new Promise((resolve, reject) => { + visitor.event(trackInfo.category, trackInfo.action, trackInfo.label, trackInfo.value, { p: this.currentPage }, (err: Error) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + private trackPageView(visitor: ua.Visitor, trackInfo: IGoogleAnalyticsPageviewData): Promise { + return new Promise((resolve, reject) => { + this.currentPage = trackInfo.path; + + const pageViewData: ua.PageviewParams = { + dp: trackInfo.path, + dt: trackInfo.title + }; + + visitor.pageview(pageViewData, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + private getUserAgentString(): string { + let osString = ""; + const osRelease = this.$osInfo.release(); + + if (this.$hostInfo.isWindows) { + osString = `Windows NT ${osRelease}`; + } else if (this.$hostInfo.isDarwin) { + osString = `Macintosh`; + const macRelease = this.getMacOSReleaseVersion(osRelease); + if (macRelease) { + osString += `; Intel Mac OS X ${macRelease}`; + } + } else { + osString = `Linux x86`; + if (this.$osInfo.arch() === "x64") { + osString += "_64"; + } + } + + const userAgent = `tnsCli/${this.$staticConfig.version} (${osString}; ${this.$osInfo.arch()})`; + + return userAgent; + } + + private getMacOSReleaseVersion(osRelease: string): string { + // https://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history + // Each macOS version is labeled 10., where it looks like is taken from the major version returned by os.release() (16.x.x for example) and subtracting 4 from it. + // So the version becomes "10.12" in this case. + // Could be improved by spawning `system_profiler SPSoftwareDataType` and getting the System Version line from the result. + const majorVersion = osRelease && _.first(osRelease.split(".")); + return majorVersion && `10.${+majorVersion - 4}`; + } +} + +$injector.register("googleAnalyticsProvider", GoogleAnalyticsProvider); diff --git a/lib/services/android-debug-service.ts b/lib/services/android-debug-service.ts index 54c59b2f1b..a161179e93 100644 --- a/lib/services/android-debug-service.ts +++ b/lib/services/android-debug-service.ts @@ -1,34 +1,24 @@ import { sleep } from "../common/helpers"; -import { ChildProcess } from "child_process"; import { DebugServiceBase } from "./debug-service-base"; -class AndroidDebugService extends DebugServiceBase implements IPlatformDebugService { - private _device: Mobile.IAndroidDevice = null; - private _debuggerClientProcess: ChildProcess; - +export class AndroidDebugService extends DebugServiceBase implements IPlatformDebugService { + private _packageName: string; public get platform() { return "android"; } - private get device(): Mobile.IAndroidDevice { - return this._device; - } - - private set device(newDevice) { - this._device = newDevice; - } - - constructor(private $devicesService: Mobile.IDevicesService, + constructor(protected device: Mobile.IAndroidDevice, + protected $devicesService: Mobile.IDevicesService, private $errors: IErrors, private $logger: ILogger, - private $config: IConfiguration, private $androidDeviceDiscovery: Mobile.IDeviceDiscovery, private $androidProcessService: Mobile.IAndroidProcessService, private $net: INet) { - super(); + super(device, $devicesService); } - public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + this._packageName = debugData.applicationIdentifier; return debugOptions.emulator ? this.debugOnEmulator(debugData, debugOptions) : this.debugOnDevice(debugData, debugOptions); @@ -36,20 +26,24 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ public async debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); - let action = (device: Mobile.IAndroidDevice): Promise => { - this.device = device; - return this.debugStartCore(debugData.applicationIdentifier, debugOptions); - }; + const action = (device: Mobile.IAndroidDevice): Promise => this.debugStartCore(debugData.applicationIdentifier, debugOptions); await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); } - public async debugStop(): Promise { - this.stopDebuggerClient(); - return; + public debugStop(): Promise { + return this.removePortForwarding(); } - private async debugOnEmulator(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + protected getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string { + const debugOpts = _.cloneDeep(debugOptions); + debugOpts.useBundledDevTools = debugOpts.useBundledDevTools === undefined ? true : debugOpts.useBundledDevTools; + + const chromeDebugUrl = super.getChromeDebugUrl(debugOpts, port); + return chromeDebugUrl; + } + + private async debugOnEmulator(debugData: IDebugData, debugOptions: IDebugOptions): Promise { // Assure we've detected the emulator as device // For example in case deployOnEmulator had stated new emulator instance // we need some time to detect it. Let's force detection. @@ -57,15 +51,20 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ return this.debugOnDevice(debugData, debugOptions); } + private async removePortForwarding(packageName?: string): Promise { + const port = await this.getForwardedLocalDebugPortForPackageName(this.device.deviceInfo.identifier, packageName || this._packageName); + return this.device.adb.executeCommand(["forward", "--remove", `tcp:${port}`]); + } + private async getForwardedLocalDebugPortForPackageName(deviceId: string, packageName: string): Promise { let port = -1; - let forwardsResult = await this.device.adb.executeCommand(["forward", "--list"]); + const forwardsResult = await this.device.adb.executeCommand(["forward", "--list"]); - let unixSocketName = `${packageName}-inspectorServer`; + const unixSocketName = `${packageName}-inspectorServer`; //matches 123a188909e6czzc tcp:40001 localabstract:org.nativescript.testUnixSockets-debug - let regexp = new RegExp(`(?:${deviceId} tcp:)([\\d]+)(?= localabstract:${unixSocketName})`, "g"); - let match = regexp.exec(forwardsResult); + const regexp = new RegExp(`(?:${deviceId} tcp:)([\\d]+)(?= localabstract:${unixSocketName})`, "g"); + const match = regexp.exec(forwardsResult); if (match) { port = parseInt(match[1]); @@ -82,7 +81,7 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ return this.device.adb.executeCommand(["forward", `tcp:${local}`, `localabstract:${remote}`]); } - private async debugOnDevice(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + private async debugOnDevice(debugData: IDebugData, debugOptions: IDebugOptions): Promise { let packageFile = ""; if (!debugOptions.start && !debugOptions.emulator) { @@ -92,31 +91,28 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); - let action = (device: Mobile.IAndroidDevice): Promise => this.debugCore(device, packageFile, debugData.applicationIdentifier, debugOptions); + const action = (device: Mobile.IAndroidDevice): Promise => this.debugCore(device, packageFile, debugData.applicationIdentifier, debugOptions); - const result = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); - - return _.map(result, r => r.result); + const deviceActionResult = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return deviceActionResult[0].result; } private async debugCore(device: Mobile.IAndroidDevice, packageFile: string, packageName: string, debugOptions: IDebugOptions): Promise { - this.device = device; - await this.printDebugPort(device.deviceInfo.identifier, packageName); if (debugOptions.start) { return await this.attachDebugger(device.deviceInfo.identifier, packageName, debugOptions); } else if (debugOptions.stop) { - await this.detachDebugger(packageName); + await this.removePortForwarding(); return null; } else { - await this.startAppWithDebugger(packageFile, packageName, debugOptions); + await this.debugStartCore(packageName, debugOptions); return await this.attachDebugger(device.deviceInfo.identifier, packageName, debugOptions); } } private async printDebugPort(deviceId: string, packageName: string): Promise { - let port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); + const port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); this.$logger.info("device: " + deviceId + " debug port: " + port + "\n"); } @@ -125,23 +121,9 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ this.$errors.failWithoutHelp(`The application ${packageName} does not appear to be running on ${deviceId} or is not built with debugging enabled.`); } - let startDebuggerCommand = ["am", "broadcast", "-a", `\"${packageName}-debug\"`, "--ez", "enable", "true"]; - await this.device.adb.executeShellCommand(startDebuggerCommand); - - let port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); - return `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${port}`; - } - - private detachDebugger(packageName: string): Promise { - return this.device.adb.executeShellCommand(["am", "broadcast", "-a", `${packageName}-debug`, "--ez", "enable", "false"]); - } + const port = await this.getForwardedLocalDebugPortForPackageName(deviceId, packageName); - private async startAppWithDebugger(packageFile: string, packageName: string, debugOptions: IDebugOptions): Promise { - if (!debugOptions.emulator && !this.$config.debugLivesync) { - await this.device.applicationManager.uninstallApplication(packageName); - await this.device.applicationManager.installApplication(packageFile); - } - await this.debugStartCore(packageName, debugOptions); + return this.getChromeDebugUrl(debugOptions, port); } private async debugStartCore(packageName: string, debugOptions: IDebugOptions): Promise { @@ -163,11 +145,11 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ } private async waitForDebugger(packageName: String): Promise { - let waitText: string = `0 /data/local/tmp/${packageName}-debugger-started`; + const waitText: string = `0 /data/local/tmp/${packageName}-debugger-started`; let maxWait = 12; let debuggerStarted: boolean = false; while (maxWait > 0 && !debuggerStarted) { - let forwardsResult = await this.device.adb.executeShellCommand(["ls", "-s", `/data/local/tmp/${packageName}-debugger-started`]); + const forwardsResult = await this.device.adb.executeShellCommand(["ls", "-s", `/data/local/tmp/${packageName}-debugger-started`]); maxWait--; @@ -190,13 +172,6 @@ class AndroidDebugService extends DebugServiceBase implements IPlatformDebugServ return !!_.find(debuggableApps, a => a.appIdentifier === appIdentifier); } - - private stopDebuggerClient(): void { - if (this._debuggerClientProcess) { - this._debuggerClientProcess.kill(); - this._debuggerClientProcess = null; - } - } } -$injector.register("androidDebugService", AndroidDebugService); +$injector.register("androidDebugService", AndroidDebugService, false); diff --git a/lib/services/android-project-properties-manager.ts b/lib/services/android-project-properties-manager.ts index 2d1b250779..71a1338b61 100644 --- a/lib/services/android-project-properties-manager.ts +++ b/lib/services/android-project-properties-manager.ts @@ -14,8 +14,8 @@ export class AndroidProjectPropertiesManager implements IAndroidProjectPropertie public async getProjectReferences(): Promise { if (!this.projectReferences || this.dirty) { - let allProjectProperties = await this.getAllProjectProperties(); - let allProjectPropertiesKeys = _.keys(allProjectProperties); + const allProjectProperties = await this.getAllProjectProperties(); + const allProjectPropertiesKeys = _.keys(allProjectProperties); this.projectReferences = _(allProjectPropertiesKeys) .filter(key => _.startsWith(key, "android.library.reference.")) .map(key => this.createLibraryReference(key, allProjectProperties[key])) @@ -26,16 +26,16 @@ export class AndroidProjectPropertiesManager implements IAndroidProjectPropertie } public async addProjectReference(referencePath: string): Promise { - let references = await this.getProjectReferences(); - let libRefExists = _.some(references, r => path.normalize(r.path) === path.normalize(referencePath)); + const references = await this.getProjectReferences(); + const libRefExists = _.some(references, r => path.normalize(r.path) === path.normalize(referencePath)); if (!libRefExists) { await this.addToPropertyList("android.library.reference", referencePath); } } public async removeProjectReference(referencePath: string): Promise { - let references = await this.getProjectReferences(); - let libRefExists = _.some(references, r => path.normalize(r.path) === path.normalize(referencePath)); + const references = await this.getProjectReferences(); + const libRefExists = _.some(references, r => path.normalize(r.path) === path.normalize(referencePath)); if (libRefExists) { await this.removeFromPropertyList("android.library.reference", referencePath); } else { @@ -65,7 +65,7 @@ export class AndroidProjectPropertiesManager implements IAndroidProjectPropertie } private async addToPropertyList(key: string, value: string): Promise { - let editor = await this.createEditor(); + const editor = await this.createEditor(); let i = 1; while (editor.get(this.buildKeyName(key, i))) { i++; @@ -77,8 +77,8 @@ export class AndroidProjectPropertiesManager implements IAndroidProjectPropertie } private async removeFromPropertyList(key: string, value: string): Promise { - let editor = await this.createEditor(); - let valueLowerCase = value.toLowerCase(); + const editor = await this.createEditor(); + const valueLowerCase = value.toLowerCase(); let i = 1; let currentValue: any; while (currentValue = editor.get(this.buildKeyName(key, i))) { diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 1401628ae0..f8ef8c412a 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -35,7 +35,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $injector: IInjector, private $pluginVariablesService: IPluginVariablesService, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $config: IConfiguration, private $npm: INodePackageManager) { super($fs, $projectDataService); this._androidProjectPropertiesManagers = Object.create(null); @@ -50,8 +49,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject if (projectData && projectData.platformsDir && this._platformsDirCache !== projectData.platformsDir) { this._platformsDirCache = projectData.platformsDir; - let projectRoot = path.join(projectData.platformsDir, "android"); - let packageName = this.getProjectNameFromId(projectData); + const projectRoot = path.join(projectData.platformsDir, "android"); + const packageName = this.getProjectNameFromId(projectData); this._platformData = { frameworkPackageName: "tns-android", normalizedPlatformName: "Android", @@ -65,7 +64,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return [ `${packageName}-${buildMode}.apk`, - `${projectData.projectName}-${buildMode}.apk` + `${projectData.projectName}-${buildMode}.apk`, + `${projectData.projectName}.apk` ]; }, frameworkFilesExtensions: [".jar", ".dat", ".so"], @@ -97,31 +97,32 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.$androidToolsInfo.validateAndroidHomeEnvVariable({ showWarningsAsErrors: true }); - let javaCompilerVersion = await this.$sysInfo.getJavaCompilerVersion(); + const javaCompilerVersion = await this.$sysInfo.getJavaCompilerVersion(); await this.$androidToolsInfo.validateJavacVersion(javaCompilerVersion, { showWarningsAsErrors: true }); + + await this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true }); } public async validatePlugins(): Promise { Promise.resolve(); } - public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, pathToTemplate?: string): Promise { + public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise { if (semver.lt(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE)) { this.$errors.failWithoutHelp(`The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.`); } this.$fs.ensureDirectoryExists(this.getPlatformData(projectData).projectRoot); - this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true }); - let androidToolsInfo = this.$androidToolsInfo.getToolsInfo(); - let targetSdkVersion = androidToolsInfo.targetSdkVersion; + const androidToolsInfo = this.$androidToolsInfo.getToolsInfo(); + const targetSdkVersion = androidToolsInfo && androidToolsInfo.targetSdkVersion; this.$logger.trace(`Using Android SDK '${targetSdkVersion}'.`); this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "libs", "-R"); - if (pathToTemplate) { - let mainPath = path.join(this.getPlatformData(projectData).projectRoot, "src", "main"); + if (config.pathToTemplate) { + const mainPath = path.join(this.getPlatformData(projectData).projectRoot, "src", "main"); this.$fs.createDirectory(mainPath); - shell.cp("-R", path.join(path.resolve(pathToTemplate), "*"), mainPath); + shell.cp("-R", path.join(path.resolve(config.pathToTemplate), "*"), mainPath); } else { this.copy(this.getPlatformData(projectData).projectRoot, frameworkDir, "src", "-R"); } @@ -138,27 +139,30 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.cleanResValues(targetSdkVersion, projectData, frameworkVersion); - let npmConfig = { - "save": true, + const npmConfig: INodePackageManagerInstallOptions = { + save: true, "save-dev": true, "save-exact": true, - "silent": true + silent: true, + disableNpmInstall: false, + frameworkPath: config.frameworkPath, + ignoreScripts: config.ignoreScripts }; - let projectPackageJson: any = this.$fs.readJson(projectData.projectFilePath); + const projectPackageJson: any = this.$fs.readJson(projectData.projectFilePath); - for (let dependency of AndroidProjectService.REQUIRED_DEV_DEPENDENCIES) { + for (const dependency of AndroidProjectService.REQUIRED_DEV_DEPENDENCIES) { let dependencyVersionInProject = (projectPackageJson.dependencies && projectPackageJson.dependencies[dependency.name]) || (projectPackageJson.devDependencies && projectPackageJson.devDependencies[dependency.name]); if (!dependencyVersionInProject) { await this.$npm.install(`${dependency.name}@${dependency.version}`, projectData.projectDir, npmConfig); } else { - let cleanedVerson = semver.clean(dependencyVersionInProject); + const cleanedVerson = semver.clean(dependencyVersionInProject); // The plugin version is not valid. Check node_modules for the valid version. if (!cleanedVerson) { - let pathToPluginPackageJson = path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME, dependency.name, constants.PACKAGE_JSON_FILE_NAME); + const pathToPluginPackageJson = path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME, dependency.name, constants.PACKAGE_JSON_FILE_NAME); dependencyVersionInProject = this.$fs.exists(pathToPluginPackageJson) && this.$fs.readJson(pathToPluginPackageJson).version; } @@ -166,13 +170,13 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.$errors.failWithoutHelp(`Your project have installed ${dependency.name} version ${cleanedVerson} but Android platform requires version ${dependency.version}.`); } } - }; + } } private cleanResValues(targetSdkVersion: number, projectData: IProjectData, frameworkVersion: string): void { - let resDestinationDir = this.getAppResourcesDestinationDirectoryPath(projectData, frameworkVersion); - let directoriesInResFolder = this.$fs.readDirectory(resDestinationDir); - let directoriesToClean = directoriesInResFolder + const resDestinationDir = this.getAppResourcesDestinationDirectoryPath(projectData, frameworkVersion); + const directoriesInResFolder = this.$fs.readDirectory(resDestinationDir); + const directoriesToClean = directoriesInResFolder .map(dir => { return { dirName: dir, @@ -195,15 +199,15 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject // Interpolate the apilevel and package this.interpolateConfigurationFile(projectData, platformSpecificData); - let stringsFilePath = path.join(this.getAppResourcesDestinationDirectoryPath(projectData), 'values', 'strings.xml'); + const stringsFilePath = path.join(this.getAppResourcesDestinationDirectoryPath(projectData), 'values', 'strings.xml'); shell.sed('-i', /__NAME__/, projectData.projectName, stringsFilePath); shell.sed('-i', /__TITLE_ACTIVITY__/, projectData.projectName, stringsFilePath); - let gradleSettingsFilePath = path.join(this.getPlatformData(projectData).projectRoot, "settings.gradle"); + const gradleSettingsFilePath = path.join(this.getPlatformData(projectData).projectRoot, "settings.gradle"); shell.sed('-i', /__PROJECT_NAME__/, this.getProjectNameFromId(projectData), gradleSettingsFilePath); // will replace applicationId in app/App_Resources/Android/app.gradle if it has not been edited by the user - let userAppGradleFilePath = path.join(projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, "app.gradle"); + const userAppGradleFilePath = path.join(projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, "app.gradle"); try { shell.sed('-i', /__PACKAGE__/, projectData.projectId, userAppGradleFilePath); @@ -213,16 +217,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } public interpolateConfigurationFile(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): void { - let manifestPath = this.getPlatformData(projectData).configurationFilePath; + const manifestPath = this.getPlatformData(projectData).configurationFilePath; shell.sed('-i', /__PACKAGE__/, projectData.projectId, manifestPath); - const sdk = (platformSpecificData && platformSpecificData.sdk) || this.$androidToolsInfo.getToolsInfo().compileSdkVersion.toString(); - shell.sed('-i', /__APILEVEL__/, sdk, manifestPath); + if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { + const sdk = (platformSpecificData && platformSpecificData.sdk) || (this.$androidToolsInfo.getToolsInfo().compileSdkVersion || "").toString(); + shell.sed('-i', /__APILEVEL__/, sdk, manifestPath); + } } private getProjectNameFromId(projectData: IProjectData): string { let id: string; if (projectData && projectData.projectId) { - let idParts = projectData.projectId.split("."); + const idParts = projectData.projectId.split("."); id = idParts[idParts.length - 1]; } @@ -239,7 +245,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async updatePlatform(currentVersion: string, newVersion: string, canUpdate: boolean, projectData: IProjectData, addPlatform?: Function, removePlatforms?: (platforms: string[]) => Promise): Promise { if (semver.eq(newVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE)) { - let platformLowercase = this.getPlatformData(projectData).normalizedPlatformName.toLowerCase(); + const platformLowercase = this.getPlatformData(projectData).normalizedPlatformName.toLowerCase(); await removePlatforms([platformLowercase.split("@")[0]]); await addPlatform(platformLowercase); return false; @@ -250,7 +256,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { if (this.canUseGradle(projectData)) { - let buildOptions = this.getBuildOptions(buildConfig, projectData); + const buildOptions = this.getBuildOptions(buildConfig, projectData); if (this.$logger.getLevel() === "TRACE") { buildOptions.unshift("--stacktrace"); buildOptions.unshift("--debug"); @@ -278,13 +284,13 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private getBuildOptions(settings: IAndroidBuildOptionsSettings, projectData: IProjectData): Array { this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true }); - let androidToolsInfo = this.$androidToolsInfo.getToolsInfo(); - let compileSdk = androidToolsInfo.compileSdkVersion; - let targetSdk = this.getTargetFromAndroidManifest(projectData) || compileSdk; - let buildToolsVersion = androidToolsInfo.buildToolsVersion; - let appCompatVersion = androidToolsInfo.supportRepositoryVersion; - let generateTypings = androidToolsInfo.generateTypings; - let buildOptions = [ + const androidToolsInfo = this.$androidToolsInfo.getToolsInfo(); + const compileSdk = androidToolsInfo.compileSdkVersion; + const targetSdk = this.getTargetFromAndroidManifest(projectData) || compileSdk; + const buildToolsVersion = androidToolsInfo.buildToolsVersion; + const appCompatVersion = androidToolsInfo.supportRepositoryVersion; + const generateTypings = androidToolsInfo.generateTypings; + const buildOptions = [ `-PcompileSdk=android-${compileSdk}`, `-PtargetSdk=${targetSdk}`, `-PbuildToolsVersion=${buildToolsVersion}`, @@ -320,9 +326,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } public ensureConfigurationFileInAppResources(projectData: IProjectData): void { - let originalAndroidManifestFilePath = path.join(projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.getPlatformData(projectData).configurationFileName); + const originalAndroidManifestFilePath = path.join(projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.getPlatformData(projectData).configurationFileName); - let manifestExists = this.$fs.exists(originalAndroidManifestFilePath); + const manifestExists = this.$fs.exists(originalAndroidManifestFilePath); if (!manifestExists) { this.$logger.warn('No manifest found in ' + originalAndroidManifestFilePath); @@ -333,16 +339,16 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void { - let resourcesDirPath = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); - let valuesDirRegExp = /^values/; - let resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp)); + const resourcesDirPath = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); + const valuesDirRegExp = /^values/; + const resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp)); _.each(resourcesDirs, resourceDir => { this.$fs.deleteDirectory(path.join(this.getAppResourcesDestinationDirectoryPath(projectData), resourceDir)); }); } public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { - let pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME); + const pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME); await this.processResourcesFromPlugin(pluginData, pluginPlatformsFolderPath, projectData); } @@ -351,27 +357,30 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } private async processResourcesFromPlugin(pluginData: IPluginData, pluginPlatformsFolderPath: string, projectData: IProjectData): Promise { - let configurationsDirectoryPath = path.join(this.getPlatformData(projectData).projectRoot, "configurations"); + const configurationsDirectoryPath = path.join(this.getPlatformData(projectData).projectRoot, "configurations"); this.$fs.ensureDirectoryExists(configurationsDirectoryPath); - let pluginConfigurationDirectoryPath = path.join(configurationsDirectoryPath, pluginData.name); + const pluginConfigurationDirectoryPath = path.join(configurationsDirectoryPath, pluginData.name); if (this.$fs.exists(pluginPlatformsFolderPath)) { this.$fs.ensureDirectoryExists(pluginConfigurationDirectoryPath); + const isScoped = pluginData.name.indexOf("@") === 0; + const flattenedDependencyName = isScoped ? pluginData.name.replace("/", "_") : pluginData.name; + // Copy all resources from plugin - let resourcesDestinationDirectoryPath = path.join(this.getPlatformData(projectData).projectRoot, "src", pluginData.name); + const resourcesDestinationDirectoryPath = path.join(this.getPlatformData(projectData).projectRoot, "src", flattenedDependencyName); this.$fs.ensureDirectoryExists(resourcesDestinationDirectoryPath); shell.cp("-Rf", path.join(pluginPlatformsFolderPath, "*"), resourcesDestinationDirectoryPath); const filesForInterpolation = this.$fs.enumerateFilesInDirectorySync(resourcesDestinationDirectoryPath, file => this.$fs.getFsStats(file).isDirectory() || path.extname(file) === constants.XML_FILE_EXTENSION) || []; - for (let file of filesForInterpolation) { + for (const file of filesForInterpolation) { this.$logger.trace(`Interpolate data for plugin file: ${file}`); await this.$pluginVariablesService.interpolate(pluginData, file, projectData); } } // Copy include.gradle file - let includeGradleFilePath = path.join(pluginPlatformsFolderPath, "include.gradle"); + const includeGradleFilePath = path.join(pluginPlatformsFolderPath, "include.gradle"); if (this.$fs.exists(includeGradleFilePath)) { shell.cp("-f", includeGradleFilePath, pluginConfigurationDirectoryPath); } @@ -380,7 +389,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject public async removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { try { // check whether the dependency that's being removed has native code - let pluginConfigDir = path.join(this.getPlatformData(projectData).projectRoot, "configurations", pluginData.name); + const pluginConfigDir = path.join(this.getPlatformData(projectData).projectRoot, "configurations", pluginData.name); if (this.$fs.exists(pluginConfigDir)) { await this.cleanProject(this.getPlatformData(projectData).projectRoot, projectData); } @@ -397,26 +406,24 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return; } - public async beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDictionary): Promise { - if (!this.$config.debugLivesync) { - if (dependencies) { - let platformDir = path.join(projectData.platformsDir, "android"); - let buildDir = path.join(platformDir, "build-tools"); - let checkV8dependants = path.join(buildDir, "check-v8-dependants.js"); - if (this.$fs.exists(checkV8dependants)) { - let stringifiedDependencies = JSON.stringify(dependencies); - try { - await this.spawn('node', [checkV8dependants, stringifiedDependencies, projectData.platformsDir], { stdio: "inherit" }); - } catch (e) { - this.$logger.info("Checking for dependants on v8 public API failed. This is likely caused because of cyclic production dependencies. Error code: " + e.code + "\nMore information: https://github.com/NativeScript/nativescript-cli/issues/2561"); - } + public async beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise { + if (dependencies) { + const platformDir = path.join(projectData.platformsDir, "android"); + const buildDir = path.join(platformDir, "build-tools"); + const checkV8dependants = path.join(buildDir, "check-v8-dependants.js"); + if (this.$fs.exists(checkV8dependants)) { + const stringifiedDependencies = JSON.stringify(dependencies); + try { + await this.spawn('node', [checkV8dependants, stringifiedDependencies, projectData.platformsDir], { stdio: "inherit" }); + } catch (e) { + this.$logger.info("Checking for dependants on v8 public API failed. This is likely caused because of cyclic production dependencies. Error code: " + e.code + "\nMore information: https://github.com/NativeScript/nativescript-cli/issues/2561"); } } + } - let projectRoot = this.getPlatformData(projectData).projectRoot; + const projectRoot = this.getPlatformData(projectData).projectRoot; - await this.cleanProject(projectRoot, projectData); - } + await this.cleanProject(projectRoot, projectData); } public stopServices(projectRoot: string): Promise { @@ -424,17 +431,23 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } public async cleanProject(projectRoot: string, projectData: IProjectData): Promise { - const buildOptions = this.getBuildOptions({ release: false }, projectData); - buildOptions.unshift("clean"); - await this.executeGradleCommand(projectRoot, buildOptions); + if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { + const buildOptions = this.getBuildOptions({ release: false }, projectData); + buildOptions.unshift("clean"); + await this.executeGradleCommand(projectRoot, buildOptions); + } } public async cleanDeviceTempFolder(deviceIdentifier: string, projectData: IProjectData): Promise { - let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier }); - let deviceRootPath = `/data/local/tmp/${projectData.projectId}`; + const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier }); + const deviceRootPath = `/data/local/tmp/${projectData.projectId}`; await adb.executeShellCommand(["rm", "-rf", deviceRootPath]); } + public async checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise { + // Nothing android specific to check yet. + } + private _canUseGradle: boolean; private canUseGradle(projectData: IProjectData, frameworkVersion?: string): boolean { if (!this._canUseGradle) { @@ -450,7 +463,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): void { - let paths = files.split(' ').map(p => path.join(frameworkDir, p)); + const paths = files.split(' ').map(p => path.join(frameworkDir, p)); shell.cp(cpArg, paths, projectRoot); } @@ -485,9 +498,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private getTargetFromAndroidManifest(projectData: IProjectData): string { let versionInManifest: string; if (this.$fs.exists(this.getPlatformData(projectData).configurationFilePath)) { - let targetFromAndroidManifest: string = this.$fs.readText(this.getPlatformData(projectData).configurationFilePath); + const targetFromAndroidManifest: string = this.$fs.readText(this.getPlatformData(projectData).configurationFilePath); if (targetFromAndroidManifest) { - let match = targetFromAndroidManifest.match(/.*?android:targetSdkVersion=\"(.*?)\"/); + const match = targetFromAndroidManifest.match(/.*?android:targetSdkVersion=\"(.*?)\"/); if (match && match[1]) { versionInManifest = match[1]; } @@ -498,16 +511,24 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } private async executeGradleCommand(projectRoot: string, gradleArgs: string[], childProcessOpts?: SpawnOptions, spawnFromEventOptions?: ISpawnFromEventOptions): Promise { - const gradlew = this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew"; + if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { + const gradlew = this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew"; - childProcessOpts = childProcessOpts || {}; - childProcessOpts.cwd = childProcessOpts.cwd || projectRoot; - childProcessOpts.stdio = childProcessOpts.stdio || "inherit"; + const localArgs = [...gradleArgs]; + if (this.$logger.getLevel() === "INFO") { + localArgs.push("--quiet"); + this.$logger.info("Gradle build..."); + } + + childProcessOpts = childProcessOpts || {}; + childProcessOpts.cwd = childProcessOpts.cwd || projectRoot; + childProcessOpts.stdio = childProcessOpts.stdio || "inherit"; - return await this.spawn(gradlew, - gradleArgs, - childProcessOpts, - spawnFromEventOptions); + return await this.spawn(gradlew, + localArgs, + childProcessOpts, + spawnFromEventOptions); + } } } diff --git a/lib/services/app-files-updater.ts b/lib/services/app-files-updater.ts index dde5831ce4..f7de291a29 100644 --- a/lib/services/app-files-updater.ts +++ b/lib/services/app-files-updater.ts @@ -50,7 +50,7 @@ export class AppFilesUpdater { } protected readSourceDir(): string[] { - let tnsDir = path.join(this.appSourceDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); + const tnsDir = path.join(this.appSourceDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); return this.fs.enumerateFilesInDirectorySync(this.appSourceDirectoryPath, null, { includeEmptyDirectories: true }).filter(dirName => dirName !== tnsDir); } @@ -59,7 +59,7 @@ export class AppFilesUpdater { let sourceFiles = this.readSourceDir(); if (this.options.release) { - let testsFolderPath = path.join(this.appSourceDirectoryPath, 'tests'); + const testsFolderPath = path.join(this.appSourceDirectoryPath, 'tests'); sourceFiles = sourceFiles.filter(source => source.indexOf(testsFolderPath) === -1); } @@ -76,7 +76,7 @@ export class AppFilesUpdater { protected copyAppSourceFiles(sourceFiles: string[]): void { sourceFiles.map(source => { - let destinationPath = path.join(this.appDestinationDirectoryPath, path.relative(this.appSourceDirectoryPath, source)); + const destinationPath = path.join(this.appDestinationDirectoryPath, path.relative(this.appSourceDirectoryPath, source)); let exists = fs.lstatSync(source); if (exists.isSymbolicLink()) { diff --git a/lib/services/cocoapods-service.ts b/lib/services/cocoapods-service.ts index c07d49bf1a..e9f8dfc117 100644 --- a/lib/services/cocoapods-service.ts +++ b/lib/services/cocoapods-service.ts @@ -21,18 +21,18 @@ export class CocoaPodsService implements ICocoaPodsService { throw new Error(`The Podfile ${pathToPodfile} does not exist.`); } - let podfileContent = this.$fs.readText(pathToPodfile); - let hookStart = `${hookName} do`; + const podfileContent = this.$fs.readText(pathToPodfile); + const hookStart = `${hookName} do`; - let hookDefinitionRegExp = new RegExp(`${hookStart} *(\\|(\\w+)\\|)?`, "g"); + const hookDefinitionRegExp = new RegExp(`${hookStart} *(\\|(\\w+)\\|)?`, "g"); let newFunctionNameIndex = 1; - let newFunctions: IRubyFunction[] = []; + const newFunctions: IRubyFunction[] = []; - let replacedContent = podfileContent.replace(hookDefinitionRegExp, (substring: string, firstGroup: string, secondGroup: string, index: number): string => { - let newFunctionName = `${hookName}${newFunctionNameIndex++}`; + const replacedContent = podfileContent.replace(hookDefinitionRegExp, (substring: string, firstGroup: string, secondGroup: string, index: number): string => { + const newFunctionName = `${hookName}${newFunctionNameIndex++}`; let newDefinition = `def ${newFunctionName}`; - let rubyFunction: IRubyFunction = { functionName: newFunctionName }; + const rubyFunction: IRubyFunction = { functionName: newFunctionName }; // firstGroup is the block parameter, secondGroup is the block parameter name. if (firstGroup && secondGroup) { newDefinition = `${newDefinition} (${secondGroup})`; @@ -45,7 +45,7 @@ export class CocoaPodsService implements ICocoaPodsService { if (newFunctions.length > 1) { // Execute all methods in the hook and pass the parameter to them. - let blokParameterName = "installer"; + const blokParameterName = "installer"; let mergedHookContent = `${hookStart} |${blokParameterName}|${EOL}`; _.each(newFunctions, (rubyFunction: IRubyFunction) => { @@ -59,7 +59,7 @@ export class CocoaPodsService implements ICocoaPodsService { mergedHookContent = `${mergedHookContent}end`; - let newPodfileContent = `${replacedContent}${EOL}${mergedHookContent}`; + const newPodfileContent = `${replacedContent}${EOL}${mergedHookContent}`; this.$fs.writeFile(pathToPodfile, newPodfileContent); } } diff --git a/lib/services/debug-data-service.ts b/lib/services/debug-data-service.ts index 038e2b6091..3584f2165f 100644 --- a/lib/services/debug-data-service.ts +++ b/lib/services/debug-data-service.ts @@ -1,5 +1,5 @@ export class DebugDataService implements IDebugDataService { - public createDebugData(projectData: IProjectData, options: IOptions): IDebugData { + public createDebugData(projectData: IProjectData, options: IDeviceIdentifier): IDebugData { return { applicationIdentifier: projectData.projectId, projectDir: projectData.projectDir, diff --git a/lib/services/debug-service-base.ts b/lib/services/debug-service-base.ts index 417c080454..054c8f8578 100644 --- a/lib/services/debug-service-base.ts +++ b/lib/services/debug-service-base.ts @@ -1,9 +1,16 @@ import { EventEmitter } from "events"; export abstract class DebugServiceBase extends EventEmitter implements IPlatformDebugService { + constructor( + protected device: Mobile.IDevice, + protected $devicesService: Mobile.IDevicesService + ) { + super(); + } + public abstract get platform(): string; - public abstract async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; + public abstract async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise; public abstract async debugStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise; @@ -12,10 +19,35 @@ export abstract class DebugServiceBase extends EventEmitter implements IPlatform protected getCanExecuteAction(deviceIdentifier: string): (device: Mobile.IDevice) => boolean { return (device: Mobile.IDevice): boolean => { if (deviceIdentifier) { - return device.deviceInfo.identifier === deviceIdentifier; + let isSearchedDevice = device.deviceInfo.identifier === deviceIdentifier; + if (!isSearchedDevice) { + const deviceByDeviceOption = this.$devicesService.getDeviceByDeviceOption(); + isSearchedDevice = deviceByDeviceOption && device.deviceInfo.identifier === deviceByDeviceOption.deviceInfo.identifier; + } + + return isSearchedDevice; } else { return true; } }; } + + protected getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string { + // corresponds to 55.0.2883 Chrome version + const commitSHA = debugOptions.devToolsCommit || "02e6bde1bbe34e43b309d4ef774b1168d25fd024"; + debugOptions.useHttpUrl = debugOptions.useHttpUrl === undefined ? false : debugOptions.useHttpUrl; + + let chromeDevToolsPrefix = `chrome-devtools://devtools/remote/serve_file/@${commitSHA}`; + + if (debugOptions.useBundledDevTools) { + chromeDevToolsPrefix = "chrome-devtools://devtools/bundled"; + } + + if (debugOptions.useHttpUrl) { + chromeDevToolsPrefix = `https://chrome-devtools-frontend.appspot.com/serve_file/@${commitSHA}`; + } + + const chromeUrl = `${chromeDevToolsPrefix}/inspector.html?experiments=true&ws=localhost:${port}`; + return chromeUrl; + } } diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts index d1195add67..ab6e730835 100644 --- a/lib/services/debug-service.ts +++ b/lib/services/debug-service.ts @@ -1,51 +1,62 @@ import { platform } from "os"; +import { parse } from "url"; import { EventEmitter } from "events"; -import { CONNECTION_ERROR_EVENT_NAME } from "../constants"; +import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors } from "../constants"; +import { CONNECTED_STATUS } from "../common/constants"; +import { DebugTools, TrackActionNames } from "../constants"; -// This service can't implement IDebugService because -// the debug method returns only one result. -class DebugService extends EventEmitter { +export class DebugService extends EventEmitter implements IDebugService { + private _platformDebugServices: IDictionary; constructor(private $devicesService: Mobile.IDevicesService, - private $androidDebugService: IPlatformDebugService, - private $iOSDebugService: IPlatformDebugService, private $errors: IErrors, + private $injector: IInjector, private $hostInfo: IHostInfo, - private $mobileHelper: Mobile.IMobileHelper) { + private $mobileHelper: Mobile.IMobileHelper, + private $analyticsService: IAnalyticsService) { super(); - this.attachConnectionErrorHandlers(); + this._platformDebugServices = {}; } - public async debug(debugData: IDebugData, options: IDebugOptions): Promise { + public async debug(debugData: IDebugData, options: IDebugOptions): Promise { const device = this.$devicesService.getDeviceByIdentifier(debugData.deviceIdentifier); - const debugService = this.getDebugService(device); if (!device) { - this.$errors.failWithoutHelp(`Can't find device with identifier ${debugData.deviceIdentifier}`); + this.$errors.failWithoutHelp(`Cannot find device with identifier ${debugData.deviceIdentifier}.`); + } + + if (device.deviceInfo.status !== CONNECTED_STATUS) { + this.$errors.failWithoutHelp(`The device with identifier ${debugData.deviceIdentifier} is unreachable. Make sure it is Trusted and try again.`); } + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.Debug, + device, + additionalData: this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform) && (!options || !options.chrome) ? DebugTools.Inspector : DebugTools.Chrome, + projectDir: debugData.projectDir + }); + if (!(await device.applicationManager.isApplicationInstalled(debugData.applicationIdentifier))) { this.$errors.failWithoutHelp(`The application ${debugData.applicationIdentifier} is not installed on device with identifier ${debugData.deviceIdentifier}.`); } - const debugOptions: IDebugOptions = _.merge({}, options); - debugOptions.start = true; + const debugOptions: IDebugOptions = _.cloneDeep(options); // TODO: Check if app is running. // For now we can only check if app is running on Android. // After we find a way to check on iOS we should use it here. - const isAppRunning = true; - let result: string[]; - debugOptions.chrome = !debugOptions.client; + let result: string; + + const debugService = this.getDebugService(device); + if (!debugService) { + this.$errors.failWithoutHelp(`Unsupported device OS: ${device.deviceInfo.platform}. You can debug your applications only on iOS or Android.`); + } + if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { - if (device.isEmulator && !debugData.pathToAppPackage) { + if (device.isEmulator && !debugData.pathToAppPackage && debugOptions.debugBrk) { this.$errors.failWithoutHelp("To debug on iOS simulator you need to provide path to the app package."); } if (this.$hostInfo.isWindows) { - if (!isAppRunning) { - this.$errors.failWithoutHelp(`Application ${debugData.applicationIdentifier} is not running. To be able to debug the application on Windows you must run it.`); - } - debugOptions.emulator = false; } else if (!this.$hostInfo.isDarwin) { this.$errors.failWithoutHelp(`Debugging on iOS devices is not supported for ${platform()} yet.`); @@ -56,22 +67,57 @@ class DebugService extends EventEmitter { result = await debugService.debug(debugData, debugOptions); } - return _.first(result); + return this.getDebugInformation(result, device.deviceInfo.identifier); } - private getDebugService(device: Mobile.IDevice): IPlatformDebugService { - if (this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { - return this.$iOSDebugService; - } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { - return this.$androidDebugService; + public debugStop(deviceIdentifier: string): Promise { + const debugService = this.getDebugServiceByIdentifier(deviceIdentifier); + return debugService.debugStop(); + } + + protected getDebugService(device: Mobile.IDevice): IPlatformDebugService { + if (!this._platformDebugServices[device.deviceInfo.identifier]) { + const platform = device.deviceInfo.platform; + if (this.$mobileHelper.isiOSPlatform(platform)) { + this._platformDebugServices[device.deviceInfo.identifier] = this.$injector.resolve("iOSDebugService", { device }); + } else if (this.$mobileHelper.isAndroidPlatform(platform)) { + this._platformDebugServices[device.deviceInfo.identifier] = this.$injector.resolve("androidDebugService", { device }); + } else { + this.$errors.failWithoutHelp(DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING); + } + + this.attachConnectionErrorHandlers(this._platformDebugServices[device.deviceInfo.identifier]); } + + return this._platformDebugServices[device.deviceInfo.identifier]; + } + + private getDebugServiceByIdentifier(deviceIdentifier: string): IPlatformDebugService { + const device = this.$devicesService.getDeviceByIdentifier(deviceIdentifier); + return this.getDebugService(device); } - private attachConnectionErrorHandlers() { + private attachConnectionErrorHandlers(platformDebugService: IPlatformDebugService) { let connectionErrorHandler = (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e); connectionErrorHandler = connectionErrorHandler.bind(this); - this.$androidDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); - this.$iOSDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); + platformDebugService.on(CONNECTION_ERROR_EVENT_NAME, connectionErrorHandler); + } + + private getDebugInformation(fullUrl: string, deviceIdentifier: string): IDebugInformation { + const debugInfo: IDebugInformation = { + url: fullUrl, + port: 0, + deviceIdentifier + }; + + if (fullUrl) { + const parseQueryString = true; + const wsQueryParam = parse(fullUrl, parseQueryString).query.ws; + const hostPortSplit = wsQueryParam && wsQueryParam.split(":"); + debugInfo.port = hostPortSplit && +hostPortSplit[1]; + } + + return debugInfo; } } diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 1b4d5113f3..b2ceb4eba6 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -2,7 +2,7 @@ import { EOL } from "os"; import * as semver from "semver"; import * as path from "path"; import * as helpers from "../common/helpers"; -let clui = require("clui"); +const clui = require("clui"); class DoctorService implements IDoctorService { private static PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__"; @@ -33,7 +33,7 @@ class DoctorService implements IDoctorService { public async printWarnings(configOptions?: { trackResult: boolean }): Promise { let result = false; - let sysInfo = await this.$sysInfo.getSysInfo(this.$staticConfig.pathToPackageJson); + const sysInfo = await this.$sysInfo.getSysInfo(this.$staticConfig.pathToPackageJson); if (!sysInfo.adbVer) { this.$logger.warn("WARNING: adb from the Android SDK is not installed or is not configured properly."); @@ -80,7 +80,7 @@ class DoctorService implements IDoctorService { } if (sysInfo.xcodeVer && sysInfo.cocoapodVer) { - let problemWithCocoaPods = await this.verifyCocoaPods(); + const problemWithCocoaPods = await this.verifyCocoaPods(); if (problemWithCocoaPods) { this.$logger.warn("WARNING: There was a problem with CocoaPods"); this.$logger.out("Verify that CocoaPods are configured properly."); @@ -95,7 +95,7 @@ class DoctorService implements IDoctorService { result = true; } - if (await this.$xcprojService.verifyXcproj(false)) { + if (sysInfo.xcodeVer && sysInfo.cocoapodVer && await this.$xcprojService.verifyXcproj(false)) { result = true; } } else { @@ -103,10 +103,10 @@ class DoctorService implements IDoctorService { this.$logger.out("To be able to work with iOS devices and projects, you need Mac OS X Mavericks or later." + EOL); } - let androidToolsIssues = this.$androidToolsInfo.validateInfo(); - let javaVersionIssue = await this.$androidToolsInfo.validateJavacVersion(sysInfo.javacVersion); - let pythonIssues = await this.validatePythonPackages(); - let doctorResult = result || androidToolsIssues || javaVersionIssue || pythonIssues; + const androidToolsIssues = this.$androidToolsInfo.validateInfo(); + const javaVersionIssue = await this.$androidToolsInfo.validateJavacVersion(sysInfo.javacVersion); + const pythonIssues = await this.validatePythonPackages(); + const doctorResult = result || androidToolsIssues || javaVersionIssue || pythonIssues; if (!configOptions || configOptions.trackResult) { await this.$analyticsService.track("DoctorEnvironmentSetup", doctorResult ? "incorrect" : "correct"); @@ -123,10 +123,8 @@ class DoctorService implements IDoctorService { } } - let versionsInformation: IVersionInformation[] = []; try { - versionsInformation = await this.$versionsService.getComponentsForUpdate(); - this.printVersionsInformation(versionsInformation); + await this.$versionsService.checkComponentsForUpdate(); } catch (err) { this.$logger.error("Cannot get the latest versions information from npm. Please try again later."); } @@ -134,17 +132,6 @@ class DoctorService implements IDoctorService { return doctorResult; } - private printVersionsInformation(versionsInformation: IVersionInformation[]) { - if (versionsInformation && versionsInformation.length) { - let table: any = this.$versionsService.createTableWithVersionsInformation(versionsInformation); - - this.$logger.warn("Updates available"); - this.$logger.out(table.toString() + EOL); - } else { - this.$logger.out("Your components are up-to-date." + EOL); - } - } - private async promptForDocs(link: string): Promise { if (await this.$prompter.confirm("Do you want to visit the official documentation?", () => helpers.isInteractive())) { this.$opener.open(link); @@ -170,21 +157,28 @@ class DoctorService implements IDoctorService { private async verifyCocoaPods(): Promise { this.$logger.out("Verifying CocoaPods. This may take more than a minute, please be patient."); - let temp = require("temp"); + const temp = require("temp"); temp.track(); - let projDir = temp.mkdirSync("nativescript-check-cocoapods"); - let packageJsonData = { + const projDir = temp.mkdirSync("nativescript-check-cocoapods"); + const packageJsonData = { "name": "nativescript-check-cocoapods", "version": "0.0.1" }; this.$fs.writeJson(path.join(projDir, "package.json"), packageJsonData); - let spinner = new clui.Spinner("Installing iOS runtime."); + const spinner = new clui.Spinner("Installing iOS runtime."); try { spinner.start(); - await this.$npm.install("tns-ios", projDir, { global: false, "ignore-scripts": true, production: true, save: true }); + await this.$npm.install("tns-ios", projDir, { + global: false, + production: true, + save: true, + disableNpmInstall: false, + frameworkPath: null, + ignoreScripts: true + }); spinner.stop(); - let iosDir = path.join(projDir, "node_modules", "tns-ios", "framework"); + const iosDir = path.join(projDir, "node_modules", "tns-ios", "framework"); this.$fs.writeFile( path.join(iosDir, "Podfile"), `${this.$cocoapodsService.getPodfileHeader(DoctorService.PROJECT_NAME_PLACEHOLDER)}pod 'AFNetworking', '~> 1.0'${this.$cocoapodsService.getPodfileFooter()}` @@ -192,7 +186,7 @@ class DoctorService implements IDoctorService { spinner.message("Verifying CocoaPods. This may take some time, please be patient."); spinner.start(); - let future = this.$childProcess.spawnFromEvent( + const future = this.$childProcess.spawnFromEvent( this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod", ["install"], "exit", @@ -200,7 +194,7 @@ class DoctorService implements IDoctorService { { throwError: false } ); - let result = await this.$progressIndicator.showProgressIndicator(future, 5000); + const result = await this.$progressIndicator.showProgressIndicator(future, 5000); if (result.exitCode) { this.$logger.out(result.stdout, result.stderr); return true; diff --git a/lib/services/emulator-platform-service.ts b/lib/services/emulator-platform-service.ts index 6bcd24de6f..b2a9af9e83 100644 --- a/lib/services/emulator-platform-service.ts +++ b/lib/services/emulator-platform-service.ts @@ -1,4 +1,5 @@ import { createTable, deferPromise } from "../common/helpers"; +import { DeviceTypes } from "../common/constants"; export class EmulatorPlatformService implements IEmulatorPlatformService { @@ -15,9 +16,9 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { if (this.$mobileHelper.isAndroidPlatform(info.platform)) { this.$options.avd = this.$options.device; this.$options.device = null; - let platformsData: IPlatformsData = $injector.resolve("platformsData"); - let platformData = platformsData.getPlatformData(info.platform, projectData); - let emulatorServices = platformData.emulatorServices; + const platformsData: IPlatformsData = $injector.resolve("platformsData"); + const platformData = platformsData.getPlatformData(info.platform, projectData); + const emulatorServices = platformData.emulatorServices; emulatorServices.checkAvailability(); await emulatorServices.checkDependencies(); await emulatorServices.startEmulator(); @@ -27,13 +28,13 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { if (this.$mobileHelper.isiOSPlatform(info.platform)) { await this.stopEmulator(info.platform); - let deferred = deferPromise(); + const deferred = deferPromise(); await this.$childProcess.exec(`open -a Simulator --args -CurrentDeviceUDID ${info.id}`); - let timeoutFunc = async () => { + const timeoutFunc = async () => { info = await this.getEmulatorInfo("ios", info.id); if (info.isRunning) { await this.$devicesService.initialize({ platform: info.platform, deviceId: info.id }); - let device = this.$devicesService.getDeviceByIdentifier(info.id); + const device = this.$devicesService.getDeviceByIdentifier(info.id); await device.applicationManager.checkForApplicationUpdates(); deferred.resolve(); return; @@ -55,22 +56,22 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { public async getEmulatorInfo(platform: string, idOrName: string): Promise { if (this.$mobileHelper.isAndroidPlatform(platform)) { - let androidEmulators = this.getAndroidEmulators(); - let found = androidEmulators.filter((info: IEmulatorInfo) => info.id === idOrName); + const androidEmulators = this.getAndroidEmulators(); + const found = androidEmulators.filter((info: IEmulatorInfo) => info.id === idOrName); if (found.length > 0) { return found[0]; } await this.$devicesService.initialize({ platform: platform, deviceId: null, skipInferPlatform: true }); let info: IEmulatorInfo = null; - let action = async (device: Mobile.IDevice) => { + const action = async (device: Mobile.IDevice) => { if (device.deviceInfo.identifier === idOrName) { info = { id: device.deviceInfo.identifier, name: device.deviceInfo.displayName, version: device.deviceInfo.version, platform: "Android", - type: "emulator", + type: DeviceTypes.Emulator, isRunning: true }; } @@ -80,15 +81,15 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { } if (this.$mobileHelper.isiOSPlatform(platform)) { - let emulators = await this.getiOSEmulators(); + const emulators = await this.getiOSEmulators(); let sdk: string = null; - let versionStart = idOrName.indexOf("("); + const versionStart = idOrName.indexOf("("); if (versionStart > 0) { sdk = idOrName.substring(versionStart + 1, idOrName.indexOf(")", versionStart)).trim(); idOrName = idOrName.substring(0, versionStart - 1).trim(); } - let found = emulators.filter((info: IEmulatorInfo) => { - let sdkMatch = sdk ? info.version === sdk : true; + const found = emulators.filter((info: IEmulatorInfo) => { + const sdkMatch = sdk ? info.version === sdk : true; return sdkMatch && info.id === idOrName || info.name === idOrName; }); return found.length > 0 ? found[0] : null; @@ -101,14 +102,14 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { public async listAvailableEmulators(platform: string): Promise { let emulators: IEmulatorInfo[] = []; if (!platform || this.$mobileHelper.isiOSPlatform(platform)) { - let iosEmulators = await this.getiOSEmulators(); + const iosEmulators = await this.getiOSEmulators(); if (iosEmulators) { emulators = emulators.concat(iosEmulators); } } if (!platform || this.$mobileHelper.isAndroidPlatform(platform)) { - let androidEmulators = this.getAndroidEmulators(); + const androidEmulators = this.getAndroidEmulators(); if (androidEmulators) { emulators = emulators.concat(androidEmulators); } @@ -118,20 +119,20 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { } public async getiOSEmulators(): Promise { - let output = await this.$childProcess.exec("xcrun simctl list --json"); - let list = JSON.parse(output); - let emulators: IEmulatorInfo[] = []; - for (let osName in list["devices"]) { + const output = await this.$childProcess.exec("xcrun simctl list --json"); + const list = JSON.parse(output); + const emulators: IEmulatorInfo[] = []; + for (const osName in list["devices"]) { if (osName.indexOf("iOS") === -1) { continue; } - let os = list["devices"][osName]; - let version = this.parseiOSVersion(osName); - for (let device of os) { + const os = list["devices"][osName]; + const version = this.parseiOSVersion(osName); + for (const device of os) { if (device["availability"] !== "(available)") { continue; } - let emulatorInfo: IEmulatorInfo = { + const emulatorInfo: IEmulatorInfo = { id: device["udid"], name: device["name"], isRunning: device["state"] === "Booted", @@ -166,8 +167,8 @@ export class EmulatorPlatformService implements IEmulatorPlatformService { private outputEmulators(title: string, emulators: IEmulatorInfo[]) { this.$logger.out(title); - let table: any = createTable(["Device Name", "Platform", "Version", "Device Identifier"], []); - for (let info of emulators) { + const table: any = createTable(["Device Name", "Platform", "Version", "Device Identifier"], []); + for (const info of emulators) { table.push([info.name, info.platform, info.version, info.id]); } diff --git a/lib/services/emulator-settings-service.ts b/lib/services/emulator-settings-service.ts index 806d0139bb..d9bd1b6f26 100644 --- a/lib/services/emulator-settings-service.ts +++ b/lib/services/emulator-settings-service.ts @@ -4,9 +4,9 @@ export class EmulatorSettingsService implements Mobile.IEmulatorSettingsService constructor(private $injector: IInjector) { } public canStart(platform: string): boolean { - let platformService = this.$injector.resolve("platformService"); // this should be resolved here due to cyclic dependency + const platformService = this.$injector.resolve("platformService"); // this should be resolved here due to cyclic dependency - let installedPlatforms = platformService.getInstalledPlatforms(); + const installedPlatforms = platformService.getInstalledPlatforms(); return _.includes(installedPlatforms, platform.toLowerCase()); } diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts index 756e037f00..0104e117fe 100644 --- a/lib/services/extensibility-service.ts +++ b/lib/services/extensibility-service.ts @@ -32,13 +32,10 @@ export class ExtensibilityService implements IExtensibilityService { const localPath = path.resolve(extensionName); const packageName = this.$fs.exists(localPath) ? localPath : extensionName; - const realName = (await this.$npm.install(packageName, this.pathToExtensions, npmOpts))[0]; + const installResultInfo = await this.$npm.install(packageName, this.pathToExtensions, npmOpts); this.$logger.trace(`Finished installation of extension '${extensionName}'. Trying to load it now.`); - // In case the extension is already installed, the $npm.install method will not return the name of the package. - // Fallback to the original value. - // NOTE: This will not be required once $npm.install starts working correctly. - return await this.loadExtension(realName || extensionName); + return { extensionName: installResultInfo.name }; } @exported("extensibilityService") @@ -77,7 +74,8 @@ export class ExtensibilityService implements IExtensibilityService { return null; } - private async loadExtension(extensionName: string): Promise { + @exported("extensibilityService") + public async loadExtension(extensionName: string): Promise { try { await this.assertExtensionIsInstalled(extensionName); @@ -86,7 +84,7 @@ export class ExtensibilityService implements IExtensibilityService { return { extensionName }; } catch (error) { this.$logger.warn(`Error while loading ${extensionName} is: ${error.message}`); - const err = new Error(`Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + const err = new Error(`Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds. Error: ${error.message}`); err.extensionName = extensionName; throw err; } diff --git a/lib/services/info-service.ts b/lib/services/info-service.ts index 40eb96a1b3..e7cb63c8ac 100644 --- a/lib/services/info-service.ts +++ b/lib/services/info-service.ts @@ -3,9 +3,9 @@ export class InfoService implements IInfoService { private $logger: ILogger) { } public async printComponentsInfo(): Promise { - let allComponentsInfo = await this.$versionsService.getAllComponentsVersions(); + const allComponentsInfo = await this.$versionsService.getAllComponentsVersions(); - let table: any = this.$versionsService.createTableWithVersionsInformation(allComponentsInfo); + const table: any = this.$versionsService.createTableWithVersionsInformation(allComponentsInfo); this.$logger.out("All NativeScript components versions information"); this.$logger.out(table.toString()); diff --git a/lib/services/init-service.ts b/lib/services/init-service.ts index f60751fa83..4017d29863 100644 --- a/lib/services/init-service.ts +++ b/lib/services/init-service.ts @@ -31,7 +31,7 @@ export class InitService implements IInitService { projectData = this.$fs.readJson(this.projectFilePath); } - let projectDataBackup = _.extend({}, projectData); + const projectDataBackup = _.extend({}, projectData); if (!projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE]) { projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE] = {}; @@ -42,30 +42,30 @@ export class InitService implements IInitService { projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE]["id"] = await this.getProjectId(); if (this.$options.frameworkName && this.$options.frameworkVersion) { - let currentPlatformData = projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][this.$options.frameworkName] || {}; + const currentPlatformData = projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][this.$options.frameworkName] || {}; projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][this.$options.frameworkName] = _.extend(currentPlatformData, this.buildVersionData(this.$options.frameworkVersion)); } else { - let $platformsData = this.$injector.resolve("platformsData"); - let $projectData = this.$injector.resolve("projectData"); + const $platformsData = this.$injector.resolve("platformsData"); + const $projectData = this.$injector.resolve("projectData"); $projectData.initializeProjectData(path.dirname(this.projectFilePath)); - for (let platform of $platformsData.platformsNames) { - let platformData: IPlatformData = $platformsData.getPlatformData(platform, $projectData); + for (const platform of $platformsData.platformsNames) { + const platformData: IPlatformData = $platformsData.getPlatformData(platform, $projectData); if (!platformData.targetedOS || (platformData.targetedOS && _.includes(platformData.targetedOS, process.platform))) { - let currentPlatformData = projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][platformData.frameworkPackageName] || {}; + const currentPlatformData = projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][platformData.frameworkPackageName] || {}; projectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][platformData.frameworkPackageName] = _.extend(currentPlatformData, await this.getVersionData(platformData.frameworkPackageName)); } - }; + } } - let dependencies = projectData.dependencies; + const dependencies = projectData.dependencies; if (!dependencies) { projectData.dependencies = Object.create(null); } // In case console is interactive and --force is not specified, do not read the version from package.json, show all available versions to the user. - let tnsCoreModulesVersionInPackageJson = this.useDefaultValue ? projectData.dependencies[constants.TNS_CORE_MODULES_NAME] : null; + const tnsCoreModulesVersionInPackageJson = this.useDefaultValue ? projectData.dependencies[constants.TNS_CORE_MODULES_NAME] : null; projectData.dependencies[constants.TNS_CORE_MODULES_NAME] = tnsCoreModulesVersionInPackageJson || (await this.getVersionData(constants.TNS_CORE_MODULES_NAME))["version"]; this.$fs.writeJson(this.projectFilePath, projectData); @@ -79,7 +79,7 @@ export class InitService implements IInitService { private get projectFilePath(): string { if (!this._projectFilePath) { - let projectDir = path.resolve(this.$options.path || "."); + const projectDir = path.resolve(this.$options.path || "."); this._projectFilePath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); } @@ -91,7 +91,7 @@ export class InitService implements IInitService { return this.$options.appid; } - let defaultAppId = this.$projectHelper.generateDefaultAppId(path.basename(path.dirname(this.projectFilePath)), constants.DEFAULT_APP_IDENTIFIER_PREFIX); + const defaultAppId = this.$projectHelper.generateDefaultAppId(path.basename(path.dirname(this.projectFilePath)), constants.DEFAULT_APP_IDENTIFIER_PREFIX); if (this.useDefaultValue) { return defaultAppId; } @@ -100,26 +100,26 @@ export class InitService implements IInitService { } private async getVersionData(packageName: string): Promise { - let latestVersion = await this.$npmInstallationManager.getLatestCompatibleVersion(packageName); + const latestVersion = await this.$npmInstallationManager.getLatestCompatibleVersion(packageName); if (this.useDefaultValue) { return this.buildVersionData(latestVersion); } - let allVersions: any = await this.$npm.view(packageName, { "versions": true }); - let versions = _.filter(allVersions, (version: string) => semver.gte(version, InitService.MIN_SUPPORTED_FRAMEWORK_VERSIONS[packageName])); + const allVersions: any = await this.$npm.view(packageName, { "versions": true }); + const versions = _.filter(allVersions, (version: string) => semver.gte(version, InitService.MIN_SUPPORTED_FRAMEWORK_VERSIONS[packageName])); if (versions.length === 1) { this.$logger.info(`Only ${versions[0]} version is available for ${packageName}.`); return this.buildVersionData(versions[0]); } - let sortedVersions = versions.sort(helpers.versionCompare).reverse(); + const sortedVersions = versions.sort(helpers.versionCompare).reverse(); //TODO: plamen5kov: don't offer versions from next (they are not available) - let version = await this.$prompter.promptForChoice(`${packageName} version:`, sortedVersions); + const version = await this.$prompter.promptForChoice(`${packageName} version:`, sortedVersions); return this.buildVersionData(version); } private buildVersionData(version: string): IStringDictionary { - let result: IStringDictionary = {}; + const result: IStringDictionary = {}; result[InitService.VERSION_KEY_NAME] = version; diff --git a/lib/services/ios-debug-service.ts b/lib/services/ios-debug-service.ts index 26105413af..a7adaf468e 100644 --- a/lib/services/ios-debug-service.ts +++ b/lib/services/ios-debug-service.ts @@ -4,7 +4,7 @@ import * as path from "path"; import * as log4js from "log4js"; import { ChildProcess } from "child_process"; import { DebugServiceBase } from "./debug-service-base"; -import { CONNECTION_ERROR_EVENT_NAME } from "../constants"; +import { CONNECTION_ERROR_EVENT_NAME, AWAIT_NOTIFICATION_TIMEOUT_SECONDS } from "../constants"; import { getPidFromiOSSimulatorLogs } from "../common/helpers"; import byline = require("byline"); @@ -13,18 +13,19 @@ const inspectorBackendPort = 18181; const inspectorAppName = "NativeScript Inspector.app"; const inspectorNpmPackageName = "tns-ios-inspector"; const inspectorUiDir = "WebInspectorUI/"; -const TIMEOUT_SECONDS = 9; -class IOSDebugService extends DebugServiceBase implements IPlatformDebugService { +export class IOSDebugService extends DebugServiceBase implements IPlatformDebugService { private _lldbProcess: ChildProcess; private _sockets: net.Socket[] = []; private _childProcess: ChildProcess; private _socketProxy: any; - constructor(private $platformService: IPlatformService, + constructor(protected device: Mobile.IDevice, + protected $devicesService: Mobile.IDevicesService, + private $platformService: IPlatformService, private $iOSEmulatorServices: Mobile.IEmulatorPlatformServices, - private $devicesService: Mobile.IDevicesService, private $childProcess: IChildProcess, + private $hostInfo: IHostInfo, private $logger: ILogger, private $errors: IErrors, private $npmInstallationManager: INpmInstallationManager, @@ -32,7 +33,7 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor, private $processService: IProcessService, private $socketProxyFactory: ISocketProxyFactory) { - super(); + super(device, $devicesService); this.$processService.attachToProcessExitSignals(this, this.debugStop); this.$socketProxyFactory.on(CONNECTION_ERROR_EVENT_NAME, (e: Error) => this.emit(CONNECTION_ERROR_EVENT_NAME, e)); } @@ -41,7 +42,7 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService return "ios"; } - public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + public debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { if (debugOptions.debugBrk && debugOptions.start) { this.$errors.failWithoutHelp("Expected exactly one of the --debug-brk or --start options."); } @@ -52,9 +53,9 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService if (debugOptions.emulator) { if (debugOptions.start) { - return [await this.emulatorStart(debugData, debugOptions)]; + return this.emulatorStart(debugData, debugOptions); } else { - return [await this.emulatorDebugBrk(debugData, debugOptions)]; + return this.emulatorDebugBrk(debugData, debugOptions); } } else { if (debugOptions.start) { @@ -94,6 +95,14 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService } } + protected getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string { + const debugOpts = _.cloneDeep(debugOptions); + debugOpts.useBundledDevTools = debugOpts.useBundledDevTools === undefined ? false : debugOpts.useBundledDevTools; + + const chromeDebugUrl = super.getChromeDebugUrl(debugOpts, port); + return chromeDebugUrl; + } + private async killProcess(childProcess: ChildProcess): Promise { if (childProcess) { return new Promise((resolve, reject) => { @@ -104,8 +113,8 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService } private async emulatorDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise { - let args = debugOptions.debugBrk ? "--nativescript-debug-brk" : "--nativescript-debug-start"; - let child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, { + const args = debugOptions.debugBrk ? "--nativescript-debug-brk" : "--nativescript-debug-start"; + const child_process = await this.$iOSEmulatorServices.runApplicationOnEmulator(debugData.pathToAppPackage, { waitForDebugger: true, captureStdin: true, args: args, @@ -113,11 +122,11 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService skipInstall: true }); - let lineStream = byline(child_process.stdout); + const lineStream = byline(child_process.stdout); this._childProcess = child_process; lineStream.on('data', (line: NodeBuffer) => { - let lineText = line.toString(); + const lineText = line.toString(); if (lineText && _.startsWith(lineText, debugData.applicationIdentifier)) { const pid = getPidFromiOSSimulatorLogs(debugData.applicationIdentifier, lineText); if (!pid) { @@ -142,14 +151,14 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService private async emulatorStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { const result = await this.wireDebuggerClient(debugData, debugOptions); - let attachRequestMessage = this.$iOSNotification.getAttachRequest(debugData.applicationIdentifier); + const attachRequestMessage = this.$iOSNotification.getAttachRequest(debugData.applicationIdentifier); - let iOSEmulator = this.$iOSEmulatorServices; + const iOSEmulator = this.$iOSEmulatorServices; await iOSEmulator.postDarwinNotification(attachRequestMessage); return result; } - private async deviceDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + private async deviceDebugBrk(debugData: IDebugData, debugOptions: IDebugOptions): Promise { await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); const action = async (device: iOSDevice.IOSDevice) => { if (device.isEmulator) { @@ -162,7 +171,7 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService justlaunch: debugOptions.justlaunch }; // we intentionally do not wait on this here, because if we did, we'd miss the AppLaunching notification - let startApplicationAction = this.$platformService.startApplication(this.platform, runOptions, debugData.applicationIdentifier); + const startApplicationAction = this.$platformService.startApplication(this.platform, runOptions, debugData.applicationIdentifier); const result = await this.debugBrkCore(device, debugData, debugOptions); @@ -171,33 +180,32 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService return result; }; - const results = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); - return _.map(results, r => r.result); + const deviceActionResult = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return deviceActionResult[0].result; } private async debugBrkCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions): Promise { - await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, TIMEOUT_SECONDS, TIMEOUT_SECONDS, debugData.applicationIdentifier, debugOptions.debugBrk); + await this.$iOSSocketRequestExecutor.executeLaunchRequest(device.deviceInfo.identifier, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, debugData.applicationIdentifier, debugOptions.debugBrk); return this.wireDebuggerClient(debugData, debugOptions, device); } - private async deviceStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + private async deviceStart(debugData: IDebugData, debugOptions: IDebugOptions): Promise { await this.$devicesService.initialize({ platform: this.platform, deviceId: debugData.deviceIdentifier }); const action = async (device: Mobile.IiOSDevice) => device.isEmulator ? await this.emulatorStart(debugData, debugOptions) : await this.deviceStartCore(device, debugData, debugOptions); - const results = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); - return _.map(results, r => r.result); + const deviceActionResult = await this.$devicesService.execute(action, this.getCanExecuteAction(debugData.deviceIdentifier)); + return deviceActionResult[0].result; } private async deviceStartCore(device: Mobile.IiOSDevice, debugData: IDebugData, debugOptions: IDebugOptions): Promise { - await this.$iOSSocketRequestExecutor.executeAttachRequest(device, TIMEOUT_SECONDS, debugData.applicationIdentifier); + await this.$iOSSocketRequestExecutor.executeAttachRequest(device, AWAIT_NOTIFICATION_TIMEOUT_SECONDS, debugData.applicationIdentifier); return this.wireDebuggerClient(debugData, debugOptions, device); } private async wireDebuggerClient(debugData: IDebugData, debugOptions: IDebugOptions, device?: Mobile.IiOSDevice): Promise { - if (debugOptions.chrome) { + if (debugOptions.chrome || !this.$hostInfo.isDarwin) { this._socketProxy = await this.$socketProxyFactory.createWebSocketProxy(this.getSocketFactory(device)); - const commitSHA = "02e6bde1bbe34e43b309d4ef774b1168d25fd024"; // corresponds to 55.0.2883 Chrome version - return `chrome-devtools://devtools/remote/serve_file/@${commitSHA}/inspector.html?experiments=true&ws=localhost:${this._socketProxy.options.port}`; + return this.getChromeDebugUrl(debugOptions, this._socketProxy.options.port); } else { this._socketProxy = await this.$socketProxyFactory.createTCPSocketProxy(this.getSocketFactory(device)); await this.openAppInspector(this._socketProxy.address(), debugData, debugOptions); @@ -207,12 +215,12 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService private async openAppInspector(fileDescriptor: string, debugData: IDebugData, debugOptions: IDebugOptions): Promise { if (debugOptions.client) { - let inspectorPath = await this.$npmInstallationManager.getInspectorFromCache(inspectorNpmPackageName, debugData.projectDir); + const inspectorPath = await this.$npmInstallationManager.getInspectorFromCache(inspectorNpmPackageName, debugData.projectDir); - let inspectorSourceLocation = path.join(inspectorPath, inspectorUiDir, "Main.html"); - let inspectorApplicationPath = path.join(inspectorPath, inspectorAppName); + const inspectorSourceLocation = path.join(inspectorPath, inspectorUiDir, "Main.html"); + const inspectorApplicationPath = path.join(inspectorPath, inspectorAppName); - let cmd = `open -a '${inspectorApplicationPath}' --args '${inspectorSourceLocation}' '${debugData.projectName}' '${fileDescriptor}'`; + const cmd = `open -a '${inspectorApplicationPath}' --args '${inspectorSourceLocation}' '${debugData.projectName}' '${fileDescriptor}'`; await this.$childProcess.exec(cmd); } else { this.$logger.info("Suppressing debugging client."); @@ -231,4 +239,4 @@ class IOSDebugService extends DebugServiceBase implements IPlatformDebugService } } -$injector.register("iOSDebugService", IOSDebugService); +$injector.register("iOSDebugService", IOSDebugService, false); diff --git a/lib/services/ios-entitlements-service.ts b/lib/services/ios-entitlements-service.ts new file mode 100644 index 0000000000..0f749e31cf --- /dev/null +++ b/lib/services/ios-entitlements-service.ts @@ -0,0 +1,72 @@ +import * as path from "path"; +import * as constants from "../constants"; +import { PlistSession } from "plist-merge-patch"; + +export class IOSEntitlementsService { + constructor(private $fs: IFileSystem, + private $logger: ILogger, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $mobileHelper: Mobile.IMobileHelper, + private $pluginsService: IPluginsService) { + } + + public static readonly DefaultEntitlementsName: string = "app.entitlements"; + + private getDefaultAppEntitlementsPath(projectData: IProjectData) : string { + const entitlementsName = IOSEntitlementsService.DefaultEntitlementsName; + const entitlementsPath = path.join(projectData.projectDir, + constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, + this.$mobileHelper.normalizePlatformName(this.$devicePlatformsConstants.iOS), + entitlementsName); + return entitlementsPath; + } + + public getPlatformsEntitlementsPath(projectData: IProjectData) : string { + return path.join(projectData.platformsDir, this.$devicePlatformsConstants.iOS.toLowerCase(), + projectData.projectName, projectData.projectName + ".entitlements"); + } + public getPlatformsEntitlementsRelativePath(projectData: IProjectData): string { + return path.join(projectData.projectName, projectData.projectName + ".entitlements"); + } + + public async merge(projectData: IProjectData): Promise { + const session = new PlistSession({ log: (txt: string) => this.$logger.trace("App.entitlements: " + txt) }); + + const projectDir = projectData.projectDir; + const makePatch = (plistPath: string) => { + if (!this.$fs.exists(plistPath)) { + this.$logger.trace("No plist found at: " + plistPath); + return; + } + + this.$logger.trace("Schedule merge plist at: " + plistPath); + session.patch({ + name: path.relative(projectDir, plistPath), + read: () => this.$fs.readText(plistPath) + }); + }; + + const allPlugins = await this.getAllInstalledPlugins(projectData); + for (const plugin of allPlugins) { + const pluginInfoPlistPath = path.join(plugin.pluginPlatformsFolderPath(this.$devicePlatformsConstants.iOS), + IOSEntitlementsService.DefaultEntitlementsName); + makePatch(pluginInfoPlistPath); + } + + const appEntitlementsPath = this.getDefaultAppEntitlementsPath(projectData); + if (this.$fs.exists(appEntitlementsPath)) { + makePatch(appEntitlementsPath); + } + + const plistContent = session.build(); + this.$logger.trace("App.entitlements: Write to: " + this.getPlatformsEntitlementsPath(projectData)); + this.$fs.writeFile(this.getPlatformsEntitlementsPath(projectData), plistContent); + return; + } + + private getAllInstalledPlugins(projectData: IProjectData): Promise { + return this.$pluginsService.getAllInstalledPlugins(projectData); + } +} + +$injector.register("iOSEntitlementsService", IOSEntitlementsService); diff --git a/lib/services/ios-log-filter.ts b/lib/services/ios-log-filter.ts index 8f105a33ca..fde1997d3a 100644 --- a/lib/services/ios-log-filter.ts +++ b/lib/services/ios-log-filter.ts @@ -1,28 +1,28 @@ -let sourcemap = require("source-map"); +const sourcemap = require("source-map"); import * as path from "path"; import { cache } from "../common/decorators"; import * as iOSLogFilterBase from "../common/mobile/ios/ios-log-filter"; export class IOSLogFilter extends iOSLogFilterBase.IOSLogFilter implements Mobile.IPlatformLogFilter { - protected infoFilterRegex = /^.*?(:.*?((CONSOLE LOG|JS ERROR).*?)|(:.*?)|(:.*?))$/im; + protected infoFilterRegex = /^.*?((?::)?.*?(((?:CONSOLE|JS) (?:LOG|ERROR)).*?))$/im; private partialLine: string = null; constructor($loggingLevels: Mobile.ILoggingLevels, private $fs: IFileSystem, private $projectData: IProjectData) { - super($loggingLevels); - } + super($loggingLevels); + } public filterData(data: string, logLevel: string, pid?: string): string { - data = super.filterData(data, logLevel, pid); + data = super.filterData(data, logLevel, pid); if (pid && data && data.indexOf(`[${pid}]`) === -1) { return null; } if (data) { - let skipLastLine = data[data.length - 1] !== "\n"; - let lines = data.split("\n"); + const skipLastLine = data[data.length - 1] !== "\n"; + const lines = data.split("\n"); let result = ""; for (let i = 0; i < lines.length; i++) { let line = lines[i]; @@ -41,8 +41,8 @@ export class IOSLogFilter extends iOSLogFilterBase.IOSLogFilter implements Mobil // This code removes unnecessary information from log messages. The output looks like: // CONSOLE LOG file:///location:row:column: if (pid) { - let searchString = "[" + pid + "]: "; - let pidIndex = line.indexOf(searchString); + const searchString = "[" + pid + "]: "; + const pidIndex = line.indexOf(searchString); if (pidIndex > 0) { line = line.substring(pidIndex + searchString.length, line.length); this.getOriginalFileLocation(line); @@ -68,17 +68,17 @@ export class IOSLogFilter extends iOSLogFilterBase.IOSLogFilter implements Mobil const projectDir = this.getProjectDir(); if (fileIndex >= 0 && projectDir) { - let parts = data.substring(fileIndex + fileString.length).split(":"); + const parts = data.substring(fileIndex + fileString.length).split(":"); if (parts.length >= 4) { - let file = parts[0]; - let sourceMapFile = path.join(projectDir, file + ".map"); - let row = parseInt(parts[1]); - let column = parseInt(parts[2]); + const file = parts[0]; + const sourceMapFile = path.join(projectDir, file + ".map"); + const row = parseInt(parts[1]); + const column = parseInt(parts[2]); if (this.$fs.exists(sourceMapFile)) { - let sourceMap = this.$fs.readText(sourceMapFile); - let smc = new sourcemap.SourceMapConsumer(sourceMap); - let originalPosition = smc.originalPositionFor({ line: row, column: column }); - let sourceFile = smc.sources.length > 0 ? file.replace(smc.file, smc.sources[0]) : file; + const sourceMap = this.$fs.readText(sourceMapFile); + const smc = new sourcemap.SourceMapConsumer(sourceMap); + const originalPosition = smc.originalPositionFor({ line: row, column: column }); + const sourceFile = smc.sources.length > 0 ? file.replace(smc.file, smc.sources[0]) : file; data = data.substring(0, fileIndex + fileString.length) + sourceFile + ":" + originalPosition.line + ":" diff --git a/lib/services/ios-notification-service.ts b/lib/services/ios-notification-service.ts deleted file mode 100644 index 36b8f6dc87..0000000000 --- a/lib/services/ios-notification-service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as constants from "../common/constants"; - -export class IOSNotificationService implements IiOSNotificationService { - constructor(private $iosDeviceOperations: IIOSDeviceOperations) { } - - public async awaitNotification(deviceIdentifier: string, socket: number, timeout: number): Promise { - const notificationResponse = await this.$iosDeviceOperations.awaitNotificationResponse([{ - deviceId: deviceIdentifier, - socket: socket, - timeout: timeout, - responseCommandType: constants.IOS_RELAY_NOTIFICATION_COMMAND_TYPE, - responsePropertyName: "Name" - }]); - - return _.first(notificationResponse[deviceIdentifier]).response; - } - - public async postNotification(deviceIdentifier: string, notification: string, commandType?: string): Promise { - commandType = commandType || constants.IOS_POST_NOTIFICATION_COMMAND_TYPE; - const response = await this.$iosDeviceOperations.postNotification([{ deviceId: deviceIdentifier, commandType: commandType, notificationName: notification }]); - return _.first(response[deviceIdentifier]).response; - } -} - -$injector.register("iOSNotificationService", IOSNotificationService); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index f68525d998..3ecfcd4162 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -2,17 +2,19 @@ import * as path from "path"; import * as shell from "shelljs"; import * as os from "os"; import * as semver from "semver"; -import * as xcode from "xcode"; import * as constants from "../constants"; import * as helpers from "../common/helpers"; import { attachAwaitDetach } from "../common/helpers"; import * as projectServiceBaseLib from "./platform-project-service-base"; -import { PlistSession } from "plist-merge-patch"; +import { PlistSession, Reporter } from "plist-merge-patch"; import { EOL } from "os"; import * as temp from "temp"; import * as plist from "plist"; -import { Xcode } from "pbxproj-dom/xcode"; import { IOSProvisionService } from "./ios-provision-service"; +import { IOSEntitlementsService } from "./ios-entitlements-service"; +import { XCConfigService } from "./xcconfig-service"; +import * as simplePlist from "simple-plist"; +import * as mobileprovision from "ios-mobileprovision-finder"; export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { private static XCODE_PROJECT_EXT_NAME = ".xcodeproj"; @@ -39,10 +41,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $devicesService: Mobile.IDevicesService, private $mobileHelper: Mobile.IMobileHelper, + private $hostInfo: IHostInfo, private $pluginVariablesService: IPluginVariablesService, private $xcprojService: IXcprojService, private $iOSProvisionService: IOSProvisionService, - private $sysInfo: ISysInfo) { + private $pbxprojDomXcode: IPbxprojDomXcode, + private $xcode: IXcode, + private $iOSEntitlementsService: IOSEntitlementsService, + private $sysInfo: ISysInfo, + private $xCConfigService: XCConfigService) { super($fs, $projectDataService); } @@ -54,7 +61,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } if (projectData && projectData.platformsDir && this._platformsDirCache !== projectData.platformsDir) { - let projectRoot = path.join(projectData.platformsDir, "ios"); + const projectRoot = path.join(projectData.platformsDir, this.$devicePlatformsConstants.iOS.toLowerCase()); this._platformData = { frameworkPackageName: "tns-ios", @@ -67,10 +74,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ emulatorBuildOutputPath: path.join(projectRoot, "build", "emulator"), getValidPackageNames: (buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[] => { if (buildOptions.isForDevice) { - return [projectData.projectName + ".ipa"]; + return [`${projectData.projectName}.ipa`]; } - return [projectData.projectName + ".app"]; + return [`${projectData.projectName}.app`, `${projectData.projectName}.zip`]; }, frameworkFilesExtensions: [".a", ".framework", ".bin"], frameworkDirectoriesExtensions: [".framework"], @@ -86,18 +93,28 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return this._platformData; } - public async validateOptions(projectId: string, provision: true | string): Promise { + public async validateOptions(projectId: string, provision: true | string, teamId: true | string): Promise { + if (provision && teamId) { + this.$errors.failWithoutHelp("The options --provision and --teamId are mutually exclusive."); + } + if (provision === true) { - await this.$iOSProvisionService.list(projectId); + await this.$iOSProvisionService.listProvisions(projectId); this.$errors.failWithoutHelp("Please provide provisioning profile uuid or name with the --provision option."); return false; } + if (teamId === true) { + await this.$iOSProvisionService.listTeams(); + this.$errors.failWithoutHelp("Please provide team id or team name with the --teamId options."); + return false; + } + return true; } public getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string { - let frameworkVersion = this.getFrameworkVersion(this.getPlatformData(projectData).frameworkPackageName, projectData.projectDir); + const frameworkVersion = this.getFrameworkVersion(this.getPlatformData(projectData).frameworkPackageName, projectData.projectDir); if (semver.lt(frameworkVersion, "1.3.0")) { return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName, "Resources", "icons"); @@ -107,27 +124,31 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } public async validate(): Promise { + if (!this.$hostInfo.isDarwin) { + return; + } + try { await this.$childProcess.exec("which xcodebuild"); } catch (error) { this.$errors.fail("Xcode is not installed. Make sure you have Xcode installed and added to your PATH"); } - let xcodeBuildVersion = await this.getXcodeVersion(); + const xcodeBuildVersion = await this.getXcodeVersion(); if (helpers.versionCompare(xcodeBuildVersion, IOSProjectService.XCODEBUILD_MIN_VERSION) < 0) { this.$errors.fail("NativeScript can only run in Xcode version %s or greater", IOSProjectService.XCODEBUILD_MIN_VERSION); } } // TODO: Remove Promise, reason: readDirectory - unable until androidProjectService has async operations. - public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, pathToTemplate?: string): Promise { + public async createProject(frameworkDir: string, frameworkVersion: string, projectData: IProjectData, config: ICreateProjectOptions): Promise { this.$fs.ensureDirectoryExists(path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER)); - if (pathToTemplate) { + if (config.pathToTemplate) { // Copy everything except the template from the runtime this.$fs.readDirectory(frameworkDir) .filter(dirName => dirName.indexOf(IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER) === -1) .forEach(dirName => shell.cp("-R", path.join(frameworkDir, dirName), this.getPlatformData(projectData).projectRoot)); - shell.cp("-rf", path.join(pathToTemplate, "*"), this.getPlatformData(projectData).projectRoot); + shell.cp("-rf", path.join(config.pathToTemplate, "*"), this.getPlatformData(projectData).projectRoot); } else { shell.cp("-R", path.join(frameworkDir, "*"), this.getPlatformData(projectData).projectRoot); } @@ -136,7 +157,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ //TODO: plamen5kov: revisit this method, might have unnecessary/obsolete logic public async interpolateData(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - let projectRootFilePath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER); + const projectRootFilePath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER); // Starting with NativeScript for iOS 1.6.0, the project Info.plist file resides not in the platform project, // but in the hello-world app template as a platform specific resource. if (this.$fs.exists(path.join(projectRootFilePath, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + "-Info.plist"))) { @@ -144,8 +165,8 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } this.replaceFileName("-Prefix.pch", projectRootFilePath, projectData); - let xcschemeDirPath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + IOSProjectService.XCODE_PROJECT_EXT_NAME, "xcshareddata/xcschemes"); - let xcschemeFilePath = path.join(xcschemeDirPath, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + IOSProjectService.XCODE_SCHEME_EXT_NAME); + const xcschemeDirPath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + IOSProjectService.XCODE_PROJECT_EXT_NAME, "xcshareddata/xcschemes"); + const xcschemeFilePath = path.join(xcschemeDirPath, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + IOSProjectService.XCODE_SCHEME_EXT_NAME); if (this.$fs.exists(xcschemeFilePath)) { this.$logger.debug("Found shared scheme at xcschemeFilePath, renaming to match project name."); @@ -160,7 +181,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.replaceFileName(IOSProjectService.XCODE_PROJECT_EXT_NAME, this.getPlatformData(projectData).projectRoot, projectData); - let pbxprojFilePath = this.getPbxProjPath(projectData); + const pbxprojFilePath = this.getPbxProjPath(projectData); this.replaceFileContent(pbxprojFilePath, projectData); } @@ -178,24 +199,24 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ * Returns the path to the .xcarchive. */ public async archive(projectData: IProjectData, buildConfig?: IBuildConfig, options?: { archivePath?: string }): Promise { - let projectRoot = this.getPlatformData(projectData).projectRoot; - let archivePath = options && options.archivePath ? path.resolve(options.archivePath) : path.join(projectRoot, "/build/archive/", projectData.projectName + ".xcarchive"); - let args = ["archive", "-archivePath", archivePath, "-configuration", + const projectRoot = this.getPlatformData(projectData).projectRoot; + const archivePath = options && options.archivePath ? path.resolve(options.archivePath) : path.join(projectRoot, "/build/archive/", projectData.projectName + ".xcarchive"); + const args = ["archive", "-archivePath", archivePath, "-configuration", (!buildConfig || buildConfig.release) ? "Release" : "Debug"] .concat(this.xcbuildProjectArgs(projectRoot, projectData, "scheme")); - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", { stdio: 'inherit' }); + await this.xcodebuild(args, projectRoot, buildConfig && buildConfig.buildOutputStdio); return archivePath; } /** * Exports .xcarchive for AppStore distribution. */ - public async exportArchive(projectData: IProjectData, options: { archivePath: string, exportDir?: string, teamID?: string }): Promise { - let projectRoot = this.getPlatformData(projectData).projectRoot; - let archivePath = options.archivePath; + public async exportArchive(projectData: IProjectData, options: { archivePath: string, exportDir?: string, teamID?: string, provision?: string }): Promise { + const projectRoot = this.getPlatformData(projectData).projectRoot; + const archivePath = options.archivePath; // The xcodebuild exportPath expects directory and writes the .ipa at that directory. - let exportPath = path.resolve(options.exportDir || path.join(projectRoot, "/build/archive")); - let exportFile = path.join(exportPath, projectData.projectName + ".ipa"); + const exportPath = path.resolve(options.exportDir || path.join(projectRoot, "/build/archive")); + const exportFile = path.join(exportPath, projectData.projectName + ".ipa"); // These are the options that you can set in the Xcode UI when exporting for AppStore deployment. let plistTemplate = ` @@ -207,6 +228,13 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ plistTemplate += ` teamID ${options.teamID} `; + } + if (options && options.provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectId} + ${options.provision} + `; } plistTemplate += ` method app-store @@ -219,50 +247,74 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ // Save the options... temp.track(); - let exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); + const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); this.$fs.writeFile(exportOptionsPlist, plistTemplate); - let args = ["-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ]; - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", { stdio: 'inherit' }); - + await this.xcodebuild( + [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", exportPath, + "-exportOptionsPlist", exportOptionsPlist + ], + projectRoot); return exportFile; } /** * Exports .xcarchive for a development device. */ - private async exportDevelopmentArchive(projectData: IProjectData, buildConfig: IBuildConfig, options: { archivePath: string, exportDir?: string, teamID?: string }): Promise { - let platformData = this.getPlatformData(projectData); - let projectRoot = platformData.projectRoot; - let archivePath = options.archivePath; - let buildOutputPath = path.join(projectRoot, "build", "device"); - - // The xcodebuild exportPath expects directory and writes the .ipa at that directory. - let exportPath = path.resolve(options.exportDir || buildOutputPath); - let exportFile = path.join(exportPath, projectData.projectName + ".ipa"); + private async exportDevelopmentArchive(projectData: IProjectData, buildConfig: IBuildConfig, options: { archivePath: string, exportDir?: string, teamID?: string, provision?: string }): Promise { + const platformData = this.getPlatformData(projectData); + const projectRoot = platformData.projectRoot; + const archivePath = options.archivePath; + const buildOutputPath = path.join(projectRoot, "build", "device"); + const exportOptionsMethod = await this.getExportOptionsMethod(projectData); + let plistTemplate = ` + + + + method + ${exportOptionsMethod}`; + if (options && options.provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectId} + ${options.provision} +`; + } + plistTemplate += ` + uploadBitcode + + +`; - let args = ["-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", platformData.configurationFilePath - ]; - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", - { stdio: buildConfig.buildOutputStdio || 'inherit', cwd: this.getPlatformData(projectData).projectRoot }, - { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); + // Save the options... + temp.track(); + const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); + this.$fs.writeFile(exportOptionsPlist, plistTemplate); + // The xcodebuild exportPath expects directory and writes the .ipa at that directory. + const exportPath = path.resolve(options.exportDir || buildOutputPath); + const exportFile = path.join(exportPath, projectData.projectName + ".ipa"); + + await this.xcodebuild( + [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", exportPath, + "-exportOptionsPlist", exportOptionsPlist + ], + projectRoot, buildConfig.buildOutputStdio); return exportFile; } private xcbuildProjectArgs(projectRoot: string, projectData: IProjectData, product?: "scheme" | "target"): string[] { - let xcworkspacePath = path.join(projectRoot, projectData.projectName + ".xcworkspace"); + const xcworkspacePath = path.join(projectRoot, projectData.projectName + ".xcworkspace"); if (this.$fs.exists(xcworkspacePath)) { return ["-workspace", xcworkspacePath, product ? "-" + product : "-scheme", projectData.projectName]; } else { - let xcodeprojPath = path.join(projectRoot, projectData.projectName + ".xcodeproj"); + const xcodeprojPath = path.join(projectRoot, projectData.projectName + ".xcodeproj"); return ["-project", xcodeprojPath, product ? "-" + product : "-target", projectData.projectName]; } } @@ -276,7 +328,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ basicArgs = basicArgs.concat(this.xcbuildProjectArgs(projectRoot, projectData)); // Starting from tns-ios 1.4 the xcconfig file is referenced in the project template - let frameworkVersion = this.getFrameworkVersion(this.getPlatformData(projectData).frameworkPackageName, projectData.projectDir); + const frameworkVersion = this.getFrameworkVersion(this.getPlatformData(projectData).frameworkPackageName, projectData.projectDir); if (semver.lt(frameworkVersion, "1.4.0")) { basicArgs.push("-xcconfig", path.join(projectRoot, projectData.projectName, "build.xcconfig")); } @@ -303,13 +355,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ handler, this.buildForSimulator(projectRoot, basicArgs, projectData, buildConfig.buildOutputStdio)); } + + this.validateApplicationIdentifier(projectData); } public async validatePlugins(projectData: IProjectData): Promise { - let installedPlugins = await (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); - for (let pluginData of installedPlugins) { - let pluginsFolderExists = this.$fs.exists(path.join(pluginData.pluginPlatformsFolderPath(this.$devicePlatformsConstants.iOS.toLowerCase()), "Podfile")); - let cocoaPodVersion = await this.$sysInfo.getCocoapodVersion(); + const installedPlugins = await (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); + for (const pluginData of installedPlugins) { + const pluginsFolderExists = this.$fs.exists(path.join(pluginData.pluginPlatformsFolderPath(this.$devicePlatformsConstants.iOS.toLowerCase()), "Podfile")); + const cocoaPodVersion = await this.$sysInfo.getCocoapodVersion(); if (pluginsFolderExists && !cocoaPodVersion) { this.$errors.failWithoutHelp(`${pluginData.name} has Podfile and you don't have Cocoapods installed or it is not configured correctly. Please verify Cocoapods can work on your machine.`); } @@ -318,22 +372,25 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private async buildForDevice(projectRoot: string, args: string[], buildConfig: IBuildConfig, projectData: IProjectData): Promise { - let defaultArchitectures = [ + const defaultArchitectures = [ 'ARCHS=armv7 arm64', 'VALID_ARCHS=armv7 arm64' ]; // build only for device specific architecture if (!buildConfig.release && !buildConfig.architectures) { - await this.$devicesService.initialize({ platform: this.$devicePlatformsConstants.iOS.toLowerCase(), deviceId: buildConfig.device }); - let instances = this.$devicesService.getDeviceInstances(); - let devicesArchitectures = _(instances) + await this.$devicesService.initialize({ + platform: this.$devicePlatformsConstants.iOS.toLowerCase(), deviceId: buildConfig.device, + skipEmulatorStart: true + }); + const instances = this.$devicesService.getDeviceInstances(); + const devicesArchitectures = _(instances) .filter(d => this.$mobileHelper.isiOSPlatform(d.deviceInfo.platform) && d.deviceInfo.activeArchitecture) .map(d => d.deviceInfo.activeArchitecture) .uniq() .value(); if (devicesArchitectures.length > 0) { - let architectures = [ + const architectures = [ `ARCHS=${devicesArchitectures.join(" ")}`, `VALID_ARCHS=${devicesArchitectures.join(" ")}` ]; @@ -351,44 +408,78 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ "CONFIGURATION_BUILD_DIR=" + path.join(projectRoot, "build", "device") ]); - let xcodeBuildVersion = await this.getXcodeVersion(); + const xcodeBuildVersion = await this.getXcodeVersion(); if (helpers.versionCompare(xcodeBuildVersion, "8.0") >= 0) { await this.setupSigningForDevice(projectRoot, buildConfig, projectData); } - if (buildConfig && buildConfig.codeSignIdentity) { - args.push(`CODE_SIGN_IDENTITY=${buildConfig.codeSignIdentity}`); - } + await this.xcodebuild(args, projectRoot, buildConfig.buildOutputStdio); + await this.createIpa(projectRoot, projectData, buildConfig); + } - if (buildConfig && buildConfig.mobileProvisionIdentifier) { - args.push(`PROVISIONING_PROFILE=${buildConfig.mobileProvisionIdentifier}`); + private async xcodebuild(args: string[], cwd: string, stdio: any = "inherit"): Promise { + const localArgs = [...args]; + const xcodeBuildVersion = await this.getXcodeVersion(); + try { + if (helpers.versionCompare(xcodeBuildVersion, "9.0") >= 0) { + localArgs.push("-allowProvisioningUpdates"); + } + } catch (e) { + this.$logger.warn("Failed to detect whether -allowProvisioningUpdates can be used with your xcodebuild version due to error: " + e); } - - if (buildConfig && buildConfig.teamIdentifier) { - args.push(`DEVELOPMENT_TEAM=${buildConfig.teamIdentifier}`); + if (this.$logger.getLevel() === "INFO") { + localArgs.push("-quiet"); + this.$logger.info("Xcode build..."); } - - // this.$logger.out("xcodebuild..."); - await this.$childProcess.spawnFromEvent("xcodebuild", - args, + return this.$childProcess.spawnFromEvent("xcodebuild", + localArgs, "exit", - { stdio: buildConfig.buildOutputStdio || "inherit", cwd: this.getPlatformData(projectData).projectRoot }, + { stdio: stdio || "inherit", cwd }, { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); - // this.$logger.out("xcodebuild build succeded."); + } - await this.createIpa(projectRoot, projectData, buildConfig); + private async setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string) { + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); + const signing = xcode.getSigning(projectData.projectName); + + let shouldUpdateXcode = false; + if (signing && signing.style === "Automatic") { + if (signing.team !== teamId) { + // Maybe the provided team is name such as "Telerik AD" and we need to convert it to CH******37 + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (!teamIdsForName.some(id => id === signing.team)) { + shouldUpdateXcode = true; + } + } + } else { + shouldUpdateXcode = true; + } + + if (shouldUpdateXcode) { + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (teamIdsForName.length > 0) { + this.$logger.trace(`Team id ${teamIdsForName[0]} will be used for team name "${teamId}".`); + teamId = teamIdsForName[0]; + } + + xcode.setAutomaticSigningStyle(projectData.projectName, teamId); + xcode.save(); + + this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`); + } else { + this.$logger.trace(`The specified ${teamId} is already set in the Xcode.`); + } } - private async setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: any): Promise { + private async setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: mobileprovision.provision.MobileProvision): Promise { if (provision) { - const pbxprojPath = path.join(projectRoot, projectData.projectName + ".xcodeproj", "project.pbxproj"); - const xcode = Xcode.open(pbxprojPath); + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); const signing = xcode.getSigning(projectData.projectName); let shouldUpdateXcode = false; if (signing && signing.style === "Manual") { - for (let config in signing.configurations) { - let options = signing.configurations[config]; + for (const config in signing.configurations) { + const options = signing.configurations[config]; if (options.name !== provision && options.uuid !== provision) { shouldUpdateXcode = true; break; @@ -399,10 +490,8 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } if (shouldUpdateXcode) { - // This is slow, it read through 260 mobileprovision files on my machine and does quite some checking whether provisioning profiles and devices will match. - // That's why we try to avoid id by checking in the Xcode first. const pickStart = Date.now(); - const mobileprovision = await this.$iOSProvisionService.pick(provision, projectData.projectId); + const mobileprovision = mobileProvisionData || await this.$iOSProvisionService.pick(provision, projectData.projectId); const pickEnd = Date.now(); this.$logger.trace("Searched and " + (mobileprovision ? "found" : "failed to find ") + " matching provisioning profile. (" + (pickEnd - pickStart) + "ms.)"); if (!mobileprovision) { @@ -418,9 +507,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ xcode.save(); // this.cache(uuid); - this.$logger.trace("Set Manual signing style and provisioning profile."); + this.$logger.trace(`Set Manual signing style and provisioning profile: ${mobileprovision.Name} (${mobileprovision.UUID})`); } else { - this.$logger.trace("The specified provisioning profile allready set in the Xcode."); + this.$logger.trace(`The specified provisioning profile is already set in the Xcode: ${provision}`); } } else { // read uuid from Xcode and cache... @@ -428,23 +517,21 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private async setupSigningForDevice(projectRoot: string, buildConfig: IiOSBuildConfig, projectData: IProjectData): Promise { - const pbxprojPath = path.join(projectRoot, projectData.projectName + ".xcodeproj", "project.pbxproj"); - const xcode = Xcode.open(pbxprojPath); + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); const signing = xcode.getSigning(projectData.projectName); - if ((this.readXCConfigProvisioningProfile(projectData) || this.readXCConfigProvisioningProfileForIPhoneOs(projectData)) && (!signing || signing.style !== "Manual")) { + const hasProvisioningProfileInXCConfig = + this.readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData) || + this.readXCConfigProvisioningProfileSpecifier(projectData) || + this.readXCConfigProvisioningProfileForIPhoneOs(projectData) || + this.readXCConfigProvisioningProfile(projectData); + + if (hasProvisioningProfileInXCConfig && (!signing || signing.style !== "Manual")) { xcode.setManualSigningStyle(projectData.projectName); xcode.save(); } else if (!buildConfig.provision && !(signing && signing.style === "Manual" && !buildConfig.teamId)) { - if (buildConfig) { - delete buildConfig.teamIdentifier; - } - const teamId = await this.getDevelopmentTeam(projectData, buildConfig.teamId); - - xcode.setAutomaticSigningStyle(projectData.projectName, teamId); - xcode.save(); - this.$logger.trace("Set Automatic signing style and team."); + await this.setupSigningFromTeam(projectRoot, projectData, teamId); } } @@ -457,20 +544,12 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ "CONFIGURATION_BUILD_DIR=" + path.join(projectRoot, "build", "emulator"), "CODE_SIGN_IDENTITY=" ]); - - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", - { stdio: buildOutputStdio || "inherit", cwd: this.getPlatformData(projectData).projectRoot }, - { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); + await this.xcodebuild(args, projectRoot, buildOutputStdio); } private async createIpa(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { - let xarchivePath = await this.archive(projectData, buildConfig); - let exportFileIpa = await this.exportDevelopmentArchive(projectData, - buildConfig, - { - archivePath: xarchivePath, - }); - + const archivePath = await this.archive(projectData, buildConfig); + const exportFileIpa = await this.exportDevelopmentArchive(projectData, buildConfig, { archivePath, provision: buildConfig.provision || buildConfig.mobileProvisionIdentifier }); return exportFileIpa; } @@ -483,37 +562,37 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private async addFramework(frameworkPath: string, projectData: IProjectData): Promise { - await this.validateFramework(frameworkPath); + if (!this.$hostInfo.isWindows) { + this.validateFramework(frameworkPath); - let project = this.createPbxProj(projectData); - let frameworkName = path.basename(frameworkPath, path.extname(frameworkPath)); - let frameworkBinaryPath = path.join(frameworkPath, frameworkName); - let isDynamic = _.includes((await this.$childProcess.spawnFromEvent("otool", ["-Vh", frameworkBinaryPath], "close")).stdout, " DYLIB "); + const project = this.createPbxProj(projectData); + const frameworkName = path.basename(frameworkPath, path.extname(frameworkPath)); + const frameworkBinaryPath = path.join(frameworkPath, frameworkName); + const isDynamic = _.includes((await this.$childProcess.spawnFromEvent("file", [frameworkBinaryPath], "close")).stdout, "dynamically linked"); + const frameworkAddOptions: IXcode.Options = { customFramework: true }; - let frameworkAddOptions: xcode.Options = { customFramework: true }; + if (isDynamic) { + frameworkAddOptions["embed"] = true; + } - if (isDynamic) { - frameworkAddOptions["embed"] = true; + const frameworkRelativePath = '$(SRCROOT)/' + this.getLibSubpathRelativeToProjectPath(frameworkPath, projectData); + project.addFramework(frameworkRelativePath, frameworkAddOptions); + this.savePbxProj(project, projectData); } - - let frameworkRelativePath = '$(SRCROOT)/' + this.getLibSubpathRelativeToProjectPath(frameworkPath, projectData); - project.addFramework(frameworkRelativePath, frameworkAddOptions); - this.savePbxProj(project, projectData); - } private async addStaticLibrary(staticLibPath: string, projectData: IProjectData): Promise { await this.validateStaticLibrary(staticLibPath); // Copy files to lib folder. - let libraryName = path.basename(staticLibPath, ".a"); - let headersSubpath = path.join(path.dirname(staticLibPath), "include", libraryName); + const libraryName = path.basename(staticLibPath, ".a"); + const headersSubpath = path.join(path.dirname(staticLibPath), "include", libraryName); // Add static library to project file and setup header search paths - let project = this.createPbxProj(projectData); - let relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(staticLibPath, projectData); + const project = this.createPbxProj(projectData); + const relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(staticLibPath, projectData); project.addFramework(relativeStaticLibPath); - let relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath, projectData)); + const relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath, projectData)); project.addToHeaderSearchPaths({ relativePath: relativeHeaderSearchPath }); this.generateModulemap(headersSubpath, libraryName); @@ -521,14 +600,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } public canUpdatePlatform(installedModuleDir: string, projectData: IProjectData): boolean { - let currentXcodeProjectFile = this.buildPathToCurrentXcodeProjectFile(projectData); - let currentXcodeProjectFileContent = this.$fs.readFile(currentXcodeProjectFile); + const currentXcodeProjectFile = this.buildPathToCurrentXcodeProjectFile(projectData); + const currentXcodeProjectFileContent = this.$fs.readFile(currentXcodeProjectFile); - let newXcodeProjectFile = this.buildPathToNewXcodeProjectFile(installedModuleDir); + const newXcodeProjectFile = this.buildPathToNewXcodeProjectFile(installedModuleDir); this.replaceFileContent(newXcodeProjectFile, projectData); - let newXcodeProjectFileContent = this.$fs.readFile(newXcodeProjectFile); + const newXcodeProjectFileContent = this.$fs.readFile(newXcodeProjectFile); - let contentIsTheSame = currentXcodeProjectFileContent.toString() === newXcodeProjectFileContent.toString(); + const contentIsTheSame = currentXcodeProjectFileContent.toString() === newXcodeProjectFileContent.toString(); if (!contentIsTheSame) { this.$logger.warn(`The content of the current project file: ${currentXcodeProjectFile} and the new project file: ${newXcodeProjectFile} is different.`); @@ -549,18 +628,18 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private provideLaunchScreenIfMissing(projectData: IProjectData): void { try { this.$logger.trace("Checking if we need to provide compatability LaunchScreen.xib"); - let platformData = this.getPlatformData(projectData); - let projectPath = path.join(platformData.projectRoot, projectData.projectName); - let projectPlist = this.getInfoPlistPath(projectData); - let plistContent = plist.parse(this.$fs.readText(projectPlist)); - let storyName = plistContent["UILaunchStoryboardName"]; + const platformData = this.getPlatformData(projectData); + const projectPath = path.join(platformData.projectRoot, projectData.projectName); + const projectPlist = this.getInfoPlistPath(projectData); + const plistContent = plist.parse(this.$fs.readText(projectPlist)); + const storyName = plistContent["UILaunchStoryboardName"]; this.$logger.trace(`Examining ${projectPlist} UILaunchStoryboardName: "${storyName}".`); if (storyName !== "LaunchScreen") { this.$logger.trace("The project has its UILaunchStoryboardName set to " + storyName + " which is not the pre v2.1.0 default LaunchScreen, probably the project is migrated so we are good to go."); return; } - let expectedStoryPath = path.join(projectPath, "Resources", "LaunchScreen.storyboard"); + const expectedStoryPath = path.join(projectPath, "Resources", "LaunchScreen.storyboard"); if (this.$fs.exists(expectedStoryPath)) { // Found a LaunchScreen on expected path this.$logger.trace("LaunchScreen.storyboard was found. Project is up to date."); @@ -568,28 +647,28 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } this.$logger.trace("LaunchScreen file not found at: " + expectedStoryPath); - let expectedXibPath = path.join(projectPath, "en.lproj", "LaunchScreen.xib"); + const expectedXibPath = path.join(projectPath, "en.lproj", "LaunchScreen.xib"); if (this.$fs.exists(expectedXibPath)) { this.$logger.trace("Obsolete LaunchScreen.xib was found. It'k OK, we are probably running with iOS runtime from pre v2.1.0."); return; } this.$logger.trace("LaunchScreen file not found at: " + expectedXibPath); - let isTheLaunchScreenFile = (fileName: string) => fileName === "LaunchScreen.xib" || fileName === "LaunchScreen.storyboard"; - let matches = this.$fs.enumerateFilesInDirectorySync(projectPath, isTheLaunchScreenFile, { enumerateDirectories: false }); + const isTheLaunchScreenFile = (fileName: string) => fileName === "LaunchScreen.xib" || fileName === "LaunchScreen.storyboard"; + const matches = this.$fs.enumerateFilesInDirectorySync(projectPath, isTheLaunchScreenFile, { enumerateDirectories: false }); if (matches.length > 0) { this.$logger.trace("Found LaunchScreen by slowly traversing all files here: " + matches + "\nConsider moving the LaunchScreen so it could be found at: " + expectedStoryPath); return; } - let compatabilityXibPath = path.join(projectPath, "Resources", "LaunchScreen.xib"); + const compatabilityXibPath = path.join(projectPath, "Resources", "LaunchScreen.xib"); this.$logger.warn(`Failed to find LaunchScreen.storyboard but it was specified in the Info.plist. Consider updating the resources in app/App_Resources/iOS/. A good starting point would be to create a new project and diff the changes with your current one. Also the following repo may be helpful: https://github.com/NativeScript/template-hello-world/tree/master/App_Resources/iOS We will now place an empty obsolete compatability white screen LauncScreen.xib for you in ${path.relative(projectData.projectDir, compatabilityXibPath)} so your app may appear as it did in pre v2.1.0 versions of the ios runtime.`); - let content = ` + const content = ` @@ -619,35 +698,39 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public async prepareProject(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - let provision = platformSpecificData && platformSpecificData.provision; + const projectRoot = path.join(projectData.platformsDir, "ios"); + const provision = platformSpecificData && platformSpecificData.provision; + const teamId = platformSpecificData && platformSpecificData.teamId; if (provision) { - let projectRoot = path.join(projectData.platformsDir, "ios"); - await this.setupSigningFromProvision(projectRoot, provision); + await this.setupSigningFromProvision(projectRoot, projectData, provision, platformSpecificData.mobileProvisionData); + } + if (teamId) { + await this.setupSigningFromTeam(projectRoot, projectData, teamId); } - let project = this.createPbxProj(projectData); + const project = this.createPbxProj(projectData); this.provideLaunchScreenIfMissing(projectData); - let resources = project.pbxGroupByName("Resources"); + const resources = project.pbxGroupByName("Resources"); if (resources) { - let references = project.pbxFileReferenceSection(); + const references = project.pbxFileReferenceSection(); - let xcodeProjectImages = _.map(resources.children, resource => this.replace(references[resource.value].name)); + const xcodeProjectImages = _.map(resources.children, resource => this.replace(references[resource.value].name)); this.$logger.trace("Images from Xcode project"); this.$logger.trace(xcodeProjectImages); - let appResourcesImages = this.$fs.readDirectory(this.getAppResourcesDestinationDirectoryPath(projectData)); + const appResourcesImages = this.$fs.readDirectory(this.getAppResourcesDestinationDirectoryPath(projectData)); this.$logger.trace("Current images from App_Resources"); this.$logger.trace(appResourcesImages); - let imagesToAdd = _.difference(appResourcesImages, xcodeProjectImages); + const imagesToAdd = _.difference(appResourcesImages, xcodeProjectImages); this.$logger.trace(`New images to add into xcode project: ${imagesToAdd.join(", ")}`); _.each(imagesToAdd, image => project.addResourceFile(path.relative(this.getPlatformData(projectData).projectRoot, path.join(this.getAppResourcesDestinationDirectoryPath(projectData), image)))); - let imagesToRemove = _.difference(xcodeProjectImages, appResourcesImages); + const imagesToRemove = _.difference(xcodeProjectImages, appResourcesImages); this.$logger.trace(`Images to remove from xcode project: ${imagesToRemove.join(", ")}`); _.each(imagesToRemove, image => project.removeResourceFile(path.join(this.getAppResourcesDestinationDirectoryPath(projectData), image))); @@ -656,8 +739,8 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void { - let platformFolder = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); - let filterFile = (filename: string) => this.$fs.deleteFile(path.join(platformFolder, filename)); + const platformFolder = path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName); + const filterFile = (filename: string) => this.$fs.deleteFile(path.join(platformFolder, filename)); filterFile(this.getPlatformData(projectData).configurationFileName); @@ -665,9 +748,10 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public async processConfigurationFilesFromAppResources(release: boolean, projectData: IProjectData): Promise { - await this.mergeInfoPlists(projectData); + await this.mergeInfoPlists({ release }, projectData); + await this.$iOSEntitlementsService.merge(projectData); await this.mergeProjectXcconfigFiles(release, projectData); - for (let pluginData of await this.getAllInstalledPlugins(projectData)) { + for (const pluginData of await this.getAllInstalledPlugins(projectData)) { await this.$pluginVariablesService.interpolatePluginVariables(pluginData, this.getPlatformData(projectData).configurationFilePath, projectData); } @@ -696,9 +780,9 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return Promise.resolve(); } - private async mergeInfoPlists(projectData: IProjectData): Promise { - let projectDir = projectData.projectDir; - let infoPlistPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); + private async mergeInfoPlists(buildOptions: IRelease, projectData: IProjectData): Promise { + const projectDir = projectData.projectDir; + const infoPlistPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); this.ensureConfigurationFileInAppResources(); if (!this.$fs.exists(infoPlistPath)) { @@ -706,8 +790,14 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return; } - let session = new PlistSession({ log: (txt: string) => this.$logger.trace("Info.plist: " + txt) }); - let makePatch = (plistPath: string) => { + const reporterTraceMessage = "Info.plist:"; + const reporter: Reporter = { + log: (txt: string) => this.$logger.trace(`${reporterTraceMessage} ${txt}`), + warn: (txt: string) => this.$logger.warn(`${reporterTraceMessage} ${txt}`) + }; + + const session = new PlistSession(reporter); + const makePatch = (plistPath: string) => { if (!this.$fs.exists(plistPath)) { this.$logger.trace("No plist found at: " + plistPath); return; @@ -720,9 +810,9 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f }); }; - let allPlugins = await this.getAllInstalledPlugins(projectData); - for (let plugin of allPlugins) { - let pluginInfoPlistPath = path.join(plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME), this.getPlatformData(projectData).configurationFileName); + const allPlugins = await this.getAllInstalledPlugins(projectData); + for (const plugin of allPlugins) { + const pluginInfoPlistPath = path.join(plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME), this.getPlatformData(projectData).configurationFileName); makePatch(pluginInfoPlistPath); } @@ -737,13 +827,37 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f CFBundleIdentifier - ${ projectData.projectId} + ${projectData.projectId} + + ` + }); + } + + if (!buildOptions.release && projectData.projectId) { + session.patch({ + name: "CFBundleURLTypes from package.json nativescript.id", + read: () => + ` + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ${projectData.projectId.replace(/[^A-Za-z0-9]/g, "")} + + + ` }); } - let plistContent = session.build(); + const plistContent = session.build(); this.$logger.trace("Info.plist: Write to: " + this.getPlatformData(projectData).configurationFilePath); this.$fs.writeFile(this.getPlatformData(projectData).configurationFilePath, plistContent); @@ -778,7 +892,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private getLibSubpathRelativeToProjectPath(targetPath: string, projectData: IProjectData): string { - let frameworkPath = path.relative(this.getPlatformData(projectData).projectRoot, targetPath); + const frameworkPath = path.relative(this.getPlatformData(projectData).projectRoot, targetPath); return frameworkPath; } @@ -787,7 +901,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private createPbxProj(projectData: IProjectData): any { - let project = new xcode.project(this.getPbxProjPath(projectData)); + const project = new this.$xcode.project(this.getPbxProjPath(projectData)); project.parseSync(); return project; @@ -798,7 +912,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData, opts?: any): Promise { - let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); + const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); await this.prepareFrameworks(pluginPlatformsFolderPath, pluginData, projectData); await this.prepareStaticLibs(pluginPlatformsFolderPath, pluginData, projectData); @@ -806,7 +920,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public async removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise { - let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); + const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); this.removeFrameworks(pluginPlatformsFolderPath, pluginData, projectData); this.removeStaticLibs(pluginPlatformsFolderPath, pluginData, projectData); @@ -815,24 +929,23 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f public async afterPrepareAllPlugins(projectData: IProjectData): Promise { if (this.$fs.exists(this.getProjectPodFilePath(projectData))) { - let projectPodfileContent = this.$fs.readText(this.getProjectPodFilePath(projectData)); + const projectPodfileContent = this.$fs.readText(this.getProjectPodFilePath(projectData)); this.$logger.trace("Project Podfile content"); this.$logger.trace(projectPodfileContent); - let firstPostInstallIndex = projectPodfileContent.indexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME); + const firstPostInstallIndex = projectPodfileContent.indexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME); if (firstPostInstallIndex !== -1 && firstPostInstallIndex !== projectPodfileContent.lastIndexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME)) { this.$cocoapodsService.mergePodfileHookContent(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME, this.getProjectPodFilePath(projectData)); } - let xcuserDataPath = path.join(this.getXcodeprojPath(projectData), "xcuserdata"); - let sharedDataPath = path.join(this.getXcodeprojPath(projectData), "xcshareddata"); + const xcuserDataPath = path.join(this.getXcodeprojPath(projectData), "xcuserdata"); + const sharedDataPath = path.join(this.getXcodeprojPath(projectData), "xcshareddata"); if (!this.$fs.exists(xcuserDataPath) && !this.$fs.exists(sharedDataPath)) { this.$logger.info("Creating project scheme..."); - await this.checkIfXcodeprojIsRequired(); - let createSchemeRubyScript = `ruby -e "require 'xcodeproj'; xcproj = Xcodeproj::Project.open('${projectData.projectName}.xcodeproj'); xcproj.recreate_user_schemes; xcproj.save"`; + const createSchemeRubyScript = `ruby -e "require 'xcodeproj'; xcproj = Xcodeproj::Project.open('${projectData.projectName}.xcodeproj'); xcproj.recreate_user_schemes; xcproj.save"`; await this.$childProcess.exec(createSchemeRubyScript, { cwd: this.getPlatformData(projectData).projectRoot }); } @@ -844,10 +957,57 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return Promise.resolve(); } + public async checkForChanges(changesInfo: IProjectChangesInfo, {provision, teamId}: IProjectChangesOptions, projectData: IProjectData): Promise { + const hasProvision = provision !== undefined; + const hasTeamId = teamId !== undefined; + if (hasProvision || hasTeamId) { + // Check if the native project's signing is set to the provided provision... + const pbxprojPath = this.getPbxProjPath(projectData); + + if (this.$fs.exists(pbxprojPath)) { + const xcode = this.$pbxprojDomXcode.Xcode.open(pbxprojPath); + const signing = xcode.getSigning(projectData.projectName); + + if (hasProvision) { + if (signing && signing.style === "Manual") { + for (const name in signing.configurations) { + const config = signing.configurations[name]; + if (config.uuid !== provision && config.name !== provision) { + changesInfo.signingChanged = true; + break; + } + } + } else { + // Specifying provisioning profile requires "Manual" signing style. + // If the current signing style was not "Manual" it was probably "Automatic" or, + // it was not uniform for the debug and release build configurations. + changesInfo.signingChanged = true; + } + } + if (hasTeamId) { + if (signing && signing.style === "Automatic") { + if (signing.team !== teamId) { + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (!teamIdsForName.some(id => id === signing.team)) { + changesInfo.signingChanged = true; + } + } + } else { + // Specifying team id or name requires "Automatic" signing style. + // If the current signing style was not "Automatic" it was probably "Manual". + changesInfo.signingChanged = true; + } + } + } else { + changesInfo.signingChanged = true; + } + } + } + private getAllLibsForPluginWithFileExtension(pluginData: IPluginData, fileExtension: string): string[] { - let filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === fileExtension; + const filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === fileExtension; return this.getAllNativeLibrariesForPlugin(pluginData, IOSProjectService.IOS_PLATFORM_NAME, filterCallback); - }; + } private buildPathToCurrentXcodeProjectFile(projectData: IProjectData): string { return path.join(projectData.platformsDir, "ios", `${projectData.projectName}.xcodeproj`, "project.pbxproj"); @@ -857,17 +1017,18 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return path.join(newModulesDir, constants.PROJECT_FRAMEWORK_FOLDER_NAME, `${IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER}.xcodeproj`, "project.pbxproj"); } - private async validateFramework(libraryPath: string): Promise { - let infoPlistPath = path.join(libraryPath, "Info.plist"); + private validateFramework(libraryPath: string): void { + const infoPlistPath = path.join(libraryPath, "Info.plist"); if (!this.$fs.exists(infoPlistPath)) { this.$errors.failWithoutHelp("The bundle at %s does not contain an Info.plist file.", libraryPath); } - let packageType = (await this.$childProcess.spawnFromEvent("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundlePackageType", infoPlistPath], "close")).stdout.trim(); + const plistJson = simplePlist.readFileSync(infoPlistPath); + const packageType = plistJson["CFBundlePackageType"]; + if (packageType !== "FMWK") { this.$errors.failWithoutHelp("The bundle at %s does not appear to be a dynamic framework.", libraryPath); } - } private async validateStaticLibrary(libraryPath: string): Promise { @@ -875,8 +1036,8 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$errors.failWithoutHelp(`The bundle at ${libraryPath} does not contain a valid static library in the '.a' file format.`); } - let expectedArchs = ["armv7", "arm64", "i386"]; - let archsInTheFatFile = await this.$childProcess.exec("lipo -i " + libraryPath); + const expectedArchs = ["armv7", "arm64", "i386"]; + const archsInTheFatFile = await this.$childProcess.exec("lipo -i " + libraryPath); expectedArchs.forEach(expectedArch => { if (archsInTheFatFile.indexOf(expectedArch) < 0) { @@ -886,14 +1047,14 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private replaceFileContent(file: string, projectData: IProjectData): void { - let fileContent = this.$fs.readText(file); - let replacedContent = helpers.stringReplaceAll(fileContent, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, projectData.projectName); + const fileContent = this.$fs.readText(file); + const replacedContent = helpers.stringReplaceAll(fileContent, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, projectData.projectName); this.$fs.writeFile(file, replacedContent); } private replaceFileName(fileNamePart: string, fileRootLocation: string, projectData: IProjectData): void { - let oldFileName = IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + fileNamePart; - let newFileName = projectData.projectName + fileNamePart; + const oldFileName = IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + fileNamePart; + const newFileName = projectData.projectName + fileNamePart; this.$fs.rename(path.join(fileRootLocation, oldFileName), path.join(fileRootLocation, newFileName)); } @@ -910,10 +1071,10 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f await this.$xcprojService.verifyXcproj(true); this.$logger.info("Installing pods..."); - let podTool = this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod"; - let childProcess = await this.$childProcess.spawnFromEvent(podTool, ["install"], "close", { cwd: this.getPlatformData(projectData).projectRoot, stdio: ['pipe', process.stdout, 'pipe'] }); + const podTool = this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod"; + const childProcess = await this.$childProcess.spawnFromEvent(podTool, ["install"], "close", { cwd: this.getPlatformData(projectData).projectRoot, stdio: ['pipe', process.stdout, 'pipe'] }); if (childProcess.stderr) { - let warnings = childProcess.stderr.match(/(\u001b\[(?:\d*;){0,5}\d*m[\s\S]+?\u001b\[(?:\d*;){0,5}\d*m)|(\[!\].*?\n)|(.*?warning.*)/gi); + const warnings = childProcess.stderr.match(/(\u001b\[(?:\d*;){0,5}\d*m[\s\S]+?\u001b\[(?:\d*;){0,5}\d*m)|(\[!\].*?\n)|(.*?warning.*)/gi); _.each(warnings, (warning: string) => { this.$logger.warnWithLabel(warning.replace("\n", "")); }); @@ -936,26 +1097,26 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private async prepareFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData): Promise { - for (let fileName of this.getAllLibsForPluginWithFileExtension(pluginData, ".framework")) { + for (const fileName of this.getAllLibsForPluginWithFileExtension(pluginData, ".framework")) { await this.addFramework(path.join(pluginPlatformsFolderPath, fileName), projectData); } } private async prepareStaticLibs(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData): Promise { - for (let fileName of this.getAllLibsForPluginWithFileExtension(pluginData, ".a")) { + for (const fileName of this.getAllLibsForPluginWithFileExtension(pluginData, ".a")) { await this.addStaticLibrary(path.join(pluginPlatformsFolderPath, fileName), projectData); } } private async prepareCocoapods(pluginPlatformsFolderPath: string, projectData: IProjectData, opts?: any): Promise { - let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + const pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); if (this.$fs.exists(pluginPodFilePath)) { - let pluginPodFileContent = this.$fs.readText(pluginPodFilePath), - pluginPodFilePreparedContent = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent), - projectPodFileContent = this.$fs.exists(this.getProjectPodFilePath(projectData)) ? this.$fs.readText(this.getProjectPodFilePath(projectData)) : ""; + const pluginPodFileContent = this.$fs.readText(pluginPodFilePath); + const pluginPodFilePreparedContent = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); + let projectPodFileContent = this.$fs.exists(this.getProjectPodFilePath(projectData)) ? this.$fs.readText(this.getProjectPodFilePath(projectData)) : ""; if (!~projectPodFileContent.indexOf(pluginPodFilePreparedContent)) { - let podFileHeader = this.$cocoapodsService.getPodfileHeader(projectData.projectName), + const podFileHeader = this.$cocoapodsService.getPodfileHeader(projectData.projectName), podFileFooter = this.$cocoapodsService.getPodfileFooter(); if (_.startsWith(projectPodFileContent, podFileHeader)) { @@ -966,10 +1127,10 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f projectPodFileContent = projectPodFileContent.substr(0, projectPodFileContent.length - podFileFooter.length); } - let contentToWrite = `${podFileHeader}${projectPodFileContent}${pluginPodFilePreparedContent}${podFileFooter}`; + const contentToWrite = `${podFileHeader}${projectPodFileContent}${pluginPodFilePreparedContent}${podFileFooter}`; this.$fs.writeFile(this.getProjectPodFilePath(projectData), contentToWrite); - let project = this.createPbxProj(projectData); + const project = this.createPbxProj(projectData); this.savePbxProj(project, projectData); } } @@ -980,9 +1141,9 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private removeFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData): void { - let project = this.createPbxProj(projectData); + const project = this.createPbxProj(projectData); _.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".framework"), fileName => { - let relativeFrameworkPath = this.getLibSubpathRelativeToProjectPath(fileName, projectData); + const relativeFrameworkPath = this.getLibSubpathRelativeToProjectPath(fileName, projectData); project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true }); }); @@ -990,15 +1151,15 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private removeStaticLibs(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData): void { - let project = this.createPbxProj(projectData); + const project = this.createPbxProj(projectData); _.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".a"), fileName => { - let staticLibPath = path.join(pluginPlatformsFolderPath, fileName); - let relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath), projectData); + const staticLibPath = path.join(pluginPlatformsFolderPath, fileName); + const relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath), projectData); project.removeFramework(relativeStaticLibPath); - let headersSubpath = path.join("include", path.basename(staticLibPath, ".a")); - let relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath, projectData)); + const headersSubpath = path.join("include", path.basename(staticLibPath, ".a")); + const relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath, projectData)); project.removeFromHeaderSearchPaths({ relativePath: relativeHeaderSearchPath }); }); @@ -1006,12 +1167,12 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private removeCocoapods(pluginPlatformsFolderPath: string, projectData: IProjectData): void { - let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + const pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); if (this.$fs.exists(pluginPodFilePath) && this.$fs.exists(this.getProjectPodFilePath(projectData))) { - let pluginPodFileContent = this.$fs.readText(pluginPodFilePath); + const pluginPodFileContent = this.$fs.readText(pluginPodFilePath); let projectPodFileContent = this.$fs.readText(this.getProjectPodFilePath(projectData)); - let contentToRemove = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); + const contentToRemove = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); projectPodFileContent = helpers.stringReplaceAll(projectPodFileContent, contentToRemove, ""); if (projectPodFileContent.trim() === `use_frameworks!${os.EOL}${os.EOL}target "${projectData.projectName}" do${os.EOL}${os.EOL}end`) { this.$fs.deleteFile(this.getProjectPodFilePath(projectData)); @@ -1026,8 +1187,8 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private generateModulemap(headersFolderPath: string, libraryName: string): void { - let headersFilter = (fileName: string, containingFolderPath: string) => (path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).isFile()); - let headersFolderContents = this.$fs.readDirectory(headersFolderPath); + const headersFilter = (fileName: string, containingFolderPath: string) => (path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).isFile()); + const headersFolderContents = this.$fs.readDirectory(headersFolderPath); let headers = _(headersFolderContents).filter(item => headersFilter(item, headersFolderPath)).value(); if (!headers.length) { @@ -1037,7 +1198,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f headers = _.map(headers, value => `header "${value}"`); - let modulemap = `module ${libraryName} { explicit module ${libraryName} { ${headers.join(" ")} } }`; + const modulemap = `module ${libraryName} { explicit module ${libraryName} { ${headers.join(" ")} } }`; this.$fs.writeFile(path.join(headersFolderPath, "module.modulemap"), modulemap); } @@ -1047,31 +1208,44 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } await this.checkIfXcodeprojIsRequired(); - let escapedProjectFile = projectFile.replace(/'/g, "\\'"), + const escapedProjectFile = projectFile.replace(/'/g, "\\'"), escapedPluginFile = pluginFile.replace(/'/g, "\\'"), mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedProjectFile}').merge(Xcodeproj::Config.new('${escapedPluginFile}')).save_as(Pathname.new('${escapedProjectFile}'))`; await this.$childProcess.exec(`ruby -e "${mergeScript}"`); } private async mergeProjectXcconfigFiles(release: boolean, projectData: IProjectData): Promise { - this.$fs.deleteFile(release ? this.getPluginsReleaseXcconfigFilePath(projectData) : this.getPluginsDebugXcconfigFilePath(projectData)); + const pluginsXcconfigFilePath = release ? this.getPluginsReleaseXcconfigFilePath(projectData) : this.getPluginsDebugXcconfigFilePath(projectData); + this.$fs.deleteFile(pluginsXcconfigFilePath); - let allPlugins: IPluginData[] = await (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); - for (let plugin of allPlugins) { - let pluginPlatformsFolderPath = plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); - let pluginXcconfigFilePath = path.join(pluginPlatformsFolderPath, "build.xcconfig"); + const allPlugins: IPluginData[] = await (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); + for (const plugin of allPlugins) { + const pluginPlatformsFolderPath = plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); + const pluginXcconfigFilePath = path.join(pluginPlatformsFolderPath, "build.xcconfig"); if (this.$fs.exists(pluginXcconfigFilePath)) { - await this.mergeXcconfigFiles(pluginXcconfigFilePath, release ? this.getPluginsReleaseXcconfigFilePath(projectData) : this.getPluginsDebugXcconfigFilePath(projectData)); + await this.mergeXcconfigFiles(pluginXcconfigFilePath, pluginsXcconfigFilePath); } } - let appResourcesXcconfigPath = path.join(projectData.projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); + const appResourcesXcconfigPath = path.join(projectData.projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); if (this.$fs.exists(appResourcesXcconfigPath)) { - await this.mergeXcconfigFiles(appResourcesXcconfigPath, release ? this.getPluginsReleaseXcconfigFilePath(projectData) : this.getPluginsDebugXcconfigFilePath(projectData)); + await this.mergeXcconfigFiles(appResourcesXcconfigPath, pluginsXcconfigFilePath); } - let podFilesRootDirName = path.join("Pods", "Target Support Files", `Pods-${projectData.projectName}`); - let podFolder = path.join(this.getPlatformData(projectData).projectRoot, podFilesRootDirName); + // Set Entitlements Property to point to default file if not set explicitly by the user. + const entitlementsPropertyValue = this.$xCConfigService.readPropertyValue(pluginsXcconfigFilePath, constants.CODE_SIGN_ENTITLEMENTS); + if (entitlementsPropertyValue === null) { + temp.track(); + const tempEntitlementsDir = temp.mkdirSync("entitlements"); + const tempEntitlementsFilePath = path.join(tempEntitlementsDir, "set-entitlements.xcconfig"); + const entitlementsRelativePath = this.$iOSEntitlementsService.getPlatformsEntitlementsRelativePath(projectData); + this.$fs.writeFile(tempEntitlementsFilePath, `CODE_SIGN_ENTITLEMENTS = ${entitlementsRelativePath}${EOL}`); + + await this.mergeXcconfigFiles(tempEntitlementsFilePath, pluginsXcconfigFilePath); + } + + const podFilesRootDirName = path.join("Pods", "Target Support Files", `Pods-${projectData.projectName}`); + const podFolder = path.join(this.getPlatformData(projectData).projectRoot, podFilesRootDirName); if (this.$fs.exists(podFolder)) { if (release) { await this.mergeXcconfigFiles(path.join(this.getPlatformData(projectData).projectRoot, podFilesRootDirName, `Pods-${projectData.projectName}.release.xcconfig`), this.getPluginsReleaseXcconfigFilePath(projectData)); @@ -1082,9 +1256,9 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private async checkIfXcodeprojIsRequired(): Promise { - let xcprojInfo = await this.$xcprojService.getXcprojInfo(); + const xcprojInfo = await this.$xcprojService.getXcprojInfo(); if (xcprojInfo.shouldUseXcproj && !xcprojInfo.xcprojAvailable) { - let errorMessage = `You are using CocoaPods version ${xcprojInfo.cocoapodVer} which does not support Xcode ${xcprojInfo.xcodeVersion.major}.${xcprojInfo.xcodeVersion.minor} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH. Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`; + const errorMessage = `You are using CocoaPods version ${xcprojInfo.cocoapodVer} which does not support Xcode ${xcprojInfo.xcodeVersion.major}.${xcprojInfo.xcodeVersion.minor} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH. Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`; this.$errors.failWithoutHelp(errorMessage); @@ -1101,98 +1275,52 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$errors.fail("xcodebuild execution failed. Make sure that you have latest Xcode and tools installed."); } - let splitedXcodeBuildVersion = xcodeBuildVersion.split("."); - if (splitedXcodeBuildVersion.length === 3) { - xcodeBuildVersion = `${splitedXcodeBuildVersion[0]}.${splitedXcodeBuildVersion[1]}`; - } + const splitedXcodeBuildVersion = xcodeBuildVersion.split("."); + xcodeBuildVersion = `${splitedXcodeBuildVersion[0] || 0}.${splitedXcodeBuildVersion[1] || 0}`; return xcodeBuildVersion; } - private getDevelopmentTeams(): Array<{ id: string, name: string }> { - let dir = path.join(process.env.HOME, "Library/MobileDevice/Provisioning Profiles/"); - let files = this.$fs.readDirectory(dir); - let teamIds: any = {}; - for (let file of files) { - let filePath = path.join(dir, file); - let data = this.$fs.readText(filePath, "utf8"); - let teamId = this.getProvisioningProfileValue("TeamIdentifier", data); - let teamName = this.getProvisioningProfileValue("TeamName", data); - if (teamId) { - teamIds[teamId] = teamName; - } - } - - let teamIdsArray = new Array<{ id: string, name: string }>(); - for (let teamId in teamIds) { - teamIdsArray.push({ id: teamId, name: teamIds[teamId] }); - } - - return teamIdsArray; - } - - private getProvisioningProfileValue(name: string, text: string): string { - let findStr = "" + name + ""; - let index = text.indexOf(findStr); - if (index > 0) { - index = text.indexOf("", index + findStr.length); - if (index > 0) { - index += "".length; - let endIndex = text.indexOf("", index); - let result = text.substring(index, endIndex); - return result; - } - } - - return null; + private getBuildXCConfigFilePath(projectData: IProjectData): string { + const buildXCConfig = path.join(projectData.appResourcesDirectoryPath, + this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); + return buildXCConfig; } - private readXCConfig(flag: string, projectData: IProjectData): string { - let xcconfigFile = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); - if (this.$fs.exists(xcconfigFile)) { - let text = this.$fs.readText(xcconfigFile); - let teamId: string; - text.split(/\r?\n/).forEach((line) => { - line = line.replace(/\/(\/)[^\n]*$/, ""); - if (line.indexOf(flag) >= 0) { - teamId = line.split("=")[1].trim(); - if (teamId[teamId.length - 1] === ';') { - teamId = teamId.slice(0, -1); - } - } - }); - if (teamId) { - return teamId; - } - } + private readTeamId(projectData: IProjectData): string { + let teamId = this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); - let fileName = path.join(this.getPlatformData(projectData).projectRoot, "teamid"); + const fileName = path.join(this.getPlatformData(projectData).projectRoot, "teamid"); if (this.$fs.exists(fileName)) { - return this.$fs.readText(fileName); + teamId = this.$fs.readText(fileName); } - return null; - } - - private readTeamId(projectData: IProjectData): string { - return this.readXCConfig("DEVELOPMENT_TEAM", projectData); + return teamId; } private readXCConfigProvisioningProfile(projectData: IProjectData): string { - return this.readXCConfig("PROVISIONING_PROFILE", projectData); + return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE"); } private readXCConfigProvisioningProfileForIPhoneOs(projectData: IProjectData): string { - return this.readXCConfig("PROVISIONING_PROFILE[sdk=iphoneos*]", projectData); + return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE[sdk=iphoneos*]"); + } + + private readXCConfigProvisioningProfileSpecifier(projectData: IProjectData): string { + return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER"); + } + + private readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData: IProjectData): string { + return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"); } private async getDevelopmentTeam(projectData: IProjectData, teamId?: string): Promise { teamId = teamId || this.readTeamId(projectData); if (!teamId) { - let teams = this.getDevelopmentTeams(); + const teams = await this.$iOSProvisionService.getDevelopmentTeams(); this.$logger.warn("Xcode 8 requires a team id to be specified when building for device."); - this.$logger.warn("You can specify the team id by setting the DEVELOPMENT_TEAM setting in build.xcconfig file located in App_Resources folder of your app, or by using the --teamId option when calling run, debug or livesync commnads."); + this.$logger.warn("You can specify the team id by setting the DEVELOPMENT_TEAM setting in build.xcconfig file located in App_Resources folder of your app, or by using the --teamId option when calling run, debug or livesync commands."); if (teams.length === 1) { teamId = teams[0].id; this.$logger.warn("Found and using the following development team installed on your system: " + teams[0].name + " (" + teams[0].id + ")"); @@ -1201,22 +1329,22 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$errors.failWithoutHelp(`Unable to determine default development team. Available development teams are: ${_.map(teams, team => team.id)}. Specify team in app/App_Resources/iOS/build.xcconfig file in the following way: DEVELOPMENT_TEAM = `); } - let choices: string[] = []; - for (let team of teams) { + const choices: string[] = []; + for (const team of teams) { choices.push(team.name + " (" + team.id + ")"); } - let choice = await this.$prompter.promptForChoice('Found multiple development teams, select one:', choices); + const choice = await this.$prompter.promptForChoice('Found multiple development teams, select one:', choices); teamId = teams[choices.indexOf(choice)].id; - let choicesPersist = [ + const choicesPersist = [ "Yes, set the DEVELOPMENT_TEAM setting in build.xcconfig file.", "Yes, persist the team id in platforms folder.", "No, don't persist this setting." ]; - let choicePersist = await this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist); + const choicePersist = await this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist); switch (choicesPersist.indexOf(choicePersist)) { case 0: - let xcconfigFile = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); + const xcconfigFile = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); break; case 1: @@ -1227,8 +1355,40 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } } } + + this.$logger.trace(`Selected teamId is '${teamId}'.`); + return teamId; } + + private validateApplicationIdentifier(projectData: IProjectData): void { + const projectDir = projectData.projectDir; + const infoPlistPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); + const mergedPlistPath = this.getPlatformData(projectData).configurationFilePath; + + if (!this.$fs.exists(infoPlistPath) || !this.$fs.exists(mergedPlistPath)) { + return; + } + + const infoPlist = plist.parse(this.$fs.readText(infoPlistPath)); + const mergedPlist = plist.parse(this.$fs.readText(mergedPlistPath)); + + if (infoPlist.CFBundleIdentifier && infoPlist.CFBundleIdentifier !== mergedPlist.CFBundleIdentifier) { + this.$logger.warnWithLabel("The CFBundleIdentifier key inside the 'Info.plist' will be overriden by the 'id' inside 'package.json'."); + } + } + + private getExportOptionsMethod(projectData: IProjectData): string { + const embeddedMobileProvisionPath = path.join(this.getPlatformData(projectData).deviceBuildOutputPath, `${projectData.projectName}.app`, "embedded.mobileprovision"); + const provision = mobileprovision.provision.readFromFile(embeddedMobileProvisionPath); + + return { + "Development": "development", + "AdHoc": "ad-hoc", + "Distribution": "app-store", + "Enterprise": "enterprise" + }[provision.Type]; + } } $injector.register("iOSProjectService", IOSProjectService); diff --git a/lib/services/ios-provision-service.ts b/lib/services/ios-provision-service.ts index 3ab4522a53..19c0ba380c 100644 --- a/lib/services/ios-provision-service.ts +++ b/lib/services/ios-provision-service.ts @@ -1,7 +1,7 @@ import * as mobileprovision from "ios-mobileprovision-finder"; -import { createTable } from "../common/helpers"; +import { createTable, quoteString } from "../common/helpers"; -const months = ["Jan", "Feb", "Marc", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"]; +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; function formatDate(date: Date): string { return `${date.getDay()} ${months[date.getMonth()]} ${date.getFullYear()}`; } @@ -22,14 +22,14 @@ export class IOSProvisionService { || match.nonEligable.find(prov => prov.Name === uuidOrName); } - public async list(projectId: string): Promise { + public async listProvisions(projectId: string): Promise { const data = await this.queryProvisioningProfilesAndDevices(projectId); const devices = data.devices; const match = data.match; function formatSupportedDeviceCount(prov: mobileprovision.provision.MobileProvision) { if (devices.length > 0 && prov.Type === "Development") { - return prov.ProvisionedDevices.reduce((count, device) => count + (devices.indexOf(device) >= 0 ? 1 : 0), 0) + "/" + devices.length + " targets"; + return prov.ProvisionedDevices.filter(device => devices.indexOf(device) >= 0).length + "/" + devices.length + " targets"; } else { return ""; } @@ -49,7 +49,7 @@ export class IOSProvisionService { function pushProvision(prov: mobileprovision.provision.MobileProvision) { table.push(["", "", "", ""]); - table.push(["\"" + prov.Name + "\"", prov.TeamName, prov.Type, formatTotalDeviceCount(prov)]); + table.push([quoteString(prov.Name), prov.TeamName, prov.Type, formatTotalDeviceCount(prov)]); table.push([prov.UUID, prov.TeamIdentifier && prov.TeamIdentifier.length > 0 ? "(" + prov.TeamIdentifier[0] + ")" : "", formatDate(prov.ExpirationDate), formatSupportedDeviceCount(prov)]); table.push([prov.Entitlements["application-identifier"], "", "", ""]); } @@ -59,7 +59,12 @@ export class IOSProvisionService { this.$logger.out(); this.$logger.out("There are also " + match.nonEligable.length + " non-eligable provisioning profiles."); this.$logger.out(); + } + public async listTeams(): Promise { + const teams = await this.getDevelopmentTeams(); + const table = createTable(["Team Name", "Team ID"], teams.map(team => [quoteString(team.name), team.id])); + this.$logger.out(table.toString()); } private async queryProvisioningProfilesAndDevices(projectId: string): Promise<{ devices: string[], match: mobileprovision.provision.Result }> { @@ -77,7 +82,8 @@ export class IOSProvisionService { devices = [this.$options.device]; } else { await this.$devicesService.initialize({ - platform: "ios" + platform: "ios", + skipEmulatorStart: true }); devices = _(this.$devicesService.getDeviceInstances()) .filter(d => this.$mobileHelper.isiOSPlatform(d.deviceInfo.platform)) @@ -89,6 +95,31 @@ export class IOSProvisionService { return { devices, match }; } + + public async getDevelopmentTeams(): Promise<{ id: string, name: string }[]> { + const teams: { [teamName: string]: Set } = {}; + // NOTE: We are reading all provisioning profiles and collect team information from them. + // It would be better if we can check the Apple ID registered in Xcode and read the teams associated with it. + mobileprovision.provision.read().forEach(provision => + provision.TeamIdentifier && provision.TeamIdentifier.forEach(id => { + if (!teams[provision.TeamName]) { + teams[provision.TeamName] = new Set(); + } + teams[provision.TeamName].add(id); + }) + ); + const teamsArray = Object.keys(teams).reduce((arr, name) => { + teams[name].forEach(id => arr.push({ id, name })); + return arr; + }, []); + return teamsArray; + } + + public async getTeamIdsWithName(teamName: string): Promise { + const allTeams = await this.getDevelopmentTeams(); + const matchingTeamIds = allTeams.filter(team => team.name === teamName).map(team => team.id); + return matchingTeamIds; + } } $injector.register("iOSProvisionService", IOSProvisionService); diff --git a/lib/services/itmstransporter-service.ts b/lib/services/itmstransporter-service.ts index c41969ffa0..83dbbf3698 100644 --- a/lib/services/itmstransporter-service.ts +++ b/lib/services/itmstransporter-service.ts @@ -30,7 +30,7 @@ export class ITMSTransporterService implements IITMSTransporterService { } temp.track(); - let itmsTransporterPath = await this.getITMSTransporterPath(), + const itmsTransporterPath = await this.getITMSTransporterPath(), ipaFileName = "app.ipa", itmsDirectory = temp.mkdirSync("itms-"), innerDirectory = path.join(itmsDirectory, "mybundle.itmsp"), @@ -43,7 +43,7 @@ export class ITMSTransporterService implements IITMSTransporterService { this.$fs.copyFile(data.ipaFilePath, ipaFileLocation); - let ipaFileHash = await this.$fs.getFileShasum(ipaFileLocation, { algorithm: "md5" }), + const ipaFileHash = await this.$fs.getFileShasum(ipaFileLocation, { algorithm: "md5" }), ipaFileSize = this.$fs.getFileSize(ipaFileLocation), metadata = this.getITMSMetadataXml(iOSApplication.adamId, ipaFileName, ipaFileHash, ipaFileSize); @@ -54,11 +54,14 @@ export class ITMSTransporterService implements IITMSTransporterService { public async getiOSApplications(credentials: ICredentials): Promise { if (!this._itunesConnectApplications) { - let requestBody = this.getContentDeliveryRequestBody(credentials), + const requestBody = this.getContentDeliveryRequestBody(credentials), contentDeliveryResponse = await this.$httpClient.httpRequest({ url: "https://contentdelivery.itunes.apple.com/WebObjects/MZLabelService.woa/json/MZITunesProducerService", method: "POST", - body: requestBody + body: requestBody, + headers: { + "Content-Length": requestBody.length + } }), contentDeliveryBody: IContentDeliveryBody = JSON.parse(contentDeliveryResponse.body); @@ -85,12 +88,12 @@ export class ITMSTransporterService implements IITMSTransporterService { * @return {IFuture} The iTunes Connect application. */ private async getiOSApplication(username: string, password: string, bundleId: string): Promise { - let iOSApplications = await this.getiOSApplications({ username, password }); + const iOSApplications = await this.getiOSApplications({ username, password }); if (!iOSApplications || !iOSApplications.length) { this.$errors.failWithoutHelp(`Cannot find any registered applications for Apple ID ${username} in iTunes Connect.`); } - let iOSApplication = _.find(iOSApplications, app => app.bundleId === bundleId); + const iOSApplication = _.find(iOSApplications, app => app.bundleId === bundleId); if (!iOSApplication) { this.$errors.failWithoutHelp(`Cannot find registered applications that match the specified identifier ${bundleId} in iTunes Connect.`); @@ -115,10 +118,10 @@ export class ITMSTransporterService implements IITMSTransporterService { this.$logger.trace("--ipa set - extracting .ipa file to get app's bundle identifier"); temp.track(); - let destinationDir = temp.mkdirSync("ipa-"); + const destinationDir = temp.mkdirSync("ipa-"); await this.$fs.unzip(ipaFileFullPath, destinationDir); - let payloadDir = path.join(destinationDir, "Payload"); + const payloadDir = path.join(destinationDir, "Payload"); let allApps = this.$fs.readDirectory(payloadDir); this.$logger.debug("ITMSTransporter .ipa Payload files:"); @@ -130,10 +133,10 @@ export class ITMSTransporterService implements IITMSTransporterService { } else if (allApps.length <= 0) { this.$errors.failWithoutHelp("In the .ipa the ITMSTransporter is uploading there must be at least one .app file."); } - let appFile = path.join(payloadDir, allApps[0]); + const appFile = path.join(payloadDir, allApps[0]); - let plistObject = await this.$bplistParser.parseFile(path.join(appFile, "Info.plist")); - let bundleId = plistObject && plistObject[0] && plistObject[0].CFBundleIdentifier; + const plistObject = await this.$bplistParser.parseFile(path.join(appFile, "Info.plist")); + const bundleId = plistObject && plistObject[0] && plistObject[0].CFBundleIdentifier; if (!bundleId) { this.$errors.failWithoutHelp(`Unable to determine bundle identifier from ${ipaFileFullPath}.`); } @@ -148,9 +151,9 @@ export class ITMSTransporterService implements IITMSTransporterService { private async getITMSTransporterPath(): Promise { if (!this._itmsTransporterPath) { - let xcodePath = await this.$xcodeSelectService.getContentsDirectoryPath(), - xcodeVersion = await this.$xcodeSelectService.getXcodeVersion(), - result = path.join(xcodePath, "Applications", "Application Loader.app", "Contents"); + const xcodePath = await this.$xcodeSelectService.getContentsDirectoryPath(); + const xcodeVersion = await this.$xcodeSelectService.getXcodeVersion(); + let result = path.join(xcodePath, "Applications", "Application Loader.app", "Contents"); xcodeVersion.patch = xcodeVersion.patch || "0"; // iTMS Transporter's path has been modified in Xcode 6.3 @@ -170,12 +173,12 @@ export class ITMSTransporterService implements IITMSTransporterService { return this._itmsTransporterPath; } - private getContentDeliveryRequestBody(credentials: ICredentials): string { + private getContentDeliveryRequestBody(credentials: ICredentials): Buffer { // All of those values except credentials are hardcoded // Apple's content delivery API is very picky with handling requests // and if only one of these ends up missing the API returns // a response with 200 status code and an error - return JSON.stringify({ + return Buffer.from(JSON.stringify({ id: "1", // magic number jsonrpc: "2.0", method: "lookupSoftwareApplications", @@ -186,7 +189,7 @@ export class ITMSTransporterService implements IITMSTransporterService { Application: "Application Loader", OSIdentifier: "Mac OS X 10.8.5 (x86_64)" } - }); + }), "utf8"); } private getITMSMetadataXml(appleId: string, ipaFileName: string, ipaFileHash: string, ipaFileSize: number): string { diff --git a/lib/services/karma-execution.ts b/lib/services/karma-execution.ts index 2223bef675..728bffcf71 100644 --- a/lib/services/karma-execution.ts +++ b/lib/services/karma-execution.ts @@ -2,7 +2,7 @@ import * as path from "path"; process.on("message", (data: any) => { if (data.karmaConfig) { - let pathToKarma = path.join(data.karmaConfig.projectDir, 'node_modules/karma'), + const pathToKarma = path.join(data.karmaConfig.projectDir, 'node_modules/karma'), KarmaServer = require(path.join(pathToKarma, 'lib/server')), karma = new KarmaServer(data.karmaConfig, (exitCode: number) => { //Exit with the correct exit code and signal the manager process. diff --git a/lib/services/livesync/android-device-livesync-service.ts b/lib/services/livesync/android-device-livesync-service.ts index 0ea189229b..b19cc0d7ad 100644 --- a/lib/services/livesync/android-device-livesync-service.ts +++ b/lib/services/livesync/android-device-livesync-service.ts @@ -1,103 +1,127 @@ import { DeviceAndroidDebugBridge } from "../../common/mobile/android/device-android-debug-bridge"; import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; +import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; import * as helpers from "../../common/helpers"; +import { LiveSyncPaths } from "../../constants"; +import { cache } from "../../common/decorators"; import * as path from "path"; import * as net from "net"; -class AndroidLiveSyncService implements INativeScriptDeviceLiveSyncService { +export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { private static BACKEND_PORT = 18182; private device: Mobile.IAndroidDevice; constructor(_device: Mobile.IDevice, private $mobileHelper: Mobile.IMobileHelper, + private $devicePathProvider: IDevicePathProvider, private $injector: IInjector, - private $androidDebugService: IDebugService, - private $liveSyncProvider: ILiveSyncProvider) { + protected $platformsData: IPlatformsData) { + super($platformsData); this.device = (_device); } - public get debugService(): IDebugService { - return this.$androidDebugService; - } + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { + const deviceAppData = liveSyncInfo.deviceAppData; + const localToDevicePaths = liveSyncInfo.modifiedFilesData; + const deviceProjectRootDirname = await this.$devicePathProvider.getDeviceProjectRootPath(liveSyncInfo.deviceAppData.device, { + appIdentifier: liveSyncInfo.deviceAppData.appIdentifier, + getDirname: true + }); - public async refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean, projectData: IProjectData): Promise { await this.device.adb.executeShellCommand( ["chmod", - "777", - await deviceAppData.getDeviceProjectRootPath(), - `/data/local/tmp/${deviceAppData.appIdentifier}`, - `/data/local/tmp/${deviceAppData.appIdentifier}/sync`] - ); + "777", + path.dirname(deviceProjectRootDirname), + deviceProjectRootDirname, + `${deviceProjectRootDirname}/sync`] + ); - let canExecuteFastSync = !forceExecuteFullSync && !_.some(localToDevicePaths, (localToDevicePath: any) => !this.$liveSyncProvider.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); + const reloadedSuccessfully = await this.reloadApplicationFiles(deviceAppData, localToDevicePaths); - if (canExecuteFastSync) { - return this.reloadPage(deviceAppData, localToDevicePaths); + const canExecuteFastSync = reloadedSuccessfully && !liveSyncInfo.isFullSync && !_.some(localToDevicePaths, + (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, this.device.deviceInfo.platform)); + + if (!canExecuteFastSync) { + return this.restartApplication(deviceAppData); } + } - return this.restartApplication(deviceAppData); + private async cleanLivesyncDirectories(deviceAppData: Mobile.IDeviceAppData): Promise { + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, { + appIdentifier: deviceAppData.appIdentifier, + getDirname: true + }); + + await this.device.adb.executeShellCommand(["rm", "-rf", await this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.FULLSYNC_DIR_NAME), + this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.SYNC_DIR_NAME), + await this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.REMOVEDSYNC_DIR_NAME)]); } private async restartApplication(deviceAppData: Mobile.IDeviceAppData): Promise { - let devicePathRoot = `/data/data/${deviceAppData.appIdentifier}/files`; - let devicePath = this.$mobileHelper.buildDevicePath(devicePathRoot, "code_cache", "secondary_dexes", "proxyThumb"); + const devicePathRoot = `/data/data/${deviceAppData.appIdentifier}/files`; + const devicePath = this.$mobileHelper.buildDevicePath(devicePathRoot, "code_cache", "secondary_dexes", "proxyThumb"); await this.device.adb.executeShellCommand(["rm", "-rf", devicePath]); await this.device.applicationManager.restartApplication(deviceAppData.appIdentifier); } public async beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): Promise { - let deviceRootPath = this.getDeviceRootPath(deviceAppData.appIdentifier), - deviceRootDir = path.dirname(deviceRootPath), - deviceRootBasename = path.basename(deviceRootPath), - listResult = await this.device.adb.executeShellCommand(["ls", "-l", deviceRootDir]), - regex = new RegExp(`^-.*${deviceRootBasename}$`, "m"), - matchingFile = (listResult || "").match(regex); + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, { + appIdentifier: deviceAppData.appIdentifier, + getDirname: true + }); + const deviceRootDir = path.dirname(deviceRootPath); + const deviceRootBasename = path.basename(deviceRootPath); + const listResult = await this.device.adb.executeShellCommand(["ls", "-l", deviceRootDir]); + const regex = new RegExp(`^-.*${deviceRootBasename}$`, "m"); + const matchingFile = (listResult || "").match(regex); // Check if there is already a file with deviceRootBasename. If so, delete it as it breaks LiveSyncing. if (matchingFile && matchingFile[0] && _.startsWith(matchingFile[0], '-')) { await this.device.adb.executeShellCommand(["rm", "-f", deviceRootPath]); } - this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, "fullsync"), - this.$mobileHelper.buildDevicePath(deviceRootPath, "sync"), - await this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync")]); + await this.cleanLivesyncDirectories(deviceAppData); } - private async reloadPage(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - await this.device.adb.executeCommand(["forward", `tcp:${AndroidLiveSyncService.BACKEND_PORT.toString()}`, `localabstract:${deviceAppData.appIdentifier}-livesync`]); - if (!await this.sendPageReloadMessage()) { - await this.restartApplication(deviceAppData); + private async reloadApplicationFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + await this.device.adb.executeCommand(["forward", `tcp:${AndroidDeviceLiveSyncService.BACKEND_PORT.toString()}`, `localabstract:${deviceAppData.appIdentifier}-livesync`]); + + if (await this.awaitRuntimeReloadSuccessMessage()) { + await this.cleanLivesyncDirectories(deviceAppData); + } else { + return false; } + return true; } - public async removeFiles(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise { - let deviceRootPath = this.getDeviceRootPath(appIdentifier); + public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(deviceAppData.device, { + appIdentifier: deviceAppData.appIdentifier, + getDirname: true + }); - for (let localToDevicePathData of localToDevicePaths) { - let relativeUnixPath = _.trimStart(helpers.fromWindowsRelativePathToUnix(localToDevicePathData.getRelativeToProjectBasePath()), "/"); - let deviceFilePath = this.$mobileHelper.buildDevicePath(deviceRootPath, "removedsync", relativeUnixPath); + for (const localToDevicePathData of localToDevicePaths) { + const relativeUnixPath = _.trimStart(helpers.fromWindowsRelativePathToUnix(localToDevicePathData.getRelativeToProjectBasePath()), "/"); + const deviceFilePath = this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.REMOVEDSYNC_DIR_NAME, relativeUnixPath); await this.device.adb.executeShellCommand(["mkdir", "-p", path.dirname(deviceFilePath), " && ", "touch", deviceFilePath]); } - await this.getDeviceHashService(projectId).removeHashes(localToDevicePaths); - } - - public async afterInstallApplicationAction(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectId: string): Promise { - await this.getDeviceHashService(projectId).uploadHashFileToDevice(localToDevicePaths); - return false; + await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths); } - private getDeviceRootPath(appIdentifier: string): string { - return `/data/local/tmp/${appIdentifier}`; + @cache() + public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { + const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); + return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); } - private async sendPageReloadMessage(): Promise { + private async awaitRuntimeReloadSuccessMessage(): Promise { return new Promise((resolve, reject) => { let isResolved = false; - let socket = new net.Socket(); + const socket = new net.Socket(); - socket.connect(AndroidLiveSyncService.BACKEND_PORT, '127.0.0.1', () => { + socket.connect(AndroidDeviceLiveSyncService.BACKEND_PORT, '127.0.0.1', () => { socket.write(new Buffer([0, 0, 0, 1, 1])); }); socket.on("data", (data: any) => { @@ -118,15 +142,4 @@ class AndroidLiveSyncService implements INativeScriptDeviceLiveSyncService { }); }); } - - private _deviceHashService: Mobile.IAndroidDeviceHashService; - private getDeviceHashService(projectId: string): Mobile.IAndroidDeviceHashService { - if (!this._deviceHashService) { - let adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - this._deviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: adb, appIdentifier: projectId }); - } - - return this._deviceHashService; - } } -$injector.register("androidLiveSyncServiceLocator", { factory: AndroidLiveSyncService }); diff --git a/lib/services/livesync/android-livesync-service.ts b/lib/services/livesync/android-livesync-service.ts new file mode 100644 index 0000000000..d9416643c5 --- /dev/null +++ b/lib/services/livesync/android-livesync-service.ts @@ -0,0 +1,21 @@ +import { AndroidDeviceLiveSyncService } from "./android-device-livesync-service"; +import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; + +export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { + constructor(protected $platformsData: IPlatformsData, + protected $projectFilesManager: IProjectFilesManager, + private $injector: IInjector, + $devicePathProvider: IDevicePathProvider, + $fs: IFileSystem, + $logger: ILogger, + $projectFilesProvider: IProjectFilesProvider, + ) { + super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); + } + + protected _getDeviceLiveSyncService(device: Mobile.IDevice): INativeScriptDeviceLiveSyncService { + const service = this.$injector.resolve(AndroidDeviceLiveSyncService, { _device: device }); + return service; + } +} +$injector.register("androidLiveSyncService", AndroidLiveSyncService); diff --git a/lib/services/livesync/device-livesync-service-base.ts b/lib/services/livesync/device-livesync-service-base.ts new file mode 100644 index 0000000000..bb1f44961f --- /dev/null +++ b/lib/services/livesync/device-livesync-service-base.ts @@ -0,0 +1,21 @@ +import { cache } from "../../common/decorators"; +import * as path from "path"; + +export abstract class DeviceLiveSyncServiceBase { + private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml", ".html"]; + + constructor(protected $platformsData: IPlatformsData) { } + + public canExecuteFastSync(filePath: string, projectData: IProjectData, platform: string): boolean { + const fastSyncFileExtensions = this.getFastLiveSyncFileExtensions(platform, projectData); + return _.includes(fastSyncFileExtensions, path.extname(filePath)); + } + + @cache() + private getFastLiveSyncFileExtensions(platform: string, projectData: IProjectData): string[] { + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const fastSyncFileExtensions = DeviceLiveSyncServiceBase.FAST_SYNC_FILE_EXTENSIONS.concat(platformData.fastLivesyncFileExtensions); + return fastSyncFileExtensions; + } + +} diff --git a/lib/services/livesync/ios-device-livesync-service.ts b/lib/services/livesync/ios-device-livesync-service.ts index a309233506..a29ed506ff 100644 --- a/lib/services/livesync/ios-device-livesync-service.ts +++ b/lib/services/livesync/ios-device-livesync-service.ts @@ -2,10 +2,11 @@ import * as helpers from "../../common/helpers"; import * as constants from "../../constants"; import * as minimatch from "minimatch"; import * as net from "net"; +import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; let currentPageReloadId = 0; -class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { +export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements INativeScriptDeviceLiveSyncService { private static BACKEND_PORT = 18181; private socket: net.Socket; private device: Mobile.IiOSDevice; @@ -15,23 +16,13 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { private $iOSNotification: IiOSNotification, private $iOSEmulatorServices: Mobile.IiOSSimulatorService, private $logger: ILogger, - private $options: IOptions, - private $iOSDebugService: IDebugService, private $fs: IFileSystem, - private $liveSyncProvider: ILiveSyncProvider, - private $processService: IProcessService) { - + private $processService: IProcessService, + protected $platformsData: IPlatformsData) { + super($platformsData); this.device = _device; } - public get debugService(): IDebugService { - return this.$iOSDebugService; - } - - public async afterInstallApplicationAction(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - return this.$options.watch; - } - private async setupSocketIfNeeded(projectId: string): Promise { if (this.socket) { return true; @@ -40,15 +31,14 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { if (this.device.isEmulator) { await this.$iOSEmulatorServices.postDarwinNotification(this.$iOSNotification.getAttachRequest(projectId)); try { - this.socket = await helpers.connectEventuallyUntilTimeout(() => net.connect(IOSLiveSyncService.BACKEND_PORT), 5000); + this.socket = await helpers.connectEventuallyUntilTimeout(() => net.connect(IOSDeviceLiveSyncService.BACKEND_PORT), 5000); } catch (e) { this.$logger.debug(e); return false; } } else { - let timeout = 9000; - await this.$iOSSocketRequestExecutor.executeAttachRequest(this.device, timeout, projectId); - this.socket = await this.device.connectToPort(IOSLiveSyncService.BACKEND_PORT); + await this.$iOSSocketRequestExecutor.executeAttachRequest(this.device, constants.AWAIT_NOTIFICATION_TIMEOUT_SECONDS, projectId); + this.socket = await this.device.connectToPort(IOSDeviceLiveSyncService.BACKEND_PORT); } this.attachEventHandlers(); @@ -56,24 +46,26 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { return true; } - public async removeFiles(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - await Promise.all(_.map(localToDevicePaths, localToDevicePathData => this.device.fileSystem.deleteFile(localToDevicePathData.getDevicePath(), appIdentifier))); + public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + await Promise.all(_.map(localToDevicePaths, localToDevicePathData => this.device.fileSystem.deleteFile(localToDevicePathData.getDevicePath(), deviceAppData.appIdentifier))); } - public async refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean, projectData: IProjectData): Promise { - if (forceExecuteFullSync) { + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { + const deviceAppData = liveSyncInfo.deviceAppData; + const localToDevicePaths = liveSyncInfo.modifiedFilesData; + if (liveSyncInfo.isFullSync) { await this.restartApplication(deviceAppData, projectData.projectName); return; } let scriptRelatedFiles: Mobile.ILocalToDevicePathData[] = []; - let scriptFiles = _.filter(localToDevicePaths, localToDevicePath => _.endsWith(localToDevicePath.getDevicePath(), ".js")); + const scriptFiles = _.filter(localToDevicePaths, localToDevicePath => _.endsWith(localToDevicePath.getDevicePath(), ".js")); constants.LIVESYNC_EXCLUDED_FILE_PATTERNS.forEach(pattern => scriptRelatedFiles = _.concat(scriptRelatedFiles, localToDevicePaths.filter(file => minimatch(file.getDevicePath(), pattern, { nocase: true })))); - let otherFiles = _.difference(localToDevicePaths, _.concat(scriptFiles, scriptRelatedFiles)); - let shouldRestart = _.some(otherFiles, (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.$liveSyncProvider.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); + const otherFiles = _.difference(localToDevicePaths, _.concat(scriptFiles, scriptRelatedFiles)); + const shouldRestart = _.some(otherFiles, (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); - if (shouldRestart || (!this.$options.liveEdit && scriptFiles.length)) { + if (shouldRestart || (!liveSyncInfo.useLiveEdit && scriptFiles.length)) { await this.restartApplication(deviceAppData, projectData.projectName); return; } @@ -92,7 +84,7 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { private async reloadPage(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { if (localToDevicePaths.length) { - let message = JSON.stringify({ + const message = JSON.stringify({ method: "Page.reload", params: { ignoreCache: false @@ -105,9 +97,9 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { } private async liveEdit(localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - for (let localToDevicePath of localToDevicePaths) { - let content = this.$fs.readText(localToDevicePath.getLocalPath()); - let message = JSON.stringify({ + for (const localToDevicePath of localToDevicePaths) { + const content = this.$fs.readText(localToDevicePath.getLocalPath()); + const message = JSON.stringify({ method: "Debugger.setScriptSource", params: { scriptUrl: localToDevicePath.getRelativeToProjectBasePath(), @@ -141,20 +133,21 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { try { await new Promise((resolve, reject) => { let isResolved = false; - let length = Buffer.byteLength(message, "utf16le"); - let payload = new Buffer(length + 4); + const length = Buffer.byteLength(message, "utf16le"); + const payload = new Buffer(length + 4); payload.writeInt32BE(length, 0); payload.write(message, 4, length, "utf16le"); - this.socket.once("error", (error: Error) => { + const errorCallback = (error: Error) => { if (!isResolved) { isResolved = true; reject(error); } - }); + }; + this.socket.once("error", errorCallback); this.socket.write(payload, "utf16le", () => { - this.socket.removeAllListeners("error"); + this.socket.removeListener("error", errorCallback); if (!isResolved) { isResolved = true; @@ -175,4 +168,3 @@ class IOSLiveSyncService implements INativeScriptDeviceLiveSyncService { } } } -$injector.register("iosLiveSyncServiceLocator", { factory: IOSLiveSyncService }); diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts new file mode 100644 index 0000000000..da1e921fe0 --- /dev/null +++ b/lib/services/livesync/ios-livesync-service.ts @@ -0,0 +1,75 @@ +import * as path from "path"; +import * as temp from "temp"; + +import { IOSDeviceLiveSyncService } from "./ios-device-livesync-service"; +import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; +import { APP_FOLDER_NAME, TNS_MODULES_FOLDER_NAME } from "../../constants"; + +export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { + constructor(protected $fs: IFileSystem, + protected $platformsData: IPlatformsData, + protected $projectFilesManager: IProjectFilesManager, + private $injector: IInjector, + $devicePathProvider: IDevicePathProvider, + $logger: ILogger, + $projectFilesProvider: IProjectFilesProvider, + ) { + super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); + } + + public async fullSync(syncInfo: IFullSyncInfo): Promise { + const device = syncInfo.device; + + if (device.isEmulator) { + return super.fullSync(syncInfo); + } + + const projectData = syncInfo.projectData; + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const deviceAppData = await this.getAppData(syncInfo); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + + temp.track(); + const tempZip = temp.path({ prefix: "sync", suffix: ".zip" }); + const tempApp = temp.mkdirSync("app"); + this.$logger.trace("Creating zip file: " + tempZip); + this.$fs.copyFile(path.join(path.dirname(projectFilesPath), `${APP_FOLDER_NAME}/*`), tempApp); + + if (!syncInfo.syncAllFiles) { + this.$logger.info("Skipping node_modules folder! Use the syncAllFiles option to sync files from this folder."); + this.$fs.deleteDirectory(path.join(tempApp, TNS_MODULES_FOLDER_NAME)); + } + + await this.$fs.zipFiles(tempZip, this.$fs.enumerateFilesInDirectorySync(tempApp), (res) => { + return path.join(APP_FOLDER_NAME, path.relative(tempApp, res)); + }); + + await device.fileSystem.transferFiles(deviceAppData, [{ + getLocalPath: () => tempZip, + getDevicePath: () => deviceAppData.deviceSyncZipPath, + getRelativeToProjectBasePath: () => "../sync.zip", + deviceProjectRootPath: await deviceAppData.getDeviceProjectRootPath() + }]); + + return { + deviceAppData, + isFullSync: true, + modifiedFilesData: [] + }; + } + + public liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { + if (liveSyncInfo.isReinstalled) { + // In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir. + return this.fullSync({ projectData: liveSyncInfo.projectData, device, syncAllFiles: liveSyncInfo.syncAllFiles, watch: true }); + } else { + return super.liveSyncWatchAction(device, liveSyncInfo); + } + } + + protected _getDeviceLiveSyncService(device: Mobile.IDevice): INativeScriptDeviceLiveSyncService { + const service = this.$injector.resolve(IOSDeviceLiveSyncService, { _device: device }); + return service; + } +} +$injector.register("iOSLiveSyncService", IOSLiveSyncService); diff --git a/lib/services/livesync/livesync-command-helper.ts b/lib/services/livesync/livesync-command-helper.ts new file mode 100644 index 0000000000..54be3c7a19 --- /dev/null +++ b/lib/services/livesync/livesync-command-helper.ts @@ -0,0 +1,105 @@ +export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { + + constructor(private $platformService: IPlatformService, + private $projectData: IProjectData, + private $options: IOptions, + private $liveSyncService: ILiveSyncService, + private $iosDeviceOperations: IIOSDeviceOperations, + private $mobileHelper: Mobile.IMobileHelper, + private $platformsData: IPlatformsData, + private $analyticsService: IAnalyticsService, + private $errors: IErrors) { + this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); + } + + public getPlatformsForOperation(platform: string): string[] { + const availablePlatforms = platform ? [platform] : _.values(this.$platformsData.availablePlatforms); + return availablePlatforms; + } + + public async executeLiveSyncOperation(devices: Mobile.IDevice[], platform: string, deviceDebugMap?: IDictionary): Promise { + if (!devices || !devices.length) { + if (platform) { + this.$errors.failWithoutHelp("Unable to find applicable devices to execute operation. Ensure connected devices are trusted and try again."); + } else { + this.$errors.failWithoutHelp("Unable to find applicable devices to execute operation and unable to start emulator when platform is not specified."); + } + } + + const workingWithiOSDevices = !platform || this.$mobileHelper.isiOSPlatform(platform); + const shouldKeepProcessAlive = this.$options.watch || !this.$options.justlaunch; + if (workingWithiOSDevices && shouldKeepProcessAlive) { + this.$iosDeviceOperations.setShouldDispose(false); + } + + if (this.$options.release || this.$options.bundle) { + await this.runInReleaseMode(platform); + return; + } + + // Now let's take data for each device: + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + platformSpecificOptions: this.$options, + + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, this.$projectData); + const result = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, this.$projectData); + return result; + }, + debugggingEnabled: deviceDebugMap && deviceDebugMap[d.deviceInfo.identifier] + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { + projectDir: this.$projectData.projectDir, + skipWatcher: !this.$options.watch, + watchAllFiles: this.$options.syncAllFiles, + clean: this.$options.clean, + debugOptions: this.$options + }; + + await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); + + } + + private async runInReleaseMode(platform: string): Promise { + const runPlatformOptions: IRunPlatformOptions = { + device: this.$options.device, + emulator: this.$options.emulator, + justlaunch: this.$options.justlaunch + }; + + const deployOptions = _.merge({ + projectDir: this.$projectData.projectDir, + clean: true, + }, this.$options.argv); + + const availablePlatforms = this.getPlatformsForOperation(platform); + for (const currentPlatform of availablePlatforms) { + await this.$platformService.deployPlatform(currentPlatform, this.$options, deployOptions, this.$projectData, this.$options); + await this.$platformService.startApplication(currentPlatform, runPlatformOptions, this.$projectData.projectId); + this.$platformService.trackProjectType(this.$projectData); + } + } +} + +$injector.register("liveSyncCommandHelper", LiveSyncCommandHelper); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 3e11bf2708..941f396f07 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -1,133 +1,673 @@ -import * as constants from "../../constants"; -import * as helpers from "../../common/helpers"; import * as path from "path"; -import { NodeModulesDependenciesBuilder } from "../../tools/node-modules/node-modules-dependencies-builder"; +import * as choki from "chokidar"; +import { EOL } from "os"; +import { EventEmitter } from "events"; +import { hook } from "../../common/helpers"; +import { APP_FOLDER_NAME, PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME, DEBUGGER_DETACHED_EVENT_NAME, TrackActionNames } from "../../constants"; +import { FileExtensions, DeviceTypes } from "../../common/constants"; +const deviceDescriptorPrimaryKey = "identifier"; -let choki = require("chokidar"); +const LiveSyncEvents = { + liveSyncStopped: "liveSyncStopped", + // In case we name it error, EventEmitter expects instance of Error to be raised and will also raise uncaught exception in case there's no handler + liveSyncError: "liveSyncError", + liveSyncExecuted: "liveSyncExecuted", + liveSyncStarted: "liveSyncStarted", + liveSyncNotification: "notify" +}; -class LiveSyncService implements ILiveSyncService { - private _isInitialized = false; +export class LiveSyncService extends EventEmitter implements IDebugLiveSyncService { + // key is projectDir + protected liveSyncProcessesInfo: IDictionary = {}; - constructor(private $errors: IErrors, - private $platformsData: IPlatformsData, - private $platformService: IPlatformService, - private $injector: IInjector, + constructor(private $platformService: IPlatformService, + private $projectDataService: IProjectDataService, private $devicesService: Mobile.IDevicesService, - private $options: IOptions, + private $mobileHelper: Mobile.IMobileHelper, + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, private $logger: ILogger, - private $dispatcher: IFutureDispatcher, + private $processService: IProcessService, private $hooksService: IHooksService, - private $processService: IProcessService) { } + private $pluginsService: IPluginsService, + private $debugService: IDebugService, + private $errors: IErrors, + private $debugDataService: IDebugDataService, + private $analyticsService: IAnalyticsService, + private $injector: IInjector) { + super(); + } - public get isInitialized(): boolean { // This function is used from https://github.com/NativeScript/nativescript-dev-typescript/blob/master/lib/before-prepare.js#L4 - return this._isInitialized; + public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], + liveSyncData: ILiveSyncInfo): Promise { + const projectData = this.$projectDataService.getProjectData(liveSyncData.projectDir); + await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); + await this.liveSyncOperation(deviceDescriptors, liveSyncData, projectData); } - public async liveSync(platform: string, projectData: IProjectData, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise { - if (this.$options.justlaunch) { - this.$options.watch = false; - } - let liveSyncData: ILiveSyncData[] = []; + public async stopLiveSync(projectDir: string, deviceIdentifiers?: string[], stopOptions?: { shouldAwaitAllActions: boolean }): Promise { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectDir]; - if (platform) { - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); - liveSyncData.push(await this.prepareLiveSyncData(platform, projectData)); - } else if (this.$options.device) { - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); - platform = this.$devicesService.getDeviceByIdentifier(this.$options.device).deviceInfo.platform; - liveSyncData.push(await this.prepareLiveSyncData(platform, projectData)); - } else { - await this.$devicesService.initialize({ skipInferPlatform: true, skipDeviceDetectionInterval: true }); + if (liveSyncProcessInfo) { + // In case we are coming from error during livesync, the current action is the one that erred (but we are still executing it), + // so we cannot await it as this will cause infinite loop. + const shouldAwaitPendingOperation = !stopOptions || stopOptions.shouldAwaitAllActions; + + const deviceIdentifiersToRemove = deviceIdentifiers || _.map(liveSyncProcessInfo.deviceDescriptors, d => d.identifier); + + const removedDeviceIdentifiers = _.remove(liveSyncProcessInfo.deviceDescriptors, descriptor => _.includes(deviceIdentifiersToRemove, descriptor.identifier)) + .map(descriptor => descriptor.identifier); + + // In case deviceIdentifiers are not passed, we should stop the whole LiveSync. + if (!deviceIdentifiers || !deviceIdentifiers.length || !liveSyncProcessInfo.deviceDescriptors || !liveSyncProcessInfo.deviceDescriptors.length) { + if (liveSyncProcessInfo.timer) { + clearTimeout(liveSyncProcessInfo.timer); + } + + if (liveSyncProcessInfo.watcherInfo && liveSyncProcessInfo.watcherInfo.watcher) { + liveSyncProcessInfo.watcherInfo.watcher.close(); + } + + liveSyncProcessInfo.watcherInfo = null; + liveSyncProcessInfo.isStopped = true; - for (let installedPlatform of this.$platformService.getInstalledPlatforms(projectData)) { - if (this.$devicesService.getDevicesForPlatform(installedPlatform).length === 0) { - await this.$devicesService.startEmulator(installedPlatform); + if (liveSyncProcessInfo.actionsChain && shouldAwaitPendingOperation) { + await liveSyncProcessInfo.actionsChain; } - liveSyncData.push(await this.prepareLiveSyncData(installedPlatform, projectData)); + liveSyncProcessInfo.deviceDescriptors = []; + + // Kill typescript watcher + const projectData = this.$projectDataService.getProjectData(projectDir); + await this.$hooksService.executeAfterHooks('watch', { + hookArgs: { + projectData + } + }); + } else if (liveSyncProcessInfo.currentSyncAction && shouldAwaitPendingOperation) { + await liveSyncProcessInfo.currentSyncAction; + } + + // Emit LiveSync stopped when we've really stopped. + _.each(removedDeviceIdentifiers, deviceIdentifier => { + this.emit(LiveSyncEvents.liveSyncStopped, { projectDir, deviceIdentifier }); + }); + } + } + + public getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[] { + const liveSyncProcessesInfo = this.liveSyncProcessesInfo[projectDir] || {}; + const currentDescriptors = liveSyncProcessesInfo.deviceDescriptors; + return currentDescriptors || []; + } + + private async refreshApplication(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOpts?: IDebugOptions, outputPath?: string): Promise { + const deviceDescriptor = this.getDeviceDescriptor(liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, projectData.projectDir); + + return deviceDescriptor && deviceDescriptor.debugggingEnabled ? + this.refreshApplicationWithDebug(projectData, liveSyncResultInfo, debugOpts, outputPath) : + this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOpts, outputPath); + } + + private async refreshApplicationWithoutDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOpts?: IDebugOptions, outputPath?: string, settings?: IShouldSkipEmitLiveSyncNotification): Promise { + const platformLiveSyncService = this.getLiveSyncService(liveSyncResultInfo.deviceAppData.platform); + try { + await platformLiveSyncService.refreshApplication(projectData, liveSyncResultInfo); + } catch (err) { + this.$logger.info(`Error while trying to start application ${projectData.projectId} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Error is: ${err.message || err}`); + const msg = `Unable to start application ${projectData.projectId} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}. Try starting it manually.`; + this.$logger.warn(msg); + if (!settings || !settings.shouldSkipEmitLiveSyncNotification) { + this.emit(LiveSyncEvents.liveSyncNotification, { + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId, + deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, + notification: msg + }); } } - if (liveSyncData.length === 0) { - this.$errors.fail("There are no platforms installed in this project. Please specify platform or install one by using `tns platform add` command!"); + this.emit(LiveSyncEvents.liveSyncExecuted, { + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId, + syncedFiles: liveSyncResultInfo.modifiedFilesData.map(m => m.getLocalPath()), + deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, + isFullSync: liveSyncResultInfo.isFullSync + }); + + this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); + } + + private async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOptions: IDebugOptions, outputPath?: string): Promise { + await this.$platformService.trackProjectType(projectData); + + const deviceAppData = liveSyncResultInfo.deviceAppData; + + const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier; + await this.$debugService.debugStop(deviceIdentifier); + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier }); + + const applicationId = deviceAppData.appIdentifier; + const attachDebuggerOptions: IAttachDebuggerOptions = { + platform: liveSyncResultInfo.deviceAppData.device.deviceInfo.platform, + isEmulator: liveSyncResultInfo.deviceAppData.device.isEmulator, + projectDir: projectData.projectDir, + deviceIdentifier, + debugOptions, + outputPath + }; + + try { + await deviceAppData.device.applicationManager.stopApplication(applicationId, projectData.projectName); + // Now that we've stopped the application we know it isn't started, so set debugOptions.start to false + // so that it doesn't default to true in attachDebugger method + debugOptions = debugOptions || {}; + debugOptions.start = false; + } catch (err) { + this.$logger.trace("Could not stop application during debug livesync. Will try to restart app instead.", err); + if ((err.message || err) === "Could not find developer disk image") { + // Set isFullSync here to true because we are refreshing with debugger + // We want to force a restart instead of accidentally performing LiveEdit or FastSync + liveSyncResultInfo.isFullSync = true; + await this.refreshApplicationWithoutDebug(projectData, liveSyncResultInfo, debugOptions, outputPath, { shouldSkipEmitLiveSyncNotification: true }); + this.emit(USER_INTERACTION_NEEDED_EVENT_NAME, attachDebuggerOptions); + return; + } else { + throw err; + } } - this._isInitialized = true; // If we want before-prepare hooks to work properly, this should be set after preparePlatform function + const deviceOption = { + deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier, + debugOptions: debugOptions, + }; - await this.liveSyncCore(liveSyncData, applicationReloadAction, projectData); + return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, { projectDir: projectData.projectDir }); } - private async prepareLiveSyncData(platform: string, projectData: IProjectData): Promise { - platform = platform || this.$devicesService.platform; - let platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + public async attachDebugger(settings: IAttachDebuggerOptions): Promise { + // Default values + if (settings.debugOptions) { + settings.debugOptions.chrome = settings.debugOptions.chrome === undefined ? true : settings.debugOptions.chrome; + settings.debugOptions.start = settings.debugOptions.start === undefined ? true : settings.debugOptions.start; + } else { + settings.debugOptions = { + chrome: true, + start: true + }; + } + + const projectData = this.$projectDataService.getProjectData(settings.projectDir); + const debugData = this.$debugDataService.createDebugData(projectData, { device: settings.deviceIdentifier }); - let liveSyncData: ILiveSyncData = { - platform: platform, - appIdentifier: projectData.projectId, - projectFilesPath: path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME), - syncWorkingDirectory: projectData.projectDir, - excludedProjectDirsAndFiles: this.$options.release ? constants.LIVESYNC_EXCLUDED_FILE_PATTERNS : [] + // Of the properties below only `buildForDevice` and `release` are currently used. + // Leaving the others with placeholder values so that they may not be forgotten in future implementations. + const buildConfig: IBuildConfig = { + buildForDevice: !settings.isEmulator, + release: false, + device: settings.deviceIdentifier, + provision: null, + teamId: null, + projectDir: settings.projectDir }; + debugData.pathToAppPackage = this.$platformService.lastOutputPath(settings.platform, buildConfig, projectData, settings.outputPath); - return liveSyncData; + return this.printDebugInformation(await this.$debugService.debug(debugData, settings.debugOptions)); } - @helpers.hook('livesync') - private async liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise { - await this.$platformService.trackProjectType(projectData); + public printDebugInformation(debugInformation: IDebugInformation): IDebugInformation { + if (!!debugInformation.url) { + this.emit(DEBUGGER_ATTACHED_EVENT_NAME, debugInformation); + this.$logger.info(`To start debugging, open the following URL in Chrome:${EOL}${debugInformation.url}${EOL}`.cyan); + } - let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise)[] = []; + return debugInformation; + } + + public enableDebugging(deviceOpts: IEnableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[] { + return _.map(deviceOpts, d => this.enableDebuggingCore(d, debuggingAdditionalOptions)); + } + + private getDeviceDescriptor(deviceIdentifier: string, projectDir: string) { + const deviceDescriptors = this.getLiveSyncDeviceDescriptors(projectDir); + + return _.find(deviceDescriptors, d => d.identifier === deviceIdentifier); + } + + private async enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { + const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir); + if (!currentDeviceDescriptor) { + this.$errors.failWithoutHelp(`Couldn't enable debugging for ${deviceOption.deviceIdentifier}`); + } + + currentDeviceDescriptor.debugggingEnabled = true; + const currentDeviceInstance = this.$devicesService.getDeviceByIdentifier(deviceOption.deviceIdentifier); + const attachDebuggerOptions: IAttachDebuggerOptions = { + deviceIdentifier: deviceOption.deviceIdentifier, + isEmulator: currentDeviceInstance.isEmulator, + outputPath: currentDeviceDescriptor.outputPath, + platform: currentDeviceInstance.deviceInfo.platform, + projectDir: debuggingAdditionalOptions.projectDir, + debugOptions: deviceOption.debugOptions + }; + + let debugInformation: IDebugInformation; + try { + debugInformation = await this.attachDebugger(attachDebuggerOptions); + } catch (err) { + this.$logger.trace("Couldn't attach debugger, will modify options and try again.", err); + attachDebuggerOptions.debugOptions.start = false; + try { + debugInformation = await this.attachDebugger(attachDebuggerOptions); + } catch (innerErr) { + this.$logger.trace("Couldn't attach debugger with modified options.", innerErr); + throw err; + } + } + + return debugInformation; + } + + private async enableDebuggingCore(deviceOption: IEnableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { + const liveSyncProcessInfo: ILiveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir]; + if (liveSyncProcessInfo && liveSyncProcessInfo.currentSyncAction) { + await liveSyncProcessInfo.currentSyncAction; + } + + return this.enableDebuggingCoreWithoutWaitingCurrentAction(deviceOption, debuggingAdditionalOptions); + } + + public disableDebugging(deviceOptions: IDisableDebuggingDeviceOptions[], debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise[] { + return _.map(deviceOptions, d => this.disableDebuggingCore(d, debuggingAdditionalOptions)); + } + + public async disableDebuggingCore(deviceOption: IDisableDebuggingDeviceOptions, debuggingAdditionalOptions: IDebuggingAdditionalOptions): Promise { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[debuggingAdditionalOptions.projectDir]; + if (liveSyncProcessInfo.currentSyncAction) { + await liveSyncProcessInfo.currentSyncAction; + } + + const currentDeviceDescriptor = this.getDeviceDescriptor(deviceOption.deviceIdentifier, debuggingAdditionalOptions.projectDir); + if (currentDeviceDescriptor) { + currentDeviceDescriptor.debugggingEnabled = false; + } else { + this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}`); + } + + const currentDevice = this.$devicesService.getDeviceByIdentifier(currentDeviceDescriptor.identifier); + if (!currentDevice) { + this.$errors.failWithoutHelp(`Couldn't disable debugging for ${deviceOption.deviceIdentifier}. Could not find device.`); + } + + await this.$debugService.debugStop(currentDevice.deviceInfo.identifier); + this.emit(DEBUGGER_DETACHED_EVENT_NAME, { deviceIdentifier: currentDeviceDescriptor.identifier }); + } - for (let dataItem of liveSyncData) { - let service: IPlatformLiveSyncService = this.$injector.resolve("platformLiveSyncService", { _liveSyncData: dataItem }); - watchForChangeActions.push((event: string, filePath: string, dispatcher: IFutureDispatcher) => - service.partialSync(event, filePath, dispatcher, applicationReloadAction, projectData)); + @hook("liveSync") + private async liveSyncOperation(deviceDescriptors: ILiveSyncDeviceInfo[], + liveSyncData: ILiveSyncInfo, projectData: IProjectData): Promise { + // In case liveSync is called for a second time for the same projectDir. + const isAlreadyLiveSyncing = this.liveSyncProcessesInfo[projectData.projectDir] && !this.liveSyncProcessesInfo[projectData.projectDir].isStopped; - await service.fullSync(projectData, applicationReloadAction); + // Prevent cases where liveSync is called consecutive times with the same device, for example [ A, B, C ] and then [ A, B, D ] - we want to execute initialSync only for D. + const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir); + const deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors; + this.setLiveSyncProcessInfo(liveSyncData.projectDir, deviceDescriptors); + + await this.initialSync(projectData, deviceDescriptorsForInitialSync, liveSyncData); + + if (!liveSyncData.skipWatcher && this.liveSyncProcessesInfo[projectData.projectDir].deviceDescriptors.length) { + // Should be set after prepare + this.$injector.resolve("usbLiveSyncService").isInitialized = true; + + await this.startWatcher(projectData, liveSyncData); } + } + + private setLiveSyncProcessInfo(projectDir: string, deviceDescriptors: ILiveSyncDeviceInfo[]): void { + this.liveSyncProcessesInfo[projectDir] = this.liveSyncProcessesInfo[projectDir] || Object.create(null); + this.liveSyncProcessesInfo[projectDir].actionsChain = this.liveSyncProcessesInfo[projectDir].actionsChain || Promise.resolve(); + this.liveSyncProcessesInfo[projectDir].currentSyncAction = this.liveSyncProcessesInfo[projectDir].actionsChain; + this.liveSyncProcessesInfo[projectDir].isStopped = false; - if (this.$options.watch && !this.$options.justlaunch) { - await this.$hooksService.executeBeforeHooks('watch'); - await this.partialSync(liveSyncData[0].syncWorkingDirectory, watchForChangeActions, projectData); + const currentDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectDir); + this.liveSyncProcessesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), deviceDescriptorPrimaryKey); + } + + private getLiveSyncService(platform: string): IPlatformLiveSyncService { + if (this.$mobileHelper.isiOSPlatform(platform)) { + return this.$injector.resolve("iOSLiveSyncService"); + } else if (this.$mobileHelper.isAndroidPlatform(platform)) { + return this.$injector.resolve("androidLiveSyncService"); } + + this.$errors.failWithoutHelp(`Invalid platform ${platform}. Supported platforms are: ${this.$mobileHelper.platformNames.join(", ")}`); } - private partialSync(syncWorkingDirectory: string, onChangedActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise)[], projectData: IProjectData): void { - let that = this; - let dependenciesBuilder = this.$injector.resolve(NodeModulesDependenciesBuilder, {}); - let productionDependencies = dependenciesBuilder.getProductionDependencies(projectData.projectDir); - let pattern = ["app"]; + private async ensureLatestAppPackageIsInstalledOnDevice(options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions, nativePrepare?: INativePrepare): Promise { + const platform = options.device.deviceInfo.platform; + const appInstalledOnDeviceResult: IAppInstalledOnDeviceResult = { appInstalled: false }; + if (options.preparedPlatforms.indexOf(platform) === -1) { + options.preparedPlatforms.push(platform); - if (this.$options.syncAllFiles) { - pattern.push("package.json"); + const platformSpecificOptions = options.deviceBuildInfoDescriptor.platformSpecificOptions || {}; + await this.$platformService.preparePlatform(platform, { + bundle: false, + release: false, + }, null, options.projectData, platformSpecificOptions, options.modifiedFiles, nativePrepare); + } + + const buildResult = await this.installedCachedAppPackage(platform, options); + if (buildResult) { + appInstalledOnDeviceResult.appInstalled = true; + return appInstalledOnDeviceResult; + } + + const shouldBuild = await this.$platformService.shouldBuild(platform, + options.projectData, + { buildForDevice: !options.device.isEmulator, clean: options.liveSyncData && options.liveSyncData.clean }, + options.deviceBuildInfoDescriptor.outputPath); + let pathToBuildItem = null; + let action = LiveSyncTrackActionNames.LIVESYNC_OPERATION; + if (shouldBuild) { + pathToBuildItem = await options.deviceBuildInfoDescriptor.buildAction(); + options.rebuiltInformation.push({ isEmulator: options.device.isEmulator, platform, pathToBuildItem }); + action = LiveSyncTrackActionNames.LIVESYNC_OPERATION_BUILD; + } + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.LiveSync, + device: options.device, + projectDir: options.projectData.projectDir + }); + + await this.trackAction(action, platform, options); + + const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options.deviceBuildInfoDescriptor.outputPath); + if (shouldInstall) { + await this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + appInstalledOnDeviceResult.appInstalled = true; + } + + return appInstalledOnDeviceResult; + } + + private async trackAction(action: string, platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { + if (!options.settings[platform][options.device.deviceInfo.type]) { + let isForDevice = !options.device.isEmulator; + options.settings[platform][options.device.deviceInfo.type] = true; + if (this.$mobileHelper.isAndroidPlatform(platform)) { + options.settings[platform][DeviceTypes.Emulator] = true; + options.settings[platform][DeviceTypes.Device] = true; + isForDevice = null; + } + + await this.$platformService.trackActionForPlatform({ action, platform, isForDevice }); + } + + await this.$platformService.trackActionForPlatform({ action: LiveSyncTrackActionNames.DEVICE_INFO, platform, isForDevice: !options.device.isEmulator, deviceOsVersion: options.device.deviceInfo.version }); + } + + private async installedCachedAppPackage(platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { + const rebuildInfo = _.find(options.rebuiltInformation, info => info.isEmulator === options.device.isEmulator && info.platform === platform); + + if (rebuildInfo) { + // Case where we have three devices attached, a change that requires build is found, + // we'll rebuild the app only for the first device, but we should install new package on all three devices. + await this.$platformService.installApplication(options.device, { release: false }, options.projectData, rebuildInfo.pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); + return rebuildInfo.pathToBuildItem; + } + + return null; + } + + private async initialSync(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { + const preparedPlatforms: string[] = []; + const rebuiltInformation: ILiveSyncBuildInfo[] = []; + + const settings = this.getDefaultLatestAppPackageInstalledSettings(); + // Now fullSync + const deviceAction = async (device: Mobile.IDevice): Promise => { + try { + const platform = device.deviceInfo.platform; + const deviceBuildInfoDescriptor = _.find(deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); + + await this.ensureLatestAppPackageIsInstalledOnDevice({ + device, + preparedPlatforms, + rebuiltInformation, + projectData, + deviceBuildInfoDescriptor, + liveSyncData, + settings + }, { skipNativePrepare: deviceBuildInfoDescriptor.skipNativePrepare }); + + const liveSyncResultInfo = await this.getLiveSyncService(platform).fullSync({ + projectData, device, + syncAllFiles: liveSyncData.watchAllFiles, + useLiveEdit: liveSyncData.useLiveEdit, + watch: !liveSyncData.skipWatcher + }); + await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); + await this.refreshApplication(projectData, liveSyncResultInfo, liveSyncData.debugOptions, deviceBuildInfoDescriptor.outputPath); + + this.emit(LiveSyncEvents.liveSyncStarted, { + projectDir: projectData.projectDir, + deviceIdentifier: device.deviceInfo.identifier, + applicationIdentifier: projectData.projectId + }); + } catch (err) { + this.$logger.warn(`Unable to apply changes on device: ${device.deviceInfo.identifier}. Error is: ${err.message}.`); + + this.emit(LiveSyncEvents.liveSyncError, { + error: err, + deviceIdentifier: device.deviceInfo.identifier, + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId + }); + + await this.stopLiveSync(projectData.projectDir, [device.deviceInfo.identifier], { shouldAwaitAllActions: false }); + } + }; + + // Execute the action only on the deviceDescriptors passed to initialSync. + // In case where we add deviceDescriptors to already running application, we've already executed initialSync for them. + await this.addActionToChain(projectData.projectDir, () => this.$devicesService.execute(deviceAction, (device: Mobile.IDevice) => _.some(deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier))); + } + + private getDefaultLatestAppPackageInstalledSettings(): ILatestAppPackageInstalledSettings { + return { + [this.$devicePlatformsConstants.Android]: { + [DeviceTypes.Device]: false, + [DeviceTypes.Emulator]: false + }, + [this.$devicePlatformsConstants.iOS]: { + [DeviceTypes.Device]: false, + [DeviceTypes.Emulator]: false + } + }; + } + + private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo): Promise { + const pattern = [APP_FOLDER_NAME]; + + if (liveSyncData.watchAllFiles) { + const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); + pattern.push(PACKAGE_JSON_FILE_NAME); // watch only production node_module/packages same one prepare uses - for (let index in productionDependencies) { - pattern.push("node_modules/" + productionDependencies[index].name); + for (const index in productionDependencies) { + pattern.push(productionDependencies[index].directory); } } - let watcher = choki.watch(pattern, { ignoreInitial: true, cwd: syncWorkingDirectory }).on("all", (event: string, filePath: string) => { - that.$dispatcher.dispatch(async () => { - try { - filePath = path.join(syncWorkingDirectory, filePath); - for (let i = 0; i < onChangedActions.length; i++) { - that.$logger.trace(`Event '${event}' triggered for path: '${filePath}'`); - await onChangedActions[i](event, filePath, that.$dispatcher); - } - } catch (err) { - that.$logger.info(`Unable to sync file ${filePath}. Error is:${err.message}`.red.bold); - that.$logger.info("Try saving it again or restart the livesync operation."); + const currentWatcherInfo = this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo; + + if (!currentWatcherInfo || currentWatcherInfo.pattern !== pattern) { + if (currentWatcherInfo) { + currentWatcherInfo.watcher.close(); + } + + let filesToSync: string[] = []; + let filesToRemove: string[] = []; + let timeoutTimer: NodeJS.Timer; + + const startTimeout = () => { + timeoutTimer = setTimeout(async () => { + // Push actions to the queue, do not start them simultaneously + await this.addActionToChain(projectData.projectDir, async () => { + if (filesToSync.length || filesToRemove.length) { + try { + const currentFilesToSync = _.cloneDeep(filesToSync); + filesToSync = []; + + const currentFilesToRemove = _.cloneDeep(filesToRemove); + filesToRemove = []; + + const allModifiedFiles = [].concat(currentFilesToSync).concat(currentFilesToRemove); + const preparedPlatforms: string[] = []; + const rebuiltInformation: ILiveSyncBuildInfo[] = []; + + const latestAppPackageInstalledSettings = this.getDefaultLatestAppPackageInstalledSettings(); + + await this.$devicesService.execute(async (device: Mobile.IDevice) => { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir]; + const deviceBuildInfoDescriptor = _.find(liveSyncProcessInfo.deviceDescriptors, dd => dd.identifier === device.deviceInfo.identifier); + + const appInstalledOnDeviceResult = await this.ensureLatestAppPackageIsInstalledOnDevice({ + device, + preparedPlatforms, + rebuiltInformation, + projectData, + deviceBuildInfoDescriptor, + settings: latestAppPackageInstalledSettings, + modifiedFiles: allModifiedFiles + }, { skipNativePrepare: deviceBuildInfoDescriptor.skipNativePrepare }); + + const service = this.getLiveSyncService(device.deviceInfo.platform); + const settings: ILiveSyncWatchInfo = { + projectData, + filesToRemove: currentFilesToRemove, + filesToSync: currentFilesToSync, + isReinstalled: appInstalledOnDeviceResult.appInstalled, + syncAllFiles: liveSyncData.watchAllFiles, + useLiveEdit: liveSyncData.useLiveEdit + }; + + const liveSyncResultInfo = await service.liveSyncWatchAction(device, settings); + await this.refreshApplication(projectData, liveSyncResultInfo, liveSyncData.debugOptions, deviceBuildInfoDescriptor.outputPath); + }, + (device: Mobile.IDevice) => { + const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir]; + return liveSyncProcessInfo && _.some(liveSyncProcessInfo.deviceDescriptors, deviceDescriptor => deviceDescriptor.identifier === device.deviceInfo.identifier); + } + ); + } catch (err) { + const allErrors = (err).allErrors; + + if (allErrors && _.isArray(allErrors)) { + for (const deviceError of allErrors) { + this.$logger.warn(`Unable to apply changes for device: ${deviceError.deviceIdentifier}. Error is: ${deviceError.message}.`); + + this.emit(LiveSyncEvents.liveSyncError, { + error: deviceError, + deviceIdentifier: deviceError.deviceIdentifier, + projectDir: projectData.projectDir, + applicationIdentifier: projectData.projectId + }); + + await this.stopLiveSync(projectData.projectDir, [deviceError.deviceIdentifier], { shouldAwaitAllActions: false }); + } + } + } + } + }); + }, 250); + + this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer; + }; + + await this.$hooksService.executeBeforeHooks('watch', { + hookArgs: { + projectData } }); - }); - this.$processService.attachToProcessExitSignals(this, () => { - watcher.close(pattern); - }); + const watcherOptions: choki.WatchOptions = { + ignoreInitial: true, + cwd: liveSyncData.projectDir, + awaitWriteFinish: { + pollInterval: 100, + stabilityThreshold: 500 + }, + ignored: ["**/.*", ".*"] // hidden files + }; + + const watcher = choki.watch(pattern, watcherOptions) + .on("all", async (event: string, filePath: string) => { + clearTimeout(timeoutTimer); + + filePath = path.join(liveSyncData.projectDir, filePath); + + this.$logger.trace(`Chokidar raised event ${event} for ${filePath}.`); - this.$dispatcher.run(); + if (event === "add" || event === "addDir" || event === "change" /* <--- what to do when change event is raised ? */) { + filesToSync.push(filePath); + } else if (event === "unlink" || event === "unlinkDir") { + filesToRemove.push(filePath); + } + + // Do not sync typescript files directly - wait for javascript changes to occur in order to restart the app only once + if (path.extname(filePath) !== FileExtensions.TYPESCRIPT_FILE) { + startTimeout(); + } + }); + + this.liveSyncProcessesInfo[liveSyncData.projectDir].watcherInfo = { watcher, pattern }; + this.liveSyncProcessesInfo[liveSyncData.projectDir].timer = timeoutTimer; + + this.$processService.attachToProcessExitSignals(this, () => { + _.keys(this.liveSyncProcessesInfo).forEach(projectDir => { + // Do not await here, we are in process exit's handler. + this.stopLiveSync(projectDir); + }); + }); + + this.$devicesService.on("deviceLost", async (device: Mobile.IDevice) => { + await this.stopLiveSync(projectData.projectDir, [device.deviceInfo.identifier]); + }); + } } + + private async addActionToChain(projectDir: string, action: () => Promise): Promise { + const liveSyncInfo = this.liveSyncProcessesInfo[projectDir]; + if (liveSyncInfo) { + liveSyncInfo.actionsChain = liveSyncInfo.actionsChain.then(async () => { + if (!liveSyncInfo.isStopped) { + liveSyncInfo.currentSyncAction = action(); + const res = await liveSyncInfo.currentSyncAction; + return res; + } + }); + + const result = await liveSyncInfo.actionsChain; + return result; + } + } + +} + +$injector.register("liveSyncService", LiveSyncService); + +/** + * This class is used only for old versions of nativescript-dev-typescript plugin. + * It should be replaced with liveSyncService.isInitalized. + * Consider adding get and set methods for isInitialized, + * so whenever someone tries to access the value of isInitialized, + * they'll get a warning to update the plugins (like nativescript-dev-typescript). + */ +export class DeprecatedUsbLiveSyncService { + public isInitialized = false; } -$injector.register("usbLiveSyncService", LiveSyncService); +$injector.register("usbLiveSyncService", DeprecatedUsbLiveSyncService); diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service-base.ts new file mode 100644 index 0000000000..27500ab6d9 --- /dev/null +++ b/lib/services/livesync/platform-livesync-service-base.ts @@ -0,0 +1,142 @@ +import * as path from "path"; +import * as util from "util"; +import { APP_FOLDER_NAME } from "../../constants"; + +export abstract class PlatformLiveSyncServiceBase { + private _deviceLiveSyncServicesCache: IDictionary = {}; + + constructor(protected $fs: IFileSystem, + protected $logger: ILogger, + protected $platformsData: IPlatformsData, + protected $projectFilesManager: IProjectFilesManager, + private $devicePathProvider: IDevicePathProvider, + private $projectFilesProvider: IProjectFilesProvider) { } + + public getDeviceLiveSyncService(device: Mobile.IDevice, applicationIdentifier: string): INativeScriptDeviceLiveSyncService { + const key = device.deviceInfo.identifier + applicationIdentifier; + if (!this._deviceLiveSyncServicesCache[key]) { + this._deviceLiveSyncServicesCache[key] = this._getDeviceLiveSyncService(device); + } + + return this._deviceLiveSyncServicesCache[key]; + } + + protected abstract _getDeviceLiveSyncService(device: Mobile.IDevice): INativeScriptDeviceLiveSyncService; + + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { + if (liveSyncInfo.isFullSync || liveSyncInfo.modifiedFilesData.length) { + const deviceLiveSyncService = this.getDeviceLiveSyncService(liveSyncInfo.deviceAppData.device, projectData.projectId); + this.$logger.info("Refreshing application..."); + await deviceLiveSyncService.refreshApplication(projectData, liveSyncInfo); + } + } + + public async fullSync(syncInfo: IFullSyncInfo): Promise { + const projectData = syncInfo.projectData; + const device = syncInfo.device; + const deviceLiveSyncService = this.getDeviceLiveSyncService(device, syncInfo.projectData.projectId); + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const deviceAppData = await this.getAppData(syncInfo); + + if (deviceLiveSyncService.beforeLiveSyncAction) { + await deviceLiveSyncService.beforeLiveSyncAction(deviceAppData); + } + + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, []); + const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, true); + + return { + modifiedFilesData, + isFullSync: true, + deviceAppData + }; + } + + public async liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { + const projectData = liveSyncInfo.projectData; + const syncInfo = _.merge({ device, watch: true }, liveSyncInfo); + const deviceAppData = await this.getAppData(syncInfo); + + const modifiedLocalToDevicePaths: Mobile.ILocalToDevicePathData[] = []; + if (liveSyncInfo.filesToSync.length) { + const filesToSync = liveSyncInfo.filesToSync; + const mappedFiles = _.map(filesToSync, filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)); + + // Some plugins modify platforms dir on afterPrepare (check nativescript-dev-sass) - we want to sync only existing file. + const existingFiles = mappedFiles.filter(m => m && this.$fs.exists(m)); + this.$logger.trace("Will execute livesync for files: ", existingFiles); + const skippedFiles = _.difference(mappedFiles, existingFiles); + if (skippedFiles.length) { + this.$logger.trace("The following files will not be synced as they do not exist:", skippedFiles); + } + + if (existingFiles.length) { + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, + projectFilesPath, existingFiles, []); + modifiedLocalToDevicePaths.push(...localToDevicePaths); + await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, false); + } + } + + if (liveSyncInfo.filesToRemove.length) { + const filePaths = liveSyncInfo.filesToRemove; + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + + const mappedFiles = _(filePaths) + .map(filePath => this.$projectFilesProvider.mapFilePath(filePath, device.deviceInfo.platform, projectData)) + .filter(filePath => !!filePath) + .value(); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, mappedFiles, []); + modifiedLocalToDevicePaths.push(...localToDevicePaths); + + const deviceLiveSyncService = this.getDeviceLiveSyncService(device, projectData.projectId); + await deviceLiveSyncService.removeFiles(deviceAppData, localToDevicePaths); + } + + return { + modifiedFilesData: modifiedLocalToDevicePaths, + isFullSync: liveSyncInfo.isReinstalled, + deviceAppData + }; + } + + protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { + let transferredFiles = localToDevicePaths; + if (isFullSync) { + transferredFiles = await deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); + } else { + await deviceAppData.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); + } + + this.logFilesSyncInformation(transferredFiles, "Successfully transferred %s.", this.$logger.info); + + return transferredFiles; + } + + protected async getAppData(syncInfo: IFullSyncInfo): Promise { + const deviceProjectRootOptions: IDeviceProjectRootOptions = _.assign({ appIdentifier: syncInfo.projectData.projectId }, syncInfo); + return { + appIdentifier: syncInfo.projectData.projectId, + device: syncInfo.device, + platform: syncInfo.device.deviceInfo.platform, + getDeviceProjectRootPath: () => this.$devicePathProvider.getDeviceProjectRootPath(syncInfo.device, deviceProjectRootOptions), + deviceSyncZipPath: this.$devicePathProvider.getDeviceSyncZipPath(syncInfo.device), + isLiveSyncSupported: async () => true + }; + } + + private logFilesSyncInformation(localToDevicePaths: Mobile.ILocalToDevicePathData[], message: string, action: Function): void { + if (localToDevicePaths && localToDevicePaths.length < 10) { + _.each(localToDevicePaths, (file: Mobile.ILocalToDevicePathData) => { + action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow)); + }); + } else { + action.call(this.$logger, util.format(message, "all files")); + } + } + +} diff --git a/lib/services/livesync/platform-livesync-service.ts b/lib/services/livesync/platform-livesync-service.ts deleted file mode 100644 index 1aea236585..0000000000 --- a/lib/services/livesync/platform-livesync-service.ts +++ /dev/null @@ -1,263 +0,0 @@ -import syncBatchLib = require("../../common/services/livesync/sync-batch"); -import * as path from "path"; -import * as minimatch from "minimatch"; -import * as util from "util"; -import * as helpers from "../../common/helpers"; - -const livesyncInfoFileName = ".nslivesyncinfo"; - -export abstract class PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { - private batch: IDictionary = Object.create(null); - private livesyncData: IDictionary = Object.create(null); - - protected liveSyncData: ILiveSyncData; - - constructor(_liveSyncData: ILiveSyncData, - private $devicesService: Mobile.IDevicesService, - private $mobileHelper: Mobile.IMobileHelper, - private $logger: ILogger, - private $options: IOptions, - private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, - private $injector: IInjector, - private $projectFilesManager: IProjectFilesManager, - private $projectFilesProvider: IProjectFilesProvider, - private $platformService: IPlatformService, - private $projectChangesService: IProjectChangesService, - private $liveSyncProvider: ILiveSyncProvider, - private $fs: IFileSystem) { - this.liveSyncData = _liveSyncData; - } - - public async fullSync(projectData: IProjectData, postAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise { - let appIdentifier = this.liveSyncData.appIdentifier; - let platform = this.liveSyncData.platform; - let projectFilesPath = this.liveSyncData.projectFilesPath; - let canExecute = this.getCanExecuteAction(platform, appIdentifier); - let action = async (device: Mobile.IDevice): Promise => { - await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); - - let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device); - let localToDevicePaths: Mobile.ILocalToDevicePathData[] = null; - if (await this.shouldTransferAllFiles(platform, deviceAppData, projectData)) { - localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, this.liveSyncData.excludedProjectDirsAndFiles); - await this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, true); - await device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(platform, projectData), await this.getLiveSyncInfoFilePath(deviceAppData), appIdentifier); - } - - if (postAction) { - await this.finishLivesync(deviceAppData); - await postAction(deviceAppData); - return; - } - - await this.refreshApplication(deviceAppData, localToDevicePaths, true, projectData); - await this.finishLivesync(deviceAppData); - }; - await this.$devicesService.execute(action, canExecute); - } - - public async partialSync(event: string, filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise { - if (this.isFileExcluded(filePath, this.liveSyncData.excludedProjectDirsAndFiles)) { - this.$logger.trace(`Skipping livesync for changed file ${filePath} as it is excluded in the patterns: ${this.liveSyncData.excludedProjectDirsAndFiles.join(", ")}`); - return; - } - - if (event === "add" || event === "addDir" || event === "change") { - this.batchSync(filePath, dispatcher, afterFileSyncAction, projectData); - } else if (event === "unlink" || event === "unlinkDir") { - await this.syncRemovedFile(filePath, afterFileSyncAction, projectData); - } - } - - protected getCanExecuteAction(platform: string, appIdentifier: string): (dev: Mobile.IDevice) => boolean { - let isTheSamePlatformAction = ((device: Mobile.IDevice) => device.deviceInfo.platform.toLowerCase() === platform.toLowerCase()); - if (this.$options.device) { - return (device: Mobile.IDevice): boolean => isTheSamePlatformAction(device) && device.deviceInfo.identifier === this.$devicesService.getDeviceByDeviceOption().deviceInfo.identifier; - } - return isTheSamePlatformAction; - } - - public async refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], isFullSync: boolean, projectData: IProjectData): Promise { - let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(deviceAppData.device.deviceInfo.platform, deviceAppData.device); - this.$logger.info("Refreshing application..."); - await deviceLiveSyncService.refreshApplication(deviceAppData, localToDevicePaths, isFullSync, projectData); - } - - protected async finishLivesync(deviceAppData: Mobile.IDeviceAppData): Promise { - // This message is important because it signals Visual Studio Code that livesync has finished and debugger can be attached. - this.$logger.info(`Successfully synced application ${deviceAppData.appIdentifier} on device ${deviceAppData.device.deviceInfo.identifier}.\n`); - } - - protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { - this.$logger.info("Transferring project files..."); - let canTransferDirectory = isFullSync && (this.$devicesService.isAndroidDevice(deviceAppData.device) || this.$devicesService.isiOSSimulator(deviceAppData.device)); - if (canTransferDirectory) { - await deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); - } else { - await this.$liveSyncProvider.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync); - } - this.logFilesSyncInformation(localToDevicePaths, "Successfully transferred %s.", this.$logger.info); - } - - protected resolveDeviceSpecificLiveSyncService(platform: string, device: Mobile.IDevice): INativeScriptDeviceLiveSyncService { - return this.$injector.resolve(this.$liveSyncProvider.deviceSpecificLiveSyncServices[platform.toLowerCase()], { _device: device }); - } - - private isFileExcluded(filePath: string, excludedPatterns: string[]): boolean { - let isFileExcluded = false; - _.each(excludedPatterns, pattern => { - if (minimatch(filePath, pattern, { nocase: true })) { - isFileExcluded = true; - return false; - } - }); - - // skip hidden files, to prevent reload of the app for hidden files - // created temporarily by the IDEs - if (this.isUnixHiddenPath(filePath)) { - isFileExcluded = true; - } - - return isFileExcluded; - } - - private isUnixHiddenPath(filePath: string): boolean { - return (/(^|\/)\.[^\/\.]/g).test(filePath); - } - - private batchSync(filePath: string, dispatcher: IFutureDispatcher, afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): void { - let platformBatch: ISyncBatch = this.batch[this.liveSyncData.platform]; - if (!platformBatch || !platformBatch.syncPending) { - let done = async () => { - dispatcher.dispatch(async () => { - try { - for (let platform in this.batch) { - let batch = this.batch[platform]; - await batch.syncFiles(async (filesToSync: string[]) => { - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - await this.$platformService.preparePlatform(this.liveSyncData.platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, { provision: this.$options.provision, sdk: this.$options.sdk }, filesToSync); - let canExecute = this.getCanExecuteAction(this.liveSyncData.platform, this.liveSyncData.appIdentifier); - let deviceFileAction = (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => this.transferFiles(deviceAppData, localToDevicePaths, this.liveSyncData.projectFilesPath, !filePath); - let action = this.getSyncAction(filesToSync, deviceFileAction, afterFileSyncAction, projectData); - await this.$devicesService.execute(action, canExecute); - }); - } - } catch (err) { - this.$logger.warn(`Unable to sync files. Error is:`, err.message); - } - }); - }; - - this.batch[this.liveSyncData.platform] = this.$injector.resolve(syncBatchLib.SyncBatch, { done: done }); - this.livesyncData[this.liveSyncData.platform] = this.liveSyncData; - } - - this.batch[this.liveSyncData.platform].addFile(filePath); - } - - private async syncRemovedFile(filePath: string, - afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, projectData: IProjectData): Promise { - let deviceFilesAction = (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => { - let deviceLiveSyncService = this.resolveDeviceSpecificLiveSyncService(this.liveSyncData.platform, deviceAppData.device); - return deviceLiveSyncService.removeFiles(this.liveSyncData.appIdentifier, localToDevicePaths, projectData.projectId); - }; - let canExecute = this.getCanExecuteAction(this.liveSyncData.platform, this.liveSyncData.appIdentifier); - let action = this.getSyncAction([filePath], deviceFilesAction, afterFileSyncAction, projectData); - await this.$devicesService.execute(action, canExecute); - } - - private getSyncAction( - filesToSync: string[], - fileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, - afterFileSyncAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise, - projectData: IProjectData): (device: Mobile.IDevice) => Promise { - let action = async (device: Mobile.IDevice): Promise => { - let deviceAppData: Mobile.IDeviceAppData = null; - let localToDevicePaths: Mobile.ILocalToDevicePathData[] = null; - let isFullSync = false; - - if (this.$projectChangesService.currentChanges.changesRequireBuild) { - let buildConfig: IBuildConfig = { - buildForDevice: !device.isEmulator, - projectDir: this.$options.path, - release: this.$options.release, - teamId: this.$options.teamId, - device: this.$options.device, - provision: this.$options.provision, - }; - let platform = device.deviceInfo.platform; - if (this.$platformService.shouldBuild(platform, projectData, buildConfig)) { - await this.$platformService.buildPlatform(platform, buildConfig, projectData); - } - - await this.$platformService.installApplication(device, buildConfig, projectData); - deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device); - isFullSync = true; - } else { - deviceAppData = this.$deviceAppDataFactory.create(this.liveSyncData.appIdentifier, this.$mobileHelper.normalizePlatformName(this.liveSyncData.platform), device); - const mappedFiles = filesToSync.map((file: string) => this.$projectFilesProvider.mapFilePath(file, device.deviceInfo.platform, projectData)); - - // Some plugins modify platforms dir on afterPrepare (check nativescript-dev-sass) - we want to sync only existing file. - const existingFiles = mappedFiles.filter(m => this.$fs.exists(m)); - - this.$logger.trace("Will execute livesync for files: ", existingFiles); - - const skippedFiles = _.difference(mappedFiles, existingFiles); - - if (skippedFiles.length) { - this.$logger.trace("The following files will not be synced as they do not exist:", skippedFiles); - } - - localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, this.liveSyncData.projectFilesPath, mappedFiles, this.liveSyncData.excludedProjectDirsAndFiles); - - await fileSyncAction(deviceAppData, localToDevicePaths); - } - - if (!afterFileSyncAction) { - await this.refreshApplication(deviceAppData, localToDevicePaths, isFullSync, projectData); - } - - await device.fileSystem.putFile(this.$projectChangesService.getPrepareInfoFilePath(device.deviceInfo.platform, projectData), await this.getLiveSyncInfoFilePath(deviceAppData), this.liveSyncData.appIdentifier); - - await this.finishLivesync(deviceAppData); - - if (afterFileSyncAction) { - await afterFileSyncAction(deviceAppData, localToDevicePaths); - } - }; - - return action; - } - - private async shouldTransferAllFiles(platform: string, deviceAppData: Mobile.IDeviceAppData, projectData: IProjectData): Promise { - try { - if (this.$options.clean) { - return false; - } - let fileText = await this.$platformService.readFile(deviceAppData.device, await this.getLiveSyncInfoFilePath(deviceAppData), projectData); - let remoteLivesyncInfo: IPrepareInfo = JSON.parse(fileText); - let localPrepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - return remoteLivesyncInfo.time !== localPrepareInfo.time; - } catch (e) { - return true; - } - } - - private async getLiveSyncInfoFilePath(deviceAppData: Mobile.IDeviceAppData): Promise { - let deviceRootPath = path.dirname(await deviceAppData.getDeviceProjectRootPath()); - let deviceFilePath = helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, livesyncInfoFileName)); - return deviceFilePath; - } - - private logFilesSyncInformation(localToDevicePaths: Mobile.ILocalToDevicePathData[], message: string, action: Function): void { - if (localToDevicePaths && localToDevicePaths.length < 10) { - _.each(localToDevicePaths, (file: Mobile.ILocalToDevicePathData) => { - action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow)); - }); - } else { - action.call(this.$logger, util.format(message, "all files")); - } - } -} - -$injector.register("platformLiveSyncService", PlatformLiveSyncServiceBase); diff --git a/lib/services/local-build-service.ts b/lib/services/local-build-service.ts index f76b8ad6bc..c705416d1f 100644 --- a/lib/services/local-build-service.ts +++ b/lib/services/local-build-service.ts @@ -10,7 +10,13 @@ export class LocalBuildService extends EventEmitter { public async build(platform: string, platformBuildOptions: IPlatformBuildData, platformTemplate?: string): Promise { this.$projectData.initializeProjectData(platformBuildOptions.projectDir); - await this.$platformService.preparePlatform(platform, platformBuildOptions, platformTemplate, this.$projectData, { provision: platformBuildOptions.provision, sdk: null }); + await this.$platformService.preparePlatform(platform, platformBuildOptions, platformTemplate, this.$projectData, { + provision: platformBuildOptions.provision, + teamId: platformBuildOptions.teamId, + sdk: null, + frameworkPath: null, + ignoreScripts: false + }); const handler = (data: any) => { data.projectDir = platformBuildOptions.projectDir; this.emit(BUILD_OUTPUT_EVENT_NAME, data); diff --git a/lib/services/platform-project-service-base.ts b/lib/services/platform-project-service-base.ts index b2cb7f7805..6672516cb6 100644 --- a/lib/services/platform-project-service-base.ts +++ b/lib/services/platform-project-service-base.ts @@ -11,11 +11,11 @@ export class PlatformProjectServiceBase extends EventEmitter implements IPlatfor } protected getAllNativeLibrariesForPlugin(pluginData: IPluginData, platform: string, filter: (fileName: string, _pluginPlatformsFolderPath: string) => boolean): string[] { - let pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, platform), - nativeLibraries: string[] = []; + const pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, platform); + let nativeLibraries: string[] = []; if (pluginPlatformsFolderPath && this.$fs.exists(pluginPlatformsFolderPath)) { - let platformsContents = this.$fs.readDirectory(pluginPlatformsFolderPath); + const platformsContents = this.$fs.readDirectory(pluginPlatformsFolderPath); nativeLibraries = _(platformsContents) .filter(platformItemName => filter(platformItemName, pluginPlatformsFolderPath)) .value(); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 1df21befb2..268c2c2095 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -1,6 +1,7 @@ import * as path from "path"; import * as shell from "shelljs"; import * as constants from "../constants"; +import { Configurations } from "../common/constants"; import * as helpers from "../common/helpers"; import * as semver from "semver"; import { EventEmitter } from "events"; @@ -8,7 +9,7 @@ import { AppFilesUpdater } from "./app-files-updater"; import { attachAwaitDetach } from "../common/helpers"; import * as temp from "temp"; temp.track(); -let clui = require("clui"); +const clui = require("clui"); const buildInfoFileName = ".nsbuildinfo"; @@ -34,21 +35,18 @@ export class PlatformService extends EventEmitter implements IPlatformService { private $projectFilesManager: IProjectFilesManager, private $mobileHelper: Mobile.IMobileHelper, private $hostInfo: IHostInfo, + private $devicePathProvider: IDevicePathProvider, private $xmlValidator: IXmlValidator, private $npm: INodePackageManager, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, private $projectChangesService: IProjectChangesService, - private $emulatorPlatformService: IEmulatorPlatformService, - private $analyticsService: IAnalyticsService, - private $messages: IMessages, - private $staticConfig: Config.IStaticConfig) { + private $analyticsService: IAnalyticsService) { super(); } - public async cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, framworkPath?: string): Promise { - for (let platform of platforms) { - let version: string = this.getCurrentPlatformVersion(platform, projectData); + public async cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, framworkPath?: string): Promise { + for (const platform of platforms) { + const version: string = this.getCurrentPlatformVersion(platform, projectData); let platformWithVersion: string = platform; if (version !== undefined) { @@ -56,53 +54,48 @@ export class PlatformService extends EventEmitter implements IPlatformService { } await this.removePlatforms([platform], projectData); - await this.addPlatforms([platformWithVersion], platformTemplate, projectData, platformSpecificData); + await this.addPlatforms([platformWithVersion], platformTemplate, projectData, config); } } - public async addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, frameworkPath?: string): Promise { - let platformsDir = projectData.platformsDir; + public async addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, frameworkPath?: string): Promise { + const platformsDir = projectData.platformsDir; this.$fs.ensureDirectoryExists(platformsDir); - for (let platform of platforms) { - await this.addPlatform(platform.toLowerCase(), platformTemplate, projectData, platformSpecificData, frameworkPath); + for (const platform of platforms) { + this.validatePlatform(platform, projectData); + const platformPath = path.join(projectData.platformsDir, platform); + + if (this.$fs.exists(platformPath)) { + this.$errors.failWithoutHelp(`Platform ${platform} already added`); + } + + await this.addPlatform(platform.toLowerCase(), platformTemplate, projectData, config, frameworkPath); } } private getCurrentPlatformVersion(platform: string, projectData: IProjectData): string { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let currentPlatformData: any = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const currentPlatformData: any = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); let version: string; if (currentPlatformData && currentPlatformData[constants.VERSION_STRING]) { version = currentPlatformData[constants.VERSION_STRING]; - }; + } return version; } - private async addPlatform(platformParam: string, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, frameworkPath?: string): Promise { - let data = platformParam.split("@"), - platform = data[0].toLowerCase(), - version = data[1]; + private async addPlatform(platformParam: string, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, frameworkPath?: string, nativePrepare?: INativePrepare): Promise { + const data = platformParam.split("@"); + const platform = data[0].toLowerCase(); + let version = data[1]; - this.validatePlatform(platform, projectData); - - let platformPath = path.join(projectData.platformsDir, platform); - - if (this.$fs.exists(platformPath)) { - this.$errors.failWithoutHelp("Platform %s already added", platform); - } - - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); if (version === undefined) { version = this.getCurrentPlatformVersion(platform, projectData); } - // Copy platform specific files in platforms dir - let platformProjectService = platformData.platformProjectService; - await platformProjectService.validate(projectData); - // Log the values for project this.$logger.trace("Creating NativeScript project for the %s platform", platform); this.$logger.trace("Path: %s", platformData.projectRoot); @@ -112,7 +105,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out("Copying template files..."); let packageToInstall = ""; - let npmOptions: IStringDictionary = { + const npmOptions: IStringDictionary = { pathToSave: path.join(projectData.platformsDir, platform), dependencyType: "save" }; @@ -122,15 +115,17 @@ export class PlatformService extends EventEmitter implements IPlatformService { npmOptions["version"] = version; } - let spinner = new clui.Spinner("Installing " + packageToInstall); - let projectDir = projectData.projectDir; + const spinner = new clui.Spinner("Installing " + packageToInstall); + const projectDir = projectData.projectDir; + const platformPath = path.join(projectData.platformsDir, platform); + try { spinner.start(); - let downloadedPackagePath = await this.$npmInstallationManager.install(packageToInstall, projectDir, npmOptions); + const downloadedPackagePath = await this.$npmInstallationManager.install(packageToInstall, projectDir, npmOptions); let frameworkDir = path.join(downloadedPackagePath, constants.PROJECT_FRAMEWORK_FOLDER_NAME); frameworkDir = path.resolve(frameworkDir); - let coreModuleName = await this.addPlatformCore(platformData, frameworkDir, platformTemplate, projectData, platformSpecificData); + const coreModuleName = await this.addPlatformCore(platformData, frameworkDir, platformTemplate, projectData, config, nativePrepare); await this.$npm.uninstall(coreModuleName, { save: true }, projectData.projectDir); } catch (err) { this.$fs.deleteDirectory(platformPath); @@ -139,33 +134,41 @@ export class PlatformService extends EventEmitter implements IPlatformService { spinner.stop(); } + this.$fs.ensureDirectoryExists(platformPath); this.$logger.out("Project successfully created."); - } - private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - let coreModuleData = this.$fs.readJson(path.join(frameworkDir, "../", "package.json")); - let installedVersion = coreModuleData.version; - let coreModuleName = coreModuleData.name; + private async addPlatformCore(platformData: IPlatformData, frameworkDir: string, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, nativePrepare?: INativePrepare): Promise { + const coreModuleData = this.$fs.readJson(path.join(frameworkDir, "..", "package.json")); + const installedVersion = coreModuleData.version; + const customTemplateOptions = await this.getPathToPlatformTemplate(platformTemplate, platformData.frameworkPackageName, projectData.projectDir); + config.pathToTemplate = customTemplateOptions && customTemplateOptions.pathToTemplate; - let customTemplateOptions = await this.getPathToPlatformTemplate(platformTemplate, platformData.frameworkPackageName, projectData.projectDir); - let pathToTemplate = customTemplateOptions && customTemplateOptions.pathToTemplate; - await platformData.platformProjectService.createProject(path.resolve(frameworkDir), installedVersion, projectData, pathToTemplate); - platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); - await platformData.platformProjectService.interpolateData(projectData, platformSpecificData); - platformData.platformProjectService.afterCreateProject(platformData.projectRoot, projectData); + if (!nativePrepare || !nativePrepare.skipNativePrepare) { + const platformDir = path.join(projectData.platformsDir, platformData.normalizedPlatformName.toLowerCase()); + this.$fs.deleteDirectory(platformDir); + await this.addPlatformCoreNative(platformData, frameworkDir, installedVersion, projectData, config); + } - let frameworkPackageNameData: any = { version: installedVersion }; + const frameworkPackageNameData: any = { version: installedVersion }; if (customTemplateOptions) { frameworkPackageNameData.template = customTemplateOptions.selectedTemplate; } this.$projectDataService.setNSValue(projectData.projectDir, platformData.frameworkPackageName, frameworkPackageNameData); + const coreModuleName = coreModuleData.name; return coreModuleName; } + private async addPlatformCoreNative(platformData: IPlatformData, frameworkDir: string, installedVersion: string, projectData: IProjectData, config: IPlatformOptions): Promise { + await platformData.platformProjectService.createProject(path.resolve(frameworkDir), installedVersion, projectData, config); + platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); + await platformData.platformProjectService.interpolateData(projectData, config); + platformData.platformProjectService.afterCreateProject(platformData.projectRoot, projectData); + } + private async getPathToPlatformTemplate(selectedTemplate: string, frameworkPackageName: string, projectDir: string): Promise<{ selectedTemplate: string, pathToTemplate: string }> { if (!selectedTemplate) { // read data from package.json's nativescript key @@ -175,18 +178,15 @@ export class PlatformService extends EventEmitter implements IPlatformService { } if (selectedTemplate) { - let tempDir = temp.mkdirSync("platform-template"); + const tempDir = temp.mkdirSync("platform-template"); + this.$fs.writeJson(path.join(tempDir, constants.PACKAGE_JSON_FILE_NAME), {}); try { - /* - * Output of npm.install is array of arrays. For example: - * [ [ 'test-android-platform-template@0.0.1', - * 'C:\\Users\\~1\\AppData\\Local\\Temp\\1\\platform-template11627-15560-rm3ngx\\node_modules\\test-android-platform-template', - * undefined, - * undefined, - * '..\\..\\..\\android-platform-template' ] ] - * Project successfully created. - */ - let pathToTemplate = (await this.$npm.install(selectedTemplate, tempDir))[0]; + const npmInstallResult = await this.$npm.install(selectedTemplate, tempDir, { + disableNpmInstall: false, + frameworkPath: null, + ignoreScripts: false + }); + const pathToTemplate = path.join(tempDir, constants.NODE_MODULES_FOLDER_NAME, npmInstallResult.name); return { selectedTemplate, pathToTemplate }; } catch (err) { this.$logger.trace("Error while trying to install specified template: ", err); @@ -202,12 +202,12 @@ export class PlatformService extends EventEmitter implements IPlatformService { return []; } - let subDirs = this.$fs.readDirectory(projectData.platformsDir); + const subDirs = this.$fs.readDirectory(projectData.platformsDir); return _.filter(subDirs, p => this.$platformsData.platformsNames.indexOf(p) > -1); } public getAvailablePlatforms(projectData: IProjectData): string[] { - let installedPlatforms = this.getInstalledPlatforms(projectData); + const installedPlatforms = this.getInstalledPlatforms(projectData); return _.filter(this.$platformsData.platformsNames, p => { return installedPlatforms.indexOf(p) < 0 && this.isPlatformSupportedForOS(p, projectData); // Only those not already installed }); @@ -216,39 +216,13 @@ export class PlatformService extends EventEmitter implements IPlatformService { public getPreparedPlatforms(projectData: IProjectData): string[] { return _.filter(this.$platformsData.platformsNames, p => { return this.isPlatformPrepared(p, projectData); }); } + public async preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, filesToSync?: Array, nativePrepare?: INativePrepare): Promise { + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const changesInfo = await this.initialPrepare(platform, platformData, appFilesUpdaterOptions, platformTemplate, projectData, config, nativePrepare); + const requiresNativePrepare = (!nativePrepare || !nativePrepare.skipNativePrepare) && changesInfo.nativePlatformStatus === constants.NativePlatformStatus.requiresPrepare; - public async preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, filesToSync?: Array): Promise { - this.validatePlatform(platform, projectData); - - await this.trackProjectType(projectData); - - //We need dev-dependencies here, so before-prepare hooks will be executed correctly. - try { - await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); - } catch (err) { - this.$logger.trace(err); - this.$errors.failWithoutHelp(`Unable to install dependencies. Make sure your package.json is valid and all dependencies are correct. Error is: ${err.message}`); - } - - let platformData = this.$platformsData.getPlatformData(platform, projectData); - await this.$pluginsService.validate(platformData, projectData); - - await this.ensurePlatformInstalled(platform, platformTemplate, projectData, platformSpecificData); - let changesInfo = this.$projectChangesService.checkForChanges(platform, projectData, { bundle: appFilesUpdaterOptions.bundle, release: appFilesUpdaterOptions.release, provision: platformSpecificData.provision }); - - this.$logger.trace("Changes info in prepare platform:", changesInfo); - - if (changesInfo.hasChanges) { - // android build artifacts need to be cleaned up when switching from release to debug builds - if (platform.toLowerCase() === "android") { - let previousPrepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - // clean up prepared plugins when not building for release - if (previousPrepareInfo && previousPrepareInfo.release !== appFilesUpdaterOptions.release) { - await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); - } - } - - await this.preparePlatformCore(platform, appFilesUpdaterOptions, projectData, platformSpecificData, changesInfo, filesToSync); + if (changesInfo.hasChanges || appFilesUpdaterOptions.bundle || requiresNativePrepare) { + await this.preparePlatformCore(platform, appFilesUpdaterOptions, projectData, config, changesInfo, filesToSync, nativePrepare); this.$projectChangesService.savePrepareInfo(platform, projectData); } else { this.$logger.out("Skipping prepare."); @@ -257,33 +231,93 @@ export class PlatformService extends EventEmitter implements IPlatformService { return true; } - public async validateOptions(provision: true | string, projectData: IProjectData, platform?: string): Promise { + public async validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string): Promise { if (platform) { platform = this.$mobileHelper.normalizePlatformName(platform); this.$logger.trace("Validate options for platform: " + platform); - let platformData = this.$platformsData.getPlatformData(platform, projectData); - return await platformData.platformProjectService.validateOptions(projectData.projectId, provision); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + return await platformData.platformProjectService.validateOptions(projectData.projectId, provision, teamId); } else { let valid = true; - for (let availablePlatform in this.$platformsData.availablePlatforms) { + for (const availablePlatform in this.$platformsData.availablePlatforms) { this.$logger.trace("Validate options for platform: " + availablePlatform); - let platformData = this.$platformsData.getPlatformData(availablePlatform, projectData); - valid = valid && await platformData.platformProjectService.validateOptions(projectData.projectId, provision); + const platformData = this.$platformsData.getPlatformData(availablePlatform, projectData); + valid = valid && await platformData.platformProjectService.validateOptions(projectData.projectId, provision, teamId); } return valid; } } + private async cleanProject(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformData: IPlatformData, projectData: IProjectData): Promise { + // android build artifacts need to be cleaned up + // when switching between debug, release and webpack builds + if (platform.toLowerCase() !== "android") { + return; + } + + const previousPrepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); + if (!previousPrepareInfo) { + return; + } + + const { release: previousWasRelease, bundle: previousWasBundle } = previousPrepareInfo; + const { release: currentIsRelease, bundle: currentIsBundle } = appFilesUpdaterOptions; + if ((previousWasRelease !== currentIsRelease) || (previousWasBundle !== currentIsBundle)) { + await platformData.platformProjectService.cleanProject(platformData.projectRoot, projectData); + } + } + + private async initialPrepare(platform: string, platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, nativePrepare?: INativePrepare): Promise { + this.validatePlatform(platform, projectData); + + await this.trackProjectType(projectData); + + //We need dev-dependencies here, so before-prepare hooks will be executed correctly. + try { + await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); + } catch (err) { + this.$logger.trace(err); + this.$errors.failWithoutHelp(`Unable to install dependencies. Make sure your package.json is valid and all dependencies are correct. Error is: ${err.message}`); + } + + await this.ensurePlatformInstalled(platform, platformTemplate, projectData, config, nativePrepare); + + const bundle = appFilesUpdaterOptions.bundle; + const nativePlatformStatus = (nativePrepare && nativePrepare.skipNativePrepare) ? constants.NativePlatformStatus.requiresPlatformAdd : constants.NativePlatformStatus.requiresPrepare; + const changesInfo = await this.$projectChangesService.checkForChanges(platform, projectData, { bundle, release: appFilesUpdaterOptions.release, provision: config.provision, teamId: config.teamId, nativePlatformStatus }); + + this.$logger.trace("Changes info in prepare platform:", changesInfo); + return changesInfo; + } + /* Hooks are expected to use "filesToSync" parameter, as to give plugin authors additional information about the sync process.*/ @helpers.hook('prepare') - private async preparePlatformCore(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, filesToSync?: Array): Promise { + private async preparePlatformCore(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, filesToSync?: Array, nativePrepare?: INativePrepare): Promise { this.$logger.out("Preparing project..."); - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: appFilesUpdaterOptions.release }); + await this.preparePlatformCoreJS(platform, platformData, appFilesUpdaterOptions, projectData, platformSpecificData, changesInfo, filesToSync, projectFilesConfig); + + if (!nativePrepare || !nativePrepare.skipNativePrepare) { + await this.preparePlatformCoreNative(platform, platformData, appFilesUpdaterOptions, projectData, platformSpecificData, changesInfo, projectFilesConfig); + } + const directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; + if (!changesInfo || !changesInfo.modulesChanged) { + excludedDirs.push(constants.TNS_MODULES_FOLDER_NAME); + } + + this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, projectFilesConfig, excludedDirs); + + this.$logger.out(`Project successfully prepared (${platform})`); + } + + private async preparePlatformCoreJS(platform: string, platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, filesToSync?: Array, projectFilesConfig?: IProjectFilesConfig): Promise { if (!changesInfo || changesInfo.appFilesChanged) { - await this.copyAppFiles(platform, appFilesUpdaterOptions, projectData); + await this.copyAppFiles(platformData, appFilesUpdaterOptions, projectData); // remove the App_Resources folder from the app/assets as here we're applying other files changes. const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); @@ -293,41 +327,49 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - if (!changesInfo || changesInfo.appResourcesChanged) { - await this.copyAppFiles(platform, appFilesUpdaterOptions, projectData); - this.copyAppResources(platform, projectData); - await platformData.platformProjectService.prepareProject(projectData, platformSpecificData); + if (!changesInfo || changesInfo.modulesChanged) { + await this.copyTnsModules(platform, platformData, projectData, projectFilesConfig); } + } - if (!changesInfo || changesInfo.modulesChanged) { - await this.copyTnsModules(platform, projectData); + public async preparePlatformCoreNative(platform: string, platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, changesInfo?: IProjectChangesInfo, projectFilesConfig?: IProjectFilesConfig): Promise { + if (changesInfo.hasChanges) { + await this.cleanProject(platform, appFilesUpdaterOptions, platformData, projectData); } - let directoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - let excludedDirs = [constants.APP_RESOURCES_FOLDER_NAME]; - if (!changesInfo || !changesInfo.modulesChanged) { - excludedDirs.push(constants.TNS_MODULES_FOLDER_NAME); + if (!changesInfo || changesInfo.changesRequirePrepare) { + await this.copyAppFiles(platformData, appFilesUpdaterOptions, projectData); + this.copyAppResources(platformData, projectData); + await platformData.platformProjectService.prepareProject(projectData, platformSpecificData); } - this.$projectFilesManager.processPlatformSpecificFiles(directoryPath, platform, excludedDirs); + if (!changesInfo || changesInfo.modulesChanged || appFilesUpdaterOptions.bundle) { + await this.$pluginsService.validate(platformData, projectData); + + const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; + + const tnsModulesDestinationPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); + // Process node_modules folder + await this.$nodeModulesBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime, projectData, projectFilesConfig); + } if (!changesInfo || changesInfo.configChanged || changesInfo.modulesChanged) { await platformData.platformProjectService.processConfigurationFilesFromAppResources(appFilesUpdaterOptions.release, projectData); } platformData.platformProjectService.interpolateConfigurationFile(projectData, platformSpecificData); - - this.$logger.out("Project successfully prepared (" + platform + ")"); + this.$projectChangesService.setNativePlatformStatus(platform, projectData, + { nativePlatformStatus: constants.NativePlatformStatus.alreadyPrepared }); } - private async copyAppFiles(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData): Promise { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + private async copyAppFiles(platformData: IPlatformData, appFilesUpdaterOptions: IAppFilesUpdaterOptions, projectData: IProjectData): Promise { platformData.platformProjectService.ensureConfigurationFileInAppResources(projectData); - let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); // Copy app folder to native project this.$fs.ensureDirectoryExists(appDestinationDirectoryPath); - let appSourceDirectoryPath = path.join(projectData.projectDir, constants.APP_FOLDER_NAME); + const appSourceDirectoryPath = path.join(projectData.projectDir, constants.APP_FOLDER_NAME); const appUpdater = new AppFilesUpdater(appSourceDirectoryPath, appDestinationDirectoryPath, appFilesUpdaterOptions, this.$fs); appUpdater.updateApp(sourceFiles => { @@ -335,28 +377,26 @@ export class PlatformService extends EventEmitter implements IPlatformService { }); } - private copyAppResources(platform: string, projectData: IProjectData): void { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - let appResourcesDirectoryPath = path.join(appDestinationDirectoryPath, constants.APP_RESOURCES_FOLDER_NAME); + private copyAppResources(platformData: IPlatformData, projectData: IProjectData): void { + const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const appResourcesDirectoryPath = path.join(appDestinationDirectoryPath, constants.APP_RESOURCES_FOLDER_NAME); if (this.$fs.exists(appResourcesDirectoryPath)) { platformData.platformProjectService.prepareAppResources(appResourcesDirectoryPath, projectData); - let appResourcesDestination = platformData.platformProjectService.getAppResourcesDestinationDirectoryPath(projectData); + const appResourcesDestination = platformData.platformProjectService.getAppResourcesDestinationDirectoryPath(projectData); this.$fs.ensureDirectoryExists(appResourcesDestination); shell.cp("-Rf", path.join(appResourcesDirectoryPath, platformData.normalizedPlatformName, "*"), appResourcesDestination); this.$fs.deleteDirectory(appResourcesDirectoryPath); } } - private async copyTnsModules(platform: string, projectData: IProjectData): Promise { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - let lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; + private async copyTnsModules(platform: string, platformData: IPlatformData, projectData: IProjectData, projectFilesConfig?: IProjectFilesConfig): Promise { + const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const lastModifiedTime = this.$fs.exists(appDestinationDirectoryPath) ? this.$fs.getFsStats(appDestinationDirectoryPath).mtime : null; try { - let tnsModulesDestinationPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); + const tnsModulesDestinationPath = path.join(appDestinationDirectoryPath, constants.TNS_MODULES_FOLDER_NAME); // Process node_modules folder - await this.$nodeModulesBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime, projectData); + await this.$nodeModulesBuilder.prepareJSNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime, projectData, projectFilesConfig); } catch (error) { this.$logger.debug(error); shell.rm("-rf", appDestinationDirectoryPath); @@ -364,32 +404,38 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - public async shouldBuild(platform: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + public async shouldBuild(platform: string, projectData: IProjectData, buildConfig: IBuildConfig, outputPath?: string): Promise { if (this.$projectChangesService.currentChanges.changesRequireBuild) { return true; } - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let forDevice = !buildConfig || buildConfig.buildForDevice; - let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath; + + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const forDevice = !buildConfig || buildConfig.buildForDevice; + outputPath = outputPath || (forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath); if (!this.$fs.exists(outputPath)) { return true; } - let packageNames = platformData.getValidPackageNames({ isForDevice: forDevice }); - let packages = this.getApplicationPackages(outputPath, packageNames); + + const packageNames = platformData.getValidPackageNames({ isForDevice: forDevice }); + const packages = this.getApplicationPackages(outputPath, packageNames); if (packages.length === 0) { return true; } - let prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - let buildInfo = this.getBuildInfo(platform, platformData, buildConfig); + + const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); + const buildInfo = this.getBuildInfo(platform, platformData, buildConfig, outputPath); if (!prepareInfo || !buildInfo) { return true; } + if (buildConfig.clean) { return true; } + if (prepareInfo.time === buildInfo.prepareTime) { return false; } + return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; } @@ -421,11 +467,20 @@ export class PlatformService extends EventEmitter implements IPlatformService { public async buildPlatform(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): Promise { this.$logger.out("Building project..."); + const action = constants.TrackActionNames.Build; await this.trackProjectType(projectData); const isForDevice = this.$mobileHelper.isAndroidPlatform(platform) ? null : buildConfig && buildConfig.buildForDevice; - await this.trackActionForPlatform({ action: "Build", platform, isForDevice }); + await this.trackActionForPlatform({ action, platform, isForDevice }); + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action, + isForDevice, + platform, + projectDir: projectData.projectDir, + additionalData: `${buildConfig.release ? Configurations.Release : Configurations.Debug}_${buildConfig.clean ? constants.BuildStates.Clean : constants.BuildStates.Incremental}` + }); - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); const handler = (data: any) => { this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); this.$logger.printInfoMessageOnSameLine(data.data.toString()); @@ -433,36 +488,52 @@ export class PlatformService extends EventEmitter implements IPlatformService { await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, platformData.platformProjectService, handler, platformData.platformProjectService.buildProject(platformData.projectRoot, projectData, buildConfig)); - let prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); - let buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); - let buildInfoFile = path.join(buildInfoFilePath, buildInfoFileName); - let buildInfo: IBuildInfo = { + const buildInfoFilePath = this.getBuildOutputPath(platform, platformData, buildConfig); + this.saveBuildInfoFile(platform, projectData.projectDir, buildInfoFilePath); + + this.$logger.out("Project successfully built."); + } + + public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void { + const buildInfoFile = path.join(buildInfoFileDirname, buildInfoFileName); + + const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, this.$projectDataService.getProjectData(projectDir)); + const buildInfo = { prepareTime: prepareInfo.changesRequireBuildTime, buildTime: new Date().toString() }; + this.$fs.writeJson(buildInfoFile, buildInfo); - this.$logger.out("Project successfully built."); } - public async shouldInstall(device: Mobile.IDevice, projectData: IProjectData): Promise { - let platform = device.deviceInfo.platform; - let platformData = this.$platformsData.getPlatformData(platform, projectData); + public async shouldInstall(device: Mobile.IDevice, projectData: IProjectData, outputPath?: string): Promise { + const platform = device.deviceInfo.platform; if (!(await device.applicationManager.isApplicationInstalled(projectData.projectId))) { return true; } - let deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData); - let localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator }); + + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData); + const localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator }, outputPath); return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime; } - public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData): Promise { + public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData, packageFile?: string, outputFilePath?: string): Promise { this.$logger.out("Installing..."); - let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); - let packageFile = ""; - if (this.$devicesService.isiOSSimulator(device)) { - packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; - } else { - packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: constants.TrackActionNames.Deploy, + device, + projectDir: projectData.projectDir + }); + + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + if (!packageFile) { + if (this.$devicesService.isiOSSimulator(device)) { + packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig, outputFilePath).packageName; + } else { + packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig, outputFilePath).packageName; + } } await platformData.platformProjectService.cleanDeviceTempFolder(device.deviceInfo.identifier, projectData); @@ -470,9 +541,9 @@ export class PlatformService extends EventEmitter implements IPlatformService { await device.applicationManager.reinstallApplication(projectData.projectId, packageFile); if (!buildConfig.release) { - let deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); - let buildInfoFilePath = this.getBuildOutputPath(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator }); - let appIdentifier = projectData.projectId; + const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); + const buildInfoFilePath = outputFilePath || this.getBuildOutputPath(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator }); + const appIdentifier = projectData.projectId; await device.fileSystem.putFile(path.join(buildInfoFilePath, buildInfoFileName), deviceFilePath, appIdentifier); } @@ -480,14 +551,14 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); } - public async deployPlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, deployOptions: IDeployPlatformOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - await this.preparePlatform(platform, appFilesUpdaterOptions, deployOptions.platformTemplate, projectData, platformSpecificData); - let options: Mobile.IDevicesServicesInitializationOptions = { + public async deployPlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, deployOptions: IDeployPlatformOptions, projectData: IProjectData, config: IPlatformOptions): Promise { + await this.preparePlatform(platform, appFilesUpdaterOptions, deployOptions.platformTemplate, projectData, config); + const options: Mobile.IDevicesServicesInitializationOptions = { platform: platform, deviceId: deployOptions.device, emulator: deployOptions.emulator }; await this.$devicesService.initialize(options); - let action = async (device: Mobile.IDevice): Promise => { - let buildConfig: IBuildConfig = { + const action = async (device: Mobile.IDevice): Promise => { + const buildConfig: IBuildConfig = { buildForDevice: !this.$devicesService.isiOSSimulator(device), projectDir: deployOptions.projectDir, release: deployOptions.release, @@ -500,7 +571,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { keyStorePath: deployOptions.keyStorePath, clean: deployOptions.clean }; - let shouldBuild = await this.shouldBuild(platform, projectData, buildConfig); + const shouldBuild = await this.shouldBuild(platform, projectData, buildConfig); if (shouldBuild) { await this.buildPlatform(platform, buildConfig, projectData); } else { @@ -513,7 +584,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out("Skipping install."); } - await this.trackActionForPlatform({ action: "Deploy", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); + await this.trackActionForPlatform({ action: constants.TrackActionNames.Deploy, platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); }; await this.$devicesService.execute(action, this.getCanExecuteAction(platform, deployOptions)); @@ -522,7 +593,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { public async startApplication(platform: string, runOptions: IRunPlatformOptions, projectId: string): Promise { this.$logger.out("Starting..."); - let action = async (device: Mobile.IDevice) => { + const action = async (device: Mobile.IDevice) => { await device.applicationManager.startApplication(projectId); this.$logger.out(`Successfully started on device with identifier '${device.deviceInfo.identifier}'.`); }; @@ -531,41 +602,6 @@ export class PlatformService extends EventEmitter implements IPlatformService { await this.$devicesService.execute(action, this.getCanExecuteAction(platform, runOptions)); } - public async emulatePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, emulateOptions: IEmulatePlatformOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - if (emulateOptions.avd) { - this.$logger.warn(`Option --avd is no longer supported. Please use --device instead!`); - return Promise.resolve(); - } - - if (emulateOptions.availableDevices) { - return this.$emulatorPlatformService.listAvailableEmulators(platform); - } - - if (emulateOptions.device) { - let info = await this.$emulatorPlatformService.getEmulatorInfo(platform, emulateOptions.device); - if (info) { - if (!info.isRunning) { - await this.$emulatorPlatformService.startEmulator(info, projectData); - } - - emulateOptions.device = null; - } else { - await this.$devicesService.initialize({ platform: platform, deviceId: emulateOptions.device }); - let found: Mobile.IDeviceInfo[] = []; - if (this.$devicesService.hasDevices) { - found = this.$devicesService.getDevices().filter((deviceInfo: Mobile.IDeviceInfo) => deviceInfo.identifier === emulateOptions.device); - } - - if (found.length === 0) { - this.$errors.fail(this.$messages.Devices.NotFoundDeviceByIdentifierErrorMessage, this.$staticConfig.CLIENT_NAME.toLowerCase()); - } - } - } - - await this.deployPlatform(platform, appFilesUpdaterOptions, emulateOptions, projectData, platformSpecificData); - return this.startApplication(platform, emulateOptions, projectData.projectId); - } - private getBuildOutputPath(platform: string, platformData: IPlatformData, options: IBuildForDevice): string { if (platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { return options.buildForDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath; @@ -575,13 +611,15 @@ export class PlatformService extends EventEmitter implements IPlatformService { } private async getDeviceBuildInfoFilePath(device: Mobile.IDevice, projectData: IProjectData): Promise { - let deviceAppData = this.$deviceAppDataFactory.create(projectData.projectId, device.deviceInfo.platform, device); - let deviceRootPath = path.dirname(await deviceAppData.getDeviceProjectRootPath()); + const deviceRootPath = await this.$devicePathProvider.getDeviceProjectRootPath(device, { + appIdentifier: projectData.projectId, + getDirname: true + }); return helpers.fromWindowsRelativePathToUnix(path.join(deviceRootPath, buildInfoFileName)); } private async getDeviceBuildInfo(device: Mobile.IDevice, projectData: IProjectData): Promise { - let deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); + const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); try { return JSON.parse(await this.readFile(device, deviceFilePath, projectData)); } catch (e) { @@ -589,12 +627,12 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - private getBuildInfo(platform: string, platformData: IPlatformData, options: IBuildForDevice): IBuildInfo { - let buildInfoFilePath = this.getBuildOutputPath(platform, platformData, options); - let buildInfoFile = path.join(buildInfoFilePath, buildInfoFileName); + private getBuildInfo(platform: string, platformData: IPlatformData, options: IBuildForDevice, buildOutputPath?: string): IBuildInfo { + buildOutputPath = buildOutputPath || this.getBuildOutputPath(platform, platformData, options); + const buildInfoFile = path.join(buildOutputPath, buildInfoFileName); if (this.$fs.exists(buildInfoFile)) { try { - let buildInfoTime = this.$fs.readJson(buildInfoFile); + const buildInfoTime = this.$fs.readJson(buildInfoFile); return buildInfoTime; } catch (e) { return null; @@ -604,23 +642,23 @@ export class PlatformService extends EventEmitter implements IPlatformService { return null; } - public async cleanDestinationApp(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - await this.ensurePlatformInstalled(platform, platformTemplate, projectData, platformSpecificData); + public async cleanDestinationApp(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions): Promise { + await this.ensurePlatformInstalled(platform, platformTemplate, projectData, config); const appSourceDirectoryPath = path.join(projectData.projectDir, constants.APP_FOLDER_NAME); - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const appDestinationDirectoryPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); const appUpdater = new AppFilesUpdater(appSourceDirectoryPath, appDestinationDirectoryPath, appFilesUpdaterOptions, this.$fs); appUpdater.cleanDestinationApp(); } - public lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): string { + public lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData, outputPath?: string): string { let packageFile: string; - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); if (buildConfig.buildForDevice) { - packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName; + packageFile = this.getLatestApplicationPackageForDevice(platformData, buildConfig, outputPath).packageName; } else { - packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName; + packageFile = this.getLatestApplicationPackageForEmulator(platformData, buildConfig, outputPath).packageName; } if (!packageFile || !this.$fs.exists(packageFile)) { this.$errors.failWithoutHelp("Unable to find built application. Try 'tns build %s'.", platform); @@ -632,12 +670,12 @@ export class PlatformService extends EventEmitter implements IPlatformService { platform = platform.toLowerCase(); targetPath = path.resolve(targetPath); - let packageFile = this.lastOutputPath(platform, buildConfig, projectData); + const packageFile = this.lastOutputPath(platform, buildConfig, projectData); this.$fs.ensureDirectoryExists(path.dirname(targetPath)); if (this.$fs.exists(targetPath) && this.$fs.getFsStats(targetPath).isDirectory()) { - let sourceFileName = path.basename(packageFile); + const sourceFileName = path.basename(packageFile); this.$logger.trace(`Specified target path: '${targetPath}' is directory. Same filename will be used: '${sourceFileName}'.`); targetPath = path.join(targetPath, sourceFileName); } @@ -646,13 +684,13 @@ export class PlatformService extends EventEmitter implements IPlatformService { } public async removePlatforms(platforms: string[], projectData: IProjectData): Promise { - for (let platform of platforms) { + for (const platform of platforms) { this.validatePlatformInstalled(platform, projectData); - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); await platformData.platformProjectService.stopServices(platformData.projectRoot); - let platformDir = path.join(projectData.platformsDir, platform); + const platformDir = path.join(projectData.platformsDir, platform); this.$fs.deleteDirectory(platformDir); this.$projectDataService.removeNSProperty(projectData.projectDir, platformData.frameworkPackageName); @@ -660,24 +698,24 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - public async updatePlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - for (let platformParam of platforms) { - let data = platformParam.split("@"), + public async updatePlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, config: IPlatformOptions): Promise { + for (const platformParam of platforms) { + const data = platformParam.split("@"), platform = data[0], version = data[1]; if (this.isPlatformInstalled(platform, projectData)) { - await this.updatePlatform(platform, version, platformTemplate, projectData, platformSpecificData); + await this.updatePlatform(platform, version, platformTemplate, projectData, config); } else { - await this.addPlatform(platformParam, platformTemplate, projectData, platformSpecificData); + await this.addPlatform(platformParam, platformTemplate, projectData, config); } - }; + } } private getCanExecuteAction(platform: string, options: IDeviceEmulator): any { - let canExecute = (currentDevice: Mobile.IDevice): boolean => { + const canExecute = (currentDevice: Mobile.IDevice): boolean => { if (options.device && currentDevice && currentDevice.deviceInfo) { - let device = this.$devicesService.getDeviceByDeviceOption(); + const device = this.$devicesService.getDeviceByDeviceOption(); if (device && device.deviceInfo) { return currentDevice.deviceInfo.identifier === device.deviceInfo.identifier; } @@ -707,10 +745,6 @@ export class PlatformService extends EventEmitter implements IPlatformService { if (!this.isValidPlatform(platform, projectData)) { this.$errors.fail("Invalid platform %s. Valid platforms are %s.", platform, helpers.formatListOfNames(this.$platformsData.platformsNames)); } - - if (!this.isPlatformSupportedForOS(platform, projectData)) { - this.$errors.fail("Applications for platform %s can not be built on this OS - %s", platform, process.platform); - } } public validatePlatformInstalled(platform: string, projectData: IProjectData): void { @@ -721,9 +755,19 @@ export class PlatformService extends EventEmitter implements IPlatformService { } } - public async ensurePlatformInstalled(platform: string, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { + public async ensurePlatformInstalled(platform: string, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions, nativePrepare?: INativePrepare): Promise { + let requiresNativePlatformAdd = false; + if (!this.isPlatformInstalled(platform, projectData)) { - await this.addPlatform(platform, platformTemplate, projectData, platformSpecificData); + await this.addPlatform(platform, platformTemplate, projectData, config, "", nativePrepare); + } else { + const shouldAddNativePlatform = !nativePrepare || !nativePrepare.skipNativePrepare; + const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData); + // In case there's no prepare info, it means only platform add had been executed. So we've come from CLI and we do not need to prepare natively. + requiresNativePlatformAdd = prepareInfo && prepareInfo.nativePlatformStatus === constants.NativePlatformStatus.requiresPlatformAdd; + if (requiresNativePlatformAdd && shouldAddNativePlatform) { + await this.addPlatform(platform, platformTemplate, projectData, config, "", nativePrepare); + } } } @@ -735,21 +779,21 @@ export class PlatformService extends EventEmitter implements IPlatformService { return this.$platformsData.getPlatformData(platform, projectData); } - private isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { - let targetedOS = this.$platformsData.getPlatformData(platform, projectData).targetedOS; - let res = !targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0; + public isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { + const targetedOS = this.$platformsData.getPlatformData(platform, projectData).targetedOS; + const res = !targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0; return res; } private isPlatformPrepared(platform: string, projectData: IProjectData): boolean { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); return platformData.platformProjectService.isPlatformPrepared(platformData.projectRoot, projectData); } private getApplicationPackages(buildOutputPath: string, validPackageNames: string[]): IApplicationPackage[] { // Get latest package` that is produced from build - let candidates = this.$fs.readDirectory(buildOutputPath); - let packages = _.filter(candidates, candidate => { + const candidates = this.$fs.readDirectory(buildOutputPath); + const packages = _.filter(candidates, candidate => { return _.includes(validPackageNames, candidate); }).map(currentPackage => { currentPackage = path.join(buildOutputPath, currentPackage); @@ -766,7 +810,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { private getLatestApplicationPackage(buildOutputPath: string, validPackageNames: string[]): IApplicationPackage { let packages = this.getApplicationPackages(buildOutputPath, validPackageNames); if (packages.length === 0) { - let packageExtName = path.extname(validPackageNames[0]); + const packageExtName = path.extname(validPackageNames[0]); this.$errors.fail("No %s found in %s directory", packageExtName, buildOutputPath); } @@ -775,28 +819,28 @@ export class PlatformService extends EventEmitter implements IPlatformService { return packages[0]; } - public getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage { - return this.getLatestApplicationPackage(platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: true, isReleaseBuild: buildConfig.release })); + public getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage { + return this.getLatestApplicationPackage(outputPath || platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: true, isReleaseBuild: buildConfig.release })); } - public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage { - return this.getLatestApplicationPackage(platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: false, isReleaseBuild: buildConfig.release })); + public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage { + return this.getLatestApplicationPackage(outputPath || platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath, platformData.getValidPackageNames({ isForDevice: false, isReleaseBuild: buildConfig.release })); } - private async updatePlatform(platform: string, version: string, platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + private async updatePlatform(platform: string, version: string, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions): Promise { + const platformData = this.$platformsData.getPlatformData(platform, projectData); - let data = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); - let currentVersion = data && data.version ? data.version : "0.2.0"; + const data = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); + const currentVersion = data && data.version ? data.version : "0.2.0"; let newVersion = version === constants.PackageVersion.NEXT ? await this.$npmInstallationManager.getNextVersion(platformData.frameworkPackageName) : version || await this.$npmInstallationManager.getLatestCompatibleVersion(platformData.frameworkPackageName); - let installedModuleDir = await this.$npmInstallationManager.install(platformData.frameworkPackageName, projectData.projectDir, { version: newVersion, dependencyType: "save" }); - let cachedPackageData = this.$fs.readJson(path.join(installedModuleDir, "package.json")); + const installedModuleDir = await this.$npmInstallationManager.install(platformData.frameworkPackageName, projectData.projectDir, { version: newVersion, dependencyType: "save" }); + const cachedPackageData = this.$fs.readJson(path.join(installedModuleDir, "package.json")); newVersion = (cachedPackageData && cachedPackageData.version) || newVersion; - let canUpdate = platformData.platformProjectService.canUpdatePlatform(installedModuleDir, projectData); + const canUpdate = platformData.platformProjectService.canUpdatePlatform(installedModuleDir, projectData); await this.$npm.uninstall(platformData.frameworkPackageName, { save: true }, projectData.projectDir); if (canUpdate) { if (!semver.valid(newVersion)) { @@ -804,7 +848,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { } if (!semver.gt(currentVersion, newVersion)) { - await this.updatePlatformCore(platformData, { currentVersion, newVersion, canUpdate, platformTemplate }, projectData, platformSpecificData); + await this.updatePlatformCore(platformData, { currentVersion, newVersion, canUpdate, platformTemplate }, projectData, config); } else if (semver.eq(currentVersion, newVersion)) { this.$errors.fail("Current and new version are the same."); } else { @@ -816,17 +860,18 @@ export class PlatformService extends EventEmitter implements IPlatformService { } - private async updatePlatformCore(platformData: IPlatformData, updateOptions: IUpdatePlatformOptions, projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { + private async updatePlatformCore(platformData: IPlatformData, updateOptions: IUpdatePlatformOptions, projectData: IProjectData, config: IPlatformOptions): Promise { let packageName = platformData.normalizedPlatformName.toLowerCase(); await this.removePlatforms([packageName], projectData); packageName = updateOptions.newVersion ? `${packageName}@${updateOptions.newVersion}` : packageName; - await this.addPlatform(packageName, updateOptions.platformTemplate, projectData, platformSpecificData); + await this.addPlatform(packageName, updateOptions.platformTemplate, projectData, config); this.$logger.out("Successfully updated to version ", updateOptions.newVersion); } + // TODO: Remove this method from here. It has nothing to do with platform public async readFile(device: Mobile.IDevice, deviceFilePath: string, projectData: IProjectData): Promise { temp.track(); - let uniqueFilePath = temp.path({ suffix: ".tmp" }); + const uniqueFilePath = temp.path({ suffix: ".tmp" }); try { await device.fileSystem.getFile(deviceFilePath, projectData.projectId, uniqueFilePath); } catch (e) { @@ -834,7 +879,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { } if (this.$fs.exists(uniqueFilePath)) { - let text = this.$fs.readText(uniqueFilePath); + const text = this.$fs.readText(uniqueFilePath); shell.rm(uniqueFilePath); return text; } diff --git a/lib/services/plugin-variables-service.ts b/lib/services/plugin-variables-service.ts index 2a85e515bc..ccb822fcc5 100644 --- a/lib/services/plugin-variables-service.ts +++ b/lib/services/plugin-variables-service.ts @@ -14,9 +14,9 @@ export class PluginVariablesService implements IPluginVariablesService { } public async savePluginVariablesInProjectFile(pluginData: IPluginData, projectData: IProjectData): Promise { - let values = Object.create(null); + const values = Object.create(null); await this.executeForAllPluginVariables(pluginData, async (pluginVariableData: IPluginVariableData) => { - let pluginVariableValue = await this.getPluginVariableValue(pluginVariableData); + const pluginVariableValue = await this.getPluginVariableValue(pluginVariableData); this.ensurePluginVariableValue(pluginVariableValue, `Unable to find value for ${pluginVariableData.name} plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`); values[pluginVariableData.name] = pluginVariableValue; }, projectData); @@ -41,8 +41,8 @@ export class PluginVariablesService implements IPluginVariablesService { } public interpolateAppIdentifier(pluginConfigurationFilePath: string, projectData: IProjectData): void { - let pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath); - let newContent = this.interpolateCore("nativescript.id", projectData.projectId, pluginConfigurationFileContent); + const pluginConfigurationFileContent = this.$fs.readText(pluginConfigurationFilePath); + const newContent = this.interpolateCore("nativescript.id", projectData.projectId, pluginConfigurationFileContent); this.$fs.writeFile(pluginConfigurationFilePath, newContent); } @@ -62,20 +62,20 @@ export class PluginVariablesService implements IPluginVariablesService { } private async getPluginVariableValue(pluginVariableData: IPluginVariableData): Promise { - let pluginVariableName = pluginVariableData.name; + const pluginVariableName = pluginVariableData.name; let value = this.$pluginVariablesHelper.getPluginVariableFromVarOption(pluginVariableName); if (value) { value = value[pluginVariableName]; } else { value = pluginVariableData.defaultValue; if (!value && helpers.isInteractive()) { - let promptSchema = { + const promptSchema = { name: pluginVariableName, type: "input", message: `Enter value for ${pluginVariableName} variable:`, validate: (val: string) => !!val ? true : 'Please enter a value!' }; - let promptData = await this.$prompter.get([promptSchema]); + const promptData = await this.$prompter.get([promptSchema]); value = promptData[pluginVariableName]; } } @@ -84,13 +84,13 @@ export class PluginVariablesService implements IPluginVariablesService { } private async executeForAllPluginVariables(pluginData: IPluginData, action: (pluginVariableData: IPluginVariableData) => Promise, projectData: IProjectData): Promise { - let pluginVariables = pluginData.pluginVariables; - let pluginVariablesNames = _.keys(pluginVariables); + const pluginVariables = pluginData.pluginVariables; + const pluginVariablesNames = _.keys(pluginVariables); await Promise.all(_.map(pluginVariablesNames, pluginVariableName => action(this.createPluginVariableData(pluginData, pluginVariableName, projectData)))); } private createPluginVariableData(pluginData: IPluginData, pluginVariableName: string, projectData: IProjectData): IPluginVariableData { - let variableData = pluginData.pluginVariables[pluginVariableName]; + const variableData = pluginData.pluginVariables[pluginVariableName]; variableData.name = pluginVariableName; diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 6e6c3b09b3..7cd9e5b280 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -22,6 +22,15 @@ export class PluginsService implements IPluginsService { return this.$injector.resolve("projectFilesManager"); } + private get npmInstallOptions(): INodePackageManagerInstallOptions { + return _.merge({ + disableNpmInstall: this.$options.disableNpmInstall, + frameworkPath: this.$options.frameworkPath, + ignoreScripts: this.$options.ignoreScripts, + path: this.$options.path + }, PluginsService.NPM_CONFIG); + } + constructor(private $npm: INodePackageManager, private $fs: IFileSystem, private $options: IOptions, @@ -35,15 +44,16 @@ export class PluginsService implements IPluginsService { if (possiblePackageName.indexOf(".tgz") !== -1 && this.$fs.exists(possiblePackageName)) { plugin = possiblePackageName; } - let name = (await this.$npm.install(plugin, projectData.projectDir, PluginsService.NPM_CONFIG))[0]; - let pathToRealNpmPackageJson = path.join(projectData.projectDir, "node_modules", name, "package.json"); - let realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson); + + const name = (await this.$npm.install(plugin, projectData.projectDir, this.npmInstallOptions)).name; + const pathToRealNpmPackageJson = path.join(projectData.projectDir, "node_modules", name, "package.json"); + const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson); if (realNpmPackageJson.nativescript) { - let pluginData = this.convertToPluginData(realNpmPackageJson, projectData.projectDir); + const pluginData = this.convertToPluginData(realNpmPackageJson, projectData.projectDir); // Validate - let action = async (pluginDestinationPath: string, platform: string, platformData: IPlatformData): Promise => { + const action = async (pluginDestinationPath: string, platform: string, platformData: IPlatformData): Promise => { this.isPluginDataValidForPlatform(pluginData, platform, projectData); }; @@ -61,14 +71,14 @@ export class PluginsService implements IPluginsService { this.$logger.out(`Successfully installed plugin ${realNpmPackageJson.name}.`); } else { - this.$npm.uninstall(realNpmPackageJson.name, { save: true }, projectData.projectDir); + await this.$npm.uninstall(realNpmPackageJson.name, { save: true }, projectData.projectDir); this.$errors.failWithoutHelp(`${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`); } } public async remove(pluginName: string, projectData: IProjectData): Promise { - let removePluginNativeCodeAction = async (modulesDestinationPath: string, platform: string, platformData: IPlatformData): Promise => { - let pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir); + const removePluginNativeCodeAction = async (modulesDestinationPath: string, platform: string, platformData: IPlatformData): Promise => { + const pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir); await platformData.platformProjectService.removePluginNativeCode(pluginData, projectData); }; @@ -79,7 +89,7 @@ export class PluginsService implements IPluginsService { await this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName, projectData); let showMessage = true; - let action = async (modulesDestinationPath: string, platform: string, platformData: IPlatformData): Promise => { + const action = async (modulesDestinationPath: string, platform: string, platformData: IPlatformData): Promise => { shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName)); this.$logger.out(`Successfully removed plugin ${pluginName} for ${platform}.`); @@ -93,23 +103,18 @@ export class PluginsService implements IPluginsService { } } - public getAvailable(filter: string[]): Promise> { - let silent: boolean = true; - return this.$npm.search(filter, { "silent": silent }); - } - public async validate(platformData: IPlatformData, projectData: IProjectData): Promise { return await platformData.platformProjectService.validatePlugins(projectData); } - public async prepare(dependencyData: IDependencyData, platform: string, projectData: IProjectData): Promise { + public async prepare(dependencyData: IDependencyData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { platform = platform.toLowerCase(); - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let pluginData = this.convertToPluginData(dependencyData, projectData.projectDir); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const pluginData = this.convertToPluginData(dependencyData, projectData.projectDir); - let appFolderExists = this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); + const appFolderExists = this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); if (appFolderExists) { - this.preparePluginScripts(pluginData, platform, projectData); + this.preparePluginScripts(pluginData, platform, projectData, projectFilesConfig); await this.preparePluginNativeCode(pluginData, platform, projectData); // Show message @@ -117,10 +122,10 @@ export class PluginsService implements IPluginsService { } } - private preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData): void { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let pluginScriptsDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); - let scriptsDestinationExists = this.$fs.exists(pluginScriptsDestinationPath); + public preparePluginScripts(pluginData: IPluginData, platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): void { + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const pluginScriptsDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); + const scriptsDestinationExists = this.$fs.exists(pluginScriptsDestinationPath); if (!scriptsDestinationExists) { //tns_modules/ doesn't exist. Assuming we're running a bundled prepare. return; @@ -131,11 +136,11 @@ export class PluginsService implements IPluginsService { } //prepare platform speciffic files, .map and .ts files - this.$projectFilesManager.processPlatformSpecificFiles(pluginScriptsDestinationPath, platform); + this.$projectFilesManager.processPlatformSpecificFiles(pluginScriptsDestinationPath, platform, projectFilesConfig); } - private async preparePluginNativeCode(pluginData: IPluginData, platform: string, projectData: IProjectData): Promise { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + public async preparePluginNativeCode(pluginData: IPluginData, platform: string, projectData: IProjectData): Promise { + const platformData = this.$platformsData.getPlatformData(platform, projectData); pluginData.pluginPlatformsFolderPath = (_platform: string) => path.join(pluginData.fullPath, "platforms", _platform); await platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData); @@ -149,29 +154,34 @@ export class PluginsService implements IPluginsService { _(installedDependencies) .filter(dependencyName => _.startsWith(dependencyName, "@")) .each(scopedDependencyDir => { - let contents = this.$fs.readDirectory(path.join(this.getNodeModulesPath(projectData.projectDir), scopedDependencyDir)); + const contents = this.$fs.readDirectory(path.join(this.getNodeModulesPath(projectData.projectDir), scopedDependencyDir)); installedDependencies = installedDependencies.concat(contents.map(dependencyName => `${scopedDependencyDir}/${dependencyName}`)); }); - let packageJsonContent = this.$fs.readJson(this.getPackageJsonFilePath(projectData.projectDir)); - let allDependencies = _.keys(packageJsonContent.dependencies).concat(_.keys(packageJsonContent.devDependencies)); - let notInstalledDependencies = _.difference(allDependencies, installedDependencies); + const packageJsonContent = this.$fs.readJson(this.getPackageJsonFilePath(projectData.projectDir)); + const allDependencies = _.keys(packageJsonContent.dependencies).concat(_.keys(packageJsonContent.devDependencies)); + const notInstalledDependencies = _.difference(allDependencies, installedDependencies); if (this.$options.force || notInstalledDependencies.length) { this.$logger.trace("Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", notInstalledDependencies); - await this.$npm.install(projectData.projectDir, projectData.projectDir, { "ignore-scripts": this.$options.ignoreScripts }); + await this.$npm.install(projectData.projectDir, projectData.projectDir, { + disableNpmInstall: this.$options.disableNpmInstall, + frameworkPath: this.$options.frameworkPath, + ignoreScripts: this.$options.ignoreScripts, + path: this.$options.path + }); } } public async getAllInstalledPlugins(projectData: IProjectData): Promise { - let nodeModules = (await this.getAllInstalledModules(projectData)).map(nodeModuleData => this.convertToPluginData(nodeModuleData, projectData.projectDir)); + const nodeModules = (await this.getAllInstalledModules(projectData)).map(nodeModuleData => this.convertToPluginData(nodeModuleData, projectData.projectDir)); return _.filter(nodeModules, nodeModuleData => nodeModuleData && nodeModuleData.isPlugin); } public getDependenciesFromPackageJson(projectDir: string): IPackageJsonDepedenciesResult { - let packageJson = this.$fs.readJson(this.getPackageJsonFilePath(projectDir)); - let dependencies: IBasePluginData[] = this.getBasicPluginInformation(packageJson.dependencies); + const packageJson = this.$fs.readJson(this.getPackageJsonFilePath(projectDir)); + const dependencies: IBasePluginData[] = this.getBasicPluginInformation(packageJson.dependencies); - let devDependencies: IBasePluginData[] = this.getBasicPluginInformation(packageJson.devDependencies); + const devDependencies: IBasePluginData[] = this.getBasicPluginInformation(packageJson.devDependencies); return { dependencies, @@ -199,7 +209,7 @@ export class PluginsService implements IPluginsService { } private getDependencies(projectDir: string): string[] { - let packageJsonFilePath = this.getPackageJsonFilePath(projectDir); + const packageJsonFilePath = this.getPackageJsonFilePath(projectDir); return _.keys(require(packageJsonFilePath).dependencies); } @@ -208,7 +218,7 @@ export class PluginsService implements IPluginsService { module = this.getPackageJsonFilePathForModule(module, projectDir); } - let data = this.$fs.readJson(module); + const data = this.$fs.readJson(module); return { name: data.name, version: data.version, @@ -218,14 +228,14 @@ export class PluginsService implements IPluginsService { }; } - private convertToPluginData(cacheData: any, projectDir: string): IPluginData { - let pluginData: any = {}; + public convertToPluginData(cacheData: any, projectDir: string): IPluginData { + const pluginData: any = {}; pluginData.name = cacheData.name; pluginData.version = cacheData.version; pluginData.fullPath = cacheData.directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir)); pluginData.isPlugin = !!cacheData.nativescript || !!cacheData.moduleInfo; pluginData.pluginPlatformsFolderPath = (platform: string) => path.join(pluginData.fullPath, "platforms", platform); - let data = cacheData.nativescript || cacheData.moduleInfo; + const data = cacheData.nativescript || cacheData.moduleInfo; if (pluginData.isPlugin) { pluginData.platformsData = data.platforms; @@ -243,13 +253,13 @@ export class PluginsService implements IPluginsService { private async getAllInstalledModules(projectData: IProjectData): Promise { await this.ensure(projectData); - let nodeModules = this.getDependencies(projectData.projectDir); + const nodeModules = this.getDependencies(projectData.projectDir); return _.map(nodeModules, nodeModuleName => this.getNodeModuleData(nodeModuleName, projectData.projectDir)); } private async executeNpmCommand(npmCommandName: string, npmCommandArguments: string, projectData: IProjectData): Promise { if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) { - await this.$npm.install(npmCommandArguments, projectData.projectDir, PluginsService.NPM_CONFIG); + await this.$npm.install(npmCommandArguments, projectData.projectDir, this.npmInstallOptions); } else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) { await this.$npm.uninstall(npmCommandArguments, PluginsService.NPM_CONFIG, projectData.projectDir); } @@ -262,19 +272,19 @@ export class PluginsService implements IPluginsService { } private async executeForAllInstalledPlatforms(action: (_pluginDestinationPath: string, pl: string, _platformData: IPlatformData) => Promise, projectData: IProjectData): Promise { - let availablePlatforms = _.keys(this.$platformsData.availablePlatforms); - for (let platform of availablePlatforms) { - let isPlatformInstalled = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase())); + const availablePlatforms = _.keys(this.$platformsData.availablePlatforms); + for (const platform of availablePlatforms) { + const isPlatformInstalled = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase())); if (isPlatformInstalled) { - let platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); - let pluginDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); + const platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + const pluginDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules"); await action(pluginDestinationPath, platform.toLowerCase(), platformData); } - }; + } } private getInstalledFrameworkVersion(platform: string, projectData: IProjectData): string { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + const platformData = this.$platformsData.getPlatformData(platform, projectData); const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); return frameworkData.version; } @@ -282,10 +292,10 @@ export class PluginsService implements IPluginsService { private isPluginDataValidForPlatform(pluginData: IPluginData, platform: string, projectData: IProjectData): boolean { let isValid = true; - let installedFrameworkVersion = this.getInstalledFrameworkVersion(platform, projectData); - let pluginPlatformsData = pluginData.platformsData; + const installedFrameworkVersion = this.getInstalledFrameworkVersion(platform, projectData); + const pluginPlatformsData = pluginData.platformsData; if (pluginPlatformsData) { - let pluginVersion = (pluginPlatformsData)[platform]; + const pluginVersion = (pluginPlatformsData)[platform]; if (!pluginVersion) { this.$logger.warn(`${pluginData.name} is not supported for ${platform}.`); isValid = false; diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index befe87b170..1baafc517c 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -1,5 +1,6 @@ import * as path from "path"; -import { NODE_MODULES_FOLDER_NAME } from "../constants"; +import { NODE_MODULES_FOLDER_NAME, NativePlatformStatus, PACKAGE_JSON_FILE_NAME } from "../constants"; +import { getHash } from "../common/helpers"; const prepareInfoFileName = ".nsprepareinfo"; @@ -11,13 +12,16 @@ class ProjectChangesInfo implements IProjectChangesInfo { public configChanged: boolean; public packageChanged: boolean; public nativeChanged: boolean; + public signingChanged: boolean; + public nativePlatformStatus: NativePlatformStatus; public get hasChanges(): boolean { return this.packageChanged || this.appFilesChanged || this.appResourcesChanged || this.modulesChanged || - this.configChanged; + this.configChanged || + this.signingChanged; } public get changesRequireBuild(): boolean { @@ -25,6 +29,11 @@ class ProjectChangesInfo implements IProjectChangesInfo { this.appResourcesChanged || this.nativeChanged; } + + public get changesRequirePrepare(): boolean { + return this.appResourcesChanged || + this.signingChanged; + } } export class ProjectChangesService implements IProjectChangesService { @@ -45,13 +54,13 @@ export class ProjectChangesService implements IProjectChangesService { return this._changesInfo; } - public checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): IProjectChangesInfo { - let platformData = this.$platformsData.getPlatformData(platform, projectData); + public async checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise { + const platformData = this.$platformsData.getPlatformData(platform, projectData); this._changesInfo = new ProjectChangesInfo(); if (!this.ensurePrepareInfo(platform, projectData, projectChangesOptions)) { this._newFiles = 0; this._changesInfo.appFilesChanged = this.containsNewerFiles(projectData.appDirectoryPath, projectData.appResourcesDirectoryPath, projectData); - this._changesInfo.packageChanged = this.filesChanged([path.join(projectData.projectDir, "package.json")]); + this._changesInfo.packageChanged = this.isProjectFileChanged(projectData, platform); this._changesInfo.appResourcesChanged = this.containsNewerFiles(projectData.appResourcesDirectoryPath, null, projectData); /*done because currently all node_modules are traversed, a possible improvement could be traversing only the production dependencies*/ this._changesInfo.nativeChanged = this.containsNewerFiles( @@ -59,14 +68,15 @@ export class ProjectChangesService implements IProjectChangesService { path.join(projectData.projectDir, NODE_MODULES_FOLDER_NAME, "tns-ios-inspector"), projectData, this.fileChangeRequiresBuild); + if (this._newFiles > 0) { this._changesInfo.modulesChanged = true; } - let platformResourcesDir = path.join(projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName); + const platformResourcesDir = path.join(projectData.appResourcesDirectoryPath, platformData.normalizedPlatformName); if (platform === this.$devicePlatformsConstants.iOS.toLowerCase()) { this._changesInfo.configChanged = this.filesChanged([path.join(platformResourcesDir, platformData.configurationFileName), - path.join(platformResourcesDir, "LaunchScreen.storyboard"), - path.join(platformResourcesDir, "build.xcconfig") + path.join(platformResourcesDir, "LaunchScreen.storyboard"), + path.join(platformResourcesDir, "build.xcconfig") ]); } else { this._changesInfo.configChanged = this.filesChanged([ @@ -75,16 +85,10 @@ export class ProjectChangesService implements IProjectChangesService { ]); } } - if (platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { - const nextCommandProvisionUUID = projectChangesOptions.provision; - // We should consider reading here the provisioning profile UUID from the xcodeproj and xcconfig. - const prevProvisionUUID = this._prepareInfo.iOSProvisioningProfileUUID; - if (nextCommandProvisionUUID !== prevProvisionUUID) { - this._changesInfo.nativeChanged = true; - this._changesInfo.configChanged = true; - this._prepareInfo.iOSProvisioningProfileUUID = nextCommandProvisionUUID; - } - } + + const projectService = platformData.platformProjectService; + await projectService.checkForChanges(this._changesInfo, projectChangesOptions, projectData); + if (projectChangesOptions.bundle !== this._prepareInfo.bundle || projectChangesOptions.release !== this._prepareInfo.release) { this._changesInfo.appFilesChanged = true; this._changesInfo.appResourcesChanged = true; @@ -105,18 +109,23 @@ export class ProjectChangesService implements IProjectChangesService { if (this._prepareInfo.changesRequireBuild) { this._prepareInfo.changesRequireBuildTime = this._prepareInfo.time; } + + this._prepareInfo.projectFileHash = this.getProjectFileStrippedHash(projectData, platform); } + + this._changesInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus; + return this._changesInfo; } public getPrepareInfoFilePath(platform: string, projectData: IProjectData): string { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let prepareInfoFilePath = path.join(platformData.projectRoot, prepareInfoFileName); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const prepareInfoFilePath = path.join(platformData.projectRoot, prepareInfoFileName); return prepareInfoFilePath; } public getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo { - let prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); + const prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); let prepareInfo: IPrepareInfo = null; if (this.$fs.exists(prepareInfoFilePath)) { try { @@ -129,28 +138,45 @@ export class ProjectChangesService implements IProjectChangesService { } public savePrepareInfo(platform: string, projectData: IProjectData): void { - let prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); + const prepareInfoFilePath = this.getPrepareInfoFilePath(platform, projectData); this.$fs.writeJson(prepareInfoFilePath, this._prepareInfo); } + public setNativePlatformStatus(platform: string, projectData: IProjectData, addedPlatform: IAddedNativePlatform): void { + this._prepareInfo = this._prepareInfo || this.getPrepareInfo(platform, projectData); + if (this._prepareInfo) { + this._prepareInfo.nativePlatformStatus = addedPlatform.nativePlatformStatus; + this.savePrepareInfo(platform, projectData); + } + } + private ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): boolean { this._prepareInfo = this.getPrepareInfo(platform, projectData); if (this._prepareInfo) { - let platformData = this.$platformsData.getPlatformData(platform, projectData); - let prepareInfoFile = path.join(platformData.projectRoot, prepareInfoFileName); + this._prepareInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus && this._prepareInfo.nativePlatformStatus < projectChangesOptions.nativePlatformStatus ? + projectChangesOptions.nativePlatformStatus : + this._prepareInfo.nativePlatformStatus || projectChangesOptions.nativePlatformStatus; + + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const prepareInfoFile = path.join(platformData.projectRoot, prepareInfoFileName); this._outputProjectMtime = this.$fs.getFsStats(prepareInfoFile).mtime.getTime(); this._outputProjectCTime = this.$fs.getFsStats(prepareInfoFile).ctime.getTime(); return false; } + this._prepareInfo = { time: "", + nativePlatformStatus: projectChangesOptions.nativePlatformStatus, bundle: projectChangesOptions.bundle, release: projectChangesOptions.release, changesRequireBuild: true, + projectFileHash: this.getProjectFileStrippedHash(projectData, platform), changesRequireBuildTime: null }; + this._outputProjectMtime = 0; this._outputProjectCTime = 0; + this._changesInfo = this._changesInfo || new ProjectChangesInfo(); this._changesInfo.appFilesChanged = true; this._changesInfo.appResourcesChanged = true; this._changesInfo.modulesChanged = true; @@ -158,38 +184,60 @@ export class ProjectChangesService implements IProjectChangesService { return true; } + private getProjectFileStrippedHash(projectData: IProjectData, platform: string): string { + platform = platform.toLowerCase(); + const projectFilePath = path.join(projectData.projectDir, PACKAGE_JSON_FILE_NAME); + const projectFileContents = this.$fs.readJson(projectFilePath); + _(this.$devicePlatformsConstants) + .keys() + .map(k => k.toLowerCase()) + .difference([platform]) + .each(otherPlatform => { + delete projectFileContents.nativescript[`tns-${otherPlatform}`]; + }); + + return getHash(JSON.stringify(projectFileContents)); + } + + private isProjectFileChanged(projectData: IProjectData, platform: string): boolean { + const projectFileStrippedContentsHash = this.getProjectFileStrippedHash(projectData, platform); + const prepareInfo = this.getPrepareInfo(platform, projectData); + return projectFileStrippedContentsHash !== prepareInfo.projectFileHash; + } + private filesChanged(files: string[]): boolean { - for (let file of files) { + for (const file of files) { if (this.$fs.exists(file)) { - let fileStats = this.$fs.getFsStats(file); + const fileStats = this.$fs.getFsStats(file); if (fileStats.mtime.getTime() >= this._outputProjectMtime || fileStats.ctime.getTime() >= this._outputProjectCTime) { return true; } } } + return false; } private containsNewerFiles(dir: string, skipDir: string, projectData: IProjectData, processFunc?: (filePath: string, projectData: IProjectData) => boolean): boolean { - let files = this.$fs.readDirectory(dir); - for (let file of files) { - let filePath = path.join(dir, file); + const dirFileStat = this.$fs.getFsStats(dir); + if (this.isFileModified(dirFileStat, dir)) { + return true; + } + + const files = this.$fs.readDirectory(dir); + for (const file of files) { + const filePath = path.join(dir, file); if (filePath === skipDir) { continue; } - let fileStats = this.$fs.getFsStats(filePath); - - let changed = fileStats.mtime.getTime() >= this._outputProjectMtime || fileStats.ctime.getTime() >= this._outputProjectCTime; - if (!changed) { - let lFileStats = this.$fs.getLsStats(filePath); - changed = lFileStats.mtime.getTime() >= this._outputProjectMtime || lFileStats.ctime.getTime() >= this._outputProjectCTime; - } + const fileStats = this.$fs.getFsStats(filePath); + const changed = this.isFileModified(fileStats, filePath); if (changed) { if (processFunc) { this._newFiles++; - let filePathRelative = path.relative(projectData.projectDir, filePath); + const filePathRelative = path.relative(projectData.projectDir, filePath); if (processFunc.call(this, filePathRelative, projectData)) { return true; } @@ -203,15 +251,29 @@ export class ProjectChangesService implements IProjectChangesService { return true; } } + } return false; } + private isFileModified(filePathStat: IFsStats, filePath: string): boolean { + let changed = filePathStat.mtime.getTime() >= this._outputProjectMtime || + filePathStat.ctime.getTime() >= this._outputProjectCTime; + + if (!changed) { + const lFileStats = this.$fs.getLsStats(filePath); + changed = lFileStats.mtime.getTime() >= this._outputProjectMtime || + lFileStats.ctime.getTime() >= this._outputProjectCTime; + } + + return changed; + } + private fileChangeRequiresBuild(file: string, projectData: IProjectData) { if (path.basename(file) === "package.json") { return true; } - let projectDir = projectData.projectDir; + const projectDir = projectData.projectDir; if (_.startsWith(path.join(projectDir, file), projectData.appResourcesDirectoryPath)) { return true; } @@ -219,9 +281,9 @@ export class ProjectChangesService implements IProjectChangesService { let filePath = file; while (filePath !== NODE_MODULES_FOLDER_NAME) { filePath = path.dirname(filePath); - let fullFilePath = path.join(projectDir, path.join(filePath, "package.json")); + const fullFilePath = path.join(projectDir, path.join(filePath, "package.json")); if (this.$fs.exists(fullFilePath)) { - let json = this.$fs.readJson(fullFilePath); + const json = this.$fs.readJson(fullFilePath); if (json["nativescript"] && _.startsWith(file, path.join(filePath, "platforms"))) { return true; } diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 58b48e84a8..46703f552d 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import { ProjectData } from "../project-data"; interface IProjectFileData { projectData: any; @@ -10,7 +11,8 @@ export class ProjectDataService implements IProjectDataService { constructor(private $fs: IFileSystem, private $staticConfig: IStaticConfig, - private $logger: ILogger) { + private $logger: ILogger, + private $injector: IInjector) { } public getNSValue(projectDir: string, propertyName: string): any { @@ -31,6 +33,14 @@ export class ProjectDataService implements IProjectDataService { this.$fs.writeJson(projectFileInfo.projectFilePath, projectFileInfo.projectData); } + // TODO: Add tests + // TODO: Remove $projectData and replace it with $projectDataService.getProjectData + public getProjectData(projectDir: string): IProjectData { + const projectDataInstance = this.$injector.resolve(ProjectData); + projectDataInstance.initializeProjectData(projectDir); + return projectDataInstance; + } + private getValue(projectDir: string, propertyName: string): any { const projectData = this.getProjectFileData(projectDir).projectData; @@ -53,7 +63,7 @@ export class ProjectDataService implements IProjectDataService { const props = dottedPropertyName.split("."); let result = jsonData[props.shift()]; - for (let prop of props) { + for (const prop of props) { result = result[prop]; } @@ -64,7 +74,7 @@ export class ProjectDataService implements IProjectDataService { const projectFileInfo = this.getProjectFileData(projectDir); const props = key.split("."); - let data: any = projectFileInfo.projectData; + const data: any = projectFileInfo.projectData; let currentData = data; _.each(props, (prop, index: number) => { @@ -82,7 +92,7 @@ export class ProjectDataService implements IProjectDataService { private removeProperty(projectDir: string, propertyName: string): void { const projectFileInfo = this.getProjectFileData(projectDir); - let data: any = projectFileInfo.projectData; + const data: any = projectFileInfo.projectData; let currentData = data; const props = propertyName.split("."); const propertyToDelete = props.splice(props.length - 1, 1)[0]; diff --git a/lib/services/project-name-service.ts b/lib/services/project-name-service.ts index 3b685d9249..2346a52036 100644 --- a/lib/services/project-name-service.ts +++ b/lib/services/project-name-service.ts @@ -15,7 +15,7 @@ export class ProjectNameService implements IProjectNameService { return await this.promptForNewName("The project name is invalid.", projectName, validateOptions); } - let userCanInteract = isInteractive(); + const userCanInteract = isInteractive(); if (!this.checkIfNameStartsWithLetter(projectName)) { if (!userCanInteract) { @@ -37,7 +37,7 @@ export class ProjectNameService implements IProjectNameService { } private checkIfNameStartsWithLetter(projectName: string): boolean { - let startsWithLetterExpression = /^[a-zA-Z]/; + const startsWithLetterExpression = /^[a-zA-Z]/; return startsWithLetterExpression.test(projectName); } @@ -46,7 +46,7 @@ export class ProjectNameService implements IProjectNameService { return projectName; } - let newProjectName = await this.$prompter.getString("Enter the new project name:"); + const newProjectName = await this.$prompter.getString("Enter the new project name:"); return await this.ensureValidName(newProjectName, validateOptions); } diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 9bc4b1ba25..fcb50d79ca 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -19,8 +19,8 @@ export class ProjectService implements IProjectService { @exported("projectService") public async createProject(projectOptions: IProjectSettings): Promise { - let projectName = projectOptions.projectName, - selectedTemplate = projectOptions.template; + let projectName = projectOptions.projectName; + let selectedTemplate = projectOptions.template; if (!projectName) { this.$errors.fail("You must specify when creating a new project."); @@ -37,7 +37,7 @@ export class ProjectService implements IProjectService { this.$errors.fail("Path already exists and is not empty %s", projectDir); } - let projectId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX); + const projectId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX); this.createPackageJson(projectDir, projectId); this.$logger.trace(`Creating a new NativeScript project with name ${projectName} and id ${projectId} at location ${projectDir}`); @@ -46,12 +46,12 @@ export class ProjectService implements IProjectService { } try { - let templatePath = await this.$projectTemplatesService.prepareTemplate(selectedTemplate, projectDir); + const templatePath = await this.$projectTemplatesService.prepareTemplate(selectedTemplate, projectDir); await this.extractTemplate(projectDir, templatePath); await this.ensureAppResourcesExist(projectDir); - let templatePackageJsonData = this.getDataFromJson(templatePath); + const templatePackageJsonData = this.getDataFromJson(templatePath); if (!(templatePackageJsonData && templatePackageJsonData.dependencies && templatePackageJsonData.dependencies[constants.TNS_CORE_MODULES_NAME])) { await this.$npmInstallationManager.install(constants.TNS_CORE_MODULES_NAME, projectDir, { dependencyType: "save" }); @@ -60,9 +60,13 @@ export class ProjectService implements IProjectService { this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); //merging dependencies from template (dev && prod) this.removeMergedDependencies(projectDir, templatePackageJsonData); - await this.$npm.install(projectDir, projectDir, { "ignore-scripts": projectOptions.ignoreScripts }); + await this.$npm.install(projectDir, projectDir, { + disableNpmInstall: false, + frameworkPath: null, + ignoreScripts: projectOptions.ignoreScripts + }); - let templatePackageJson = this.$fs.readJson(path.join(templatePath, "package.json")); + const templatePackageJson = this.$fs.readJson(path.join(templatePath, "package.json")); await this.$npm.uninstall(templatePackageJson.name, { save: true }, projectDir); } catch (err) { this.$fs.deleteDirectory(projectDir); @@ -83,9 +87,9 @@ export class ProjectService implements IProjectService { } private getDataFromJson(templatePath: string): any { - let templatePackageJsonPath = path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME); + const templatePackageJsonPath = path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME); if (this.$fs.exists(templatePackageJsonPath)) { - let templatePackageJsonData = this.$fs.readJson(templatePackageJsonPath); + const templatePackageJsonData = this.$fs.readJson(templatePackageJsonPath); return templatePackageJsonData; } else { this.$logger.trace(`Template ${templatePath} does not have ${constants.PACKAGE_JSON_FILE_NAME} file.`); @@ -97,7 +101,7 @@ export class ProjectService implements IProjectService { private async extractTemplate(projectDir: string, realTemplatePath: string): Promise { this.$fs.ensureDirectoryExists(projectDir); - let appDestinationPath = path.join(projectDir, constants.APP_FOLDER_NAME); + const appDestinationPath = path.join(projectDir, constants.APP_FOLDER_NAME); this.$fs.createDirectory(appDestinationPath); this.$logger.trace(`Copying application from '${realTemplatePath}' into '${appDestinationPath}'.`); @@ -107,16 +111,22 @@ export class ProjectService implements IProjectService { } private async ensureAppResourcesExist(projectDir: string): Promise { - let appPath = path.join(projectDir, constants.APP_FOLDER_NAME), + const appPath = path.join(projectDir, constants.APP_FOLDER_NAME), appResourcesDestinationPath = path.join(appPath, constants.APP_RESOURCES_FOLDER_NAME); if (!this.$fs.exists(appResourcesDestinationPath)) { this.$fs.createDirectory(appResourcesDestinationPath); // the template installed doesn't have App_Resources -> get from a default template - let defaultTemplateName = constants.RESERVED_TEMPLATE_NAMES["default"]; - await this.$npm.install(defaultTemplateName, projectDir, { save: true, }); - let defaultTemplateAppResourcesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, + const defaultTemplateName = constants.RESERVED_TEMPLATE_NAMES["default"]; + await this.$npm.install(defaultTemplateName, projectDir, { + save: true, + disableNpmInstall: false, + frameworkPath: null, + ignoreScripts: false + }); + + const defaultTemplateAppResourcesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, defaultTemplateName, constants.APP_RESOURCES_FOLDER_NAME); if (this.$fs.exists(defaultTemplateAppResourcesPath)) { @@ -128,8 +138,8 @@ export class ProjectService implements IProjectService { } private removeMergedDependencies(projectDir: string, templatePackageJsonData: any): void { - let extractedTemplatePackageJsonPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.PACKAGE_JSON_FILE_NAME); - for (let key in templatePackageJsonData) { + const extractedTemplatePackageJsonPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.PACKAGE_JSON_FILE_NAME); + for (const key in templatePackageJsonData) { if (constants.PackageJsonKeysToKeep.indexOf(key) === -1) { delete templatePackageJsonData[key]; } @@ -141,8 +151,8 @@ export class ProjectService implements IProjectService { private mergeProjectAndTemplateProperties(projectDir: string, templatePackageJsonData: any): void { if (templatePackageJsonData) { - let projectPackageJsonPath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); - let projectPackageJsonData = this.$fs.readJson(projectPackageJsonPath); + const projectPackageJsonPath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); + const projectPackageJsonData = this.$fs.readJson(projectPackageJsonPath); this.$logger.trace("Initial project package.json data: ", projectPackageJsonData); if (projectPackageJsonData.dependencies || templatePackageJsonData.dependencies) { projectPackageJsonData.dependencies = this.mergeDependencies(projectPackageJsonData.dependencies, templatePackageJsonData.dependencies); @@ -164,8 +174,8 @@ export class ProjectService implements IProjectService { this.$logger.trace("Merging dependencies, projectDependencies are: ", projectDependencies, " templateDependencies are: ", templateDependencies); projectDependencies = projectDependencies || {}; _.extend(projectDependencies, templateDependencies || {}); - let sortedDeps: IStringDictionary = {}; - let dependenciesNames = _.keys(projectDependencies).sort(); + const sortedDeps: IStringDictionary = {}; + const dependenciesNames = _.keys(projectDependencies).sort(); _.each(dependenciesNames, (key: string) => { sortedDeps[key] = projectDependencies[key]; }); diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index fb5292384d..32ac27ec57 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -12,7 +12,7 @@ export class ProjectTemplatesService implements IProjectTemplatesService { public async prepareTemplate(originalTemplateName: string, projectDir: string): Promise { // support @ syntax - let data = originalTemplateName.split("@"), + const data = originalTemplateName.split("@"), name = data[0], version = data[1]; @@ -20,6 +20,12 @@ export class ProjectTemplatesService implements IProjectTemplatesService { await this.$analyticsService.track("Template used for project creation", templateName); + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: constants.TrackActionNames.CreateProject, + isForDevice: null, + additionalData: templateName + }); + const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir); // this removes dependencies from templates so they are not copied to app folder diff --git a/lib/services/subscription-service.ts b/lib/services/subscription-service.ts new file mode 100644 index 0000000000..73778df593 --- /dev/null +++ b/lib/services/subscription-service.ts @@ -0,0 +1,73 @@ +import * as emailValidator from "email-validator"; +import * as queryString from "querystring"; +import * as helpers from "../common/helpers"; + +export class SubscriptionService implements ISubscriptionService { + constructor(private $httpClient: Server.IHttpClient, + private $prompter: IPrompter, + private $userSettingsService: IUserSettingsService, + private $logger: ILogger) { + } + + public async subscribeForNewsletter(): Promise { + if (await this.shouldAskForEmail()) { + this.$logger.out("Leave your e-mail address here to subscribe for NativeScript newsletter and product updates, tips and tricks:"); + const email = await this.getEmail("(press Enter for blank)"); + await this.$userSettingsService.saveSetting("EMAIL_REGISTERED", true); + await this.sendEmail(email); + } + } + + /** + * Checks whether we should ask the current user if they want to subscribe to NativeScript newsletter. + * NOTE: This method is protected, not private, only because of our unit tests. + * @returns {Promise} + */ + protected async shouldAskForEmail(): Promise { + return helpers.isInteractive() && process.env.CLI_NOPROMPT !== "1" && !(await this.$userSettingsService.getSettingValue("EMAIL_REGISTERED")); + } + + private async getEmail(prompt: string, options?: IPrompterOptions): Promise { + const schema: IPromptSchema = { + message: prompt, + type: "input", + name: "inputEmail", + validate: (value: any) => { + if (value === "" || emailValidator.validate(value)) { + return true; + } + + return "Please provide a valid e-mail or simply leave it blank."; + } + }; + + const result = await this.$prompter.get([schema]); + return result.inputEmail; + } + + private async sendEmail(email: string): Promise { + if (email) { + const postData = queryString.stringify({ + 'elqFormName': "dev_uins_cli", + 'elqSiteID': '1325', + 'emailAddress': email, + 'elqCookieWrite': '0' + }); + + const options = { + url: 'https://s1325.t.eloqua.com/e/f2', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + }, + body: postData + }; + + await this.$httpClient.httpRequest(options); + } + } + +} + +$injector.register("subscriptionService", SubscriptionService); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index a547c224e4..318032c65b 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -15,8 +15,7 @@ class TestExecutionService implements ITestExecutionService { constructor(private $injector: IInjector, private $platformService: IPlatformService, private $platformsData: IPlatformsData, - private $usbLiveSyncService: ILiveSyncService, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $liveSyncService: ILiveSyncService, private $debugDataService: IDebugDataService, private $httpClient: Server.IHttpClient, private $config: IConfiguration, @@ -25,57 +24,106 @@ class TestExecutionService implements ITestExecutionService { private $options: IOptions, private $pluginsService: IPluginsService, private $errors: IErrors, - private $androidDebugService: IPlatformDebugService, - private $iOSDebugService: IPlatformDebugService, + private $debugService: IDebugService, private $devicesService: Mobile.IDevicesService, + private $analyticsService: IAnalyticsService, private $childProcess: IChildProcess) { + this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); } public platform: string; - public async startTestRunner(platform: string, projectData: IProjectData): Promise { + public async startTestRunner(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { this.platform = platform; this.$options.justlaunch = true; await new Promise((resolve, reject) => { process.on('message', async (launcherConfig: any) => { try { - let platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); - let projectDir = projectData.projectDir; - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); - let projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); - - let configOptions: IKarmaConfigOptions = JSON.parse(launcherConfig); + const platformData = this.$platformsData.getPlatformData(platform.toLowerCase(), projectData); + const projectDir = projectData.projectDir; + await this.$devicesService.initialize({ + platform: platform, + deviceId: this.$options.device, + emulator: this.$options.emulator + }); + await this.$devicesService.detectCurrentlyAttachedDevices(); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + + const configOptions: IKarmaConfigOptions = JSON.parse(launcherConfig); this.$options.debugBrk = configOptions.debugBrk; this.$options.debugTransport = configOptions.debugTransport; - let configJs = this.generateConfig(this.$options.port.toString(), configOptions); + const configJs = this.generateConfig(this.$options.port.toString(), configOptions); this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs); - let socketIoJsUrl = `http://localhost:${this.$options.port}/socket.io/socket.io.js`; - let socketIoJs = (await this.$httpClient.httpRequest(socketIoJsUrl)).body; + const socketIoJsUrl = `http://localhost:${this.$options.port}/socket.io/socket.io.js`; + const socketIoJs = (await this.$httpClient.httpRequest(socketIoJsUrl)).body; this.$fs.writeFile(path.join(projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs); - const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; - if (!await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, { provision: this.$options.provision, sdk: this.$options.sdk })) { + + if (!await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, this.$options)) { this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } + this.detourEntryPoint(projectFilesPath); const deployOptions: IDeployPlatformOptions = { clean: this.$options.clean, device: this.$options.device, - projectDir: this.$options.path, emulator: this.$options.emulator, + projectDir: this.$options.path, platformTemplate: this.$options.platformTemplate, release: this.$options.release, provision: this.$options.provision, teamId: this.$options.teamId }; - await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); - await this.$usbLiveSyncService.liveSync(platform, projectData); + + if (this.$options.bundle) { + this.$options.watch = false; + } + + const devices = this.$devicesService.getDeviceInstances(); + // Now let's take data for each device: + const platformLowerCase = this.platform && this.platform.toLowerCase(); + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !platformLowerCase || d.deviceInfo.platform.toLowerCase() === platformLowerCase) + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, // this.$options.forDevice, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, projectData); + const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, projectData); + return pathToBuildResult; + } + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { + projectDir: projectData.projectDir, + skipWatcher: !this.$options.watch || this.$options.justlaunch, + watchAllFiles: this.$options.syncAllFiles, + debugOptions: this.$options + }; + + await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); if (this.$options.debugBrk) { this.$logger.info('Starting debugger...'); - let debugService: IPlatformDebugService = this.$injector.resolve(`${platform}DebugService`); + const debugService: IPlatformDebugService = this.$injector.resolve(`${platform}DebugService`); const debugData = this.getDebugData(platform, projectData, deployOptions); await debugService.debugStart(debugData, this.$options); } @@ -90,7 +138,7 @@ class TestExecutionService implements ITestExecutionService { }); } - public async startKarmaServer(platform: string, projectData: IProjectData): Promise { + public async startKarmaServer(platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { platform = platform.toLowerCase(); this.platform = platform; @@ -101,31 +149,35 @@ class TestExecutionService implements ITestExecutionService { // We need the dependencies installed here, so we can start the Karma server. await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); - let projectDir = projectData.projectDir; - await this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }); + const projectDir = projectData.projectDir; + await this.$devicesService.initialize({ + platform: platform, + deviceId: this.$options.device, + emulator: this.$options.emulator + }); - let karmaConfig = this.getKarmaConfiguration(platform, projectData), + const karmaConfig = this.getKarmaConfiguration(platform, projectData), karmaRunner = this.$childProcess.fork(path.join(__dirname, "karma-execution.js")), launchKarmaTests = async (karmaData: any) => { this.$logger.trace("## Unit-testing: Parent process received message", karmaData); let port: string; if (karmaData.url) { port = karmaData.url.port; - let socketIoJsUrl = `http://${karmaData.url.host}/socket.io/socket.io.js`; - let socketIoJs = (await this.$httpClient.httpRequest(socketIoJsUrl)).body; + const socketIoJsUrl = `http://${karmaData.url.host}/socket.io/socket.io.js`; + const socketIoJs = (await this.$httpClient.httpRequest(socketIoJsUrl)).body; this.$fs.writeFile(path.join(projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs); } if (karmaData.launcherConfig) { - let configOptions: IKarmaConfigOptions = JSON.parse(karmaData.launcherConfig); - let configJs = this.generateConfig(port, configOptions); + const configOptions: IKarmaConfigOptions = JSON.parse(karmaData.launcherConfig); + const configJs = this.generateConfig(port, configOptions); this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs); } const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release }; // Prepare the project AFTER the TestExecutionService.CONFIG_FILE_NAME file is created in node_modules // so it will be sent to device. - if (!await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, { provision: this.$options.provision, sdk: this.$options.sdk })) { + if (!await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, projectData, this.$options)) { this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } @@ -141,12 +193,48 @@ class TestExecutionService implements ITestExecutionService { }; if (this.$options.debugBrk) { - const debugService = this.getDebugService(platform); const debugData = this.getDebugData(platform, projectData, deployOptions); - await debugService.debug(debugData, this.$options); + await this.$debugService.debug(debugData, this.$options); } else { - await this.$platformService.deployPlatform(platform, appFilesUpdaterOptions, deployOptions, projectData, { provision: this.$options.provision, sdk: this.$options.sdk }); - await this.$usbLiveSyncService.liveSync(platform, projectData); + const devices = this.$devicesService.getDeviceInstances(); + // Now let's take data for each device: + const platformLowerCase = this.platform && this.platform.toLowerCase(); + const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !platformLowerCase || d.deviceInfo.platform.toLowerCase() === platformLowerCase) + .map(d => { + const info: ILiveSyncDeviceInfo = { + identifier: d.deviceInfo.identifier, + buildAction: async (): Promise => { + const buildConfig: IBuildConfig = { + buildForDevice: !d.isEmulator, + projectDir: this.$options.path, + clean: this.$options.clean, + teamId: this.$options.teamId, + device: this.$options.device, + provision: this.$options.provision, + release: this.$options.release, + keyStoreAlias: this.$options.keyStoreAlias, + keyStorePath: this.$options.keyStorePath, + keyStoreAliasPassword: this.$options.keyStoreAliasPassword, + keyStorePassword: this.$options.keyStorePassword + }; + + await this.$platformService.buildPlatform(d.deviceInfo.platform, buildConfig, projectData); + const pathToBuildResult = await this.$platformService.lastOutputPath(d.deviceInfo.platform, buildConfig, projectData); + return pathToBuildResult; + } + }; + + return info; + }); + + const liveSyncInfo: ILiveSyncInfo = { + projectDir: projectData.projectDir, + skipWatcher: !this.$options.watch || this.$options.justlaunch, + watchAllFiles: this.$options.syncAllFiles, + debugOptions: this.$options + }; + + await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); } }; @@ -177,20 +265,20 @@ class TestExecutionService implements ITestExecutionService { allowedParameters: ICommandParameter[] = []; private detourEntryPoint(projectFilesPath: string): void { - let packageJsonPath = path.join(projectFilesPath, 'package.json'); - let packageJson = this.$fs.readJson(packageJsonPath); + const packageJsonPath = path.join(projectFilesPath, 'package.json'); + const packageJson = this.$fs.readJson(packageJsonPath); packageJson.main = TestExecutionService.MAIN_APP_NAME; this.$fs.writeJson(packageJsonPath, packageJson); } private generateConfig(port: string, options: any): string { - let nics = os.networkInterfaces(); - let ips = Object.keys(nics) + const nics = os.networkInterfaces(); + const ips = Object.keys(nics) .map(nicName => nics[nicName].filter((binding: any) => binding.family === 'IPv4')[0]) .filter(binding => binding) .map(binding => binding.address); - let config = { + const config = { port, ips, options, @@ -199,19 +287,8 @@ class TestExecutionService implements ITestExecutionService { return 'module.exports = ' + JSON.stringify(config); } - private getDebugService(platform: string): IPlatformDebugService { - let lowerCasedPlatform = platform.toLowerCase(); - if (lowerCasedPlatform === this.$devicePlatformsConstants.iOS.toLowerCase()) { - return this.$iOSDebugService; - } else if (lowerCasedPlatform === this.$devicePlatformsConstants.Android.toLowerCase()) { - return this.$androidDebugService; - } - - throw new Error(`Invalid platform ${platform}. Valid platforms are ${this.$devicePlatformsConstants.iOS} and ${this.$devicePlatformsConstants.Android}`); - } - private getKarmaConfiguration(platform: string, projectData: IProjectData): any { - let karmaConfig: any = { + const karmaConfig: any = { browsers: [platform], configFile: path.join(projectData.projectDir, 'karma.conf.js'), _NS: { @@ -248,7 +325,7 @@ class TestExecutionService implements ITestExecutionService { private getDebugData(platform: string, projectData: IProjectData, deployOptions: IDeployPlatformOptions): IDebugData { const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions); - let debugData = this.$debugDataService.createDebugData(projectData, this.$options); + const debugData = this.$debugDataService.createDebugData(projectData, this.$options); debugData.pathToAppPackage = this.$platformService.lastOutputPath(platform, buildConfig, projectData); return debugData; diff --git a/lib/services/user-settings-service.ts b/lib/services/user-settings-service.ts index f42bf2cc63..a89c8495e1 100644 --- a/lib/services/user-settings-service.ts +++ b/lib/services/user-settings-service.ts @@ -3,9 +3,14 @@ import * as userSettingsServiceBaseLib from "../common/services/user-settings-se class UserSettingsService extends userSettingsServiceBaseLib.UserSettingsServiceBase { constructor($fs: IFileSystem, - $options: IOptions) { - let userSettingsFilePath = path.join($options.profileDir, "user-settings.json"); - super(userSettingsFilePath, $fs); + $options: IOptions, + $lockfile: ILockFile) { + const userSettingsFilePath = path.join($options.profileDir, "user-settings.json"); + super(userSettingsFilePath, $fs, $lockfile); + } + + public async loadUserSettingsFile(): Promise { + await this.loadUserSettingsData(); } } $injector.register("userSettingsService", UserSettingsService); diff --git a/lib/services/versions-service.ts b/lib/services/versions-service.ts index 2334713cd8..908a3eb289 100644 --- a/lib/services/versions-service.ts +++ b/lib/services/versions-service.ts @@ -1,3 +1,4 @@ +import { EOL } from "os"; import * as constants from "../constants"; import * as semver from "semver"; import * as path from "path"; @@ -14,13 +15,14 @@ class VersionsService implements IVersionsService { private $npmInstallationManager: INpmInstallationManager, private $injector: IInjector, private $staticConfig: Config.IStaticConfig, - private $pluginsService: IPluginsService) { + private $pluginsService: IPluginsService, + private $logger: ILogger) { this.projectData = this.getProjectData(); } public async getNativescriptCliVersion(): Promise { - let currentCliVersion = this.$staticConfig.version; - let latestCliVersion = await this.$npmInstallationManager.getLatestVersion(constants.NATIVESCRIPT_KEY_NAME); + const currentCliVersion = this.$staticConfig.version; + const latestCliVersion = await this.$npmInstallationManager.getLatestVersion(constants.NATIVESCRIPT_KEY_NAME); return { componentName: constants.NATIVESCRIPT_KEY_NAME, @@ -30,21 +32,21 @@ class VersionsService implements IVersionsService { } public async getTnsCoreModulesVersion(): Promise { - let latestTnsCoreModulesVersion = await this.$npmInstallationManager.getLatestVersion(constants.TNS_CORE_MODULES_NAME); - let nativescriptCoreModulesInfo: IVersionInformation = { + const latestTnsCoreModulesVersion = await this.$npmInstallationManager.getLatestVersion(constants.TNS_CORE_MODULES_NAME); + const nativescriptCoreModulesInfo: IVersionInformation = { componentName: constants.TNS_CORE_MODULES_NAME, latestVersion: latestTnsCoreModulesVersion }; if (this.projectData) { - let nodeModulesPath = path.join(this.projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME); - let tnsCoreModulesPath = path.join(nodeModulesPath, constants.TNS_CORE_MODULES_NAME); + const nodeModulesPath = path.join(this.projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME); + const tnsCoreModulesPath = path.join(nodeModulesPath, constants.TNS_CORE_MODULES_NAME); if (!this.$fs.exists(nodeModulesPath) || !this.$fs.exists(tnsCoreModulesPath)) { await this.$pluginsService.ensureAllDependenciesAreInstalled(this.projectData); } - let currentTnsCoreModulesVersion = this.$fs.readJson(path.join(tnsCoreModulesPath, constants.PACKAGE_JSON_FILE_NAME)).version; + const currentTnsCoreModulesVersion = this.$fs.readJson(path.join(tnsCoreModulesPath, constants.PACKAGE_JSON_FILE_NAME)).version; nativescriptCoreModulesInfo.currentVersion = currentTnsCoreModulesVersion; } @@ -52,7 +54,7 @@ class VersionsService implements IVersionsService { } public async getRuntimesVersions(): Promise { - let runtimes: string[] = [ + const runtimes: string[] = [ constants.TNS_ANDROID_RUNTIME_NAME, constants.TNS_IOS_RUNTIME_NAME ]; @@ -63,17 +65,17 @@ class VersionsService implements IVersionsService { projectConfig = this.$fs.readJson(this.projectData.projectFilePath); } - let runtimesVersions: IVersionInformation[] = await Promise.all(runtimes.map(async (runtime: string) => { - let latestRuntimeVersion = await this.$npmInstallationManager.getLatestVersion(runtime); - let runtimeInformation: IVersionInformation = { + const runtimesVersions: IVersionInformation[] = await Promise.all(runtimes.map(async (runtime: string) => { + const latestRuntimeVersion = await this.$npmInstallationManager.getLatestVersion(runtime); + const runtimeInformation: IVersionInformation = { componentName: runtime, latestVersion: latestRuntimeVersion }; if (projectConfig) { - let projectRuntimeInformation = projectConfig.nativescript && projectConfig.nativescript[runtime]; + const projectRuntimeInformation = projectConfig.nativescript && projectConfig.nativescript[runtime]; if (projectRuntimeInformation) { - let runtimeVersionInProject = projectRuntimeInformation.version; + const runtimeVersionInProject = projectRuntimeInformation.version; runtimeInformation.currentVersion = runtimeVersionInProject; } } @@ -84,9 +86,9 @@ class VersionsService implements IVersionsService { return runtimesVersions; } - public async getComponentsForUpdate(): Promise { - let allComponents: IVersionInformation[] = await this.getAllComponentsVersions(); - let componentsForUpdate: IVersionInformation[] = []; + public async checkComponentsForUpdate(): Promise { + const allComponents: IVersionInformation[] = await this.getAllComponentsVersions(); + const componentsForUpdate: IVersionInformation[] = []; _.forEach(allComponents, (component: IVersionInformation) => { if (component.currentVersion && this.hasUpdate(component)) { @@ -94,23 +96,34 @@ class VersionsService implements IVersionsService { } }); - return componentsForUpdate; + this.printVersionsInformation(componentsForUpdate, allComponents); + } + + private printVersionsInformation(versionsInformation: IVersionInformation[], allComponents: IVersionInformation[]): void { + if (versionsInformation && versionsInformation.length) { + const table: any = this.createTableWithVersionsInformation(versionsInformation); + + this.$logger.warn("Updates available"); + this.$logger.out(table.toString() + EOL); + } else { + this.$logger.out(`Your components are up-to-date: ${EOL}${allComponents.map(component => component.componentName)}${EOL}`); + } } public async getAllComponentsVersions(): Promise { let allComponents: IVersionInformation[] = []; - let nativescriptCliInformation: IVersionInformation = await this.getNativescriptCliVersion(); + const nativescriptCliInformation: IVersionInformation = await this.getNativescriptCliVersion(); if (nativescriptCliInformation) { allComponents.push(nativescriptCliInformation); } - let nativescriptCoreModulesInformation: IVersionInformation = await this.getTnsCoreModulesVersion(); + const nativescriptCoreModulesInformation: IVersionInformation = await this.getTnsCoreModulesVersion(); if (nativescriptCoreModulesInformation) { allComponents.push(nativescriptCoreModulesInformation); } - let runtimesVersions: IVersionInformation[] = await this.getRuntimesVersions(); + const runtimesVersions: IVersionInformation[] = await this.getRuntimesVersions(); allComponents = allComponents.concat(runtimesVersions); @@ -118,11 +131,11 @@ class VersionsService implements IVersionsService { } public createTableWithVersionsInformation(versionsInformation: IVersionInformation[]): any { - let headers = ["Component", "Current version", "Latest version", "Information"]; - let data: string[][] = []; + const headers = ["Component", "Current version", "Latest version", "Information"]; + const data: string[][] = []; _.forEach(versionsInformation, (componentInformation: IVersionInformation) => { - let row: string[] = [ + const row: string[] = [ componentInformation.componentName, componentInformation.currentVersion, componentInformation.latestVersion @@ -142,7 +155,7 @@ class VersionsService implements IVersionsService { private getProjectData(): IProjectData { try { - let projectData: IProjectData = this.$injector.resolve("projectData"); + const projectData: IProjectData = this.$injector.resolve("projectData"); projectData.initializeProjectData(); return projectData; } catch (error) { diff --git a/lib/services/xcconfig-service.ts b/lib/services/xcconfig-service.ts new file mode 100644 index 0000000000..f595d5332b --- /dev/null +++ b/lib/services/xcconfig-service.ts @@ -0,0 +1,40 @@ +export class XCConfigService { + constructor(private $fs: IFileSystem) { + } + + /** + * Returns the Value of a Property from a XC Config file. + * @param xcconfigFilePath + * @param propertyName + */ + public readPropertyValue(xcconfigFilePath: string, propertyName: string): string { + if (this.$fs.exists(xcconfigFilePath)) { + const text = this.$fs.readText(xcconfigFilePath); + + let property: string; + let isPropertyParsed: boolean = false; + text.split(/\r?\n/).forEach((line) => { + line = line.replace(/\/(\/)[^\n]*$/, ""); + if (line.indexOf(propertyName) >= 0) { + const parts = line.split("="); + if (parts.length > 1 && parts[1]) { + property = parts[1].trim(); + isPropertyParsed = true; + if (property[property.length - 1] === ';') { + property = property.slice(0, -1); + } + } + } + }); + + if (isPropertyParsed) { + // property can be an empty string, so we don't check for that. + return property; + } + } + + return null; + } +} + +$injector.register("xCConfigService", XCConfigService); diff --git a/lib/services/xcproj-service.ts b/lib/services/xcproj-service.ts index 0746d9fb65..a3894d8dbe 100644 --- a/lib/services/xcproj-service.ts +++ b/lib/services/xcproj-service.ts @@ -13,9 +13,9 @@ class XcprojService implements IXcprojService { } public async verifyXcproj(shouldFail: boolean): Promise { - let xcprojInfo = await this.getXcprojInfo(); + const xcprojInfo = await this.getXcprojInfo(); if (xcprojInfo.shouldUseXcproj && !xcprojInfo.xcprojAvailable) { - let errorMessage = `You are using CocoaPods version ${xcprojInfo.cocoapodVer} which does not support Xcode ${xcprojInfo.xcodeVersion.major}.${xcprojInfo.xcodeVersion.minor} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH. Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`; + const errorMessage = `You are using CocoaPods version ${xcprojInfo.cocoapodVer} which does not support Xcode ${xcprojInfo.xcodeVersion.major}.${xcprojInfo.xcodeVersion.minor} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH. Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`; if (shouldFail) { this.$errors.failWithoutHelp(errorMessage); } else { @@ -30,8 +30,8 @@ class XcprojService implements IXcprojService { public async getXcprojInfo(): Promise { if (!this.xcprojInfoCache) { - let cocoapodVer = await this.$sysInfo.getCocoapodVersion(), - xcodeVersion = await this.$xcodeSelectService.getXcodeVersion(); + let cocoapodVer = await this.$sysInfo.getCocoapodVersion(); + const xcodeVersion = await this.$xcodeSelectService.getXcodeVersion(); if (cocoapodVer && !semver.valid(cocoapodVer)) { // Cocoapods betas have names like 1.0.0.beta.8 @@ -44,8 +44,8 @@ class XcprojService implements IXcprojService { // CocoaPods with version lower than 1.0.0 don't support Xcode 7.3 yet // https://github.com/CocoaPods/CocoaPods/issues/2530#issuecomment-210470123 // as a result of this all .pbxprojects touched by CocoaPods get converted to XML plist format - let shouldUseXcproj = cocoapodVer && !!(semver.lt(cocoapodVer, "1.0.0") && ~helpers.versionCompare(xcodeVersion, "7.3.0")), - xcprojAvailable: boolean; + const shouldUseXcproj = cocoapodVer && !!(semver.lt(cocoapodVer, "1.0.0") && ~helpers.versionCompare(xcodeVersion, "7.3.0")); + let xcprojAvailable: boolean; if (shouldUseXcproj) { // if that's the case we can use xcproj gem to convert them back to ASCII plist format diff --git a/lib/sys-info.ts b/lib/sys-info.ts index 6f7fdd904a..d2ac752a47 100644 --- a/lib/sys-info.ts +++ b/lib/sys-info.ts @@ -13,7 +13,7 @@ export class SysInfo extends SysInfoBase { } public async getSysInfo(pathToPackageJson: string, androidToolsInfo?: { pathToAdb: string }): Promise { - let defaultAndroidToolsInfo = { + const defaultAndroidToolsInfo = { pathToAdb: await this.$androidToolsInfo.getPathToAdbFromAndroidHome() }; diff --git a/lib/tools/node-modules/node-modules-builder.ts b/lib/tools/node-modules/node-modules-builder.ts index 52bea13107..8e84bb8031 100644 --- a/lib/tools/node-modules/node-modules-builder.ts +++ b/lib/tools/node-modules/node-modules-builder.ts @@ -1,22 +1,37 @@ import * as shelljs from "shelljs"; import { TnsModulesCopy, NpmPluginPrepare } from "./node-modules-dest-copy"; -import { NodeModulesDependenciesBuilder } from "./node-modules-dependencies-builder"; export class NodeModulesBuilder implements INodeModulesBuilder { constructor(private $fs: IFileSystem, private $injector: IInjector, - private $options: IOptions + private $options: IOptions, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder ) { } - public async prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData): Promise { + public async prepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { + const productionDependencies = this.initialPrepareNodeModules(absoluteOutputPath, platform, lastModifiedTime, projectData); + const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); + await npmPluginPrepare.preparePlugins(productionDependencies, platform, projectData, projectFilesConfig); + } + + public async prepareJSNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { + const productionDependencies = this.initialPrepareNodeModules(absoluteOutputPath, platform, lastModifiedTime, projectData); + const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); + await npmPluginPrepare.prepareJSPlugins(productionDependencies, platform, projectData, projectFilesConfig); + } + + public cleanNodeModules(absoluteOutputPath: string, platform: string): void { + shelljs.rm("-rf", absoluteOutputPath); + } + + private initialPrepareNodeModules(absoluteOutputPath: string, platform: string, lastModifiedTime: Date, projectData: IProjectData, ): IDependencyData[] { + const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir); + if (!this.$fs.exists(absoluteOutputPath)) { // Force copying if the destination doesn't exist. lastModifiedTime = null; } - let dependenciesBuilder = this.$injector.resolve(NodeModulesDependenciesBuilder, {}); - let productionDependencies = dependenciesBuilder.getProductionDependencies(projectData.projectDir); - if (!this.$options.bundle) { const tnsModulesCopy = this.$injector.resolve(TnsModulesCopy, { outputRoot: absoluteOutputPath @@ -26,12 +41,7 @@ export class NodeModulesBuilder implements INodeModulesBuilder { this.cleanNodeModules(absoluteOutputPath, platform); } - const npmPluginPrepare: NpmPluginPrepare = this.$injector.resolve(NpmPluginPrepare); - await npmPluginPrepare.preparePlugins(productionDependencies, platform, projectData); - } - - public cleanNodeModules(absoluteOutputPath: string, platform: string): void { - shelljs.rm("-rf", absoluteOutputPath); + return productionDependencies; } } diff --git a/lib/tools/node-modules/node-modules-dependencies-builder.ts b/lib/tools/node-modules/node-modules-dependencies-builder.ts index eb946c30ce..f2482819b5 100644 --- a/lib/tools/node-modules/node-modules-dependencies-builder.ts +++ b/lib/tools/node-modules/node-modules-dependencies-builder.ts @@ -1,111 +1,110 @@ import * as path from "path"; -import * as fs from "fs"; +import { NODE_MODULES_FOLDER_NAME, PACKAGE_JSON_FILE_NAME } from "../../constants"; -export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesBuilder { - private projectPath: string; - private rootNodeModulesPath: string; - private resolvedDependencies: any[]; - private seen: any; - - public constructor(private $fs: IFileSystem) { - this.seen = {}; - this.resolvedDependencies = []; - } - - public getProductionDependencies(projectPath: string): any[] { - this.projectPath = projectPath; - this.rootNodeModulesPath = path.join(this.projectPath, "node_modules"); - - let projectPackageJsonpath = path.join(this.projectPath, "package.json"); - let packageJsonContent = this.$fs.readJson(projectPackageJsonpath); - - _.keys(packageJsonContent.dependencies).forEach(dependencyName => { - let depth = 0; - let directory = path.join(this.rootNodeModulesPath, dependencyName); - - // find and traverse child with name `key`, parent's directory -> dep.directory - this.traverseDependency(dependencyName, directory, depth); - }); - - return this.resolvedDependencies; - } +interface IDependencyDescription { + parentDir: string; + name: string; + depth: number; +} - private traverseDependency(name: string, currentModulePath: string, depth: number): void { - // Check if child has been extracted in the parent's node modules, AND THEN in `node_modules` - // Slower, but prevents copying wrong versions if multiple of the same module are installed - // Will also prevent copying project's devDependency's version if current module depends on another version - let modulePath = path.join(currentModulePath, "node_modules", name); // node_modules/parent/node_modules/ - let alternativeModulePath = path.join(this.rootNodeModulesPath, name); +export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesBuilder { + public constructor(private $fs: IFileSystem) { } + + public getProductionDependencies(projectPath: string): IDependencyData[] { + const rootNodeModulesPath = path.join(projectPath, NODE_MODULES_FOLDER_NAME); + const projectPackageJsonPath = path.join(projectPath, PACKAGE_JSON_FILE_NAME); + const packageJsonContent = this.$fs.readJson(projectPackageJsonPath); + const dependencies = packageJsonContent && packageJsonContent.dependencies; + + const resolvedDependencies: IDependencyData[] = []; + + const queue: IDependencyDescription[] = _.keys(dependencies) + .map(dependencyName => ({ + parentDir: projectPath, + name: dependencyName, + depth: 0 + })); + + while (queue.length) { + const currentModule = queue.shift(); + const resolvedDependency = this.findModule(rootNodeModulesPath, currentModule.parentDir, currentModule.name, currentModule.depth, resolvedDependencies); + + if (resolvedDependency && !_.some(resolvedDependencies, r => r.directory === resolvedDependency.directory)) { + _.each(resolvedDependency.dependencies, d => { + const dependency: IDependencyDescription = { name: d, parentDir: resolvedDependency.directory, depth: resolvedDependency.depth + 1 }; + + const shouldAdd = !_.some(queue, element => + element.name === dependency.name && + element.parentDir === dependency.parentDir && + element.depth === dependency.depth); + + if (shouldAdd) { + queue.push(dependency); + } + }); + + resolvedDependencies.push(resolvedDependency); + } + } - this.findModule(modulePath, alternativeModulePath, name, depth); + return resolvedDependencies; } - private findModule(modulePath: string, alternativeModulePath: string, name: string, depth: number) { - let exists = this.moduleExists(modulePath); + private findModule(rootNodeModulesPath: string, parentModulePath: string, name: string, depth: number, resolvedDependencies: IDependencyData[]): IDependencyData { + let modulePath = path.join(parentModulePath, NODE_MODULES_FOLDER_NAME, name); // node_modules/parent/node_modules/ + const rootModulesPath = path.join(rootNodeModulesPath, name); + let depthInNodeModules = depth; - if (exists) { - if (this.seen[modulePath]) { - return; + if (!this.moduleExists(modulePath)) { + modulePath = rootModulesPath; // /node_modules/ + if (!this.moduleExists(modulePath)) { + return null; } - let dependency = this.addDependency(name, modulePath, depth + 1); - this.readModuleDependencies(modulePath, depth + 1, dependency); - } else { - modulePath = alternativeModulePath; // /node_modules/ - exists = this.moduleExists(modulePath); + depthInNodeModules = 0; + } - if (!exists) { - return; - } + if (_.some(resolvedDependencies, r => r.name === name && r.directory === modulePath)) { + return null; - if (this.seen[modulePath]) { - return; - } - - let dependency = this.addDependency(name, modulePath, 0); - this.readModuleDependencies(modulePath, 0, dependency); } - this.seen[modulePath] = true; + return this.getDependencyData(name, modulePath, depthInNodeModules); } - private readModuleDependencies(modulePath: string, depth: number, currentModule: any): void { - let packageJsonPath = path.join(modulePath, 'package.json'); - let packageJsonExists = fs.lstatSync(packageJsonPath).isFile(); + private getDependencyData(name: string, directory: string, depth: number): IDependencyData { + const dependency: IDependencyData = { + name, + directory, + depth + }; + + const packageJsonPath = path.join(directory, PACKAGE_JSON_FILE_NAME); + const packageJsonExists = this.$fs.getLsStats(packageJsonPath).isFile(); if (packageJsonExists) { - let packageJsonContents = this.$fs.readJson(packageJsonPath); + const packageJsonContents = this.$fs.readJson(packageJsonPath); if (!!packageJsonContents.nativescript) { // add `nativescript` property, necessary for resolving plugins - currentModule.nativescript = packageJsonContents.nativescript; + dependency.nativescript = packageJsonContents.nativescript; } - _.keys(packageJsonContents.dependencies).forEach((dependencyName) => { - this.traverseDependency(dependencyName, modulePath, depth); - }); + dependency.dependencies = _.keys(packageJsonContents.dependencies); + return dependency; } - } - private addDependency(name: string, directory: string, depth: number): any { - let dependency: any = { - name, - directory, - depth - }; - - this.resolvedDependencies.push(dependency); - - return dependency; + return null; } private moduleExists(modulePath: string): boolean { try { - let exists = fs.lstatSync(modulePath); - if (exists.isSymbolicLink()) { - exists = fs.lstatSync(fs.realpathSync(modulePath)); + let modulePathLsStat = this.$fs.getLsStats(modulePath); + if (modulePathLsStat.isSymbolicLink()) { + modulePathLsStat = this.$fs.getLsStats(this.$fs.realpath(modulePath)); } - return exists.isDirectory(); + + return modulePathLsStat.isDirectory(); } catch (e) { return false; } diff --git a/lib/tools/node-modules/node-modules-dest-copy.ts b/lib/tools/node-modules/node-modules-dest-copy.ts index de313c1f66..37d6a03903 100644 --- a/lib/tools/node-modules/node-modules-dest-copy.ts +++ b/lib/tools/node-modules/node-modules-dest-copy.ts @@ -15,40 +15,66 @@ export class TnsModulesCopy { ) { } - public copyModules(dependencies: any[], platform: string): void { - for (let entry in dependencies) { - let dependency = dependencies[entry]; + public copyModules(dependencies: IDependencyData[], platform: string): void { + for (const entry in dependencies) { + const dependency = dependencies[entry]; this.copyDependencyDir(dependency); if (dependency.name === constants.TNS_CORE_MODULES_NAME) { - let tnsCoreModulesResourcePath = path.join(this.outputRoot, constants.TNS_CORE_MODULES_NAME); + const tnsCoreModulesResourcePath = path.join(this.outputRoot, constants.TNS_CORE_MODULES_NAME); // Remove .ts files - let allFiles = this.$fs.enumerateFilesInDirectorySync(tnsCoreModulesResourcePath); - let matchPattern = this.$options.release ? "**/*.ts" : "**/*.d.ts"; + const allFiles = this.$fs.enumerateFilesInDirectorySync(tnsCoreModulesResourcePath); + const matchPattern = this.$options.release ? "**/*.ts" : "**/*.d.ts"; allFiles.filter(file => minimatch(file, matchPattern, { nocase: true })).map(file => this.$fs.deleteFile(file)); - shelljs.rm("-rf", path.join(tnsCoreModulesResourcePath, "node_modules")); + shelljs.rm("-rf", path.join(tnsCoreModulesResourcePath, constants.NODE_MODULES_FOLDER_NAME)); } } } - private copyDependencyDir(dependency: any): void { + private copyDependencyDir(dependency: IDependencyData): void { if (dependency.depth === 0) { - let isScoped = dependency.name.indexOf("@") === 0; - let targetDir = this.outputRoot; + const targetPackageDir = path.join(this.outputRoot, dependency.name); - if (isScoped) { - targetDir = path.join(this.outputRoot, dependency.name.substring(0, dependency.name.indexOf("/"))); - } + shelljs.mkdir("-p", targetPackageDir); - shelljs.mkdir("-p", targetDir); - shelljs.cp("-Rf", dependency.directory, targetDir); + const isScoped = dependency.name.indexOf("@") === 0; + const destinationPath = isScoped ? path.join(this.outputRoot, dependency.name.substring(0, dependency.name.indexOf("/"))) : this.outputRoot; + shelljs.cp("-RfL", dependency.directory, destinationPath); - //remove platform-specific files (processed separately by plugin services) - const targetPackageDir = path.join(targetDir, dependency.name); + // remove platform-specific files (processed separately by plugin services) shelljs.rm("-rf", path.join(targetPackageDir, "platforms")); + + this.removeNonProductionDependencies(dependency, targetPackageDir); + } + } + + private removeNonProductionDependencies(dependency: IDependencyData, targetPackageDir: string): void { + const packageJsonFilePath = path.join(dependency.directory, constants.PACKAGE_JSON_FILE_NAME); + if (!this.$fs.exists(packageJsonFilePath)) { + return; + } + + const packageJsonContent = this.$fs.readJson(packageJsonFilePath); + const productionDependencies = packageJsonContent.dependencies; + + const dependenciesFolder = path.join(targetPackageDir, constants.NODE_MODULES_FOLDER_NAME); + if (this.$fs.exists(dependenciesFolder)) { + const dependencies = _.flatten(this.$fs.readDirectory(dependenciesFolder) + .map(dir => { + if (_.startsWith(dir, "@")) { + const pathToDir = path.join(dependenciesFolder, dir); + const contents = this.$fs.readDirectory(pathToDir); + return _.map(contents, subDir => `${dir}/${subDir}`); + } + + return dir; + })); + + dependencies.filter(dir => !productionDependencies || !productionDependencies.hasOwnProperty(dir)) + .forEach(dir => shelljs.rm("-rf", path.join(dependenciesFolder, dir))); } } } @@ -57,22 +83,23 @@ export class NpmPluginPrepare { constructor( private $fs: IFileSystem, private $pluginsService: IPluginsService, - private $platformsData: IPlatformsData + private $platformsData: IPlatformsData, + private $logger: ILogger ) { } - protected async beforePrepare(dependencies: IDictionary, platform: string, projectData: IProjectData): Promise { + protected async beforePrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise { await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.beforePrepareAllPlugins(projectData, dependencies); } - protected async afterPrepare(dependencies: IDictionary, platform: string, projectData: IProjectData): Promise { + protected async afterPrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise { await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.afterPrepareAllPlugins(projectData); this.writePreparedDependencyInfo(dependencies, platform, projectData); } - private writePreparedDependencyInfo(dependencies: IDictionary, platform: string, projectData: IProjectData): void { - let prepareData: IDictionary = {}; - _.values(dependencies).forEach(d => { + private writePreparedDependencyInfo(dependencies: IDependencyData[], platform: string, projectData: IProjectData): void { + const prepareData: IDictionary = {}; + _.each(dependencies, d => { prepareData[d.name] = true; }); this.$fs.createDirectory(this.preparedPlatformsDir(platform, projectData)); @@ -101,10 +128,10 @@ export class NpmPluginPrepare { return this.$fs.readJson(this.preparedPlatformsFile(platform, projectData), "utf8"); } - private allPrepared(dependencies: IDictionary, platform: string, projectData: IProjectData): boolean { + private allPrepared(dependencies: IDependencyData[], platform: string, projectData: IProjectData): boolean { let result = true; const previouslyPrepared = this.getPreviouslyPreparedDependencies(platform, projectData); - _.values(dependencies).forEach(d => { + _.each(dependencies, d => { if (!previouslyPrepared[d.name]) { result = false; } @@ -112,20 +139,43 @@ export class NpmPluginPrepare { return result; } - public async preparePlugins(dependencies: IDictionary, platform: string, projectData: IProjectData): Promise { + public async preparePlugins(dependencies: IDependencyData[], platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { if (_.isEmpty(dependencies) || this.allPrepared(dependencies, platform, projectData)) { return; } await this.beforePrepare(dependencies, platform, projectData); - for (let dependencyKey in dependencies) { + for (const dependencyKey in dependencies) { const dependency = dependencies[dependencyKey]; - let isPlugin = !!dependency.nativescript; + const isPlugin = !!dependency.nativescript; if (isPlugin) { - await this.$pluginsService.prepare(dependency, platform, projectData); + const pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); + await this.$pluginsService.preparePluginNativeCode(pluginData, platform, projectData); } } await this.afterPrepare(dependencies, platform, projectData); } + + public async prepareJSPlugins(dependencies: IDependencyData[], platform: string, projectData: IProjectData, projectFilesConfig: IProjectFilesConfig): Promise { + if (_.isEmpty(dependencies) || this.allPrepared(dependencies, platform, projectData)) { + return; +} + + for (const dependencyKey in dependencies) { + const dependency = dependencies[dependencyKey]; + const isPlugin = !!dependency.nativescript; + if (isPlugin) { + platform = platform.toLowerCase(); + const pluginData = this.$pluginsService.convertToPluginData(dependency, projectData.projectDir); + const platformData = this.$platformsData.getPlatformData(platform, projectData); + const appFolderExists = this.$fs.exists(path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); + if (appFolderExists) { + this.$pluginsService.preparePluginScripts(pluginData, platform, projectData, projectFilesConfig); + // Show message + this.$logger.out(`Successfully prepared plugin ${pluginData.name} for ${platform}.`); + } + } + } + } } diff --git a/lib/xml-validator.ts b/lib/xml-validator.ts index 25d31dcf89..6d33e1ca28 100644 --- a/lib/xml-validator.ts +++ b/lib/xml-validator.ts @@ -10,8 +10,8 @@ export class XmlValidator implements IXmlValidator { sourceFiles .filter(file => _.endsWith(file, constants.XML_FILE_EXTENSION)) .forEach(file => { - let errorOutput = this.getXmlFileErrors(file); - let hasErrors = !!errorOutput; + const errorOutput = this.getXmlFileErrors(file); + const hasErrors = !!errorOutput; xmlHasErrors = xmlHasErrors || hasErrors; if (hasErrors) { this.$logger.info(`${file} has syntax errors.`.red.bold); @@ -23,8 +23,8 @@ export class XmlValidator implements IXmlValidator { public getXmlFileErrors(sourceFile: string): string { let errorOutput = ""; - let fileContents = this.$fs.readText(sourceFile); - let domErrorHandler = (level: any, msg: string) => { + const fileContents = this.$fs.readText(sourceFile); + const domErrorHandler = (level: any, msg: string) => { errorOutput += level + EOL + msg + EOL; }; this.getDomParser(domErrorHandler).parseFromString(fileContents, "text/xml"); @@ -33,8 +33,8 @@ export class XmlValidator implements IXmlValidator { } private getDomParser(errorHandler: (level: any, msg: string) => void): any { - let DomParser = require("xmldom").DOMParser; - let parser = new DomParser({ + const DomParser = require("xmldom").DOMParser; + const parser = new DomParser({ locator: {}, errorHandler: errorHandler }); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 0000000000..86732bf537 --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,5424 @@ +{ + "name": "nativescript", + "version": "3.3.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chai": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.1.tgz", + "integrity": "sha1-N/6neWF8/sP9KxmgJH6LvdUTO/Y=", + "dev": true + }, + "@types/chai-as-promised": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-0.0.31.tgz", + "integrity": "sha1-4ekF6m2XHa/K02VgyPH3p9aQxeU=", + "dev": true, + "requires": { + "@types/chai": "4.0.1" + } + }, + "@types/chokidar": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.6.0.tgz", + "integrity": "sha1-2xhDNg1UjyZ+84o1+Tj+IkM8Uoc=", + "dev": true, + "requires": { + "@types/node": "6.0.61" + } + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "dev": true, + "requires": { + "@types/node": "6.0.61" + } + }, + "@types/lockfile": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/lockfile/-/lockfile-1.0.0.tgz", + "integrity": "sha512-pD6JuijPmrfi84qF3/TzGQ7zi0QIX+d7ZdetD6jUA6cp+IsCzAquXZfi5viesew+pfpOTIdAVKuh1SHA7KeKzg==", + "dev": true + }, + "@types/node": { + "version": "6.0.61", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.61.tgz", + "integrity": "sha1-7qF0itmd7K8xm1cQFwGGMZdKxvA=", + "dev": true + }, + "@types/qr-image": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/qr-image/-/qr-image-3.2.0.tgz", + "integrity": "sha1-09XkVzWSOnIpoHbH/JC6ufDcYgs=", + "dev": true, + "requires": { + "@types/node": "6.0.61" + } + }, + "@types/request": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@types/request/-/request-0.0.45.tgz", + "integrity": "sha1-xuUr6LEI6wNcNaqa9Wo4omDD5+Y=", + "dev": true, + "requires": { + "@types/form-data": "0.0.33", + "@types/node": "6.0.61" + } + }, + "@types/semver": { + "version": "5.3.32", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.3.32.tgz", + "integrity": "sha1-p6/cS+5xPAgRFM2BG1G+EJB9e64=", + "dev": true + }, + "@types/source-map": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/source-map/-/source-map-0.5.0.tgz", + "integrity": "sha1-3TS72OMv5OdPLj2KwH+KpbRaR6w=", + "dev": true + }, + "@types/universal-analytics": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/universal-analytics/-/universal-analytics-0.4.1.tgz", + "integrity": "sha512-AZSPpDUEZ4mAgO9geHc62dp/xCLmBJ1yIpbgTq5W/cWcVQsxmU/FyKwYKHXk2hnT9TAmYVFFdAijMrCdYjuHsA==", + "dev": true + }, + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true + }, + "acorn": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", + "integrity": "sha1-U/4WERH5EquZnuiHqQoLxSgi/XU=", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "ansicolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + }, + "anymatch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", + "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "requires": { + "arrify": "1.0.1", + "micromatch": "2.3.11" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true, + "requires": { + "chalk": "1.1.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", + "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "big-integer": { + "version": "1.6.23", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.23.tgz", + "integrity": "sha1-6F1QgiDHTj9DpM5y7tUfPaTblNE=" + }, + "binary-extensions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", + "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=" + }, + "body-parser": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", + "integrity": "sha1-EBXLH+LEQ4WCWVgdtTMy+NDPUPk=", + "dev": true, + "requires": { + "bytes": "2.2.0", + "content-type": "1.0.2", + "debug": "2.2.0", + "depd": "1.1.0", + "http-errors": "1.3.1", + "iconv-lite": "0.4.13", + "on-finished": "2.3.0", + "qs": "5.2.0", + "raw-body": "2.1.7", + "type-is": "1.6.15" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "qs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", + "integrity": "sha1-qfMRQq9GjLcrJbMBNrokVoNJFr4=", + "dev": true + } + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "bplist-creator": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", + "integrity": "sha1-N98VNgkoJLh8QvlXsBNEEXNyrkU=", + "requires": { + "stream-buffers": "2.2.0" + } + }, + "bplist-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.0.tgz", + "integrity": "sha1-Ywgj8gVkN9Tb78IOhAF/i6xI4Ag=" + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "bufferpack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bufferpack/-/bufferpack-0.0.6.tgz", + "integrity": "sha1-+z2HOKDh5OA7z/mfmnX57Bip1z4=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "byline": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/byline/-/byline-4.2.1.tgz", + "integrity": "sha1-90pm+m2P7/iLJyXgsrDPgwzfP4Y=" + }, + "bytes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz", + "integrity": "sha1-/TVGSkA/b5EXwt42Cez/nK4ABYg=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "cardinal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-1.0.0.tgz", + "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "requires": { + "ansicolors": "0.2.1", + "redeyed": "1.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chai": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.0.2.tgz", + "integrity": "sha1-L3MnxN5vOF3XeHmZ4qsCaXoyuDs=", + "dev": true, + "requires": { + "assertion-error": "1.0.2", + "check-error": "1.0.2", + "deep-eql": "2.0.2", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.3" + } + }, + "chai-as-promised": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.0.0.tgz", + "integrity": "sha1-yH7mE+qhlnZjk9pvu0BS8RKs9nU=", + "dev": true, + "requires": { + "check-error": "1.0.2", + "eslint": "3.19.0" + } + }, + "chalk": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.0.tgz", + "integrity": "sha1-CbRTzsSXp1Ug5KYK5IIUqHAOCSE=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "requires": { + "anymatch": "1.3.0", + "async-each": "1.0.1", + "fsevents": "1.1.2", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, + "circular-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", + "dev": true + }, + "cli-color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.3.2.tgz", + "integrity": "sha1-dfpfcowwjMSsWUsF4GzF2A2szYY=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.24", + "memoizee": "0.3.10", + "timers-ext": "0.1.2" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "1.0.1" + } + }, + "cli-table": { + "version": "https://github.com/telerik/cli-table/tarball/v0.3.1.2", + "integrity": "sha1-B+E8MRgVTFOJPTvnp+CNmQ6G2Fk=", + "requires": { + "colors": "1.0.3", + "lodash": "3.6.0", + "wcwidth": "1.0.1" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "lodash": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.6.0.tgz", + "integrity": "sha1-Umao9J3Zib5Pn2gbbyoMVShdDZo=" + } + } + }, + "cli-width": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", + "integrity": "sha1-pNKT72frt7iNSk1CwMzwDE0eNm0=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=" + }, + "clui": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/clui/-/clui-0.3.1.tgz", + "integrity": "sha1-AT0ILOht2/BguG05J/iauMM79CM=", + "requires": { + "cli-color": "0.3.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "coffee-script": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz", + "integrity": "sha1-EpOLz5vhlI+gBvkuDEyegXBRCMA=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "content-type": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "csproj2ts": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/csproj2ts/-/csproj2ts-0.0.8.tgz", + "integrity": "sha1-nRxxniDELM6MTeKQCO/DVUn9Em8=", + "dev": true, + "requires": { + "es6-promise": "4.1.1", + "lodash": "4.17.4", + "semver": "5.3.0", + "xml2js": "0.4.17" + }, + "dependencies": { + "es6-promise": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", + "integrity": "sha1-iBHpCRXZoNujYnTwskLb2nj5ySo=", + "dev": true + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "d": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz", + "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=", + "requires": { + "es5-ext": "0.10.24" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "date-format": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.0.tgz", + "integrity": "sha1-CSBoY6sHDrRZrOpVQsvYVrEZZrM=" + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-eql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz", + "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=", + "dev": true, + "requires": { + "type-detect": "3.0.0" + }, + "dependencies": { + "type-detect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz", + "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "1.0.2" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "doctrine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", + "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "email-validator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-1.0.4.tgz", + "integrity": "sha1-NHZdk3157uh3P7Q2FvxMAWSCEW8=" + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es5-ext": { + "version": "0.10.24", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.24.tgz", + "integrity": "sha1-pVh3yZJLwMjZvTwsvhdJWsFwmxQ=", + "requires": { + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", + "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-symbol": "3.1.1" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "0.10.24" + } + } + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-iterator": "2.0.1", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.24" + } + } + } + }, + "es6-promise": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", + "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc=", + "dev": true + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.24" + } + } + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "0.10.24" + } + } + } + }, + "es6-weak-map": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-0.1.4.tgz", + "integrity": "sha1-cGzvnpmqI2undmwjnIueKG6n0ig=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.24", + "es6-iterator": "0.1.3", + "es6-symbol": "2.0.1" + }, + "dependencies": { + "es6-iterator": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz", + "integrity": "sha1-1vWLjE/EE8JJtLqhl2j45NfIlE4=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.24", + "es6-symbol": "2.0.1" + } + }, + "es6-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz", + "integrity": "sha1-dhtcZ8/U8dGK+yNPaR1nhoLLO/M=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.24" + } + } + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.24" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24", + "es6-iterator": "2.0.1", + "es6-symbol": "3.1.1" + } + } + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "chalk": "1.1.3", + "concat-stream": "1.6.0", + "debug": "2.6.8", + "doctrine": "2.0.0", + "escope": "3.6.0", + "espree": "3.4.3", + "esquery": "1.0.0", + "estraverse": "4.2.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "glob": "7.1.2", + "globals": "9.18.0", + "ignore": "3.3.3", + "imurmurhash": "0.1.4", + "inquirer": "0.12.0", + "is-my-json-valid": "2.16.0", + "is-resolvable": "1.0.0", + "js-yaml": "3.9.0", + "json-stable-stringify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.13.1", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "1.2.1", + "progress": "1.1.8", + "require-uncached": "1.0.3", + "shelljs": "0.7.6", + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1", + "table": "3.8.3", + "text-table": "0.2.0", + "user-home": "2.0.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "cli-width": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", + "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", + "dev": true + }, + "inquirer": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "1.4.0", + "ansi-regex": "2.1.1", + "chalk": "1.1.3", + "cli-cursor": "1.0.2", + "cli-width": "2.1.0", + "figures": "1.7.0", + "lodash": "4.13.1", + "readline2": "1.0.1", + "run-async": "0.1.0", + "rx-lite": "3.1.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "through": "2.3.8" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "mute-stream": "0.0.5" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "espree": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz", + "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=", + "dev": true, + "requires": { + "acorn": "5.1.1", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.0.tgz", + "integrity": "sha1-dM+w5K5D8LgVQdzDAFD52ssfcH4=" + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.24" + }, + "dependencies": { + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "0.10.24" + } + } + } + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.3" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": "0.6.5" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.2.2", + "object-assign": "4.1.1" + } + }, + "file-sync-cmp": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", + "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "filesize": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.1.2.tgz", + "integrity": "sha1-jB0EdXYIY3CZmyPDLyF8TVGr8oo=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "dev": true, + "requires": { + "glob": "5.0.15" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.2", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "flat-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", + "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", + "dev": true, + "requires": { + "circular-json": "0.3.1", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha1-MoK3E/s62A7eDp/PRhG1qm/AM/Q=", + "optional": true, + "requires": { + "nan": "2.6.2", + "node-pre-gyp": "0.6.36" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "bundled": true, + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, + "gaze": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.0.tgz", + "integrity": "sha1-dNP/sBEO3nFcnxW7pWxum4UdPOA=", + "requires": { + "globule": "1.2.0" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "1.0.2" + } + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + }, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "globule": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", + "requires": { + "glob": "7.1.2", + "lodash": "4.17.4", + "minimatch": "3.0.2" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "grunt": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.1.tgz", + "integrity": "sha1-6HeHZOlEsY8yuw8QuQeEdcnftWs=", + "dev": true, + "requires": { + "coffee-script": "1.10.0", + "dateformat": "1.0.12", + "eventemitter2": "0.4.14", + "exit": "0.1.2", + "findup-sync": "0.3.0", + "glob": "7.0.6", + "grunt-cli": "1.2.0", + "grunt-known-options": "1.1.0", + "grunt-legacy-log": "1.0.0", + "grunt-legacy-util": "1.0.0", + "iconv-lite": "0.4.18", + "js-yaml": "3.5.5", + "minimatch": "3.0.2", + "nopt": "3.0.6", + "path-is-absolute": "1.0.1", + "rimraf": "2.2.8" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.2", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "grunt-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "dev": true, + "requires": { + "findup-sync": "0.3.0", + "grunt-known-options": "1.1.0", + "nopt": "3.0.6", + "resolve": "1.1.7" + } + }, + "iconv-lite": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha1-I9hlaxaq5nQqwpcy6o8DNqR4nPI=", + "dev": true + }, + "js-yaml": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", + "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "2.7.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "grunt-contrib-clean": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.0.0.tgz", + "integrity": "sha1-ay7ZQRfix//jLuBFeMlv5GJam20=", + "dev": true, + "requires": { + "async": "1.5.2", + "rimraf": "2.6.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + } + } + }, + "grunt-contrib-copy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", + "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "file-sync-cmp": "0.1.1" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + } + } + }, + "grunt-contrib-watch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.0.0.tgz", + "integrity": "sha1-hKGnodar0m7VaEE0lscxM+mQAY8=", + "dev": true, + "requires": { + "async": "1.5.2", + "gaze": "1.1.0", + "lodash": "3.10.1", + "tiny-lr": "0.2.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "grunt-known-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.0.tgz", + "integrity": "sha1-pCdO6zL6dl2lp6OxcSYXzjsUQUk=", + "dev": true + }, + "grunt-legacy-log": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-1.0.0.tgz", + "integrity": "sha1-+4bxgJhHvAfcR4Q/ns1srLYt8tU=", + "dev": true, + "requires": { + "colors": "1.1.2", + "grunt-legacy-log-utils": "1.0.0", + "hooker": "0.2.3", + "lodash": "3.10.1", + "underscore.string": "3.2.3" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-1.0.0.tgz", + "integrity": "sha1-p7ji0Ps1taUPSvmG/BEnSevJbz0=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "lodash": "4.3.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "lodash": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", + "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.0.0.tgz", + "integrity": "sha1-OGqnjcbtUJhsKxiVcmWxtIq7m4Y=", + "dev": true, + "requires": { + "async": "1.5.2", + "exit": "0.1.2", + "getobject": "0.1.0", + "hooker": "0.2.3", + "lodash": "4.3.0", + "underscore.string": "3.2.3", + "which": "1.2.14" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "lodash": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.3.0.tgz", + "integrity": "sha1-79nEpuxT87BUEkKZFcPkgk5NJaQ=", + "dev": true + } + } + }, + "grunt-shell": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-1.3.0.tgz", + "integrity": "sha1-3lYGCpNN+OzuZAdLYcYwSQDXEVg=", + "dev": true, + "requires": { + "chalk": "1.1.0", + "npm-run-path": "1.0.0", + "object-assign": "4.1.1" + } + }, + "grunt-ts": { + "version": "6.0.0-beta.16", + "resolved": "https://registry.npmjs.org/grunt-ts/-/grunt-ts-6.0.0-beta.16.tgz", + "integrity": "sha1-wC9P+cgRAE7suTOJBaBds/hpIMM=", + "dev": true, + "requires": { + "chokidar": "1.7.0", + "csproj2ts": "0.0.8", + "detect-indent": "4.0.0", + "detect-newline": "2.1.0", + "es6-promise": "0.1.2", + "jsmin2": "1.2.1", + "lodash": "4.17.4", + "ncp": "0.5.1", + "rimraf": "2.2.6", + "semver": "5.3.0", + "strip-bom": "2.0.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "rimraf": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", + "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", + "dev": true + } + } + }, + "handlebars": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", + "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=" + }, + "http-errors": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "statuses": "1.3.1" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.1" + } + }, + "iconv-lite": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.11.tgz", + "integrity": "sha1-LstC/SlHRJIiCaLnxATayHk9it4=" + }, + "ignore": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", + "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.9.0.tgz", + "integrity": "sha1-c2bjijMeYZBJWKzlstpKml9jZ5g=", + "requires": { + "ansi-regex": "2.1.1", + "chalk": "1.1.0", + "cli-width": "1.1.1", + "figures": "1.7.0", + "lodash": "3.10.1", + "readline2": "0.1.1", + "run-async": "0.1.0", + "rx-lite": "2.5.2", + "strip-ansi": "3.0.1", + "through": "2.3.8" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, + "interpret": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", + "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ios-device-lib": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/ios-device-lib/-/ios-device-lib-0.4.9.tgz", + "integrity": "sha1-aSmddfsrTeNakIb/CbQMQFWv1fY=", + "requires": { + "bufferpack": "0.0.6", + "node-uuid": "1.4.7" + }, + "dependencies": { + "node-uuid": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", + "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=" + } + } + }, + "ios-mobileprovision-finder": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/ios-mobileprovision-finder/-/ios-mobileprovision-finder-1.0.10.tgz", + "integrity": "sha1-UaXn+TzUCwN/fI8+JwXjSI11VgE=", + "requires": { + "chalk": "1.1.3", + "plist": "2.1.0", + "yargs": "6.6.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "plist": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz", + "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=", + "requires": { + "base64-js": "1.2.0", + "xmlbuilder": "8.2.2", + "xmldom": "0.1.21" + } + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + } + } + }, + "ios-sim-portable": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/ios-sim-portable/-/ios-sim-portable-3.1.1.tgz", + "integrity": "sha1-AmL3x3N6ZnfyAI48rem2KMEIN6c=", + "requires": { + "bplist-parser": "https://github.com/telerik/node-bplist-parser/tarball/master", + "colors": "0.6.2", + "lodash": "3.2.0", + "osenv": "0.1.3", + "plist": "1.1.0", + "shelljs": "0.7.0", + "yargs": "4.7.1" + }, + "dependencies": { + "bplist-parser": { + "version": "https://github.com/telerik/node-bplist-parser/tarball/master", + "integrity": "sha1-X7BVlo1KqtkGi+pkMyFRiYKHtFY=", + "requires": { + "big-integer": "1.6.23" + } + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" + }, + "lodash": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.2.0.tgz", + "integrity": "sha1-S/UKMkP5rrC6xBpV09WZBnWkYvs=" + }, + "set-blocking": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-1.0.0.tgz", + "integrity": "sha1-zV5dk4BI3xrJLf6S4fFq3WVvXsU=" + }, + "shelljs": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.0.tgz", + "integrity": "sha1-P28uSWXOxWX2X/OGHWRPh5KBpXY=", + "requires": { + "glob": "7.1.2", + "interpret": "1.0.3", + "rechoir": "0.6.2" + } + }, + "yargs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.7.1.tgz", + "integrity": "sha1-5gQyZYozh/8mnAKOrN5KUS5Djf8=", + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "lodash.assign": "4.2.0", + "os-locale": "1.4.0", + "pkg-conf": "1.1.3", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1", + "set-blocking": "1.0.0", + "string-width": "1.0.2", + "window-size": "0.2.0", + "y18n": "3.2.1", + "yargs-parser": "2.4.1" + } + }, + "yargs-parser": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", + "requires": { + "camelcase": "3.0.0", + "lodash.assign": "4.2.0" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "1.8.0" + } + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-my-json-valid": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true, + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "4.0.1", + "xtend": "4.0.1" + }, + "dependencies": { + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + } + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-resolvable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", + "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", + "dev": true, + "requires": { + "tryit": "1.0.3" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.0", + "glob": "5.0.15", + "handlebars": "4.0.10", + "js-yaml": "3.9.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.2.14", + "wordwrap": "1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.2", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.0.tgz", + "integrity": "sha1-T/u/JcKsljuCmdx02n43QN4cGM4=", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsmin2": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jsmin2/-/jsmin2-1.2.1.tgz", + "integrity": "sha1-iPvi+/dfCpH2YCD9mBzWk/S/5X4=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "livereload-js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", + "integrity": "sha1-bIclfmSKtHW8JOoldFftzB+NC8I=", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "lockfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.3.tgz", + "integrity": "sha1-Jjj8OaAzHpysGgS3F5mTHJxQ33k=" + }, + "lodash": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz", + "integrity": "sha1-g+SxCRP0hJbU0W/sSlYK8u50S2g=" + }, + "lodash-node": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash-node/-/lodash-node-2.4.1.tgz", + "integrity": "sha1-6oL3sQDHM9GkKvdoAeUGEF4qgOw=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._basecreate": "3.0.3", + "lodash._isiterateecall": "3.0.9" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" + }, + "log4js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-1.0.1.tgz", + "integrity": "sha1-+vZMEFa2NSpfu/CpO2Gpl5qEIsw=", + "requires": { + "debug": "2.6.8", + "semver": "5.3.0", + "streamroller": "0.2.2" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "requires": { + "es5-ext": "0.10.24" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "marked": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" + }, + "marked-terminal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-2.0.0.tgz", + "integrity": "sha1-Xq9Wi+ZvaGVBr6UqVYKAMQox3i0=", + "requires": { + "cardinal": "1.0.0", + "chalk": "1.1.3", + "cli-table": "https://github.com/telerik/cli-table/tarball/v0.3.1.2", + "lodash.assign": "4.2.0", + "node-emoji": "1.7.0" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memoizee": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.3.10.tgz", + "integrity": "sha1-TsoNiu057J0Bf0xcLy9kMvQuXI8=", + "requires": { + "d": "0.1.1", + "es5-ext": "0.10.24", + "es6-weak-map": "0.1.4", + "event-emitter": "0.3.5", + "lru-queue": "0.1.0", + "next-tick": "0.2.2", + "timers-ext": "0.1.2" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.3" + } + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.2.tgz", + "integrity": "sha1-DzmKcwDqRB6cNIyD2Yq4ydv5xAo=", + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.1.2.tgz", + "integrity": "sha1-Ufk7Qyv34bF1/8Iog8zQvjLbprU=", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.5", + "glob": "7.0.5", + "growl": "1.9.2", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "glob": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.2", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "moment": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.10.6.tgz", + "integrity": "sha1-bLIZZ8ecunsMpeZmRPFzZis++nc=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=" + }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", + "optional": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "ncp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", + "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=", + "dev": true + }, + "next-tick": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", + "integrity": "sha1-ddpKkn7liH45BliABltzNkE7MQ0=" + }, + "node-emoji": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.7.0.tgz", + "integrity": "sha1-pABJCqxAm2FtE5QVMiAPEorwN/k=", + "requires": { + "lodash.toarray": "4.4.0", + "string.prototype.codepointat": "0.2.0" + } + }, + "node-uuid": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.3.3.tgz", + "integrity": "sha1-09tNe1aBDZ5AMjQnZigq8HORcps=" + }, + "node-xml": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-xml/-/node-xml-1.0.2.tgz", + "integrity": "sha1-zqWQgrEAxwM3ERylPh84+CnjvMs=" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.1.0" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.3.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.0.2" + } + }, + "npm-run-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz", + "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=", + "dev": true, + "requires": { + "path-key": "1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "open": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", + "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.3.tgz", + "integrity": "sha1-g88FxtZFj8TVrGNi6jJdkvJ1Qhc=", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "1.3.1" + } + }, + "parseurl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz", + "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pbxproj-dom": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pbxproj-dom/-/pbxproj-dom-1.0.11.tgz", + "integrity": "sha1-MTbG1tphwkOW8Byvr5xBJGaPTAU=" + }, + "pegjs": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.6.2.tgz", + "integrity": "sha1-dGUfioAORE22iOTuro7bZWN6F6U=" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-1.1.3.tgz", + "integrity": "sha1-N45W1v0T6Iv7b0ol33qD+qvduls=", + "requires": { + "find-up": "1.1.2", + "load-json-file": "1.1.0", + "object-assign": "4.1.1", + "symbol": "0.2.3" + } + }, + "plist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-1.1.0.tgz", + "integrity": "sha1-/2cIWQyXzEOOe8Rd5SUb1yXz+J0=", + "requires": { + "base64-js": "0.0.6", + "util-deprecate": "1.0.0", + "xmlbuilder": "2.2.1", + "xmldom": "0.1.21" + }, + "dependencies": { + "base64-js": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.6.tgz", + "integrity": "sha1-e4WfefC7vVWGe6Z6f6s5fiSiCUc=" + }, + "util-deprecate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.0.tgz", + "integrity": "sha1-MAevASwUDq4m3gVXbsInhcrDq/I=" + }, + "xmlbuilder": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.2.1.tgz", + "integrity": "sha1-kyZDDxMNh0NdTECGZDqikm4QWjI=", + "requires": { + "lodash-node": "2.4.1" + } + } + } + }, + "plist-merge-patch": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/plist-merge-patch/-/plist-merge-patch-0.1.1.tgz", + "integrity": "sha1-1xceGwXAh+W6BpN1QK385QQot/w=", + "requires": { + "lodash": "4.17.4", + "plist": "2.1.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "plist": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz", + "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=", + "requires": { + "base64-js": "1.2.0", + "xmlbuilder": "8.2.2", + "xmldom": "0.1.21" + } + } + } + }, + "plistlib": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/plistlib/-/plistlib-0.2.1.tgz", + "integrity": "sha1-RukssEmKCjrtO+5bXE2IKQVNqGw=", + "requires": { + "async": "0.2.10", + "moment": "2.4.0", + "node-xml": "1.0.2", + "underscore": "1.5.2", + "xml": "0.0.12" + }, + "dependencies": { + "moment": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.4.0.tgz", + "integrity": "sha1-Bt2N+7/bU6A1EAgKx4gWPJSQ510=" + } + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "progress-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.1.1.tgz", + "integrity": "sha1-nsvxh5MsSUHVUCGRkNdN7ArEX1Q=", + "requires": { + "single-line-log": "0.3.1", + "speedometer": "0.1.4", + "through2": "0.2.3" + } + }, + "properties-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.2.3.tgz", + "integrity": "sha1-91kSVfcHq7/yJ8e1a2N9uwNzoQ8=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qr-image": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/qr-image/-/qr-image-3.2.0.tgz", + "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "raw-body": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", + "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", + "dev": true, + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.13", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.2", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + } + }, + "readline2": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-0.1.1.tgz", + "integrity": "sha1-mUQ7pug7gw7zBRv9fcJBqCco1Wg=", + "requires": { + "mute-stream": "0.0.4", + "strip-ansi": "2.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", + "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=" + }, + "mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha1-qSGZYKbV1dBGWXruUSUsZlX3F34=" + }, + "strip-ansi": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", + "integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=", + "requires": { + "ansi-regex": "1.1.1" + } + } + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "1.3.3" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "redeyed": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", + "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "requires": { + "esprima": "3.0.0" + }, + "dependencies": { + "esprima": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", + "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=" + } + } + }, + "regex-cache": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "requires": { + "is-equal-shallow": "0.1.3", + "is-primitive": "2.0.0" + } + }, + "remove-trailing-separator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", + "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "resolve": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "1.1.1", + "onetime": "1.1.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "requires": { + "once": "1.4.0" + } + }, + "rx-lite": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-2.5.2.tgz", + "integrity": "sha1-X+9C1Nbna6tRmdIXEyfbcJ5Y5jQ=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "shelljs": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.6.tgz", + "integrity": "sha1-N5zM+1a5HIYB5HkzVutTgpJN6a0=", + "requires": { + "glob": "7.1.2", + "interpret": "1.0.3", + "rechoir": "0.6.2" + } + }, + "should": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/should/-/should-7.0.2.tgz", + "integrity": "sha1-HfJOAqlxzx1ZWu0mfi2D8kw3CYM=", + "dev": true, + "requires": { + "should-equal": "0.5.0", + "should-format": "0.3.0", + "should-type": "0.2.0" + } + }, + "should-equal": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-0.5.0.tgz", + "integrity": "sha1-x5fxNfMGf+tp6+zbMGscP+IbPm8=", + "dev": true, + "requires": { + "should-type": "0.2.0" + } + }, + "should-format": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-0.3.0.tgz", + "integrity": "sha1-QgB+wKochupEkUzJER8bnyfTzqw=", + "dev": true, + "requires": { + "should-type": "0.2.0" + } + }, + "should-type": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-0.2.0.tgz", + "integrity": "sha1-ZwfvlVKdmJ3MCY/gdTqx+RNrt/Y=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-plist": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-0.2.1.tgz", + "integrity": "sha1-cXZts1IyaSjPOoByQrp2IyJjZyM=", + "requires": { + "bplist-creator": "0.0.7", + "bplist-parser": "0.1.1", + "plist": "2.0.1" + }, + "dependencies": { + "base64-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz", + "integrity": "sha1-1kAMrBxMZgl22Q0HoENR2JOV9eg=" + }, + "bplist-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz", + "integrity": "sha1-1g1dzCDLptx+HymbNdPh+V2vuuY=", + "requires": { + "big-integer": "1.6.23" + } + }, + "plist": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-2.0.1.tgz", + "integrity": "sha1-CjLKlIGxw2TpLhjcVch23p0B2os=", + "requires": { + "base64-js": "1.1.2", + "xmlbuilder": "8.2.2", + "xmldom": "0.1.21" + } + } + } + }, + "single-line-log": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.3.1.tgz", + "integrity": "sha1-p61lB/IYzl3+FsS/LWWSRkGeegY=" + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + }, + "source-map-support": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true, + "requires": { + "source-map": "0.5.6" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "speedometer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", + "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + }, + "stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" + }, + "streamroller": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.2.2.tgz", + "integrity": "sha1-oTQg4EFp5XPbBo9ZIO4j2IGr/jM=", + "requires": { + "date-format": "0.0.0", + "debug": "0.7.4", + "readable-stream": "1.1.14" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string.prototype.codepointat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz", + "integrity": "sha1-aybpvTr8qnvjtCabUm3huCAArHg=" + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "symbol": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/symbol/-/symbol-0.2.3.tgz", + "integrity": "sha1-O5hzuKkB5Hxu/iFSajrDcu8ou8c=" + }, + "table": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "4.11.8", + "ajv-keywords": "1.5.1", + "chalk": "1.1.3", + "lodash": "4.13.1", + "slice-ansi": "0.0.4", + "string-width": "2.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.0.tgz", + "integrity": "sha1-AwZkVh/BRslCPsfZeP4kV0N/5tA=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + } + } + }, + "tabtab": { + "version": "https://github.com/Icenium/node-tabtab/tarball/master", + "integrity": "sha1-xhMOobFKxMBo+ayFSilgv9cESkk=" + }, + "temp": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", + "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", + "requires": { + "os-tmpdir": "1.0.2", + "rimraf": "2.2.8" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", + "requires": { + "readable-stream": "1.1.14", + "xtend": "2.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "timers-ext": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.2.tgz", + "integrity": "sha1-YcxHp2wavTGV8UUn+XjViulMUgQ=", + "requires": { + "es5-ext": "0.10.24", + "next-tick": "1.0.0" + }, + "dependencies": { + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + } + } + }, + "tiny-lr": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.2.1.tgz", + "integrity": "sha1-s/26gC5dVqM8L28QeUsy5Hescp0=", + "dev": true, + "requires": { + "body-parser": "1.14.2", + "debug": "2.2.0", + "faye-websocket": "0.10.0", + "livereload-js": "2.2.2", + "parseurl": "1.3.1", + "qs": "5.1.0" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "qs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz", + "integrity": "sha1-TZMuXH6kEcynajEtOaYGIA/VDNk=", + "dev": true + } + } + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "tryit": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", + "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", + "dev": true + }, + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "dev": true + }, + "tslint": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.4.3.tgz", + "integrity": "sha1-dhyEArgONHt3M6BDkKdXslNYBGc=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "colors": "1.1.2", + "commander": "2.9.0", + "diff": "3.3.0", + "glob": "7.1.2", + "minimatch": "3.0.4", + "resolve": "1.3.3", + "semver": "5.3.0", + "tslib": "1.7.1", + "tsutils": "2.6.1" + }, + "dependencies": { + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha1-BWaVFQ16qTI3yn43isOxaCt5Y7k=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + } + } + }, + "tsutils": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.6.1.tgz", + "integrity": "sha1-mOzwCVlPTkr4hAV75M9BpFC8djc=", + "dev": true, + "requires": { + "tslib": "1.7.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.15" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.1.tgz", + "integrity": "sha1-w8yxbdqgsjFN4DHn5v7onlujRrw=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "ultron": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", + "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" + }, + "underscore": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", + "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=" + }, + "underscore.string": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.2.3.tgz", + "integrity": "sha1-gGmSYzZl1eX8tNsfs6hi62jp5to=", + "dev": true + }, + "universal-analytics": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.15.tgz", + "integrity": "sha512-9Dt6WBWsHsmv74G+N/rmEgi6KFZxVvQXkVhr0disegeUryybQAUQwMD1l5EtqaOu+hSOGbhL/hPPQYisZIqPRw==", + "requires": { + "async": "1.2.1", + "request": "2.81.0", + "underscore": "1.5.2", + "uuid": "3.0.1" + }, + "dependencies": { + "async": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz", + "integrity": "sha1-pIFqF81f9RbfosdpikUzabl5DeA=" + } + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "requires": { + "extsprintf": "1.0.2" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "1.0.3" + } + }, + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "dev": true, + "requires": { + "websocket-extensions": "0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", + "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=", + "dev": true + }, + "which": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "window-size": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" + }, + "winreg": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-0.0.17.tgz", + "integrity": "sha1-ysqg4a2hdVXMGgDs/JQA1/ODrQE=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "ws": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-2.2.0.tgz", + "integrity": "sha1-MhinsevRWgnFa7EqPpQ6lg63veU=", + "requires": { + "ultron": "1.1.0" + } + }, + "xcode": { + "version": "https://github.com/NativeScript/node-xcode/archive/1.4.0.tar.gz", + "integrity": "sha1-pws+vYIXCzhk70x9hYy3eXmOHjg=", + "requires": { + "node-uuid": "1.3.3", + "pegjs": "0.6.2" + } + }, + "xml": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/xml/-/xml-0.0.12.tgz", + "integrity": "sha1-8Is0cQmRK+AChXhfRvFa2OUKX2c=" + }, + "xml2js": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "dev": true, + "requires": { + "sax": "1.2.4", + "xmlbuilder": "4.2.1" + }, + "dependencies": { + "xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "dev": true, + "requires": { + "lodash": "4.13.1" + } + } + } + }, + "xmlbuilder": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=" + }, + "xmldom": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.21.tgz", + "integrity": "sha1-op4SENqx8QwlZltegBKbqo1pqXs=" + }, + "xmlhttprequest": { + "version": "https://github.com/telerik/node-XMLHttpRequest/tarball/master", + "integrity": "sha1-gyu8L8J4DhCCCmdOlAQRCpQ0M6c=" + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "0.4.0" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.0.0.tgz", + "integrity": "sha1-kAR5306L9qsOhyFvXtKydguWg0U=", + "requires": { + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "window-size": "0.2.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "requires": { + "camelcase": "3.0.0" + } + }, + "zipstream": { + "version": "https://github.com/Icenium/node-zipstream/tarball/master", + "integrity": "sha1-nYck2cc4rn9wPjWMHrJniS1yh3o=" + } + } +} diff --git a/package.json b/package.json index 559d82b4d3..ec9320ba40 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", "preferGlobal": true, - "version": "3.0.1", + "version": "3.3.0", "author": "Telerik ", "description": "Command-line interface for building NativeScript projects", "bin": { @@ -15,7 +15,9 @@ "preuninstall": "node preuninstall.js", "mocha": "node test-scripts/mocha.js", "tsc": "tsc", - "test-watch": "node ./dev/tsc-to-mocha-watch.js" + "tslint": "tslint -p tsconfig.json --type-check", + "test-watch": "node ./dev/tsc-to-mocha-watch.js", + "tslint-fix": "tslint -p tsconfig.json --type-check --fix" }, "repository": { "type": "git", @@ -31,7 +33,7 @@ "bufferpack": "0.0.6", "byline": "4.2.1", "chalk": "1.1.0", - "chokidar": "^1.6.1", + "chokidar": "1.7.0", "cli-table": "https://github.com/telerik/cli-table/tarball/v0.3.1.2", "clui": "0.3.1", "colors": "1.1.2", @@ -42,10 +44,10 @@ "glob": "^7.0.3", "iconv-lite": "0.4.11", "inquirer": "0.9.0", - "ios-device-lib": "0.4.1", - "ios-mobileprovision-finder": "1.0.9", - "ios-sim-portable": "~3.0.0", - "lockfile": "1.0.1", + "ios-device-lib": "0.4.9", + "ios-mobileprovision-finder": "1.0.10", + "ios-sim-portable": "3.1.1", + "lockfile": "1.0.3", "lodash": "4.13.1", "log4js": "1.0.1", "marked": "0.3.6", @@ -56,19 +58,21 @@ "mute-stream": "0.0.5", "open": "0.0.5", "osenv": "0.1.3", - "pbxproj-dom": "1.0.9", + "pbxproj-dom": "1.0.11", "plist": "1.1.0", - "plist-merge-patch": "0.0.9", + "plist-merge-patch": "0.1.1", "plistlib": "0.2.1", "progress-stream": "1.1.1", "properties-parser": "0.2.3", - "qrcode-generator": "1.0.0", + "qr-image": "3.2.0", "request": "2.81.0", "semver": "5.3.0", "shelljs": "0.7.6", + "simple-plist": "0.2.1", "source-map": "0.5.6", "tabtab": "https://github.com/Icenium/node-tabtab/tarball/master", "temp": "0.8.3", + "universal-analytics": "0.4.15", "uuid": "3.0.1", "winreg": "0.0.17", "ws": "2.2.0", @@ -80,31 +84,33 @@ }, "analyze": true, "devDependencies": { - "@types/chai": "3.4.34", - "@types/chai-as-promised": "0.0.29", - "@types/lodash": "4.14.50", + "@types/chai": "4.0.1", + "@types/chai-as-promised": "0.0.31", + "@types/chokidar": "1.6.0", + "@types/lockfile": "1.0.0", "@types/node": "6.0.61", - "@types/request": "0.0.42", + "@types/qr-image": "3.2.0", + "@types/request": "0.0.45", "@types/semver": "^5.3.31", "@types/source-map": "0.5.0", - "chai": "3.5.0", - "chai-as-promised": "6.0.0", + "@types/universal-analytics": "0.4.1", + "chai": "4.0.2", + "chai-as-promised": "7.0.0", "grunt": "1.0.1", "grunt-contrib-clean": "1.0.0", "grunt-contrib-copy": "1.0.0", "grunt-contrib-watch": "1.0.0", "grunt-shell": "1.3.0", - "grunt-ts": "6.0.0-beta.6", - "grunt-tslint": "4.0.0", + "grunt-ts": "6.0.0-beta.16", "istanbul": "0.4.5", "mocha": "3.1.2", - "mocha-typescript": "1.0.23", "should": "7.0.2", - "tslint": "4.3.1", - "typescript": "2.1.4" + "source-map-support": "^0.4.14", + "tslint": "5.4.3", + "typescript": "2.4.1" }, "license": "Apache-2.0", "engines": { - "node": ">=6.0.0 <8.0.0" + "node": ">=6.0.0 <9.0.0" } } diff --git a/postinstall.js b/postinstall.js index a5bb15e6a1..8c3a76fab2 100644 --- a/postinstall.js +++ b/postinstall.js @@ -1,8 +1,9 @@ "use strict"; var child_process = require("child_process"); -var commandArgs = ["bin/tns", "post-install-cli"]; var path = require("path"); +var constants = require(path.join(__dirname, "lib", "constants")); +var commandArgs = [path.join(__dirname, "bin", "tns"), constants.POST_INSTALL_COMMAND_NAME]; var nodeArgs = require(path.join(__dirname, "lib", "common", "scripts", "node-args")).getNodeArgs(); -child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), {stdio: "inherit"}); +child_process.spawn(process.argv[0], nodeArgs.concat(commandArgs), { stdio: "inherit" }); diff --git a/setup/native-script.ps1 b/setup/native-script.ps1 index cb5f028724..d90687ee39 100644 --- a/setup/native-script.ps1 +++ b/setup/native-script.ps1 @@ -5,7 +5,9 @@ # @powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://www.nativescript.org/setup/win'))" # To run it inside a WINDOWS POWERSHELL console against the production branch (only one supported with self-elevation) use # start-process -FilePath PowerShell.exe -Verb Runas -Wait -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command iex ((new-object net.webclient).DownloadString('https://www.nativescript.org/setup/win'))" - +param( + [switch] $SilentMode +) # Check if latest .NET framework installed is at least 4 $dotNetVersions = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version,Release -EA 0 | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select Version $latestDotNetVersion = $dotNetVersions.GetEnumerator() | Sort-Object Version | Select-Object -Last 1 @@ -27,7 +29,7 @@ if (-not $isElevated) { } # Help with installing other dependencies -$script:answer = "" +$script:answer = if ($SilentMode) {"a"} else {""} function Install($programName, $message, $script, $shouldExit) { if ($script:answer -ne "a") { Write-Host -ForegroundColor Green "Allow the script to install $($programName)?" @@ -55,8 +57,8 @@ function Pause { } # Actually installing all other dependencies -# Install Chocolately -Install "Chocolately(It's mandatory for the rest of the script)" "Installing Chocolately" "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" +# Install Chocolatey +Install "Chocolatey (It's mandatory for the rest of the script)" "Installing Chocolatey" "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" if ((Get-Command "cinst" -ErrorAction SilentlyContinue) -eq $null) { Write-Host -ForegroundColor Red "Chocolatey is not installed or not configured properly. Download it from https://chocolatey.org/, install, set it up and run this script again." @@ -64,7 +66,7 @@ if ((Get-Command "cinst" -ErrorAction SilentlyContinue) -eq $null) { exit 1 } -# Install dependenciess with Chocolately +# Install dependenciess with Chocolatey Install "Google Chrome" "Installing Google Chrome (required to debug NativeScript apps)" "cinst googlechrome --force --yes" @@ -81,7 +83,7 @@ if (!$env:ANDROID_HOME) { $androidHome = Resolve-Path $androidHomeJoinedPath | Select-Object -ExpandProperty Path } else { - $androidHome = "$env:localappdata\Android\android-sdk" + $androidHome = "${Env:SystemDrive}\Android\android-sdk" } $env:ANDROID_HOME = $androidHome; @@ -97,29 +99,25 @@ if (!$env:JAVA_HOME) { # setup android sdk # following commands are separated in case of having to answer to license agreements -# the android tool will introduce a --accept-license option in subsequent releases -$androidExecutable = [io.path]::combine($env:ANDROID_HOME, "tools", "android") -echo y | cmd /c "$androidExecutable" update sdk --filter "platform-tools" --all --no-ui -echo y | cmd /c "$androidExecutable" update sdk --filter "tools" --all --no-ui -echo y | cmd /c "$androidExecutable" update sdk --filter "android-23" --all --no-ui -echo y | cmd /c "$androidExecutable" update sdk --filter "build-tools-25.0.2" --all --no-ui -echo y | cmd /c "$androidExecutable" update sdk --filter "build-tools-23.0.3" --all --no-ui -echo y | cmd /c "$androidExecutable" update sdk --filter "extra-android-m2repository" --all --no-ui +$androidExecutable = [io.path]::combine($env:ANDROID_HOME, "tools", "bin", "sdkmanager") +echo y | cmd /c "$androidExecutable" "platform-tools" +echo y | cmd /c "$androidExecutable" "tools" +echo y | cmd /c "$androidExecutable" "build-tools;25.0.2" +echo y | cmd /c "$androidExecutable" "platforms;android-25" +echo y | cmd /c "$androidExecutable" "extras;android;m2repository" +echo y | cmd /c "$androidExecutable" "extras;google;m2repository" if ((Read-Host "Do you want to install Android emulator?") -eq 'y') { if ((Read-Host "Do you want to install HAXM (Hardware accelerated Android emulator)?") -eq 'y') { - echo y | cmd /c "$androidExecutable" update sdk --filter extra-intel-Hardware_Accelerated_Execution_Manager --all --no-ui - + echo y | cmd /c "$androidExecutable" "extras;intel;Hardware_Accelerated_Execution_Manager" $haxmSilentInstaller = [io.path]::combine($env:ANDROID_HOME, "extras", "intel", "Hardware_Accelerated_Execution_Manager", "silent_install.bat") cmd /c "$haxmSilentInstaller" - - echo y | cmd /c "$androidExecutable" update sdk --filter sys-img-x86-android-23 --all --no-ui - echo no | cmd /c "$androidExecutable" create avd -n Emulator-Api23-Default -t android-23 --abi default/x86 -c 12M -f - } else { - echo y | cmd /c "$androidExecutable" update sdk --filter sys-img-armeabi-v7a-android-23 --all --no-ui - echo no | cmd /c "$androidExecutable" create avd -n Emulator-Api23-Default -t android-23 --abi default/armeabi-v7a -c 12M -f + echo y | cmd /c "$androidExecutable" "system-images;android-25;google_apis;x86" + } + else { + echo y | cmd /c "$androidExecutable" "system-images;android-25;google_apis;armeabi-v7a" } } Write-Host -ForegroundColor Green "This script has modified your environment. You need to log off and log back on for the changes to take effect." -Pause +Pause \ No newline at end of file diff --git a/setup/native-script.rb b/setup/native-script.rb index b70aa25d31..fa6752ee68 100755 --- a/setup/native-script.rb +++ b/setup/native-script.rb @@ -92,13 +92,13 @@ def install_environment_variable(name, value) exit end +install("Google Chrome", "Installing Google Chrome, used for debugging", "brew cask install google-chrome", false, false); install("Java SE Development Kit", "Installing the Java SE Development Kit... This might take some time, please, be patient. (You will be prompted for your password)", 'brew cask install java', false, false) install("Android SDK", "Installing Android SDK", 'brew tap caskroom/cask; brew cask install android-sdk', false) unless ENV["ANDROID_HOME"] require 'pathname' - # if android-sdk was installed through brew, there should be a symlink in /usr/local/opt/android-sdk pointing to the actual sdk - android_home = "/usr/local/opt/android-sdk" + android_home = "/usr/local/share/android-sdk" unless Pathname.new(android_home).exist? require 'mkmf' # if there's no such symlink then try to find the `android-sdk` directory through the `android` executable @@ -129,31 +129,25 @@ def install_environment_variable(name, value) # the android tool will introduce a --accept-license option in subsequent releases error_msg = "There seem to be some problems with the Android configuration" -android_executable = File.join(ENV["ANDROID_HOME"], "tools", "bin", "sdkmanager") -execute("echo y | #{android_executable} \"platform-tools\"", error_msg) -execute("echo y | #{android_executable} \"tools\"", error_msg) -execute("echo y | #{android_executable} \"build-tools;25.0.2\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-25\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-24\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-23\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-22\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-21\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-19\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-18\"", error_msg) -execute("echo y | #{android_executable} \"platforms;android-17\"", error_msg) +sdk_manager = File.join(ENV["ANDROID_HOME"], "tools", "bin", "sdkmanager") +execute("echo y | #{sdk_manager} \"platform-tools\"", error_msg) +execute("echo y | #{sdk_manager} \"tools\"", error_msg) +execute("echo y | #{sdk_manager} \"build-tools;25.0.2\"", error_msg) +execute("echo y | #{sdk_manager} \"platforms;android-25\"", error_msg) +execute("echo y | #{sdk_manager} \"extras;android;m2repository\"", error_msg) +execute("echo y | #{sdk_manager} \"extras;google;m2repository\"", error_msg) puts "Do you want to install Android emulator? (y/n)" if gets.chomp.downcase == "y" puts "Do you want to install HAXM (Hardware accelerated Android emulator)? (y/n)" if gets.chomp.downcase == "y" - execute("echo y | #{android_executable} \"extras;intel;Hardware_Accelerated_Execution_Manager\"", error_msg) - + execute("echo y | #{sdk_manager} \"extras;intel;Hardware_Accelerated_Execution_Manager\"", error_msg) haxm_silent_installer = File.join(ENV["ANDROID_HOME"], "extras", "intel", "Hardware_Accelerated_Execution_Manager", "silent_install.sh") execute("sudo #{haxm_silent_installer}", "There seem to be some problems with the Android configuration") - else - end - execute("echo y | #{android_executable} \"system-images;android-25;google_apis;x86\"", error_msg) - execute("echo y | #{android_executable} \"system-images;android-24;default;x86\"", error_msg) + execute("echo y | #{sdk_manager} \"system-images;android-25;default;x86\"", error_msg) + else + execute("echo y | #{sdk_manager} \"system-images;android-25;google_apis;armeabi-v7a\"", error_msg) + end end puts "The ANDROID_HOME and JAVA_HOME environment variables have been added to your .bash_profile/.zprofile" diff --git a/test/android-project-properties-manager.ts b/test/android-project-properties-manager.ts index 8b3a316e76..a92ab97f40 100644 --- a/test/android-project-properties-manager.ts +++ b/test/android-project-properties-manager.ts @@ -14,7 +14,7 @@ temp.track(); import { assert } from "chai"; function createTestInjector(): IInjector { - let testInjector = new yok.Yok(); + const testInjector = new yok.Yok(); testInjector.register("propertiesParser", ProjectPropertiesParserLib.PropertiesParser); testInjector.register("fs", FsLib.FileSystem); testInjector.register("hostInfo", HostInfoLib.HostInfo); @@ -29,113 +29,113 @@ function createTestInjector(): IInjector { describe("Android project properties parser tests", () => { it("adds project reference", async () => { - let testInjector = createTestInjector(); - let fs = testInjector.resolve("fs"); + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); - let projectPropertiesFileContent = 'target=android-21'; - let tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); + const projectPropertiesFileContent = 'target=android-21'; + const tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); fs.writeFile(path.join(tempFolder, "project.properties"), projectPropertiesFileContent); - let projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( + const projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( ProjectPropertiesManagerLib.AndroidProjectPropertiesManager, { directoryPath: tempFolder }); await projectPropertiesManager.addProjectReference("testValue"); - let expectedContent = 'target=android-21' + '\n' + + const expectedContent = 'target=android-21' + '\n' + 'android.library.reference.1=testValue'; - let actualContent = fs.readText(path.join(tempFolder, "project.properties")); + const actualContent = fs.readText(path.join(tempFolder, "project.properties")); assert.equal(expectedContent, actualContent); assert.equal(1, _.keys(await projectPropertiesManager.getProjectReferences()).length); }); it("adds project reference if another referencence already exists in project.properties file", async () => { - let testInjector = createTestInjector(); - let fs = testInjector.resolve("fs"); + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); - let projectPropertiesFileContent = 'target=android-21' + '\n' + + const projectPropertiesFileContent = 'target=android-21' + '\n' + 'android.library.reference.1=someValue'; - let tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); + const tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); fs.writeFile(path.join(tempFolder, "project.properties"), projectPropertiesFileContent); - let projectPropertiesManager = testInjector.resolve( + const projectPropertiesManager = testInjector.resolve( ProjectPropertiesManagerLib.AndroidProjectPropertiesManager, { directoryPath: tempFolder }); await projectPropertiesManager.addProjectReference("testValue"); - let expectedContent = ['target=android-21', + const expectedContent = ['target=android-21', 'android.library.reference.1=someValue', 'android.library.reference.2=testValue'].join('\n'); - let actualContent = fs.readText(path.join(tempFolder, "project.properties")); + const actualContent = fs.readText(path.join(tempFolder, "project.properties")); assert.equal(expectedContent, actualContent); assert.equal(2, _.keys(await projectPropertiesManager.getProjectReferences()).length); }); it("adds project reference if more than one references exist in project.properties file", async () => { - let testInjector = createTestInjector(); - let fs = testInjector.resolve("fs"); + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); - let projectPropertiesFileContent = ['target=android-21', + const projectPropertiesFileContent = ['target=android-21', 'android.library.reference.1=value1', 'android.library.reference.2=value2', 'android.library.reference.3=value3', 'android.library.reference.4=value4', 'android.library.reference.5=value5'].join('\n'); - let tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); + const tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); fs.writeFile(path.join(tempFolder, "project.properties"), projectPropertiesFileContent); - let projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( + const projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( ProjectPropertiesManagerLib.AndroidProjectPropertiesManager, { directoryPath: tempFolder }); await projectPropertiesManager.addProjectReference("testValue"); - let expectedContent = projectPropertiesFileContent + '\n' + + const expectedContent = projectPropertiesFileContent + '\n' + 'android.library.reference.6=testValue'; - let actualContent = fs.readText(path.join(tempFolder, "project.properties")); + const actualContent = fs.readText(path.join(tempFolder, "project.properties")); assert.equal(expectedContent, actualContent); assert.equal(6, _.keys(await projectPropertiesManager.getProjectReferences()).length); }); it("removes project reference if only one reference exists", async () => { - let testInjector = createTestInjector(); - let fs = testInjector.resolve("fs"); + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); - let projectPropertiesFileContent = 'android.library.reference.1=value1' + '\n' + + const projectPropertiesFileContent = 'android.library.reference.1=value1' + '\n' + 'target=android-21'; - let tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); + const tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); fs.writeFile(path.join(tempFolder, "project.properties"), projectPropertiesFileContent); - let projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( + const projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( ProjectPropertiesManagerLib.AndroidProjectPropertiesManager, { directoryPath: tempFolder }); await projectPropertiesManager.removeProjectReference("value1"); - let expectedContent = 'target=android-21'; - let actualContent = fs.readText(path.join(tempFolder, "project.properties")); + const expectedContent = 'target=android-21'; + const actualContent = fs.readText(path.join(tempFolder, "project.properties")); assert.equal(expectedContent, actualContent); assert.equal(0, _.keys(await projectPropertiesManager.getProjectReferences()).length); }); it("removes project reference when another references exist before and after the specified reference", async () => { - let testInjector = createTestInjector(); - let fs = testInjector.resolve("fs"); + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); - let projectPropertiesFileContent = ['target=android-17', + const projectPropertiesFileContent = ['target=android-17', 'android.library.reference.1=value1', 'android.library.reference.2=value2', 'android.library.reference.3=value3', 'android.library.reference.4=value4', 'android.library.reference.5=value5'].join('\n'); - let tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); + const tempFolder = temp.mkdirSync("AndroidProjectPropertiesManager"); fs.writeFile(path.join(tempFolder, "project.properties"), projectPropertiesFileContent); - let projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( + const projectPropertiesManager: IAndroidProjectPropertiesManager = testInjector.resolve( ProjectPropertiesManagerLib.AndroidProjectPropertiesManager, { directoryPath: tempFolder }); await projectPropertiesManager.removeProjectReference("value3"); - let expectedContent = ['target=android-17', + const expectedContent = ['target=android-17', 'android.library.reference.1=value1', 'android.library.reference.2=value2', 'android.library.reference.3=value4', 'android.library.reference.4=value5'].join('\n') + '\n'; - let actualContent = fs.readText(path.join(tempFolder, "project.properties")); + const actualContent = fs.readText(path.join(tempFolder, "project.properties")); assert.equal(expectedContent, actualContent); assert.equal(4, _.keys(await projectPropertiesManager.getProjectReferences()).length); diff --git a/test/base-service-test.ts b/test/base-service-test.ts new file mode 100644 index 0000000000..5cff01b74e --- /dev/null +++ b/test/base-service-test.ts @@ -0,0 +1,16 @@ +import * as yok from "../lib/common/yok"; + +export abstract class BaseServiceTest { + protected injector: IInjector; + constructor() { + this.injector = new yok.Yok(); + + this.initInjector(); + } + + abstract initInjector(): void; + + resolve(name: string, ctorArguments?: IDictionary): any { + return this.injector.resolve(name); + } +} diff --git a/test/cocoapods-service.ts b/test/cocoapods-service.ts index f8e11c5958..f63770aeba 100644 --- a/test/cocoapods-service.ts +++ b/test/cocoapods-service.ts @@ -10,7 +10,7 @@ interface IMergePodfileHooksTestCase { } function createTestInjector(): IInjector { - let testInjector: IInjector = new yok.Yok(); + const testInjector: IInjector = new yok.Yok(); testInjector.register("fs", {}); testInjector.register("cocoapodsService", CocoaPodsService); @@ -31,8 +31,8 @@ describe("Cocoapods service", () => { let cocoapodsService: ICocoaPodsService; let newPodfileContent: string; - let mockFileSystem = (injector: IInjector, podfileContent: string): void => { - let fs: IFileSystem = injector.resolve("fs"); + const mockFileSystem = (injector: IInjector, podfileContent: string): void => { + const fs: IFileSystem = injector.resolve("fs"); fs.exists = () => true; fs.readText = () => podfileContent; @@ -41,7 +41,7 @@ describe("Cocoapods service", () => { }; }; - let testCaces: IMergePodfileHooksTestCase[] = [ + const testCaces: IMergePodfileHooksTestCase[] = [ { input: ` target 'MyApp' do diff --git a/test/commands/post-install.ts b/test/commands/post-install.ts new file mode 100644 index 0000000000..5cadda5c0b --- /dev/null +++ b/test/commands/post-install.ts @@ -0,0 +1,59 @@ +import { Yok } from "../../lib/common/yok"; +import { assert } from "chai"; +import { PostInstallCliCommand } from "../../lib/commands/post-install"; + +const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("fs", { + setCurrentUserAsOwner: async (path: string, owner: string): Promise => undefined + }); + + testInjector.register("subscriptionService", { + subscribeForNewsletter: async (): Promise => undefined + }); + + testInjector.register("staticConfig", {}); + + testInjector.register("commandsService", { + tryExecuteCommand: async (commandName: string, commandArguments: string[]): Promise => undefined + }); + + testInjector.register("htmlHelpService", { + generateHtmlPages: async (): Promise => undefined + }); + + testInjector.register("options", {}); + + testInjector.register("doctorService", { + printWarnings: async (configOptions?: { trackResult: boolean }): Promise => undefined + }); + + testInjector.register("analyticsService", { + checkConsent: async (): Promise => undefined, + track: async (featureName: string, featureValue: string): Promise => undefined + }); + + testInjector.register("logger", { + out: (formatStr?: any, ...args: any[]): void => undefined, + printMarkdown: (...args: any[]): void => undefined + }); + + testInjector.registerCommand("post-install-cli", PostInstallCliCommand); + + return testInjector; +}; + +describe("post-install command", () => { + it("calls subscriptionService.subscribeForNewsletter method", async () => { + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve("subscriptionService"); + let isSubscribeForNewsletterCalled = false; + subscriptionService.subscribeForNewsletter = async (): Promise => { + isSubscribeForNewsletterCalled = true; + }; + const postInstallCommand = testInjector.resolveCommand("post-install-cli"); + + await postInstallCommand.execute([]); + assert.isTrue(isSubscribeForNewsletterCalled, "post-install-cli command must call subscriptionService.subscribeForNewsletter"); + }); +}); diff --git a/test/debug.ts b/test/debug.ts index dc7de422e9..efd107b116 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -1,6 +1,6 @@ import * as stubs from "./stubs"; import * as yok from "../lib/common/yok"; -import { DebugAndroidCommand } from "../lib/commands/debug"; +import { DebugAndroidCommand, DebugPlatformCommand } from "../lib/commands/debug"; import { assert } from "chai"; import { Configuration, StaticConfig } from "../lib/config"; import { Options } from "../lib/options"; @@ -9,9 +9,13 @@ import { FileSystem } from "../lib/common/file-system"; import { AndroidProjectService } from "../lib/services/android-project-service"; import { AndroidDebugBridge } from "../lib/common/mobile/android/android-debug-bridge"; import { AndroidDebugBridgeResultHandler } from "../lib/common/mobile/android/android-debug-bridge-result-handler"; +import { DebugCommandErrors } from "../lib/constants"; +import { CONNECTED_STATUS, UNREACHABLE_STATUS } from "../lib/common/constants"; +const helpers = require("../lib/common/helpers"); +const originalIsInteracive = helpers.isInteractive; function createTestInjector(): IInjector { - let testInjector: IInjector = new yok.Yok(); + const testInjector: IInjector = new yok.Yok(); testInjector.register("debug|android", DebugAndroidCommand); testInjector.register("config", Configuration); @@ -19,18 +23,22 @@ function createTestInjector(): IInjector { testInjector.register("logger", stubs.LoggerStub); testInjector.register("options", Options); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); - testInjector.register('devicesService', {}); testInjector.register('childProcess', stubs.ChildProcessStub); - testInjector.register('androidDebugService', stubs.DebugServiceStub); testInjector.register('fs', FileSystem); testInjector.register('errors', stubs.ErrorsStub); testInjector.register('hostInfo', {}); testInjector.register("analyticsService", { - trackException: async () => undefined, - checkConsent: async () => undefined, - trackFeature: async () => undefined + trackException: async (): Promise => undefined, + checkConsent: async (): Promise => undefined, + trackFeature: async (): Promise => undefined }); - testInjector.register("usbLiveSyncService", stubs.LiveSyncServiceStub); + testInjector.register('devicesService', { + initialize: async () => { /* Intentionally left blank */ }, + detectCurrentlyAttachedDevices: async () => { /* Intentionally left blank */ }, + getDeviceInstances: (): any[] => { return []; }, + execute: async (): Promise => ({}) + }); + testInjector.register("liveSyncService", stubs.LiveSyncServiceStub); testInjector.register("androidProjectService", AndroidProjectService); testInjector.register("androidToolsInfo", stubs.AndroidToolsInfoStub); testInjector.register("hostInfo", {}); @@ -41,6 +49,7 @@ function createTestInjector(): IInjector { testInjector.register("pluginVariablesService", {}); testInjector.register("deviceAppDataFactory", {}); testInjector.register("projectTemplatesService", {}); + testInjector.register("debugService", {}); testInjector.register("xmlValidator", {}); testInjector.register("npm", {}); testInjector.register("debugDataService", { @@ -57,48 +66,291 @@ function createTestInjector(): IInjector { } }); + testInjector.register("prompter", {}); + testInjector.registerCommand("debug|android", DebugAndroidCommand); + testInjector.register("liveSyncCommandHelper", { + executeLiveSyncOperation: async (): Promise => { + return null; + } + }); + return testInjector; } -describe("Debugger tests", () => { - let testInjector: IInjector; +describe("debug command tests", () => { + describe("getDeviceForDebug", () => { + it("throws error when both --for-device and --emulator are passed", async () => { + const testInjector = createTestInjector(); + const options = testInjector.resolve("options"); + options.forDevice = options.emulator = true; + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); + await assert.isRejected(debugCommand.getDeviceForDebug(), DebugCommandErrors.UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR); + }); - beforeEach(() => { - testInjector = createTestInjector(); - }); + it("returns selected device, when --device is passed", async () => { + const testInjector = createTestInjector(); + const devicesService = testInjector.resolve("devicesService"); + const deviceInstance = {}; + const specifiedDeviceOption = "device1"; + devicesService.getDevice = async (deviceOption: string): Promise => { + if (deviceOption === specifiedDeviceOption) { + return deviceInstance; + } + }; - it("Ensures that debugLivesync flag is true when executing debug --watch command", async () => { - let debugCommand: ICommand = testInjector.resolve("debug|android"); - let options: IOptions = testInjector.resolve("options"); - options.watch = true; - await debugCommand.execute(["android", "--watch"]); - let config: IConfiguration = testInjector.resolve("config"); - assert.isTrue(config.debugLivesync); - }); + const options = testInjector.resolve("options"); + options.device = specifiedDeviceOption; + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); + const selectedDeviceInstance = await debugCommand.getDeviceForDebug(); + assert.deepEqual(selectedDeviceInstance, deviceInstance); + }); + + const assertErrorIsThrown = async (getDeviceInstancesResult: Mobile.IDevice[], passedOptions?: { forDevice: boolean, emulator: boolean }) => { + const testInjector = createTestInjector(); + if (passedOptions) { + const options = testInjector.resolve("options"); + options.forDevice = passedOptions.forDevice; + options.emulator = passedOptions.emulator; + } + + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => getDeviceInstancesResult; + + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); + await assert.isRejected(debugCommand.getDeviceForDebug(), DebugCommandErrors.NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS); + }; + + it("throws error when there are no devices/emulators available", () => { + return assertErrorIsThrown([]); + }); + + it("throws error when there are no devices/emulators available for selected platform", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "ios", + status: CONNECTED_STATUS + } + } + ]); + }); + + it("throws error when there are only not-trusted devices/emulators available for selected platform", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "android", + status: UNREACHABLE_STATUS + } + } + ]); + }); + + it("throws error when there are only devices and --emulator is passed", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS + }, + isEmulator: false + } + ], { + forDevice: false, + emulator: true + }); + }); + + it("throws error when there are only emulators and --forDevice is passed", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS + }, + isEmulator: true + } + ], { + forDevice: true, + emulator: false + }); + }); + + it("returns the only available device/emulator when it matches passed -- options", async () => { + const testInjector = createTestInjector(); + const deviceInstance = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS + }, + isEmulator: true + }; + + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => [deviceInstance]; + + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); + const actualDeviceInstance = await debugCommand.getDeviceForDebug(); + assert.deepEqual(actualDeviceInstance, deviceInstance); + }); + + describe("when multiple devices are detected", () => { + beforeEach(() => { + helpers.isInteractive = originalIsInteracive; + }); + + after(() => { + helpers.isInteractive = originalIsInteracive; + }); + + describe("when terminal is interactive", () => { + + it("prompts the user with information about available devices for specified platform only and returns the selected device instance", async () => { + helpers.isInteractive = () => true; + const testInjector = createTestInjector(); + const deviceInstance1 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance1", + displayName: "displayName1" + }, + isEmulator: true + }; - it("Ensures that beforePrepareAllPlugins will not call gradle when livesyncing", async () => { - let config: IConfiguration = testInjector.resolve("config"); - config.debugLivesync = true; - let childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); - let androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); - let spawnFromEventCount = childProcess.spawnFromEventCount; - await androidProjectService.beforePrepareAllPlugins(projectData); - assert.isTrue(spawnFromEventCount === 0); - assert.isTrue(spawnFromEventCount === childProcess.spawnFromEventCount); + const deviceInstance2 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance2", + displayName: "displayName2" + }, + isEmulator: true + }; + + const iOSDeviceInstance = { + deviceInfo: { + platform: "ios", + status: CONNECTED_STATUS, + identifier: "iosDevice", + displayName: "iPhone" + }, + isEmulator: true + }; + + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => [deviceInstance1, deviceInstance2, iOSDeviceInstance]; + + let choicesPassedToPrompter: string[]; + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = async (promptMessage: string, choices: any[]): Promise => { + choicesPassedToPrompter = choices; + return choices[1]; + }; + + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); + const actualDeviceInstance = await debugCommand.getDeviceForDebug(); + const expectedChoicesPassedToPrompter = [deviceInstance1, deviceInstance2].map(d => `${d.deviceInfo.identifier} - ${d.deviceInfo.displayName}`); + assert.deepEqual(choicesPassedToPrompter, expectedChoicesPassedToPrompter); + + assert.deepEqual(actualDeviceInstance, deviceInstance2); + }); + }); + + describe("when terminal is not interactive", () => { + beforeEach(() => { + helpers.isInteractive = () => false; + }); + + const assertCorrectInstanceIsUsed = async (opts: { forDevice: boolean, emulator: boolean, isEmulatorTest: boolean, excludeLastDevice?: boolean }) => { + const testInjector = createTestInjector(); + const deviceInstance1 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance1", + displayName: "displayName1", + version: "5.1" + }, + isEmulator: opts.isEmulatorTest + }; + + const deviceInstance2 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance2", + displayName: "displayName2", + version: "6.0" + }, + isEmulator: opts.isEmulatorTest + }; + + const deviceInstance3 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance3", + displayName: "displayName3", + version: "7.1" + }, + isEmulator: !opts.isEmulatorTest + }; + + const options = testInjector.resolve("options"); + options.forDevice = opts.forDevice; + options.emulator = opts.emulator; + + const devicesService = testInjector.resolve("devicesService"); + const deviceInstances = [deviceInstance1, deviceInstance2]; + if (!opts.excludeLastDevice) { + deviceInstances.push(deviceInstance3); + } + + devicesService.getDeviceInstances = (): Mobile.IDevice[] => deviceInstances; + + const debugCommand = testInjector.resolve(DebugPlatformCommand, { debugService: {}, platform: "android" }); + const actualDeviceInstance = await debugCommand.getDeviceForDebug(); + + assert.deepEqual(actualDeviceInstance, deviceInstance2); + }; + + it("returns the emulator with highest API level when --emulator is passed", () => { + return assertCorrectInstanceIsUsed({ forDevice: false, emulator: true, isEmulatorTest: true }); + }); + + it("returns the device with highest API level when --forDevice is passed", () => { + return assertCorrectInstanceIsUsed({ forDevice: true, emulator: false, isEmulatorTest: false }); + }); + + it("returns the emulator with highest API level when neither --emulator and --forDevice are passed", () => { + return assertCorrectInstanceIsUsed({ forDevice: false, emulator: false, isEmulatorTest: true }); + }); + + it("returns the device with highest API level when neither --emulator and --forDevice are passed and emulators are not available", async () => { + return assertCorrectInstanceIsUsed({ forDevice: false, emulator: false, isEmulatorTest: false, excludeLastDevice: true }); + }); + }); + }); }); - it("Ensures that beforePrepareAllPlugins will call gradle with clean option when *NOT* livesyncing", async () => { - let config: IConfiguration = testInjector.resolve("config"); - config.debugLivesync = false; - let childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); - let androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); - let spawnFromEventCount = childProcess.spawnFromEventCount; - await androidProjectService.beforePrepareAllPlugins(projectData); - assert.isTrue(childProcess.lastCommand.indexOf("gradle") !== -1); - assert.isTrue(childProcess.lastCommandArgs[0] === "clean"); - assert.isTrue(spawnFromEventCount === 0); - assert.isTrue(spawnFromEventCount + 1 === childProcess.spawnFromEventCount); + describe("Debugger tests", () => { + let testInjector: IInjector; + + beforeEach(() => { + testInjector = createTestInjector(); + }); + + it("Ensures that beforePrepareAllPlugins will call gradle with clean option when *NOT* livesyncing", async () => { + const childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); + const androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); + const projectData: IProjectData = testInjector.resolve("projectData"); + const spawnFromEventCount = childProcess.spawnFromEventCount; + await androidProjectService.beforePrepareAllPlugins(projectData); + assert.isTrue(childProcess.lastCommand.indexOf("gradle") !== -1); + assert.isTrue(childProcess.lastCommandArgs[0] === "clean"); + assert.isTrue(spawnFromEventCount === 0); + assert.isTrue(spawnFromEventCount + 1 === childProcess.spawnFromEventCount); + }); }); }); diff --git a/test/ios-entitlements-service.ts b/test/ios-entitlements-service.ts new file mode 100644 index 0000000000..2edfb58ba1 --- /dev/null +++ b/test/ios-entitlements-service.ts @@ -0,0 +1,187 @@ +import temp = require("temp"); +import { EOL } from "os"; +import { assert } from "chai"; +import { IOSEntitlementsService } from "../lib/services/ios-entitlements-service"; +import * as yok from "../lib/common/yok"; +import * as stubs from "./stubs"; +import * as FsLib from "../lib/common/file-system"; +import * as MobilePlatformsCapabilitiesLib from "../lib/common/appbuilder/mobile-platforms-capabilities"; +import * as MobileHelperLib from "../lib/common/mobile/mobile-helper"; +import * as DevicePlatformsConstantsLib from "../lib/common/mobile/device-platforms-constants"; +import * as ErrorsLib from "../lib/common/errors"; +import * as path from "path"; + +// start tracking temporary folders/files +temp.track(); + +describe("IOSEntitlements Service Tests", () => { + const createTestInjector = (): IInjector => { + const testInjector = new yok.Yok(); + + testInjector.register('platformsData', stubs.PlatformsDataStub); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register('iOSEntitlementsService', IOSEntitlementsService); + + testInjector.register("fs", FsLib.FileSystem); + testInjector.register("mobileHelper", MobileHelperLib.MobileHelper); + testInjector.register("devicePlatformsConstants", DevicePlatformsConstantsLib.DevicePlatformsConstants); + testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilitiesLib.MobilePlatformsCapabilities); + testInjector.register("errors", ErrorsLib.Errors); + + testInjector.register("pluginsService", { + getAllInstalledPlugins: async (): Promise => [] + }); + + return testInjector; + }; + + let injector: IInjector; + let platformsData: any; + let projectData: IProjectData; + let fs: IFileSystem; + let iOSEntitlementsService: IOSEntitlementsService; + let destinationFilePath: string; + + beforeEach(() => { + injector = createTestInjector(); + + platformsData = injector.resolve("platformsData"); + projectData = platformsData.getPlatformData(); + projectData.projectName = 'testApp'; + + projectData.platformsDir = temp.mkdirSync("platformsDir"); + projectData.projectDir = temp.mkdirSync("projectDir"); + + fs = injector.resolve("$fs"); + + iOSEntitlementsService = injector.resolve("iOSEntitlementsService"); + destinationFilePath = iOSEntitlementsService.getPlatformsEntitlementsPath(projectData); + }); + + describe("Ensure paths constructed are correct", () => { + it("Ensure destination entitlements relative path is calculated correctly.", () => { + const expected = path.join("testApp", "testApp.entitlements"); + const actual = iOSEntitlementsService.getPlatformsEntitlementsRelativePath(projectData); + assert.equal(actual, expected); + }); + + it("Ensure full path to entitlements in platforms dir is correct", () => { + const expected = path.join(projectData.platformsDir, "ios", "testApp", "testApp.entitlements"); + const actual = iOSEntitlementsService.getPlatformsEntitlementsPath(projectData); + assert.equal(actual, expected); + }); + }); + + describe("Merge", () => { + const defaultPlistContent = ` + + + +`; + const defaultAppResourcesEntitlementsContent = ` + + + + aps-environment + development + +`; + const defaultPluginEntitlementsContent = ` + + + + aps-environment + production + +`; + const namedAppResourcesEntitlementsContent = ` + + + + nameKey + appResources + +`; + const mergedEntitlementsContent = ` + + + + aps-environment + production + nameKey + appResources + +`; + + function assertContent(actual: string, expected: string) { + const strip = (x: string) => { + return x.replace(EOL, '').trim(); + }; + assert.equal(strip(actual), strip(expected)); + } + + it("Merge creates a default entitlements file.", async () => { + // act + await iOSEntitlementsService.merge(projectData); + + // assert + const actual = fs.readText(destinationFilePath); + assertContent(actual, defaultPlistContent); + }); + + it("Merge uses the entitlements from App_Resources folder", async () => { + const appResourcesEntitlement = (iOSEntitlementsService).getDefaultAppEntitlementsPath(projectData); + fs.writeFile(appResourcesEntitlement, defaultAppResourcesEntitlementsContent); + + // act + await iOSEntitlementsService.merge(projectData); + + // assert + const actual = fs.readText(destinationFilePath); + assertContent(actual, defaultAppResourcesEntitlementsContent); + }); + + it("Merge uses the entitlements file from a Plugin", async () => { + const pluginsService = injector.resolve("pluginsService"); + const testPluginFolderPath = temp.mkdirSync("testPlugin"); + pluginsService.getAllInstalledPlugins = async () => [{ + pluginPlatformsFolderPath: (platform: string) => { + return testPluginFolderPath; + } + }]; + const pluginAppEntitlementsPath = path.join(testPluginFolderPath, IOSEntitlementsService.DefaultEntitlementsName); + fs.writeFile(pluginAppEntitlementsPath, defaultPluginEntitlementsContent); + + // act + await iOSEntitlementsService.merge(projectData); + + // assert + const actual = fs.readText(destinationFilePath); + assertContent(actual, defaultPluginEntitlementsContent); + }); + + it("Merge uses App_Resources and Plugins and merges all keys", async () => { + // setup app resoruces + const appResourcesEntitlement = (iOSEntitlementsService).getDefaultAppEntitlementsPath(projectData); + fs.writeFile(appResourcesEntitlement, namedAppResourcesEntitlementsContent); + + // setup plugin entitlements + const pluginsService = injector.resolve("pluginsService"); + const testPluginFolderPath = temp.mkdirSync("testPlugin"); + pluginsService.getAllInstalledPlugins = async () => [{ + pluginPlatformsFolderPath: (platform: string) => { + return testPluginFolderPath; + } + }]; + const pluginAppEntitlementsPath = path.join(testPluginFolderPath, IOSEntitlementsService.DefaultEntitlementsName); + fs.writeFile(pluginAppEntitlementsPath, defaultPluginEntitlementsContent); + + // act + await iOSEntitlementsService.merge(projectData); + + // assert + const actual = fs.readText(destinationFilePath); + assertContent(actual, mergedEntitlementsContent); + }); + }); +}); diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index abf9288b69..30b7e58629 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import { EOL } from "os"; import * as ChildProcessLib from "../lib/common/child-process"; import * as ConfigLib from "../lib/config"; import * as ErrorsLib from "../lib/common/errors"; @@ -6,6 +7,8 @@ import * as FileSystemLib from "../lib/common/file-system"; import * as HostInfoLib from "../lib/common/host-info"; import * as iOSProjectServiceLib from "../lib/services/ios-project-service"; import { IOSProjectService } from "../lib/services/ios-project-service"; +import { IOSEntitlementsService } from "../lib/services/ios-entitlements-service"; +import { XCConfigService } from "../lib/services/xcconfig-service"; import * as LoggerLib from "../lib/common/logger"; import * as OptionsLib from "../lib/options"; import * as yok from "../lib/common/yok"; @@ -20,10 +23,16 @@ import { DeviceDiscovery } from "../lib/common/mobile/mobile-core/device-discove import { IOSDeviceDiscovery } from "../lib/common/mobile/mobile-core/ios-device-discovery"; import { AndroidDeviceDiscovery } from "../lib/common/mobile/mobile-core/android-device-discovery"; import { PluginVariablesService } from "../lib/services/plugin-variables-service"; +import { PluginsService } from "../lib/services/plugins-service"; import { PluginVariablesHelper } from "../lib/common/plugin-variables-helper"; import { Utils } from "../lib/common/utils"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; +import { NpmInstallationManager } from "../lib/npm-installation-manager"; +import { NodePackageManager } from "../lib/node-package-manager"; +import * as constants from "../lib/constants"; + import { assert } from "chai"; +import { IOSProvisionService } from "../lib/services/ios-provision-service"; import temp = require("temp"); temp.track(); @@ -39,7 +48,7 @@ class IOSSimulatorDiscoveryMock extends DeviceDiscovery { } function createTestInjector(projectPath: string, projectName: string): IInjector { - let testInjector = new yok.Yok(); + const testInjector = new yok.Yok(); testInjector.register("childProcess", ChildProcessLib.ChildProcess); testInjector.register("config", ConfigLib.Configuration); testInjector.register("errors", ErrorsLib.Errors); @@ -51,6 +60,8 @@ function createTestInjector(projectPath: string, projectName: string): IInjector testInjector.register("cocoapodsService", CocoaPodsService); testInjector.register("iOSProjectService", iOSProjectServiceLib.IOSProjectService); testInjector.register("iOSProvisionService", {}); + testInjector.register("xCConfigService", XCConfigService); + testInjector.register("iOSEntitlementsService", IOSEntitlementsService); testInjector.register("logger", LoggerLib.Logger); testInjector.register("options", OptionsLib.Options); testInjector.register("projectData", { @@ -78,18 +89,36 @@ function createTestInjector(projectPath: string, projectName: string): IInjector testInjector.register("loggingLevels", LoggingLevels); testInjector.register("utils", Utils); testInjector.register("iTunesValidator", {}); - testInjector.register("xcprojService", {}); + testInjector.register("xcprojService", { + getXcprojInfo: () => { + return { + shouldUseXcproj: false + }; + } + }); testInjector.register("iosDeviceOperations", {}); testInjector.register("pluginVariablesService", PluginVariablesService); testInjector.register("pluginVariablesHelper", PluginVariablesHelper); + testInjector.register("pluginsService", PluginsService); testInjector.register("androidProcessService", {}); testInjector.register("processService", {}); testInjector.register("sysInfo", {}); + testInjector.register("pbxprojDomXcode", {}); + testInjector.register("xcode", { + project: class { + constructor() { /* */ } + parseSync() { /* */ } + pbxGroupByName() { /* */ } + } + }); + testInjector.register("npmInstallationManager", NpmInstallationManager); + testInjector.register("npm", NodePackageManager); + testInjector.register("xCConfigService", XCConfigService); return testInjector; } function createPackageJson(testInjector: IInjector, projectPath: string, projectName: string) { - let packageJsonData = { + const packageJsonData = { "name": projectName, "version": "0.1.0", "nativescript": { @@ -105,14 +134,14 @@ function createPackageJson(testInjector: IInjector, projectPath: string, project } function expectOption(args: string[], option: string, value: string, message?: string): void { - let index = args.indexOf(option); + const index = args.indexOf(option); assert.ok(index >= 0, "Expected " + option + " to be set."); assert.ok(args.length > index + 1, "Expected " + option + " to have value"); assert.equal(args[index + 1], value, message); } function readOption(args: string[], option: string): string { - let index = args.indexOf(option); + const index = args.indexOf(option); assert.ok(index >= 0, "Expected " + option + " to be set."); assert.ok(args.length > index + 1, "Expected " + option + " to have value"); return args[index + 1]; @@ -121,16 +150,16 @@ function readOption(args: string[], option: string): string { describe("iOSProjectService", () => { describe("archive", () => { async function setupArchive(options?: { archivePath?: string }): Promise<{ run: () => Promise, assert: () => void }> { - let hasCustomArchivePath = options && options.archivePath; + const hasCustomArchivePath = options && options.archivePath; - let projectName = "projectDirectory"; - let projectPath = temp.mkdirSync(projectName); + const projectName = "projectDirectory"; + const projectPath = temp.mkdirSync(projectName); - let testInjector = createTestInjector(projectPath, projectName); - let iOSProjectService = testInjector.resolve("iOSProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const testInjector = createTestInjector(projectPath, projectName); + const iOSProjectService = testInjector.resolve("iOSProjectService"); + const projectData: IProjectData = testInjector.resolve("projectData"); - let childProcess = testInjector.resolve("childProcess"); + const childProcess = testInjector.resolve("childProcess"); let xcodebuildExeced = false; let archivePath: string; @@ -170,20 +199,25 @@ describe("iOSProjectService", () => { } }; } - it("by default exports xcodearchive to platforms/ios/build/archive/.xcarchive", async () => { - let setup = await setupArchive(); - await setup.run(); - setup.assert(); - }); - it("can pass archivePath to xcodebuild -archivePath", async () => { - let setup = await setupArchive({ archivePath: "myarchive.xcarchive" }); - await setup.run(); - setup.assert(); - }); + + if (require("os").platform() !== "darwin") { + console.log("Skipping iOS archive tests. They can work only on macOS"); + } else { + it("by default exports xcodearchive to platforms/ios/build/archive/.xcarchive", async () => { + const setup = await setupArchive(); + await setup.run(); + setup.assert(); + }); + it("can pass archivePath to xcodebuild -archivePath", async () => { + const setup = await setupArchive({ archivePath: "myarchive.xcarchive" }); + await setup.run(); + setup.assert(); + }); + } }); describe("exportArchive", () => { - let noTeamPlist = ` + const noTeamPlist = ` @@ -196,7 +230,7 @@ describe("iOSProjectService", () => { `; - let myTeamPlist = ` + const myTeamPlist = ` @@ -212,17 +246,17 @@ describe("iOSProjectService", () => { `; async function testExportArchive(options: { teamID?: string }, expectedPlistContent: string): Promise { - let projectName = "projectDirectory"; - let projectPath = temp.mkdirSync(projectName); + const projectName = "projectDirectory"; + const projectPath = temp.mkdirSync(projectName); - let testInjector = createTestInjector(projectPath, projectName); - let iOSProjectService = testInjector.resolve("iOSProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const testInjector = createTestInjector(projectPath, projectName); + const iOSProjectService = testInjector.resolve("iOSProjectService"); + const projectData: IProjectData = testInjector.resolve("projectData"); - let archivePath = path.join(projectPath, "platforms", "ios", "build", "archive", projectName + ".xcarchive"); + const archivePath = path.join(projectPath, "platforms", "ios", "build", "archive", projectName + ".xcarchive"); - let childProcess = testInjector.resolve("childProcess"); - let fs = testInjector.resolve("fs"); + const childProcess = testInjector.resolve("childProcess"); + const fs = testInjector.resolve("fs"); let xcodebuildExeced = false; @@ -234,32 +268,36 @@ describe("iOSProjectService", () => { expectOption(args, "-archivePath", archivePath, "Expected the -archivePath to be passed to xcodebuild."); expectOption(args, "-exportPath", path.join(projectPath, "platforms", "ios", "build", "archive"), "Expected the -archivePath to be passed to xcodebuild."); - let plist = readOption(args, "-exportOptionsPlist"); + const plist = readOption(args, "-exportOptionsPlist"); assert.ok(plist); - let plistContent = fs.readText(plist); + const plistContent = fs.readText(plist); // There may be better way to equal property lists assert.equal(plistContent, expectedPlistContent, "Mismatch in exportOptionsPlist content"); return Promise.resolve(); }; - let resultIpa = await iOSProjectService.exportArchive(projectData, { archivePath, teamID: options.teamID }); - let expectedIpa = path.join(projectPath, "platforms", "ios", "build", "archive", projectName + ".ipa"); + const resultIpa = await iOSProjectService.exportArchive(projectData, { archivePath, teamID: options.teamID }); + const expectedIpa = path.join(projectPath, "platforms", "ios", "build", "archive", projectName + ".ipa"); assert.equal(resultIpa, expectedIpa, "Expected IPA at the specified location"); assert.ok(xcodebuildExeced, "Expected xcodebuild to be executed"); } - it("calls xcodebuild -exportArchive to produce .IPA", async () => { - await testExportArchive({}, noTeamPlist); - }); + if (require("os").platform() !== "darwin") { + console.log("Skipping iOS export archive tests. They can work only on macOS"); + } else { + it("calls xcodebuild -exportArchive to produce .IPA", async () => { + await testExportArchive({}, noTeamPlist); + }); - it("passes the --team-id option down the xcodebuild -exportArchive throug the -exportOptionsPlist", async () => { - await testExportArchive({ teamID: "MyTeam" }, myTeamPlist); - }); + it("passes the --team-id option down the xcodebuild -exportArchive throug the -exportOptionsPlist", async () => { + await testExportArchive({ teamID: "MyTeam" }, myTeamPlist); + }); + } }); }); @@ -268,13 +306,13 @@ describe("Cocoapods support", () => { console.log("Skipping Cocoapods tests. They cannot work on windows"); } else { it("adds plugin with Podfile", async () => { - let projectName = "projectDirectory"; - let projectPath = temp.mkdirSync(projectName); + const projectName = "projectDirectory"; + const projectPath = temp.mkdirSync(projectName); - let testInjector = createTestInjector(projectPath, projectName); - let fs: IFileSystem = testInjector.resolve("fs"); + const testInjector = createTestInjector(projectPath, projectName); + const fs: IFileSystem = testInjector.resolve("fs"); - let packageJsonData = { + const packageJsonData = { "name": "myProject", "version": "0.1.0", "nativescript": { @@ -286,10 +324,10 @@ describe("Cocoapods support", () => { }; fs.writeJson(path.join(projectPath, "package.json"), packageJsonData); - let platformsFolderPath = path.join(projectPath, "platforms", "ios"); + const platformsFolderPath = path.join(projectPath, "platforms", "ios"); fs.createDirectory(platformsFolderPath); - let iOSProjectService = testInjector.resolve("iOSProjectService"); + const iOSProjectService = testInjector.resolve("iOSProjectService"); iOSProjectService.prepareFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): Promise => { return Promise.resolve(); }; @@ -304,26 +342,26 @@ describe("Cocoapods support", () => { }; iOSProjectService.savePbxProj = (): Promise => Promise.resolve(); - let pluginPath = temp.mkdirSync("pluginDirectory"); - let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios"); - let pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile"); - let pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n"); + const pluginPath = temp.mkdirSync("pluginDirectory"); + const pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios"); + const pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + const pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n"); fs.writeFile(pluginPodfilePath, pluginPodfileContent); - let pluginData = { + const pluginData = { pluginPlatformsFolderPath(platform: string): string { return pluginPlatformsFolderPath; } }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); await iOSProjectService.preparePluginNativeCode(pluginData, projectData); - let projectPodfilePath = path.join(platformsFolderPath, "Podfile"); + const projectPodfilePath = path.join(platformsFolderPath, "Podfile"); assert.isTrue(fs.exists(projectPodfilePath)); - let actualProjectPodfileContent = fs.readText(projectPodfilePath); - let expectedProjectPodfileContent = ["use_frameworks!\n", + const actualProjectPodfileContent = fs.readText(projectPodfilePath); + const expectedProjectPodfileContent = ["use_frameworks!\n", `target "${projectName}" do`, `# Begin Podfile - ${pluginPodfilePath} `, ` ${pluginPodfileContent} `, @@ -333,13 +371,13 @@ describe("Cocoapods support", () => { assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent); }); it("adds and removes plugin with Podfile", async () => { - let projectName = "projectDirectory2"; - let projectPath = temp.mkdirSync(projectName); + const projectName = "projectDirectory2"; + const projectPath = temp.mkdirSync(projectName); - let testInjector = createTestInjector(projectPath, projectName); - let fs: IFileSystem = testInjector.resolve("fs"); + const testInjector = createTestInjector(projectPath, projectName); + const fs: IFileSystem = testInjector.resolve("fs"); - let packageJsonData = { + const packageJsonData = { "name": "myProject2", "version": "0.1.0", "nativescript": { @@ -351,10 +389,10 @@ describe("Cocoapods support", () => { }; fs.writeJson(path.join(projectPath, "package.json"), packageJsonData); - let platformsFolderPath = path.join(projectPath, "platforms", "ios"); + const platformsFolderPath = path.join(projectPath, "platforms", "ios"); fs.createDirectory(platformsFolderPath); - let iOSProjectService = testInjector.resolve("iOSProjectService"); + const iOSProjectService = testInjector.resolve("iOSProjectService"); iOSProjectService.prepareFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): Promise => { return Promise.resolve(); }; @@ -375,26 +413,26 @@ describe("Cocoapods support", () => { }; iOSProjectService.savePbxProj = (): Promise => Promise.resolve(); - let pluginPath = temp.mkdirSync("pluginDirectory"); - let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios"); - let pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile"); - let pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n"); + const pluginPath = temp.mkdirSync("pluginDirectory"); + const pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios"); + const pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + const pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n"); fs.writeFile(pluginPodfilePath, pluginPodfileContent); - let pluginData = { + const pluginData = { pluginPlatformsFolderPath(platform: string): string { return pluginPlatformsFolderPath; } }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); await iOSProjectService.preparePluginNativeCode(pluginData, projectData); - let projectPodfilePath = path.join(platformsFolderPath, "Podfile"); + const projectPodfilePath = path.join(platformsFolderPath, "Podfile"); assert.isTrue(fs.exists(projectPodfilePath)); - let actualProjectPodfileContent = fs.readText(projectPodfilePath); - let expectedProjectPodfileContent = ["use_frameworks!\n", + const actualProjectPodfileContent = fs.readText(projectPodfilePath); + const expectedProjectPodfileContent = ["use_frameworks!\n", `target "${projectName}" do`, `# Begin Podfile - ${pluginPodfilePath} `, ` ${pluginPodfileContent} `, @@ -416,17 +454,17 @@ describe("Static libraries support", () => { return; } - let projectName = "projectDirectory"; - let projectPath = temp.mkdirSync(projectName); - let libraryName = "testLibrary1"; - let headers = ["TestHeader1.h", "TestHeader2.h"]; - let testInjector = createTestInjector(projectPath, projectName); - let fs: IFileSystem = testInjector.resolve("fs"); - let staticLibraryPath = path.join(path.join(temp.mkdirSync("pluginDirectory"), "platforms", "ios")); - let staticLibraryHeadersPath = path.join(staticLibraryPath, "include", libraryName); + const projectName = "TNSApp"; + const projectPath = temp.mkdirSync(projectName); + const libraryName = "testLibrary1"; + const headers = ["TestHeader1.h", "TestHeader2.h"]; + const testInjector = createTestInjector(projectPath, projectName); + const fs: IFileSystem = testInjector.resolve("fs"); + const staticLibraryPath = path.join(path.join(temp.mkdirSync("pluginDirectory"), "platforms", "ios")); + const staticLibraryHeadersPath = path.join(staticLibraryPath, "include", libraryName); it("checks validation of header files", async () => { - let iOSProjectService = testInjector.resolve("iOSProjectService"); + const iOSProjectService = testInjector.resolve("iOSProjectService"); fs.ensureDirectoryExists(staticLibraryHeadersPath); _.each(headers, header => { fs.writeFile(path.join(staticLibraryHeadersPath, header), ""); }); @@ -444,15 +482,15 @@ describe("Static libraries support", () => { }); it("checks generation of modulemaps", () => { - let iOSProjectService = testInjector.resolve("iOSProjectService"); + const iOSProjectService = testInjector.resolve("iOSProjectService"); fs.ensureDirectoryExists(staticLibraryHeadersPath); _.each(headers, header => { fs.writeFile(path.join(staticLibraryHeadersPath, header), ""); }); iOSProjectService.generateModulemap(staticLibraryHeadersPath, libraryName); // Read the generated modulemap and verify it. let modulemap = fs.readFile(path.join(staticLibraryHeadersPath, "module.modulemap")); - let headerCommands = _.map(headers, value => `header "${value}"`); - let modulemapExpectation = `module ${libraryName} { explicit module ${libraryName} { ${headerCommands.join(" ")} } }`; + const headerCommands = _.map(headers, value => `header "${value}"`); + const modulemapExpectation = `module ${libraryName} { explicit module ${libraryName} { ${headerCommands.join(" ")} } }`; assert.equal(modulemap, modulemapExpectation); @@ -473,16 +511,361 @@ describe("Static libraries support", () => { describe("Relative paths", () => { it("checks for correct calculation of relative paths", () => { - let projectName = "projectDirectory"; - let projectPath = temp.mkdirSync(projectName); - let subpath = path.join(projectPath, "sub", "path"); + const projectName = "projectDirectory"; + const projectPath = temp.mkdirSync(projectName); + const subpath = path.join(projectPath, "sub", "path"); - let testInjector = createTestInjector(projectPath, projectName); + const testInjector = createTestInjector(projectPath, projectName); createPackageJson(testInjector, projectPath, projectName); - let iOSProjectService = testInjector.resolve("iOSProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const iOSProjectService = testInjector.resolve("iOSProjectService"); + const projectData: IProjectData = testInjector.resolve("projectData"); - let result = iOSProjectService.getLibSubpathRelativeToProjectPath(subpath, projectData); + const result = iOSProjectService.getLibSubpathRelativeToProjectPath(subpath, projectData); assert.equal(result, path.join("..", "..", "sub", "path")); }); }); + +describe("iOS Project Service Signing", () => { + let testInjector: IInjector; + let projectName: string; + let projectDirName: string; + let projectPath: string; + let files: any; + let iOSProjectService: IPlatformProjectService; + let projectData: any; + let pbxproj: string; + let iOSProvisionService: IOSProvisionService; + let pbxprojDomXcode: IPbxprojDomXcode; + + beforeEach(() => { + files = {}; + projectName = "TNSApp" + Math.ceil(Math.random() * 1000); + projectDirName = projectName + "Dir"; + projectPath = temp.mkdirSync(projectDirName); + testInjector = createTestInjector(projectPath, projectDirName); + testInjector.register("fs", { + files: {}, + readJson(path: string): any { + if (this.exists(path)) { + return JSON.stringify(files[path]); + } else { + return null; + } + }, + exists(path: string): boolean { + return path in files; + } + }); + testInjector.register("pbxprojDomXcode", { Xcode: {} }); + pbxproj = path.join(projectPath, `platforms/ios/${projectDirName}.xcodeproj/project.pbxproj`); + iOSProjectService = testInjector.resolve("iOSProjectService"); + iOSProvisionService = testInjector.resolve("iOSProvisionService"); + pbxprojDomXcode = testInjector.resolve("pbxprojDomXcode"); + projectData = testInjector.resolve("projectData"); + iOSProvisionService.pick = async (uuidOrName: string, projId: string) => { + return ({ + "NativeScriptDev": { + Name: "NativeScriptDev", + CreationDate: null, + ExpirationDate: null, + TeamName: "Telerik AD", + TeamIdentifier: ["TKID101"], + ProvisionedDevices: [], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "12345", + ProvisionsAllDevices: false, + ApplicationIdentifierPrefix: null, + DeveloperCertificates: null, + Type: "Development" + }, + "NativeScriptDist": { + Name: "NativeScriptDist", + CreationDate: null, + ExpirationDate: null, + TeamName: "Telerik AD", + TeamIdentifier: ["TKID202"], + ProvisionedDevices: [], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "6789", + ProvisionsAllDevices: true, + ApplicationIdentifierPrefix: null, + DeveloperCertificates: null, + Type: "Distribution" + }, + "NativeScriptAdHoc": { + Name: "NativeScriptAdHoc", + CreationDate: null, + ExpirationDate: null, + TeamName: "Telerik AD", + TeamIdentifier: ["TKID303"], + ProvisionedDevices: [], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "1010", + ProvisionsAllDevices: true, + ApplicationIdentifierPrefix: null, + DeveloperCertificates: null, + Type: "Distribution" + } + })[uuidOrName]; + }; + }); + + describe("Check for Changes", () => { + it("sets signingChanged if no Xcode project exists", async () => { + const changes = {}; + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); + assert.isTrue(!!changes.signingChanged); + }); + it("sets signingChanged if the Xcode projects is configured with Automatic signing, but proivsion is specified", async () => { + files[pbxproj] = ""; + pbxprojDomXcode.Xcode.open = function (path: string) { + assert.equal(path, pbxproj); + return { + getSigning(x: string) { + return { style: "Automatic" }; + } + }; + }; + const changes = {}; + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); + assert.isTrue(!!changes.signingChanged); + }); + it("sets signingChanged if the Xcode projects is configured with Manual signing, but the proivsion specified differs the selected in the pbxproj", async () => { + files[pbxproj] = ""; + pbxprojDomXcode.Xcode.open = function (path: string) { + assert.equal(path, pbxproj); + return { + getSigning() { + return { + style: "Manual", configurations: { + Debug: { name: "NativeScriptDev2" }, + Release: { name: "NativeScriptDev2" } + } + }; + } + }; + }; + const changes = {}; + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); + assert.isTrue(!!changes.signingChanged); + }); + it("does not set signingChanged if the Xcode projects is configured with Manual signing and proivsion matches", async () => { + files[pbxproj] = ""; + pbxprojDomXcode.Xcode.open = function (path: string) { + assert.equal(path, pbxproj); + return { + getSigning() { + return { + style: "Manual", configurations: { + Debug: { name: "NativeScriptDev" }, + Release: { name: "NativeScriptDev" } + } + }; + } + }; + }; + const changes = {}; + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); + assert.isFalse(!!changes.signingChanged); + }); + }); + + describe("specifying provision", () => { + describe("from Automatic to provision name", () => { + beforeEach(() => { + files[pbxproj] = ""; + pbxprojDomXcode.Xcode.open = function (path: string) { + return { + getSigning(x: string) { + return { style: "Automatic", teamID: "AutoTeam" }; + } + }; + }; + }); + it("fails with proper error if the provision can not be found", async () => { + try { + await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev2", teamId: undefined }); + } catch (e) { + assert.isTrue(e.toString().indexOf("Failed to find mobile provision with UUID or Name: NativeScriptDev2") >= 0); + } + }); + it("succeeds if the provision name is provided for development cert", async () => { + const stack: any = []; + pbxprojDomXcode.Xcode.open = function (path: string) { + assert.equal(path, pbxproj); + return { + getSigning() { + return { style: "Automatic", teamID: "AutoTeam" }; + }, + save() { + stack.push("save()"); + }, + setManualSigningStyle(targetName: string, manualSigning: any) { + stack.push({ targetName, manualSigning }); + } + }; + }; + await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev", teamId: undefined }); + assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID101", uuid: "12345", name: "NativeScriptDev", identity: "iPhone Developer" } }, "save()"]); + }); + it("succeds if the provision name is provided for distribution cert", async () => { + const stack: any = []; + pbxprojDomXcode.Xcode.open = function (path: string) { + assert.equal(path, pbxproj); + return { + getSigning() { + return { style: "Automatic", teamID: "AutoTeam" }; + }, + save() { + stack.push("save()"); + }, + setManualSigningStyle(targetName: string, manualSigning: any) { + stack.push({ targetName, manualSigning }); + } + }; + }; + await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDist", teamId: undefined }); + assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID202", uuid: "6789", name: "NativeScriptDist", identity: "iPhone Distribution" } }, "save()"]); + }); + it("succeds if the provision name is provided for adhoc cert", async () => { + const stack: any = []; + pbxprojDomXcode.Xcode.open = function (path: string) { + assert.equal(path, pbxproj); + return { + getSigning() { + return { style: "Automatic", teamID: "AutoTeam" }; + }, + save() { + stack.push("save()"); + }, + setManualSigningStyle(targetName: string, manualSigning: any) { + stack.push({ targetName, manualSigning }); + } + }; + }; + await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptAdHoc", teamId: undefined }); + assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID303", uuid: "1010", name: "NativeScriptAdHoc", identity: "iPhone Distribution" } }, "save()"]); + }); + }); + }); +}); + +describe("Merge Project XCConfig files", () => { + if (require("os").platform() !== "darwin") { + console.log("Skipping 'Merge Project XCConfig files' tests. They can work only on macOS"); + return; + } + const assertPropertyValues = (expected: any, xcconfigPath: string, injector: IInjector) => { + const service = injector.resolve('xCConfigService'); + _.forOwn(expected, (value, key) => { + const actual = service.readPropertyValue(xcconfigPath, key); + assert.equal(actual, value); + }); + }; + + let projectName: string; + let projectPath: string; + let testInjector: IInjector; + let iOSProjectService: IOSProjectService; + let projectData: IProjectData; + let fs: IFileSystem; + let appResourcesXcconfigPath: string; + let appResourceXCConfigContent: string; + let iOSEntitlementsService: IOSEntitlementsService; + + beforeEach(() => { + projectName = "projectDirectory"; + projectPath = temp.mkdirSync(projectName); + + testInjector = createTestInjector(projectPath, projectName); + iOSProjectService = testInjector.resolve("iOSProjectService"); + projectData = testInjector.resolve("projectData"); + projectData.projectDir = projectPath; + + iOSEntitlementsService = testInjector.resolve("iOSEntitlementsService"); + + appResourcesXcconfigPath = path.join(projectData.projectDir, constants.APP_FOLDER_NAME, + constants.APP_RESOURCES_FOLDER_NAME, "iOS", "build.xcconfig"); + appResourceXCConfigContent = `CODE_SIGN_IDENTITY = iPhone Distribution + // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html + // DEVELOPMENT_TEAM = YOUR_TEAM_ID; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + `; + const testPackageJson = { + "name": "test-project", + "version": "0.0.1" + }; + fs = testInjector.resolve("fs"); + fs.writeJson(path.join(projectPath, "package.json"), testPackageJson); + }); + + it("Uses the build.xcconfig file content from App_Resources", async () => { + // setup app_resource build.xcconfig + fs.writeFile(appResourcesXcconfigPath, appResourceXCConfigContent); + + // run merge for all release: debug|release + for (const release in [true, false]) { + await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + + const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) + : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + + assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon', + 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME': 'LaunchImage', + 'CODE_SIGN_IDENTITY': 'iPhone Distribution' + }; + assertPropertyValues(expected, destinationFilePath, testInjector); + } + }); + + it("Adds the entitlements property if not set by the user", async () => { + for (const release in [true, false]) { + await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + + const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) + : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + + assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); + const expected = { + 'CODE_SIGN_ENTITLEMENTS': iOSEntitlementsService.getPlatformsEntitlementsRelativePath(projectData) + }; + assertPropertyValues(expected, destinationFilePath, testInjector); + } + }); + + it("The user specified entitlements property takes precedence", async () => { + // setup app_resource build.xcconfig + const expectedEntitlementsFile = 'user.entitlements'; + const xcconfigEntitlements = appResourceXCConfigContent + `${EOL}CODE_SIGN_ENTITLEMENTS = ${expectedEntitlementsFile}`; + fs.writeFile(appResourcesXcconfigPath, xcconfigEntitlements); + + // run merge for all release: debug|release + for (const release in [true, false]) { + await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + + const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) + : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + + assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon', + 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME': 'LaunchImage', + 'CODE_SIGN_IDENTITY': 'iPhone Distribution', + 'CODE_SIGN_ENTITLEMENTS': expectedEntitlementsFile + }; + assertPropertyValues(expected, destinationFilePath, testInjector); + } + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts index 6541568281..5a8f874618 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,5 +1,6 @@ --recursive --reporter spec +--require source-map-support/register --require test/test-bootstrap.js --timeout 150000 test/ diff --git a/test/nativescript-cli-lib.ts b/test/nativescript-cli-lib.ts index 394eebd871..10854e48f0 100644 --- a/test/nativescript-cli-lib.ts +++ b/test/nativescript-cli-lib.ts @@ -13,11 +13,15 @@ describe("nativescript-cli-lib", () => { }); const publicApi: any = { + settingsService: ["setSettings"], deviceEmitter: null, projectService: ["createProject", "isValidNativeScriptProject"], localBuildService: ["build"], deviceLogProvider: null, - extensibilityService: ["loadExtensions", "getInstalledExtensions", "installExtension", "uninstallExtension"] + npm: ["install", "uninstall", "view", "search"], + extensibilityService: ["loadExtensions", "loadExtension", "getInstalledExtensions", "installExtension", "uninstallExtension"], + liveSyncService: ["liveSync", "stopLiveSync", "enableDebugging", "disableDebugging", "attachDebugger"], + debugService: ["debug"] }; const pathToEntryPoint = path.join(__dirname, "..", "lib", "nativescript-cli-lib.js").replace(/\\/g, "\\\\"); diff --git a/test/npm-installation-manager.ts b/test/npm-installation-manager.ts index a65ca053b4..fa06505ca0 100644 --- a/test/npm-installation-manager.ts +++ b/test/npm-installation-manager.ts @@ -11,11 +11,10 @@ import * as yok from "../lib/common/yok"; import ChildProcessLib = require("../lib/common/child-process"); function createTestInjector(): IInjector { - let testInjector = new yok.Yok(); + const testInjector = new yok.Yok(); testInjector.register("config", ConfigLib.Configuration); testInjector.register("logger", LoggerLib.Logger); - testInjector.register("lockfile", {}); testInjector.register("errors", ErrorsLib.Errors); testInjector.register("options", OptionsLib.Options); testInjector.register("fs", FsLib.FileSystem); @@ -63,7 +62,7 @@ interface ITestData { } describe("Npm installation manager tests", () => { - let testData: IDictionary = { + const testData: IDictionary = { "when there's only one available version and it matches CLI's version": { versions: ["1.4.0"], packageLatestVersion: "1.4.0", @@ -146,24 +145,30 @@ describe("Npm installation manager tests", () => { packageLatestVersion: "1.4.0", cliVersion: "1.6.0-2016-10-01-182", expectedResult: "1.4.0" + }, + "When CLI Version has patch version larger than an existing package, should return max compliant package from the same major.minor version": { + versions: ["1.0.0", "1.0.1", "1.4.0", "2.5.0", "2.5.1", "2.5.2", "3.0.0"], + packageLatestVersion: "3.0.0", + cliVersion: "2.5.4", + expectedResult: "2.5.2" } }; _.each(testData, (currentTestData: ITestData, testName: string) => { it(`returns correct latest compatible version, ${testName}`, async () => { - let testInjector = createTestInjector(); + const testInjector = createTestInjector(); mockNpm(testInjector, currentTestData.versions, currentTestData.packageLatestVersion); // Mock staticConfig.version - let staticConfig = testInjector.resolve("staticConfig"); + const staticConfig = testInjector.resolve("staticConfig"); staticConfig.version = currentTestData.cliVersion; // Mock npmInstallationManager.getLatestVersion - let npmInstallationManager = testInjector.resolve("npmInstallationManager"); + const npmInstallationManager = testInjector.resolve("npmInstallationManager"); npmInstallationManager.getLatestVersion = (packageName: string) => Promise.resolve(currentTestData.packageLatestVersion); - let actualLatestCompatibleVersion = await npmInstallationManager.getLatestCompatibleVersion(""); + const actualLatestCompatibleVersion = await npmInstallationManager.getLatestCompatibleVersion(""); assert.equal(actualLatestCompatibleVersion, currentTestData.expectedResult); }); }); diff --git a/test/npm-support.ts b/test/npm-support.ts index 261cfe9ad0..b4757e924c 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -20,24 +20,23 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; -import { LockFile } from "../lib/lockfile"; import ProjectChangesLib = require("../lib/services/project-changes-service"); import { Messages } from "../lib/common/messages/messages"; +import { NodeModulesDependenciesBuilder } from "../lib/tools/node-modules/node-modules-dependencies-builder"; import path = require("path"); import temp = require("temp"); temp.track(); -let assert = require("chai").assert; -let nodeModulesFolderName = "node_modules"; -let packageJsonName = "package.json"; +const assert = require("chai").assert; +const nodeModulesFolderName = "node_modules"; +const packageJsonName = "package.json"; function createTestInjector(): IInjector { - let testInjector = new yok.Yok(); + const testInjector = new yok.Yok(); testInjector.register("fs", FsLib.FileSystem); testInjector.register("adb", {}); testInjector.register("options", OptionsLib.Options); @@ -48,7 +47,6 @@ function createTestInjector(): IInjector { testInjector.register("platformService", PlatformServiceLib.PlatformService); testInjector.register("logger", stubs.LoggerStub); testInjector.register("npmInstallationManager", {}); - testInjector.register("lockfile", LockFile); testInjector.register("prompter", {}); testInjector.register("sysInfo", {}); testInjector.register("androidProjectService", {}); @@ -73,7 +71,6 @@ function createTestInjector(): IInjector { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); @@ -81,23 +78,26 @@ function createTestInjector(): IInjector { testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService); testInjector.register("analyticsService", { - track: async () => undefined + track: async (): Promise => undefined }); testInjector.register("messages", Messages); + testInjector.register("nodeModulesDependenciesBuilder", NodeModulesDependenciesBuilder); + + testInjector.register("devicePathProvider", {}); return testInjector; } function createProject(testInjector: IInjector, dependencies?: any): string { - let tempFolder = temp.mkdirSync("npmSupportTests"); - let options = testInjector.resolve("options"); + const tempFolder = temp.mkdirSync("npmSupportTests"); + const options = testInjector.resolve("options"); options.path = tempFolder; dependencies = dependencies || { "lodash": "3.9.3" }; - let packageJsonData: any = { + const packageJsonData: any = { "name": "testModuleName", "version": "0.1.0", "nativescript": { @@ -119,27 +119,27 @@ function createProject(testInjector: IInjector, dependencies?: any): string { } async function setupProject(dependencies?: any): Promise { - let testInjector = createTestInjector(); - let projectFolder = createProject(testInjector, dependencies); + const testInjector = createTestInjector(); + const projectFolder = createProject(testInjector, dependencies); - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); // Creates app folder - let appFolderPath = path.join(projectFolder, "app"); + const appFolderPath = path.join(projectFolder, "app"); fs.createDirectory(appFolderPath); - let appResourcesFolderPath = path.join(appFolderPath, "App_Resources"); + const appResourcesFolderPath = path.join(appFolderPath, "App_Resources"); fs.createDirectory(appResourcesFolderPath); fs.createDirectory(path.join(appResourcesFolderPath, "Android")); fs.createDirectory(path.join(appResourcesFolderPath, "Android", "mockdir")); fs.createDirectory(path.join(appFolderPath, "tns_modules")); // Creates platforms/android folder - let androidFolderPath = path.join(projectFolder, "platforms", "android"); + const androidFolderPath = path.join(projectFolder, "platforms", "android"); fs.ensureDirectoryExists(androidFolderPath); // Mock platform data - let appDestinationFolderPath = path.join(androidFolderPath, "assets"); - let platformsData = testInjector.resolve("platformsData"); + const appDestinationFolderPath = path.join(androidFolderPath, "assets"); + const platformsData = testInjector.resolve("platformsData"); platformsData.getPlatformData = (platform: string) => { return { @@ -159,7 +159,9 @@ async function setupProject(dependencies?: any): Promise { ensureConfigurationFileInAppResources: (): any => null, interpolateConfigurationFile: (): void => undefined, isPlatformPrepared: (projectRoot: string) => false, - validatePlugins: (projectData: IProjectData) => Promise.resolve() + validatePlugins: (projectData: IProjectData) => Promise.resolve(), + checkForChanges: () => { /* */ }, + cleanProject: () => Promise.resolve() } }; }; @@ -172,15 +174,15 @@ async function setupProject(dependencies?: any): Promise { } async function addDependencies(testInjector: IInjector, projectFolder: string, dependencies: any, devDependencies?: any): Promise { - let fs = testInjector.resolve("fs"); - let packageJsonPath = path.join(projectFolder, "package.json"); - let packageJsonData = fs.readJson(packageJsonPath); + const fs = testInjector.resolve("fs"); + const packageJsonPath = path.join(projectFolder, "package.json"); + const packageJsonData = fs.readJson(packageJsonPath); - let currentDependencies = packageJsonData.dependencies; + const currentDependencies = packageJsonData.dependencies; _.extend(currentDependencies, dependencies); if (devDependencies) { - let currentDevDependencies = packageJsonData.devDependencies; + const currentDevDependencies = packageJsonData.devDependencies; _.extend(currentDevDependencies, devDependencies); } fs.writeJson(packageJsonPath, packageJsonData); @@ -192,19 +194,19 @@ async function preparePlatform(testInjector: IInjector): Promise { projectData.initializeProjectData(); const options: IOptions = testInjector.resolve("options"); - await platformService.preparePlatform("android", { bundle: options.bundle, release: options.release }, "", projectData, { provision: options.provision, sdk: options.sdk }); + await platformService.preparePlatform("android", { bundle: options.bundle, release: options.release }, "", projectData, options); } describe("Npm support tests", () => { let testInjector: IInjector, projectFolder: string, appDestinationFolderPath: string; beforeEach(async () => { - let projectSetup = await setupProject(); + const projectSetup = await setupProject(); testInjector = projectSetup.testInjector; projectFolder = projectSetup.projectFolder; appDestinationFolderPath = projectSetup.appDestinationFolderPath; }); it("Ensures that the installed dependencies are prepared correctly", async () => { - let fs: IFileSystem = testInjector.resolve("fs"); + const fs: IFileSystem = testInjector.resolve("fs"); // Setup await addDependencies(testInjector, projectFolder, { "bplist": "0.0.4" }); @@ -212,9 +214,9 @@ describe("Npm support tests", () => { await preparePlatform(testInjector); // Assert - let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + const tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); - let results = fs.enumerateFilesInDirectorySync(tnsModulesFolderPath, (file, stat) => { + const results = fs.enumerateFilesInDirectorySync(tnsModulesFolderPath, (file, stat) => { return true; }, { enumerateDirectories: true }); @@ -225,38 +227,38 @@ describe("Npm support tests", () => { }); it("Ensures that scoped dependencies are prepared correctly", async () => { // Setup - let fs = testInjector.resolve("fs"); - let scopedName = "@reactivex/rxjs"; - let dependencies: any = {}; + const fs = testInjector.resolve("fs"); + const scopedName = "@reactivex/rxjs"; + const dependencies: any = {}; dependencies[scopedName] = "0.0.0-prealpha.3"; // Do not pass dependencies object as the sinopia cannot work with scoped dependencies. Instead move them manually. await addDependencies(testInjector, projectFolder, dependencies); // Act await preparePlatform(testInjector); // Assert - let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); - let scopedDependencyPath = path.join(tnsModulesFolderPath, "@reactivex", "rxjs"); + const tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + const scopedDependencyPath = path.join(tnsModulesFolderPath, "@reactivex", "rxjs"); assert.isTrue(fs.exists(scopedDependencyPath)); }); it("Ensures that scoped dependencies are prepared correctly when are not in root level", async () => { // Setup - let customPluginName = "plugin-with-scoped-dependency"; - let customPluginDirectory = temp.mkdirSync("custom-plugin-directory"); + const customPluginName = "plugin-with-scoped-dependency"; + const customPluginDirectory = temp.mkdirSync("custom-plugin-directory"); - let fs: IFileSystem = testInjector.resolve("fs"); + const fs: IFileSystem = testInjector.resolve("fs"); await fs.unzip(path.join("resources", "test", `${customPluginName}.zip`), customPluginDirectory); await addDependencies(testInjector, projectFolder, { "plugin-with-scoped-dependency": `file:${path.join(customPluginDirectory, customPluginName)}` }); // Act await preparePlatform(testInjector); // Assert - let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); - let results = fs.enumerateFilesInDirectorySync(tnsModulesFolderPath, (file, stat) => { + const tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + const results = fs.enumerateFilesInDirectorySync(tnsModulesFolderPath, (file, stat) => { return true; }, { enumerateDirectories: true }); - let filteredResults = results.filter((val) => { + const filteredResults = results.filter((val) => { return _.endsWith(val, path.join("@scoped-plugin", "inner-plugin")); }); @@ -264,9 +266,9 @@ describe("Npm support tests", () => { }); it("Ensures that tns_modules absent when bundling", async () => { - let fs = testInjector.resolve("fs"); - let options = testInjector.resolve("options"); - let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + const fs = testInjector.resolve("fs"); + const options = testInjector.resolve("options"); + const tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); try { options.bundle = false; @@ -288,12 +290,12 @@ describe("Npm support tests", () => { describe("Flatten npm modules tests", () => { it("Doesn't handle the dependencies of devDependencies", async () => { - let projectSetup = await setupProject({}); - let testInjector = projectSetup.testInjector; - let projectFolder = projectSetup.projectFolder; - let appDestinationFolderPath = projectSetup.appDestinationFolderPath; + const projectSetup = await setupProject({}); + const testInjector = projectSetup.testInjector; + const projectFolder = projectSetup.projectFolder; + const appDestinationFolderPath = projectSetup.appDestinationFolderPath; - let devDependencies = { + const devDependencies = { "gulp": "3.9.0", "gulp-jscs": "1.6.0", "gulp-jshint": "1.11.0" @@ -304,32 +306,32 @@ describe("Flatten npm modules tests", () => { await preparePlatform(testInjector); // Assert - let fs = testInjector.resolve("fs"); - let tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); + const fs = testInjector.resolve("fs"); + const tnsModulesFolderPath = path.join(appDestinationFolderPath, "app", "tns_modules"); - let gulpFolderPath = path.join(tnsModulesFolderPath, "gulp"); + const gulpFolderPath = path.join(tnsModulesFolderPath, "gulp"); assert.isFalse(fs.exists(gulpFolderPath)); - let gulpJscsFolderPath = path.join(tnsModulesFolderPath, "gulp-jscs"); + const gulpJscsFolderPath = path.join(tnsModulesFolderPath, "gulp-jscs"); assert.isFalse(fs.exists(gulpJscsFolderPath)); - let gulpJshint = path.join(tnsModulesFolderPath, "gulp-jshint"); + const gulpJshint = path.join(tnsModulesFolderPath, "gulp-jshint"); assert.isFalse(fs.exists(gulpJshint)); - // Get all gulp dependencies - let gulpJsonContent = fs.readJson(path.join(projectFolder, nodeModulesFolderName, "gulp", packageJsonName)); + // Get all gulp dependencies + const gulpJsonContent = fs.readJson(path.join(projectFolder, nodeModulesFolderName, "gulp", packageJsonName)); _.each(_.keys(gulpJsonContent.dependencies), dependency => { assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency))); }); // Get all gulp-jscs dependencies - let gulpJscsJsonContent = fs.readJson(path.join(projectFolder, nodeModulesFolderName, "gulp-jscs", packageJsonName)); + const gulpJscsJsonContent = fs.readJson(path.join(projectFolder, nodeModulesFolderName, "gulp-jscs", packageJsonName)); _.each(_.keys(gulpJscsJsonContent.dependencies), dependency => { assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency))); }); // Get all gulp-jshint dependencies - let gulpJshintJsonContent = fs.readJson(path.join(projectFolder, nodeModulesFolderName, "gulp-jshint", packageJsonName)); + const gulpJshintJsonContent = fs.readJson(path.join(projectFolder, nodeModulesFolderName, "gulp-jshint", packageJsonName)); _.each(_.keys(gulpJshintJsonContent.dependencies), dependency => { assert.isFalse(fs.exists(path.join(tnsModulesFolderPath, dependency))); }); diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 202f0d8c45..07d11e6430 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -15,7 +15,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -28,11 +27,15 @@ let isCommandExecuted = true; class PlatformData implements IPlatformData { frameworkPackageName = "tns-android"; normalizedPlatformName = "Android"; - platformProjectService: IPlatformProjectService = null; + platformProjectService: IPlatformProjectService = { + validate: async (projectData: IProjectData): Promise => { + // intentionally left blank + } + }; emulatorServices: Mobile.IEmulatorPlatformServices = null; projectRoot = ""; deviceBuildOutputPath = ""; - getValidPackageNames = (buildOptions: {isForDevice?: boolean, isReleaseBuild?: boolean}) => [""]; + getValidPackageNames = (buildOptions: { isForDevice?: boolean, isReleaseBuild?: boolean }) => [""]; validPackageNamesForDevice: string[] = []; frameworkFilesExtensions = [".jar", ".dat"]; appDestinationDirectoryPath = ""; @@ -43,11 +46,11 @@ class PlatformData implements IPlatformData { class ErrorsNoFailStub implements IErrors { printCallStack: boolean = false; - fail(formatStr: string, ...args: any[]): void; - fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): void; + fail(formatStr: string, ...args: any[]): never; + fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): never; - fail(...args: any[]) { throw new Error(); } - failWithoutHelp(message: string, ...args: any[]): void { + fail(...args: any[]): never { throw new Error(); } + failWithoutHelp(message: string, ...args: any[]): never { throw new Error(); } @@ -88,11 +91,12 @@ class PlatformsData implements IPlatformsData { } function createTestInjector() { - let testInjector = new yok.Yok(); + const testInjector = new yok.Yok(); testInjector.register("injector", testInjector); testInjector.register("hooksService", stubs.HooksServiceStub); testInjector.register("staticConfig", StaticConfigLib.StaticConfig); + testInjector.register("nodeModulesDependenciesBuilder", {}); testInjector.register('platformService', PlatformServiceLib.PlatformService); testInjector.register('errors', ErrorsNoFailStub); testInjector.register('logger', stubs.LoggerStub); @@ -108,7 +112,6 @@ function createTestInjector() { testInjector.registerCommand("platform|remove", PlatformRemoveCommandLib.RemovePlatformCommand); testInjector.registerCommand("platform|update", PlatformUpdateCommandLib.UpdatePlatformCommand); testInjector.registerCommand("platform|clean", PlatformCleanCommandLib.CleanCommand); - testInjector.register("lockfile", {}); testInjector.register("resources", {}); testInjector.register("commandsServiceProvider", { registerDynamicSubCommands: () => { /* intentionally left blank */ } @@ -122,7 +125,7 @@ function createTestInjector() { prepareNodeModulesFolder: () => { /* intentionally left blank */ } }); testInjector.register("pluginsService", { - getAllInstalledPlugins: async () => [] + getAllInstalledPlugins: async (): Promise => [] }); testInjector.register("projectFilesManager", ProjectFilesManagerLib.ProjectFilesManager); testInjector.register("hooksService", stubs.HooksServiceStub); @@ -131,7 +134,6 @@ function createTestInjector() { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); @@ -140,9 +142,10 @@ function createTestInjector() { testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService); testInjector.register("analyticsService", { - track: async () => undefined + track: async () => async (): Promise => undefined }); testInjector.register("messages", Messages); + testInjector.register("devicePathProvider", {}); return testInjector; } @@ -434,8 +437,8 @@ describe('Platform Service Tests', () => { }); it("will call removePlatform and addPlatform on the platformService passing the provided platforms", async () => { - let platformActions: { action: string, platforms: string[] }[] = []; - let cleanCommand = testInjector.resolveCommand("platform|clean"); + const platformActions: { action: string, platforms: string[] }[] = []; + const cleanCommand = testInjector.resolveCommand("platform|clean"); platformService.removePlatforms = async (platforms: string[]) => { platformActions.push({ action: "removePlatforms", platforms }); @@ -449,7 +452,7 @@ describe('Platform Service Tests', () => { await cleanCommand.execute(["ios"]); - let expectedPlatformActions = [ + const expectedPlatformActions = [ { action: "removePlatforms", platforms: ["ios"] }, { action: "addPlatforms", platforms: ["ios"] }, ]; diff --git a/test/platform-service.ts b/test/platform-service.ts index 7d7216d184..5c12c376fa 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -13,7 +13,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -22,15 +21,16 @@ import ProjectChangesLib = require("../lib/services/project-changes-service"); import { Messages } from "../lib/common/messages/messages"; require("should"); -let temp = require("temp"); +const temp = require("temp"); temp.track(); function createTestInjector() { - let testInjector = new yok.Yok(); + const testInjector = new yok.Yok(); testInjector.register('platformService', PlatformServiceLib.PlatformService); testInjector.register('errors', stubs.ErrorsStub); testInjector.register('logger', stubs.LoggerStub); + testInjector.register("nodeModulesDependenciesBuilder", {}); testInjector.register('npmInstallationManager', stubs.NpmInstallationManagerStub); testInjector.register('projectData', stubs.ProjectDataStub); testInjector.register('platformsData', stubs.PlatformsDataStub); @@ -39,7 +39,6 @@ function createTestInjector() { testInjector.register('projectDataService', stubs.ProjectDataService); testInjector.register('prompter', {}); testInjector.register('sysInfo', {}); - testInjector.register('lockfile', stubs.LockFile); testInjector.register("commandsService", { tryExecuteCommand: () => { /* intentionally left blank */ } }); @@ -49,6 +48,9 @@ function createTestInjector() { testInjector.register("nodeModulesBuilder", { prepareNodeModules: () => { return Promise.resolve(); + }, + prepareJSNodeModules: () => { + return Promise.resolve(); } }); testInjector.register("pluginsService", { @@ -69,7 +71,6 @@ function createTestInjector() { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("xmlValidator", XmlValidator); @@ -82,15 +83,77 @@ function createTestInjector() { testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService); testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService); testInjector.register("analyticsService", { - track: async () => undefined + track: async (): Promise => undefined }); testInjector.register("messages", Messages); + testInjector.register("devicePathProvider", {}); return testInjector; } +class CreatedTestData { + files: string[]; + + resources: { + ios: string[], + android: string[] + }; + + testDirData: { + tempFolder: string, + appFolderPath: string, + app1FolderPath: string, + appDestFolderPath: string, + appResourcesFolderPath: string + }; + + constructor() { + this.files = []; + this.resources = { + ios: [], + android: [] + }; + + this.testDirData = { + tempFolder: "", + appFolderPath: "", + app1FolderPath: "", + appDestFolderPath: "", + appResourcesFolderPath: "" + }; + } +} + +class DestinationFolderVerifier { + static verify(data: any, fs: IFileSystem) { + _.forOwn(data, (folder, folderRoot) => { + _.each(folder.filesWithContent || [], (file) => { + const filePath = path.join(folderRoot, file.name); + assert.isTrue(fs.exists(filePath), `Expected file ${filePath} to be present.`); + assert.equal(fs.readFile(filePath).toString(), file.content, `File content for ${filePath} doesn't match.`); + }); + + _.each(folder.missingFiles || [], (file) => { + assert.isFalse(fs.exists(path.join(folderRoot, file)), `Expected file ${file} to be missing.`); + }); + + _.each(folder.presentFiles || [], (file) => { + assert.isTrue(fs.exists(path.join(folderRoot, file)), `Expected file ${file} to be present.`); + }); + }); + } +} + describe('Platform Service Tests', () => { let platformService: IPlatformService, testInjector: IInjector; + const config: IPlatformOptions = { + ignoreScripts: false, + provision: null, + teamId: null, + sdk: null, + frameworkPath: null + }; + beforeEach(() => { testInjector = createTestInjector(); testInjector.register("fs", stubs.FileSystemStub); @@ -100,96 +163,95 @@ describe('Platform Service Tests', () => { describe("add platform unit tests", () => { describe("#add platform()", () => { it("should not fail if platform is not normalized", async () => { - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let projectData: IProjectData = testInjector.resolve("projectData"); - - await platformService.addPlatforms(["Android"], "", projectData, null); - await platformService.addPlatforms(["ANDROID"], "", projectData, null); - await platformService.addPlatforms(["AnDrOiD"], "", projectData, null); - await platformService.addPlatforms(["androiD"], "", projectData, null); - - await platformService.addPlatforms(["iOS"], "", projectData, null); - await platformService.addPlatforms(["IOS"], "", projectData, null); - await platformService.addPlatforms(["IoS"], "", projectData, null); - await platformService.addPlatforms(["iOs"], "", projectData, null); + const projectData: IProjectData = testInjector.resolve("projectData"); + await platformService.addPlatforms(["Android"], "", projectData, config); + await platformService.addPlatforms(["ANDROID"], "", projectData, config); + await platformService.addPlatforms(["AnDrOiD"], "", projectData, config); + await platformService.addPlatforms(["androiD"], "", projectData, config); + + await platformService.addPlatforms(["iOS"], "", projectData, config); + await platformService.addPlatforms(["IOS"], "", projectData, config); + await platformService.addPlatforms(["IoS"], "", projectData, config); + await platformService.addPlatforms(["iOs"], "", projectData, config); }); it("should fail if platform is already installed", async () => { - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); // By default fs.exists returns true, so the platforms directory should exists - await assert.isRejected(platformService.addPlatforms(["android"], "", projectData, null)); - await assert.isRejected(platformService.addPlatforms(["ios"], "", projectData, null)); + await assert.isRejected(platformService.addPlatforms(["android"], "", projectData, config)); + await assert.isRejected(platformService.addPlatforms(["ios"], "", projectData, config)); }); it("should fail if npm is unavalible", async () => { - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let errorMessage = "Npm is unavalible"; - let npmInstallationManager = testInjector.resolve("npmInstallationManager"); + const errorMessage = "Npm is unavalible"; + const npmInstallationManager = testInjector.resolve("npmInstallationManager"); npmInstallationManager.install = () => { throw new Error(errorMessage); }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); try { - await platformService.addPlatforms(["android"], "", projectData, null); + await platformService.addPlatforms(["android"], "", projectData, config); } catch (err) { assert.equal(errorMessage, err.message); } }); it("should respect platform version in package.json's nativescript key", async () => { const versionString = "2.5.0"; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let nsValueObject: any = {}; + const nsValueObject: any = {}; nsValueObject[VERSION_STRING] = versionString; - let projectDataService = testInjector.resolve("projectDataService"); + const projectDataService = testInjector.resolve("projectDataService"); projectDataService.getNSValue = () => nsValueObject; - let npmInstallationManager = testInjector.resolve("npmInstallationManager"); + const npmInstallationManager = testInjector.resolve("npmInstallationManager"); npmInstallationManager.install = (packageName: string, packageDir: string, options: INpmInstallOptions) => { assert.deepEqual(options.version, versionString); return ""; }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); - await platformService.addPlatforms(["android"], "", projectData, null); - await platformService.addPlatforms(["ios"], "", projectData, null); + await platformService.addPlatforms(["android"], "", projectData, config); + await platformService.addPlatforms(["ios"], "", projectData, config); }); it("should install latest platform if no information found in package.json's nativescript key", async () => { - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let projectDataService = testInjector.resolve("projectDataService"); + const projectDataService = testInjector.resolve("projectDataService"); projectDataService.getNSValue = (): any => null; - let npmInstallationManager = testInjector.resolve("npmInstallationManager"); + const npmInstallationManager = testInjector.resolve("npmInstallationManager"); npmInstallationManager.install = (packageName: string, packageDir: string, options: INpmInstallOptions) => { assert.deepEqual(options.version, undefined); return ""; }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); - await platformService.addPlatforms(["android"], "", projectData, null); - await platformService.addPlatforms(["ios"], "", projectData, null); + await platformService.addPlatforms(["android"], "", projectData, config); + await platformService.addPlatforms(["ios"], "", projectData, config); }); }); describe("#add platform(ios)", () => { it("should call validate method", async () => { - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let errorMessage = "Xcode is not installed or Xcode version is smaller that 5.0"; - let platformsData = testInjector.resolve("platformsData"); - let platformProjectService = platformsData.getPlatformData().platformProjectService; - let projectData: IProjectData = testInjector.resolve("projectData"); + const errorMessage = "Xcode is not installed or Xcode version is smaller that 5.0"; + const platformsData = testInjector.resolve("platformsData"); + const platformProjectService = platformsData.getPlatformData().platformProjectService; + const projectData: IProjectData = testInjector.resolve("projectData"); platformProjectService.validate = () => { throw new Error(errorMessage); }; try { - await platformService.addPlatforms(["ios"], "", projectData, null); + await platformService.addPlatforms(["ios"], "", projectData, config); } catch (err) { assert.equal(errorMessage, err.message); } @@ -197,19 +259,19 @@ describe('Platform Service Tests', () => { }); describe("#add platform(android)", () => { it("should fail if java, ant or android are not installed", async () => { - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let errorMessage = "Java, ant or android are not installed"; - let platformsData = testInjector.resolve("platformsData"); - let platformProjectService = platformsData.getPlatformData().platformProjectService; + const errorMessage = "Java, ant or android are not installed"; + const platformsData = testInjector.resolve("platformsData"); + const platformProjectService = platformsData.getPlatformData().platformProjectService; platformProjectService.validate = () => { throw new Error(errorMessage); }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); try { - await platformService.addPlatforms(["android"], "", projectData, null); + await platformService.addPlatforms(["android"], "", projectData, config); } catch (err) { assert.equal(errorMessage, err.message); } @@ -221,7 +283,7 @@ describe('Platform Service Tests', () => { it("should fail when platforms are not added", async () => { const ExpectedErrorsCaught = 2; let errorsCaught = 0; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); testInjector.resolve("fs").exists = () => false; try { @@ -239,9 +301,9 @@ describe('Platform Service Tests', () => { assert.isTrue(errorsCaught === ExpectedErrorsCaught); }); it("shouldn't fail when platforms are added", async () => { - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); testInjector.resolve("fs").exists = () => false; - await platformService.addPlatforms(["android"], "", projectData, null); + await platformService.addPlatforms(["android"], "", projectData, config); testInjector.resolve("fs").exists = () => true; await platformService.removePlatforms(["android"], projectData); @@ -251,30 +313,30 @@ describe('Platform Service Tests', () => { describe("clean platform unit tests", () => { it("should preserve the specified in the project nativescript version", async () => { const versionString = "2.4.1"; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.exists = () => false; - let nsValueObject: any = {}; + const nsValueObject: any = {}; nsValueObject[VERSION_STRING] = versionString; - let projectDataService = testInjector.resolve("projectDataService"); + const projectDataService = testInjector.resolve("projectDataService"); projectDataService.getNSValue = () => nsValueObject; - let npmInstallationManager = testInjector.resolve("npmInstallationManager"); + const npmInstallationManager = testInjector.resolve("npmInstallationManager"); npmInstallationManager.install = (packageName: string, packageDir: string, options: INpmInstallOptions) => { assert.deepEqual(options.version, versionString); return ""; }; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); platformService.removePlatforms = (platforms: string[], prjctData: IProjectData): Promise => { nsValueObject[VERSION_STRING] = undefined; return Promise.resolve(); }; - await platformService.cleanPlatforms(["android"], "", projectData, null); + await platformService.cleanPlatforms(["android"], "", projectData, config); nsValueObject[VERSION_STRING] = versionString; - await platformService.cleanPlatforms(["ios"], "", projectData, null); + await platformService.cleanPlatforms(["ios"], "", projectData, config); }); }); @@ -288,9 +350,9 @@ describe('Platform Service Tests', () => { describe("update Platform", () => { describe("#updatePlatform(platform)", () => { it("should fail when the versions are the same", async () => { - let npmInstallationManager: INpmInstallationManager = testInjector.resolve("npmInstallationManager"); + const npmInstallationManager: INpmInstallationManager = testInjector.resolve("npmInstallationManager"); npmInstallationManager.getLatestVersion = async () => "0.2.0"; - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); await assert.isRejected(platformService.updatePlatforms(["android"], "", projectData, null)); }); @@ -307,47 +369,42 @@ describe('Platform Service Tests', () => { }); function prepareDirStructure() { - let tempFolder = temp.mkdirSync("prepare platform"); + const tempFolder = temp.mkdirSync("prepare_platform"); - let appFolderPath = path.join(tempFolder, "app"); + const appFolderPath = path.join(tempFolder, "app"); fs.createDirectory(appFolderPath); - let testsFolderPath = path.join(appFolderPath, "tests"); + const nodeModulesPath = path.join(tempFolder, "node_modules"); + fs.createDirectory(nodeModulesPath); + + const testsFolderPath = path.join(appFolderPath, "tests"); fs.createDirectory(testsFolderPath); - let app1FolderPath = path.join(tempFolder, "app1"); + const app1FolderPath = path.join(tempFolder, "app1"); fs.createDirectory(app1FolderPath); - let appDestFolderPath = path.join(tempFolder, "appDest"); - let appResourcesFolderPath = path.join(appDestFolderPath, "App_Resources"); + const appDestFolderPath = path.join(tempFolder, "appDest"); + const appResourcesFolderPath = path.join(appDestFolderPath, "App_Resources"); + fs.writeJson(path.join(tempFolder, "package.json"), { + name: "testname", + nativescript: { + id: "org.nativescript.testname" + } + }); return { tempFolder, appFolderPath, app1FolderPath, appDestFolderPath, appResourcesFolderPath }; } - async function testPreparePlatform(platformToTest: string, release?: boolean) { - let testDirData = prepareDirStructure(); - - // Add platform specific files to app and app1 folders - let platformSpecificFiles = [ - "test1.ios.js", "test1-ios-js", "test2.android.js", "test2-android-js" - ]; - - let destinationDirectories = [testDirData.appFolderPath, testDirData.app1FolderPath]; - - _.each(destinationDirectories, directoryPath => { - _.each(platformSpecificFiles, filePath => { - let fileFullPath = path.join(directoryPath, filePath); - fs.writeFile(fileFullPath, "testData"); - }); - }); - - let platformsData = testInjector.resolve("platformsData"); + async function execPreparePlatform(platformToTest: string, testDirData: any, + release?: boolean) { + const platformsData = testInjector.resolve("platformsData"); platformsData.platformsNames = ["ios", "android"]; platformsData.getPlatformData = (platform: string) => { return { appDestinationDirectoryPath: testDirData.appDestFolderPath, appResourcesDestinationDirectoryPath: testDirData.appResourcesFolderPath, normalizedPlatformName: platformToTest, + configurationFileName: platformToTest === "ios" ? "Info.plist" : "AndroidManifest.xml", projectRoot: testDirData.tempFolder, platformProjectService: { prepareProject: (): any => null, @@ -355,24 +412,75 @@ describe('Platform Service Tests', () => { createProject: (projectRoot: string, frameworkDir: string) => Promise.resolve(), interpolateData: (projectRoot: string) => Promise.resolve(), afterCreateProject: (projectRoot: string): any => null, - getAppResourcesDestinationDirectoryPath: () => "", + getAppResourcesDestinationDirectoryPath: (projectData: IProjectData, frameworkVersion?: string): string => { + if (platform.toLowerCase() === "ios") { + const dirPath = path.join(testDirData.appDestFolderPath, "Resources"); + fs.ensureDirectoryExists(dirPath); + return dirPath; + } else { + const dirPath = path.join(testDirData.appDestFolderPath, "src", "main", "res"); + fs.ensureDirectoryExists(dirPath); + return dirPath; + } + }, processConfigurationFilesFromAppResources: () => Promise.resolve(), ensureConfigurationFileInAppResources: (): any => null, interpolateConfigurationFile: (): void => undefined, - isPlatformPrepared: (projectRoot: string) => false + isPlatformPrepared: (projectRoot: string) => false, + prepareAppResources: (appResourcesDirectoryPath: string, projectData: IProjectData): void => undefined, + checkForChanges: () => { /* */ } } }; }; - let projectData = testInjector.resolve("projectData"); + const projectData = testInjector.resolve("projectData"); projectData.projectDir = testDirData.tempFolder; + projectData.appDirectoryPath = testDirData.appFolderPath; + projectData.appResourcesDirectoryPath = path.join(testDirData.appFolderPath, "App_Resources"); + projectData.projectName = "app"; platformService = testInjector.resolve("platformService"); const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: false, release: release }; - await platformService.preparePlatform(platformToTest, appFilesUpdaterOptions, "", projectData, { provision: null, sdk: null }); + await platformService.preparePlatform(platformToTest, appFilesUpdaterOptions, "", projectData, { provision: null, teamId: null, sdk: null, frameworkPath: null, ignoreScripts: false }); + } - let test1FileName = platformToTest.toLowerCase() === "ios" ? "test1.js" : "test2.js"; - let test2FileName = platformToTest.toLowerCase() === "ios" ? "test2.js" : "test1.js"; + async function testPreparePlatform(platformToTest: string, release?: boolean): Promise { + const testDirData = prepareDirStructure(); + const created: CreatedTestData = new CreatedTestData(); + created.testDirData = testDirData; + + // Add platform specific files to app and app1 folders + const platformSpecificFiles = [ + "test1.ios.js", "test1-ios-js", "test2.android.js", "test2-android-js", + "main.js" + ]; + + const destinationDirectories = [testDirData.appFolderPath, testDirData.app1FolderPath]; + + _.each(destinationDirectories, directoryPath => { + _.each(platformSpecificFiles, filePath => { + const fileFullPath = path.join(directoryPath, filePath); + fs.writeFile(fileFullPath, "testData"); + + created.files.push(fileFullPath); + }); + }); + + // Add App_Resources file to app and app1 folders + _.each(destinationDirectories, directoryPath => { + const iosIconFullPath = path.join(directoryPath, "App_Resources/iOS/icon.png"); + fs.writeFile(iosIconFullPath, "test-image"); + created.resources.ios.push(iosIconFullPath); + + const androidFullPath = path.join(directoryPath, "App_Resources/Android/icon.png"); + fs.writeFile(androidFullPath, "test-image"); + created.resources.android.push(androidFullPath); + }); + + await execPreparePlatform(platformToTest, testDirData, release); + + const test1FileName = platformToTest.toLowerCase() === "ios" ? "test1.js" : "test2.js"; + const test2FileName = platformToTest.toLowerCase() === "ios" ? "test2.js" : "test1.js"; // Asserts that the files in app folder are process as platform specific assert.isTrue(fs.exists(path.join(testDirData.appDestFolderPath, "app", test1FileName))); @@ -388,6 +496,13 @@ describe('Platform Service Tests', () => { // Asserts that the files in tests folder aren't copied assert.isFalse(fs.exists(path.join(testDirData.appDestFolderPath, "tests")), "Asserts that the files in tests folder aren't copied"); } + + return created; + } + + function updateFile(files: string[], fileName: string, content: string) { + const fileToUpdate = _.find(files, (f) => f.indexOf(fileName) !== -1); + fs.writeFile(fileToUpdate, content); } it("should process only files in app folder when preparing for iOS platform", async () => { @@ -406,15 +521,320 @@ describe('Platform Service Tests', () => { await testPreparePlatform("Android", true); }); + function getDefaultFolderVerificationData(platform: string, appDestFolderPath: string) { + const data: any = {}; + if (platform.toLowerCase() === "ios") { + data[path.join(appDestFolderPath, "app")] = { + missingFiles: ["test1.ios.js", "test2.android.js", "test2.js", "App_Resources"], + presentFiles: ["test1.js", "test2-android-js", "test1-ios-js", "main.js"] + }; + + data[appDestFolderPath] = { + filesWithContent: [ + { + name: "Resources/icon.png", + content: "test-image" + } + ] + }; + } else { + data[path.join(appDestFolderPath, "app")] = { + missingFiles: ["test1.android.js", "test2.ios.js", "test1.js"], + presentFiles: ["test2.js", "test2-android-js", "test1-ios-js"] + }; + + data[appDestFolderPath] = { + filesWithContent: [ + { + name: "src/main/res/icon.png", + content: "test-image" + } + ] + }; + } + + return data; + } + + function mergeModifications(def: any, mod: any) { + // custom merge to reflect changes + const merged: any = _.cloneDeep(def); + _.forOwn(mod, (modFolder, folderRoot) => { + // whole folder not present in Default + if (!def.hasOwnProperty(folderRoot)) { + merged[folderRoot] = _.cloneDeep(modFolder[folderRoot]); + } else { + const defFolder = def[folderRoot]; + merged[folderRoot].filesWithContent = _.merge(defFolder.filesWithContent || [], modFolder.filesWithContent || []); + merged[folderRoot].missingFiles = (defFolder.missingFiles || []).concat(modFolder.missingFiles || []); + merged[folderRoot].presentFiles = (defFolder.presentFiles || []).concat(modFolder.presentFiles || []); + + // remove the missingFiles from the presentFiles if they were initially there + if (modFolder.missingFiles) { + merged[folderRoot].presentFiles = _.difference(defFolder.presentFiles, modFolder.missingFiles); + } + + // remove the presentFiles from the missingFiles if they were initially there. + if (modFolder.presentFiles) { + merged[folderRoot].missingFiles = _.difference(defFolder.presentFiles, modFolder.presentFiles); + } + } + }); + + return merged; + } + + // Executes a changes test case: + // 1. Executes Prepare Platform for the Platform + // 2. Applies some changes to the App. Persists the expected Modifications + // 3. Executes again Prepare Platform for the Platform + // 4. Gets the Default Destination App Structure and merges it with the Modifications + // 5. Asserts the Destination App matches our expectations + async function testChangesApplied(platform: string, applyChangesFn: (createdTestData: CreatedTestData) => any) { + const createdTestData = await testPreparePlatform(platform); + + const modifications = applyChangesFn(createdTestData); + + await execPreparePlatform(platform, createdTestData.testDirData); + + const defaultStructure = getDefaultFolderVerificationData(platform, createdTestData.testDirData.appDestFolderPath); + + const merged = mergeModifications(defaultStructure, modifications); + + DestinationFolderVerifier.verify(merged, fs); + } + + it("should sync only changed files, without special folders (iOS)", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "updated-content-ios"; + updateFile(createdTestData.files, "test1.ios.js", expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { + filesWithContent: [ + { + name: "test1.js", + content: expectedFileContent + } + ] + }; + return modifications; + }; + await testChangesApplied("iOS", applyChangesFn); + }); + + it("should sync only changed files, without special folders (Android) #2697", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "updated-content-android"; + updateFile(createdTestData.files, "test2.android.js", expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { + filesWithContent: [ + { + name: "test2.js", + content: expectedFileContent + } + ] + }; + return modifications; + }; + await testChangesApplied("Android", applyChangesFn); + }); + + it("Ensure App_Resources get reloaded after change in the app folder (iOS) #2560", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "updated-icon-content"; + const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/iOS/icon.png"); + fs.writeFile(iconPngPath, expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[createdTestData.testDirData.appDestFolderPath] = { + filesWithContent: [ + { + name: "Resources/icon.png", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("iOS", applyChangesFn); + }); + + it("Ensure App_Resources get reloaded after change in the app folder (Android) #2560", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "updated-icon-content"; + const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/Android/icon.png"); + fs.writeFile(iconPngPath, expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[createdTestData.testDirData.appDestFolderPath] = { + filesWithContent: [ + { + name: "src/main/res/icon.png", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("Android", applyChangesFn); + }); + + it("Ensure App_Resources get reloaded after a new file appears in the app folder (iOS) #2560", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "new-file-content"; + const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/iOS/new-file.png"); + fs.writeFile(iconPngPath, expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[createdTestData.testDirData.appDestFolderPath] = { + filesWithContent: [ + { + name: "Resources/new-file.png", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("iOS", applyChangesFn); + }); + + it("Ensure App_Resources get reloaded after a new file appears in the app folder (Android) #2560", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "new-file-content"; + const iconPngPath = path.join(createdTestData.testDirData.appFolderPath, "App_Resources/Android/new-file.png"); + fs.writeFile(iconPngPath, expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[createdTestData.testDirData.appDestFolderPath] = { + filesWithContent: [ + { + name: "src/main/res/new-file.png", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("Android", applyChangesFn); + }); + + it("should sync new platform specific files (iOS)", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "new-content-ios"; + fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.ios.js"), expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { + filesWithContent: [ + { + name: "test3.js", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("iOS", applyChangesFn); + }); + + it("should sync new platform specific files (Android)", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "new-content-android"; + fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.android.js"), expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { + filesWithContent: [ + { + name: "test3.js", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("Android", applyChangesFn); + }); + + it("should sync new common files (iOS)", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "new-content-ios"; + fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.js"), expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { + filesWithContent: [ + { + name: "test3.js", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("iOS", applyChangesFn); + }); + + it("should sync new common file (Android)", async () => { + const applyChangesFn = (createdTestData: CreatedTestData) => { + // apply changes + const expectedFileContent = "new-content-android"; + fs.writeFile(path.join(createdTestData.testDirData.appFolderPath, "test3.js"), expectedFileContent); + + // construct the folder modifications data + const modifications: any = {}; + modifications[path.join(createdTestData.testDirData.appDestFolderPath, "app")] = { + filesWithContent: [ + { + name: "test3.js", + content: expectedFileContent + } + ] + }; + + return modifications; + }; + await testChangesApplied("Android", applyChangesFn); + }); + it("invalid xml is caught", async () => { require("colors"); - let testDirData = prepareDirStructure(); + const testDirData = prepareDirStructure(); // generate invalid xml - let fileFullPath = path.join(testDirData.appFolderPath, "file.xml"); + const fileFullPath = path.join(testDirData.appFolderPath, "file.xml"); fs.writeFile(fileFullPath, ""); - let platformsData = testInjector.resolve("platformsData"); + const platformsData = testInjector.resolve("platformsData"); platformsData.platformsNames = ["android"]; platformsData.getPlatformData = (platform: string) => { return { @@ -432,21 +852,23 @@ describe('Platform Service Tests', () => { processConfigurationFilesFromAppResources: () => Promise.resolve(), ensureConfigurationFileInAppResources: (): any => null, interpolateConfigurationFile: (): void => undefined, - isPlatformPrepared: (projectRoot: string) => false - } + isPlatformPrepared: (projectRoot: string) => false, + checkForChanges: () => { /* */ } + }, + frameworkPackageName: "tns-ios" }; }; - let projectData = testInjector.resolve("projectData"); + const projectData = testInjector.resolve("projectData"); projectData.projectDir = testDirData.tempFolder; platformService = testInjector.resolve("platformService"); - let oldLoggerWarner = testInjector.resolve("$logger").warn; + const oldLoggerWarner = testInjector.resolve("$logger").warn; let warnings: string = ""; try { testInjector.resolve("$logger").warn = (text: string) => warnings += text; const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: false, release: false }; - await platformService.preparePlatform("android", appFilesUpdaterOptions, "", projectData, { provision: null, sdk: null }); + await platformService.preparePlatform("android", appFilesUpdaterOptions, "", projectData, { provision: null, teamId: null, sdk: null, frameworkPath: null, ignoreScripts: false }); } finally { testInjector.resolve("$logger").warn = oldLoggerWarner; } diff --git a/test/plugin-prepare.ts b/test/plugin-prepare.ts index 73bcde4131..bca777f0c8 100644 --- a/test/plugin-prepare.ts +++ b/test/plugin-prepare.ts @@ -7,20 +7,20 @@ class TestNpmPluginPrepare extends NpmPluginPrepare { public preparedDependencies: IDictionary = {}; constructor(private previouslyPrepared: IDictionary) { - super(null, null, null); + super(null, null, null, null); } protected getPreviouslyPreparedDependencies(platform: string): IDictionary { return this.previouslyPrepared; } - protected async beforePrepare(dependencies: IDictionary, platform: string): Promise { - _.values(dependencies).forEach(d => { + protected async beforePrepare(dependencies: IDependencyData[], platform: string): Promise { + _.each(dependencies, d => { this.preparedDependencies[d.name] = true; }); } - protected async afterPrepare(dependencies: IDictionary, platform: string): Promise { + protected async afterPrepare(dependencies: IDependencyData[], platform: string): Promise { // DO NOTHING } } @@ -28,38 +28,41 @@ class TestNpmPluginPrepare extends NpmPluginPrepare { describe("Plugin preparation", () => { it("skips prepare if no plugins", async () => { const pluginPrepare = new TestNpmPluginPrepare({}); - await pluginPrepare.preparePlugins({}, "android", null); + await pluginPrepare.preparePlugins([], "android", null, {}); assert.deepEqual({}, pluginPrepare.preparedDependencies); }); it("skips prepare if every plugin prepared", async () => { const pluginPrepare = new TestNpmPluginPrepare({ "tns-core-modules-widgets": true }); - const testDependencies: IDictionary = { - "0": { + const testDependencies: IDependencyData[] = [ + { name: "tns-core-modules-widgets", - version: "1.0.0", + depth: 0, + directory: "some dir", nativescript: null, } - }; - await pluginPrepare.preparePlugins(testDependencies, "android", null); + ]; + await pluginPrepare.preparePlugins(testDependencies, "android", null, {}); assert.deepEqual({}, pluginPrepare.preparedDependencies); }); it("saves prepared plugins after preparation", async () => { const pluginPrepare = new TestNpmPluginPrepare({ "tns-core-modules-widgets": true }); - const testDependencies: IDictionary = { - "0": { + const testDependencies: IDependencyData[] = [ + { name: "tns-core-modules-widgets", - version: "1.0.0", + depth: 0, + directory: "some dir", nativescript: null, }, - "1": { + { name: "nativescript-calendar", - version: "1.0.0", + depth: 0, + directory: "some dir", nativescript: null, } - }; - await pluginPrepare.preparePlugins(testDependencies, "android", null); + ]; + await pluginPrepare.preparePlugins(testDependencies, "android", null, {}); const prepareData = { "tns-core-modules-widgets": true, "nativescript-calendar": true }; assert.deepEqual(prepareData, pluginPrepare.preparedDependencies); }); diff --git a/test/plugin-variables-service.ts b/test/plugin-variables-service.ts index 3576c4d284..2beb1737e0 100644 --- a/test/plugin-variables-service.ts +++ b/test/plugin-variables-service.ts @@ -17,7 +17,7 @@ import * as temp from "temp"; temp.track(); function createTestInjector(): IInjector { - let testInjector = new Yok(); + const testInjector = new Yok(); testInjector.register("messagesService", MessagesService); testInjector.register("errors", Errors); @@ -32,7 +32,7 @@ function createTestInjector(): IInjector { testInjector.register("projectHelper", ProjectHelper); testInjector.register("prompter", { get: () => { - let errors: IErrors = testInjector.resolve("errors"); + const errors: IErrors = testInjector.resolve("errors"); errors.fail("$prompter.get function shouldn't be called!"); } }); @@ -42,12 +42,12 @@ function createTestInjector(): IInjector { } async function createProjectFile(testInjector: IInjector): Promise { - let tempFolder = temp.mkdirSync("pluginVariablesService"); + const tempFolder = temp.mkdirSync("pluginVariablesService"); - let options = testInjector.resolve("options"); + const options = testInjector.resolve("options"); options.path = tempFolder; - let projectData = { + const projectData = { "name": "myProject", "nativescript": {} }; @@ -57,7 +57,7 @@ async function createProjectFile(testInjector: IInjector): Promise { } function createPluginData(pluginVariables: any): IPluginData { - let pluginData = { + const pluginData = { name: "myTestPlugin", version: "", fullPath: "", @@ -82,19 +82,19 @@ describe("Plugin Variables service", () => { describe("plugin add when the console is non interactive", () => { beforeEach(() => { - let helpers = require("./../lib/common/helpers"); + const helpers = require("./../lib/common/helpers"); helpers.isInteractive = () => false; }); it("fails when no --var option and no default value are specified", async () => { await createProjectFile(testInjector); - let pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); - let expectedError = `Unable to find value for MY_TEST_PLUGIN_VARIABLE plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`; + const expectedError = `Unable to find value for MY_TEST_PLUGIN_VARIABLE plugin variable from ${pluginData.name} plugin. Ensure the --var option is specified or the plugin variable has default value.`; let actualError: string = null; try { @@ -108,126 +108,126 @@ describe("Plugin Variables service", () => { it("does not fail when --var option is specified", async () => { await createProjectFile(testInjector); - let pluginVariableValue = "myAppId"; + const pluginVariableValue = "myAppId"; testInjector.resolve("options").var = { "MY_APP_ID": pluginVariableValue }; - let pluginVariables = { "MY_APP_ID": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const pluginVariables = { "MY_APP_ID": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData); - let fs = testInjector.resolve("fs"); - let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + const fs = testInjector.resolve("fs"); + const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); + const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["MY_APP_ID"]); }); it("does not fail when default value is specified", async () => { await createProjectFile(testInjector); - let defaultPluginValue = "myDefaultValue"; - let pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": { defaultValue: defaultPluginValue } }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData = testInjector.resolve("projectData"); + const defaultPluginValue = "myDefaultValue"; + const pluginVariables = { "MY_TEST_PLUGIN_VARIABLE": { defaultValue: defaultPluginValue } }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData); - let fs = testInjector.resolve("fs"); - let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + const fs = testInjector.resolve("fs"); + const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); + const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); assert.equal(defaultPluginValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["MY_TEST_PLUGIN_VARIABLE"]); }); }); describe("plugin add when the console is interactive", () => { beforeEach(() => { - let helpers = require("./../lib/common/helpers"); + const helpers = require("./../lib/common/helpers"); helpers.isInteractive = () => true; }); it("prompt for plugin variable value when no --var option and no default value are specified", async () => { await createProjectFile(testInjector); - let pluginVariableValue = "testAppURL"; - let prompter = testInjector.resolve("prompter"); + const pluginVariableValue = "testAppURL"; + const prompter = testInjector.resolve("prompter"); prompter.get = async () => ({ "APP_URL": pluginVariableValue }); - let pluginVariables = { "APP_URL": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData = testInjector.resolve("projectData"); + const pluginVariables = { "APP_URL": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData); - let fs = testInjector.resolve("fs"); - let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + const fs = testInjector.resolve("fs"); + const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); + const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["APP_URL"]); }); it("does not prompt for plugin variable value when default value is specified", async () => { await createProjectFile(testInjector); - let defaultPluginValue = "myAppNAme"; - let pluginVariables = { "APP_NAME": { defaultValue: defaultPluginValue } }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData = testInjector.resolve("projectData"); + const defaultPluginValue = "myAppNAme"; + const pluginVariables = { "APP_NAME": { defaultValue: defaultPluginValue } }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData); - let fs = testInjector.resolve("fs"); - let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + const fs = testInjector.resolve("fs"); + const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); + const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); assert.equal(defaultPluginValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["APP_NAME"]); }); it("does not prompt for plugin variable value when --var option is specified", async () => { await createProjectFile(testInjector); - let pluginVariableValue = "pencho.goshko"; + const pluginVariableValue = "pencho.goshko"; testInjector.resolve("options").var = { "USERNAME": pluginVariableValue }; - let pluginVariables = { "USERNAME": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData = testInjector.resolve("projectData"); + const pluginVariables = { "USERNAME": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); await pluginVariablesService.savePluginVariablesInProjectFile(pluginData, projectData); - let fs = testInjector.resolve("fs"); - let staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); + const fs = testInjector.resolve("fs"); + const staticConfig: IStaticConfig = testInjector.resolve("staticConfig"); - let projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); + const projectFileContent = fs.readJson(path.join(projectData.projectDir, "package.json")); assert.equal(pluginVariableValue, projectFileContent[staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE][`${pluginData.name}-variables`]["USERNAME"]); }); }); describe("plugin interpolation", () => { it("fails when the plugin value is undefined", async () => { - let tempFolder = await createProjectFile(testInjector); + const tempFolder = await createProjectFile(testInjector); - let pluginVariables = { "MY_VAR": {} }; - let pluginData = createPluginData(pluginVariables); + const pluginVariables = { "MY_VAR": {} }; + const pluginData = createPluginData(pluginVariables); - let fs: IFileSystem = testInjector.resolve("fs"); - let filePath = path.join(tempFolder, "myfile"); + const fs: IFileSystem = testInjector.resolve("fs"); + const filePath = path.join(tempFolder, "myfile"); fs.writeFile(filePath, ""); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let projectData = testInjector.resolve("projectData"); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const projectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); - let expectedError = "Unable to find the value for MY_VAR plugin variable into project package.json file. Verify that your package.json file is correct and try again."; + const expectedError = "Unable to find the value for MY_VAR plugin variable into project package.json file. Verify that your package.json file is correct and try again."; let error: string = null; try { await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData); @@ -239,36 +239,36 @@ describe("Plugin Variables service", () => { }); it("interpolates correctly plugin variable value", async () => { - let tempFolder = await createProjectFile(testInjector); + const tempFolder = await createProjectFile(testInjector); - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); - let fs: IFileSystem = testInjector.resolve("fs"); + const fs: IFileSystem = testInjector.resolve("fs"); // Write plugin variables values to package.json file - let packageJsonFilePath = path.join(projectData.projectDir, "package.json"); - let data = fs.readJson(packageJsonFilePath); + const packageJsonFilePath = path.join(projectData.projectDir, "package.json"); + const data = fs.readJson(packageJsonFilePath); data["nativescript"]["myTestPlugin-variables"] = { "FB_APP_NAME": "myFacebookAppName" }; fs.writeJson(packageJsonFilePath, data); - let pluginVariables = { "FB_APP_NAME": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let pluginConfigurationFileContent = '' + + const pluginVariables = { "FB_APP_NAME": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const pluginConfigurationFileContent = '' + '' + '' + '' + '' + ''; - let filePath = path.join(tempFolder, "myfile"); + const filePath = path.join(tempFolder, "myfile"); fs.writeFile(filePath, pluginConfigurationFileContent); await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData); - let result = fs.readText(filePath); - let expectedResult = '' + + const result = fs.readText(filePath); + const expectedResult = '' + '' + '' + '' + @@ -279,36 +279,36 @@ describe("Plugin Variables service", () => { }); it("interpolates correctly case sensive plugin variable value", async () => { - let tempFolder = await createProjectFile(testInjector); + const tempFolder = await createProjectFile(testInjector); - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); - let fs: IFileSystem = testInjector.resolve("fs"); + const fs: IFileSystem = testInjector.resolve("fs"); // Write plugin variables values to package.json file - let packageJsonFilePath = path.join(projectData.projectDir, "package.json"); - let data = fs.readJson(packageJsonFilePath); + const packageJsonFilePath = path.join(projectData.projectDir, "package.json"); + const data = fs.readJson(packageJsonFilePath); data["nativescript"]["myTestPlugin-variables"] = { "FB_APP_NAME": "myFacebookAppName" }; fs.writeJson(packageJsonFilePath, data); - let pluginVariables = { "FB_APP_NAME": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let pluginConfigurationFileContent = '' + + const pluginVariables = { "FB_APP_NAME": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const pluginConfigurationFileContent = '' + '' + '' + '' + '' + ''; - let filePath = path.join(tempFolder, "myfile"); + const filePath = path.join(tempFolder, "myfile"); fs.writeFile(filePath, pluginConfigurationFileContent); await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData); - let result = fs.readText(filePath); - let expectedResult = '' + + const result = fs.readText(filePath); + const expectedResult = '' + '' + '' + '' + @@ -319,37 +319,37 @@ describe("Plugin Variables service", () => { }); it("interpolates correctly more than one plugin variables values", async () => { - let tempFolder = await createProjectFile(testInjector); + const tempFolder = await createProjectFile(testInjector); - let projectData: IProjectData = testInjector.resolve("projectData"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); - let fs: IFileSystem = testInjector.resolve("fs"); + const fs: IFileSystem = testInjector.resolve("fs"); - let packageJsonFilePath = path.join(projectData.projectDir, "package.json"); - let data = fs.readJson(packageJsonFilePath); + const packageJsonFilePath = path.join(projectData.projectDir, "package.json"); + const data = fs.readJson(packageJsonFilePath); data["nativescript"]["myTestPlugin-variables"] = { "FB_APP_NAME": "myFacebookAppName", "FB_APP_URL": "myFacebookAppURl" }; fs.writeJson(packageJsonFilePath, data); - let pluginVariables = { "FB_APP_NAME": {}, "FB_APP_URL": {} }; - let pluginData = createPluginData(pluginVariables); - let pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); - let pluginConfigurationFileContent = '' + + const pluginVariables = { "FB_APP_NAME": {}, "FB_APP_URL": {} }; + const pluginData = createPluginData(pluginVariables); + const pluginVariablesService: IPluginVariablesService = testInjector.resolve("pluginVariablesService"); + const pluginConfigurationFileContent = '' + '' + '' + '' + '' + '' + ''; - let filePath = path.join(tempFolder, "myfile"); + const filePath = path.join(tempFolder, "myfile"); fs.writeFile(filePath, pluginConfigurationFileContent); await pluginVariablesService.interpolatePluginVariables(pluginData, filePath, projectData); - let result = fs.readText(filePath); - let expectedResult = '' + + const result = fs.readText(filePath); + const expectedResult = '' + '' + '' + '' + diff --git a/test/plugins-service.ts b/test/plugins-service.ts index cc440a608d..6360e2ee0d 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -27,7 +27,6 @@ import { DeviceAppDataFactory } from "../lib/common/mobile/device-app-data/devic import { LocalToDevicePathDataFactory } from "../lib/common/mobile/local-to-device-path-data-factory"; import { MobileHelper } from "../lib/common/mobile/mobile-helper"; import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; -import { DeviceAppDataProvider } from "../lib/providers/device-app-data-provider"; import { MobilePlatformsCapabilities } from "../lib/mobile-platforms-capabilities"; import { DevicePlatformsConstants } from "../lib/common/mobile/device-platforms-constants"; import { XmlValidator } from "../lib/xml-validator"; @@ -39,7 +38,7 @@ temp.track(); let isErrorThrown = false; function createTestInjector() { - let testInjector = new Yok(); + const testInjector = new Yok(); testInjector.register("messagesService", MessagesService); testInjector.register("npm", NodePackageManager); testInjector.register("fs", FileSystem); @@ -70,14 +69,16 @@ function createTestInjector() { registerDynamicSubCommands: () => { /* intentionally empty body */ } }); testInjector.register("hostInfo", HostInfo); - testInjector.register("lockfile", {}); testInjector.register("projectHelper", ProjectHelper); testInjector.register("pluginsService", PluginsService); testInjector.register("analyticsService", { trackException: () => { return Promise.resolve(); }, checkConsent: () => { return Promise.resolve(); }, - trackFeature: () => { return Promise.resolve(); } + trackFeature: () => { return Promise.resolve(); }, + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve(), + trackInGoogleAnalytics: (data: IGoogleAnalyticsData) => Promise.resolve(), + trackAcceptFeatureUsage: (settings: { acceptTrackFeatureUsage: boolean }) => Promise.resolve() }); testInjector.register("projectFilesManager", ProjectFilesManager); testInjector.register("pluginVariablesService", { @@ -90,7 +91,6 @@ function createTestInjector() { testInjector.register("localToDevicePathDataFactory", LocalToDevicePathDataFactory); testInjector.register("mobileHelper", MobileHelper); testInjector.register("projectFilesProvider", ProjectFilesProvider); - testInjector.register("deviceAppDataProvider", DeviceAppDataProvider); testInjector.register("mobilePlatformsCapabilities", MobilePlatformsCapabilities); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); testInjector.register("projectTemplatesService", { @@ -103,11 +103,11 @@ function createTestInjector() { } function createProjectFile(testInjector: IInjector): string { - let tempFolder = temp.mkdirSync("pluginsService"); - let options = testInjector.resolve("options"); + const tempFolder = temp.mkdirSync("pluginsService"); + const options = testInjector.resolve("options"); options.path = tempFolder; - let packageJsonData = { + const packageJsonData = { "name": "testModuleName", "version": "0.1.0", "nativescript": { @@ -123,7 +123,7 @@ function createProjectFile(testInjector: IInjector): string { } function mockBeginCommand(testInjector: IInjector, expectedErrorMessage: string) { - let errors = testInjector.resolve("errors"); + const errors = testInjector.resolve("errors"); errors.beginCommand = async (action: () => Promise): Promise => { try { return await action(); @@ -137,11 +137,9 @@ function mockBeginCommand(testInjector: IInjector, expectedErrorMessage: string) async function addPluginWhenExpectingToFail(testInjector: IInjector, plugin: string, expectedErrorMessage: string, command?: string) { createProjectFile(testInjector); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; pluginsService.ensureAllDependenciesAreInstalled = () => { return Promise.resolve(); @@ -150,14 +148,14 @@ async function addPluginWhenExpectingToFail(testInjector: IInjector, plugin: str mockBeginCommand(testInjector, "Exception: " + expectedErrorMessage); isErrorThrown = false; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [plugin]); assert.isTrue(isErrorThrown); } function createAndroidManifestFile(projectFolder: string, fs: IFileSystem): void { - let manifest = ` + const manifest = ` @@ -183,7 +181,7 @@ function createAndroidManifestFile(projectFolder: string, fs: IFileSystem): void describe("Plugins service", () => { let testInjector: IInjector; - let commands = ["add", "install"]; + const commands = ["add", "install"]; beforeEach(() => { testInjector = createTestInjector(); testInjector.registerCommand("plugin|add", AddPluginCommand); @@ -199,41 +197,39 @@ describe("Plugins service", () => { await addPluginWhenExpectingToFail(testInjector, "lodash", "lodash is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.", command); }); it("fails when the plugin is already installed", async () => { - let pluginName = "plugin1"; - let projectFolder = createProjectFile(testInjector); - let fs = testInjector.resolve("fs"); + const pluginName = "plugin1"; + const projectFolder = createProjectFile(testInjector); + const fs = testInjector.resolve("fs"); // Add plugin - let projectFilePath = path.join(projectFolder, "package.json"); - let projectData = require(projectFilePath); + const projectFilePath = path.join(projectFolder, "package.json"); + const projectData = require(projectFilePath); projectData.dependencies = {}; projectData.dependencies[pluginName] = "^1.0.0"; fs.writeJson(projectFilePath, projectData); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projData: IProjectData) => { - return [{ - name: "plugin1" - }]; + return [{ name: "plugin1" }]; }; mockBeginCommand(testInjector, "Exception: " + 'Plugin "plugin1" is already installed.'); isErrorThrown = false; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [pluginName]); assert.isTrue(isErrorThrown); }); it("fails when the plugin does not support the installed framework", async () => { let isWarningMessageShown = false; - let expectedWarningMessage = "mySamplePlugin 1.5.0 for android is not compatible with the currently installed framework version 1.4.0."; + const expectedWarningMessage = "mySamplePlugin 1.5.0 for android is not compatible with the currently installed framework version 1.4.0."; // Creates plugin in temp folder - let pluginName = "mySamplePlugin"; - let projectFolder = createProjectFile(testInjector); - let pluginFolderPath = path.join(projectFolder, pluginName); - let pluginJsonData = { + const pluginName = "mySamplePlugin"; + const projectFolder = createProjectFile(testInjector); + const pluginFolderPath = path.join(projectFolder, pluginName); + const pluginJsonData = { "name": pluginName, "version": "0.0.1", "nativescript": { @@ -242,7 +238,7 @@ describe("Plugins service", () => { } }, }; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData); // Adds android platform @@ -251,24 +247,22 @@ describe("Plugins service", () => { fs.createDirectory(path.join(projectFolder, "platforms", "android", "app")); // Mock logger.warn - let logger = testInjector.resolve("logger"); + const logger = testInjector.resolve("logger"); logger.warn = (message: string) => { assert.equal(message, expectedWarningMessage); isWarningMessageShown = true; }; // Mock pluginsService - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); pluginsService.getAllInstalledPlugins = async (projData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; // Mock platformsData - let platformsData = testInjector.resolve("platformsData"); + const platformsData = testInjector.resolve("platformsData"); platformsData.getPlatformData = (platform: string) => { return { appDestinationDirectoryPath: path.join(projectFolder, "platforms", "android"), @@ -282,81 +276,77 @@ describe("Plugins service", () => { assert.isTrue(isWarningMessageShown); }); it("adds plugin by name", async () => { - let pluginName = "plugin1"; - let projectFolder = createProjectFile(testInjector); + const pluginName = "plugin1"; + const projectFolder = createProjectFile(testInjector); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [pluginName]); - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); // Asserts that the all plugin's content is successfully added to node_modules folder - let nodeModulesFolderPath = path.join(projectFolder, "node_modules"); + const nodeModulesFolderPath = path.join(projectFolder, "node_modules"); assert.isTrue(fs.exists(nodeModulesFolderPath)); - let pluginFolderPath = path.join(nodeModulesFolderPath, pluginName); + const pluginFolderPath = path.join(nodeModulesFolderPath, pluginName); assert.isTrue(fs.exists(pluginFolderPath)); - let pluginFiles = ["injex.js", "main.js", "package.json"]; + const pluginFiles = ["injex.js", "main.js", "package.json"]; _.each(pluginFiles, pluginFile => { assert.isTrue(fs.exists(path.join(pluginFolderPath, pluginFile))); }); // Asserts that the plugin is added in package.json file - let packageJsonContent = fs.readJson(path.join(projectFolder, "package.json")); - let actualDependencies = packageJsonContent.dependencies; - let expectedDependencies = { "plugin1": "^1.0.3" }; - let expectedDependenciesExact = { "plugin1": "1.0.3" }; + const packageJsonContent = fs.readJson(path.join(projectFolder, "package.json")); + const actualDependencies = packageJsonContent.dependencies; + const expectedDependencies = { "plugin1": "^1.0.3" }; + const expectedDependenciesExact = { "plugin1": "1.0.3" }; assert.isTrue(_.isEqual(actualDependencies, expectedDependencies) || _.isEqual(actualDependencies, expectedDependenciesExact)); }); it("adds plugin by name and version", async () => { - let pluginName = "plugin1"; - let projectFolder = createProjectFile(testInjector); + const pluginName = "plugin1"; + const projectFolder = createProjectFile(testInjector); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [pluginName + "@1.0.0"]); - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); // Assert that the all plugin's content is successfully added to node_modules folder - let nodeModulesFolderPath = path.join(projectFolder, "node_modules"); + const nodeModulesFolderPath = path.join(projectFolder, "node_modules"); assert.isTrue(fs.exists(nodeModulesFolderPath)); - let pluginFolderPath = path.join(nodeModulesFolderPath, pluginName); + const pluginFolderPath = path.join(nodeModulesFolderPath, pluginName); assert.isTrue(fs.exists(pluginFolderPath)); - let pluginFiles = ["injex.js", "main.js", "package.json"]; + const pluginFiles = ["injex.js", "main.js", "package.json"]; _.each(pluginFiles, pluginFile => { assert.isTrue(fs.exists(path.join(pluginFolderPath, pluginFile))); }); // Assert that the plugin is added in package.json file - let packageJsonContent = fs.readJson(path.join(projectFolder, "package.json")); - let actualDependencies = packageJsonContent.dependencies; - let expectedDependencies = { "plugin1": "^1.0.0" }; - let expectedDependenciesExact = { "plugin1": "1.0.0" }; + const packageJsonContent = fs.readJson(path.join(projectFolder, "package.json")); + const actualDependencies = packageJsonContent.dependencies; + const expectedDependencies = { "plugin1": "^1.0.0" }; + const expectedDependenciesExact = { "plugin1": "1.0.0" }; assert.isTrue(_.isEqual(actualDependencies, expectedDependencies) || _.isEqual(actualDependencies, expectedDependenciesExact)); }); it("adds plugin by local path", async () => { // Creates a plugin in tempFolder - let pluginName = "mySamplePlugin"; - let projectFolder = createProjectFile(testInjector); - let pluginFolderPath = path.join(projectFolder, pluginName); - let pluginJsonData = { + const pluginName = "mySamplePlugin"; + const projectFolder = createProjectFile(testInjector); + const pluginFolderPath = path.join(projectFolder, pluginName); + const pluginJsonData = { "name": pluginName, "version": "0.0.1", "nativescript": { @@ -365,25 +355,23 @@ describe("Plugins service", () => { } }, }; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [pluginFolderPath]); // Assert that the all plugin's content is successfully added to node_modules folder - let nodeModulesFolderPath = path.join(projectFolder, "node_modules"); + const nodeModulesFolderPath = path.join(projectFolder, "node_modules"); assert.isTrue(fs.exists(nodeModulesFolderPath)); assert.isTrue(fs.exists(path.join(nodeModulesFolderPath, pluginName))); - let pluginFiles = ["package.json"]; + const pluginFiles = ["package.json"]; _.each(pluginFiles, pluginFile => { assert.isTrue(fs.exists(path.join(nodeModulesFolderPath, pluginName, pluginFile))); }); @@ -393,10 +381,10 @@ describe("Plugins service", () => { }); it("doesn't install dev dependencies when --production option is specified", async () => { // Creates a plugin in tempFolder - let pluginName = "mySamplePlugin"; - let projectFolder = createProjectFile(testInjector); - let pluginFolderPath = path.join(projectFolder, pluginName); - let pluginJsonData = { + const pluginName = "mySamplePlugin"; + const projectFolder = createProjectFile(testInjector); + const pluginFolderPath = path.join(projectFolder, pluginName); + const pluginJsonData = { "name": pluginName, "version": "0.0.1", "nativescript": { @@ -408,32 +396,30 @@ describe("Plugins service", () => { "grunt": "0.4.2" } }; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; // Mock options - let options = testInjector.resolve("options"); + const options = testInjector.resolve("options"); options.production = true; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [pluginFolderPath]); - let nodeModulesFolderPath = path.join(projectFolder, "node_modules"); + const nodeModulesFolderPath = path.join(projectFolder, "node_modules"); assert.isFalse(fs.exists(path.join(nodeModulesFolderPath, pluginName, "node_modules", "grunt"))); }); it("install dev dependencies when --production option is not specified", async () => { // Creates a plugin in tempFolder - let pluginName = "mySamplePlugin"; - let projectFolder = createProjectFile(testInjector); - let pluginFolderPath = path.join(projectFolder, pluginName); - let pluginJsonData = { + const pluginName = "mySamplePlugin"; + const projectFolder = createProjectFile(testInjector); + const pluginFolderPath = path.join(projectFolder, pluginName); + const pluginJsonData = { "name": pluginName, "version": "0.0.1", "nativescript": { @@ -448,21 +434,19 @@ describe("Plugins service", () => { "grunt": "0.4.2" } }; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData); - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; // Mock options - let options = testInjector.resolve("options"); + const options = testInjector.resolve("options"); options.production = false; - let commandsService = testInjector.resolve(CommandsService); + const commandsService = testInjector.resolve(CommandsService); await commandsService.tryExecuteCommand(`plugin|${command}`, [pluginFolderPath]); }); }); @@ -474,36 +458,35 @@ describe("Plugins service", () => { testInjector.registerCommand("plugin|add", AddPluginCommand); }); it("fails if the plugin contains incorrect xml", async () => { - let pluginName = "mySamplePlugin"; - let projectFolder = createProjectFile(testInjector); - let pluginFolderPath = path.join(projectFolder, pluginName); - let pluginJsonData = { - "name": pluginName, - "version": "0.0.1", - "nativescript": { - "platforms": { - "android": "0.10.0" + const pluginName = "mySamplePlugin"; + const projectFolder = createProjectFile(testInjector); + const pluginFolderPath = path.join(projectFolder, pluginName); + const pluginJsonData: IDependencyData = { + name: pluginName, + nativescript: { + platforms: { + android: "0.10.0" } - } + }, + depth: 0, + directory: "some dir" }; - let fs = testInjector.resolve("fs"); + const fs = testInjector.resolve("fs"); fs.writeJson(path.join(pluginFolderPath, "package.json"), pluginJsonData); // Adds AndroidManifest.xml file in platforms/android folder createAndroidManifestFile(projectFolder, fs); // Mock plugins service - let pluginsService: IPluginsService = testInjector.resolve("pluginsService"); + const pluginsService: IPluginsService = testInjector.resolve("pluginsService"); pluginsService.getAllInstalledPlugins = async (projectData: IProjectData) => { - return [{ - name: "" - }]; + return [{ name: "" }]; }; - let appDestinationDirectoryPath = path.join(projectFolder, "platforms", "android"); + const appDestinationDirectoryPath = path.join(projectFolder, "platforms", "android"); // Mock platformsData - let platformsData = testInjector.resolve("platformsData"); + const platformsData = testInjector.resolve("platformsData"); platformsData.getPlatformData = (platform: string) => { return { appDestinationDirectoryPath: appDestinationDirectoryPath, @@ -517,25 +500,25 @@ describe("Plugins service", () => { }; // Ensure the pluginDestinationPath folder exists - let pluginPlatformsDirPath = path.join(projectFolder, "node_modules", pluginName, "platforms", "android"); - let projectData: IProjectData = testInjector.resolve("projectData"); + const pluginPlatformsDirPath = path.join(projectFolder, "node_modules", pluginName, "platforms", "android"); + const projectData: IProjectData = testInjector.resolve("projectData"); projectData.initializeProjectData(); fs.ensureDirectoryExists(pluginPlatformsDirPath); // Creates invalid plugin's AndroidManifest.xml file - let xml = '' + + const xml = '' + '' + ''; - let pluginConfigurationFilePath = path.join(pluginPlatformsDirPath, "AndroidManifest.xml"); + const pluginConfigurationFilePath = path.join(pluginPlatformsDirPath, "AndroidManifest.xml"); fs.writeFile(pluginConfigurationFilePath, xml); // Expected error message. The assertion happens in mockBeginCommand - let expectedErrorMessage = `Exception: Invalid xml file ${pluginConfigurationFilePath}. Additional technical information: element parse error: Exception: Invalid xml file ` + + const expectedErrorMessage = `Exception: Invalid xml file ${pluginConfigurationFilePath}. Additional technical information: element parse error: Exception: Invalid xml file ` + `${pluginConfigurationFilePath}. Additional technical information: unclosed xml attribute` + `\n@#[line:1,col:39].` + `\n@#[line:1,col:39].`; mockBeginCommand(testInjector, expectedErrorMessage); - await pluginsService.prepare(pluginJsonData, "android", projectData); + await pluginsService.prepare(pluginJsonData, "android", projectData, {}); }); }); }); diff --git a/test/post-install.ts b/test/post-install.ts new file mode 100644 index 0000000000..7f2f98bd7a --- /dev/null +++ b/test/post-install.ts @@ -0,0 +1,35 @@ +import { assert } from "chai"; + +// Use require instead of import in order to replace the `spawn` method of child_process +const childProcess = require("child_process"); + +import { SpawnOptions, ChildProcess } from "child_process"; +import * as path from "path"; +import { POST_INSTALL_COMMAND_NAME } from "../lib/constants"; + +describe("postinstall.js", () => { + it("calls post-install-cli command of CLI", () => { + const originalSpawn = childProcess.spawn; + let isSpawnCalled = false; + let argsPassedToSpawn: string[] = []; + childProcess.spawn = (command: string, args?: string[], options?: SpawnOptions): ChildProcess => { + isSpawnCalled = true; + argsPassedToSpawn = args; + + return null; + }; + + require(path.join(__dirname, "..", "postinstall")); + + childProcess.spawn = originalSpawn; + + assert.isTrue(isSpawnCalled, "child_process.spawn must be called from postinstall.js"); + + const expectedPathToCliExecutable = path.join(__dirname, "..", "bin", "tns"); + + assert.isTrue(argsPassedToSpawn.indexOf(expectedPathToCliExecutable) !== -1, `The spawned args must contain path to TNS. + Expected path is: ${expectedPathToCliExecutable}, current args are: ${argsPassedToSpawn}.`); + assert.isTrue(argsPassedToSpawn.indexOf(POST_INSTALL_COMMAND_NAME) !== -1, `The spawned args must contain the name of the post-install command. + Expected path is: ${expectedPathToCliExecutable}, current args are: ${argsPassedToSpawn}.`); + }); +}); diff --git a/test/project-changes-service.ts b/test/project-changes-service.ts new file mode 100644 index 0000000000..47e8451bdd --- /dev/null +++ b/test/project-changes-service.ts @@ -0,0 +1,151 @@ +import * as path from "path"; +import { BaseServiceTest } from "./base-service-test"; +import temp = require("temp"); +import { assert } from "chai"; +import { PlatformsData } from "../lib/platforms-data"; +import { ProjectChangesService } from "../lib/services/project-changes-service"; +import * as Constants from "../lib/constants"; +import { FileSystem } from "../lib/common/file-system"; + +// start tracking temporary folders/files +temp.track(); + +class ProjectChangesServiceTest extends BaseServiceTest { + public projectDir: string; + + constructor() { + super(); + } + + initInjector(): void { + this.projectDir = temp.mkdirSync("projectDir"); + this.injector.register("projectData", { + projectDir: this.projectDir + }); + + this.injector.register("platformsData", PlatformsData); + this.injector.register("androidProjectService", {}); + this.injector.register("iOSProjectService", {}); + this.injector.register("fs", FileSystem); + this.injector.register("devicePlatformsConstants", {}); + this.injector.register("devicePlatformsConstants", {}); + this.injector.register("projectChangesService", ProjectChangesService); + + const fs = this.injector.resolve("fs"); + fs.writeJson(path.join(this.projectDir, Constants.PACKAGE_JSON_FILE_NAME), { + nativescript: { + id: "org.nativescript.test" + } + }); + + } + + get projectChangesService(): IProjectChangesService { + return this.injector.resolve("projectChangesService"); + } + + get projectData(): IProjectData { + return this.injector.resolve("projectData"); + } + + get platformsData(): any { + return this.injector.resolve("platformsData"); + } +} + +describe("Project Changes Service Tests", () => { + let serviceTest: ProjectChangesServiceTest; + beforeEach(() => { + serviceTest = new ProjectChangesServiceTest(); + + const platformsDir = path.join( + serviceTest.projectDir, + Constants.PLATFORMS_DIR_NAME + ); + + serviceTest.platformsData.getPlatformData = + (platform: string) => { + if (platform.toLowerCase() === "ios") { + return { + projectRoot: path.join(platformsDir, platform), + get platformProjectService(): any { + return { + checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { + changesInfo.signingChanged = true; + } + }; + } + }; + } else { + return { + projectRoot: path.join(platformsDir, platform), + get platformProjectService(): any { + return { + checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { /* no android changes */ } + }; + } + }; + } + }; + }); + + describe("Get Prepare Info File Path", () => { + it("Gets the correct Prepare Info path for ios/android", () => { + for (const platform of ["ios", "android"]) { + const actualPrepareInfoPath = serviceTest.projectChangesService + .getPrepareInfoFilePath(platform, this.projectData); + + const expectedPrepareInfoPath = path.join(serviceTest.projectDir, + Constants.PLATFORMS_DIR_NAME, + platform, + ".nsprepareinfo"); + assert.equal(actualPrepareInfoPath, expectedPrepareInfoPath); + } + }); + }); + + describe("Get Prepare Info", () => { + it("Returns empty if file path doesn't exists", () => { + for (const platform of ["ios", "android"]) { + const projectInfo = serviceTest.projectChangesService.getPrepareInfo(platform, this.projectData); + + assert.isNull(projectInfo); + } + }); + + it("Reads the Prepare Info correctly", () => { + const fs: FileSystem = serviceTest.resolve("fs"); + for (const platform of ["ios", "android"]) { + // arrange + const prepareInfoPath = path.join(serviceTest.projectDir, Constants.PLATFORMS_DIR_NAME, + platform, ".nsprepareinfo"); + const expectedPrepareInfo: IPrepareInfo = { + time: new Date().toString(), + bundle: true, + release: false, + changesRequireBuild: true, + changesRequireBuildTime: new Date().toString(), + iOSProvisioningProfileUUID: "provisioning_profile_test", + projectFileHash: "", + nativePlatformStatus: Constants.NativePlatformStatus.requiresPlatformAdd + }; + fs.writeJson(prepareInfoPath, expectedPrepareInfo); + + // act + const actualPrepareInfo = serviceTest.projectChangesService.getPrepareInfo(platform, this.projectData); + + // assert + assert.deepEqual(actualPrepareInfo, expectedPrepareInfo); + } + }); + }); + + describe("Accumulates Changes From Project Services", () => { + it("accumulates changes from the project service", async () => { + const iOSChanges = await serviceTest.projectChangesService.checkForChanges("ios", serviceTest.projectData, { bundle: false, release: false, provision: undefined, teamId: undefined }); + assert.isTrue(!!iOSChanges.signingChanged, "iOS signingChanged expected to be true"); + const androidChanges = await serviceTest.projectChangesService.checkForChanges("android", serviceTest.projectData, { bundle: false, release: false, provision: undefined, teamId: undefined }); + assert.isFalse(!!androidChanges.signingChanged, "Android signingChanged expected to be false"); + }); + }); +}); diff --git a/test/project-commands.ts b/test/project-commands.ts index 3b201324a1..d5ddccaa41 100644 --- a/test/project-commands.ts +++ b/test/project-commands.ts @@ -7,7 +7,7 @@ import { assert } from "chai"; let selectedTemplateName: string; let isProjectCreated: boolean; -let dummyArgs = ["dummyArgsString"]; +const dummyArgs = ["dummyArgsString"]; class ProjectServiceMock implements IProjectService { async createProject(projectOptions: IProjectSettings): Promise { @@ -27,7 +27,7 @@ class ProjectNameValidatorMock implements IProjectNameValidator { } function createTestInjector() { - let testInjector = new Yok(); + const testInjector = new Yok(); testInjector.register("injector", testInjector); testInjector.register("staticConfig", {}); diff --git a/test/project-files-provider.ts b/test/project-files-provider.ts index 8d21a50966..0edd3f5566 100644 --- a/test/project-files-provider.ts +++ b/test/project-files-provider.ts @@ -3,13 +3,13 @@ import { ProjectFilesProvider } from "../lib/providers/project-files-provider"; import { assert } from "chai"; import * as path from "path"; -let projectDir = "projectDir", +const projectDir = "projectDir", appDestinationDirectoryPath = "appDestinationDirectoryPath", appResourcesDestinationDirectoryPath = "appResourcesDestinationDirectoryPath", appSourceDir = path.join(projectDir, "app"); function createTestInjector(): IInjector { - let testInjector = new Yok(); + const testInjector = new Yok(); testInjector.register("mobileHelper", { platformNames: ["Android", "iOS"] }); @@ -36,8 +36,8 @@ function createTestInjector(): IInjector { } describe("project-files-provider", () => { - let testInjector: IInjector, - projectFilesProvider: IProjectFilesProvider; + let testInjector: IInjector; + let projectFilesProvider: IProjectFilesProvider; beforeEach(() => { testInjector = createTestInjector(); @@ -56,38 +56,38 @@ describe("project-files-provider", () => { describe("mapFilePath", () => { it("returns file path from prepared project when path from app dir is passed", () => { - let projectData: IProjectData = testInjector.resolve("projectData"); - let mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "test.js"), "android", projectData); + const projectData: IProjectData = testInjector.resolve("projectData"); + const mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "test.js"), "android", projectData, {}); assert.deepEqual(mappedFilePath, path.join(appDestinationDirectoryPath, "app", "test.js")); }); it("returns file path from prepared project when path from app/App_Resources/platform dir is passed", () => { - let projectData: IProjectData = testInjector.resolve("projectData"); - let mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "App_Resources", "android", "test.js"), "android", projectData); + const projectData: IProjectData = testInjector.resolve("projectData"); + const mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "App_Resources", "android", "test.js"), "android", projectData, {}); assert.deepEqual(mappedFilePath, path.join(appResourcesDestinationDirectoryPath, "test.js")); }); it("returns null when path from app/App_Resources/android dir is passed and iOS platform is specified", () => { - let projectData: IProjectData = testInjector.resolve("projectData"); - let mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "App_Resources", "android", "test.js"), "iOS", projectData); + const projectData: IProjectData = testInjector.resolve("projectData"); + const mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "App_Resources", "android", "test.js"), "iOS", projectData, {}); assert.deepEqual(mappedFilePath, null); }); it("returns null when path from app/App_Resources/ dir (not platform specific) is passed", () => { - let projectData: IProjectData = testInjector.resolve("projectData"); - let mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "App_Resources", "test.js"), "android", projectData); + const projectData: IProjectData = testInjector.resolve("projectData"); + const mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "App_Resources", "test.js"), "android", projectData, {}); assert.deepEqual(mappedFilePath, null); }); it("returns file path from prepared project when path from app dir is passed and it contains platform in its name", () => { - let projectData: IProjectData = testInjector.resolve("projectData"); - let mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "test.android.js"), "android", projectData); + const projectData: IProjectData = testInjector.resolve("projectData"); + const mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "test.android.js"), "android", projectData, {}); assert.deepEqual(mappedFilePath, path.join(appDestinationDirectoryPath, "app", "test.js")); }); it("returns file path from prepared project when path from app dir is passed and it contains configuration in its name", () => { - let projectData: IProjectData = testInjector.resolve("projectData"); - let mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "test.debug.js"), "android", projectData); + const projectData: IProjectData = testInjector.resolve("projectData"); + const mappedFilePath = projectFilesProvider.mapFilePath(path.join(appSourceDir, "test.debug.js"), "android", projectData, {}); assert.deepEqual(mappedFilePath, path.join(appDestinationDirectoryPath, "app", "test.js")); }); }); diff --git a/test/project-name-service.ts b/test/project-name-service.ts index 2fa9bb3989..d354a295af 100644 --- a/test/project-name-service.ts +++ b/test/project-name-service.ts @@ -3,11 +3,11 @@ import { ProjectNameService } from "../lib/services/project-name-service"; import { assert } from "chai"; import { ErrorsStub, LoggerStub } from "./stubs"; -let mockProjectNameValidator = { +const mockProjectNameValidator = { validate: () => true }; -let dummyString: string = "dummyString"; +const dummyString: string = "dummyString"; function createTestInjector(): IInjector { let testInjector: IInjector; @@ -28,8 +28,8 @@ function createTestInjector(): IInjector { describe("Project Name Service Tests", () => { let testInjector: IInjector; let projectNameService: IProjectNameService; - let validProjectName = "valid"; - let invalidProjectNames = ["1invalid", "app"]; + const validProjectName = "valid"; + const invalidProjectNames = ["1invalid", "app"]; beforeEach(() => { testInjector = createTestInjector(); @@ -37,17 +37,17 @@ describe("Project Name Service Tests", () => { }); it("returns correct name when valid name is entered", async () => { - let actualProjectName = await projectNameService.ensureValidName(validProjectName); + const actualProjectName = await projectNameService.ensureValidName(validProjectName); assert.deepEqual(actualProjectName, validProjectName); }); _.each(invalidProjectNames, invalidProjectName => { it(`returns correct name when "${invalidProjectName}" is entered several times and then valid name is entered`, async () => { - let prompter = testInjector.resolve("prompter"); + const prompter = testInjector.resolve("prompter"); prompter.confirm = (message: string): Promise => Promise.resolve(false); - let incorrectInputsLimit = 5; + const incorrectInputsLimit = 5; let incorrectInputsCount = 0; prompter.getString = async (message: string): Promise => { @@ -60,13 +60,13 @@ describe("Project Name Service Tests", () => { } }; - let actualProjectName = await projectNameService.ensureValidName(invalidProjectName); + const actualProjectName = await projectNameService.ensureValidName(invalidProjectName); assert.deepEqual(actualProjectName, validProjectName); }); it(`returns the invalid name when "${invalidProjectName}" is entered and --force flag is present`, async () => { - let actualProjectName = await projectNameService.ensureValidName(validProjectName, { force: true }); + const actualProjectName = await projectNameService.ensureValidName(validProjectName, { force: true }); assert.deepEqual(actualProjectName, validProjectName); }); diff --git a/test/project-service.ts b/test/project-service.ts index 8a1c7b0ad4..2ed5961274 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -19,16 +19,39 @@ import { Options } from "../lib/options"; import { HostInfo } from "../lib/common/host-info"; import { ProjectTemplatesService } from "../lib/services/project-templates-service"; -let mockProjectNameValidator = { +const mockProjectNameValidator = { validate: () => true }; -let dummyString: string = "dummyString"; +const dummyString: string = "dummyString"; let hasPromptedForString = false; -let originalIsInteractive = helpers.isInteractive; +const originalIsInteractive = helpers.isInteractive; temp.track(); +async function prepareTestingPath(testInjector: IInjector, packageToInstall: string, packageName: string, options?: INpmInstallOptions): Promise { + options = options || { dependencyType: "save" }; + const fs = testInjector.resolve("fs"); + + const npmInstallationManager = testInjector.resolve("npmInstallationManager"); + const defaultTemplateDir = temp.mkdirSync("project-service"); + fs.writeJson(path.join(defaultTemplateDir, constants.PACKAGE_JSON_FILE_NAME), { + "name": "defaultTemplate", + "version": "1.0.0", + "description": "dummy", + "license": "MIT", + "readme": "dummy", + "repository": "dummy" + }); + + await npmInstallationManager.install(packageToInstall, defaultTemplateDir, options); + const defaultTemplatePath = path.join(defaultTemplateDir, constants.NODE_MODULES_FOLDER_NAME, packageName); + + fs.deleteDirectory(path.join(defaultTemplatePath, constants.NODE_MODULES_FOLDER_NAME)); + + return defaultTemplatePath; +} + class ProjectIntegrationTest { public testInjector: IInjector; @@ -37,7 +60,7 @@ class ProjectIntegrationTest { } public async createProject(projectOptions: IProjectSettings): Promise { - let projectService: IProjectService = this.testInjector.resolve("projectService"); + const projectService: IProjectService = this.testInjector.resolve("projectService"); if (!projectOptions.template) { projectOptions.template = constants.RESERVED_TEMPLATE_NAMES["default"]; } @@ -45,13 +68,13 @@ class ProjectIntegrationTest { } public async assertProject(tempFolder: string, projectName: string, appId: string, projectSourceDirectory: string): Promise { - let fs: IFileSystem = this.testInjector.resolve("fs"); - let projectDir = path.join(tempFolder, projectName); - let appDirectoryPath = path.join(projectDir, "app"); - let platformsDirectoryPath = path.join(projectDir, "platforms"); - let tnsProjectFilePath = path.join(projectDir, "package.json"); - let tnsModulesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, constants.TNS_CORE_MODULES_NAME); - let packageJsonContent = fs.readJson(tnsProjectFilePath); + const fs: IFileSystem = this.testInjector.resolve("fs"); + const projectDir = path.join(tempFolder, projectName); + const appDirectoryPath = path.join(projectDir, "app"); + const platformsDirectoryPath = path.join(projectDir, "platforms"); + const tnsProjectFilePath = path.join(projectDir, "package.json"); + const tnsModulesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, constants.TNS_CORE_MODULES_NAME); + const packageJsonContent = fs.readJson(tnsProjectFilePath); assert.isTrue(fs.exists(appDirectoryPath)); assert.isTrue(fs.exists(platformsDirectoryPath)); @@ -61,37 +84,37 @@ class ProjectIntegrationTest { assert.isFalse(fs.isEmptyDir(appDirectoryPath)); assert.isTrue(fs.isEmptyDir(platformsDirectoryPath)); - let actualAppId = packageJsonContent["nativescript"].id; - let expectedAppId = appId; + const actualAppId = packageJsonContent["nativescript"].id; + const expectedAppId = appId; assert.equal(actualAppId, expectedAppId); - let tnsCoreModulesRecord = packageJsonContent["dependencies"][constants.TNS_CORE_MODULES_NAME]; + const tnsCoreModulesRecord = packageJsonContent["dependencies"][constants.TNS_CORE_MODULES_NAME]; assert.isTrue(tnsCoreModulesRecord !== null); - let sourceDir = projectSourceDirectory; + const sourceDir = projectSourceDirectory; // Hidden files (starting with dots ".") are not copied. - let expectedFiles = fs.enumerateFilesInDirectorySync(sourceDir, (file, stat) => stat.isDirectory() || !_.startsWith(path.basename(file), ".")); - let actualFiles = fs.enumerateFilesInDirectorySync(appDirectoryPath); + const expectedFiles = fs.enumerateFilesInDirectorySync(sourceDir, (file, stat) => stat.isDirectory() || !_.startsWith(path.basename(file), ".")); + const actualFiles = fs.enumerateFilesInDirectorySync(appDirectoryPath); assert.isTrue(actualFiles.length >= expectedFiles.length, "Files in created project must be at least as files in app dir."); _.each(expectedFiles, file => { - let relativeToProjectDir = helpers.getRelativeToRootPath(sourceDir, file); - let filePathInApp = path.join(appDirectoryPath, relativeToProjectDir); + const relativeToProjectDir = helpers.getRelativeToRootPath(sourceDir, file); + const filePathInApp = path.join(appDirectoryPath, relativeToProjectDir); assert.isTrue(fs.exists(filePathInApp), `File ${filePathInApp} does not exist.`); }); // assert dependencies and devDependencies are copied from template to real project - let sourcePackageJsonContent = fs.readJson(path.join(sourceDir, "package.json")); - let missingDeps = _.difference(_.keys(sourcePackageJsonContent.dependencies), _.keys(packageJsonContent.dependencies)); - let missingDevDeps = _.difference(_.keys(sourcePackageJsonContent.devDependencies), _.keys(packageJsonContent.devDependencies)); + const sourcePackageJsonContent = fs.readJson(path.join(sourceDir, "package.json")); + const missingDeps = _.difference(_.keys(sourcePackageJsonContent.dependencies), _.keys(packageJsonContent.dependencies)); + const missingDevDeps = _.difference(_.keys(sourcePackageJsonContent.devDependencies), _.keys(packageJsonContent.devDependencies)); assert.deepEqual(missingDeps, [], `All dependencies from template must be copied to project's package.json. Missing ones are: ${missingDeps.join(", ")}.`); assert.deepEqual(missingDevDeps, [], `All devDependencies from template must be copied to project's package.json. Missing ones are: ${missingDevDeps.join(", ")}.`); // assert App_Resources are prepared correctly - let appResourcesDir = path.join(appDirectoryPath, "App_Resources"); - let appResourcesContents = fs.readDirectory(appResourcesDir); + const appResourcesDir = path.join(appDirectoryPath, "App_Resources"); + const appResourcesContents = fs.readDirectory(appResourcesDir); assert.deepEqual(appResourcesContents, ["Android", "iOS"], "Project's app/App_Resources must contain Android and iOS directories."); } @@ -114,12 +137,14 @@ class ProjectIntegrationTest { this.testInjector.register("fs", FileSystem); this.testInjector.register("projectDataService", ProjectDataServiceLib.ProjectDataService); this.testInjector.register("staticConfig", StaticConfig); - this.testInjector.register("analyticsService", { track: async (): Promise => undefined }); + this.testInjector.register("analyticsService", { + track: async (): Promise => undefined, + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() + }); this.testInjector.register("npmInstallationManager", NpmInstallationManager); this.testInjector.register("npm", NpmLib.NodePackageManager); this.testInjector.register("httpClient", HttpClientLib.HttpClient); - this.testInjector.register("lockfile", stubs.LockFile); this.testInjector.register("options", Options); this.testInjector.register("hostInfo", HostInfo); @@ -145,128 +170,55 @@ describe("Project Service Tests", () => { const noAppResourcesTemplateName = "tns-template-hello-world-ts"; before(async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let fs: IFileSystem = projectIntegrationTest.testInjector.resolve("fs"); - let npmInstallationManager: INpmInstallationManager = projectIntegrationTest.testInjector.resolve("npmInstallationManager"); - - let defaultTemplateDir = temp.mkdirSync("defaultTemplate"); - fs.writeJson(path.join(defaultTemplateDir, "package.json"), { - "name": "defaultTemplate", - "version": "1.0.0", - "description": "dummy", - "license": "MIT", - "readme": "dummy", - "repository": "dummy" - }); + const projectIntegrationTest = new ProjectIntegrationTest(); - await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["default"], defaultTemplateDir, { dependencyType: "save" }); - defaultTemplatePath = path.join(defaultTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["default"]); - - fs.deleteDirectory(path.join(defaultTemplatePath, "node_modules")); - - let defaultSpecificVersionTemplateDir = temp.mkdirSync("defaultTemplateSpeciffic"); - fs.writeJson(path.join(defaultSpecificVersionTemplateDir, "package.json"), { - "name": "defaultTemplateSpecialVersion", - "version": "1.0.0", - "description": "dummy", - "license": "MIT", - "readme": "dummy", - "repository": "dummy" - }); - - await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["default"], defaultSpecificVersionTemplateDir, { version: "1.4.0", dependencyType: "save" }); - defaultSpecificVersionTemplatePath = path.join(defaultSpecificVersionTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["default"]); - - fs.deleteDirectory(path.join(defaultSpecificVersionTemplatePath, "node_modules")); - - let angularTemplateDir = temp.mkdirSync("angularTemplate"); - fs.writeJson(path.join(angularTemplateDir, "package.json"), { - "name": "angularTemplate", - "version": "1.0.0", - "description": "dummy", - "license": "MIT", - "readme": "dummy", - "repository": "dummy" - }); - - await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["angular"], angularTemplateDir, { dependencyType: "save" }); - angularTemplatePath = path.join(angularTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["angular"]); - - fs.deleteDirectory(path.join(angularTemplatePath, "node_modules")); - - let typescriptTemplateDir = temp.mkdirSync("typescriptTemplate"); - fs.writeJson(path.join(typescriptTemplateDir, "package.json"), { - "name": "typescriptTemplate", - "version": "1.0.0", - "description": "dummy", - "license": "MIT", - "readme": "dummy", - "repository": "dummy" - }); - - await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["typescript"], typescriptTemplateDir, { dependencyType: "save" }); - typescriptTemplatePath = path.join(typescriptTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["typescript"]); - - fs.deleteDirectory(path.join(typescriptTemplatePath, "node_modules")); - let noAppResourcesTemplateDir = temp.mkdirSync("noAppResources"); - fs.writeJson(path.join(noAppResourcesTemplateDir, "package.json"), { - "name": "blankTemplate", - "version": "1.0.0", - "description": "dummy", - "license": "MIT", - "readme": "dummy", - "repository": "dummy" - }); - - await npmInstallationManager.install(noAppResourcesTemplateName, noAppResourcesTemplateDir, { - dependencyType: "save", - version: "2.0.0" - }); - noAppResourcesTemplatePath = path.join(noAppResourcesTemplateDir, "node_modules", noAppResourcesTemplateName); - - fs.deleteDirectory(path.join(noAppResourcesTemplatePath, "node_modules")); + defaultTemplatePath = await prepareTestingPath(projectIntegrationTest.testInjector, constants.RESERVED_TEMPLATE_NAMES["default"], constants.RESERVED_TEMPLATE_NAMES["default"]); + defaultSpecificVersionTemplatePath = await prepareTestingPath(projectIntegrationTest.testInjector, constants.RESERVED_TEMPLATE_NAMES["default"], constants.RESERVED_TEMPLATE_NAMES["default"], { version: "1.4.0", dependencyType: "save" }); + angularTemplatePath = await prepareTestingPath(projectIntegrationTest.testInjector, constants.RESERVED_TEMPLATE_NAMES["angular"], constants.RESERVED_TEMPLATE_NAMES["angular"]); + typescriptTemplatePath = await prepareTestingPath(projectIntegrationTest.testInjector, constants.RESERVED_TEMPLATE_NAMES["typescript"], constants.RESERVED_TEMPLATE_NAMES["typescript"]); + noAppResourcesTemplatePath = await prepareTestingPath(projectIntegrationTest.testInjector, noAppResourcesTemplateName, noAppResourcesTemplateName, { dependencyType: "save", version: "2.0.0" }); }); it("creates valid project from default template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("project"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("project"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, pathToProject: tempFolder }); await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", defaultTemplatePath); }); it("creates valid project from default template when --template default is specified", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("project"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("project"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: "default", pathToProject: tempFolder }); await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", defaultTemplatePath); }); it("creates valid project from default template when --template default@version is specified", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("project"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("project"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: "default@1.4.0", pathToProject: tempFolder }); await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", defaultSpecificVersionTemplatePath); }); it("creates valid project from a template without App_Resources", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("project"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("project"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: noAppResourcesTemplateName + "@2.0.0", pathToProject: tempFolder }); await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", noAppResourcesTemplatePath); }); it("creates valid project from typescript template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectTypescript"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectTypescript"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: "typescript", pathToProject: tempFolder }); @@ -274,18 +226,18 @@ describe("Project Service Tests", () => { }); it("creates valid project from tsc template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectTsc"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectTsc"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: "tsc", pathToProject: tempFolder }); await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", typescriptTemplatePath); }); it("creates valid project from angular template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectAngular"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectAngular"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: "angular", pathToProject: tempFolder }); @@ -293,9 +245,9 @@ describe("Project Service Tests", () => { }); it("creates valid project from ng template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectNg"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectNg"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, template: "ng", pathToProject: tempFolder }); @@ -303,14 +255,14 @@ describe("Project Service Tests", () => { }); it("creates valid project from local directory template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectLocalDir"); - let projectName = "myapp"; - let options = projectIntegrationTest.testInjector.resolve("options"); + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectLocalDir"); + const projectName = "myapp"; + const options = projectIntegrationTest.testInjector.resolve("options"); options.path = tempFolder; - let tempDir = temp.mkdirSync("template"); - let fs: IFileSystem = projectIntegrationTest.testInjector.resolve("fs"); + const tempDir = temp.mkdirSync("template"); + const fs: IFileSystem = projectIntegrationTest.testInjector.resolve("fs"); fs.writeJson(path.join(tempDir, "package.json"), { name: "myCustomTemplate", version: "1.0.0", @@ -337,45 +289,49 @@ describe("Project Service Tests", () => { }); it("creates valid project from tarball", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectLocalDir"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectLocalDir"); + const projectName = "myapp"; + const template = "https://github.com/NativeScript/template-hello-world/tarball/master"; await projectIntegrationTest.createProject({ projectName: projectName, - template: "https://github.com/NativeScript/template-hello-world/tarball/master", + template, pathToProject: tempFolder }); - await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", defaultTemplatePath); + const projectSourceDirectory = await prepareTestingPath(projectIntegrationTest.testInjector, template, constants.RESERVED_TEMPLATE_NAMES["default"]); + await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectSourceDirectory); }); it("creates valid project from git url", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("projectLocalDir"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("projectLocalDir"); + const projectName = "myapp"; + const template = "https://github.com/NativeScript/template-hello-world.git"; await projectIntegrationTest.createProject({ projectName: projectName, - template: "https://github.com/NativeScript/template-hello-world.git", + template, pathToProject: tempFolder }); - await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", defaultTemplatePath); + const projectSourceDirectory = await prepareTestingPath(projectIntegrationTest.testInjector, template, constants.RESERVED_TEMPLATE_NAMES["default"]); + await projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectSourceDirectory); }); it("creates valid project with specified id from default template", async () => { - let projectIntegrationTest = new ProjectIntegrationTest(); - let tempFolder = temp.mkdirSync("project1"); - let projectName = "myapp"; + const projectIntegrationTest = new ProjectIntegrationTest(); + const tempFolder = temp.mkdirSync("project1"); + const projectName = "myapp"; await projectIntegrationTest.createProject({ projectName: projectName, pathToProject: tempFolder, appId: "my.special.id" }); await projectIntegrationTest.assertProject(tempFolder, projectName, "my.special.id", defaultTemplatePath); }); describe("project name validation tests", () => { - let validProjectName = "valid"; - let invalidProjectName = "1invalid"; + const validProjectName = "valid"; + const invalidProjectName = "1invalid"; let projectIntegrationTest: ProjectIntegrationTest; let tempFolder: string; let options: IOptions; @@ -395,13 +351,13 @@ describe("Project Service Tests", () => { }); it("creates project when is interactive and incorrect name is specified and the --force option is set", async () => { - let projectName = invalidProjectName; + const projectName = invalidProjectName; await projectIntegrationTest.createProject({ projectName: projectName, pathToProject: tempFolder, force: true }); await projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`, defaultTemplatePath); }); it("creates project when is interactive and incorrect name is specified and the user confirms to use the incorrect name", async () => { - let projectName = invalidProjectName; + const projectName = invalidProjectName; prompter.confirm = (message: string): Promise => Promise.resolve(true); await projectIntegrationTest.createProject({ projectName: projectName, pathToProject: tempFolder }); @@ -409,7 +365,7 @@ describe("Project Service Tests", () => { }); it("prompts for new name when is interactive and incorrect name is specified and the user does not confirm to use the incorrect name", async () => { - let projectName = invalidProjectName; + const projectName = invalidProjectName; prompter.confirm = (message: string): Promise => Promise.resolve(false); @@ -418,11 +374,11 @@ describe("Project Service Tests", () => { }); it("creates project when is interactive and incorrect name s specified and the user does not confirm to use the incorrect name and enters incorrect name again several times and then enters correct name", async () => { - let projectName = invalidProjectName; + const projectName = invalidProjectName; prompter.confirm = (message: string): Promise => Promise.resolve(false); - let incorrectInputsLimit = 5; + const incorrectInputsLimit = 5; let incorrectInputsCount = 0; prompter.getString = async (message: string): Promise => { @@ -442,14 +398,14 @@ describe("Project Service Tests", () => { }); it("does not create project when is not interactive and incorrect name is specified", async () => { - let projectName = invalidProjectName; + const projectName = invalidProjectName; helpers.isInteractive = () => false; await assert.isRejected(projectIntegrationTest.createProject({ projectName: projectName, pathToProject: tempFolder, force: false })); }); it("creates project when is not interactive and incorrect name is specified and the --force option is set", async () => { - let projectName = invalidProjectName; + const projectName = invalidProjectName; helpers.isInteractive = () => false; await projectIntegrationTest.createProject({ projectName: projectName, pathToProject: tempFolder, force: true }); diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts index 6ec449d736..e227a232e3 100644 --- a/test/project-templates-service.ts +++ b/test/project-templates-service.ts @@ -7,10 +7,10 @@ import temp = require("temp"); import * as constants from "../lib/constants"; let isDeleteDirectoryCalledForNodeModulesDir = false; -let nativeScriptValidatedTemplatePath = "nsValidatedTemplatePath"; +const nativeScriptValidatedTemplatePath = "nsValidatedTemplatePath"; function createTestInjector(configuration?: { shouldNpmInstallThrow: boolean, npmInstallationDirContents: string[], npmInstallationDirNodeModulesContents: string[] }): IInjector { - let injector = new Yok(); + const injector = new Yok(); injector.register("errors", stubs.ErrorsStub); injector.register("logger", stubs.LoggerStub); injector.register("fs", { @@ -50,7 +50,10 @@ function createTestInjector(configuration?: { shouldNpmInstallThrow: boolean, np injector.register("projectTemplatesService", ProjectTemplatesService); - injector.register("analyticsService", { track: async () => undefined }); + injector.register("analyticsService", { + track: async (): Promise => undefined, + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() + }); return injector; } @@ -67,7 +70,7 @@ describe("project-templates-service", () => { it("when npm install fails", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: true, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: null }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - let tempFolder = temp.mkdirSync("preparetemplate"); + const tempFolder = temp.mkdirSync("preparetemplate"); await assert.isRejected(projectTemplatesService.prepareTemplate("invalidName", tempFolder)); }); }); @@ -76,8 +79,8 @@ describe("project-templates-service", () => { it("when reserved template name is used", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - let tempFolder = temp.mkdirSync("preparetemplate"); - let actualPathToTemplate = await projectTemplatesService.prepareTemplate("typescript", tempFolder); + const tempFolder = temp.mkdirSync("preparetemplate"); + const actualPathToTemplate = await projectTemplatesService.prepareTemplate("typescript", tempFolder); assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); }); @@ -85,8 +88,8 @@ describe("project-templates-service", () => { it("when reserved template name is used (case-insensitive test)", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - let tempFolder = temp.mkdirSync("preparetemplate"); - let actualPathToTemplate = await projectTemplatesService.prepareTemplate("tYpEsCriPT", tempFolder); + const tempFolder = temp.mkdirSync("preparetemplate"); + const actualPathToTemplate = await projectTemplatesService.prepareTemplate("tYpEsCriPT", tempFolder); assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); }); @@ -94,8 +97,8 @@ describe("project-templates-service", () => { it("uses defaultTemplate when undefined is passed as parameter", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - let tempFolder = temp.mkdirSync("preparetemplate"); - let actualPathToTemplate = await projectTemplatesService.prepareTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], tempFolder); + const tempFolder = temp.mkdirSync("preparetemplate"); + const actualPathToTemplate = await projectTemplatesService.prepareTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], tempFolder); assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); }); diff --git a/test/services/android-debug-service.ts b/test/services/android-debug-service.ts new file mode 100644 index 0000000000..478575b9af --- /dev/null +++ b/test/services/android-debug-service.ts @@ -0,0 +1,160 @@ +import { AndroidDebugService } from "../../lib/services/android-debug-service"; +import { Yok } from "../../lib/common/yok"; +import * as stubs from "../stubs"; +import { assert } from "chai"; + +const expectedDevToolsCommitSha = "02e6bde1bbe34e43b309d4ef774b1168d25fd024"; + +class AndroidDebugServiceInheritor extends AndroidDebugService { + constructor(protected $devicesService: Mobile.IDevicesService, + $errors: IErrors, + $logger: ILogger, + $androidDeviceDiscovery: Mobile.IDeviceDiscovery, + $androidProcessService: Mobile.IAndroidProcessService, + $net: INet) { + super({}, $devicesService, $errors, $logger, $androidDeviceDiscovery, $androidProcessService, $net); + } + + public getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string { + return super.getChromeDebugUrl(debugOptions, port); + } +} + +const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("devicesService", {}); + testInjector.register("errors", stubs.ErrorsStub); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register("androidDeviceDiscovery", {}); + testInjector.register("androidProcessService", {}); + testInjector.register("net", {}); + + return testInjector; +}; + +interface IChromeUrlTestCase { + debugOptions: IDebugOptions; + expectedChromeUrl: string; + scenarioName: string; +} + +describe("androidDebugService", () => { + describe("getChromeDebugUrl", () => { + const expectedPort = 12345; + const customDevToolsCommit = "customDevToolsCommit"; + + const chromUrlTestCases: IChromeUrlTestCase[] = [ + // Default CLI behavior: + { + scenarioName: "useBundledDevTools and useHttpUrl are not passed", + debugOptions: {}, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // When useBundledDevTools is true + { + scenarioName: "useBundledDevTools is true and useHttpUrl is not passed", + debugOptions: { + useBundledDevTools: true + }, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is true and useHttpUrl is false", + debugOptions: { + useBundledDevTools: true, + useHttpUrl: false + }, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is true and useHttpUrl is true", + debugOptions: { + useBundledDevTools: true, + useHttpUrl: true + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // When useBundledDevTools is false + { + scenarioName: "useBundledDevTools is false and useHttpUrl is not passed", + debugOptions: { + useBundledDevTools: false + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is false and useHttpUrl is false", + debugOptions: { + useBundledDevTools: false, + useHttpUrl: false + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is false and useHttpUrl is true", + debugOptions: { + useBundledDevTools: false, + useHttpUrl: true + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // When useBundledDevTools is not passed + { + scenarioName: "useBundledDevTools is not passed and useHttpUrl is false", + debugOptions: { + useHttpUrl: false + }, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is not passed and useHttpUrl is true", + debugOptions: { + useHttpUrl: true + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // devToolsCommit tests + { + scenarioName: "devToolsCommit defaults to ${expectedDevToolsCommitSha} when useBundledDevTools is set to false", + debugOptions: { + useBundledDevTools: false + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is disregarded when useBundledDevTools is not passed", + debugOptions: {}, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is set to passed value when useBundledDevTools is set to false", + debugOptions: { + useBundledDevTools: false, + devToolsCommit: customDevToolsCommit + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${customDevToolsCommit}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is set to passed value when useHttpUrl is set to true", + debugOptions: { + useHttpUrl: true, + devToolsCommit: customDevToolsCommit + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${customDevToolsCommit}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + } + ]; + + for (const testCase of chromUrlTestCases) { + it(`returns correct url when ${testCase.scenarioName}`, () => { + const testInjector = createTestInjector(); + const androidDebugService = testInjector.resolve(AndroidDebugServiceInheritor); + const actualChromeUrl = androidDebugService.getChromeDebugUrl(testCase.debugOptions, expectedPort); + assert.equal(actualChromeUrl, testCase.expectedChromeUrl); + }); + } + }); +}); diff --git a/test/services/debug-service.ts b/test/services/debug-service.ts new file mode 100644 index 0000000000..e3ab301b44 --- /dev/null +++ b/test/services/debug-service.ts @@ -0,0 +1,235 @@ +import { DebugService } from "../../lib/services/debug-service"; +import { Yok } from "../../lib/common/yok"; +import * as stubs from "../stubs"; +import { assert } from "chai"; +import { EventEmitter } from "events"; +import * as constants from "../../lib/common/constants"; +import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors } from "../../lib/constants"; + +const fakeChromeDebugPort = 123; +const fakeChromeDebugUrl = `fakeChromeDebugUrl?experiments=true&ws=localhost:${fakeChromeDebugPort}`; +const defaultDeviceIdentifier = "Nexus5"; + +class PlatformDebugService extends EventEmitter /* implements IPlatformDebugService */ { + public async debug(debugData: IDebugData, debugOptions: IDebugOptions): Promise { + return fakeChromeDebugUrl; + } +} + +interface IDebugTestDeviceInfo { + deviceInfo: { + status: string; + platform: string; + identifier: string; + }; + + isEmulator: boolean; +} + +interface IDebugTestData { + isDeviceFound: boolean; + deviceInformation: IDebugTestDeviceInfo; + isApplicationInstalledOnDevice: boolean; + hostInfo: { + isWindows: boolean; + isDarwin: boolean; + }; +} + +const getDefaultDeviceInformation = (platform?: string): IDebugTestDeviceInfo => ({ + deviceInfo: { + status: constants.CONNECTED_STATUS, + platform: platform || "Android", + identifier: defaultDeviceIdentifier + }, + + isEmulator: false +}); + +const getDefaultTestData = (platform?: string): IDebugTestData => ({ + isDeviceFound: true, + deviceInformation: getDefaultDeviceInformation(platform), + isApplicationInstalledOnDevice: true, + hostInfo: { + isWindows: false, + isDarwin: true + } +}); + +describe("debugService", () => { + const getTestInjectorForTestConfiguration = (testData: IDebugTestData): IInjector => { + const testInjector = new Yok(); + testInjector.register("devicesService", { + getDeviceByIdentifier: (identifier: string): Mobile.IDevice => { + return testData.isDeviceFound ? + { + deviceInfo: testData.deviceInformation.deviceInfo, + + applicationManager: { + isApplicationInstalled: async (appIdentifier: string): Promise => testData.isApplicationInstalledOnDevice + }, + + isEmulator: testData.deviceInformation.isEmulator + } : null; + } + }); + + testInjector.register("androidDebugService", PlatformDebugService); + + testInjector.register("iOSDebugService", PlatformDebugService); + + testInjector.register("mobileHelper", { + isAndroidPlatform: (platform: string) => { + return platform.toLowerCase() === "android"; + }, + isiOSPlatform: (platform: string) => { + return platform.toLowerCase() === "ios"; + } + }); + + testInjector.register("errors", stubs.ErrorsStub); + + testInjector.register("hostInfo", testData.hostInfo); + + testInjector.register("logger", stubs.LoggerStub); + + testInjector.register("analyticsService", { + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() + }); + + return testInjector; + }; + + describe("debug", () => { + const getDebugData = (deviceIdentifier?: string): IDebugData => ({ + applicationIdentifier: "org.nativescript.app1", + deviceIdentifier: deviceIdentifier || defaultDeviceIdentifier, + projectDir: "/Users/user/app1", + projectName: "app1" + }); + + describe("rejects the result promise when", () => { + const assertIsRejected = async (testData: IDebugTestData, expectedError: string, userSpecifiedOptions?: IDebugOptions): Promise => { + const testInjector = getTestInjectorForTestConfiguration(testData); + const debugService = testInjector.resolve(DebugService); + + const debugData = getDebugData(); + await assert.isRejected(debugService.debug(debugData, userSpecifiedOptions), expectedError); + }; + + it("there's no attached device as the specified identifier", async () => { + const testData = getDefaultTestData(); + testData.isDeviceFound = false; + + await assertIsRejected(testData, "Cannot find device with identifier"); + }); + + it("the device is not trusted", async () => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.status = constants.UNREACHABLE_STATUS; + + await assertIsRejected(testData, "is unreachable. Make sure it is Trusted "); + }); + + it("the application is not installed on device", async () => { + const testData = getDefaultTestData(); + testData.isApplicationInstalledOnDevice = false; + + await assertIsRejected(testData, "is not installed on device with identifier"); + }); + + it("the OS is neither Windows or macOS and device is iOS", async () => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.platform = "iOS"; + testData.hostInfo.isDarwin = testData.hostInfo.isWindows = false; + + await assertIsRejected(testData, "Debugging on iOS devices is not supported for"); + }); + + it("device is neither iOS or Android", async () => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.platform = "WP8"; + + await assertIsRejected(testData, DebugCommandErrors.UNSUPPORTED_DEVICE_OS_FOR_DEBUGGING); + }); + + it("when trying to debug on iOS Simulator on macOS, debug-brk is passed, but pathToAppPackage is not", async () => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.platform = "iOS"; + testData.deviceInformation.isEmulator = true; + + await assertIsRejected(testData, "To debug on iOS simulator you need to provide path to the app package.", { debugBrk: true }); + }); + + const assertIsRejectedWhenPlatformDebugServiceFails = async (platform: string): Promise => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.platform = platform; + + const testInjector = getTestInjectorForTestConfiguration(testData); + const expectedErrorMessage = "Platform specific error"; + const platformDebugService = testInjector.resolve(`${platform}DebugService`); + platformDebugService.debug = async (debugData: IDebugData, debugOptions: IDebugOptions): Promise => { + throw new Error(expectedErrorMessage); + }; + + const debugService = testInjector.resolve(DebugService); + + const debugData = getDebugData(); + await assert.isRejected(debugService.debug(debugData, null), expectedErrorMessage); + }; + + it("androidDebugService's debug method fails", async () => { + await assertIsRejectedWhenPlatformDebugServiceFails("android"); + }); + + it("iOSDebugService's debug method fails", async () => { + await assertIsRejectedWhenPlatformDebugServiceFails("iOS"); + }); + }); + + describe(`raises ${CONNECTION_ERROR_EVENT_NAME} event`, () => { + _.each(["android", "iOS"], platform => { + it(`when ${platform}DebugService raises ${CONNECTION_ERROR_EVENT_NAME} event`, async () => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.platform = platform; + + const testInjector = getTestInjectorForTestConfiguration(testData); + const debugService = testInjector.resolve(DebugService); + let dataRaisedForConnectionError: any = null; + debugService.on(CONNECTION_ERROR_EVENT_NAME, (data: any) => { + dataRaisedForConnectionError = data; + }); + + const debugData = getDebugData(); + await assert.isFulfilled(debugService.debug(debugData, null)); + + const expectedErrorData = { deviceId: "deviceId", message: "my message", code: 2048 }; + const platformDebugService = testInjector.resolve(`${platform}DebugService`); + platformDebugService.emit(CONNECTION_ERROR_EVENT_NAME, expectedErrorData); + assert.deepEqual(dataRaisedForConnectionError, expectedErrorData); + }); + }); + }); + + describe("returns chrome url along with port returned by platform specific debug service", () => { + _.each(["android", "iOS"], platform => { + it(`for ${platform} device`, async () => { + const testData = getDefaultTestData(); + testData.deviceInformation.deviceInfo.platform = platform; + + const testInjector = getTestInjectorForTestConfiguration(testData); + const debugService = testInjector.resolve(DebugService); + + const debugData = getDebugData(); + const debugInfo = await debugService.debug(debugData, null); + + assert.deepEqual(debugInfo, { + url: fakeChromeDebugUrl, + port: fakeChromeDebugPort, + deviceIdentifier: debugData.deviceIdentifier + }); + }); + }); + }); + }); +}); diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts index 3e02472ee8..3243331703 100644 --- a/test/services/extensibility-service.ts +++ b/test/services/extensibility-service.ts @@ -72,12 +72,12 @@ describe("extensibilityService", () => { fs.readDirectory = (dir: string): string[] => [userSpecifiedValue]; const npm: INodePackageManager = testInjector.resolve("npm"); - let argsPassedToNpmInstall: any = {}; + const argsPassedToNpmInstall: any = {}; npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { argsPassedToNpmInstall.packageName = packageName; argsPassedToNpmInstall.pathToSave = pathToSave; argsPassedToNpmInstall.config = config; - return [userSpecifiedValue]; + return { name: userSpecifiedValue }; }; const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); @@ -130,43 +130,12 @@ describe("extensibilityService", () => { fs.readDirectory = (dir: string): string[] => [extensionName]; const npm: INodePackageManager = testInjector.resolve("npm"); - npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => [extensionName]; + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => ({ name: extensionName }); const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); const actualResult = await extensibilityService.installExtension(extensionName); assert.deepEqual(actualResult, { extensionName }); }); - - it("throws error that has extensionName property when unable to load extension", async () => { - const expectedErrorMessage = "Require failed"; - - const extensionName = "extension1"; - const testInjector = getTestInjector(); - const fs: IFileSystem = testInjector.resolve("fs"); - fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionName; - - fs.readDirectory = (dir: string): string[] => [extensionName]; - - const npm: INodePackageManager = testInjector.resolve("npm"); - npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => [extensionName]; - - const requireService: IRequireService = testInjector.resolve("requireService"); - requireService.require = (pathToRequire: string) => { - throw new Error(expectedErrorMessage); - }; - - const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); - let isErrorRaised = false; - try { - await extensibilityService.installExtension(extensionName); - } catch (err) { - isErrorRaised = true; - assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); - assert.deepEqual(err.extensionName, extensionName); - } - - assert.isTrue(isErrorRaised); - }); }); describe("loadExtensions", () => { @@ -230,7 +199,7 @@ describe("extensibilityService", () => { npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { assert.deepEqual(packageName, extensionNames[0]); isNpmInstallCalled = true; - return [packageName]; + return { name: packageName }; }; const expectedResults: IExtensionData[] = _.map(extensionNames, extensionName => ({ extensionName })); @@ -269,7 +238,7 @@ describe("extensibilityService", () => { }; const expectedResults: any[] = _.map(extensionNames, extensionName => ({ extensionName })); - expectedResults[0] = new Error("Unable to load extension extension1. You will not be able to use the functionality that it adds."); + expectedResults[0] = new Error("Unable to load extension extension1. You will not be able to use the functionality that it adds. Error: Unable to load module."); const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); const promises = extensibilityService.loadExtensions(); assert.deepEqual(promises.length, extensionNames.length); @@ -282,13 +251,14 @@ describe("extensibilityService", () => { assert.deepEqual(err.message, expectedResults[index].message); assert.deepEqual(err.extensionName, extensionNames[index]); }); - }; + } }); it("rejects all promises when unable to read node_modules dir (simulate EPERM error)", async () => { const testInjector = getTestInjector(); const extensionNames = ["extension1", "extension2", "extension3"]; const fs: IFileSystem = testInjector.resolve("fs"); + const expectedErrorMessage = `Unable to read ${constants.NODE_MODULES_FOLDER_NAME} dir.`; fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) === "extensions" || path.basename(pathToCheck) === constants.PACKAGE_JSON_FILE_NAME; fs.readJson = (filename: string, encoding?: string): any => { const dependencies: any = {}; @@ -303,7 +273,7 @@ describe("extensibilityService", () => { fs.readDirectory = (dir: string): string[] => { isReadDirCalled = true; assert.deepEqual(path.basename(dir), constants.NODE_MODULES_FOLDER_NAME); - throw new Error(`Unable to read ${constants.NODE_MODULES_FOLDER_NAME} dir.`); + throw new Error(expectedErrorMessage); }; const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); @@ -314,10 +284,10 @@ describe("extensibilityService", () => { await loadExtensionPromise.then(res => { throw new Error("Shouldn't get here!"); }, err => { const extensionName = extensionNames[index]; - assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds. Error: ${expectedErrorMessage}`); assert.deepEqual(err.extensionName, extensionName); }); - }; + } assert.deepEqual(promises.length, extensionNames.length); assert.isTrue(isReadDirCalled, "readDirectory should have been called for the extensions."); @@ -326,6 +296,9 @@ describe("extensibilityService", () => { it("rejects all promises when unable to install extensions to extension dir (simulate EPERM error)", async () => { const testInjector = getTestInjector(); const extensionNames = ["extension1", "extension2", "extension3"]; + const expectedErrorMessages = ["Unable to install to node_modules dir.", + "expected 'extension2' to deeply equal 'extension1'", + "expected 'extension3' to deeply equal 'extension1'"]; const fs: IFileSystem = testInjector.resolve("fs"); fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) === "extensions" || path.basename(pathToCheck) === constants.PACKAGE_JSON_FILE_NAME; fs.readJson = (filename: string, encoding?: string): any => { @@ -346,10 +319,11 @@ describe("extensibilityService", () => { let isNpmInstallCalled = false; const npm: INodePackageManager = testInjector.resolve("npm"); + const expectedErrorMessage = `Unable to install to ${constants.NODE_MODULES_FOLDER_NAME} dir.`; npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => { assert.deepEqual(packageName, extensionNames[0]); isNpmInstallCalled = true; - throw new Error(`Unable to install to ${constants.NODE_MODULES_FOLDER_NAME} dir.`); + throw new Error(expectedErrorMessage); }; const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); @@ -358,14 +332,15 @@ describe("extensibilityService", () => { for (let index = 0; index < promises.length; index++) { const loadExtensionPromise = promises[index]; await loadExtensionPromise.then(res => { - console.log("######### res = ", res); throw new Error("Shouldn't get here!"); + throw new Error("Shouldn't get here!"); }, + err => { const extensionName = extensionNames[index]; - assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds.`); + assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds. Error: ${expectedErrorMessages[index]}`); assert.deepEqual(err.extensionName, extensionName); }); - }; + } assert.deepEqual(promises.length, extensionNames.length); assert.isTrue(isNpmInstallCalled, "Npm install should have been called for the extensions."); @@ -493,7 +468,7 @@ describe("extensibilityService", () => { fs.readDirectory = (dir: string): string[] => [userSpecifiedValue]; const npm: INodePackageManager = testInjector.resolve("npm"); - let argsPassedToNpmInstall: any = {}; + const argsPassedToNpmInstall: any = {}; npm.uninstall = async (packageName: string, config?: any, path?: string): Promise => { argsPassedToNpmInstall.packageName = packageName; argsPassedToNpmInstall.pathToSave = path; @@ -609,4 +584,37 @@ describe("extensibilityService", () => { assert.deepEqual(extensibilityService.getInstalledExtensions(), dependencies); }); }); + + describe("loadExtension", () => { + it("throws error that has extensionName property when unable to load extension", async () => { + const expectedErrorMessage = "Require failed"; + + const extensionName = "extension1"; + const testInjector = getTestInjector(); + const fs: IFileSystem = testInjector.resolve("fs"); + fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionName; + + fs.readDirectory = (dir: string): string[] => [extensionName]; + + const npm: INodePackageManager = testInjector.resolve("npm"); + npm.install = async (packageName: string, pathToSave: string, config?: any): Promise => ({ name: extensionName }); + + const requireService: IRequireService = testInjector.resolve("requireService"); + requireService.require = (pathToRequire: string) => { + throw new Error(expectedErrorMessage); + }; + + const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); + let isErrorRaised = false; + try { + await extensibilityService.loadExtension(extensionName); + } catch (err) { + isErrorRaised = true; + assert.deepEqual(err.message, `Unable to load extension ${extensionName}. You will not be able to use the functionality that it adds. Error: ${expectedErrorMessage}`); + assert.deepEqual(err.extensionName, extensionName); + } + + assert.isTrue(isErrorRaised); + }); + }); }); diff --git a/test/services/ios-debug-service.ts b/test/services/ios-debug-service.ts new file mode 100644 index 0000000000..23af05478b --- /dev/null +++ b/test/services/ios-debug-service.ts @@ -0,0 +1,189 @@ +import { IOSDebugService } from "../../lib/services/ios-debug-service"; +import { Yok } from "../../lib/common/yok"; +import * as stubs from "../stubs"; +import { assert } from "chai"; + +const expectedDevToolsCommitSha = "02e6bde1bbe34e43b309d4ef774b1168d25fd024"; + +class IOSDebugServiceInheritor extends IOSDebugService { + constructor(protected $devicesService: Mobile.IDevicesService, + $platformService: IPlatformService, + $iOSEmulatorServices: Mobile.IEmulatorPlatformServices, + $childProcess: IChildProcess, + $hostInfo: IHostInfo, + $logger: ILogger, + $errors: IErrors, + $npmInstallationManager: INpmInstallationManager, + $iOSNotification: IiOSNotification, + $iOSSocketRequestExecutor: IiOSSocketRequestExecutor, + $processService: IProcessService, + $socketProxyFactory: ISocketProxyFactory) { + super({}, $devicesService, $platformService, $iOSEmulatorServices, $childProcess, $hostInfo, $logger, $errors, + $npmInstallationManager, $iOSNotification, $iOSSocketRequestExecutor, $processService, $socketProxyFactory); + } + + public getChromeDebugUrl(debugOptions: IDebugOptions, port: number): string { + return super.getChromeDebugUrl(debugOptions, port); + } +} + +const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("devicesService", {}); + testInjector.register("platformService", {}); + testInjector.register("iOSEmulatorServices", {}); + testInjector.register("childProcess", {}); + + testInjector.register("errors", stubs.ErrorsStub); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register("hostInfo", {}); + testInjector.register("npmInstallationManager", {}); + testInjector.register("iOSNotification", {}); + testInjector.register("iOSSocketRequestExecutor", {}); + testInjector.register("processService", { + attachToProcessExitSignals: (context: any, callback: () => void): void => undefined + }); + + testInjector.register("socketProxyFactory", { + on: (event: string | symbol, listener: Function): any => undefined + }); + + return testInjector; +}; + +interface IChromeUrlTestCase { + debugOptions: IDebugOptions; + expectedChromeUrl: string; + scenarioName: string; +} + +describe("iOSDebugService", () => { + describe("getChromeDebugUrl", () => { + const expectedPort = 12345; + const customDevToolsCommit = "customDevToolsCommit"; + + const chromUrlTestCases: IChromeUrlTestCase[] = [ + // Default CLI behavior: + { + scenarioName: "useBundledDevTools and useHttpUrl are not passed", + debugOptions: {}, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // When useBundledDevTools is true + { + scenarioName: "useBundledDevTools is true and useHttpUrl is not passed", + debugOptions: { + useBundledDevTools: true + }, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is true and useHttpUrl is false", + debugOptions: { + useBundledDevTools: true, + useHttpUrl: false + }, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is true and useHttpUrl is true", + debugOptions: { + useBundledDevTools: true, + useHttpUrl: true + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // When useBundledDevTools is false + { + scenarioName: "useBundledDevTools is false and useHttpUrl is not passed", + debugOptions: { + useBundledDevTools: false + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is false and useHttpUrl is false", + debugOptions: { + useBundledDevTools: false, + useHttpUrl: false + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is false and useHttpUrl is true", + debugOptions: { + useBundledDevTools: false, + useHttpUrl: true + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // When useBundledDevTools is not passed + { + scenarioName: "useBundledDevTools is not passed and useHttpUrl is false", + debugOptions: { + useHttpUrl: false + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "useBundledDevTools is not passed and useHttpUrl is true", + debugOptions: { + useHttpUrl: true + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + + // devToolsCommit tests + { + scenarioName: "devToolsCommit defaults to ${expectedDevToolsCommitSha} and is used in result when useBundledDevTools is not passed", + debugOptions: {}, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${expectedDevToolsCommitSha}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is set to passed value when useBundledDevTools is not passed", + debugOptions: { + devToolsCommit: customDevToolsCommit + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${customDevToolsCommit}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is set to passed value when useBundledDevTools is set to false", + debugOptions: { + useBundledDevTools: false, + devToolsCommit: customDevToolsCommit + }, + expectedChromeUrl: `chrome-devtools://devtools/remote/serve_file/@${customDevToolsCommit}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is set to passed value when useHttpUrl is set to true", + debugOptions: { + useHttpUrl: true, + devToolsCommit: customDevToolsCommit + }, + expectedChromeUrl: `https://chrome-devtools-frontend.appspot.com/serve_file/@${customDevToolsCommit}/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + }, + { + scenarioName: "devToolsCommit is disregarded when useBundledDevTools is set to true", + debugOptions: { + useBundledDevTools: true, + devToolsCommit: customDevToolsCommit + }, + expectedChromeUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=localhost:${expectedPort}`, + } + + ]; + + for (const testCase of chromUrlTestCases) { + it(`returns correct url when ${testCase.scenarioName}`, () => { + const testInjector = createTestInjector(); + const iOSDebugService = testInjector.resolve(IOSDebugServiceInheritor); + const actualChromeUrl = iOSDebugService.getChromeDebugUrl(testCase.debugOptions, expectedPort); + assert.equal(actualChromeUrl, testCase.expectedChromeUrl); + }); + } + + }); +}); diff --git a/test/services/ios-log-filter.ts b/test/services/ios-log-filter.ts new file mode 100644 index 0000000000..eba20f5ffe --- /dev/null +++ b/test/services/ios-log-filter.ts @@ -0,0 +1,135 @@ +import { IOSLogFilter } from "../../lib/services/ios-log-filter"; +import { Yok } from "../../lib/common/yok"; +import { LoggingLevels } from "../../lib/common/mobile/logging-levels"; +import * as assert from "assert"; + +function createTestInjector(): IInjector { + const testInjector = new Yok(); + testInjector.register("loggingLevels", LoggingLevels); + testInjector.register("fs", { + exists: () => false + }); + testInjector.register("projectData", { + initializeProjectData: () => { /* empty */ }, + projectDir: "test" + }); + + return testInjector; +} + +describe("iOSLogFilter", () => { + let testInjector: IInjector; + let logFilter: Mobile.IPlatformLogFilter; + const testData = [ + { + version: 9, + originalDataArr: [ + "May 24 15:54:38 Dragons-iPhone backboardd(BaseBoard)[62] : Unable to bootstrap_look_up port with name .gsEvents: unknown error code (1102)", + "May 24 15:54:51 Dragons-iPhone locationd[67] : Client com.apple.springboard disconnected", + "May 24 14:44:59 iPad-90 NativeScript250[790] : CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", + "May 24 14:44:59 iPad-90 NativeScript250[790] : CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", + "May 24 14:44:59 iPad-90 mobile_installation_proxy[355] : 0x1f197000 LoadInfoPlist: Failed to create CFBundle from URL file:///private/var/mobile/Containers/Bundle/Application/EB4866CC-25D2-4A3B-AA6C-70FFA08B908E/NativeScript143.app", + "May 24 14:44:59 iPad-90 mobile_installation_proxy[355] : 0x1f197000 LoadInfoPlist: Failed to create CFBundle from URL file:///private/var/mobile/Containers/Bundle/Application/0DA02818-DCAE-407C-979D-D55F4F36F8D2/NativeScript300.app", + " May 24 14:44:59 iPad-90 mobile_installation_proxy[355] : 0x1f197000 LoadInfoPlist: Failed to create CFBundle from URL file:///private/var/mobile/Containers/Bundle/Application/B0EE9362-7BDD-4FF2-868F-857B76D9D8D3/Cordova370.app", + " May 24 14:44:59 iPad-90 NativeScript250[790] : CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3472:32: EXCEPTION: Uncaught (in promise): Error: CUSTOM EXCEPTION", + "Aug 22 10:59:20 MCSOFAPPBLD TestApp[52946]: CONSOLE LOG file:///app/home/home-view-model.js:6:20: CUSTOM CONSOLE LOG", + "" + ], + infoExpectedArr: [ + null, + null, + "CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", + "CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", + null, + null, + null, + "CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3472:32: EXCEPTION: Uncaught (in promise): Error: CUSTOM EXCEPTION", + "CONSOLE LOG file:///app/home/home-view-model.js:6:20: CUSTOM CONSOLE LOG", + "" + ] + }, + { + version: 10, + originalDataArr: [ + "May 24 15:54:52 Dragons-iPhone apsd(PersistentConnection)[90] : 2017-05-24 15:54:52 +0300 apsd[90]: performing call back", + "May 24 15:54:52 Dragons-iPhone NativeScript250(NativeScript)[356] : CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", + "May 24 15:54:52 Dragons-iPhone NativeScript250(NativeScript)[356] : CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3472:32: EXCEPTION: Uncaught (in promise): Error: CUSTOM EXCEPTION", + " May 24 15:54:52 Dragons-iPhone NativeScript250(NativeScript)[356] : CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3477:36: ORIGINAL STACKTRACE:", + " May 24 15:54:52 Dragons-iPhone NativeScript250(NativeScript)[356] : CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3478:36: resolvePromise@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:416:40", + "resolvePromise@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:401:31", + "file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:449:31", + "invokeTask@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:223:42", + "onInvokeTask@file:///app/tns_modules/@angular/core/bundles/core.umd.js:4382:51", + "invokeTask@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:222:54", + "runTask@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:123:57", + "drainMicroTaskQueue@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:355:42", + "promiseReactionJob@[native code]", + "UIApplicationMain@[native code]", + "start@file:///app/tns_modules/tns-core-modules/application/application.js:251:26", + "bootstrapApp@file:///app/tns_module", + "Aug 22 10:59:20 MCSOFAPPBLD TestApp[52946]: CONSOLE LOG file:///app/home/home-view-model.js:6:20: CUSTOM CONSOLE LOG", + "" + ], + infoExpectedArr: [ + null, + "CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", + "CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3472:32: EXCEPTION: Uncaught (in promise): Error: CUSTOM EXCEPTION", + "CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3477:36: ORIGINAL STACKTRACE:", + "CONSOLE ERROR file:///app/tns_modules/@angular/core/bundles/core.umd.js:3478:36: resolvePromise@file:///app/tns_modules/nativescript-angular/zone-js/dist/zone-nativescript.js:416:40", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + "CONSOLE LOG file:///app/home/home-view-model.js:6:20: CUSTOM CONSOLE LOG", + "" + ] + } + ]; + const infoLogLevel = "INFO"; + const fullLogLevel = "FULL"; + + beforeEach(() => { + testInjector = createTestInjector(); + logFilter = testInjector.resolve(IOSLogFilter); + }); + + describe("filterData", () => { + testData.forEach(data => { + it(`returns correct data when logLevel is ${fullLogLevel} on iOS ${data.version} and all data is passed at once`, () => { + const actualData = logFilter.filterData(data.originalDataArr.join("\n"), fullLogLevel, null); + const actualArr = actualData.split("\n").map(line => line.trim()); + const expectedArr = data.originalDataArr.map(line => line.trim()); + assert.deepEqual(actualArr, expectedArr); + }); + + it(`returns correct data when logLevel is ${fullLogLevel} on iOS ${data.version} and data is passed one line at a time`, () => { + data.originalDataArr.forEach(line => { + const actualData = logFilter.filterData(line, fullLogLevel, null); + assert.deepEqual(actualData.trim(), line.trim()); + }); + }); + + it(`parses data incorrectly when logLevel is ${infoLogLevel} on iOS ${data.version} and all data is passed at once`, () => { + const actualData = logFilter.filterData(data.originalDataArr.join("\n"), infoLogLevel, null); + const actualArr = actualData.split("\n").map(line => line.trim()); + const expectedArr = ["CONSOLE LOG file:///app/modules/homeView/homeView.component.js:13:24: CUSTOM CONSOLE LOG", ""]; + assert.deepEqual(actualArr, expectedArr); + }); + + it(`returns correct data when logLevel is ${infoLogLevel} on iOS ${data.version} and data is passed one line at a time`, () => { + data.originalDataArr.forEach((line, index) => { + const actualData = logFilter.filterData(line, infoLogLevel, null); + const expectedData = data.infoExpectedArr[index]; + assert.deepEqual(actualData && actualData.trim(), expectedData && expectedData); + }); + }); + }); + }); +}); diff --git a/test/services/livesync-service.ts b/test/services/livesync-service.ts new file mode 100644 index 0000000000..3279bea316 --- /dev/null +++ b/test/services/livesync-service.ts @@ -0,0 +1,159 @@ +import { Yok } from "../../lib/common/yok"; +import { assert } from "chai"; +import { LiveSyncService } from "../../lib/services/livesync/livesync-service"; +import { LoggerStub } from "../stubs"; + +const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + + testInjector.register("platformService", {}); + testInjector.register("projectDataService", { + getProjectData: (projectDir: string): IProjectData => ({}) + }); + + testInjector.register("devicesService", {}); + testInjector.register("mobileHelper", {}); + testInjector.register("devicePlatformsConstants", {}); + testInjector.register("nodeModulesDependenciesBuilder", {}); + testInjector.register("logger", LoggerStub); + testInjector.register("processService", {}); + testInjector.register("debugService", {}); + testInjector.register("errors", {}); + testInjector.register("debugDataService", {}); + testInjector.register("hooksService", { + executeAfterHooks: (commandName: string, hookArguments?: IDictionary): Promise => Promise.resolve() + }); + + testInjector.register("pluginsService", {}); + testInjector.register("analyticsService", {}); + testInjector.register("injector", testInjector); + + return testInjector; +}; + +class LiveSyncServiceInheritor extends LiveSyncService { + constructor($platformService: IPlatformService, + $projectDataService: IProjectDataService, + $devicesService: Mobile.IDevicesService, + $mobileHelper: Mobile.IMobileHelper, + $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + $logger: ILogger, + $processService: IProcessService, + $hooksService: IHooksService, + $pluginsService: IPluginsService, + $debugService: IDebugService, + $errors: IErrors, + $debugDataService: IDebugDataService, + $analyticsService: IAnalyticsService, + $injector: IInjector) { + + super( + $platformService, + $projectDataService, + $devicesService, + $mobileHelper, + $devicePlatformsConstants, + $nodeModulesDependenciesBuilder, + $logger, + $processService, + $hooksService, + $pluginsService, + $debugService, + $errors, + $debugDataService, + $analyticsService, + $injector + ); + } + + public liveSyncProcessesInfo: IDictionary = {}; +} + +interface IStopLiveSyncTestCase { + name: string; + currentDeviceIdentifiers: string[]; + expectedDeviceIdentifiers: string[]; + deviceIdentifiersToBeStopped?: string[]; +} + +describe("liveSyncService", () => { + describe("stopLiveSync", () => { + const getLiveSyncProcessInfo = (): ILiveSyncProcessInfo => ({ + actionsChain: Promise.resolve(), + currentSyncAction: Promise.resolve(), + isStopped: false, + timer: setTimeout(() => undefined, 1000), + watcherInfo: { + watcher: { + close: (): any => undefined + }, + pattern: "pattern" + }, + deviceDescriptors: [] + }); + + const getDeviceDescriptor = (identifier: string): ILiveSyncDeviceInfo => ({ + identifier, + outputPath: "", + skipNativePrepare: false, + platformSpecificOptions: null, + buildAction: () => Promise.resolve("") + }); + + const testCases: IStopLiveSyncTestCase[] = [ + { + name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers", + currentDeviceIdentifiers: ["device1", "device2", "device3"], + expectedDeviceIdentifiers: ["device1", "device2", "device3"] + }, + { + name: "stops LiveSync operation for all devices and emits liveSyncStopped for all of them when stopLiveSync is called without deviceIdentifiers (when a single device is attached)", + currentDeviceIdentifiers: ["device1"], + expectedDeviceIdentifiers: ["device1"] + }, + { + name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them (when a single device is attached)", + currentDeviceIdentifiers: ["device1"], + expectedDeviceIdentifiers: ["device1"], + deviceIdentifiersToBeStopped: ["device1"] + }, + { + name: "stops LiveSync operation for specified devices and emits liveSyncStopped for each of them", + currentDeviceIdentifiers: ["device1", "device2", "device3"], + expectedDeviceIdentifiers: ["device1", "device3"], + deviceIdentifiersToBeStopped: ["device1", "device3"] + }, + { + name: "does not raise liveSyncStopped event for device, which is not currently being liveSynced", + currentDeviceIdentifiers: ["device1", "device2", "device3"], + expectedDeviceIdentifiers: ["device1"], + deviceIdentifiersToBeStopped: ["device1", "device4"] + } + ]; + + for (const testCase of testCases) { + it(testCase.name, async () => { + const testInjector = createTestInjector(); + const liveSyncService = testInjector.resolve(LiveSyncServiceInheritor); + const projectDir = "projectDir"; + const emittedDeviceIdentifiersForLiveSyncStoppedEvent: string[] = []; + liveSyncService.on("liveSyncStopped", (data: { projectDir: string, deviceIdentifier: string }) => { + assert.equal(data.projectDir, projectDir); + emittedDeviceIdentifiersForLiveSyncStoppedEvent.push(data.deviceIdentifier); + }); + + // Setup liveSyncProcessesInfo for current test + liveSyncService.liveSyncProcessesInfo[projectDir] = getLiveSyncProcessInfo(); + const deviceDescriptors = testCase.currentDeviceIdentifiers.map(d => getDeviceDescriptor(d)); + liveSyncService.liveSyncProcessesInfo[projectDir].deviceDescriptors.push(...deviceDescriptors); + + await liveSyncService.stopLiveSync(projectDir, testCase.deviceIdentifiersToBeStopped); + + assert.deepEqual(emittedDeviceIdentifiersForLiveSyncStoppedEvent, testCase.expectedDeviceIdentifiers); + }); + } + + }); + +}); diff --git a/test/services/project-data-service.ts b/test/services/project-data-service.ts index f68c29d449..71112f4abb 100644 --- a/test/services/project-data-service.ts +++ b/test/services/project-data-service.ts @@ -53,13 +53,15 @@ const createTestInjector = (readTextData?: string): IInjector => { testInjector.register("projectDataService", ProjectDataService); + testInjector.register("injector", testInjector); + return testInjector; }; describe("projectDataService", () => { const generateJsonDataFromTestData = (currentTestData: any, skipNativeScriptKey?: boolean) => { const props = currentTestData.propertyName.split("."); - let data: any = {}; + const data: any = {}; let currentData: any = skipNativeScriptKey ? data : (data[CLIENT_NAME_KEY_IN_PROJECT_FILE] = {}); _.each(props, (prop, index: number) => { @@ -155,7 +157,7 @@ describe("projectDataService", () => { const props = currentTestData.propertyName.split("."); props.splice(props.length - 1, 1); - let data: any = {}; + const data: any = {}; let currentData: any = data[CLIENT_NAME_KEY_IN_PROJECT_FILE] = {}; _.each(props, (prop) => { @@ -212,7 +214,7 @@ describe("projectDataService", () => { describe("removeDependency", () => { it("removes specified dependency from project file", () => { - let currentTestData = { + const currentTestData = { propertyName: "dependencies.myDeps", propertyValue: "1.0.0" }; diff --git a/test/services/subscription-service.ts b/test/services/subscription-service.ts new file mode 100644 index 0000000000..c9fc7548e6 --- /dev/null +++ b/test/services/subscription-service.ts @@ -0,0 +1,286 @@ +import { Yok } from "../../lib/common/yok"; +import { assert } from "chai"; +import { SubscriptionService } from "../../lib/services/subscription-service"; +import { LoggerStub } from "../stubs"; +import { stringify } from "querystring"; +const helpers = require("../../lib/common/helpers"); + +interface IValidateTestData { + name: string; + valuePassedToValidate: string; + expectedResult: boolean | string; +} + +const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("logger", LoggerStub); + + testInjector.register("userSettingsService", { + getSettingValue: async (value: string) => true, + saveSetting: async (key: string, value: any): Promise => undefined + }); + + testInjector.register("prompter", { + get: async (schemas: IPromptSchema[]): Promise => ({ + inputEmail: "SomeEmail" + }) + }); + + testInjector.register("httpClient", { + httpRequest: async (options: any, proxySettings?: IProxySettings): Promise => undefined + }); + + return testInjector; +}; + +class SubscriptionServiceTester extends SubscriptionService { + public shouldAskForEmailResult: boolean = null; + + constructor($httpClient: Server.IHttpClient, + $prompter: IPrompter, + $userSettingsService: IUserSettingsService, + $logger: ILogger) { + super($httpClient, $prompter, $userSettingsService, $logger); + } + + public async shouldAskForEmail(): Promise { + if (this.shouldAskForEmailResult !== null) { + return this.shouldAskForEmailResult; + } + + return super.shouldAskForEmail(); + } +} + +describe("subscriptionService", () => { + describe("shouldAskForEmail", () => { + describe("returns false", () => { + it("when terminal is not interactive", async () => { + const originalIsInteractive = helpers.isInteractive; + helpers.isInteractive = () => false; + + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + const shouldAskForEmailResult = await subscriptionService.shouldAskForEmail(); + + helpers.isInteractive = originalIsInteractive; + + assert.isFalse(shouldAskForEmailResult, "When console is not interactive, we should not ask for email."); + }); + + it("when environment variable CLI_NOPROMPT is set to 1", async () => { + const originalIsInteractive = helpers.isInteractive; + helpers.isInteractive = () => true; + + const originalCliNoPrompt = process.env.CLI_NOPROMPT; + process.env.CLI_NOPROMPT = "1"; + + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + const shouldAskForEmailResult = await subscriptionService.shouldAskForEmail(); + + helpers.isInteractive = originalIsInteractive; + process.env.CLI_NOPROMPT = originalCliNoPrompt; + + assert.isFalse(shouldAskForEmailResult, "When the environment variable CLI_NOPROMPT is set to 1, we should not ask for email."); + }); + + it("when user had already been asked for mail", async () => { + const originalIsInteractive = helpers.isInteractive; + helpers.isInteractive = () => true; + + const originalCliNoPrompt = process.env.CLI_NOPROMPT; + process.env.CLI_NOPROMPT = "random_value"; + + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + const shouldAskForEmailResult = await subscriptionService.shouldAskForEmail(); + + helpers.isInteractive = originalIsInteractive; + process.env.CLI_NOPROMPT = originalCliNoPrompt; + + assert.isFalse(shouldAskForEmailResult, "When the user had already been asked for mail, we should not ask for email."); + }); + }); + + describe("returns true", () => { + it("when console is interactive, CLI_NOPROMPT is not 1 and we have not asked user before that", async () => { + const originalIsInteractive = helpers.isInteractive; + helpers.isInteractive = () => true; + + const originalCliNoPrompt = process.env.CLI_NOPROMPT; + process.env.CLI_NOPROMPT = "random_value"; + + const testInjector = createTestInjector(); + const userSettingsService = testInjector.resolve("userSettingsService"); + userSettingsService.getSettingValue = async (settingName: string): Promise => false; + + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + const shouldAskForEmailResult = await subscriptionService.shouldAskForEmail(); + + helpers.isInteractive = originalIsInteractive; + process.env.CLI_NOPROMPT = originalCliNoPrompt; + + assert.isTrue(shouldAskForEmailResult, "We should ask the user for email when console is interactiv, CLI_NOPROMPT is not 1 and we have never asked the user before."); + }); + }); + }); + + describe("subscribeForNewsletter", () => { + it("does nothing when shouldAskForEmail returns false", async () => { + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + subscriptionService.shouldAskForEmailResult = false; + const logger = testInjector.resolve("logger"); + let loggerOutput = ""; + logger.out = (...args: string[]): void => { + loggerOutput += args.join(" "); + }; + + await subscriptionService.subscribeForNewsletter(); + assert.deepEqual(loggerOutput, ""); + }); + + it("shows message that asks for e-mail address", async () => { + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + subscriptionService.shouldAskForEmailResult = true; + + const logger = testInjector.resolve("logger"); + let loggerOutput = ""; + + logger.out = (...args: string[]): void => { + loggerOutput += args.join(" "); + }; + + await subscriptionService.subscribeForNewsletter(); + + assert.equal(loggerOutput, "Leave your e-mail address here to subscribe for NativeScript newsletter and product updates, tips and tricks:"); + }); + + const expectedMessageInPrompter = "(press Enter for blank)"; + it(`calls prompter with specific message - ${expectedMessageInPrompter}`, async () => { + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + subscriptionService.shouldAskForEmailResult = true; + const prompter = testInjector.resolve("prompter"); + let schemasPassedToPromter: IPromptSchema[] = null; + prompter.get = async (schemas: IPromptSchema[]): Promise => { + schemasPassedToPromter = schemas; + + return { inputEmail: "SomeEmail" }; + }; + + await subscriptionService.subscribeForNewsletter(); + + assert.isNotNull(schemasPassedToPromter, "Prompter should have been called."); + assert.equal(schemasPassedToPromter.length, 1, "A single schema should have been passed to schemas."); + + assert.equal(schemasPassedToPromter[0].message, expectedMessageInPrompter); + }); + + describe("calls prompter with validate method", () => { + const testData: IValidateTestData[] = [ + { + name: "returning true when empty string is passed", + valuePassedToValidate: "", + expectedResult: true + }, + { + name: "returning true when passing valid email", + valuePassedToValidate: "abc@def.gh", + expectedResult: true + }, + { + name: "returning specific message when invalid email is passed", + valuePassedToValidate: "abcdef.gh", + expectedResult: "Please provide a valid e-mail or simply leave it blank." + } + ]; + + _.each(testData, testCase => { + it(testCase.name, async () => { + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + subscriptionService.shouldAskForEmailResult = true; + const prompter = testInjector.resolve("prompter"); + let schemasPassedToPromter: IPromptSchema[] = null; + prompter.get = async (schemas: IPromptSchema[]): Promise => { + schemasPassedToPromter = schemas; + return { inputEmail: "SomeEmail" }; + }; + + await subscriptionService.subscribeForNewsletter(); + + const schemaPassedToPromter = schemasPassedToPromter[0]; + const resultOfValidateMethod = schemaPassedToPromter.validate(testCase.valuePassedToValidate); + assert.equal(resultOfValidateMethod, testCase.expectedResult); + }); + }); + + }); + + const emailRegisteredKey = "EMAIL_REGISTERED"; + it(`persists ${emailRegisteredKey} setting with value true in user settings`, async () => { + const testInjector = createTestInjector(); + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + subscriptionService.shouldAskForEmailResult = true; + const userSettingsService = testInjector.resolve("userSettingsService"); + let keyPassedToSaveSetting: string = null; + let valuePassedToSaveSetting: boolean = null; + userSettingsService.saveSetting = async (key: string, value: any): Promise => { + keyPassedToSaveSetting = key; + valuePassedToSaveSetting = value; + }; + + await subscriptionService.subscribeForNewsletter(); + + assert.deepEqual(keyPassedToSaveSetting, emailRegisteredKey); + assert.deepEqual(valuePassedToSaveSetting, true); + }); + + it("calls httpRequest with concrete data", async () => { + const email = "abc@def.gh"; + + const postData = stringify({ + 'elqFormName': "dev_uins_cli", + 'elqSiteID': '1325', + 'emailAddress': email, + 'elqCookieWrite': '0' + }); + + const expectedOptions = { + url: 'https://s1325.t.eloqua.com/e/f2', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + }, + body: postData + }; + + const testInjector = createTestInjector(); + + const prompter = testInjector.resolve("prompter"); + let schemasPassedToPromter: IPromptSchema[] = null; + prompter.get = async (schemas: IPromptSchema[]): Promise => { + schemasPassedToPromter = schemas; + return { inputEmail: email }; + }; + + const httpClient = testInjector.resolve("httpClient"); + let optionsPassedToHttpRequest: any = null; + httpClient.httpRequest = async (options: any, proxySettings?: IProxySettings): Promise => { + optionsPassedToHttpRequest = options; + return null; + }; + + const subscriptionService = testInjector.resolve(SubscriptionServiceTester); + subscriptionService.shouldAskForEmailResult = true; + + await subscriptionService.subscribeForNewsletter(); + + assert.deepEqual(optionsPassedToHttpRequest, expectedOptions); + }); + }); +}); diff --git a/test/stubs.ts b/test/stubs.ts index be91dca62e..04f128f8dc 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -35,6 +35,14 @@ export class LoggerStub implements ILogger { printMarkdown(message: string): void { } } +export class ProcessServiceStub implements IProcessService { + public listenersCount: number; + + public attachToProcessExitSignals(context: any, callback: () => void): void { + return undefined; + } +} + export class FileSystemStub implements IFileSystem { async zipFiles(zipFile: string, files: string[], zipPathCallback: (path: string) => string): Promise { return undefined; @@ -169,21 +177,23 @@ export class FileSystemStub implements IFileSystem { } deleteEmptyParents(directory: string): void { } -} -export class ErrorsStub implements IErrors { - constructor() { - new (require("../lib/common/errors").Errors)(); // we need the side effect of require'ing errors + utimes(path: string, atime: Date, mtime: Date): void { } + + realpath(filePath: string): string { + return null; } +} - fail(formatStr: string, ...args: any[]): void; - fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): void; +export class ErrorsStub implements IErrors { + fail(formatStr: string, ...args: any[]): never; + fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): never; - fail(...args: any[]) { - throw args; + fail(...args: any[]): never { + throw new Error(require("util").format.apply(null, args || [])); } - failWithoutHelp(message: string, ...args: any[]): void { + failWithoutHelp(message: string, ...args: any[]): never { throw new Error(message); } @@ -204,7 +214,7 @@ export class ErrorsStub implements IErrors { } export class NpmInstallationManagerStub implements INpmInstallationManager { - async install(packageName: string, pathToSave?: string, version?: string): Promise { + async install(packageName: string, pathToSave?: string, options?: INpmInstallOptions): Promise { return Promise.resolve(""); } @@ -243,30 +253,6 @@ export class ProjectDataStub implements IProjectData { } } -export class PlatformsDataStub extends EventEmitter implements IPlatformsData { - public platformsNames: string[]; - - public getPlatformData(platform: string, projectData: IProjectData): IPlatformData { - return { - frameworkPackageName: "", - platformProjectService: new PlatformProjectServiceStub(), - emulatorServices: undefined, - projectRoot: "", - normalizedPlatformName: "", - appDestinationDirectoryPath: "", - deviceBuildOutputPath: "", - getValidPackageNames: (buildOptions: { isForDevice?: boolean, isReleaseBuild?: boolean }) => [], - frameworkFilesExtensions: [], - relativeToFrameworkConfigurationFilePath: "", - fastLivesyncFileExtensions: [] - }; - } - - public get availablePlatforms(): any { - return undefined; - } -} - export class PlatformProjectServiceStub extends EventEmitter implements IPlatformProjectService { getPlatformData(projectData: IProjectData): IPlatformData { return { @@ -355,6 +341,33 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor async cleanProject(projectRoot: string, projectData: IProjectData): Promise { return Promise.resolve(); } + async checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise { + // Nothing yet. + } +} + +export class PlatformsDataStub extends EventEmitter implements IPlatformsData { + public platformsNames: string[]; + + public getPlatformData(platform: string, projectData: IProjectData): IPlatformData { + return { + frameworkPackageName: "", + platformProjectService: new PlatformProjectServiceStub(), + emulatorServices: undefined, + projectRoot: "", + normalizedPlatformName: "", + appDestinationDirectoryPath: "", + deviceBuildOutputPath: "", + getValidPackageNames: (buildOptions: { isForDevice?: boolean, isReleaseBuild?: boolean }) => [], + frameworkFilesExtensions: [], + relativeToFrameworkConfigurationFilePath: "", + fastLivesyncFileExtensions: [] + }; + } + + public get availablePlatforms(): any { + return undefined; + } } export class ProjectDataService implements IProjectDataService { @@ -367,6 +380,8 @@ export class ProjectDataService implements IProjectDataService { removeNSProperty(propertyName: string): void { } removeDependency(dependencyName: string): void { } + + getProjectData(projectDir: string): IProjectData { return null; } } export class ProjectHelperStub implements IProjectHelper { @@ -429,13 +444,13 @@ export class PrompterStub implements IPrompter { } async getPassword(prompt: string, options?: IAllowEmpty): Promise { chai.assert.ok(prompt in this.passwords, `PrompterStub didn't expect to give password for: ${prompt}`); - let result = this.passwords[prompt]; + const result = this.passwords[prompt]; delete this.passwords[prompt]; return result; } async getString(prompt: string, options?: IPrompterOptions): Promise { chai.assert.ok(prompt in this.strings, `PrompterStub didn't expect to be asked for: ${prompt}`); - let result = this.strings[prompt]; + const result = this.strings[prompt]; delete this.strings[prompt]; return result; } @@ -450,10 +465,10 @@ export class PrompterStub implements IPrompter { } assert() { - for (let key in this.strings) { + for (const key in this.strings) { throw unexpected(`PrompterStub was instructed to reply with "${this.strings[key]}" to a "${key}" question, but was never asked!`); } - for (let key in this.passwords) { + for (const key in this.passwords) { throw unexpected(`PrompterStub was instructed to reply with "${this.passwords[key]}" to a "${key}" password request, but was never asked!`); } } @@ -464,13 +479,13 @@ function unreachable(): Error { } function unexpected(msg: string): Error { - let err = new chai.AssertionError(msg); + const err = new chai.AssertionError(msg); err.showDiff = false; return err; } export class DebugServiceStub extends EventEmitter implements IPlatformDebugService { - public async debug(): Promise { + public async debug(): Promise { return; } @@ -486,15 +501,23 @@ export class DebugServiceStub extends EventEmitter implements IPlatformDebugServ } export class LiveSyncServiceStub implements ILiveSyncService { - public async liveSync(platform: string, projectData: IProjectData, applicationReloadAction?: (deviceAppData: Mobile.IDeviceAppData) => Promise): Promise { + public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise { return; } + + public async stopLiveSync(projectDir: string): Promise { + return; + } + + public getLiveSyncDeviceDescriptors(projectDir: string): ILiveSyncDeviceInfo[] { + return []; + } } export class AndroidToolsInfoStub implements IAndroidToolsInfo { public getToolsInfo(): IAndroidToolsInfoData { - let infoData: IAndroidToolsInfoData = Object.create(null); - infoData.androidHomeEnvVar = ""; + const infoData: IAndroidToolsInfoData = Object.create(null); + infoData.androidHomeEnvVar = "ANDROID_HOME"; infoData.compileSdkVersion = 23; infoData.buildToolsVersion = "23"; infoData.targetSdkVersion = 23; @@ -545,7 +568,7 @@ export class ChildProcessStub { } export class ProjectChangesService implements IProjectChangesService { - public checkForChanges(platform: string): IProjectChangesInfo { + public async checkForChanges(platform: string): Promise { return {}; } @@ -563,6 +586,10 @@ export class ProjectChangesService implements IProjectChangesService { public get currentChanges(): IProjectChangesInfo { return {}; } + + public setNativePlatformStatus(platform: string, projectData: IProjectData, nativePlatformStatus: IAddedNativePlatform): void { + return; + } } export class CommandsService implements ICommandsService { @@ -608,6 +635,10 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic return []; } + public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void { + return; + } + public async removePlatforms(platforms: string[]): Promise { } @@ -644,10 +675,6 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic return Promise.resolve(); } - public emulatePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, emulateOptions: IEmulatePlatformOptions): Promise { - return Promise.resolve(); - } - public cleanDestinationApp(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, platformTemplate: string): Promise { return Promise.resolve(); } @@ -660,6 +687,10 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic } + isPlatformSupportedForOS(platform: string, projectData: IProjectData): boolean { + return true; + } + public getLatestApplicationPackageForDevice(platformData: IPlatformData): IApplicationPackage { return null; } diff --git a/test/test-bootstrap.ts b/test/test-bootstrap.ts index 30f03b8fba..f9cb09e9ac 100644 --- a/test/test-bootstrap.ts +++ b/test/test-bootstrap.ts @@ -9,13 +9,16 @@ const cliGlobal = global; cliGlobal._ = require("lodash"); cliGlobal.$injector = require("../lib/common/yok").injector; +// Requiring colors will modify the prototype of String +// We need it as in some places we use ., which is undefined when colors is not required +// So we sometimes miss warnings in the tests as we receive "undefined". +require("colors"); + use(require("chai-as-promised")); $injector.register("analyticsService", { - trackException: (): { wait(): void } => { - return { - wait: () => undefined - }; + trackException: async (exception: any, message: string): Promise => { + // Intentionally left blank. } }); diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index 09c62c8457..50bb3afc3f 100644 --- a/test/tns-appstore-upload.ts +++ b/test/tns-appstore-upload.ts @@ -1,10 +1,8 @@ -import { suite, test/*, only*/ } from "mocha-typescript"; import { PublishIOS } from "../lib/commands/appstore-upload"; import { PrompterStub, LoggerStub, ProjectDataStub } from "./stubs"; import * as chai from "chai"; import * as yok from "../lib/common/yok"; -@suite("tns appstore") class AppStore { static itunesconnect = { user: "person@private.com", @@ -74,10 +72,10 @@ class AppStore { initInjector(services?: { commands?: { [service: string]: any }, services?: { [service: string]: any } }) { this.injector = new yok.Yok(); if (services) { - for (let cmd in services.commands) { + for (const cmd in services.commands) { this.injector.registerCommand(cmd, services.commands[cmd]); } - for (let serv in services.services) { + for (const serv in services.services) { this.injector.register(serv, services.services[serv]); } } @@ -142,7 +140,6 @@ class AppStore { }; } - @test("without args, prompts for itunesconnect credentionals, prepares, archives and uploads") async noArgs() { this.expectItunesPrompt(); this.expectPreparePlatform(); @@ -155,7 +152,6 @@ class AppStore { this.assert(); } - @test("with command line itunesconnect credentionals, prepares, archives and uploads") async itunesconnectArgs() { this.expectPreparePlatform(); this.expectArchive(); @@ -167,7 +163,6 @@ class AppStore { this.assert(); } - @test("passes --team-id to xcodebuild exportArchive") async teamIdOption() { this.expectItunesPrompt(); this.expectPreparePlatform(); @@ -182,3 +177,21 @@ class AppStore { this.assert(); } } + +describe("tns appstore", () => { + it("without args, prompts for itunesconnect credentionals, prepares, archives and uploads", async () => { + const instance = new AppStore(); + instance.before(); + await instance.noArgs(); + }); + it("with command line itunesconnect credentionals, prepares, archives and uploads", async () => { + const instance = new AppStore(); + instance.before(); + await instance.itunesconnectArgs(); + }); + it("passes --team-id to xcodebuild exportArchive", async () => { + const instance = new AppStore(); + instance.before(); + await instance.teamIdOption(); + }); +}); diff --git a/test/tools/node-modules/node-modules-dependencies-builder.ts b/test/tools/node-modules/node-modules-dependencies-builder.ts new file mode 100644 index 0000000000..0b8d85dcb3 --- /dev/null +++ b/test/tools/node-modules/node-modules-dependencies-builder.ts @@ -0,0 +1,319 @@ +import { Yok } from "../../../lib/common/yok"; +import { assert } from "chai"; +import { NodeModulesDependenciesBuilder } from "../../../lib/tools/node-modules/node-modules-dependencies-builder"; +import * as path from "path"; +import * as constants from "../../../lib/constants"; + +interface IDependencyInfo { + name: string; + version: string; + depth: number; + dependencies?: IDependencyInfo[]; + nativescript?: any; +} + +// TODO: Add integration tests. +// The tests assumes npm 3 or later is used, so all dependencies (and their dependencies) will be installed at the root node_modules +describe("nodeModulesDependenciesBuilder", () => { + const pathToProject = "some path"; + const getTestInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("fs", { + readJson: (pathToFile: string): any => undefined + }); + + return testInjector; + }; + + describe("getProductionDependencies", () => { + describe("returns empty array", () => { + const validateResultIsEmpty = async (resultOfReadJson: any) => { + const testInjector = getTestInjector(); + const fs = testInjector.resolve("fs"); + fs.readJson = (filename: string, encoding?: string): any => { + return resultOfReadJson; + }; + + const nodeModulesDependenciesBuilder = testInjector.resolve(NodeModulesDependenciesBuilder); + const result = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject); + + assert.deepEqual(result, []); + }; + + it("when package.json does not have any data", async () => { + await validateResultIsEmpty(null); + }); + + it("when package.json does not have dependencies section", async () => { + await validateResultIsEmpty({ name: "some name", devDependencies: { a: "1.0.0" } }); + }); + }); + + describe("returns correct dependencies", () => { + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Helper functions for easier writing of consecutive tests in the suite. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + const getPathToDependencyInNodeModules = (dependencyName: string, parentDir?: string): string => { + return path.join(parentDir || pathToProject, constants.NODE_MODULES_FOLDER_NAME, dependencyName); + }; + + const getNodeModuleInfoForExpecteDependency = (name: string, depth: number, nativescript?: any, dependencies?: string[]): IDependencyData => { + const result: IDependencyData = { + name: path.basename(name), + directory: getPathToDependencyInNodeModules(name), + depth, + dependencies: dependencies || [] + }; + + if (nativescript) { + result.nativescript = nativescript; + } + + return result; + }; + + const getPathToPackageJsonOfDependency = (dependencyName: string, parentDir?: string): string => { + return path.join(getPathToDependencyInNodeModules(dependencyName, parentDir), constants.PACKAGE_JSON_FILE_NAME); + }; + + const getDependenciesObjectFromDependencyInfo = (depInfos: IDependencyInfo[], nativescript: any): { dependencies: any, nativescript?: any } => { + const dependencies: any = {}; + _.each(depInfos, innerDependency => { + dependencies[innerDependency.name] = innerDependency.version; + }); + + const result: any = { + dependencies + }; + + if (nativescript) { + result.nativescript = nativescript; + } + + return result; + }; + + const getDependenciesObject = (filename: string, deps: IDependencyInfo[], parentDir: string): { dependencies: any } => { + let result: { dependencies: any } = null; + for (const dependencyInfo of deps) { + const pathToPackageJson = getPathToPackageJsonOfDependency(dependencyInfo.name, parentDir); + if (filename === pathToPackageJson) { + return getDependenciesObjectFromDependencyInfo(dependencyInfo.dependencies, dependencyInfo.nativescript); + } + + if (dependencyInfo.dependencies) { + result = getDependenciesObject(filename, dependencyInfo.dependencies, path.join(parentDir, constants.NODE_MODULES_FOLDER_NAME, dependencyInfo.name)); + if (result) { + break; + } + } + } + + return result; + }; + + const generateTest = (rootDeps: IDependencyInfo[]): INodeModulesDependenciesBuilder => { + const testInjector = getTestInjector(); + const nodeModulesDependenciesBuilder = testInjector.resolve(NodeModulesDependenciesBuilder); + const fs = testInjector.resolve("fs"); + + fs.readJson = (filename: string, encoding?: string): any => { + const innerDependency = getDependenciesObject(filename, rootDeps, pathToProject); + return innerDependency || getDependenciesObjectFromDependencyInfo(rootDeps, null); + }; + + const isDirectory = (searchedPath: string, currentRootPath: string, deps: IDependencyInfo[], currentDepthLevel: number): boolean => { + let result = false; + + for (const dependencyInfo of deps) { + const pathToDependency = path.join(currentRootPath, constants.NODE_MODULES_FOLDER_NAME, dependencyInfo.name); + + if (pathToDependency === searchedPath && currentDepthLevel === dependencyInfo.depth) { + return true; + } + + if (dependencyInfo.dependencies) { + result = isDirectory(searchedPath, pathToDependency, dependencyInfo.dependencies, currentDepthLevel + 1); + if (result) { + break; + } + } + } + + return result; + }; + + const isPackageJsonOfDependency = (searchedPath: string, currentRootPath: string, deps: IDependencyInfo[], currentDepthLevel: number): boolean => { + let result = false; + for (const dependencyInfo of deps) { + const pathToDependency = path.join(currentRootPath, constants.NODE_MODULES_FOLDER_NAME, dependencyInfo.name); + + const pathToPackageJson = path.join(pathToDependency, constants.PACKAGE_JSON_FILE_NAME); + + if (pathToPackageJson === searchedPath && currentDepthLevel === dependencyInfo.depth) { + return true; + } + + if (dependencyInfo.dependencies) { + result = isPackageJsonOfDependency(searchedPath, pathToDependency, dependencyInfo.dependencies, currentDepthLevel + 1); + if (result) { + break; + } + } + } + + return result; + }; + + fs.getLsStats = (pathToStat: string): any => { + return { + isDirectory: (): boolean => isDirectory(pathToStat, pathToProject, rootDeps, 0), + isSymbolicLink: (): boolean => false, + isFile: (): boolean => isPackageJsonOfDependency(pathToStat, pathToProject, rootDeps, 0) + }; + }; + + return nodeModulesDependenciesBuilder; + }; + + const generateDependency = (name: string, version: string, depth: number, dependencies: IDependencyInfo[], nativescript?: any): IDependencyInfo => { + return { + name, + version, + depth, + dependencies, + nativescript + }; + }; + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * END of helper functions for easier writing of consecutive tests in the suite. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + const firstPackage = "firstPackage"; + const secondPackage = "secondPackage"; + const thirdPackage = "thirdPackage"; + + it("when all dependencies are installed at the root level of the project", async () => { + // The test validates the following dependency tree, when npm 3+ is used. + // + // ├── firstPackage@1.0.0 + // ├── secondPackage@1.1.0 + // └── thirdPackage@1.2.0 + + const rootDeps: IDependencyInfo[] = [ + generateDependency(firstPackage, "1.0.0", 0, null), + generateDependency(secondPackage, "1.1.0", 0, null), + generateDependency(thirdPackage, "1.2.0", 0, null) + ]; + + const nodeModulesDependenciesBuilder = generateTest(rootDeps); + const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject); + + const expectedResult: IDependencyData[] = [ + getNodeModuleInfoForExpecteDependency(firstPackage, 0), + getNodeModuleInfoForExpecteDependency(secondPackage, 0), + getNodeModuleInfoForExpecteDependency(thirdPackage, 0) + ]; + + assert.deepEqual(actualResult, expectedResult); + }); + + it("when the project has a dependency to a package and one of the other packages has dependency to other version of this package", async () => { + // The test validates the following dependency tree, when npm 3+ is used. + // + // ├─┬ firstPackage@1.0.0 + // │ └── secondPackage@1.2.0 + // └── secondPackage@1.1.0 + + const rootDeps: IDependencyInfo[] = [ + generateDependency(firstPackage, "1.0.0", 0, [generateDependency(secondPackage, "1.2.0", 1, null)]), + generateDependency(secondPackage, "1.1.0", 0, null) + ]; + + const expectedResult: IDependencyData[] = [ + getNodeModuleInfoForExpecteDependency(firstPackage, 0, null, [secondPackage]), + getNodeModuleInfoForExpecteDependency(secondPackage, 0), + getNodeModuleInfoForExpecteDependency(path.join(firstPackage, constants.NODE_MODULES_FOLDER_NAME, secondPackage), 1) + ]; + + const nodeModulesDependenciesBuilder = generateTest(rootDeps); + const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject); + assert.deepEqual(actualResult, expectedResult); + }); + + it("when several package depend on different versions of other packages", async () => { + // The test validates the following dependency tree, when npm 3+ is used. + // + // ├─┬ firstPackage@1.0.0 + // │ ├─┬ secondPackage@1.1.0 + // │ │ └── thirdPackage@1.2.0 + // │ └── thirdPackage@1.1.0 + // ├── secondPackage@1.0.0 + // └── thirdPackage@1.0.0 + + const rootDeps: IDependencyInfo[] = [ + generateDependency(firstPackage, "1.0.0", 0, [ + generateDependency(secondPackage, "1.1.0", 1, [ + generateDependency(thirdPackage, "1.2.0", 2, null) + ]), + generateDependency(thirdPackage, "1.1.0", 1, null) + ]), + generateDependency(secondPackage, "1.0.0", 0, null), + generateDependency(thirdPackage, "1.0.0", 0, null) + ]; + + const pathToSecondPackageInsideFirstPackage = path.join(firstPackage, constants.NODE_MODULES_FOLDER_NAME, secondPackage); + const expectedResult: IDependencyData[] = [ + getNodeModuleInfoForExpecteDependency(firstPackage, 0, null, [secondPackage, thirdPackage]), + getNodeModuleInfoForExpecteDependency(secondPackage, 0), + getNodeModuleInfoForExpecteDependency(thirdPackage, 0), + getNodeModuleInfoForExpecteDependency(pathToSecondPackageInsideFirstPackage, 1, null, [thirdPackage]), + getNodeModuleInfoForExpecteDependency(path.join(firstPackage, constants.NODE_MODULES_FOLDER_NAME, thirdPackage), 1), + getNodeModuleInfoForExpecteDependency(path.join(pathToSecondPackageInsideFirstPackage, constants.NODE_MODULES_FOLDER_NAME, thirdPackage), 2), + ]; + + const nodeModulesDependenciesBuilder = generateTest(rootDeps); + const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject); + assert.deepEqual(actualResult, expectedResult); + }); + + it("when the installed packages have nativescript data in their package.json", async () => { + // The test validates the following dependency tree, when npm 3+ is used. + // + // ├── firstPackage@1.0.0 + // ├── secondPackage@1.1.0 + // └── thirdPackage@1.2.0 + + const getNativeScriptDataForPlugin = (pluginName: string): any => { + return { + platforms: { + "tns-android": "x.x.x", + "tns-ios": "x.x.x", + }, + + customPropertyUsedForThisTestOnly: pluginName + }; + }; + + const rootDeps: IDependencyInfo[] = [ + generateDependency(firstPackage, "1.0.0", 0, null, getNativeScriptDataForPlugin(firstPackage)), + generateDependency(secondPackage, "1.1.0", 0, null, getNativeScriptDataForPlugin(secondPackage)), + generateDependency(thirdPackage, "1.2.0", 0, null, getNativeScriptDataForPlugin(thirdPackage)) + ]; + + const nodeModulesDependenciesBuilder = generateTest(rootDeps); + const actualResult = await nodeModulesDependenciesBuilder.getProductionDependencies(pathToProject); + + const expectedResult: IDependencyData[] = [ + getNodeModuleInfoForExpecteDependency(firstPackage, 0, getNativeScriptDataForPlugin(firstPackage)), + getNodeModuleInfoForExpecteDependency(secondPackage, 0, getNativeScriptDataForPlugin(secondPackage)), + getNodeModuleInfoForExpecteDependency(thirdPackage, 0, getNativeScriptDataForPlugin(thirdPackage)) + ]; + + assert.deepEqual(actualResult, expectedResult); + }); + }); + }); +}); diff --git a/test/xcconfig-service.ts b/test/xcconfig-service.ts new file mode 100644 index 0000000000..9a96daecf8 --- /dev/null +++ b/test/xcconfig-service.ts @@ -0,0 +1,188 @@ +import temp = require("temp"); +import { assert } from "chai"; +import { XCConfigService } from "../lib/services/xcconfig-service"; +import * as yok from "../lib/common/yok"; + +// start tracking temporary folders/files +temp.track(); + +describe("XCConfig Service Tests", () => { + const createTestInjector = (): IInjector => { + const testInjector = new yok.Yok(); + testInjector.register("fs", { + exists: (path: string) => { + return true; + } + }); + + testInjector.register('xCConfigService', XCConfigService); + + return testInjector; + }; + + const assertPropertyValues = (expected: any, injector: IInjector) => { + const service = getXCConfigService(injector); + _.forOwn(expected, (value, key) => { + const actual = service.readPropertyValue("any", key); + assert.equal(actual, value); + }); + }; + + function getXCConfigService(injector: IInjector): XCConfigService { + return injector.resolve("xCConfigService"); + } + + function getFileSystemMock(injector: IInjector): any { + return injector.resolve('$fs'); + } + + describe("Read Property Value", () => { + it("Return empty value, for unexistent file", () => { + const injector = createTestInjector(); + injector.register("fs", { + exists: (path: string) => { + return false; + } + }); + + const service = getXCConfigService(injector); + const actual = service.readPropertyValue("any", "any"); + + assert.isNull(actual); + }); + + it("Returns correct value for well-formatted document", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `// You can add custom settings here + // for example you can uncomment the following line to force distribution code signing + CODE_SIGN_IDENTITY = iPhone Distribution + // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html + // DEVELOPMENT_TEAM = YOUR_TEAM_ID; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;`; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon', + 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME': 'LaunchImage', + 'CODE_SIGN_IDENTITY': 'iPhone Distribution' + }; + + assertPropertyValues(expected, injector); + }); + + it("Returns correct value for values with missing ; at the end of the line.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `// You can add custom settings here + // for example you can uncomment the following line to force distribution code signing + CODE_SIGN_IDENTITY = iPhone Distribution + // To build for device with XCode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html + // DEVELOPMENT_TEAM = YOUR_TEAM_ID + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage`; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon', + 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME': 'LaunchImage', + 'CODE_SIGN_IDENTITY': 'iPhone Distribution' + }; + + assertPropertyValues(expected, injector); + }); + + it("Doesn't read value from a commented-out line.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `// DEVELOPMENT_TEAM = YOUR_TEAM_ID; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;`; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon', + 'DEVELOPMENT_TEAM': null + }; + + assertPropertyValues(expected, injector); + }); + + it("Returns correct empty value.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return ` + ASSETCATALOG_COMPILER_APPICON_NAME = ; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + `; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': '', + 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME': 'LaunchImage' + }; + + assertPropertyValues(expected, injector); + }); + + it("First part only property doesn't break the service.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `ASSETCATALOG_COMPILER_APPICON_NAME`; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': null + }; + + assertPropertyValues(expected, injector); + }); + + it("Invalid config property value with = doesn't break the service.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `ASSETCATALOG_COMPILER_APPICON_NAME=`; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': null + }; + + assertPropertyValues(expected, injector); + }); + + it("Property with space is read correctly.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `ASSETCATALOG_COMPILER_APPICON_NAME= `; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': '' + }; + + assertPropertyValues(expected, injector); + }); + + it("Ensure property can be an empty value.", () => { + const injector = createTestInjector(); + const fs = getFileSystemMock(injector); + fs.readText = (filename: string, options?: IReadFileOptions | string): string => { + return `ASSETCATALOG_COMPILER_APPICON_NAME= ;`; + }; + + const expected = { + 'ASSETCATALOG_COMPILER_APPICON_NAME': '' + }; + + assertPropertyValues(expected, injector); + }); + }); +}); diff --git a/tslint.json b/tslint.json index e37be6cc07..908fdf9f32 100644 --- a/tslint.json +++ b/tslint.json @@ -13,6 +13,7 @@ false, 140 ], + "prefer-const": true, "no-consecutive-blank-lines": true, "no-construct": true, "no-debugger": true,