Skip to content

Commit f16d29b

Browse files
committed
refactor: update command
1 parent 520ce8d commit f16d29b

File tree

8 files changed

+252
-147
lines changed

8 files changed

+252
-147
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ $injector.requirePublicClass("buildController", "./controllers/build-controller"
5050
$injector.requirePublicClass("runController", "./controllers/run-controller");
5151
$injector.requirePublicClass("debugController", "./controllers/debug-controller");
5252
$injector.requirePublicClass("previewAppController", "./controllers/preview-app-controller");
53+
$injector.requirePublicClass("updateController", "./controllers/update-controller");
5354
$injector.requirePublicClass("migrateController", "./controllers/migrate-controller");
5455

5556
$injector.require("prepareDataService", "./services/prepare-data-service");

lib/commands/update.ts

Lines changed: 15 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,31 @@
1-
import * as path from "path";
2-
import * as constants from "../constants";
3-
import { ValidatePlatformCommandBase } from "./command-base";
4-
5-
export class UpdateCommand extends ValidatePlatformCommandBase implements ICommand {
1+
export class UpdateCommand implements ICommand {
62
public allowedParameters: ICommandParameter[] = [];
3+
public static readonly SHOULD_MIGRATE_PROJECT_MESSAGE = 'This project is not compatible with the current NativeScript version and cannot be updated. Use "tns migrate" to make your project compatible.';
4+
public static readonly PROJECT_UP_TO_DATE_MESSAGE = 'This project is up to date.';
75

86
constructor(
9-
private $fs: IFileSystem,
10-
private $logger: ILogger,
11-
$options: IOptions,
12-
private $platformCommandHelper: IPlatformCommandHelper,
13-
$platformsDataService: IPlatformsDataService,
14-
$platformValidationService: IPlatformValidationService,
15-
private $pluginsService: IPluginsService,
16-
$projectData: IProjectData,
17-
private $projectDataService: IProjectDataService) {
18-
super($options, $platformsDataService, $platformValidationService, $projectData);
7+
private $updateController: IUpdateController,
8+
private $migrateController: IMigrateController,
9+
private $options: IOptions,
10+
private $errors: IErrors,
11+
private $projectData: IProjectData) {
1912
this.$projectData.initializeProjectData();
2013
}
2114

22-
static readonly folders: string[] = [
23-
constants.LIB_DIR_NAME,
24-
constants.HOOKS_DIR_NAME,
25-
constants.PLATFORMS_DIR_NAME,
26-
constants.NODE_MODULES_FOLDER_NAME
27-
];
28-
static readonly tempFolder: string = ".tmp_backup";
29-
static readonly updateFailMessage: string = "Could not update the project!";
30-
static readonly backupFailMessage: string = "Could not backup project folders!";
31-
3215
public async execute(args: string[]): Promise<void> {
33-
const tmpDir = path.join(this.$projectData.projectDir, UpdateCommand.tempFolder);
34-
35-
try {
36-
this.backup(tmpDir);
37-
} catch (error) {
38-
this.$logger.error(UpdateCommand.backupFailMessage);
39-
this.$fs.deleteDirectory(tmpDir);
40-
return;
41-
}
42-
43-
try {
44-
await this.executeCore(args);
45-
} catch (error) {
46-
this.restoreBackup(tmpDir);
47-
this.$logger.error(UpdateCommand.updateFailMessage);
48-
} finally {
49-
this.$fs.deleteDirectory(tmpDir);
50-
}
16+
await this.$updateController.update(this.$projectData.projectDir, args[0], this.$options.frameworkPath);
5117
}
5218

53-
public async canExecute(args: string[]): Promise<ICanExecuteCommandOutput> {
54-
const platforms = this.getPlatforms();
55-
56-
let canExecute = true;
57-
for (const platform of platforms.packagePlatforms) {
58-
const output = await super.canExecuteCommandBase(platform);
59-
canExecute = canExecute && output.canExecute;
60-
}
61-
62-
let result = null;
63-
64-
if (canExecute) {
65-
result = {
66-
canExecute: args.length < 2 && this.$projectData.projectDir !== "",
67-
suppressCommandHelp: false
68-
};
69-
} else {
70-
result = {
71-
canExecute: false,
72-
suppressCommandHelp: true
73-
};
74-
}
75-
76-
return result;
77-
}
78-
79-
private async executeCore(args: string[]): Promise<void> {
80-
const platforms = this.getPlatforms();
81-
82-
for (const platform of _.xor(platforms.installed, platforms.packagePlatforms)) {
83-
const platformData = this.$platformsDataService.getPlatformData(platform, this.$projectData);
84-
this.$projectDataService.removeNSProperty(this.$projectData.projectDir, platformData.frameworkPackageName);
85-
}
86-
87-
await this.$platformCommandHelper.removePlatforms(platforms.installed, this.$projectData);
88-
await this.$pluginsService.remove(constants.TNS_CORE_MODULES_NAME, this.$projectData);
89-
if (!!this.$projectData.dependencies[constants.TNS_CORE_MODULES_WIDGETS_NAME]) {
90-
await this.$pluginsService.remove(constants.TNS_CORE_MODULES_WIDGETS_NAME, this.$projectData);
19+
public async canExecute(args: string[]): Promise<boolean> {
20+
if (await this.$migrateController.shouldMigrate({projectDir: this.$projectData.projectDir})) {
21+
this.$errors.failWithoutHelp(UpdateCommand.SHOULD_MIGRATE_PROJECT_MESSAGE);
9122
}
9223

93-
for (const folder of UpdateCommand.folders) {
94-
this.$fs.deleteDirectory(path.join(this.$projectData.projectDir, folder));
24+
if (!await this.$updateController.shouldUpdate({projectDir:this.$projectData.projectDir, version: args[0]})) {
25+
this.$errors.failWithoutHelp(UpdateCommand.PROJECT_UP_TO_DATE_MESSAGE);
9526
}
9627

97-
if (args.length === 1) {
98-
for (const platform of platforms.packagePlatforms) {
99-
await this.$platformCommandHelper.addPlatforms([platform + "@" + args[0]], this.$projectData, this.$options.frameworkPath);
100-
}
101-
102-
await this.$pluginsService.add(`${constants.TNS_CORE_MODULES_NAME}@${args[0]}`, this.$projectData);
103-
} else {
104-
await this.$platformCommandHelper.addPlatforms(platforms.packagePlatforms, this.$projectData, this.$options.frameworkPath);
105-
await this.$pluginsService.add(constants.TNS_CORE_MODULES_NAME, this.$projectData);
106-
}
107-
108-
await this.$pluginsService.ensureAllDependenciesAreInstalled(this.$projectData);
109-
}
110-
111-
private getPlatforms(): { installed: string[], packagePlatforms: string[] } {
112-
const installedPlatforms = this.$platformCommandHelper.getInstalledPlatforms(this.$projectData);
113-
const availablePlatforms = this.$platformCommandHelper.getAvailablePlatforms(this.$projectData);
114-
const packagePlatforms: string[] = [];
115-
116-
for (const platform of availablePlatforms) {
117-
const platformData = this.$platformsDataService.getPlatformData(platform, this.$projectData);
118-
const platformVersion = this.$projectDataService.getNSValue(this.$projectData.projectDir, platformData.frameworkPackageName);
119-
if (platformVersion) {
120-
packagePlatforms.push(platform);
121-
}
122-
}
123-
124-
return {
125-
installed: installedPlatforms,
126-
packagePlatforms: installedPlatforms.concat(packagePlatforms)
127-
};
128-
}
129-
130-
private restoreBackup(tmpDir: string): void {
131-
this.$fs.copyFile(path.join(tmpDir, constants.PACKAGE_JSON_FILE_NAME), this.$projectData.projectDir);
132-
for (const folder of UpdateCommand.folders) {
133-
this.$fs.deleteDirectory(path.join(this.$projectData.projectDir, folder));
134-
135-
const folderToCopy = path.join(tmpDir, folder);
136-
137-
if (this.$fs.exists(folderToCopy)) {
138-
this.$fs.copyFile(folderToCopy, this.$projectData.projectDir);
139-
}
140-
}
141-
}
142-
143-
private backup(tmpDir: string): void {
144-
this.$fs.deleteDirectory(tmpDir);
145-
this.$fs.createDirectory(tmpDir);
146-
this.$fs.copyFile(path.join(this.$projectData.projectDir, constants.PACKAGE_JSON_FILE_NAME), tmpDir);
147-
for (const folder of UpdateCommand.folders) {
148-
const folderToCopy = path.join(this.$projectData.projectDir, folder);
149-
if (this.$fs.exists(folderToCopy)) {
150-
this.$fs.copyFile(folderToCopy, tmpDir);
151-
}
152-
}
28+
return args.length < 2 && this.$projectData.projectDir !== "";
15329
}
15430
}
15531

lib/controllers/update-controller.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import * as path from "path";
2+
import * as semver from "semver";
3+
import * as constants from "../constants";
4+
import { BaseUpdateController } from "./base-update-controller";
5+
6+
export class UpdateController extends BaseUpdateController implements IUpdateController {
7+
constructor(
8+
protected $fs: IFileSystem,
9+
protected $platformsDataService: IPlatformsDataService,
10+
protected $platformCommandHelper: IPlatformCommandHelper,
11+
protected $packageInstallationManager: IPackageInstallationManager,
12+
protected $packageManager: IPackageManager,
13+
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
14+
private $addPlatformService: IAddPlatformService,
15+
private $logger: ILogger,
16+
private $pluginsService: IPluginsService,
17+
private $pacoteService: IPacoteService,
18+
private $projectDataService: IProjectDataService) {
19+
super($fs, $platformCommandHelper, $platformsDataService, $packageInstallationManager, $packageManager);
20+
this.getTemplateManifest = _.memoize(this._getTemplateManifest, (...args) => {
21+
return args.join("@");
22+
});
23+
}
24+
private getTemplateManifest: Function;
25+
static readonly folders: string[] = [
26+
constants.LIB_DIR_NAME,
27+
constants.HOOKS_DIR_NAME,
28+
//constants.PLATFORMS_DIR_NAME,
29+
//constants.NODE_MODULES_FOLDER_NAME,
30+
constants.WEBPACK_CONFIG_NAME,
31+
constants.PACKAGE_JSON_FILE_NAME,
32+
constants.PACKAGE_LOCK_JSON_FILE_NAME
33+
];
34+
35+
static readonly tempFolder: string = ".update_backup";
36+
static readonly updateFailMessage: string = "Could not update the project!";
37+
static readonly backupFailMessage: string = "Could not backup project folders!";
38+
39+
public async update(projectDir: string, version?: string): Promise<void> {
40+
const projectData = this.$projectDataService.getProjectData(projectDir);
41+
const tmpDir = path.join(projectDir, UpdateController.tempFolder);
42+
43+
try {
44+
this.backup(UpdateController.folders, tmpDir, projectData);
45+
} catch (error) {
46+
this.$logger.error(UpdateController.backupFailMessage);
47+
this.$fs.deleteDirectory(tmpDir);
48+
return;
49+
}
50+
51+
try {
52+
await this.cleanUpProject(projectData);
53+
await this.updateProject(projectData, version);
54+
} catch (error) {
55+
this.restoreBackup(UpdateController.folders, tmpDir, projectData);
56+
this.$logger.error(UpdateController.updateFailMessage);
57+
}
58+
}
59+
60+
public async shouldUpdate({projectDir, version}: {projectDir: string, version?: string}): Promise<boolean> {
61+
const projectData = this.$projectDataService.getProjectData(projectDir);
62+
const templateName = this.getTemplateName(projectData);
63+
const templateManifest = await this.getTemplateManifest(templateName, version);
64+
const dependencies = templateManifest.dependencies;
65+
const devDependencies = templateManifest.devDependencies;
66+
67+
if (
68+
await this.hasDependenciesToUpdate({dependencies, areDev: false, projectData}) ||
69+
await this.hasDependenciesToUpdate({dependencies: devDependencies, areDev: true, projectData})
70+
) {
71+
return true;
72+
}
73+
74+
for (const platform in this.$devicePlatformsConstants) {
75+
const lowercasePlatform = platform.toLowerCase();
76+
const platformData = this.$platformsDataService.getPlatformData(lowercasePlatform, projectData);
77+
const currentPlatformData = this.$projectDataService.getNSValueFromContent(templateManifest, platformData.frameworkPackageName);
78+
const runtimeVersion = currentPlatformData && currentPlatformData.version;
79+
if (runtimeVersion && await this.shouldUpdateRuntimeVersion({targetVersion: runtimeVersion, platform, projectData, shouldAdd: false})) {
80+
return true;
81+
}
82+
}
83+
}
84+
85+
private async cleanUpProject(projectData: IProjectData) {
86+
this.$logger.info("Clean old project artefacts.");
87+
this.$fs.deleteDirectory(path.join(projectData.projectDir, constants.HOOKS_DIR_NAME));
88+
this.$fs.deleteDirectory(path.join(projectData.projectDir, constants.PLATFORMS_DIR_NAME));
89+
this.$fs.deleteDirectory(path.join(projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME));
90+
this.$fs.deleteFile(path.join(projectData.projectDir, constants.WEBPACK_CONFIG_NAME));
91+
this.$fs.deleteFile(path.join(projectData.projectDir, constants.PACKAGE_LOCK_JSON_FILE_NAME));
92+
this.$logger.info("Clean old project artefacts complete.");
93+
}
94+
95+
private async updateProject(projectData: IProjectData, version: string): Promise<void> {
96+
const templateName = this.getTemplateName(projectData);
97+
const templateManifest = await this.getTemplateManifest(templateName, version);
98+
99+
this.$logger.info("Start updating dependencies.");
100+
await this.updateDependencies({ dependencies: templateManifest.dependencies, areDev: false, projectData});
101+
this.$logger.info("Finished updating dependencies.");
102+
this.$logger.info("Start updating devDependencies.");
103+
await this.updateDependencies({ dependencies: templateManifest.devDependencies, areDev: true, projectData});
104+
this.$logger.info("Finished updating devDependencies.");
105+
106+
this.$logger.info("Start updating runtimes.");
107+
await this.updateRuntimes(templateManifest, projectData);
108+
this.$logger.info("Finished updating runtimes.");
109+
110+
this.$logger.info("Install packages.");
111+
await this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
112+
disableNpmInstall: false,
113+
frameworkPath: null,
114+
ignoreScripts: false,
115+
path: projectData.projectDir
116+
});
117+
}
118+
119+
private async updateDependencies( {dependencies, areDev, projectData} : {dependencies: IDictionary<string>, areDev: boolean, projectData: IProjectData}) {
120+
for (const dependency in dependencies) {
121+
const templateVersion = dependencies[dependency];
122+
if (this.shouldSkipDependency({ packageName: dependency, isDev: areDev }, projectData)) {
123+
continue;
124+
}
125+
126+
if (await this.shouldUpdateDependency(dependency, templateVersion, areDev, projectData)) {
127+
this.$logger.info(`Updating '${dependency}' to version '${templateVersion}'.`);
128+
this.$pluginsService.addToPackageJson(dependency, templateVersion, areDev, projectData.projectDir);
129+
}
130+
}
131+
}
132+
133+
private async shouldUpdateDependency(dependency: string, targetVersion: string, isDev: boolean, projectData: IProjectData) {
134+
const collection = isDev ? projectData.devDependencies : projectData.dependencies;
135+
const projectVersion = collection[dependency];
136+
const maxSatisfyingTargetVersion = await this.$packageInstallationManager.maxSatisfyingVersion(dependency, targetVersion);
137+
const maxSatisfyingProjectVersion = await this.getMaxDependencyVersion(dependency, projectVersion);
138+
139+
if (maxSatisfyingProjectVersion && semver.gt(maxSatisfyingTargetVersion, maxSatisfyingProjectVersion)) {
140+
return true;
141+
}
142+
}
143+
144+
private async hasDependenciesToUpdate({dependencies, areDev, projectData}: {dependencies: IDictionary<string>, areDev: boolean, projectData:IProjectData}) {
145+
for (const dependency in dependencies) {
146+
const templateVersion = dependencies[dependency];
147+
if (this.shouldSkipDependency({ packageName: dependency, isDev: areDev }, projectData)) {
148+
continue;
149+
}
150+
151+
if (await this.shouldUpdateDependency(dependency, templateVersion, areDev, projectData)) {
152+
return true;
153+
}
154+
}
155+
}
156+
157+
private async updateRuntimes(templateData: Object, projectData: IProjectData) {
158+
for (const platform in this.$devicePlatformsConstants) {
159+
const lowercasePlatform = platform.toLowerCase();
160+
const platformData = this.$platformsDataService.getPlatformData(lowercasePlatform, projectData);
161+
const currentPlatformData = this.$projectDataService.getNSValueFromContent(templateData, platformData.frameworkPackageName);
162+
const runtimeVersion = currentPlatformData && currentPlatformData.version;
163+
if (runtimeVersion && await this.shouldUpdateRuntimeVersion({targetVersion: runtimeVersion, platform, projectData, shouldAdd: false})) {
164+
this.$logger.info(`Updating ${platform} platform to version '${runtimeVersion}'.`);
165+
await this.$addPlatformService.setPlatformVersion(platformData, projectData, runtimeVersion);
166+
}
167+
}
168+
}
169+
170+
private async _getTemplateManifest(templateName: string, version: string) {
171+
let packageVersion = version ? version : await this.$packageInstallationManager.getLatestCompatibleVersionSafe(templateName);
172+
packageVersion = semver.valid(version) ? version : await this.$packageManager.getTagVersion(templateName, packageVersion);
173+
packageVersion = packageVersion ? packageVersion : await this.$packageInstallationManager.getLatestCompatibleVersionSafe(templateName);
174+
175+
return await this.$pacoteService.manifest(`${templateName}@${packageVersion}`, { fullMetadata: true });
176+
}
177+
178+
private getTemplateName(projectData: IProjectData) {
179+
let template;
180+
switch (projectData.projectType) {
181+
case constants.ProjectTypes.NgFlavorName: {
182+
template = constants.RESERVED_TEMPLATE_NAMES.angular;
183+
break;
184+
}
185+
case constants.ProjectTypes.VueFlavorName: {
186+
template = constants.RESERVED_TEMPLATE_NAMES.vue;
187+
break;
188+
}
189+
case constants.ProjectTypes.TsFlavorName: {
190+
template = constants.RESERVED_TEMPLATE_NAMES.typescript;
191+
break;
192+
}
193+
case constants.ProjectTypes.JsFlavorName: {
194+
template = constants.RESERVED_TEMPLATE_NAMES.javascript;
195+
break;
196+
}
197+
}
198+
199+
return template;
200+
}
201+
}
202+
203+
$injector.register("updateController", UpdateController);

lib/definitions/project.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ interface IProjectDataService {
191191
* @returns {string[]} Array of paths to `.js` or `.ts` files.
192192
*/
193193
getAppExecutableFiles(projectDir: string): string[];
194+
195+
/**
196+
* Returns a value from `nativescript` key in project's package.json.
197+
* @param {string} jsonData The project directory - the place where the root package.json is located.
198+
* @param {string} propertyName The name of the property to be checked in `nativescript` key.
199+
* @returns {any} The value of the property.
200+
*/
201+
getNSValueFromContent(jsonData: Object, propertyName: string): any;
194202
}
195203

196204
interface IAssetItem {

0 commit comments

Comments
 (0)