From 10a2d2e3b4b6a6856b1f1898dbf9b180c0dd649b Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 31 Mar 2021 23:44:14 +0200 Subject: [PATCH] feat: ns8 (#5508) * chore: handle staleFiles * refactor: clean up karma-execution * fix(arm64): run pod install via rosetta2 * chore: webpack5 wip * test: fix assertion to include new staleFiles * chore(release): 8.0.0-alpha.0 * chore: improved webpack5 handling * chore: cleanup * fix: ignore empty compilation * chore(release): 8.0.0-alpha.1 * chore(release): 8.0.0-alpha.2 * chore: fix hmr regex * chore(release): 8.0.0-alpha.3 * feat: migrations for ns8 * chore: remove console.logs * chore: bump ios-device-lib * feat: allow defaultValue fallback in config getValue * chore: adjust migration desiredVersions --- lib/bootstrap.ts | 2 + lib/commands/migrate.ts | 11 +- lib/commands/update.ts | 2 +- lib/common/dispatchers.ts | 1 + lib/controllers/migrate-controller.ts | 1315 ++++++++++------- lib/controllers/prepare-controller.ts | 2 + lib/controllers/run-controller.ts | 15 +- lib/controllers/update-controller-base.ts | 11 +- lib/declarations.d.ts | 2 + lib/definitions/migrate.d.ts | 19 +- lib/services/cocoapods-service.ts | 12 +- lib/services/hmr-status-service.ts | 17 + lib/services/karma-execution.ts | 16 +- lib/services/project-cleanup-service.ts | 18 +- lib/services/project-config-service.ts | 4 +- .../webpack/webpack-compiler-service.ts | 161 +- lib/services/webpack/webpack.d.ts | 1 + lib/shared-event-bus.ts | 14 + package-lock.json | 8 +- package.json | 4 +- test/controllers/prepare-controller.ts | 1 + .../webpack/webpack-compiler-service.ts | 4 +- tslint.json | 2 +- 23 files changed, 1013 insertions(+), 629 deletions(-) create mode 100644 lib/shared-event-bus.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 024cd730dd..6eb120a8de 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -472,3 +472,5 @@ injector.require( "./services/metadata-filtering-service" ); injector.require("tempService", "./services/temp-service"); + +injector.require("sharedEventBus", "./shared-event-bus"); diff --git a/lib/commands/migrate.ts b/lib/commands/migrate.ts index 5fce88f3d2..09cb643343 100644 --- a/lib/commands/migrate.ts +++ b/lib/commands/migrate.ts @@ -9,6 +9,7 @@ export class MigrateCommand implements ICommand { constructor( private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $migrateController: IMigrateController, + private $staticConfig: Config.IStaticConfig, private $projectData: IProjectData, private $logger: ILogger ) { @@ -28,18 +29,12 @@ export class MigrateCommand implements ICommand { ); if (!shouldMigrateResult) { + const cliVersion = this.$staticConfig.version; this.$logger.printMarkdown( - '__Project is compatible with NativeScript "v7.0.0". To get the latest NativeScript packages execute "ns update".__' + `__Project is compatible with NativeScript \`v${cliVersion}\`__` ); return; } - // else if (shouldMigrateResult.shouldMigrate === ShouldMigrate.ADVISED) { - // // todo: this shouldn't be here, because this is already the `ns migrate` path. - // this.$logger.printMarkdown( - // '__Project should work with NativeScript "v7.0.0" but a migration is advised. Run ns migrate to migrate.__' - // ); - // return; - // } await this.$migrateController.migrate(migrationData); } diff --git a/lib/commands/update.ts b/lib/commands/update.ts index f375b0a1cf..890b0d66bc 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -61,7 +61,7 @@ export class UpdateCommand implements ICommand { this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.iOS, ], - allowInvalidVersions: true, + loose: true, }); if (shouldMigrate) { diff --git a/lib/common/dispatchers.ts b/lib/common/dispatchers.ts index cc877b7afd..05c10695b4 100644 --- a/lib/common/dispatchers.ts +++ b/lib/common/dispatchers.ts @@ -37,6 +37,7 @@ export class CommandDispatcher implements ICommandDispatcher { if (this.$logger.getLevel() === "TRACE") { // CommandDispatcher is called from external CLI's only, so pass the path to their package.json + this.$logger.trace("Collecting system information..."); const sysInfo = await this.$sysInfo.getSysInfo({ pathToNativeScriptCliPackageJson: path.join( __dirname, diff --git a/lib/controllers/migrate-controller.ts b/lib/controllers/migrate-controller.ts index d8b3077d36..37247d473b 100644 --- a/lib/controllers/migrate-controller.ts +++ b/lib/controllers/migrate-controller.ts @@ -16,6 +16,7 @@ import { IProjectDataService, } from "../definitions/project"; import { + IDependencyVersion, IMigrateController, IMigrationData, IMigrationDependency, @@ -33,6 +34,7 @@ import { } from "../definitions/platform"; import { IPluginsService } from "../definitions/plugins"; import { + IChildProcess, IDictionary, IErrors, IFileSystem, @@ -80,7 +82,8 @@ export class MigrateController private $staticConfig: Config.IStaticConfig, private $terminalSpinnerService: ITerminalSpinnerService, private $projectCleanupService: IProjectCleanupService, - private $projectBackupService: IProjectBackupService + private $projectBackupService: IProjectBackupService, + private $childProcess: IChildProcess ) { super( $fs, @@ -114,6 +117,9 @@ export class MigrateController this.$settingsService.getProfileDir(), `should-migrate-cache-${cliVersion}.json` ); + this.$logger.trace( + `Migration cache path is: ${shouldMigrateCacheFilePath}` + ); return this.$injector.resolve("jsonFileSettingsService", { jsonFileSettingsPath: shouldMigrateCacheFilePath, }); @@ -122,7 +128,8 @@ export class MigrateController private migrationDependencies: IMigrationDependency[] = [ { packageName: constants.SCOPED_TNS_CORE_MODULES, - verifiedVersion: "7.0.0", + minVersion: "6.5.0", + desiredVersion: "~8.0.0", shouldAddIfMissing: true, }, { @@ -131,13 +138,14 @@ export class MigrateController }, { packageName: "@nativescript/types", - verifiedVersion: "7.0.0", + minVersion: "7.0.0", + desiredVersion: "~8.0.0", isDev: true, }, { packageName: "tns-platform-declarations", replaceWith: "@nativescript/types", - verifiedVersion: "7.0.0", + minVersion: "6.5.0", isDev: true, }, { @@ -149,133 +157,129 @@ export class MigrateController replaceWith: constants.WEBPACK_PLUGIN_NAME, shouldRemove: true, isDev: true, - shouldMigrateAction: async () => { + async shouldMigrateAction() { return true; }, migrateAction: this.migrateWebpack.bind(this), }, { packageName: constants.WEBPACK_PLUGIN_NAME, - verifiedVersion: "3.0.0", + minVersion: "3.0.0", + desiredVersion: "~5.0.0-beta.0", shouldAddIfMissing: true, + isDev: true, }, { packageName: "nativescript-vue", - verifiedVersion: "2.8.0", - shouldMigrateAction: async ( + minVersion: "2.7.0", + desiredVersion: "~2.9.0", + async shouldMigrateAction( + dependency: IMigrationDependency, projectData: IProjectData, - allowInvalidVersions: boolean - ) => { - const dependency = { - packageName: "nativescript-vue", - verifiedVersion: "2.8.0", - isDev: false, - }; - const result = - this.hasDependency(dependency, projectData) && - (await this.shouldMigrateDependencyVersion( - dependency, - projectData, - allowInvalidVersions - )); + loose: boolean + ) { + if (!this.hasDependency(dependency, projectData)) { + return false; + } - return result; + return await this.shouldMigrateDependencyVersion( + dependency, + projectData, + loose + ); }, migrateAction: this.migrateNativeScriptVue.bind(this), }, { packageName: "nativescript-angular", replaceWith: "@nativescript/angular", - verifiedVersion: "10.0.0", + minVersion: "10.0.0", }, { packageName: "@nativescript/angular", - verifiedVersion: "10.0.0", - shouldMigrateAction: async ( + minVersion: "10.0.0", + desiredVersion: "~11.8.0", + async shouldMigrateAction( + dependency: IMigrationDependency, projectData: IProjectData, - allowInvalidVersions: boolean - ) => { - const dependency = { - packageName: "@nativescript/angular", - verifiedVersion: "10.0.0", - isDev: false, - }; - const result = - this.hasDependency(dependency, projectData) && - (await this.shouldMigrateDependencyVersion( - dependency, - projectData, - allowInvalidVersions - )); - return result; + loose: boolean + ) { + if (!this.hasDependency(dependency, projectData)) { + return false; + } + + return await this.shouldMigrateDependencyVersion( + dependency, + projectData, + loose + ); }, migrateAction: this.migrateNativeScriptAngular.bind(this), }, { packageName: "svelte-native", - verifiedVersion: "0.9.4", - shouldMigrateAction: async ( + minVersion: "0.9.0", + desiredVersion: "~0.9.4", + async shouldMigrateAction( + dependency: IMigrationDependency, projectData: IProjectData, - allowInvalidVersions: boolean - ) => { - const dependency = { - packageName: "svelte-native", - verifiedVersion: "0.9.0", // minimum version required - anything less will need a migration - isDev: false, - }; - const result = - this.hasDependency(dependency, projectData) && - (await this.shouldMigrateDependencyVersion( - dependency, - projectData, - allowInvalidVersions - )); - - return result; + loose: boolean + ) { + if (!this.hasDependency(dependency, projectData)) { + return false; + } + return await this.shouldMigrateDependencyVersion( + dependency, + projectData, + loose + ); }, migrateAction: this.migrateNativeScriptSvelte.bind(this), }, { packageName: "@nativescript/unit-test-runner", - verifiedVersion: "1.0.0", - shouldMigrateAction: async ( + minVersion: "1.0.0", + async shouldMigrateAction( + dependency: IMigrationDependency, projectData: IProjectData, - allowInvalidVersions: boolean - ) => { - const dependency = { - packageName: "@nativescript/unit-test-runner", - verifiedVersion: "1.0.0", - isDev: false, - }; - const result = - this.hasDependency(dependency, projectData) && - (await this.shouldMigrateDependencyVersion( - dependency, - projectData, - allowInvalidVersions - )); - return result; + loose: boolean + ) { + if (!this.hasDependency(dependency, projectData)) { + return false; + } + return await this.shouldMigrateDependencyVersion( + dependency, + projectData, + loose + ); }, migrateAction: this.migrateUnitTestRunner.bind(this), }, { packageName: "typescript", isDev: true, - verifiedVersion: "3.9.7", + minVersion: "3.7.0", + desiredVersion: "~4.0.0", }, ]; - get verifiedPlatformVersions(): IDictionary { + get verifiedPlatformVersions(): IDictionary { return { - [this.$devicePlatformsConstants.Android.toLowerCase()]: "6.5.3", - [this.$devicePlatformsConstants.iOS.toLowerCase()]: "6.5.2", + [this.$devicePlatformsConstants.Android.toLowerCase()]: { + minVersion: "6.5.3", + desiredVersion: "8.0.0", + }, + [this.$devicePlatformsConstants.iOS.toLowerCase()]: { + minVersion: "6.5.4", + desiredVersion: "8.0.0", + }, }; } public async shouldMigrate({ projectDir, platforms, - allowInvalidVersions = false, + loose = false, }: IMigrationData): Promise { const remainingPlatforms = []; @@ -284,14 +288,16 @@ export class MigrateController for (const platform of platforms) { const cachedResult = await this.getCachedShouldMigrate( projectDir, - platform + platform, + loose + ); + this.$logger.trace( + `Got cached result for shouldMigrate for platform: ${platform}: ${cachedResult}` ); + + // the cached result is only used if it's false, otherwise we need to check again if (cachedResult !== false) { remainingPlatforms.push(platform); - } else { - this.$logger.trace( - `Got cached result for shouldMigrate for platform: ${platform}` - ); } } @@ -299,7 +305,7 @@ export class MigrateController shouldMigrate = await this._shouldMigrate({ projectDir, platforms: remainingPlatforms, - allowInvalidVersions, + loose: loose, }); this.$logger.trace( `Executed shouldMigrate for platforms: ${remainingPlatforms}. Result is: ${shouldMigrate}` @@ -307,7 +313,11 @@ export class MigrateController if (!shouldMigrate) { for (const remainingPlatform of remainingPlatforms) { - await this.setCachedShouldMigrate(projectDir, remainingPlatform); + await this.setCachedShouldMigrate( + projectDir, + remainingPlatform, + loose + ); } } } @@ -318,12 +328,12 @@ export class MigrateController public async validate({ projectDir, platforms, - allowInvalidVersions = true, + loose = true, }: IMigrationData): Promise { const shouldMigrate = await this.shouldMigrate({ projectDir, platforms, - allowInvalidVersions, + loose, }); if (shouldMigrate) { this.$errors.fail( @@ -335,7 +345,7 @@ export class MigrateController public async migrate({ projectDir, platforms, - allowInvalidVersions = false, + loose = false, }: IMigrationData): Promise { this.spinner = this.$terminalSpinnerService.createSpinner(); const projectData = this.$projectDataService.getProjectData(projectDir); @@ -343,7 +353,7 @@ export class MigrateController this.$logger.trace("MigrationController.migrate called with", { projectDir, platforms, - allowInvalidVersions, + loose: loose, }); // ensure in git repo and require --force if not (for safety) @@ -366,7 +376,7 @@ export class MigrateController this.spinner.succeed(); // clean up project files - this.spinner.start("Cleaning up project files before migration"); + this.spinner.info("Cleaning up project files before migration"); await this.cleanUpProject(projectData); @@ -381,26 +391,25 @@ export class MigrateController this.spinner.text = "Cleaned old artifacts"; this.spinner.succeed(); - // migrate configs - this.spinner.start( - `Migrating project to use ${"nativescript.config.ts".green}` - ); + const newConfigPath = path.resolve(projectDir, "nativescript.config.ts"); + if (!this.$fs.exists(newConfigPath)) { + // migrate configs + this.spinner.start( + `Migrating project to use ${"nativescript.config.ts".green}` + ); - await this.migrateConfigs(projectDir); + await this.migrateConfigs(projectDir); - this.spinner.text = `Project has been migrated to use ${ - "nativescript.config.ts".green - }`; - this.spinner.succeed(); + this.spinner.text = `Project has been migrated to use ${ + "nativescript.config.ts".green + }`; + this.spinner.succeed(); + } // update dependencies this.spinner.start("Updating project dependencies"); - await this.migrateDependencies( - projectData, - platforms, - allowInvalidVersions - ); + await this.migrateDependencies(projectData, platforms, loose); this.spinner.text = "Project dependencies have been updated"; this.spinner.succeed(); @@ -408,13 +417,24 @@ export class MigrateController // update tsconfig const tsConfigPath = path.resolve(projectDir, "tsconfig.json"); if (this.$fs.exists(tsConfigPath)) { - this.spinner.start("Updating tsconfig.json"); + this.spinner.start(`Updating ${"tsconfig.json".yellow}`); await this.migrateTSConfig(tsConfigPath); - this.spinner.succeed("Updated tsconfig.json"); + this.spinner.succeed(`Updated ${"tsconfig.json".yellow}`); } + 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"); // @@ -433,7 +453,7 @@ export class MigrateController this.$logger.info(""); this.$logger.printMarkdown( "Project has been successfully migrated. The next step is to run `ns run ` to ensure everything is working properly." + - "\n\nPlease note that `ns migrate` does not make changes to your source code, you may need additional changes to complete the migration." + "\n\nPlease note that you may need additional changes to complete the migration." // + "\n\nYou may restore your project with `ns migrate restore`" ); @@ -496,7 +516,7 @@ export class MigrateController // await this.migrateDependencies( // projectData, // platforms, - // allowInvalidVersions + // loose // ); // } catch (error) { // const backupFolders = MigrateController.pathsToBackup; @@ -516,148 +536,10 @@ export class MigrateController // // this.spinner.info(MigrateController.MIGRATE_FINISH_MESSAGE); } - private async ensureGitCleanOrForce(projectDir: string): Promise { - const git: SimpleGit = simpleGit(projectDir); - const isGit = await git.checkIsRepo(); - const isForce = this.$options.force; - if (!isGit) { - // not a git repo and no --force - if (!isForce) { - this.$logger.printMarkdown( - `Running \`ns migrate\` in a non-git project is not recommended. If you want to skip this check run \`ns migrate --force\`.` - ); - this.$errors.fail("Not in Git repo."); - return false; - } - this.spinner.warn(`Not in Git repo, but using ${"--force".red}`); - return true; - } - - const isClean = (await git.status()).isClean(); - if (!isClean) { - if (!isForce) { - this.$logger.printMarkdown( - `Current git branch has uncommitted changes. Please commit the changes and try again. Alternatively run \`ns migrate --force\` to skip this check.` - ); - this.$errors.fail("Git branch not clean."); - return false; - } - this.spinner.warn(`Git branch not clean, but using ${"--force".red}`); - return true; - } - - return true; - } - - private async backupProject(projectDir: string): Promise { - const projectData = this.$projectDataService.getProjectData(projectDir); - const backup = this.$projectBackupService.getBackup("migration"); - backup.addPaths([ - ...MigrateController.pathsToBackup, - path.join(projectData.getAppDirectoryRelativePath(), "package.json"), - ]); - - try { - return backup.create(); - } catch (error) { - this.spinner.fail(`Project backup failed.`); - backup.remove(); - this.$errors.fail(`Project backup failed. Error is: ${error.message}`); - } - } - - private async migrateConfigs(projectDir: string): Promise { - const projectData = this.$projectDataService.getProjectData(projectDir); - - // package.json - const rootPackageJsonPath: any = path.resolve( - projectDir, - constants.PACKAGE_JSON_FILE_NAME - ); - // nested package.json - const embeddedPackageJsonPath = path.resolve( - projectData.projectDir, - projectData.getAppDirectoryRelativePath(), - constants.PACKAGE_JSON_FILE_NAME - ); - // nsconfig.json - const legacyNsConfigPath = path.resolve( - projectData.projectDir, - constants.CONFIG_NS_FILE_NAME - ); - - let rootPackageJsonData: any = {}; - if (this.$fs.exists(rootPackageJsonPath)) { - rootPackageJsonData = this.$fs.readJson(rootPackageJsonPath); - } - - // write the default config unless it already exists - const newConfigPath = this.$projectConfigService.writeDefaultConfig( - projectData.projectDir - ); - - // force legacy config mode - this.$projectConfigService.setForceUsingLegacyConfig(true); - - // all different sources are combined into configData (nested package.json, nsconfig and root package.json[nativescript]) - const configData = this.$projectConfigService.readConfig( - projectData.projectDir - ); - - // we no longer want to force legacy config mode - this.$projectConfigService.setForceUsingLegacyConfig(false); - - // move main key into root package.json - if (configData.main) { - rootPackageJsonData.main = configData.main; - delete configData.main; - } - - // detect appPath and App_Resources path - configData.appPath = this.detectAppPath(projectDir, configData); - configData.appResourcesPath = this.detectAppResourcesPath( - projectDir, - configData - ); - - // delete nativescript key from root package.json - if (rootPackageJsonData.nativescript) { - delete rootPackageJsonData.nativescript; - } - - // force the config service to use nativescript.config.ts - this.$projectConfigService.setForceUsingNewConfig(true); - // migrate data into nativescript.config.ts - const hasUpdatedConfigSuccessfully = await this.$projectConfigService.setValue( - "", // root - configData as { [key: string]: SupportedConfigValues } - ); - - if (!hasUpdatedConfigSuccessfully) { - if (typeof newConfigPath === "string") { - // only clean the config if it was created by the migration script - await this.$projectCleanupService.cleanPath(newConfigPath); - } - - this.$errors.fail( - `Failed to migrate project to use ${constants.CONFIG_FILE_NAME_TS}. One or more values could not be updated.` - ); - } - - // save root package.json - this.$fs.writeJson(rootPackageJsonPath, rootPackageJsonData); - - // delete migrated files - await this.$projectCleanupService.cleanPath(embeddedPackageJsonPath); - await this.$projectCleanupService.cleanPath(legacyNsConfigPath); - - return true; - } - private async _shouldMigrate({ projectDir, platforms, - allowInvalidVersions, + loose, }: IMigrationData): Promise { const isMigrate = _.get(this.$options, "argv._[0]") === "migrate"; const projectData = this.$projectDataService.getProjectData(projectDir); @@ -675,54 +557,63 @@ export class MigrateController const dependency = this.migrationDependencies[i]; const hasDependency = this.hasDependency(dependency, projectData); - if ( - hasDependency && - dependency.shouldMigrateAction && - (await dependency.shouldMigrateAction( + if (!hasDependency) { + if (dependency.shouldAddIfMissing) { + this.$logger.trace( + `${shouldMigrateCommonMessage}'${dependency.packageName}' is missing.` + ); + return true; + } + + continue; + } + + if (dependency.shouldMigrateAction) { + const shouldMigrate = await dependency.shouldMigrateAction.bind(this)( + dependency, projectData, - allowInvalidVersions - )) - ) { - this.$logger.trace( - `${shouldMigrateCommonMessage}'${dependency.packageName}' requires an update.` + loose ); - return true; + + if (shouldMigrate) { + this.$logger.trace( + `${shouldMigrateCommonMessage}'${dependency.packageName}' requires an update.` + ); + return true; + } } - if ( - hasDependency && - (dependency.replaceWith || dependency.shouldRemove) - ) { + if (dependency.replaceWith || dependency.shouldRemove) { this.$logger.trace( `${shouldMigrateCommonMessage}'${dependency.packageName}' is deprecated.` ); + + // in loose mode we ignore deprecated dependencies + if (loose) { + continue; + } + return true; } - if ( - hasDependency && - (await this.shouldMigrateDependencyVersion( - dependency, - projectData, - allowInvalidVersions - )) - ) { + const shouldUpdate = await this.shouldMigrateDependencyVersion( + dependency, + projectData, + loose + ); + + if (shouldUpdate) { this.$logger.trace( `${shouldMigrateCommonMessage}'${dependency.packageName}' should be updated.` ); - return true; - } - if (!hasDependency && dependency.shouldAddIfMissing) { - this.$logger.trace( - `${shouldMigrateCommonMessage}'${dependency.packageName}' is missing.` - ); return true; } } for (let platform of platforms) { - platform = platform && platform.toLowerCase(); + platform = platform?.toLowerCase(); + if ( !this.$platformValidationService.isValidPlatform(platform, projectData) ) { @@ -733,47 +624,111 @@ export class MigrateController platform, projectData, }); - if ( - hasRuntimeDependency && - (await this.shouldUpdateRuntimeVersion( - this.verifiedPlatformVersions[platform.toLowerCase()], - platform, - projectData, - allowInvalidVersions - )) - ) { - this.$logger.trace( - `${shouldMigrateCommonMessage}Platform '${platform}' should be updated.` - ); - return true; + + if (!hasRuntimeDependency) { + continue; } - } - } - private async getCachedShouldMigrate( - projectDir: string, - platform: string - ): Promise { - let cachedShouldMigrateValue = null; + const verifiedPlatformVersion = this.verifiedPlatformVersions[ + platform.toLowerCase() + ]; + const shouldUpdateRuntime = await this.shouldUpdateRuntimeVersion( + verifiedPlatformVersion, + platform, + projectData, + loose + ); - const cachedHash = await this.$jsonFileSettingsService.getSettingValue( - getHash(`${projectDir}${platform.toLowerCase()}`) - ); - const packageJsonHash = await this.getPackageJsonHash(projectDir); - if (cachedHash === packageJsonHash) { - cachedShouldMigrateValue = false; + 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 cachedShouldMigrateValue; + return false; } - private async setCachedShouldMigrate( - projectDir: string, - platform: string - ): Promise { + private async shouldMigrateDependencyVersion( + dependency: IMigrationDependency, + projectData: IProjectData, + loose: boolean + ): Promise { + const installedVersion = await this.$packageInstallationManager.getInstalledDependencyVersion( + dependency.packageName, + projectData.projectDir + ); + + const desiredVersion = dependency.desiredVersion ?? dependency.minVersion; + const minVersion = dependency.minVersion ?? dependency.desiredVersion; + + if ( + dependency.shouldUseExactVersion && + installedVersion !== desiredVersion + ) { + return true; + } + + return this.isOutdatedVersion( + installedVersion, + { minVersion, desiredVersion }, + loose + ); + } + + 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 + ): Promise { + let cachedShouldMigrateValue = null; + + const cachedHash = await this.$jsonFileSettingsService.getSettingValue( + getHash(`${projectDir}${platform.toLowerCase()}`) + loose ? "-loose" : "" + ); + const packageJsonHash = await this.getPackageJsonHash(projectDir); + if (cachedHash === packageJsonHash) { + cachedShouldMigrateValue = false; + } + + return cachedShouldMigrateValue; + } + + private async setCachedShouldMigrate( + projectDir: string, + platform: string, + loose: boolean = false + ): Promise { + this.$logger.trace( + `Caching shouldMigrate result for platform ${platform} (loose = ${loose}).` + ); const packageJsonHash = await this.getPackageJsonHash(projectDir); await this.$jsonFileSettingsService.saveSetting( - getHash(`${projectDir}${platform.toLowerCase()}`), + getHash(`${projectDir}${platform.toLowerCase()}`) + loose ? "-loose" : "", packageJsonHash ); } @@ -807,6 +762,56 @@ export class MigrateController // } // } + private async ensureGitCleanOrForce(projectDir: string): Promise { + const git: SimpleGit = simpleGit(projectDir); + const isGit = await git.checkIsRepo(); + const isForce = this.$options.force; + if (!isGit) { + // not a git repo and no --force + if (!isForce) { + this.$logger.printMarkdown( + `Running \`ns migrate\` in a non-git project is not recommended. If you want to skip this check run \`ns migrate --force\`.` + ); + this.$errors.fail("Not in Git repo."); + return false; + } + this.spinner.warn(`Not in Git repo, but using ${"--force".red}`); + return true; + } + + const isClean = (await git.status()).isClean(); + if (!isClean) { + if (!isForce) { + this.$logger.printMarkdown( + `Current git branch has uncommitted changes. Please commit the changes and try again. Alternatively run \`ns migrate --force\` to skip this check.` + ); + this.$errors.fail("Git branch not clean."); + return false; + } + this.spinner.warn(`Git branch not clean, but using ${"--force".red}`); + return true; + } + + return true; + } + + private async backupProject(projectDir: string): Promise { + const projectData = this.$projectDataService.getProjectData(projectDir); + const backup = this.$projectBackupService.getBackup("migration"); + backup.addPaths([ + ...MigrateController.pathsToBackup, + path.join(projectData.getAppDirectoryRelativePath(), "package.json"), + ]); + + try { + return backup.create(); + } catch (error) { + this.spinner.fail(`Project backup failed.`); + backup.remove(); + this.$errors.fail(`Project backup failed. Error is: ${error.message}`); + } + } + private async cleanUpProject(projectData: IProjectData): Promise { await this.$projectCleanupService.clean([ constants.HOOKS_DIR_NAME, @@ -858,7 +863,7 @@ export class MigrateController [".map"], [""] ); - const cssFiles = glob.sync("*.@(le|sa|sc|c)ss", globOptions); + const cssFiles = glob.sync("*.@(less|sass|scss|css)", globOptions); const autoGeneratedCssFiles = this.getGeneratedFiles( cssFiles, [".css"], @@ -885,7 +890,7 @@ export class MigrateController generatedFileExts: string[], sourceFileExts: string[] ): string[] { - const autoGeneratedFiles = allFiles.filter((file) => { + return allFiles.filter((file) => { let isGenerated = false; const { dir, name, ext } = path.parse(file); if (generatedFileExts.indexOf(ext) > -1) { @@ -900,44 +905,120 @@ export class MigrateController return isGenerated; }); + } + + private isOutdatedVersion( + current: string, + target: IDependencyVersion, + loose: boolean + ): boolean { + // in loose mode, a falsy version is not considered outdated + if (!current && loose) { + return false; + } + + const installed = semver.coerce(current); + const min = semver.coerce(target.minVersion); + const desired = semver.coerce(target.desiredVersion); + + // in loose mode we check if we satisfy the min version + if (loose) { + if (!installed || !min) { + return false; + } + return semver.lt(installed, min); + } + + if (!installed || !desired) { + return true; + } + // otherwise we compare with the desired version + return semver.lt(installed, desired); + } + + private detectAppPath(projectDir: string, configData: INsConfig) { + if (configData.appPath) { + return configData.appPath; + } + + const possibleAppPaths = [ + path.resolve(projectDir, constants.SRC_DIR), + path.resolve(projectDir, constants.APP_FOLDER_NAME), + ]; + + const appPath = possibleAppPaths.find((possiblePath) => + this.$fs.exists(possiblePath) + ); + if (appPath) { + const relativeAppPath = path + .relative(projectDir, appPath) + .replace(path.sep, "/"); + this.$logger.trace(`Found app source at '${appPath}'.`); + return relativeAppPath.toString(); + } + } + + private detectAppResourcesPath(projectDir: string, configData: INsConfig) { + if (configData.appResourcesPath) { + return configData.appResourcesPath; + } - return autoGeneratedFiles; + const possibleAppResourcesPaths = [ + path.resolve( + projectDir, + configData.appPath, + constants.APP_RESOURCES_FOLDER_NAME + ), + path.resolve(projectDir, constants.APP_RESOURCES_FOLDER_NAME), + ]; + + const appResourcesPath = possibleAppResourcesPaths.find((possiblePath) => + this.$fs.exists(possiblePath) + ); + if (appResourcesPath) { + const relativeAppResourcesPath = path + .relative(projectDir, appResourcesPath) + .replace(path.sep, "/"); + this.$logger.trace(`Found App_Resources at '${appResourcesPath}'.`); + return relativeAppResourcesPath.toString(); + } } private async migrateDependencies( projectData: IProjectData, platforms: string[], - allowInvalidVersions: boolean + loose: boolean ): Promise { for (let i = 0; i < this.migrationDependencies.length; i++) { const dependency = this.migrationDependencies[i]; const hasDependency = this.hasDependency(dependency, projectData); - if ( - hasDependency && - dependency.migrateAction && - (await dependency.shouldMigrateAction( - projectData, - allowInvalidVersions - )) - ) { - const newDependencies = await dependency.migrateAction( + if (!hasDependency && !dependency.shouldAddIfMissing) { + continue; + } + + if (dependency.migrateAction) { + const shouldMigrate = await dependency.shouldMigrateAction.bind(this)( + dependency, projectData, - path.join(projectData.projectDir, MigrateController.backupFolderName) + loose ); - for (const newDependency of newDependencies) { - await this.migrateDependency( - newDependency, + + if (shouldMigrate) { + const newDependencies = await dependency.migrateAction( projectData, - allowInvalidVersions + path.join( + projectData.projectDir, + MigrateController.backupFolderName + ) ); + for (const newDependency of newDependencies) { + await this.migrateDependency(newDependency, projectData, loose); + } } } - await this.migrateDependency( - dependency, - projectData, - allowInvalidVersions - ); + + await this.migrateDependency(dependency, projectData, loose); } for (const platform of platforms) { @@ -946,238 +1027,237 @@ export class MigrateController platform, projectData, }); - if ( - hasRuntimeDependency && - (await this.shouldUpdateRuntimeVersion( - this.verifiedPlatformVersions[lowercasePlatform], - platform, - projectData, - allowInvalidVersions - )) - ) { - const verifiedPlatformVersion = this.verifiedPlatformVersions[ - lowercasePlatform - ]; - const platformData = this.$platformsDataService.getPlatformData( - lowercasePlatform, - projectData - ); - this.spinner.info( - `Updating ${platform} platform to version '${verifiedPlatformVersion}'.` - ); - await this.$addPlatformService.setPlatformVersion( - platformData, - projectData, - verifiedPlatformVersion - ); - this.spinner.succeed(); + + if (!hasRuntimeDependency) { + continue; } - } - // this.spinner.info("Installing packages."); - // await this.$packageManager.install( - // projectData.projectDir, - // projectData.projectDir, - // { - // disableNpmInstall: false, - // frameworkPath: null, - // ignoreScripts: false, - // path: projectData.projectDir, - // } - // ); - // this.spinner.text = "Installing packages... Complete"; - // this.spinner.succeed(); - // - // this.spinner.succeed("Migration complete."); + const shouldUpdate = await this.shouldUpdateRuntimeVersion( + this.verifiedPlatformVersions[lowercasePlatform], + platform, + projectData, + loose + ); + + if (!shouldUpdate) { + 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 + ); + + this.spinner.succeed(); + } } private async migrateDependency( dependency: IMigrationDependency, projectData: IProjectData, - allowInvalidVersions: boolean + loose: boolean ): Promise { const hasDependency = this.hasDependency(dependency, projectData); + + // show warning if needed if (hasDependency && dependency.warning) { this.$logger.warn(dependency.warning); } - if (hasDependency && (dependency.replaceWith || dependency.shouldRemove)) { - this.$pluginsService.removeFromPackageJson( - dependency.packageName, - projectData.projectDir - ); - if (dependency.replaceWith) { - const replacementDep = _.find( - this.migrationDependencies, - (migrationPackage) => - migrationPackage.packageName === dependency.replaceWith - ); - if (!replacementDep) { - this.$errors.fail("Failed to find replacement dependency."); - } - - this.$pluginsService.addToPackageJson( - replacementDep.packageName, - replacementDep.verifiedVersion, - replacementDep.isDev, - projectData.projectDir - ); - - this.spinner.clear(); - this.$logger.info( - ` - ${dependency.packageName.yellow} has been replaced with ${ - replacementDep.packageName.cyan - } ${`v${replacementDep.verifiedVersion}`.green}` - ); - this.spinner.render(); + if (!hasDependency) { + if (!dependency.shouldAddIfMissing) { + return; } + const version = dependency.desiredVersion ?? dependency.minVersion; - return; - } - - if ( - hasDependency && - (await this.shouldMigrateDependencyVersion( - dependency, - projectData, - allowInvalidVersions - )) - ) { this.$pluginsService.addToPackageJson( dependency.packageName, - dependency.verifiedVersion, + version, dependency.isDev, projectData.projectDir ); this.spinner.clear(); this.$logger.info( - ` - ${dependency.packageName.yellow} has been updated to ${ - `v${dependency.verifiedVersion}`.green - }` + ` - ${dependency.packageName.yellow} ${ + `${version}`.green + } has been added` ); this.spinner.render(); return; } - if (!hasDependency && dependency.shouldAddIfMissing) { - this.$pluginsService.addToPackageJson( + if (dependency.replaceWith || dependency.shouldRemove) { + // remove + this.$pluginsService.removeFromPackageJson( dependency.packageName, - dependency.verifiedVersion, - dependency.isDev, + projectData.projectDir + ); + + // no replacement required - we're done + if (!dependency.replaceWith) { + return; + } + + const replacementDep = _.find( + this.migrationDependencies, + (migrationPackage) => + migrationPackage.packageName === dependency.replaceWith + ); + + if (!replacementDep) { + this.$errors.fail("Failed to find replacement dependency."); + } + + const version = dependency.desiredVersion ?? dependency.minVersion; + + // add replacement dependency + this.$pluginsService.addToPackageJson( + replacementDep.packageName, + version, + replacementDep.isDev, projectData.projectDir ); this.spinner.clear(); this.$logger.info( - ` - ${dependency.packageName.yellow} ${ - `v${dependency.verifiedVersion}`.green - } has been added` + ` - ${dependency.packageName.yellow} has been replaced with ${ + replacementDep.packageName.cyan + } ${`${version}`.green}` ); this.spinner.render(); + + return; } - } - private async shouldMigrateDependencyVersion( - dependency: IMigrationDependency, - projectData: IProjectData, - allowInvalidVersions: boolean - ): Promise { - const installedVersion = await this.$packageInstallationManager.getInstalledDependencyVersion( - dependency.packageName, - projectData.projectDir + const shouldMigrateVersion = await this.shouldMigrateDependencyVersion( + dependency, + projectData, + loose ); - const requiredVersion = dependency.verifiedVersion; - if ( - dependency.shouldUseExactVersion && - installedVersion !== requiredVersion - ) { - return true; + if (!shouldMigrateVersion) { + return; } - return this.isOutdatedVersion( - installedVersion, - requiredVersion, - allowInvalidVersions - ); - } + const version = dependency.desiredVersion ?? dependency.minVersion; - private async shouldUpdateRuntimeVersion( - targetVersion: string, - platform: string, - projectData: IProjectData, - allowInvalidVersions: boolean - ): Promise { - const installedVersion = await this.getMaxRuntimeVersion({ - platform, - projectData, - }); + this.$pluginsService.addToPackageJson( + dependency.packageName, + version, + dependency.isDev, + projectData.projectDir + ); - return this.isOutdatedVersion( - installedVersion, - targetVersion, - allowInvalidVersions + this.spinner.clear(); + this.$logger.info( + ` - ${dependency.packageName.yellow} has been updated to ${ + `${version}`.green + }` ); + this.spinner.render(); } - private isOutdatedVersion( - version: string, - targetVersion: string, - allowInvalidVersions: boolean - ): boolean { - return !!version - ? semver.lt(semver.coerce(version), targetVersion) - : !allowInvalidVersions; - } + private async migrateConfigs(projectDir: string): Promise { + const projectData = this.$projectDataService.getProjectData(projectDir); - private detectAppPath(projectDir: string, configData: INsConfig) { - if (configData.appPath) { - return configData.appPath; + // package.json + const rootPackageJsonPath: any = path.resolve( + projectDir, + constants.PACKAGE_JSON_FILE_NAME + ); + // nested package.json + const embeddedPackageJsonPath = path.resolve( + projectData.projectDir, + projectData.getAppDirectoryRelativePath(), + constants.PACKAGE_JSON_FILE_NAME + ); + // nsconfig.json + const legacyNsConfigPath = path.resolve( + projectData.projectDir, + constants.CONFIG_NS_FILE_NAME + ); + + let rootPackageJsonData: any = {}; + if (this.$fs.exists(rootPackageJsonPath)) { + rootPackageJsonData = this.$fs.readJson(rootPackageJsonPath); } - const possibleAppPaths = [ - path.resolve(projectDir, constants.SRC_DIR), - path.resolve(projectDir, constants.APP_FOLDER_NAME), - ]; + // write the default config unless it already exists + const newConfigPath = this.$projectConfigService.writeDefaultConfig( + projectData.projectDir + ); - const appPath = possibleAppPaths.find((possiblePath) => - this.$fs.exists(possiblePath) + // force legacy config mode + this.$projectConfigService.setForceUsingLegacyConfig(true); + + // all different sources are combined into configData (nested package.json, nsconfig and root package.json[nativescript]) + const configData = this.$projectConfigService.readConfig( + projectData.projectDir ); - if (appPath) { - const relativeAppPath = path - .relative(projectDir, appPath) - .replace(path.sep, "/"); - this.$logger.trace(`Found app source at '${appPath}'.`); - return relativeAppPath.toString(); - } - } - private detectAppResourcesPath(projectDir: string, configData: INsConfig) { - if (configData.appResourcesPath) { - return configData.appResourcesPath; + // we no longer want to force legacy config mode + this.$projectConfigService.setForceUsingLegacyConfig(false); + + // move main key into root package.json + if (configData.main) { + rootPackageJsonData.main = configData.main; + delete configData.main; } - const possibleAppResourcesPaths = [ - path.resolve( - projectDir, - configData.appPath, - constants.APP_RESOURCES_FOLDER_NAME - ), - path.resolve(projectDir, constants.APP_RESOURCES_FOLDER_NAME), - ]; + // detect appPath and App_Resources path + configData.appPath = this.detectAppPath(projectDir, configData); + configData.appResourcesPath = this.detectAppResourcesPath( + projectDir, + configData + ); - const appResourcesPath = possibleAppResourcesPaths.find((possiblePath) => - this.$fs.exists(possiblePath) + // delete nativescript key from root package.json + if (rootPackageJsonData.nativescript) { + delete rootPackageJsonData.nativescript; + } + + // force the config service to use nativescript.config.ts + this.$projectConfigService.setForceUsingNewConfig(true); + // migrate data into nativescript.config.ts + const hasUpdatedConfigSuccessfully = await this.$projectConfigService.setValue( + "", // root + configData as { [key: string]: SupportedConfigValues } ); - if (appResourcesPath) { - const relativeAppResourcesPath = path - .relative(projectDir, appResourcesPath) - .replace(path.sep, "/"); - this.$logger.trace(`Found App_Resources at '${appResourcesPath}'.`); - return relativeAppResourcesPath.toString(); + + if (!hasUpdatedConfigSuccessfully) { + if (typeof newConfigPath === "string") { + // only clean the config if it was created by the migration script + await this.$projectCleanupService.cleanPath(newConfigPath); + } + + this.$errors.fail( + `Failed to migrate project to use ${constants.CONFIG_FILE_NAME_TS}. One or more values could not be updated.` + ); } + + // save root package.json + this.$fs.writeJson(rootPackageJsonPath, rootPackageJsonData); + + // delete migrated files + await this.$projectCleanupService.cleanPath(embeddedPackageJsonPath); + await this.$projectCleanupService.cleanPath(legacyNsConfigPath); + + return true; } private async migrateUnitTestRunner( @@ -1215,18 +1295,44 @@ export class MigrateController } // Dependencies to migrate - const dependencies = [ + const dependencies: IMigrationDependency[] = [ { packageName: "karma-webpack", - verifiedVersion: "3.0.5", + minVersion: "3.0.5", + desiredVersion: "~5.0.0", isDev: true, shouldAddIfMissing: true, }, - { packageName: "karma-jasmine", verifiedVersion: "2.0.1", isDev: true }, - { packageName: "karma-mocha", verifiedVersion: "1.3.0", isDev: true }, - { packageName: "karma-chai", verifiedVersion: "0.1.0", isDev: true }, - { packageName: "karma-qunit", verifiedVersion: "3.1.2", isDev: true }, - { packageName: "karma", verifiedVersion: "4.1.0", isDev: true }, + { + packageName: "karma-jasmine", + minVersion: "2.0.1", + desiredVersion: "~4.0.1", + isDev: true, + }, + { + packageName: "karma-mocha", + minVersion: "1.3.0", + desiredVersion: "~2.0.1", + isDev: true, + }, + { + packageName: "karma-chai", + minVersion: "0.1.0", + desiredVersion: "~0.1.0", + isDev: true, + }, + { + packageName: "karma-qunit", + minVersion: "3.1.2", + desiredVersion: "~4.1.2", + isDev: true, + }, + { + packageName: "karma", + minVersion: "4.1.0", + desiredVersion: "~6.3.2", + isDev: true, + }, ]; return dependencies; @@ -1258,71 +1364,93 @@ export class MigrateController } private async migrateNativeScriptAngular(): Promise { - const dependencies = [ + const minVersion = "10.0.0"; + const desiredVersion = "~11.2.7"; + + /* + "@angular/router": "~11.2.7", + */ + + const dependencies: IMigrationDependency[] = [ { - packageName: "@angular/platform-browser-dynamic", - verifiedVersion: "10.0.0", + packageName: "@angular/animations", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { packageName: "@angular/common", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { packageName: "@angular/compiler", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { packageName: "@angular/core", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { packageName: "@angular/forms", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { packageName: "@angular/platform-browser", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { - packageName: "@angular/router", - verifiedVersion: "10.0.0", + packageName: "@angular/platform-browser-dynamic", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { - packageName: "rxjs", - verifiedVersion: "6.6.0", + packageName: "@angular/router", + minVersion, + desiredVersion, shouldAddIfMissing: true, }, { - packageName: "zone.js", - verifiedVersion: "0.11.1", + packageName: "rxjs", + minVersion: "6.6.0", + desiredVersion: "~6.6.7", shouldAddIfMissing: true, }, { - packageName: "@angular/animations", - verifiedVersion: "10.0.0", + packageName: "zone.js", + minVersion: "0.11.1", + desiredVersion: "~0.11.1", shouldAddIfMissing: true, }, + + // devDependencies { packageName: "@angular/compiler-cli", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion, isDev: true, }, { packageName: "@ngtools/webpack", - verifiedVersion: "10.0.0", + minVersion, + desiredVersion: "~11.2.6", isDev: true, }, + + // obsolete { packageName: "@angular-devkit/build-angular", - verifiedVersion: "0.1000.8", - isDev: true, + shouldRemove: true, }, ]; @@ -1333,26 +1461,30 @@ export class MigrateController const dependencies: IMigrationDependency[] = [ { packageName: "nativescript-vue-template-compiler", - verifiedVersion: "2.8.0", - shouldUseExactVersion: true, + minVersion: "2.7.0", + desiredVersion: "~2.8.4", isDev: true, shouldAddIfMissing: true, }, { - packageName: "vue-loader", - verifiedVersion: "15.9.3", - shouldUseExactVersion: true, + packageName: "nativescript-vue-devtools", + minVersion: "1.4.0", + desiredVersion: "~1.5.0", isDev: true, - shouldAddIfMissing: true, }, + { + packageName: "vue-loader", + shouldRemove: true, + }, + // remove any version of vue { packageName: "vue", shouldRemove: true, }, + // add latest { packageName: "vue", - verifiedVersion: "2.6.12", - shouldUseExactVersion: true, + desiredVersion: "2.6.12", isDev: true, }, ]; @@ -1364,15 +1496,15 @@ export class MigrateController const dependencies: IMigrationDependency[] = [ { packageName: "svelte-native-nativescript-ui", - verifiedVersion: "0.9.0", - shouldUseExactVersion: true, + minVersion: "0.9.0", + desiredVersion: "~0.9.0", isDev: true, shouldAddIfMissing: true, }, { packageName: "svelte-native-preprocessor", - verifiedVersion: "0.2.0", - shouldUseExactVersion: true, + minVersion: "0.2.0", + desiredVersion: "~0.2.0", isDev: true, shouldAddIfMissing: true, }, @@ -1382,10 +1514,7 @@ export class MigrateController }, { packageName: "svelte-loader-hot", - verifiedVersion: "0.3.1", - shouldUseExactVersion: true, - isDev: true, - shouldAddIfMissing: true, + shouldRemove: true, }, { packageName: "svelte", @@ -1393,7 +1522,8 @@ export class MigrateController }, { packageName: "svelte", - verifiedVersion: "3.24.1", + minVersion: "3.24.1", + desiredVersion: "3.24.1", shouldUseExactVersion: true, isDev: true, }, @@ -1403,7 +1533,7 @@ export class MigrateController } private async migrateWebpack(): Promise { - const scopedWebpackDeps = [ + const webpackDependencies = [ "@angular-devkit/core", "clean-webpack-plugin", "copy-webpack-plugin", @@ -1435,14 +1565,91 @@ export class MigrateController "webpack-sources", ]; - const dependencies = scopedWebpackDeps.map((dep) => { + return webpackDependencies.map((dep) => { return { packageName: dep, shouldRemove: true, }; }); + } - return dependencies; + private async migrateWebpack5(projectDir: string, projectData: IProjectData) { + this.spinner.start(`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", + } + ); + } catch (err) { + 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\`.` + ); + } + this.spinner.succeed(`Initialized new ${"webpack.config.js".yellow}`); + + const packageJSON = this.$fs.readJson(projectData.projectFilePath); + const currentMain = packageJSON.main; + const currentMainTS = currentMain.replace(/.js$/, ".ts"); + + const appPath = projectData.appDirectoryPath; + + const possibleMains = [ + `./${appPath}/${currentMain}`, + `./${appPath}/${currentMainTS}`, + `./app/${currentMain}`, + `./app/${currentMainTS}`, + `./src/${currentMain}`, + `./src/${currentMainTS}`, + ]; + const replacedMain = possibleMains.find((possibleMain) => { + return this.$fs.exists(path.resolve(projectDir, possibleMain)); + }); + + if (replacedMain) { + packageJSON.main = replacedMain; + this.$fs.writeJson(projectData.projectFilePath, packageJSON); + + this.spinner.info( + `Updated ${"package.json".yellow} main field to ${replacedMain.green}` + ); + } else { + this.$logger.warn(); + this.$logger.warn("Note:\n-----"); + this.$logger.printMarkdown( + `Could not determine the correct \`main\` field for \`package.json\`. Make sure to update it manually, pointing to the actual entry file relative to the \`package.json\`.\n` + ); + } + } + + private async runESLint(projectDir: string) { + // todo: run @nativescript/eslint-plugin on project folder to update imports + // const childProcess = injector.resolve('childProcess') as IChildProcess; + // const args = [ + // 'npx', + // '--package typescript', + // '--package @nativescript/eslint-plugin', + // '-c eslint-plugin', + // projectDir + // ] + // await childProcess.exec(args.join(' ')) } } diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index a9d5f1ed70..258bb9c0bb 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -247,6 +247,7 @@ export class PrepareController extends EventEmitter { if (this.persistedData && this.persistedData.length) { this.emitPrepareEvent({ files: [], + staleFiles: [], hasOnlyHotUpdateFiles: false, hasNativeChanges: result.hasNativeChanges, hmrData: null, @@ -352,6 +353,7 @@ export class PrepareController extends EventEmitter { await this.writeRuntimePackageJson(projectData, platformData); this.emitPrepareEvent({ files: [], + staleFiles: [], hasOnlyHotUpdateFiles: false, hmrData: null, hasNativeChanges: true, diff --git a/lib/controllers/run-controller.ts b/lib/controllers/run-controller.ts index 7fa173cc04..4f0ec24042 100644 --- a/lib/controllers/run-controller.ts +++ b/lib/controllers/run-controller.ts @@ -663,19 +663,19 @@ export class RunController extends EventEmitter implements IRunController { const platformLiveSyncService = this.$liveSyncServiceResolver.resolveLiveSyncService( device.deviceInfo.platform ); - const allAppFiles = - data.hmrData && - data.hmrData.fallbackFiles && - data.hmrData.fallbackFiles.length - ? data.hmrData.fallbackFiles - : data.files; + const allAppFiles = data.hmrData?.fallbackFiles?.length + ? data.hmrData.fallbackFiles + : data.files; const filesToSync = data.hasOnlyHotUpdateFiles ? data.files : allAppFiles; const watchInfo = { liveSyncDeviceData: deviceDescriptor, projectData, - filesToRemove: [], + // todo: remove stale files once everything is stable + // currently, watcher fires multiple times & may clean up unsynced files + // filesToRemove: data.staleFiles ?? [], + filesToRemove: [] as string[], filesToSync, hmrData: data.hmrData, useHotModuleReload: liveSyncInfo.useHotModuleReload, @@ -773,6 +773,7 @@ export class RunController extends EventEmitter implements IRunController { device.deviceInfo.identifier, data.hmrData.hash ); + // the timeout is assumed OK as the app could be blocked on a breakpoint if (status === HmrConstants.HMR_ERROR_STATUS) { await fullSyncAction(); diff --git a/lib/controllers/update-controller-base.ts b/lib/controllers/update-controller-base.ts index 4d014be539..5919ebc3bd 100644 --- a/lib/controllers/update-controller-base.ts +++ b/lib/controllers/update-controller-base.ts @@ -64,13 +64,12 @@ export class UpdateControllerBase { dependency: IDependency, projectData: IProjectData ): boolean { - const devDependencies = projectData.devDependencies; - const dependencies = projectData.dependencies; + const devDependencies = Object.keys(projectData.devDependencies); + const dependencies = Object.keys(projectData.dependencies); - return ( - (dependencies && dependencies[dependency.packageName]) || - (devDependencies && devDependencies[dependency.packageName]) - ); + return [...devDependencies, ...dependencies].some((packageName) => { + return packageName === dependency.packageName; + }); } protected hasRuntimeDependency({ diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index fbfe4877bd..043c288813 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -1243,3 +1243,5 @@ interface IWatchIgnoreListService { interface INpmConfigService { getConfig(): IDictionary; } + +interface ISharedEventBus extends NodeJS.EventEmitter {} diff --git a/lib/definitions/migrate.d.ts b/lib/definitions/migrate.d.ts index dcb34d8341..c4a9f5e83e 100644 --- a/lib/definitions/migrate.d.ts +++ b/lib/definitions/migrate.d.ts @@ -10,15 +10,9 @@ interface IMigrateController { interface IMigrationData extends IProjectDir { platforms: string[]; - allowInvalidVersions?: boolean; + loose?: boolean; } -// declare const enum ShouldMigrate { -// NO, -// YES , -// ADVISED -// } - interface IMigrationShouldMigrate { shouldMigrate: ShouldMigrate; reasons: string[]; @@ -29,16 +23,21 @@ interface IDependency { isDev?: boolean; } -interface IMigrationDependency extends IDependency { +interface IDependencyVersion { + minVersion?: string; + desiredVersion?: string; +} + +interface IMigrationDependency extends IDependency, IDependencyVersion { shouldRemove?: boolean; replaceWith?: string; warning?: string; - verifiedVersion?: string; shouldUseExactVersion?: boolean; shouldAddIfMissing?: boolean; shouldMigrateAction?: ( + dependency: IMigrationDependency, projectData: IProjectData, - allowInvalidVersions: boolean + loose: boolean ) => Promise; migrateAction?: ( projectData: IProjectData, diff --git a/lib/services/cocoapods-service.ts b/lib/services/cocoapods-service.ts index 4a6439d576..7c3e922026 100644 --- a/lib/services/cocoapods-service.ts +++ b/lib/services/cocoapods-service.ts @@ -57,11 +57,19 @@ export class CocoaPodsService implements ICocoaPodsService { xcodeProjPath: string ): Promise { this.$logger.info("Installing pods..."); - const podTool = this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod"; + let podTool = this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod"; + const args = ["install"]; + + if (process.platform === "darwin" && process.arch === "arm64") { + this.$logger.trace("Running on arm64 - running pod through rosetta2."); + args.unshift(podTool); + args.unshift("-x86_64"); + podTool = "arch"; + } // cocoapods print a lot of non-error information on stderr. Pipe the `stderr` to `stdout`, so we won't polute CLI's stderr output. const podInstallResult = await this.$childProcess.spawnFromEvent( podTool, - ["install"], + args, "close", { cwd: projectRoot, stdio: ["pipe", process.stdout, process.stdout] }, { throwError: false } diff --git a/lib/services/hmr-status-service.ts b/lib/services/hmr-status-service.ts index 09d267e625..288dcabd86 100644 --- a/lib/services/hmr-status-service.ts +++ b/lib/services/hmr-status-service.ts @@ -65,6 +65,23 @@ export class HmrStatusService implements IHmrStatusService { name: "failedLiveSync", platform: this.$devicePlatformsConstants.iOS.toLowerCase(), }); + + // webpack5 + const statusStringMap: any = { + success: HmrConstants.HMR_SUCCESS_STATUS, + failure: HmrConstants.HMR_ERROR_STATUS, + }; + this.$logParserService.addParseRule({ + regex: /\[HMR]\[(.+)]\s*(\w+)\s*\|/, + handler: (matches: RegExpMatchArray, deviceId: string) => { + const [hash, status] = matches.slice(1); + const mappedStatus = statusStringMap[status.trim()]; + if (mappedStatus) { + this.setData(deviceId, hash, statusStringMap[status]); + } + }, + name: "hmr-status", + }); } private handleAppCrash(matches: RegExpMatchArray, deviceId: string): void { diff --git a/lib/services/karma-execution.ts b/lib/services/karma-execution.ts index aafee13917..c2314f6ff3 100644 --- a/lib/services/karma-execution.ts +++ b/lib/services/karma-execution.ts @@ -3,14 +3,14 @@ import * as path from "path"; process.on("message", (data: any) => { if (data.karmaConfig) { 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. - process.exit(exitCode); - }); + data.karmaConfig.projectDir, + "node_modules/karma" + ); + const KarmaServer = require(path.join(pathToKarma, "lib/server")); + const karma = new KarmaServer(data.karmaConfig, (exitCode: number) => { + // Exit with the correct exit code and signal the manager process. + process.exit(exitCode); + }); karma.start(); } diff --git a/lib/services/project-cleanup-service.ts b/lib/services/project-cleanup-service.ts index e5ea46a652..ba62535471 100644 --- a/lib/services/project-cleanup-service.ts +++ b/lib/services/project-cleanup-service.ts @@ -27,6 +27,8 @@ export class ProjectCleanupService implements IProjectCleanupService { } public async cleanPath(pathToClean: string): Promise { + this.spinner.clear(); + if (!pathToClean || pathToClean.trim().length === 0) { this.$logger.trace("cleanPath called with no pathToClean."); return; @@ -38,7 +40,6 @@ export class ProjectCleanupService implements IProjectCleanupService { filePath )}`.yellow; - this.spinner.start(`Cleaning ${displayPath}`); this.$logger.trace(`Trying to clean '${filePath}'`); if (this.$fs.exists(filePath)) { @@ -46,25 +47,18 @@ export class ProjectCleanupService implements IProjectCleanupService { if (stat.isDirectory()) { this.$logger.trace(`Path '${filePath}' is a directory, deleting.`); - this.$fs.deleteDirectorySafe(filePath); - - this.spinner.text = `Cleaned directory ${displayPath}`; - this.spinner.succeed(); + this.spinner.succeed(`Cleaned directory ${displayPath}`); } else { this.$logger.trace(`Path '${filePath}' is a file, deleting.`); - this.$fs.deleteFile(filePath); - - this.spinner.text = `Cleaned file ${displayPath}`; - this.spinner.succeed(); + this.spinner.succeed(`Cleaned file ${displayPath}`); } return; } - this.$logger.trace(`Path '${filePath}' not found, skipping.`); - this.spinner.text = `Skipping ${displayPath} because it doesn't exist.`; - this.spinner.info(); + // this.spinner.text = `Skipping ${displayPath} because it doesn't exist.`; + // this.spinner.info(); } } diff --git a/lib/services/project-config-service.ts b/lib/services/project-config-service.ts index 47735f06e2..586f4df867 100644 --- a/lib/services/project-config-service.ts +++ b/lib/services/project-config-service.ts @@ -167,8 +167,8 @@ export default { } @exported("projectConfigService") - public getValue(key: string): any { - return _.get(this.readConfig(), key); + public getValue(key: string, defaultValue?: any): any { + return _.get(this.readConfig(), key, defaultValue); } @exported("projectConfigService") diff --git a/lib/services/webpack/webpack-compiler-service.ts b/lib/services/webpack/webpack-compiler-service.ts index 3889ca6b75..737a4d8957 100644 --- a/lib/services/webpack/webpack-compiler-service.ts +++ b/lib/services/webpack/webpack-compiler-service.ts @@ -28,6 +28,19 @@ import { import { ICleanupService } from "../../definitions/cleanup-service"; import { injector } from "../../common/yok"; +// todo: move out of here +interface IWebpackMessage { + type: "compilation" | "hmr-status"; + version?: number; + hash?: string; + data?: T; +} + +interface IWebpackCompilation { + emittedAssets: string[]; + staleAssets: string[]; +} + export class WebpackCompilerService extends EventEmitter implements IWebpackCompilerService { @@ -44,7 +57,7 @@ export class WebpackCompilerService private $mobileHelper: Mobile.IMobileHelper, private $cleanupService: ICleanupService, private $packageManager: IPackageManager, - private $packageInstallationManager: IPackageInstallationManager + private $packageInstallationManager: IPackageInstallationManager // private $sharedEventBus: ISharedEventBus ) { super(); } @@ -72,6 +85,35 @@ export class WebpackCompilerService childProcess.on("message", (message: string | IWebpackEmitMessage) => { this.$logger.trace("Message from webpack", message); + // if we are on webpack5 - we handle HMR in a slightly different way + if ( + typeof message === "object" && + "version" in message && + "type" in message + ) { + // first compilation can be ignored because it will be synced regardless + // handling it here would trigger 2 syncs + if (isFirstWebpackWatchCompilation) { + isFirstWebpackWatchCompilation = false; + resolve(childProcess); + return; + } + + // if ((message as IWebpackMessage).type === "hmr-status") { + // // we pass message through our event-bus to be handled wherever needed + // // in this case webpack-hmr-status-service listens for this event + // this.$sharedEventBus.emit("webpack:hmr-status", message); + // return; + // } + + return this.handleHMRMessage( + message as IWebpackMessage, + platformData, + projectData, + prepareData + ); + } + if (message === "Webpack compilation complete.") { this.$logger.info("Webpack build done!"); resolve(childProcess); @@ -247,7 +289,7 @@ export class WebpackCompilerService ): Promise { if (!this.$fs.exists(projectData.webpackConfigPath)) { this.$errors.fail( - `The webpack configuration file ${projectData.webpackConfigPath} does not exist. Ensure you have such file or set correct path in ${CONFIG_FILE_NAME_DISPLAY}` + `The webpack configuration file ${projectData.webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}.` ); } @@ -275,16 +317,11 @@ export class WebpackCompilerService const args = [ ...additionalNodeArgs, - path.join( - projectData.projectDir, - "node_modules", - "webpack", - "bin", - "webpack.js" - ), + this.getWebpackExecutablePath(projectData), + this.isWebpack5(projectData) ? `build` : null, `--config=${projectData.webpackConfigPath}`, ...envParams, - ]; + ].filter(Boolean); if (prepareData.watch) { args.push("--watch"); @@ -473,5 +510,109 @@ export class WebpackCompilerService delete this.webpackProcesses[platform]; } } + + private handleHMRMessage( + message: IWebpackMessage, + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData + ) { + // handle new webpack hmr packets + this.$logger.trace("Received message from webpack process:", message); + + if (message.type !== "compilation") { + return; + } + + this.$logger.trace("Webpack build done!"); + + const files = message.data.emittedAssets.map((asset: string) => + path.join(platformData.appDestinationDirectoryPath, "app", asset) + ); + const staleFiles = message.data.staleAssets.map((asset: string) => + path.join(platformData.appDestinationDirectoryPath, "app", asset) + ); + + // console.log({ staleFiles }); + + // extract last hash from emitted filenames + const lastHash = (() => { + const fileWithLastHash = files.find((fileName: string) => + fileName.endsWith("hot-update.js") + ); + + if (!fileWithLastHash) { + return null; + } + const matches = fileWithLastHash.match(/\.(.+).hot-update\.js/); + + if (matches) { + return matches[1]; + } + })(); + + if (!files.length) { + // ignore compilations if no new files are emitted + return; + } + + this.emit(WEBPACK_COMPILATION_COMPLETE, { + files, + staleFiles, + hasOnlyHotUpdateFiles: prepareData.hmr, + hmrData: { + hash: lastHash || message.hash, + fallbackFiles: [], + }, + platform: platformData.platformNameLowerCase, + }); + } + + private getWebpackExecutablePath(projectData: IProjectData): string { + if (this.isWebpack5(projectData)) { + const packagePath = require.resolve( + "@nativescript/webpack/package.json", + { + paths: [projectData.projectDir], + } + ); + + return path.resolve( + packagePath.replace("package.json", ""), + "dist", + "bin", + "index.js" + ); + } + + return path.join( + projectData.projectDir, + "node_modules", + "webpack", + "bin", + "webpack.js" + ); + } + + private isWebpack5(projectData: IProjectData): boolean { + try { + const packagePath = require.resolve( + "@nativescript/webpack/package.json", + { + paths: [projectData.projectDir], + } + ); + const ver = semver.coerce(require(packagePath).version); + + if (semver.satisfies(ver, ">= 5.0.0")) { + return true; + } + } catch (ignore) { + // + } + + return false; + } } + injector.register("webpackCompilerService", WebpackCompilerService); diff --git a/lib/services/webpack/webpack.d.ts b/lib/services/webpack/webpack.d.ts index d802ae29ec..a9f6fa4eb4 100644 --- a/lib/services/webpack/webpack.d.ts +++ b/lib/services/webpack/webpack.d.ts @@ -62,6 +62,7 @@ declare global { interface IFilesChangeEventData { platform: string; files: string[]; + staleFiles: string[]; hmrData: IPlatformHmrData; hasOnlyHotUpdateFiles: boolean; hasNativeChanges: boolean; diff --git a/lib/shared-event-bus.ts b/lib/shared-event-bus.ts new file mode 100644 index 0000000000..1ca82d0200 --- /dev/null +++ b/lib/shared-event-bus.ts @@ -0,0 +1,14 @@ +import { EventEmitter } from "events"; +import { injector } from "./common/yok"; +import { ISharedEventBus } from "./declarations"; + +/** + * Event Bus used by different services to emit state changes and + * allow listening for these state changes without needing a reference + * to the emitting service. + */ +class SharedEventBus extends EventEmitter implements ISharedEventBus { + // intentionally blank +} + +injector.register("sharedEventBus", SharedEventBus); diff --git a/package-lock.json b/package-lock.json index fe71805a2e..d7f626b3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nativescript", - "version": "7.2.1", + "version": "8.0.0-alpha.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5151,9 +5151,9 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, "ios-device-lib": { - "version": "0.9.0-alpha.0", - "resolved": "https://registry.npmjs.org/ios-device-lib/-/ios-device-lib-0.9.0-alpha.0.tgz", - "integrity": "sha512-y9PhpY2eJeNc/5uKdjFrvSJJPiQ9/RlvcsB/D1u0q2Kj9vueMnP2bhMvIXf+F+pvW1sv36ja0DMl8cV+kKDnkg==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/ios-device-lib/-/ios-device-lib-0.9.0.tgz", + "integrity": "sha512-cCY1zU7M7Vkq77FsFzXBDuWhlOu4RYWMko8ZJUcsp+S/lr3PeEbfKdKcl8a1j1RYokEQBhCrbBikd4pyn4chIw==", "requires": { "bufferpack": "0.0.6", "node-uuid": "1.4.7" diff --git a/package.json b/package.json index 41d326031f..af5608d74a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", "preferGlobal": true, - "version": "7.2.1", + "version": "8.0.0-alpha.3", "author": "NativeScript ", "description": "Command-line interface for building NativeScript projects", "bin": { @@ -70,7 +70,7 @@ "esprima": "4.0.1", "font-finder": "1.1.0", "glob": "7.1.6", - "ios-device-lib": "0.9.0-alpha.0", + "ios-device-lib": "0.9.0", "ios-mobileprovision-finder": "1.0.11", "ios-sim-portable": "4.2.1", "istextorbinary": "5.9.0", diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts index 72467d11d1..c84335c394 100644 --- a/test/controllers/prepare-controller.ts +++ b/test/controllers/prepare-controller.ts @@ -134,6 +134,7 @@ describe("prepareController", () => { assert.deepStrictEqual(emittedEventNames[0], PREPARE_READY_EVENT_NAME); assert.deepStrictEqual(emittedEventData[0], { files: [], + staleFiles: [], hasNativeChanges: true, hasOnlyHotUpdateFiles: false, hmrData: null, diff --git a/test/services/webpack/webpack-compiler-service.ts b/test/services/webpack/webpack-compiler-service.ts index 3cedc774eb..cb48eacab5 100644 --- a/test/services/webpack/webpack-compiler-service.ts +++ b/test/services/webpack/webpack-compiler-service.ts @@ -211,7 +211,7 @@ describe("WebpackCompilerService", () => { { webpackConfigPath }, {} ), - `The webpack configuration file ${webpackConfigPath} does not exist. Ensure you have such file or set correct path in ${CONFIG_FILE_NAME_DISPLAY}` + `The webpack configuration file ${webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}` ); }); }); @@ -227,7 +227,7 @@ describe("WebpackCompilerService", () => { { webpackConfigPath }, {} ), - `The webpack configuration file ${webpackConfigPath} does not exist. Ensure you have such file or set correct path in ${CONFIG_FILE_NAME_DISPLAY}` + `The webpack configuration file ${webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}` ); }); }); diff --git a/tslint.json b/tslint.json index d394803d07..8211d1c9c6 100644 --- a/tslint.json +++ b/tslint.json @@ -18,7 +18,7 @@ "no-construct": true, "no-debugger": true, "no-duplicate-variable": true, - "no-shadowed-variable": true, + "no-shadowed-variable": false, "no-empty": true, "no-eval": true, "no-switch-case-fall-through": true,