Skip to content

Commit ddd8328

Browse files
Fix isValidNativeScript project (#2630)
* Get latest application package for current specified configuration When we try to get the latest application package for device/emulator, we check the build output directory and get latest available .apk/.ipa. However this is not always correct as sometimes we do not build the app. Consider the following case: * tns build android --release * tns build android * tns build android --release At the last point, the build will not be executed, as there are no changes. However the last built .apk is from the debug build (we have release .apk, but it's older). So in case we try to get the last build output from last operation, CLI will return the debug.apk Fix this by checking the current build configuration and get the latest result by using it. For iOS respect the expected output - is it for device or for simulator as the output is different. * Fix isValidNativeScript project The isValidNativeScript project method is used in Fusion. However the implementation is not correct. Fix it to have correct behavior. In order to fix it, modify projectData.initialize method - there's obsolete code for migration from .tnsproject to package.json - we do not need it anymore, so remove it. Also fix a case where failing builds do not fail the build process (incorrect argument passed to childProcess.spawnFromEvent). Update documentation for PublicAPI. * Throw correct error when build for iOS in non-interactive terminal Also in case CLI's executed in non-interactive terminal and there's no DEVELOPMENT_TEAM set, tns build ios --for-device fails with message "Console is not interactive and no default action specified" which does not give any info to the users. So in this case add more descriptive error message. * Fix local builds when CLI is required as lib When CLI is required as library, the `$options` dependency is not populated. However the projectChangesService relies on it in order to determine if the project should be prepared/built. When CLI is required as library and you change only the build configuration (debug/release), the project is not rebuilt. However when you use the CLI and try the same, a new full build is triggered. Fix this by parsing required boolean flags to projectChangesService and exclude `$options` from its implementation. This way local builds will work in the same manner both from command line and when required as library. Steps to reproduce the problem: * use CLI as lib * create local build for android in debug configuration * create local build for android in release configuration - you'll notice gradle clean is not called at all and also in the `<project dir>/platforms/android/build/outputs/apks/` there are both debug and release apks. This should never happen when changing configurations. * Pass buildConfig when getting path to last build output Pass the whole buildConfig object when getting the path to the last build output. This required changes in debug services and TestExecutionService. Also change the args of cleanProject method - the `options` passed to `gradle` are now determined in the method itself instead of passing them from the caller. This way we'll not require "android-23" (the default one set in build.gradle) when we miss to pass the required options.
1 parent 62a3872 commit ddd8328

27 files changed

+248
-333
lines changed

PublicAPI.md

+26
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,32 @@ tns.projectService.createProject(projectSettings)
113113
</tr>
114114
</table>
115115

116+
* `isValidNativeScriptProject(projectDir: string): boolean` - Checks if the specified path is a valid NativeScript project. Returns `true` in case the directory is a valid project, `false` otherwise.
117+
118+
Sample usage:
119+
<table>
120+
<tr>
121+
<td>
122+
JavaScript
123+
</td>
124+
<td>
125+
TypeScript
126+
</td>
127+
</tr>
128+
<tr>
129+
<td>
130+
<pre lang="javascript">
131+
const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
132+
</pre>
133+
</td>
134+
<td>
135+
<pre lang="typescript">
136+
const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
137+
</pre>
138+
</td>
139+
</tr>
140+
</table>
141+
116142
## How to add a new method to Public API
117143
CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification.
118144
For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`.

lib/commands/appstore-upload.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class PublishIOS implements ICommand {
7373
// This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here.
7474
await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk });
7575
await this.$platformService.buildPlatform(platform, iOSBuildConfig, this.$projectData);
76-
ipaFilePath = this.$platformService.lastOutputPath(platform, { isForDevice: iOSBuildConfig.buildForDevice }, this.$projectData);
76+
ipaFilePath = this.$platformService.lastOutputPath(platform, iOSBuildConfig, this.$projectData);
7777
} else {
7878
this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission.");
7979
await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk });

lib/commands/build.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class BuildCommandBase {
2626
};
2727
await this.$platformService.buildPlatform(platform, buildConfig, this.$projectData);
2828
if (this.$options.copyTo) {
29-
this.$platformService.copyLastOutput(platform, this.$options.copyTo, { isForDevice: this.$options.forDevice }, this.$projectData);
29+
this.$platformService.copyLastOutput(platform, this.$options.copyTo, buildConfig, this.$projectData);
3030
}
3131
}
3232
}

lib/commands/debug.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@
1616
}
1717

1818
public async execute(args: string[]): Promise<void> {
19-
if (this.$options.start) {
20-
return this.debugService.debug(this.$projectData);
21-
}
22-
23-
const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release };
2419
const deployOptions: IDeployPlatformOptions = {
2520
clean: this.$options.clean,
2621
device: this.$options.device,
@@ -31,6 +26,15 @@
3126
provision: this.$options.provision,
3227
teamId: this.$options.teamId
3328
};
29+
30+
const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions);
31+
32+
if (this.$options.start) {
33+
return this.debugService.debug(this.$projectData, buildConfig);
34+
}
35+
36+
const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release };
37+
3438
await this.$platformService.deployPlatform(this.$devicesService.platform, appFilesUpdaterOptions, deployOptions, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk });
3539
this.$config.debugLivesync = true;
3640
let applicationReloadAction = async (deviceAppData: Mobile.IDeviceAppData): Promise<void> => {
@@ -45,7 +49,7 @@
4549

4650
await deviceAppData.device.applicationManager.stopApplication(applicationId);
4751

48-
await this.debugService.debug(this.$projectData);
52+
await this.debugService.debug(this.$projectData, buildConfig);
4953
};
5054
return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, this.$projectData, applicationReloadAction);
5155
}

lib/common

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const TESTING_FRAMEWORKS = ['jasmine', 'mocha', 'qunit'];
1515
export const TEST_RUNNER_NAME = "nativescript-unit-test-runner";
1616
export const LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"];
1717
export const XML_FILE_EXTENSION = ".xml";
18+
export const PLATFORMS_DIR_NAME = "platforms";
1819

1920
export class PackageVersion {
2021
static NEXT = "next";

lib/definitions/debug.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
interface IDebugService {
2-
debug(projectData: IProjectData): Promise<void>;
3-
debugStart(projectData: IProjectData): Promise<void>;
2+
debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void>;
3+
debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void>;
44
debugStop(): Promise<void>
55
platform: string;
66
}

lib/definitions/platform.d.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
interface IPlatformService extends NodeJS.EventEmitter {
22
cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, framework?: string): Promise<void>;
3-
3+
44
addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, frameworkPath?: string): Promise<void>;
55

66
/**
@@ -143,28 +143,37 @@ interface IPlatformService extends NodeJS.EventEmitter {
143143
/**
144144
* Returns information about the latest built application for device in the current project.
145145
* @param {IPlatformData} platformData Data describing the current platform.
146+
* @param {IBuildConfig} buildConfig Defines if the build is for release configuration.
146147
* @returns {IApplicationPackage} Information about latest built application.
147148
*/
148-
getLatestApplicationPackageForDevice(platformData: IPlatformData): IApplicationPackage;
149+
getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage;
149150

150151
/**
151152
* Returns information about the latest built application for simulator in the current project.
152153
* @param {IPlatformData} platformData Data describing the current platform.
154+
* @param {IBuildConfig} buildConfig Defines if the build is for release configuration.
153155
* @returns {IApplicationPackage} Information about latest built application.
154156
*/
155-
getLatestApplicationPackageForEmulator(platformData: IPlatformData): IApplicationPackage;
157+
getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage;
156158

157159
/**
158160
* Copies latest build output to a specified location.
159161
* @param {string} platform Mobile platform - Android, iOS.
160162
* @param {string} targetPath Destination where the build artifact should be copied.
161-
* @param {{isForDevice: boolean}} settings Defines if the searched artifact should be for simulator.
163+
* @param {IBuildConfig} buildConfig Defines if the searched artifact should be for simulator and is it built for release.
162164
* @param {IProjectData} projectData DTO with information about the project.
163165
* @returns {void}
164166
*/
165-
copyLastOutput(platform: string, targetPath: string, settings: {isForDevice: boolean}, projectData: IProjectData): void;
167+
copyLastOutput(platform: string, targetPath: string, buildConfig: IBuildConfig, projectData: IProjectData): void;
166168

167-
lastOutputPath(platform: string, settings: { isForDevice: boolean }, projectData: IProjectData): string;
169+
/**
170+
* Gets the latest build output.
171+
* @param {string} platform Mobile platform - Android, iOS.
172+
* @param {IBuildConfig} buildConfig Defines if the searched artifact should be for simulator and is it built for release.
173+
* @param {IProjectData} projectData DTO with information about the project.
174+
* @returns {string} The path to latest built artifact.
175+
*/
176+
lastOutputPath(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): string;
168177

169178
/**
170179
* Reads contents of a file on device.
@@ -209,8 +218,7 @@ interface IPlatformData {
209218
appDestinationDirectoryPath: string;
210219
deviceBuildOutputPath: string;
211220
emulatorBuildOutputPath?: string;
212-
validPackageNamesForDevice: string[];
213-
validPackageNamesForEmulator?: string[];
221+
getValidPackageNames(buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[];
214222
frameworkFilesExtensions: string[];
215223
frameworkDirectoriesExtensions?: string[];
216224
frameworkDirectoriesNames?: string[];

lib/definitions/project-changes.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ interface IProjectChangesInfo {
1919
changesRequireBuild: boolean;
2020
}
2121

22+
interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision {}
23+
2224
interface IProjectChangesService {
23-
checkForChanges(platform: string, projectData: IProjectData): IProjectChangesInfo;
25+
checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): IProjectChangesInfo;
2426
getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo;
2527
savePrepareInfo(platform: string, projectData: IProjectData): void;
2628
getPrepareInfoFilePath(platform: string, projectData: IProjectData): string;

lib/definitions/project.d.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,10 @@ interface IPlatformProjectService extends NodeJS.EventEmitter {
254254
/**
255255
* Removes build artifacts specific to the platform
256256
* @param {string} projectRoot The root directory of the native project.
257-
* @param {string[]} options Options that can be passed to clean command.
258257
* @param {IProjectData} projectData DTO with information about the project.
259258
* @returns {void}
260259
*/
261-
cleanProject(projectRoot: string, options: string[], projectData: IProjectData): Promise<void>
260+
cleanProject(projectRoot: string, projectData: IProjectData): Promise<void>
262261
}
263262

264263
interface IAndroidProjectPropertiesManager {

lib/project-data.ts

+23-63
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ interface IProjectType {
99
}
1010

1111
export class ProjectData implements IProjectData {
12-
private static OLD_PROJECT_FILE_NAME = ".tnsproject";
13-
1412
/**
1513
* NOTE: Order of the elements is important as the TypeScript dependencies are commonly included in Angular project as well.
1614
*/
@@ -42,45 +40,51 @@ export class ProjectData implements IProjectData {
4240

4341
constructor(private $fs: IFileSystem,
4442
private $errors: IErrors,
45-
private $logger: ILogger,
4643
private $projectHelper: IProjectHelper,
4744
private $staticConfig: IStaticConfig,
48-
private $options: IOptions) { }
45+
private $options: IOptions,
46+
private $logger: ILogger) { }
4947

50-
public initializeProjectData(projectDir? :string): void {
48+
public initializeProjectData(projectDir?: string): void {
5149
projectDir = projectDir || this.$projectHelper.projectDir;
5250
// If no project found, projectDir should be null
5351
if (projectDir) {
54-
this.initializeProjectDataCore(projectDir);
52+
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
5553
let data: any = null;
5654

57-
if (this.$fs.exists(this.projectFilePath)) {
55+
if (this.$fs.exists(projectFilePath)) {
5856
let fileContent: any = null;
5957
try {
60-
fileContent = this.$fs.readJson(this.projectFilePath);
58+
fileContent = this.$fs.readJson(projectFilePath);
6159
data = fileContent[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE];
6260
} catch (err) {
63-
this.$errors.fail({
64-
formatStr: "The project file %s is corrupted." + EOL +
65-
"Consider restoring an earlier version from your source control or backup." + EOL +
66-
"Additional technical info: %s",
67-
suppressCommandHelp: true
68-
},
69-
this.projectFilePath, err.toString());
61+
this.$errors.failWithoutHelp(`The project file ${this.projectFilePath} is corrupted. ${EOL}` +
62+
`Consider restoring an earlier version from your source control or backup.${EOL}` +
63+
`Additional technical info: ${err.toString()}`);
7064
}
7165

7266
if (data) {
67+
this.projectDir = projectDir;
68+
this.projectName = this.$projectHelper.sanitizeName(path.basename(projectDir));
69+
this.platformsDir = path.join(projectDir, constants.PLATFORMS_DIR_NAME);
70+
this.projectFilePath = projectFilePath;
71+
this.appDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME);
72+
this.appResourcesDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME);
7373
this.projectId = data.id;
7474
this.dependencies = fileContent.dependencies;
7575
this.devDependencies = fileContent.devDependencies;
7676
this.projectType = this.getProjectType();
77-
} else { // This is the case when we have package.json file but nativescipt key is not presented in it
78-
this.tryToUpgradeProject();
77+
78+
return;
7979
}
8080
}
81-
} else { // This is the case when no project file found
82-
this.tryToUpgradeProject();
8381
}
82+
83+
const currentDir = path.resolve(".");
84+
this.$logger.trace(`Unable to find project. projectDir: ${projectDir}, options.path: ${this.$options.path}, ${currentDir}`);
85+
86+
// This is the case when no project file found
87+
this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", projectDir || this.$options.path || currentDir);
8488
}
8589

8690
private getProjectType(): string {
@@ -97,49 +101,5 @@ export class ProjectData implements IProjectData {
97101

98102
return detectedProjectType;
99103
}
100-
101-
private throwNoProjectFoundError(): void {
102-
this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", this.$options.path || path.resolve("."));
103-
}
104-
105-
private tryToUpgradeProject(): void {
106-
let projectDir = this.projectDir || path.resolve(this.$options.path || ".");
107-
let oldProjectFilePath = path.join(projectDir, ProjectData.OLD_PROJECT_FILE_NAME);
108-
if (this.$fs.exists(oldProjectFilePath)) {
109-
this.upgrade(projectDir, oldProjectFilePath);
110-
} else {
111-
this.throwNoProjectFoundError();
112-
}
113-
}
114-
115-
private upgrade(projectDir: string, oldProjectFilePath: string): void {
116-
try {
117-
let oldProjectData = this.$fs.readJson(oldProjectFilePath);
118-
119-
let newProjectFilePath = this.projectFilePath || path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
120-
let newProjectData = this.$fs.exists(newProjectFilePath) ? this.$fs.readJson(newProjectFilePath) : {};
121-
newProjectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE] = oldProjectData;
122-
this.$fs.writeJson(newProjectFilePath, newProjectData);
123-
this.projectId = newProjectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE].id;
124-
125-
this.$fs.deleteFile(oldProjectFilePath);
126-
} catch (err) {
127-
this.$logger.out("An error occurred while upgrading your project.");
128-
throw err;
129-
}
130-
131-
this.initializeProjectDataCore(projectDir);
132-
133-
this.$logger.out("Successfully upgraded your project file.");
134-
}
135-
136-
private initializeProjectDataCore(projectDir: string): void {
137-
this.projectDir = projectDir;
138-
this.projectName = this.$projectHelper.sanitizeName(path.basename(projectDir));
139-
this.platformsDir = path.join(projectDir, "platforms");
140-
this.projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
141-
this.appDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME);
142-
this.appResourcesDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME);
143-
}
144104
}
145105
$injector.register("projectData", ProjectData);

lib/providers/livesync-provider.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ export class LiveSyncProvider implements ILiveSyncProvider {
4545
await this.$platformService.buildPlatform(device.deviceInfo.platform, buildConfig, projectData);
4646
let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData);
4747
if (device.isEmulator) {
48-
return this.$platformService.getLatestApplicationPackageForEmulator(platformData).packageName;
48+
return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName;
4949
}
5050

51-
return this.$platformService.getLatestApplicationPackageForDevice(platformData).packageName;
51+
return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName;
5252
}
5353

5454
public async preparePlatformForSync(platform: string, provision: any, projectData: IProjectData): Promise<void> {

lib/services/android-debug-service.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ class AndroidDebugService implements IDebugService {
2828
this._device = newDevice;
2929
}
3030

31-
public async debug(projectData: IProjectData): Promise<void> {
31+
public async debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
3232
return this.$options.emulator
33-
? this.debugOnEmulator(projectData)
34-
: this.debugOnDevice(projectData);
33+
? this.debugOnEmulator(projectData, buildConfig)
34+
: this.debugOnDevice(projectData, buildConfig);
3535
}
3636

37-
private async debugOnEmulator(projectData: IProjectData): Promise<void> {
37+
private async debugOnEmulator(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
3838
// Assure we've detected the emulator as device
3939
// For example in case deployOnEmulator had stated new emulator instance
4040
// we need some time to detect it. Let's force detection.
4141
await this.$androidDeviceDiscovery.startLookingForDevices();
42-
await this.debugOnDevice(projectData);
42+
await this.debugOnDevice(projectData, buildConfig);
4343
}
4444

4545
private isPortAvailable(candidatePort: number): Promise<boolean> {
@@ -108,7 +108,7 @@ class AndroidDebugService implements IDebugService {
108108
return this.device.adb.executeCommand(["forward", `tcp:${local}`, `localabstract:${remote}`]);
109109
}
110110

111-
private async debugOnDevice(projectData: IProjectData): Promise<void> {
111+
private async debugOnDevice(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
112112
let packageFile = "";
113113

114114
if (!this.$options.start && !this.$options.emulator) {
@@ -117,7 +117,7 @@ class AndroidDebugService implements IDebugService {
117117
this.$options.forDevice = !!cachedDeviceOption;
118118

119119
let platformData = this.$platformsData.getPlatformData(this.platform, projectData);
120-
packageFile = this.$platformService.getLatestApplicationPackageForDevice(platformData).packageName;
120+
packageFile = this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName;
121121
this.$logger.out("Using ", packageFile);
122122
}
123123

@@ -170,7 +170,7 @@ class AndroidDebugService implements IDebugService {
170170
await this.debugStartCore(packageName);
171171
}
172172

173-
public async debugStart(projectData: IProjectData): Promise<void> {
173+
public async debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
174174
await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device });
175175
let action = (device: Mobile.IAndroidDevice): Promise<void> => {
176176
this.device = device;

0 commit comments

Comments
 (0)