Skip to content

Refactor project and platform services #7

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 12 commits into from
Jul 24, 2014
9 changes: 7 additions & 2 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ require("./common/bootstrap");

$injector.require("nativescript-cli", "./nativescript-cli");

$injector.require("projectData", "./services/project-service");
$injector.require("projectService", "./services/project-service");
$injector.require("androidProjectService", "./services/project-service");
$injector.require("iOSProjectService", "./services/project-service");
$injector.require("androidProjectService", "./services/android-project-service");
$injector.require("iOSProjectService", "./services/ios-project-service");

$injector.require("projectTemplatesService", "./services/project-templates-service");

$injector.require("platformsData", "./services/platform-service");
$injector.require("platformService", "./services/platform-service");
$injector.require("platformProjectService", "./services/platform-project-service");

$injector.requireCommand("create", "./commands/create-project");
$injector.requireCommand("platform|*list", "./commands/list-platforms");
Expand Down
5 changes: 5 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
///<reference path=".d.ts"/>

export var APP_FOLDER_NAME = "app";
export var DEFAULT_PROJECT_ID = "com.telerik.tns.HelloWorld";
export var DEFAULT_PROJECT_NAME = "HelloNativescript";
1 change: 1 addition & 0 deletions lib/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface INodePackageManager {
cache: string;
load(config?: any): IFuture<void>;
install(where: string, what: string): IFuture<any>;
Copy link
Contributor

Choose a reason for hiding this comment

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

This method can be made private in the implementation. The installSafe method can be renamed to just install, then.

installSafe(packageName: string, pathToSave?: string): IFuture<string>;
}

interface IPropertiesParser {
Expand Down
14 changes: 12 additions & 2 deletions lib/definitions/platform.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ interface IPlatformService {
buildPlatform(platform: string): IFuture<void>;
}

interface IPlatformCapabilities {
interface IPlatformData {
frameworkPackageName: string;
platformProjectService: IPlatformSpecificProjectService;
projectRoot: string;
normalizedPlatformName: string;
Copy link

Choose a reason for hiding this comment

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

Do we need the normalized prefix?

targetedOS?: string[];
}
}

interface IPlatformsData {
platformsNames: string[];
getPlatformData(platform: string): IPlatformData;
}

26 changes: 15 additions & 11 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
interface IProjectService {
createProject(projectName: string, projectId: string): IFuture<void>;
createPlatformSpecificProject(platform: string): IFuture<void>;
prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture<void>;
buildProject(platform: string): IFuture<void>;
ensureProject(): void;
projectData: IProjectData;
}

interface IPlatformProjectService {
createProject(projectData: IProjectData): IFuture<void>;
buildProject(projectData: IProjectData): IFuture<void>;
}

interface IProjectData {
projectDir: string;
projectName: string;
platformsDir: string;
projectFilePath: string;
projectId?: string;
projectName?: string;
}

interface IProjectTemplatesService {
defaultTemplatePath: IFuture<string>;
installAndroidFramework(whereToInstall: string): IFuture<string>
}

interface IPlatformProjectService {
createProject(platform: string): IFuture<void>;
buildProject(platform: string): IFuture<void>;
prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture<void>;
}

interface IPlatformSpecificProjectService {
validate(): IFuture<void>;
createProject(projectRoot: string, frameworkDir: string): IFuture<void>;
interpolateData(projectRoot: string): void;
executePlatformSpecificAction(projectRoot: string): void;
Copy link

Choose a reason for hiding this comment

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

This method name is too generic. You can rename it to something like: afterCreateProject

buildProject(projectRoot: string): IFuture<void>;
}
31 changes: 31 additions & 0 deletions lib/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
import npm = require("npm");
import Future = require("fibers/future");
import shell = require("shelljs");
import path = require("path");

export class NodePackageManager implements INodePackageManager {
private static NPM_LOAD_FAILED = "Failed to retrieve data from npm. Please try again a little bit later.";

constructor(private $logger: ILogger,
private $errors: IErrors) { }

public get cache(): string {
return npm.cache;
}
Expand Down Expand Up @@ -32,5 +38,30 @@ export class NodePackageManager implements INodePackageManager {
});
return future;
}

private tryExecuteAction(action: (...args: any[]) => void, ...args: any[]): IFuture<void> {
return (() => {
try {
this.load().wait(); // It's obligatory to execute load before whatever npm function
action.apply(null, args);
} catch(error) {
this.$logger.debug(error);
this.$errors.fail(NodePackageManager.NPM_LOAD_FAILED);
}
}).future<void>()();
}

public installSafe(packageName: string, pathToSave?: string): IFuture<string> {
return (() => {
var action = (packageName: string) => {
this.install(pathToSave || npm.cache, packageName).wait();
};

this.tryExecuteAction(action, packageName).wait();

return path.join(pathToSave || npm.cache, "node_modules", packageName);

}).future<string>()();
}
}
$injector.register("npm", NodePackageManager);
171 changes: 171 additions & 0 deletions lib/services/android-project-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
///<reference path="../.d.ts"/>
import path = require("path");
import shell = require("shelljs");
import options = require("./../options");
import helpers = require("./../common/helpers");

class AndroidProjectService implements IPlatformSpecificProjectService {
constructor(private $fs: IFileSystem,
private $errors: IErrors,
private $logger: ILogger,
private $childProcess: IChildProcess,
private $projectData: IProjectData,
private $propertiesParser: IPropertiesParser) { }

public validate(): IFuture<void> {
return (() => {
this.validatePackageName(this.$projectData.projectId);
this.validateProjectName(this.$projectData.projectName);

this.checkAnt().wait() && this.checkAndroid().wait() && this.checkJava().wait();
}).future<void>()();
}

public createProject(projectRoot: string, frameworkDir: string): IFuture<void> {
return (() => {
var packageName = this.$projectData.projectId;
var packageAsPath = packageName.replace(/\./g, path.sep);

var validTarget = this.getTarget(frameworkDir).wait();
Copy link

Choose a reason for hiding this comment

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

This validation can be extracted in a separate method.

var output = this.$childProcess.exec('android list targets').wait();
if (!output.match(validTarget)) {
this.$errors.fail("Please install Android target %s the Android newest SDK). Make sure you have the latest Android tools installed as well. Run \"android\" from your command-line to install/update any missing SDKs or tools.",
validTarget.split('-')[1]);
}

shell.cp("-r", path.join(frameworkDir, "assets"), projectRoot);
Copy link

Choose a reason for hiding this comment

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

You can use an array as a second argument here: https://github.com/arturadib/shelljs#cpoptions--source_array-dest.
Ex:

var paths = "assets gen libs res".split(' ').map(p => path.join(frameworkDir, p));

shell.cp("-r", path.join(frameworkDir, "gen"), projectRoot);
shell.cp("-r", path.join(frameworkDir, "libs"), projectRoot);
shell.cp("-r", path.join(frameworkDir, "res"), projectRoot);

shell.cp("-f", path.join(frameworkDir, ".project"), projectRoot);
shell.cp("-f", path.join(frameworkDir, "AndroidManifest.xml"), projectRoot);
shell.cp("-f", path.join(frameworkDir, "project.properties"), projectRoot);

// Create src folder
var activityDir = path.join(projectRoot, 'src', packageAsPath);
this.$fs.createDirectory(activityDir).wait();

}).future<any>()();
}

public interpolateData(projectRoot: string): void {
// Interpolate the activity name and package
var stringsFilePath = path.join(projectRoot, 'res', 'values', 'strings.xml');
shell.sed('-i', /__NAME__/, this.$projectData.projectName, stringsFilePath);
shell.sed('-i', /__TITLE_ACTIVITY__/, this.$projectData.projectName, stringsFilePath);
shell.sed('-i', /__NAME__/, this.$projectData.projectName, path.join(projectRoot, '.project'));
shell.sed('-i', /__PACKAGE__/, this.$projectData.projectId, path.join(projectRoot, "AndroidManifest.xml"));
}

public executePlatformSpecificAction(projectRoot: string) {
var targetApi = this.getTarget(projectRoot).wait();
this.$logger.trace("Android target: %s", targetApi);
this.runAndroidUpdate(projectRoot, targetApi).wait();
}

public buildProject(projectRoot: string): IFuture<void> {
return (() => {
var buildConfiguration = options.release ? "release" : "debug";
var args = this.getAntArgs(buildConfiguration, projectRoot);
this.spawn('ant', args);
}).future<void>()();
}

private spawn(command: string, args: string[], options?: any): void {
if(helpers.isWindows()) {
args.unshift('/s', '/c', command);
command = 'cmd';
}

this.$childProcess.spawn(command, args, {cwd: options, stdio: 'inherit'});
}

private getAntArgs(configuration: string, projectRoot: string): string[] {
var args = [configuration, "-f", path.join(projectRoot, "build.xml")];
return args;
}

private runAndroidUpdate(projectPath: string, targetApi: string): IFuture<void> {
return (() => {
var args = [
"--path", projectPath,
"--target", targetApi
];

this.spawn("android update project", args);
}).future<void>()();
}

private validatePackageName(packageName: string): void {
//Make the package conform to Java package types
//Enforce underscore limitation
if (!/^[a-zA-Z]+(\.[a-zA-Z0-9][a-zA-Z0-9_]*)+$/.test(packageName)) {
this.$errors.fail("Package name must look like: com.company.Name");
}

//Class is a reserved word
if(/\b[Cc]lass\b/.test(packageName)) {
this.$errors.fail("class is a reserved word");
}
}

private validateProjectName(projectName: string): void {
if (projectName === '') {
this.$errors.fail("Project name cannot be empty");
}

//Classes in Java don't begin with numbers
if (/^[0-9]/.test(projectName)) {
this.$errors.fail("Project name must not begin with a number");
}
}

private getTarget(projectRoot: string): IFuture<string> {
return (() => {
var projectPropertiesFilePath = path.join(projectRoot, "project.properties");

if (this.$fs.exists(projectPropertiesFilePath).wait()) {
var properties = this.$propertiesParser.createEditor(projectPropertiesFilePath).wait();
return properties.get("target");
}

return "";
}).future<string>()();
}

private checkAnt(): IFuture<void> {
return (() => {
try {
this.$childProcess.exec("ant -version").wait();
} catch(error) {
this.$errors.fail("Error executing commands 'ant', make sure you have ant installed and added to your PATH.")
}
}).future<void>()();
}

private checkJava(): IFuture<void> {
return (() => {
try {
this.$childProcess.exec("java -version").wait();
} catch(error) {
this.$errors.fail("%s\n Failed to run 'java', make sure your java environment is set up.\n Including JDK and JRE.\n Your JAVA_HOME variable is %s", error, process.env.JAVA_HOME);
}
}).future<void>()();
}

private checkAndroid(): IFuture<void> {
return (() => {
try {
this.$childProcess.exec('android list targets').wait();
} catch(error) {
if (error.match(/command\snot\sfound/)) {
this.$errors.fail("The command \"android\" failed. Make sure you have the latest Android SDK installed, and the \"android\" command (inside the tools/ folder) is added to your path.");
} else {
this.$errors.fail("An error occurred while listing Android targets");
}
}
}).future<void>()();
}
}
$injector.register("androidProjectService", AndroidProjectService);
29 changes: 29 additions & 0 deletions lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
///<reference path="../.d.ts"/>

class IOSProjectService implements IPlatformSpecificProjectService {
public validate(): IFuture<void> {
return (() => {
}).future<void>()();
}

public interpolateData(): void {

}

public executePlatformSpecificAction(): void {

}

public createProject(): IFuture<void> {
return (() => {

}).future<any>()();
}

public buildProject(): IFuture<void> {
return (() => {

}).future<void>()();
}
}
$injector.register("iOSProjectService", IOSProjectService);
Loading