diff --git a/lib/controllers/migrate-controller.ts b/lib/controllers/migrate-controller.ts index 6e1487208a..c67ccb2a52 100644 --- a/lib/controllers/migrate-controller.ts +++ b/lib/controllers/migrate-controller.ts @@ -26,16 +26,11 @@ import { IPackageInstallationManager, IPackageManager, IPlatformCommandHelper, - IPlatformValidationService, } from "../declarations"; -import { - IAddPlatformService, - IPlatformsDataService, -} from "../definitions/platform"; +import { IPlatformsDataService } from "../definitions/platform"; import { IPluginsService } from "../definitions/plugins"; import { IChildProcess, - IDictionary, IErrors, IFileSystem, IResourceLoader, @@ -52,13 +47,6 @@ import { SupportedConfigValues } from "../tools/config-manipulation/config-trans export class MigrateController extends UpdateControllerBase implements IMigrateController { - // private static COMMON_MIGRATE_MESSAGE = - // "not affect the codebase of the application and you might need to do additional changes manually – for more information, refer to the instructions in the following blog post: https://www.nativescript.org/blog/nativescript-6.0-application-migration"; - // private static UNABLE_TO_MIGRATE_APP_ERROR = `The current application is not compatible with NativeScript CLI 7.0. - // Use the \`ns migrate\` command to migrate the app dependencies to a form compatible with NativeScript 7.0. - // Running this command will ${MigrateController.COMMON_MIGRATE_MESSAGE}`; - // private static MIGRATE_FINISH_MESSAGE = `The \`tns migrate\` command does ${MigrateController.COMMON_MIGRATE_MESSAGE}`; - constructor( protected $fs: IFileSystem, protected $platformCommandHelper: IPlatformCommandHelper, @@ -67,15 +55,12 @@ export class MigrateController protected $packageManager: IPackageManager, protected $pacoteService: IPacoteService, // private $androidResourcesMigrationService: IAndroidResourcesMigrationService, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $logger: ILogger, private $errors: IErrors, - private $addPlatformService: IAddPlatformService, private $pluginsService: IPluginsService, private $projectDataService: IProjectDataService, private $projectConfigService: IProjectConfigService, private $options: IOptions, - private $platformValidationService: IPlatformValidationService, private $resources: IResourceLoader, private $injector: IInjector, private $settingsService: ISettingsService, @@ -127,19 +112,19 @@ export class MigrateController private migrationDependencies: IMigrationDependency[] = [ { - packageName: constants.SCOPED_TNS_CORE_MODULES, + packageName: "@nativescript/core", minVersion: "6.5.0", - desiredVersion: "~8.0.0", + desiredVersion: "~8.1.1", shouldAddIfMissing: true, }, { - packageName: constants.TNS_CORE_MODULES_NAME, + packageName: "tns-core-modules", shouldRemove: true, }, { packageName: "@nativescript/types", minVersion: "7.0.0", - desiredVersion: "~8.0.0", + desiredVersion: "~8.1.0", isDev: true, }, { @@ -149,12 +134,12 @@ export class MigrateController isDev: true, }, { - packageName: constants.TNS_CORE_MODULES_WIDGETS_NAME, + packageName: "tns-core-modules-widgets", shouldRemove: true, }, { packageName: "nativescript-dev-webpack", - replaceWith: constants.WEBPACK_PLUGIN_NAME, + replaceWith: "@nativescript/webpack", shouldRemove: true, isDev: true, async shouldMigrateAction() { @@ -163,9 +148,9 @@ export class MigrateController migrateAction: this.migrateWebpack.bind(this), }, { - packageName: constants.WEBPACK_PLUGIN_NAME, + packageName: "@nativescript/webpack", minVersion: "3.0.0", - desiredVersion: "~5.0.0-beta.0", + desiredVersion: "~5.0.0", shouldAddIfMissing: true, isDev: true, }, @@ -198,7 +183,7 @@ export class MigrateController { packageName: "@nativescript/angular", minVersion: "10.0.0", - desiredVersion: "~11.8.0", + desiredVersion: "^12.2.0", async shouldMigrateAction( dependency: IMigrationDependency, projectData: IProjectData, @@ -249,7 +234,7 @@ export class MigrateController { packageName: "@nativescript/unit-test-runner", minVersion: "1.0.0", - desiredVersion: "~2.0.0", + desiredVersion: "~2.0.5", async shouldMigrateAction( dependency: IMigrationDependency, projectData: IProjectData, @@ -270,22 +255,47 @@ export class MigrateController packageName: "typescript", isDev: true, minVersion: "3.7.0", - desiredVersion: "~4.0.0", + desiredVersion: "~4.3.5", + }, + { + packageName: "node-sass", + replaceWith: "sass", + minVersion: "0.0.0", // ignore + isDev: true, + }, + { + packageName: "sass", + minVersion: "0.0.0", // ignore + desiredVersion: "~1.39.0", + isDev: true, }, - ]; - get verifiedPlatformVersions(): IDictionary { - return { - [this.$devicePlatformsConstants.Android.toLowerCase()]: { - minVersion: "6.5.3", - desiredVersion: "8.0.0", - }, - [this.$devicePlatformsConstants.iOS.toLowerCase()]: { - minVersion: "6.5.4", - desiredVersion: "8.0.0", - }, - }; - } + // runtimes + { + packageName: "tns-ios", + minVersion: "6.5.3", + replaceWith: "@nativescript/ios", + isDev: true, + }, + { + packageName: "tns-android", + minVersion: "6.5.4", + replaceWith: "@nativescript/android", + isDev: true, + }, + { + packageName: "@nativescript/ios", + minVersion: "6.5.3", + desiredVersion: "~8.1.0", + isDev: true, + }, + { + packageName: "@nativescript/android", + minVersion: "7.0.0", + desiredVersion: "~8.1.1", + isDev: true, + }, + ]; public async shouldMigrate({ projectDir, @@ -297,10 +307,15 @@ export class MigrateController let shouldMigrate = false; for (const platform of platforms) { + if (!loose) { + remainingPlatforms.push(platform); + continue; + } + + // should only run in loose mode... const cachedResult = await this.getCachedShouldMigrate( projectDir, - platform, - loose + platform ); this.$logger.trace( `Got cached result for shouldMigrate for platform: ${platform}: ${cachedResult}` @@ -322,13 +337,10 @@ export class MigrateController `Executed shouldMigrate for platforms: ${remainingPlatforms}. Result is: ${shouldMigrate}` ); - if (!shouldMigrate) { + // only cache results if running in loose mode + if (!shouldMigrate && loose) { for (const remainingPlatform of remainingPlatforms) { - await this.setCachedShouldMigrate( - projectDir, - remainingPlatform, - loose - ); + await this.setCachedShouldMigrate(projectDir, remainingPlatform); } } } @@ -379,56 +391,51 @@ export class MigrateController this.spinner.succeed("Pre-Migration verification complete"); // back up project files and folders - this.spinner.start("Backing up project files before migration"); + this.spinner.info("Backing up project files before migration"); const backup = await this.backupProject(projectDir); - this.spinner.text = "Project files have been backed up"; - this.spinner.succeed(); + this.spinner.succeed("Project files have been backed up"); // clean up project files this.spinner.info("Cleaning up project files before migration"); await this.cleanUpProject(projectData); - this.spinner.text = "Project files have been cleaned up"; - this.spinner.succeed(); + this.spinner.succeed("Project files have been cleaned up"); // clean up artifacts - this.spinner.start("Cleaning up old artifacts"); + this.spinner.info("Cleaning up old artifacts"); await this.handleAutoGeneratedFiles(backup, projectData); - this.spinner.text = "Cleaned old artifacts"; - this.spinner.succeed(); + this.spinner.succeed("Cleaned old artifacts"); const newConfigPath = path.resolve(projectDir, "nativescript.config.ts"); if (!this.$fs.exists(newConfigPath)) { // migrate configs - this.spinner.start( + this.spinner.info( `Migrating project to use ${"nativescript.config.ts".green}` ); await this.migrateConfigs(projectDir); - this.spinner.text = `Project has been migrated to use ${ - "nativescript.config.ts".green - }`; - this.spinner.succeed(); + this.spinner.succeed( + `Project has been migrated to use ${"nativescript.config.ts".green}` + ); } // update dependencies - this.spinner.start("Updating project dependencies"); + this.spinner.info("Updating project dependencies"); await this.migrateDependencies(projectData, platforms, loose); - this.spinner.text = "Project dependencies have been updated"; - this.spinner.succeed(); + this.spinner.succeed("Project dependencies have been updated"); // update tsconfig const tsConfigPath = path.resolve(projectDir, "tsconfig.json"); if (this.$fs.exists(tsConfigPath)) { - this.spinner.start(`Updating ${"tsconfig.json".yellow}`); + this.spinner.info(`Updating ${"tsconfig.json".yellow}`); await this.migrateTSConfig(tsConfigPath); @@ -437,28 +444,9 @@ export class MigrateController await this.migrateWebpack5(projectDir, projectData); - // npx -p @nativescript/webpack@alpha nativescript-webpack init - // run @nativescript/eslint over codebase - // this.spinner.start("Checking project code..."); - await this.runESLint(projectDir); - // this.spinner.succeed("Updated tsconfig.json"); - - // add latest runtimes (if they were specified in the nativescript key) - // this.spinner.start("Updating runtimes"); - // - // await wait(2000); - // this.spinner.clear(); - // this.$logger.info( - // ` - ${"@nativescript/android".yellow} ${"v7.0.0".green} has been added` - // ); - // this.spinner.render(); - // - // this.spinner.text = "Runtimes have been updated"; - // this.spinner.succeed(); - this.spinner.succeed("Migration complete."); this.$logger.info(""); @@ -476,75 +464,6 @@ export class MigrateController // restore all files - or perhaps let the user sort it out // or ns migrate restore - to restore from pre-migration backup // for some known cases, print suggestions perhaps - // - // return; - // - // this.spinner = this.$terminalSpinnerService.createSpinner(); - // - // this.spinner.start("Migrating project..."); - // // const projectData = this.$projectDataService.getProjectData(projectDir); - // const backupDir = path.join(projectDir, MigrateController.backupFolderName); - // - // try { - // this.spinner.start("Backup project configuration."); - // this.backup( - // [ - // ...MigrateController.pathsToBackup, - // path.join(projectData.getAppDirectoryRelativePath(), "package.json"), - // ], - // backupDir, - // projectData.projectDir - // ); - // this.spinner.text = "Backup project configuration complete."; - // this.spinner.succeed(); - // } catch (error) { - // // this.spinner.text = MigrateController.backupFailMessage; - // this.spinner.fail(); - // // this.$logger.error(MigrateController.backupFailMessage); - // await this.$projectCleanupService.cleanPath(backupDir); - // // this.$fs.deleteDirectory(backupDir); - // return; - // } - // - // try { - // this.spinner.start("Clean auto-generated files."); - // this.handleAutoGeneratedFiles(backupDir, projectData); - // this.spinner.text = "Clean auto-generated files complete."; - // this.spinner.succeed(); - // } catch (error) { - // this.$logger.trace( - // `Error during auto-generated files handling. ${ - // (error && error.message) || error - // }` - // ); - // } - // - // // await this.migrateOldAndroidAppResources(projectData, backupDir); - // - // try { - // await this.cleanUpProject(projectData); - // // await this.migrateConfigs(projectData); - // await this.migrateDependencies( - // projectData, - // platforms, - // loose - // ); - // } catch (error) { - // const backupFolders = MigrateController.pathsToBackup; - // const embeddedPackagePath = path.join( - // projectData.getAppDirectoryRelativePath(), - // "package.json" - // ); - // backupFolders.push(embeddedPackagePath); - // this.restoreBackup(backupFolders, backupDir, projectData.projectDir); - // this.spinner.fail(); - // // this.$errors.fail( - // // `${MigrateController.migrateFailMessage} The error is: ${error}` - // // ); - // } - // - // this.spinner.stop(); - // // this.spinner.info(MigrateController.MIGRATE_FINISH_MESSAGE); } private async _shouldMigrate({ @@ -622,51 +541,6 @@ export class MigrateController } } - for (let platform of platforms) { - platform = platform?.toLowerCase(); - - if ( - !this.$platformValidationService.isValidPlatform(platform, projectData) - ) { - continue; - } - - const hasRuntimeDependency = this.hasRuntimeDependency({ - platform, - projectData, - }); - - if (!hasRuntimeDependency) { - continue; - } - - const verifiedPlatformVersion = this.verifiedPlatformVersions[ - platform.toLowerCase() - ]; - const shouldUpdateRuntime = await this.shouldUpdateRuntimeVersion( - verifiedPlatformVersion, - platform, - projectData, - loose - ); - - if (!shouldUpdateRuntime) { - continue; - } - - this.$logger.trace( - `${shouldMigrateCommonMessage}Platform '${platform}' should be updated.` - ); - if (loose) { - this.$logger.warn( - `Platform '${platform}' should be updated. The minimum version supported is ${verifiedPlatformVersion.minVersion}` - ); - continue; - } - - return true; - } - return false; } @@ -697,29 +571,14 @@ export class MigrateController ); } - private async shouldUpdateRuntimeVersion( - version: IDependencyVersion, - platform: string, - projectData: IProjectData, - loose: boolean - ): Promise { - const installedVersion = await this.getMaxRuntimeVersion({ - platform, - projectData, - }); - - return this.isOutdatedVersion(installedVersion, version, loose); - } - private async getCachedShouldMigrate( projectDir: string, - platform: string, - loose: boolean = false + platform: string ): Promise { let cachedShouldMigrateValue = null; const cachedHash = await this.$jsonFileSettingsService.getSettingValue( - getHash(`${projectDir}${platform.toLowerCase()}`) + loose ? "-loose" : "" + getHash(`${projectDir}${platform.toLowerCase()}`) ); const packageJsonHash = await this.getPackageJsonHash(projectDir); if (cachedHash === packageJsonHash) { @@ -731,15 +590,14 @@ export class MigrateController private async setCachedShouldMigrate( projectDir: string, - platform: string, - loose: boolean = false + platform: string ): Promise { this.$logger.trace( - `Caching shouldMigrate result for platform ${platform} (loose = ${loose}).` + `Caching shouldMigrate result for platform ${platform}.` ); const packageJsonHash = await this.getPackageJsonHash(projectDir); await this.$jsonFileSettingsService.saveSetting( - getHash(`${projectDir}${platform.toLowerCase()}`) + loose ? "-loose" : "", + getHash(`${projectDir}${platform.toLowerCase()}`), packageJsonHash ); } @@ -828,7 +686,6 @@ export class MigrateController constants.HOOKS_DIR_NAME, constants.PLATFORMS_DIR_NAME, constants.NODE_MODULES_FOLDER_NAME, - constants.WEBPACK_CONFIG_NAME, constants.PACKAGE_LOCK_JSON_FILE_NAME, ]); @@ -995,84 +852,49 @@ export class MigrateController } } - private async migrateDependencies( + private async runMigrateActionIfAny( + dependency: IMigrationDependency, projectData: IProjectData, - platforms: string[], - loose: boolean + loose: boolean, + force: boolean = false ): Promise { - for (let i = 0; i < this.migrationDependencies.length; i++) { - const dependency = this.migrationDependencies[i]; - const hasDependency = this.hasDependency(dependency, projectData); - - if (!hasDependency && !dependency.shouldAddIfMissing) { - continue; - } - - if (dependency.migrateAction) { - const shouldMigrate = await dependency.shouldMigrateAction.bind(this)( + if (dependency.migrateAction) { + const shouldMigrate = + force || + (await dependency.shouldMigrateAction.bind(this)( dependency, projectData, loose - ); + )); - if (shouldMigrate) { - const newDependencies = await dependency.migrateAction( - projectData, - path.join( - projectData.projectDir, - MigrateController.backupFolderName - ) - ); - for (const newDependency of newDependencies) { - await this.migrateDependency(newDependency, projectData, loose); - } + if (shouldMigrate) { + const newDependencies = await dependency.migrateAction( + projectData, + path.join(projectData.projectDir, MigrateController.backupFolderName) + ); + for (const newDependency of newDependencies) { + await this.migrateDependency(newDependency, projectData, loose); } } - - await this.migrateDependency(dependency, projectData, loose); } + } - for (const platform of platforms) { - const lowercasePlatform = platform.toLowerCase(); - const hasRuntimeDependency = this.hasRuntimeDependency({ - platform, - projectData, - }); - - if (!hasRuntimeDependency) { - continue; - } - - const shouldUpdate = await this.shouldUpdateRuntimeVersion( - this.verifiedPlatformVersions[lowercasePlatform], - platform, - projectData, - loose - ); + private async migrateDependencies( + projectData: IProjectData, + platforms: string[], + loose: boolean + ): Promise { + for (let i = 0; i < this.migrationDependencies.length; i++) { + const dependency = this.migrationDependencies[i]; + const hasDependency = this.hasDependency(dependency, projectData); - if (!shouldUpdate) { + if (!hasDependency && !dependency.shouldAddIfMissing) { continue; } - const verifiedPlatformVersion = this.verifiedPlatformVersions[ - lowercasePlatform - ]; - const platformData = this.$platformsDataService.getPlatformData( - lowercasePlatform, - projectData - ); - - this.spinner.info( - `Updating ${platform} platform to version ${verifiedPlatformVersion.desiredVersion.green}.` - ); - - await this.$addPlatformService.setPlatformVersion( - platformData, - projectData, - verifiedPlatformVersion.desiredVersion - ); + await this.runMigrateActionIfAny(dependency, projectData, loose); - this.spinner.succeed(); + await this.migrateDependency(dependency, projectData, loose); } } @@ -1156,6 +978,13 @@ export class MigrateController ); this.spinner.render(); + await this.runMigrateActionIfAny( + replacementDep, + projectData, + loose, + true + ); + return; } @@ -1346,7 +1175,7 @@ export class MigrateController { packageName: "karma", minVersion: "4.1.0", - desiredVersion: "~6.3.2", + desiredVersion: "~6.3.4", isDev: true, }, ]; @@ -1381,11 +1210,7 @@ export class MigrateController private async migrateNativeScriptAngular(): Promise { const minVersion = "10.0.0"; - const desiredVersion = "~11.2.7"; - - /* - "@angular/router": "~11.2.7", - */ + const desiredVersion = "^12.2.5"; const dependencies: IMigrationDependency[] = [ { @@ -1439,13 +1264,13 @@ export class MigrateController { packageName: "rxjs", minVersion: "6.6.0", - desiredVersion: "~6.6.7", + desiredVersion: "~7.3.0", shouldAddIfMissing: true, }, { packageName: "zone.js", minVersion: "0.11.1", - desiredVersion: "~0.11.1", + desiredVersion: "~0.11.4", shouldAddIfMissing: true, }, @@ -1459,7 +1284,7 @@ export class MigrateController { packageName: "@ngtools/webpack", minVersion, - desiredVersion: "~11.2.6", + desiredVersion, isDev: true, }, @@ -1478,14 +1303,14 @@ export class MigrateController { packageName: "nativescript-vue-template-compiler", minVersion: "2.7.0", - desiredVersion: "~2.8.4", + desiredVersion: "~2.9.0", isDev: true, shouldAddIfMissing: true, }, { packageName: "nativescript-vue-devtools", minVersion: "1.4.0", - desiredVersion: "~1.5.0", + desiredVersion: "~1.5.1", isDev: true, }, { @@ -1496,6 +1321,18 @@ export class MigrateController packageName: "babel-loader", shouldRemove: true, }, + { + packageName: "babel-traverse", + shouldRemove: true, + }, + { + packageName: "babel-types", + shouldRemove: true, + }, + { + packageName: "babylon", + shouldRemove: true, + }, { packageName: "@babel/core", shouldRemove: true, @@ -1602,36 +1439,48 @@ export class MigrateController } private async migrateWebpack5(projectDir: string, projectData: IProjectData) { - this.spinner.start(`Initializing new ${"webpack.config.js".yellow}`); + const webpackConfigPath = path.resolve(projectDir, "webpack.config.js"); + if (this.$fs.exists(webpackConfigPath)) { + const webpackConfigContent = this.$fs.readText(webpackConfigPath); + + if (webpackConfigContent.includes("webpack.init(")) { + this.spinner.succeed( + `Project already using new ${"webpack.config.js".yellow}` + ); + return; + } + } + // clean old config before generating new one + await this.$projectCleanupService.clean(["webpack.config.js"]); + + this.spinner.info(`Initializing new ${"webpack.config.js".yellow}`); const { desiredVersion: webpackVersion } = this.migrationDependencies.find( (dep) => dep.packageName === constants.WEBPACK_PLUGIN_NAME ); try { - await this.$childProcess.spawnFromEvent( - "npx", - [ - "--package", - `@nativescript/webpack@${webpackVersion}`, - "nativescript-webpack", - "init", - ], - "close", - { - cwd: projectDir, - stdio: "ignore", - } + const scopedWebpackPackage = `@nativescript/webpack`; + const resolvedVersion = await this.$packageInstallationManager.getMaxSatisfyingVersion( + scopedWebpackPackage, + webpackVersion ); + await this.runNPX([ + "--package", + `${scopedWebpackPackage}@${resolvedVersion}`, + "nativescript-webpack", + "init", + ]); + this.spinner.succeed(`Initialized new ${"webpack.config.js".yellow}`); } catch (err) { + this.spinner.fail(`Failed to initialize ${"webpack.config.js".yellow}`); this.$logger.trace( "Failed to initialize webpack.config.js. Error is: ", err ); this.$logger.printMarkdown( - `Failed to initialize \`webpack.config.js\`, you can try again by running \`npm install\` (or yarn, pnpm) and then \`npx @nativescript/webpack init\`.` + `You can try again by running \`npm install\` (or yarn, pnpm) and then \`npx @nativescript/webpack init\`.` ); } - this.spinner.succeed(`Initialized new ${"webpack.config.js".yellow}`); const packageJSON = this.$fs.readJson(projectData.projectFilePath); const currentMain = packageJSON.main ?? "app.js"; @@ -1652,12 +1501,17 @@ export class MigrateController `./app/main.ts`, `./src/main.js`, `./src/main.ts`, - ]; - const replacedMain = possibleMains.find((possibleMain) => { - return this.$fs.exists(path.resolve(projectDir, possibleMain)); + ].map((possibleMain) => path.resolve(projectDir, possibleMain)); + + let replacedMain = possibleMains.find((possibleMain) => { + return this.$fs.exists(possibleMain); }); if (replacedMain) { + replacedMain = `./${path.relative(projectDir, replacedMain)}`.replace( + /\\/g, + "/" + ); packageJSON.main = replacedMain; this.$fs.writeJson(projectData.projectFilePath, packageJSON); @@ -1676,28 +1530,25 @@ export class MigrateController private async runESLint(projectDir: string) { this.spinner.start(`Running ESLint fixes`); try { - const childProcess = injector.resolve("childProcess") as IChildProcess; - const npxVersion = await childProcess.exec("npx -v"); - - const npxFlags = []; - - if (semver.gt(semver.coerce(npxVersion), "7.0.0")) { - npxFlags.push("-y"); - } - - const args = [ - "npx", - ...npxFlags, - "@nativescript/eslint-plugin", - projectDir, - ]; - await childProcess.exec(args.join(" ")); + await this.runNPX(["@nativescript/eslint-plugin", projectDir]); this.spinner.succeed(`Applied ESLint fixes`); } catch (err) { this.spinner.fail(`Failed to apply ESLint fixes`); this.$logger.trace("Failed to apply ESLint fixes. Error is:", err); } } + + private async runNPX(args: string[] = []) { + const npxVersion = await this.$childProcess.exec("npx -v"); + const npxFlags = []; + + if (semver.gt(semver.coerce(npxVersion), "7.0.0")) { + npxFlags.push("-y"); + } + + const args_ = ["npx", ...npxFlags, ...args]; + await this.$childProcess.exec(args_.join(" ")); + } } injector.register("migrateController", MigrateController); diff --git a/lib/controllers/update-controller.ts b/lib/controllers/update-controller.ts index a97acaab65..31aa0258b0 100644 --- a/lib/controllers/update-controller.ts +++ b/lib/controllers/update-controller.ts @@ -1,64 +1,66 @@ -import * as path from "path"; import * as semver from "semver"; import * as constants from "../constants"; import { UpdateControllerBase } from "./update-controller-base"; -import { IProjectDataService, IProjectData } from "../definitions/project"; import { - IPlatformsDataService, - IAddPlatformService, -} from "../definitions/platform"; + IProjectDataService, + IProjectData, + IProjectCleanupService, + IProjectBackupService, + IBackup, +} from "../definitions/project"; +import { IPlatformsDataService } from "../definitions/platform"; import { IPlatformCommandHelper, IPackageInstallationManager, IPackageManager, } from "../declarations"; -import { IPluginsService } from "../definitions/plugins"; -import { IFileSystem, IErrors, IDictionary } from "../common/declarations"; +import { IFileSystem, IErrors } from "../common/declarations"; import { injector } from "../common/yok"; import { PackageVersion } from "../constants"; - -interface IPackage { - name: string; - alias?: string; - isDev?: boolean; -} +import { IDependency } from "../definitions/migrate"; +import { IPluginsService } from "../definitions/plugins"; export class UpdateController extends UpdateControllerBase implements IUpdateController { - static readonly updatableDependencies: IPackage[] = [ + static readonly updatableDependencies: IDependency[] = [ + // dependencies + { + packageName: "@nativescript/core", + }, + + // devDependencies { - name: constants.SCOPED_TNS_CORE_MODULES, - alias: constants.TNS_CORE_MODULES_NAME, + packageName: "@nativescript/webpack", + isDev: true, }, - { name: constants.TNS_CORE_MODULES_NAME }, - { name: constants.TNS_CORE_MODULES_WIDGETS_NAME }, { - name: constants.SCOPED_IOS_RUNTIME_NAME, - alias: constants.TNS_IOS_RUNTIME_NAME, + packageName: "@nativescript/types", isDev: true, }, + + // runtimes { - name: constants.SCOPED_ANDROID_RUNTIME_NAME, - alias: constants.TNS_ANDROID_RUNTIME_NAME, + packageName: "@nativescript/ios", + isDev: true, + }, + { + packageName: "@nativescript/android", isDev: true, }, - { name: constants.WEBPACK_PLUGIN_NAME, isDev: true }, ]; - static readonly folders: string[] = [ + + static readonly backupFolderName: string = ".migration_backup"; + static readonly pathsToBackup: string[] = [ constants.LIB_DIR_NAME, constants.HOOKS_DIR_NAME, constants.WEBPACK_CONFIG_NAME, constants.PACKAGE_JSON_FILE_NAME, constants.PACKAGE_LOCK_JSON_FILE_NAME, + constants.CONFIG_NS_FILE_NAME, ]; - static readonly backupFolder: string = ".update_backup"; - static readonly updateFailMessage: string = "Could not update the project!"; - static readonly backupFailMessage: string = - "Could not backup project folders!"; - static readonly failedToGetTemplateManifestMessage = - "Failed to get template information for the specified version. Original error: %s"; + private spinner: ITerminalSpinner; constructor( protected $fs: IFileSystem, @@ -66,13 +68,14 @@ export class UpdateController protected $platformCommandHelper: IPlatformCommandHelper, protected $packageInstallationManager: IPackageInstallationManager, protected $packageManager: IPackageManager, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $addPlatformService: IAddPlatformService, + protected $pluginsService: IPluginsService, + protected $pacoteService: IPacoteService, private $logger: ILogger, private $errors: IErrors, - private $pluginsService: IPluginsService, - protected $pacoteService: IPacoteService, - private $projectDataService: IProjectDataService + private $projectDataService: IProjectDataService, + private $projectBackupService: IProjectBackupService, + private $projectCleanupService: IProjectCleanupService, + private $terminalSpinnerService: ITerminalSpinnerService ) { super( $fs, @@ -85,495 +88,211 @@ export class UpdateController } public async update(updateOptions: IUpdateOptions): Promise { + this.spinner = this.$terminalSpinnerService.createSpinner(); const projectData = this.$projectDataService.getProjectData( updateOptions.projectDir ); - updateOptions.version = updateOptions.version || PackageVersion.LATEST; - try { - // this is a preventive check to make sure the passed version exists before doing any backups, however - // the update can still fail if the specified tag doesn't exist in one of the updatableDependencies - // at this stage we only care to check a single dependency to catch invalid versions early. - await this.getVersionFromTag( - UpdateController.updatableDependencies[0].name, - updateOptions.version - ); - } catch (error) { - this.$errors.fail( - `${UpdateController.updateFailMessage} Reason is: ${error.message}` - ); - } - const backupDir = path.join( - updateOptions.projectDir, - UpdateController.backupFolder - ); + // back up project files and folders + this.spinner.info("Backing up project files before update"); - try { - this.backup(UpdateController.folders, backupDir, projectData.projectDir); - } catch (error) { - this.$logger.error(UpdateController.backupFailMessage); - this.$fs.deleteDirectory(backupDir); - return; - } + await this.backupProject(); - try { - await this.cleanUpProject(projectData); - await this.updateProject(projectData, updateOptions.version); - } catch (error) { - this.restoreBackup( - UpdateController.folders, - backupDir, - projectData.projectDir - ); - this.$logger.error( - `${UpdateController.updateFailMessage} Reason is: ${error.message}` - ); - } - } - - public async shouldUpdate({ - projectDir, - version, - }: { - projectDir: string; - version?: string; - }): Promise { - if (version && !semver.valid(version) && !semver.validRange(version)) { - // probably npm tag here - return true; - } - - const projectData = this.$projectDataService.getProjectData(projectDir); - const templateManifest = await this.getTemplateManifest( - projectData, - version - ); - const dependencies = this.getUpdatableDependencies( - templateManifest.dependencies - ); - const devDependencies = this.getUpdatableDependencies( - templateManifest.devDependencies - ); + this.spinner.succeed("Project files have been backed up"); - if ( - (await this.hasDependenciesToUpdate({ - dependencies, - areDev: false, - projectData, - })) || - (await this.hasDependenciesToUpdate({ - dependencies: devDependencies, - areDev: true, - projectData, - })) - ) { - return true; - } + // clean up project files + this.spinner.info("Cleaning up project files before update"); - for (const platform in this.$devicePlatformsConstants) { - const lowercasePlatform = platform.toLowerCase(); - const platformData = this.$platformsDataService.getPlatformData( - lowercasePlatform, - projectData - ); + await this.cleanUpProject(); - if ( - await this.shouldUpdateRuntimeVersion( - // templateRuntimeVersion is only set if the template has the legacy nativescript key in package.json - // in other cases, we are just going to default to using the latest - PackageVersion.LATEST, - platformData.frameworkPackageName, - platform, - projectData - ) - ) { - return true; - } - } - } + this.spinner.succeed("Project files have been cleaned up"); - private async cleanUpProject(projectData: IProjectData) { - this.$logger.info("Clean old project artifacts."); - this.$fs.deleteDirectory( - path.join(projectData.projectDir, constants.HOOKS_DIR_NAME) - ); - this.$fs.deleteDirectory( - path.join(projectData.projectDir, constants.PLATFORMS_DIR_NAME) - ); - this.$fs.deleteDirectory( - path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME) - ); - if ( - projectData.projectType === constants.ProjectTypes.ReactFlavorName || - projectData.projectType === constants.ProjectTypes.SvelteFlavorName - ) { - this.$logger.warn( - `As this project is of type ${projectData.projectType}, CLI will not update its ${constants.WEBPACK_CONFIG_NAME} file. Consider updating it manually.` - ); - } else { - this.$fs.deleteFile( - path.join(projectData.projectDir, constants.WEBPACK_CONFIG_NAME) - ); - } - this.$fs.deleteFile( - path.join(projectData.projectDir, constants.PACKAGE_LOCK_JSON_FILE_NAME) - ); - this.$logger.info("Clean old project artifacts complete."); - } + // update dependencies + this.spinner.info("Updating project dependencies"); - private async updateProject( - projectData: IProjectData, - version: string - ): Promise { - let templateManifest: any = {}; + await this.updateDependencies(projectData, updateOptions.version); - if (!version || semver.valid(version) || semver.validRange(version)) { - templateManifest = await this.getTemplateManifest(projectData, version); - } else { - templateManifest = await this.constructTemplateManifestForTag(version); - } + this.spinner.succeed("Project dependencies have been updated"); - const dependencies = this.getUpdatableDependencies( - templateManifest.dependencies - ); - const devDependencies = this.getUpdatableDependencies( - templateManifest.devDependencies - ); + this.spinner.succeed("Update complete."); - this.$logger.info("Start updating dependencies."); - await this.updateDependencies({ dependencies, areDev: false, projectData }); - this.$logger.info("Finished updating dependencies."); - this.$logger.info("Start updating devDependencies."); - await this.updateDependencies({ - dependencies: devDependencies, - areDev: true, - projectData, - }); - this.$logger.info("Finished updating devDependencies."); - - this.$logger.info("Start updating runtimes."); - await this.updateRuntimes(projectData); - this.$logger.info("Finished updating runtimes."); - - this.$logger.info("Install packages."); - await this.$packageManager.install( - projectData.projectDir, - projectData.projectDir, - { - disableNpmInstall: false, - frameworkPath: null, - ignoreScripts: false, - path: projectData.projectDir, - } + this.$logger.info(""); + this.$logger.printMarkdown( + "Project has been successfully updated. The next step is to run `ns run ` to ensure everything is working properly." + + "\n\nPlease note that you may need additional changes to complete the update." ); } - private async constructTemplateManifestForTag(tag: string): Promise { - this.$logger.trace( - `Will construct manually template manifest for tag ${tag}` + public async shouldUpdate(updateOptions: IUpdateOptions): Promise { + const projectData = this.$projectDataService.getProjectData( + updateOptions.projectDir ); + updateOptions.version = updateOptions.version || PackageVersion.LATEST; - const templateManifest: any = {}; - templateManifest.dependencies = {}; - templateManifest.devDependencies = {}; - for (const updatableDependency of UpdateController.updatableDependencies) { - const version = await this.getVersionFromTag( - updatableDependency.name, - tag + for (const dependency of UpdateController.updatableDependencies) { + this.$logger.trace( + `Checking if ${dependency.packageName} needs to be updated...` + ); + const desiredVersion = await this.getVersionFromTagOrVersion( + dependency.packageName, + updateOptions.version ); - if (!version) { - this.$errors.fail( - `Unable to execute update as package '${updatableDependency.name}' does not have version or tag '${tag}'` + + if (typeof desiredVersion === "boolean") { + this.$logger.trace( + `Package ${dependency.packageName} does not have version/tag ${updateOptions.version}. Skipping.` ); + + continue; } - const dictionaryToModify = updatableDependency.isDev - ? templateManifest.devDependencies - : templateManifest.dependencies; - dictionaryToModify[updatableDependency.name] = version; - if (updatableDependency.alias) { - const aliasVersion = await this.getVersionFromTag( - updatableDependency.name, - tag + const shouldUpdate = await this.shouldUpdateDependency( + projectData, + dependency, + desiredVersion + ); + + if (shouldUpdate) { + this.$logger.trace( + `shouldUpdate is true because '${dependency.packageName} needs to be updated.'` ); - dictionaryToModify[updatableDependency.alias] = aliasVersion; + return true; } } - templateManifest.nativescript = { - [constants.TNS_ANDROID_RUNTIME_NAME]: { - version: await this.getVersionFromTag( - constants.TNS_ANDROID_RUNTIME_NAME, - tag - ), - }, - [constants.TNS_IOS_RUNTIME_NAME]: { - version: await this.$packageManager.getTagVersion( - constants.TNS_IOS_RUNTIME_NAME, - tag - ), - }, - }; - - this.$logger.trace( - `Manually constructed template manifest for tag ${tag}. Content is: ${JSON.stringify( - templateManifest, - null, - 2 - )}` - ); + return false; + } - return templateManifest; + private async updateDependencies( + projectData: IProjectData, + version: string + ): Promise { + for (const dependency of UpdateController.updatableDependencies) { + await this.updateDependency(projectData, dependency, version); + } } - private async getVersionFromTag( - packageName: string, - tag: string - ): Promise { - const version = await this.$packageManager.getTagVersion(packageName, tag); - if (!version) { - this.$errors.fail( - `Unable to execute update as package ${packageName} does not have version/tag ${tag}. Please enter valid version or npm tag.` - ); + private async updateDependency( + projectData: IProjectData, + dependency: IDependency, + version: string + ): Promise { + if (!this.hasDependency(dependency, projectData)) { + return; } - return version; - } + const desiredVersion = await this.getVersionFromTagOrVersion( + dependency.packageName, + version + ); - private async updateDependencies({ - dependencies, - areDev, - projectData, - }: { - dependencies: IDictionary; - areDev: boolean; - projectData: IProjectData; - }) { - for (const dependency in dependencies) { - const templateVersion = dependencies[dependency]; - if ( - !this.hasDependency( - { packageName: dependency, isDev: areDev }, - projectData - ) - ) { - continue; - } + if (typeof desiredVersion === "boolean") { + this.$logger.info( + ` - ${dependency.packageName.yellow} does not have version/tag ${version.green}. ` + + "Skipping.".yellow + ); - if ( - await this.shouldUpdateDependency( - dependency, - templateVersion, - projectData - ) - ) { - this.$logger.info( - `Updating '${dependency}' to version '${templateVersion}'.` - ); - this.$pluginsService.addToPackageJson( - dependency, - templateVersion, - areDev, - projectData.projectDir - ); - } + return; } - } - private async shouldUpdateDependency( - dependency: string, - targetVersion: string, - projectData: IProjectData - ) { - const devDependencies = projectData.devDependencies || {}; - const dependencies = projectData.dependencies || {}; - const projectVersion = - dependencies[dependency] || devDependencies[dependency]; - const maxSatisfyingTargetVersion = await this.$packageInstallationManager.getMaxSatisfyingVersionSafe( - dependency, - targetVersion - ); - const maxSatisfyingProjectVersion = await this.$packageInstallationManager.getMaxSatisfyingVersionSafe( + const shouldUpdate = await this.shouldUpdateDependency( + projectData, dependency, - projectVersion - ); - return ( - maxSatisfyingProjectVersion && - maxSatisfyingTargetVersion && - semver.gt(maxSatisfyingTargetVersion, maxSatisfyingProjectVersion) + desiredVersion ); - } - private async hasDependenciesToUpdate({ - dependencies, - areDev, - projectData, - }: { - dependencies: IDictionary; - areDev: boolean; - projectData: IProjectData; - }) { - for (const dependency in dependencies) { - const templateVersion = dependencies[dependency]; - if ( - !this.hasDependency( - { packageName: dependency, isDev: areDev }, - projectData - ) - ) { - continue; - } + if (!shouldUpdate) { + return; + } - if ( - await this.shouldUpdateDependency( - dependency, - templateVersion, - projectData - ) - ) { - return true; + // check if the coerced version is the same as desired and prefix it with a ~ + // for example: + // 8.0.0 -> ~8.0.0 + // 8.0.8-next-XXX -> 8.0.8-next-XXX + const updatedVersion = (() => { + if (desiredVersion === version) { + return desiredVersion; } - } - } - private async updateRuntimes(projectData: IProjectData) { - for (const platform in this.$devicePlatformsConstants) { - const lowercasePlatform = platform.toLowerCase(); - const platformData = this.$platformsDataService.getPlatformData( - lowercasePlatform, - projectData - ); + if (semver.coerce(desiredVersion).version === desiredVersion) { + return `~${desiredVersion}`; + } - if ( - await this.shouldUpdateRuntimeVersion( - // templateRuntimeVersion is only set if the template has the legacy nativescript key in package.json - // in other cases, we are just going to default to using the latest - PackageVersion.LATEST, - platformData.frameworkPackageName, - platform, - projectData - ) - ) { - const version = await this.$packageInstallationManager.getMaxSatisfyingVersionSafe( - platformData.frameworkPackageName, - PackageVersion.LATEST - ); + return desiredVersion; + })(); - this.$logger.info( - `Updating ${platform} platform to version '${version}'.` - ); + this.$pluginsService.addToPackageJson( + dependency.packageName, + updatedVersion, + dependency.isDev, + projectData.projectDir + ); - await this.$addPlatformService.setPlatformVersion( - platformData, - projectData, - version - ); - } - } + this.$logger.info( + ` - ${dependency.packageName.yellow} has been updated to ${ + `${updatedVersion}`.green + }` + ); } - private async shouldUpdateRuntimeVersion( - templateRuntimeVersion: string, - frameworkPackageName: string, - platform: string, - projectData: IProjectData + private async shouldUpdateDependency( + projectData: IProjectData, + dependency: IDependency, + desiredVersion: string ): Promise { - const hasRuntimeDependency = this.hasRuntimeDependency({ - platform, - projectData, - }); + const installedVersion = await this.$packageInstallationManager.getInstalledDependencyVersion( + dependency.packageName, + projectData.projectDir + ); - if (!hasRuntimeDependency) { + if (!installedVersion) { return false; } - const maxTemplateRuntimeVersion = await this.$packageInstallationManager.getMaxSatisfyingVersionSafe( - frameworkPackageName, - templateRuntimeVersion - ); - const maxRuntimeVersion = await this.getMaxRuntimeVersion({ - platform, - projectData, - }); - - return ( - maxTemplateRuntimeVersion && - maxRuntimeVersion && - semver.gt(maxTemplateRuntimeVersion, maxRuntimeVersion) - ); + return installedVersion != desiredVersion; } - private getUpdatableDependencies( - dependencies: IDictionary - ): IDictionary { - const updatableDependencies: IDictionary = {}; - - UpdateController.updatableDependencies.forEach((updatableDependency) => { - if (dependencies[updatableDependency.name]) { - updatableDependencies[updatableDependency.name] = - dependencies[updatableDependency.name]; - } else if ( - updatableDependency.alias && - dependencies[updatableDependency.alias] - ) { - updatableDependencies[updatableDependency.name] = - dependencies[updatableDependency.alias]; - } - }); + private async getVersionFromTagOrVersion( + packageName: string, + versionOrTag: string + ): Promise { + if (semver.valid(versionOrTag) || semver.validRange(versionOrTag)) { + return versionOrTag; + } - return updatableDependencies; - } + const version = await this.$packageManager.getTagVersion( + packageName, + versionOrTag + ); - private getTemplateName(projectData: IProjectData) { - let template; - switch (projectData.projectType) { - case constants.ProjectTypes.NgFlavorName: { - template = constants.RESERVED_TEMPLATE_NAMES.angular; - break; - } - case constants.ProjectTypes.VueFlavorName: { - template = constants.RESERVED_TEMPLATE_NAMES.vue; - break; - } - case constants.ProjectTypes.TsFlavorName: { - template = constants.RESERVED_TEMPLATE_NAMES.typescript; - break; - } - case constants.ProjectTypes.JsFlavorName: { - template = constants.RESERVED_TEMPLATE_NAMES.javascript; - break; - } - default: { - template = constants.RESERVED_TEMPLATE_NAMES.javascript; - break; - } + if (!version) { + return false; } - return template; + return version; } - private async getTemplateManifest( - projectData: IProjectData, - version: string - ): Promise { - let templateManifest; - const templateName = this.getTemplateName(projectData); - version = - version || - (await this.$packageInstallationManager.getLatestCompatibleVersionSafe( - templateName - )); + private async backupProject(): Promise { + const backup = this.$projectBackupService.getBackup("migration"); + backup.addPaths([...UpdateController.pathsToBackup]); + try { - templateManifest = await this.getPackageManifest(templateName, version); - } catch (err) { - this.$errors.fail( - UpdateController.failedToGetTemplateManifestMessage, - err.message - ); + return backup.create(); + } catch (error) { + this.spinner.fail(`Project backup failed.`); + backup.remove(); + this.$errors.fail(`Project backup failed. Error is: ${error.message}`); } + } - return templateManifest; + private async cleanUpProject(): Promise { + await this.$projectCleanupService.clean([ + constants.HOOKS_DIR_NAME, + constants.PLATFORMS_DIR_NAME, + constants.NODE_MODULES_FOLDER_NAME, + constants.PACKAGE_LOCK_JSON_FILE_NAME, + ]); } } diff --git a/lib/services/project-config-service.ts b/lib/services/project-config-service.ts index a7cef6aef4..c30c12865e 100644 --- a/lib/services/project-config-service.ts +++ b/lib/services/project-config-service.ts @@ -310,7 +310,7 @@ export default { } public writeDefaultConfig(projectDir: string, appId?: string) { - const { TSConfigPath } = this.detectProjectConfigs(projectDir); + const TSConfigPath = path.resolve(projectDir, CONFIG_FILE_NAME_TS); if (this.$fs.exists(TSConfigPath)) { return false; diff --git a/test/controllers/update-controller.ts b/test/controllers/update-controller.ts index 70758fccad..a99cf9802d 100644 --- a/test/controllers/update-controller.ts +++ b/test/controllers/update-controller.ts @@ -3,36 +3,31 @@ import * as yok from "../../lib/common/yok"; import { UpdateController } from "../../lib/controllers/update-controller"; import { assert } from "chai"; import * as sinon from "sinon"; -import * as path from "path"; -import { Options } from "../../lib/options"; -import { StaticConfig } from "../../lib/config"; -import { SettingsService } from "../../lib/common/test/unit-tests/stubs"; -import { DevicePlatformsConstants } from "../../lib/common/mobile/device-platforms-constants"; import { IInjector } from "../../lib/common/definitions/yok"; +import { IPluginsService } from "../../lib/definitions/plugins"; const projectFolder = "test"; function createTestInjector(projectDir: string = projectFolder): IInjector { const testInjector: IInjector = new yok.Yok(); testInjector.register("logger", stubs.LoggerStub); - testInjector.register("options", Options); - testInjector.register("analyticsService", { - trackException: async (): Promise => undefined, - checkConsent: async (): Promise => undefined, - trackFeature: async (): Promise => undefined, - }); testInjector.register("errors", stubs.ErrorsStub); - testInjector.register("staticConfig", StaticConfig); + testInjector.register( + "terminalSpinnerService", + stubs.TerminalSpinnerServiceStub + ); testInjector.register("projectData", { projectDir, initializeProjectData: () => { /* empty */ }, - dependencies: {}, - }); - testInjector.register("settingsService", SettingsService); - testInjector.register("migrateController", { - shouldMigrate: () => { - return false; + dependencies: { + "@nativescript/core": "next", + }, + devDependencies: { + "@nativescript/ios": "8.0.0", + "@nativescript/android": "~8.0.0", + "@nativescript/webpack": "5.0.0-beta.9", + "@nativescript/types": "8.1.0", }, }); testInjector.register("fs", stubs.FileSystemStub); @@ -41,35 +36,68 @@ function createTestInjector(projectDir: string = projectFolder): IInjector { return "5.2.0"; }, }); - testInjector.register("packageManager", { - getTagVersion: () => { - return "2.3.0"; - }, - }); - testInjector.register("addPlatformService", { - setPlatformVersion: () => { - /**/ + getTagVersion(packageName: string, tag: string) { + switch (tag) { + case "next": + return "8.0.0-next"; + case "latest": + return "8.0.4567"; + case "JSC": + if (packageName === "@nativescript/ios") { + return "6.5.4-jsc"; + } + } + + return false; }, }); testInjector.register("pluginsService", { - addToPackageJson: () => { - /**/ - }, + addToPackageJson() {}, }); - testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); + + class PackageInstallationManagerStub extends stubs.PackageInstallationManagerStub { + getInstalledDependencyVersion = async (packageName: string) => { + const projectData = testInjector.resolve("projectData"); + const deps = { + ...projectData.dependencies, + ...projectData.devDependencies, + }; + + if (deps[packageName]) { + return deps[packageName]; + } + + return ""; + }; + } testInjector.register( "packageInstallationManager", - stubs.PackageInstallationManagerStub + PackageInstallationManagerStub ); testInjector.register("platformsDataService", stubs.NativeProjectDataStub); testInjector.register("pacoteService", stubs.PacoteServiceStub); - testInjector.register("projectDataService", stubs.ProjectDataServiceStub); + testInjector.register("projectDataService", { + getProjectData() { + return testInjector.resolve("projectData"); + }, + }); testInjector.register("updateController", UpdateController); + testInjector.register("projectBackupService", stubs.ProjectBackupServiceStub); + testInjector.register("projectCleanupService", { + clean() {}, + }); return testInjector; } +function assertCalled(stub: sinon.SinonStub, ...args: any[]) { + assert( + stub.calledWith(...args), + `Expected a call with (${args.join(", ")}).` + ); +} + describe("update controller method tests", () => { let sandbox: sinon.SinonSandbox; @@ -81,162 +109,205 @@ describe("update controller method tests", () => { sandbox.restore(); }); - it("if backup fails, platforms not deleted, temp removed", async () => { + it("if backup fails, project is not cleaned", async () => { const testInjector = createTestInjector(); - const fs = testInjector.resolve("fs"); - const deleteDirectory: sinon.SinonStub = sandbox.stub( - fs, - "deleteDirectory" + const projectBackupService = testInjector.resolve("projectBackupService"); + const projectCleanupService = testInjector.resolve("projectCleanupService"); + const updateController = testInjector.resolve("updateController"); + + projectBackupService.shouldFail(true); + + let cleanCalled = false; + projectCleanupService.clean = () => { + cleanCalled = true; + }; + + let hasError = false; + await updateController + .update({ + projectDir: projectFolder, + version: "3.3.0", + }) + .catch(() => { + hasError = true; + }); + + assert.equal(projectBackupService._backups.length, 1); + + const backup = projectBackupService._backups[0]; + + assert.isTrue( + hasError, + "expected updateController.update to throw an error" ); - sandbox.stub(fs, "copyFile").throws(); + assert.isTrue( + backup._meta.createCalled, + "expected backup.create() to have been called" + ); + assert.isFalse(cleanCalled, "clean called even though backup failed"); + assert.isTrue( + backup._meta.removeCalled, + "expected backup.remove() to have been called" + ); + }); + + it("handles exact versions", async () => { + const testInjector = createTestInjector(); const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" + ); + + const stub = sinon.stub(pluginsService, "addToPackageJson"); await updateController.update({ projectDir: projectFolder, - version: "3.3.0", + version: "8.0.1234", }); - assert.isTrue( - deleteDirectory.calledWith( - path.join(projectFolder, UpdateController.backupFolder) - ) + assertCalled(stub, "@nativescript/core", "8.0.1234"); + assertCalled(stub, "@nativescript/webpack", "8.0.1234"); + assertCalled(stub, "@nativescript/types", "8.0.1234"); + assertCalled(stub, "@nativescript/ios", "8.0.1234"); + assertCalled(stub, "@nativescript/android", "8.0.1234"); + }); + + it("handles range versions", async () => { + const testInjector = createTestInjector(); + const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" + ); + + const stub = sinon.stub(pluginsService, "addToPackageJson"); + + await updateController.update({ + projectDir: projectFolder, + version: "~8.0.1234", + }); + + assertCalled(stub, "@nativescript/core", "~8.0.1234"); + assertCalled(stub, "@nativescript/webpack", "~8.0.1234"); + assertCalled(stub, "@nativescript/types", "~8.0.1234"); + assertCalled(stub, "@nativescript/ios", "~8.0.1234"); + assertCalled(stub, "@nativescript/android", "~8.0.1234"); + }); + + it("handles range versions", async () => { + const testInjector = createTestInjector(); + const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" ); - assert.isFalse( - deleteDirectory.calledWith(path.join(projectFolder, "platforms")) + + const stub = sinon.stub(pluginsService, "addToPackageJson"); + + await updateController.update({ + projectDir: projectFolder, + version: "^8.0.1234", + }); + + assertCalled(stub, "@nativescript/core", "^8.0.1234"); + assertCalled(stub, "@nativescript/webpack", "^8.0.1234"); + assertCalled(stub, "@nativescript/types", "^8.0.1234"); + assertCalled(stub, "@nativescript/ios", "^8.0.1234"); + assertCalled(stub, "@nativescript/android", "^8.0.1234"); + }); + + it("handles latest tag versions", async () => { + const testInjector = createTestInjector(); + const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" ); + + const stub = sinon.stub(pluginsService, "addToPackageJson"); + + await updateController.update({ + projectDir: projectFolder, + version: "latest", + }); + + assertCalled(stub, "@nativescript/core", "~8.0.4567"); + assertCalled(stub, "@nativescript/webpack", "~8.0.4567"); + assertCalled(stub, "@nativescript/types", "~8.0.4567"); + assertCalled(stub, "@nativescript/ios", "~8.0.4567"); + assertCalled(stub, "@nativescript/android", "~8.0.4567"); }); - it("calls copy to temp for package.json and folders(backup)", async () => { + it("handles existing tag versions", async () => { const testInjector = createTestInjector(); - const fs = testInjector.resolve("fs"); - const copyFileStub = sandbox.stub(fs, "copyFile"); const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" + ); + + const stub = sinon.stub(pluginsService, "addToPackageJson"); await updateController.update({ projectDir: projectFolder, - version: "3.3.0", + version: "next", }); - assert.isTrue( - copyFileStub.calledWith(path.join(projectFolder, "package.json")) + assertCalled(stub, "@nativescript/core", "8.0.0-next"); + assertCalled(stub, "@nativescript/webpack", "8.0.0-next"); + assertCalled(stub, "@nativescript/types", "8.0.0-next"); + assertCalled(stub, "@nativescript/ios", "8.0.0-next"); + assertCalled(stub, "@nativescript/android", "8.0.0-next"); + }); + + it("handles non-existing tag versions", async () => { + const testInjector = createTestInjector(); + const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" ); - for (const folder of UpdateController.folders) { - assert.isTrue(copyFileStub.calledWith(path.join(projectFolder, folder))); - } + + const stub = sinon.stub(pluginsService, "addToPackageJson"); + + await updateController.update({ + projectDir: projectFolder, + version: "nonexistent", + }); + + assert(stub.notCalled); }); - it("calls copy from temp for package.json and folders to project folder(restore)", async () => { + it("handles partially existing tag versions", async () => { const testInjector = createTestInjector(); - testInjector.resolve("platformCommandHelper").removePlatforms = () => { - throw new Error(); - }; - const fs = testInjector.resolve("fs"); - const copyFileStub = sandbox.stub(fs, "copyFile"); const updateController = testInjector.resolve("updateController"); - const tempDir = path.join(projectFolder, UpdateController.backupFolder); + const pluginsService = testInjector.resolve( + "pluginsService" + ); + + const stub = sinon.stub(pluginsService, "addToPackageJson"); await updateController.update({ projectDir: projectFolder, - version: "3.3.0", + version: "JSC", }); - assert.isTrue( - copyFileStub.calledWith( - path.join(tempDir, "package.json"), - path.resolve(projectFolder, "package.json") - ) + assert(stub.calledOnce); + assertCalled(stub, "@nativescript/ios", "6.5.4-jsc"); + }); + + it("handles no version - falls back to latest", async () => { + const testInjector = createTestInjector(); + const updateController = testInjector.resolve("updateController"); + const pluginsService = testInjector.resolve( + "pluginsService" ); - for (const folder of UpdateController.folders) { - assert.isTrue( - copyFileStub.calledWith( - path.join(tempDir, folder), - path.resolve(projectFolder, folder) - ) - ); - } - }); - - // TODO: Igor and Nathan to bring back when making update/migrations work with latest - // for (const projectType of ["Angular", "React"]) { - // it(`should update dependencies from project type: ${projectType}`, async () => { - // const testInjector = createTestInjector(); - // testInjector.resolve("platformCommandHelper").removePlatforms = () => { - // throw new Error(); - // }; - - // const fs = testInjector.resolve("fs"); - // const copyFileStub = sandbox.stub(fs, "copyFile"); - // const updateController = testInjector.resolve("updateController"); - // const tempDir = path.join(projectFolder, UpdateController.backupFolder); - - // const projectDataService = testInjector.resolve("projectDataService"); - // projectDataService.getProjectData = (projectDir: string) => { - // return { - // projectDir, - // projectType, - // dependencies: { - // "tns-core-modules": "0.1.0", - // }, - // devDependencies: { - // "nativescript-dev-webpack": "1.1.3" - // } - // }; - // }; - - // const packageInstallationManager = testInjector.resolve("packageInstallationManager"); - // const latestCompatibleVersion = "1.1.1"; - // packageInstallationManager.getLatestCompatibleVersionSafe = async (packageName: string, referenceVersion?: string): Promise => { - // assert.isString(packageName); - // assert.isFalse(_.isEmpty(packageName)); - // return latestCompatibleVersion; - // }; - - // const pacoteService = testInjector.resolve("pacoteService"); - // pacoteService.manifest = async (packageName: string, options?: IPacoteManifestOptions): Promise => { - // assert.isString(packageName); - // assert.isFalse(_.isEmpty(packageName)); - - // return { - // dependencies: { - // "tns-core-modules": "1.0.0", - // "dep2": "1.1.0" - // }, - // devDependencies: { - // "devDep1": "1.2.0", - // "nativescript-dev-webpack": "1.3.0" - // }, - // name: "template1" - // }; - // }; - - // const pluginsService = testInjector.resolve("pluginsService"); - // const dataAddedToPackageJson: IDictionary = { - // dependencies: {}, - // devDependencies: {} - // }; - // pluginsService.addToPackageJson = (plugin: string, version: string, isDev: boolean, projectDir: string): void => { - // if (isDev) { - // dataAddedToPackageJson.devDependencies[plugin] = version; - // } else { - // dataAddedToPackageJson.dependencies[plugin] = version; - // } - // }; - - // await updateController.update({ projectDir: projectFolder }); - - // assert.isTrue(copyFileStub.calledWith(path.join(tempDir, "package.json"), projectFolder)); - // for (const folder of UpdateController.folders) { - // assert.isTrue(copyFileStub.calledWith(path.join(tempDir, folder), projectFolder)); - // } - - // assert.deepStrictEqual(dataAddedToPackageJson, { - // dependencies: { - // "tns-core-modules": "1.0.0", - // }, - // devDependencies: { - // "nativescript-dev-webpack": "1.3.0" - // } - // }); - // }); - // } + + const stub = sinon.stub(pluginsService, "addToPackageJson"); + + await updateController.update({ + projectDir: projectFolder, + }); + + assertCalled(stub, "@nativescript/core", "~8.0.4567"); + assertCalled(stub, "@nativescript/webpack", "~8.0.4567"); + assertCalled(stub, "@nativescript/types", "~8.0.4567"); + assertCalled(stub, "@nativescript/ios", "~8.0.4567"); + assertCalled(stub, "@nativescript/android", "~8.0.4567"); + }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 2807ef42dd..0c930038d6 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -36,6 +36,8 @@ import { IProjectTemplatesService, ITemplateData, IProjectConfigInformation, + IProjectBackupService, + IBackup, } from "../lib/definitions/project"; import { IPlatformData, @@ -501,85 +503,76 @@ export class NodePackageManagerStub implements INodePackageManager { } } -export class ProjectDataStub implements IProjectData { - packageJsonData: any; - projectDir: string; - projectName: string; - webpackConfigPath: string; +export class ProjectBackupServiceStub implements IProjectBackupService { + public _backups: any[] = []; + public _shouldFail: boolean = false; - get platformsDir(): string { - return ( - this.platformsDirCache || - (this.projectDir && join(this.projectDir, "platforms")) || - "" - ); + shouldFail(shouldFail: boolean) { + this._shouldFail = shouldFail; } - set platformsDir(value) { - this.platformsDirCache = value; - } + getBackup(name: string): IBackup { + const backup = new ProjectBackupServiceStub.Backup(); + this._backups.push(backup); - projectFilePath: string; - projectIdentifiers: Mobile.IProjectIdentifier = { - android: "org.nativescirpt.myiOSApp", - ios: "org.nativescript.myProjectApp", - }; - projectId: string; - dependencies: any; - nsConfig: any; - appDirectoryPath: string; - devDependencies: IStringDictionary; - projectType: string; - appResourcesDirectoryPath: string; - private platformsDirCache: string = ""; - public androidManifestPath: string; - public infoPlistPath: string; - public appGradlePath: string; - public gradleFilesDirectoryPath: string; - public buildXcconfigPath: string; - public podfilePath: string; - public isShared: boolean; - public previewAppSchema: string; + if (this._shouldFail) { + const origCreate = backup.create; + backup.create = () => { + origCreate.call(backup); + throw new Error("backup failed (intended for tests)"); + }; + } - public initializeProjectData(projectDir?: string): void { - this.projectDir = this.projectDir || projectDir; - this.projectIdentifiers = { android: "", ios: "" }; - this.projectId = ""; - this.projectName = ""; + return backup; } - - public initializeProjectDataFromContent(): void { - return; + backup(name: string, pathsToBackup: string[]): IBackup { + return this.getBackup(name).create(); + } + restore(name: string, pathsToRestore: string[]): IBackup { + return this.getBackup(name).restore(); } - public getAppResourcesDirectoryPath(projectDir?: string): string { - if (!projectDir) { - projectDir = this.projectDir; + static Backup = class Backup implements IBackup { + public _meta = { + createCalled: false, + restoreCalled: false, + removeCalled: false, + isUpToDateCalled: false, + addPathCalled: false, + addPathsCalled: false, + }; + constructor(public pathsToBackup: string[] = []) {} + create(): IBackup { + this._meta.createCalled = true; + + return this; } + restore(): IBackup { + this._meta.restoreCalled = true; - // always return app/App_Resources - return join( - projectDir, - constants.APP_FOLDER_NAME, - constants.APP_RESOURCES_FOLDER_NAME - ); - } + return this; + } + remove(): IBackup { + this._meta.removeCalled = true; - public getAppResourcesRelativeDirectoryPath(): string { - return ""; - } + return this; + } + isUpToDate(): boolean { + this._meta.isUpToDateCalled = true; - public getAppDirectoryPath(projectDir?: string): string { - if (!projectDir) { - projectDir = this.projectDir; + return true; } + addPath(path: string): IBackup { + this._meta.addPathCalled = true; - return join(projectDir, "app") || ""; - } + return this; + } + addPaths(paths: string[]): IBackup { + this._meta.addPathsCalled = true; - public getAppDirectoryRelativePath(): string { - return "app"; - } + return this; + } + }; } export class ProjectConfigServiceStub implements IProjectConfigService { @@ -648,6 +641,87 @@ export class ProjectConfigServiceStub implements IProjectConfigService { ): Promise {} } +export class ProjectDataStub implements IProjectData { + packageJsonData: any; + projectDir: string; + projectName: string; + webpackConfigPath: string; + + get platformsDir(): string { + return ( + this.platformsDirCache || + (this.projectDir && join(this.projectDir, "platforms")) || + "" + ); + } + + set platformsDir(value) { + this.platformsDirCache = value; + } + + projectFilePath: string; + projectIdentifiers: Mobile.IProjectIdentifier = { + android: "org.nativescirpt.myiOSApp", + ios: "org.nativescript.myProjectApp", + }; + projectId: string; + dependencies: any; + nsConfig: any; + appDirectoryPath: string; + devDependencies: IStringDictionary; + projectType: string; + appResourcesDirectoryPath: string; + private platformsDirCache: string = ""; + public androidManifestPath: string; + public infoPlistPath: string; + public appGradlePath: string; + public gradleFilesDirectoryPath: string; + public buildXcconfigPath: string; + public podfilePath: string; + public isShared: boolean; + public previewAppSchema: string; + + public initializeProjectData(projectDir?: string): void { + this.projectDir = this.projectDir || projectDir; + this.projectIdentifiers = { android: "", ios: "" }; + this.projectId = ""; + this.projectName = ""; + } + + public initializeProjectDataFromContent(): void { + return; + } + + public getAppResourcesDirectoryPath(projectDir?: string): string { + if (!projectDir) { + projectDir = this.projectDir; + } + + // always return app/App_Resources + return join( + projectDir, + constants.APP_FOLDER_NAME, + constants.APP_RESOURCES_FOLDER_NAME + ); + } + + public getAppResourcesRelativeDirectoryPath(): string { + return ""; + } + + public getAppDirectoryPath(projectDir?: string): string { + if (!projectDir) { + projectDir = this.projectDir; + } + + return join(projectDir, "app") || ""; + } + + public getAppDirectoryRelativePath(): string { + return "app"; + } +} + export class AndroidPluginBuildServiceStub implements IAndroidPluginBuildService { buildAar(options: IPluginBuildOptions): Promise { diff --git a/yarn.lock b/yarn.lock index 5c481c464b..37ce050d45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1188,13 +1188,6 @@ "resolved" "https://registry.npmjs.org/binaryextensions/-/binaryextensions-4.13.0.tgz" "version" "4.13.0" -"bindings@^1.5.0": - "integrity" "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==" - "resolved" "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" - "version" "1.5.0" - dependencies: - "file-uri-to-path" "1.0.0" - "bmp-js@^0.1.0": "integrity" "sha1-4Fpj95amwf8l9Hcex62twUjAcjM=" "resolved" "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz" @@ -2674,7 +2667,7 @@ "resolved" "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz" "version" "9.0.0" -"file-uri-to-path@1", "file-uri-to-path@1.0.0": +"file-uri-to-path@1": "integrity" "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" "resolved" "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" "version" "1.0.0" @@ -2886,24 +2879,6 @@ "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" "version" "1.0.0" -"fsevents@^1.2.7": - "integrity" "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==" - "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz" - "version" "1.2.13" - dependencies: - "bindings" "^1.5.0" - "nan" "^2.12.1" - -"fsevents@~2.1.2": - "integrity" "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==" - "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz" - "version" "2.1.3" - -"fsevents@~2.3.1": - "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" - "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - "version" "2.3.2" - "ftp@~0.3.10": "integrity" "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=" "resolved" "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz" @@ -4956,11 +4931,6 @@ "resolved" "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" "version" "0.0.8" -"nan@^2.12.1": - "integrity" "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" - "resolved" "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz" - "version" "2.14.1" - "nanoid@^1.0.7": "integrity" "sha512-4ug4BsuHxiVHoRUe1ud6rUFT3WUMmjXt1W0quL0CviZQANdan7D8kqN5/maw53hmAApY/jfzMRkC57BNNs60ZQ==" "resolved" "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz"