Skip to content

Commit dcf1745

Browse files
committed
Merge pull request #7 from NativeScript/fatme/refactor-project-service
Refactor project and platform services
2 parents e39b49c + 598d079 commit dcf1745

13 files changed

+451
-422
lines changed

lib/bootstrap.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ require("./common/bootstrap");
22

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

5+
$injector.require("projectData", "./services/project-service");
56
$injector.require("projectService", "./services/project-service");
6-
$injector.require("androidProjectService", "./services/project-service");
7-
$injector.require("iOSProjectService", "./services/project-service");
7+
$injector.require("androidProjectService", "./services/android-project-service");
8+
$injector.require("iOSProjectService", "./services/ios-project-service");
9+
810
$injector.require("projectTemplatesService", "./services/project-templates-service");
11+
12+
$injector.require("platformsData", "./services/platform-service");
913
$injector.require("platformService", "./services/platform-service");
1014

1115
$injector.requireCommand("create", "./commands/create-project");

lib/constants.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
///<reference path=".d.ts"/>
2+
3+
export var APP_FOLDER_NAME = "app";
4+
export var DEFAULT_PROJECT_ID = "com.telerik.tns.HelloWorld";
5+
export var DEFAULT_PROJECT_NAME = "HelloNativescript";
6+
export var APP_RESOURCES_FOLDER_NAME = "App_Resources";
7+
export var PROJECT_FRAMEWORK_FOLDER_NAME = "framework";
8+

lib/declarations.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
interface INodePackageManager {
22
cache: string;
33
load(config?: any): IFuture<void>;
4-
install(where: string, what: string): IFuture<any>;
4+
install(packageName: string, pathToSave?: string): IFuture<string>;
55
}
66

77
interface IPropertiesParser {

lib/definitions/platform.d.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ interface IPlatformService {
77
buildPlatform(platform: string): IFuture<void>;
88
}
99

10-
interface IPlatformCapabilities {
10+
interface IPlatformData {
11+
frameworkPackageName: string;
12+
platformProjectService: IPlatformProjectService;
13+
projectRoot: string;
14+
normalizedPlatformName: string;
1115
targetedOS?: string[];
12-
}
16+
}
17+
18+
interface IPlatformsData {
19+
platformsNames: string[];
20+
getPlatformData(platform: string): IPlatformData;
21+
}
22+

lib/definitions/project.d.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
interface IProjectService {
22
createProject(projectName: string, projectId: string): IFuture<void>;
3-
createPlatformSpecificProject(platform: string): IFuture<void>;
4-
prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture<void>;
5-
buildProject(platform: string): IFuture<void>;
63
ensureProject(): void;
7-
projectData: IProjectData;
8-
}
9-
10-
interface IPlatformProjectService {
11-
createProject(projectData: IProjectData): IFuture<void>;
12-
buildProject(projectData: IProjectData): IFuture<void>;
134
}
145

156
interface IProjectData {
167
projectDir: string;
8+
projectName: string;
179
platformsDir: string;
1810
projectFilePath: string;
1911
projectId?: string;
20-
projectName?: string;
2112
}
2213

2314
interface IProjectTemplatesService {
2415
defaultTemplatePath: IFuture<string>;
25-
installAndroidFramework(whereToInstall: string): IFuture<string>
16+
}
17+
18+
interface IPlatformProjectService {
19+
validate(): IFuture<void>;
20+
createProject(projectRoot: string, frameworkDir: string): IFuture<void>;
21+
interpolateData(projectRoot: string): void;
22+
afterCreateProject(projectRoot: string): void;
23+
prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture<void>;
24+
buildProject(projectRoot: string): IFuture<void>;
2625
}

lib/definitions/shelljs.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
declare module "shelljs" {
22
function cp(arg: string, sourcePath: string, destinationPath: string): void;
3+
function cp(arg: string, sourcePath: string[], destinationPath: string): void;
34
function sed(arg: string, oldValue: any, newValue: string, filePath: string): void;
45
function mv(source: string[], destination: string);
56
function grep(what: any, where: string): any;

lib/node-package-manager.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
import npm = require("npm");
44
import Future = require("fibers/future");
55
import shell = require("shelljs");
6+
import path = require("path");
67

78
export class NodePackageManager implements INodePackageManager {
9+
private static NPM_LOAD_FAILED = "Failed to retrieve data from npm. Please try again a little bit later.";
10+
11+
constructor(private $logger: ILogger,
12+
private $errors: IErrors) { }
13+
814
public get cache(): string {
915
return npm.cache;
1016
}
@@ -21,7 +27,21 @@ export class NodePackageManager implements INodePackageManager {
2127
return future;
2228
}
2329

24-
public install(where: string, what: string): IFuture<any> {
30+
public install(packageName: string, pathToSave?: string): IFuture<string> {
31+
return (() => {
32+
var action = (packageName: string) => {
33+
pathToSave = pathToSave || npm.cache;
34+
this.installCore(pathToSave, packageName).wait();
35+
};
36+
37+
this.tryExecuteAction(action, packageName).wait();
38+
39+
return path.join(pathToSave, "node_modules", packageName);
40+
41+
}).future<string>()();
42+
}
43+
44+
private installCore(where: string, what: string): IFuture<any> {
2545
var future = new Future<any>();
2646
npm.commands["install"](where, what, (err, data) => {
2747
if(err) {
@@ -32,5 +52,17 @@ export class NodePackageManager implements INodePackageManager {
3252
});
3353
return future;
3454
}
55+
56+
private tryExecuteAction(action: (...args: any[]) => void, ...args: any[]): IFuture<void> {
57+
return (() => {
58+
try {
59+
this.load().wait(); // It's obligatory to execute load before whatever npm function
60+
action.apply(null, args);
61+
} catch(error) {
62+
this.$logger.debug(error);
63+
this.$errors.fail(NodePackageManager.NPM_LOAD_FAILED);
64+
}
65+
}).future<void>()();
66+
}
3567
}
3668
$injector.register("npm", NodePackageManager);
+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
///<reference path="../.d.ts"/>
2+
import path = require("path");
3+
import shell = require("shelljs");
4+
import util = require("util");
5+
import options = require("./../options");
6+
import helpers = require("./../common/helpers");
7+
import constants = require("./../constants");
8+
9+
class AndroidProjectService implements IPlatformProjectService {
10+
constructor(private $fs: IFileSystem,
11+
private $errors: IErrors,
12+
private $logger: ILogger,
13+
private $childProcess: IChildProcess,
14+
private $projectData: IProjectData,
15+
private $propertiesParser: IPropertiesParser) { }
16+
17+
public validate(): IFuture<void> {
18+
return (() => {
19+
this.validatePackageName(this.$projectData.projectId);
20+
this.validateProjectName(this.$projectData.projectName);
21+
22+
this.checkAnt().wait() && this.checkAndroid().wait() && this.checkJava().wait();
23+
}).future<void>()();
24+
}
25+
26+
public createProject(projectRoot: string, frameworkDir: string): IFuture<void> {
27+
return (() => {
28+
this.validateAndroidTarget(frameworkDir); // We need framework to be installed to validate android target so we can't call this method in validate()
29+
30+
var paths = "assets gen libs res".split(' ').map(p => path.join(frameworkDir, p));
31+
shell.cp("-r", paths, projectRoot);
32+
33+
paths = ".project AndroidManifest.xml project.properties".split(' ').map(p => path.join(frameworkDir, p));
34+
shell.cp("-f", paths, projectRoot);
35+
36+
// Create src folder
37+
var packageName = this.$projectData.projectId;
38+
var packageAsPath = packageName.replace(/\./g, path.sep);
39+
var activityDir = path.join(projectRoot, 'src', packageAsPath);
40+
this.$fs.createDirectory(activityDir).wait();
41+
42+
}).future<any>()();
43+
}
44+
45+
public interpolateData(projectRoot: string): void {
46+
// Interpolate the activity name and package
47+
var stringsFilePath = path.join(projectRoot, 'res', 'values', 'strings.xml');
48+
shell.sed('-i', /__NAME__/, this.$projectData.projectName, stringsFilePath);
49+
shell.sed('-i', /__TITLE_ACTIVITY__/, this.$projectData.projectName, stringsFilePath);
50+
shell.sed('-i', /__NAME__/, this.$projectData.projectName, path.join(projectRoot, '.project'));
51+
shell.sed('-i', /__PACKAGE__/, this.$projectData.projectId, path.join(projectRoot, "AndroidManifest.xml"));
52+
}
53+
54+
public afterCreateProject(projectRoot: string) {
55+
var targetApi = this.getTarget(projectRoot).wait();
56+
this.$logger.trace("Android target: %s", targetApi);
57+
this.runAndroidUpdate(projectRoot, targetApi).wait();
58+
}
59+
60+
public prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture<void> {
61+
return (() => {
62+
var platform = normalizedPlatformName.toLowerCase();
63+
var assetsDirectoryPath = path.join(this.$projectData.platformsDir, platform, "assets");
64+
var appResourcesDirectoryPath = path.join(assetsDirectoryPath, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME);
65+
shell.cp("-r", path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), assetsDirectoryPath);
66+
67+
if (this.$fs.exists(appResourcesDirectoryPath).wait()) {
68+
shell.cp("-r", path.join(appResourcesDirectoryPath, normalizedPlatformName, "*"), path.join(this.$projectData.platformsDir, platform, "res"));
69+
this.$fs.deleteDirectory(appResourcesDirectoryPath).wait();
70+
}
71+
72+
var files = helpers.enumerateFilesInDirectorySync(path.join(assetsDirectoryPath, constants.APP_FOLDER_NAME));
73+
var platformsAsString = platforms.join("|");
74+
75+
_.each(files, fileName => {
76+
var platformInfo = AndroidProjectService.parsePlatformSpecificFileName(path.basename(fileName), platformsAsString);
77+
var shouldExcludeFile = platformInfo && platformInfo.platform !== platform;
78+
if (shouldExcludeFile) {
79+
this.$fs.deleteFile(fileName).wait();
80+
} else if (platformInfo && platformInfo.onDeviceName) {
81+
this.$fs.rename(fileName, path.join(path.dirname(fileName), platformInfo.onDeviceName)).wait();
82+
}
83+
});
84+
}).future<void>()();
85+
}
86+
87+
public buildProject(projectRoot: string): IFuture<void> {
88+
return (() => {
89+
var buildConfiguration = options.release ? "release" : "debug";
90+
var args = this.getAntArgs(buildConfiguration, projectRoot);
91+
this.spawn('ant', args);
92+
}).future<void>()();
93+
}
94+
95+
private spawn(command: string, args: string[], options?: any): void {
96+
if(helpers.isWindows()) {
97+
args.unshift('/s', '/c', command);
98+
command = 'cmd';
99+
}
100+
101+
this.$childProcess.spawn(command, args, {cwd: options, stdio: 'inherit'});
102+
}
103+
104+
private getAntArgs(configuration: string, projectRoot: string): string[] {
105+
var args = [configuration, "-f", path.join(projectRoot, "build.xml")];
106+
return args;
107+
}
108+
109+
private runAndroidUpdate(projectPath: string, targetApi: string): IFuture<void> {
110+
return (() => {
111+
var args = [
112+
"--path", projectPath,
113+
"--target", targetApi
114+
];
115+
116+
this.spawn("android update project", args);
117+
}).future<void>()();
118+
}
119+
120+
private validatePackageName(packageName: string): void {
121+
//Make the package conform to Java package types
122+
//Enforce underscore limitation
123+
if (!/^[a-zA-Z]+(\.[a-zA-Z0-9][a-zA-Z0-9_]*)+$/.test(packageName)) {
124+
this.$errors.fail("Package name must look like: com.company.Name");
125+
}
126+
127+
//Class is a reserved word
128+
if(/\b[Cc]lass\b/.test(packageName)) {
129+
this.$errors.fail("class is a reserved word");
130+
}
131+
}
132+
133+
private validateProjectName(projectName: string): void {
134+
if (projectName === '') {
135+
this.$errors.fail("Project name cannot be empty");
136+
}
137+
138+
//Classes in Java don't begin with numbers
139+
if (/^[0-9]/.test(projectName)) {
140+
this.$errors.fail("Project name must not begin with a number");
141+
}
142+
}
143+
144+
private validateAndroidTarget(frameworkDir: string) {
145+
var validTarget = this.getTarget(frameworkDir).wait();
146+
var output = this.$childProcess.exec('android list targets').wait();
147+
if (!output.match(validTarget)) {
148+
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.",
149+
validTarget.split('-')[1]);
150+
}
151+
}
152+
153+
private getTarget(projectRoot: string): IFuture<string> {
154+
return (() => {
155+
var projectPropertiesFilePath = path.join(projectRoot, "project.properties");
156+
157+
if (this.$fs.exists(projectPropertiesFilePath).wait()) {
158+
var properties = this.$propertiesParser.createEditor(projectPropertiesFilePath).wait();
159+
return properties.get("target");
160+
}
161+
162+
return "";
163+
}).future<string>()();
164+
}
165+
166+
private checkAnt(): IFuture<void> {
167+
return (() => {
168+
try {
169+
this.$childProcess.exec("ant -version").wait();
170+
} catch(error) {
171+
this.$errors.fail("Error executing commands 'ant', make sure you have ant installed and added to your PATH.")
172+
}
173+
}).future<void>()();
174+
}
175+
176+
private checkJava(): IFuture<void> {
177+
return (() => {
178+
try {
179+
this.$childProcess.exec("java -version").wait();
180+
} catch(error) {
181+
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);
182+
}
183+
}).future<void>()();
184+
}
185+
186+
private checkAndroid(): IFuture<void> {
187+
return (() => {
188+
try {
189+
this.$childProcess.exec('android list targets').wait();
190+
} catch(error) {
191+
if (error.match(/command\snot\sfound/)) {
192+
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.");
193+
} else {
194+
this.$errors.fail("An error occurred while listing Android targets");
195+
}
196+
}
197+
}).future<void>()();
198+
}
199+
200+
private static parsePlatformSpecificFileName(fileName: string, platforms: string): any {
201+
var regex = util.format("^(.+?)\.(%s)(\..+?)$", platforms);
202+
var parsed = fileName.toLowerCase().match(new RegExp(regex, "i"));
203+
if (parsed) {
204+
return {
205+
platform: parsed[2],
206+
onDeviceName: parsed[1] + parsed[3]
207+
};
208+
}
209+
return undefined;
210+
}
211+
}
212+
$injector.register("androidProjectService", AndroidProjectService);

0 commit comments

Comments
 (0)