Skip to content

Fix isValidNativeScript project #2630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions PublicAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,32 @@ tns.projectService.createProject(projectSettings)
</tr>
</table>

* `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.

Sample usage:
<table>
<tr>
<td>
JavaScript
</td>
<td>
TypeScript
</td>
</tr>
<tr>
<td>
<pre lang="javascript">
const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
</pre>
</td>
<td>
<pre lang="typescript">
const isValidProject = tns.projectService.isValidNativeScriptProject("/tmp/myProject");
</pre>
</td>
</tr>
</table>

## How to add a new method to Public API
CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification.
For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`.
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/appstore-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class PublishIOS implements ICommand {
// This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here.
await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk });
await this.$platformService.buildPlatform(platform, iOSBuildConfig, this.$projectData);
ipaFilePath = this.$platformService.lastOutputPath(platform, { isForDevice: iOSBuildConfig.buildForDevice }, this.$projectData);
ipaFilePath = this.$platformService.lastOutputPath(platform, iOSBuildConfig, this.$projectData);
} else {
this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission.");
await this.$platformService.preparePlatform(platform, appFilesUpdaterOptions, this.$options.platformTemplate, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk });
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class BuildCommandBase {
};
await this.$platformService.buildPlatform(platform, buildConfig, this.$projectData);
if (this.$options.copyTo) {
this.$platformService.copyLastOutput(platform, this.$options.copyTo, { isForDevice: this.$options.forDevice }, this.$projectData);
this.$platformService.copyLastOutput(platform, this.$options.copyTo, buildConfig, this.$projectData);
}
}
}
Expand Down
16 changes: 10 additions & 6 deletions lib/commands/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
}

public async execute(args: string[]): Promise<void> {
if (this.$options.start) {
return this.debugService.debug(this.$projectData);
}

const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release };
const deployOptions: IDeployPlatformOptions = {
clean: this.$options.clean,
device: this.$options.device,
Expand All @@ -31,6 +26,15 @@
provision: this.$options.provision,
teamId: this.$options.teamId
};

const buildConfig: IBuildConfig = _.merge({ buildForDevice: this.$options.forDevice }, deployOptions);

if (this.$options.start) {
return this.debugService.debug(this.$projectData, buildConfig);
}

const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: this.$options.bundle, release: this.$options.release };

await this.$platformService.deployPlatform(this.$devicesService.platform, appFilesUpdaterOptions, deployOptions, this.$projectData, { provision: this.$options.provision, sdk: this.$options.sdk });
this.$config.debugLivesync = true;
let applicationReloadAction = async (deviceAppData: Mobile.IDeviceAppData): Promise<void> => {
Expand All @@ -45,7 +49,7 @@

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

await this.debugService.debug(this.$projectData);
await this.debugService.debug(this.$projectData, buildConfig);
};
return this.$usbLiveSyncService.liveSync(this.$devicesService.platform, this.$projectData, applicationReloadAction);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/common
Submodule common updated 0 files
1 change: 1 addition & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const TESTING_FRAMEWORKS = ['jasmine', 'mocha', 'qunit'];
export const TEST_RUNNER_NAME = "nativescript-unit-test-runner";
export const LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"];
export const XML_FILE_EXTENSION = ".xml";
export const PLATFORMS_DIR_NAME = "platforms";

export class PackageVersion {
static NEXT = "next";
Expand Down
4 changes: 2 additions & 2 deletions lib/definitions/debug.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
interface IDebugService {
debug(projectData: IProjectData): Promise<void>;
debugStart(projectData: IProjectData): Promise<void>;
debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void>;
debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void>;
debugStop(): Promise<void>
platform: string;
}
24 changes: 16 additions & 8 deletions lib/definitions/platform.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
interface IPlatformService extends NodeJS.EventEmitter {
cleanPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, framework?: string): Promise<void>;

addPlatforms(platforms: string[], platformTemplate: string, projectData: IProjectData, platformSpecificData: IPlatformSpecificData, frameworkPath?: string): Promise<void>;

/**
Expand Down Expand Up @@ -143,28 +143,37 @@ interface IPlatformService extends NodeJS.EventEmitter {
/**
* Returns information about the latest built application for device in the current project.
* @param {IPlatformData} platformData Data describing the current platform.
* @param {IBuildConfig} buildConfig Defines if the build is for release configuration.
* @returns {IApplicationPackage} Information about latest built application.
*/
getLatestApplicationPackageForDevice(platformData: IPlatformData): IApplicationPackage;
getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage;

/**
* Returns information about the latest built application for simulator in the current project.
* @param {IPlatformData} platformData Data describing the current platform.
* @param {IBuildConfig} buildConfig Defines if the build is for release configuration.
* @returns {IApplicationPackage} Information about latest built application.
*/
getLatestApplicationPackageForEmulator(platformData: IPlatformData): IApplicationPackage;
getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig): IApplicationPackage;

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

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

/**
* Reads contents of a file on device.
Expand Down Expand Up @@ -209,8 +218,7 @@ interface IPlatformData {
appDestinationDirectoryPath: string;
deviceBuildOutputPath: string;
emulatorBuildOutputPath?: string;
validPackageNamesForDevice: string[];
validPackageNamesForEmulator?: string[];
getValidPackageNames(buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[];
frameworkFilesExtensions: string[];
frameworkDirectoriesExtensions?: string[];
frameworkDirectoriesNames?: string[];
Expand Down
4 changes: 3 additions & 1 deletion lib/definitions/project-changes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ interface IProjectChangesInfo {
changesRequireBuild: boolean;
}

interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision {}

interface IProjectChangesService {
checkForChanges(platform: string, projectData: IProjectData): IProjectChangesInfo;
checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): IProjectChangesInfo;
getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo;
savePrepareInfo(platform: string, projectData: IProjectData): void;
getPrepareInfoFilePath(platform: string, projectData: IProjectData): string;
Expand Down
3 changes: 1 addition & 2 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,10 @@ interface IPlatformProjectService extends NodeJS.EventEmitter {
/**
* Removes build artifacts specific to the platform
* @param {string} projectRoot The root directory of the native project.
* @param {string[]} options Options that can be passed to clean command.
* @param {IProjectData} projectData DTO with information about the project.
* @returns {void}
*/
cleanProject(projectRoot: string, options: string[], projectData: IProjectData): Promise<void>
cleanProject(projectRoot: string, projectData: IProjectData): Promise<void>
}

interface IAndroidProjectPropertiesManager {
Expand Down
86 changes: 23 additions & 63 deletions lib/project-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ interface IProjectType {
}

export class ProjectData implements IProjectData {
private static OLD_PROJECT_FILE_NAME = ".tnsproject";

/**
* NOTE: Order of the elements is important as the TypeScript dependencies are commonly included in Angular project as well.
*/
Expand Down Expand Up @@ -42,45 +40,51 @@ export class ProjectData implements IProjectData {

constructor(private $fs: IFileSystem,
private $errors: IErrors,
private $logger: ILogger,
private $projectHelper: IProjectHelper,
private $staticConfig: IStaticConfig,
private $options: IOptions) { }
private $options: IOptions,
private $logger: ILogger) { }

public initializeProjectData(projectDir? :string): void {
public initializeProjectData(projectDir?: string): void {
projectDir = projectDir || this.$projectHelper.projectDir;
// If no project found, projectDir should be null
if (projectDir) {
this.initializeProjectDataCore(projectDir);
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
let data: any = null;

if (this.$fs.exists(this.projectFilePath)) {
if (this.$fs.exists(projectFilePath)) {
let fileContent: any = null;
try {
fileContent = this.$fs.readJson(this.projectFilePath);
fileContent = this.$fs.readJson(projectFilePath);
data = fileContent[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE];
} catch (err) {
this.$errors.fail({
formatStr: "The project file %s is corrupted." + EOL +
"Consider restoring an earlier version from your source control or backup." + EOL +
"Additional technical info: %s",
suppressCommandHelp: true
},
this.projectFilePath, err.toString());
this.$errors.failWithoutHelp(`The project file ${this.projectFilePath} is corrupted. ${EOL}` +
`Consider restoring an earlier version from your source control or backup.${EOL}` +
`Additional technical info: ${err.toString()}`);
}

if (data) {
this.projectDir = projectDir;
this.projectName = this.$projectHelper.sanitizeName(path.basename(projectDir));
this.platformsDir = path.join(projectDir, constants.PLATFORMS_DIR_NAME);
Copy link
Contributor

@yyosifov yyosifov Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectName/platformsDir/appDirectoryPath/appResourcesDirectoryPath seem like "Computed properties" for me - they all are coming from the projectDir. Why not initialize only the project dir, and create getters for these properties so that we don't hit a bug if sometime we miss updating one of the computed ones? Do we have such a practice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea, but there's a reason to set the properties here - we are accessing them in many, many places, so computing each of them every single time will require reading the directory, the package.json, etc. This will make a lot of I/O operations. I can cache the properties after first execution, but this will not work for Fusion, which is a long living process.
So I'll keep this code until I figure out a better way for handling this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I didn't explain it correctly. What was my idea is that if you set the projectDir, all other properties - projectName, platformsDir, appDirectoryPath can be computed from the projectDir variable - without reading anything from the package.json

i.e. something like:

private _platformsDir: string; get platformsDir():boolean { if(!this._platformsDir) { this._platformsDir = path.join(this.projectDir, constants.PLATFORMS_DIR_NAME); } return this._platformsDir; }

so that we don't forget to initialize the platformsDir somewhere.

this.projectFilePath = projectFilePath;
this.appDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME);
this.appResourcesDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME);
this.projectId = data.id;
this.dependencies = fileContent.dependencies;
this.devDependencies = fileContent.devDependencies;
this.projectType = this.getProjectType();
} else { // This is the case when we have package.json file but nativescipt key is not presented in it
this.tryToUpgradeProject();

return;
}
}
} else { // This is the case when no project file found
this.tryToUpgradeProject();
}

const currentDir = path.resolve(".");
this.$logger.trace(`Unable to find project. projectDir: ${projectDir}, options.path: ${this.$options.path}, ${currentDir}`);

// This is the case when no project file found
this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", projectDir || this.$options.path || currentDir);
}

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

return detectedProjectType;
}

private throwNoProjectFoundError(): void {
this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", this.$options.path || path.resolve("."));
}

private tryToUpgradeProject(): void {
let projectDir = this.projectDir || path.resolve(this.$options.path || ".");
let oldProjectFilePath = path.join(projectDir, ProjectData.OLD_PROJECT_FILE_NAME);
if (this.$fs.exists(oldProjectFilePath)) {
this.upgrade(projectDir, oldProjectFilePath);
} else {
this.throwNoProjectFoundError();
}
}

private upgrade(projectDir: string, oldProjectFilePath: string): void {
try {
let oldProjectData = this.$fs.readJson(oldProjectFilePath);

let newProjectFilePath = this.projectFilePath || path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
let newProjectData = this.$fs.exists(newProjectFilePath) ? this.$fs.readJson(newProjectFilePath) : {};
newProjectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE] = oldProjectData;
this.$fs.writeJson(newProjectFilePath, newProjectData);
this.projectId = newProjectData[this.$staticConfig.CLIENT_NAME_KEY_IN_PROJECT_FILE].id;

this.$fs.deleteFile(oldProjectFilePath);
} catch (err) {
this.$logger.out("An error occurred while upgrading your project.");
throw err;
}

this.initializeProjectDataCore(projectDir);

this.$logger.out("Successfully upgraded your project file.");
}

private initializeProjectDataCore(projectDir: string): void {
this.projectDir = projectDir;
this.projectName = this.$projectHelper.sanitizeName(path.basename(projectDir));
this.platformsDir = path.join(projectDir, "platforms");
this.projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
this.appDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME);
this.appResourcesDirectoryPath = path.join(projectDir, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME);
}
}
$injector.register("projectData", ProjectData);
4 changes: 2 additions & 2 deletions lib/providers/livesync-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export class LiveSyncProvider implements ILiveSyncProvider {
await this.$platformService.buildPlatform(device.deviceInfo.platform, buildConfig, projectData);
let platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData);
if (device.isEmulator) {
return this.$platformService.getLatestApplicationPackageForEmulator(platformData).packageName;
return this.$platformService.getLatestApplicationPackageForEmulator(platformData, buildConfig).packageName;
}

return this.$platformService.getLatestApplicationPackageForDevice(platformData).packageName;
return this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName;
}

public async preparePlatformForSync(platform: string, provision: any, projectData: IProjectData): Promise<void> {
Expand Down
16 changes: 8 additions & 8 deletions lib/services/android-debug-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ class AndroidDebugService implements IDebugService {
this._device = newDevice;
}

public async debug(projectData: IProjectData): Promise<void> {
public async debug(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
return this.$options.emulator
? this.debugOnEmulator(projectData)
: this.debugOnDevice(projectData);
? this.debugOnEmulator(projectData, buildConfig)
: this.debugOnDevice(projectData, buildConfig);
}

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

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

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

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

let platformData = this.$platformsData.getPlatformData(this.platform, projectData);
packageFile = this.$platformService.getLatestApplicationPackageForDevice(platformData).packageName;
packageFile = this.$platformService.getLatestApplicationPackageForDevice(platformData, buildConfig).packageName;
this.$logger.out("Using ", packageFile);
}

Expand Down Expand Up @@ -170,7 +170,7 @@ class AndroidDebugService implements IDebugService {
await this.debugStartCore(packageName);
}

public async debugStart(projectData: IProjectData): Promise<void> {
public async debugStart(projectData: IProjectData, buildConfig: IBuildConfig): Promise<void> {
await this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device });
let action = (device: Mobile.IAndroidDevice): Promise<void> => {
this.device = device;
Expand Down
Loading