diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 8cc63b833d..7cf338d9b1 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -3,8 +3,17 @@ require("./common/bootstrap"); $injector.require("nativescript-cli", "./nativescript-cli"); $injector.require("projectService", "./services/project-service"); +$injector.require("androidProjectService", "./services/project-service"); +$injector.require("iOSProjectService", "./services/project-service"); $injector.require("projectTemplatesService", "./services/project-templates-service"); +$injector.require("platformService", "./services/platform-service"); -$injector.requireCommand("create", "./commands/create-project-command"); +$injector.requireCommand("create", "./commands/create-project"); +$injector.requireCommand("platform|*list", "./commands/list-platforms"); +$injector.requireCommand("platform|add", "./commands/add-platform"); +$injector.requireCommand("run", "./commands/run"); +$injector.requireCommand("prepare", "./commands/prepare"); +$injector.requireCommand("build", "./commands/build"); $injector.require("npm", "./node-package-manager"); +$injector.require("propertiesParser", "./properties-parser"); diff --git a/lib/commands/add-platform.ts b/lib/commands/add-platform.ts new file mode 100644 index 0000000000..85923e452a --- /dev/null +++ b/lib/commands/add-platform.ts @@ -0,0 +1,12 @@ +/// + +export class AddPlatformCommand implements ICommand { + constructor(private $platformService: IPlatformService) { } + + execute(args: string[]): IFuture { + return (() => { + this.$platformService.addPlatforms(args).wait(); + }).future()(); + } +} +$injector.registerCommand("platform|add", AddPlatformCommand); \ No newline at end of file diff --git a/lib/commands/build.ts b/lib/commands/build.ts new file mode 100644 index 0000000000..d483add028 --- /dev/null +++ b/lib/commands/build.ts @@ -0,0 +1,12 @@ +/// + +export class BuildCommand implements ICommand { + constructor(private $platformService: IPlatformService) { } + + execute(args: string[]): IFuture { + return (() => { + this.$platformService.buildPlatform(args[0]).wait(); + }).future()(); + } +} +$injector.registerCommand("build", BuildCommand); \ No newline at end of file diff --git a/lib/commands/create-project-command.ts b/lib/commands/create-project.ts similarity index 100% rename from lib/commands/create-project-command.ts rename to lib/commands/create-project.ts diff --git a/lib/commands/list-platforms.ts b/lib/commands/list-platforms.ts new file mode 100644 index 0000000000..855d384f13 --- /dev/null +++ b/lib/commands/list-platforms.ts @@ -0,0 +1,20 @@ +/// +import helpers = require("./../common/helpers"); + +export class ListPlatformsCommand implements ICommand { + constructor(private $platformService: IPlatformService, + private $logger: ILogger) { } + + execute(args: string[]): IFuture { + return (() => { + var availablePlatforms = this.$platformService.getAvailablePlatforms().wait(); + this.$logger.out("Available platforms: %s", helpers.formatListOfNames(availablePlatforms)); + + var installedPlatforms = this.$platformService.getInstalledPlatforms().wait(); + this.$logger.out("Installed platforms %s", helpers.formatListOfNames(installedPlatforms)); + }).future()(); + } +} +$injector.registerCommand("platform|*list", ListPlatformsCommand); + + diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts new file mode 100644 index 0000000000..4821ff66e9 --- /dev/null +++ b/lib/commands/prepare.ts @@ -0,0 +1,12 @@ +/// + +export class PrepareCommand implements ICommand { + constructor(private $platformService: IPlatformService) { } + + execute(args: string[]): IFuture { + return (() => { + this.$platformService.preparePlatform(args[0]).wait(); + }).future()(); + } +} +$injector.registerCommand("prepare", PrepareCommand); diff --git a/lib/commands/run.ts b/lib/commands/run.ts new file mode 100644 index 0000000000..39292de7c5 --- /dev/null +++ b/lib/commands/run.ts @@ -0,0 +1,12 @@ +/// + +export class RunCommand implements ICommand { + constructor(private $platformService: IPlatformService) { } + + execute(args: string[]): IFuture { + return (() => { + this.$platformService.runPlatform(args[0]).wait(); + }).future()(); + } +} +$injector.registerCommand("run", RunCommand); diff --git a/lib/common b/lib/common index 33c883f1ab..789a82292b 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 33c883f1abae2a5e5486a87f11df1d75e00e1dc8 +Subproject commit 789a82292b66159981e7e02791f4ba89a32c74a1 diff --git a/lib/declarations.ts b/lib/declarations.ts index f8bbfab6e1..ea88f0bcdd 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -1,5 +1,9 @@ interface INodePackageManager { - cache: string; - load(config?: any): IFuture; - install(where: string, what: string): IFuture; + cache: string; + load(config?: any): IFuture; + install(where: string, what: string): IFuture; +} + +interface IPropertiesParser { + createEditor(filePath: string): IFuture; } \ No newline at end of file diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts new file mode 100644 index 0000000000..0a774170bb --- /dev/null +++ b/lib/definitions/platform.d.ts @@ -0,0 +1,12 @@ +interface IPlatformService { + addPlatforms(platforms: string[]): IFuture; + getInstalledPlatforms(): IFuture; + getAvailablePlatforms(): IFuture; + runPlatform(platform: string): IFuture; + preparePlatform(platform: string): IFuture; + buildPlatform(platform: string): IFuture; +} + +interface IPlatformCapabilities { + targetedOS?: string[]; +} \ No newline at end of file diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 20934af393..a0eb4d8cfb 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -1,7 +1,26 @@ interface IProjectService { createProject(projectName: string, projectId: string): IFuture; + createPlatformSpecificProject(platform: string): IFuture; + prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture; + buildProject(platform: string): IFuture; + ensureProject(): void; + projectData: IProjectData; +} + +interface IPlatformProjectService { + createProject(projectData: IProjectData): IFuture; + buildProject(projectData: IProjectData): IFuture; +} + +interface IProjectData { + projectDir: string; + platformsDir: string; + projectFilePath: string; + projectId?: string; + projectName?: string; } interface IProjectTemplatesService { defaultTemplatePath: IFuture; + installAndroidFramework(whereToInstall: string): IFuture } \ No newline at end of file diff --git a/lib/definitions/properties-parser.d.ts b/lib/definitions/properties-parser.d.ts new file mode 100644 index 0000000000..e214062da3 --- /dev/null +++ b/lib/definitions/properties-parser.d.ts @@ -0,0 +1,3 @@ +declare module "properties-parser" { + function createEditor(path: string, callback: (err: IErrors, data: any) => void); +} \ No newline at end of file diff --git a/lib/definitions/shelljs.d.ts b/lib/definitions/shelljs.d.ts index 99ea4dc36e..0b84163555 100644 --- a/lib/definitions/shelljs.d.ts +++ b/lib/definitions/shelljs.d.ts @@ -2,4 +2,5 @@ declare module "shelljs" { function cp(arg: string, sourcePath: string, destinationPath: string): void; function sed(arg: string, oldValue: any, newValue: string, filePath: string): void; function mv(source: string[], destination: string); + function grep(what: any, where: string): any; } \ No newline at end of file diff --git a/lib/nativescript-cli.ts b/lib/nativescript-cli.ts index e2600aa613..2c58c2c502 100644 --- a/lib/nativescript-cli.ts +++ b/lib/nativescript-cli.ts @@ -1,16 +1,16 @@ /// +"use strict"; -import Fiber = require("fibers"); -import Future = require("fibers/future"); import path = require("path"); +require("./common/extensions"); require("./bootstrap"); require("./options"); import errors = require("./common/errors"); errors.installUncaughtExceptionListener(); -$injector.register("config", {"CI_LOGGER": false}); +$injector.register("config", {"CI_LOGGER": false, PROJECT_FILE_NAME: ".tnsproject", "DEBUG": process.env.NATIVESCRIPT_DEBUG}); var dispatcher = $injector.resolve("dispatcher"); -dispatcher.runMainFiber(); +dispatcher.runMainFiber(); \ No newline at end of file diff --git a/lib/node-package-manager.ts b/lib/node-package-manager.ts index 4a12295114..1f0c815e71 100644 --- a/lib/node-package-manager.ts +++ b/lib/node-package-manager.ts @@ -33,4 +33,4 @@ export class NodePackageManager implements INodePackageManager { return future; } } -$injector.register("npm", NodePackageManager); \ No newline at end of file +$injector.register("npm", NodePackageManager); diff --git a/lib/options.ts b/lib/options.ts index 56a48f873d..20f58e75e7 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -10,6 +10,7 @@ var knownOpts:any = { "path" : String, "copy-from": String, "link-to": String, + "release": String, "version": Boolean, "help": Boolean }, diff --git a/lib/properties-parser.ts b/lib/properties-parser.ts new file mode 100644 index 0000000000..9b77eeaf4f --- /dev/null +++ b/lib/properties-parser.ts @@ -0,0 +1,20 @@ +/// + +import propertiesParser = require("properties-parser"); +import Future = require("fibers/future"); + +export class PropertiesParser implements IPropertiesParser { + public createEditor(filePath: string) { + var future = new Future(); + propertiesParser.createEditor(filePath, (err, data) => { + if(err) { + future.throw(err); + } else { + future.return(data); + } + }); + + return future; + } +} +$injector.register("propertiesParser", PropertiesParser); \ No newline at end of file diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts new file mode 100644 index 0000000000..39214865dc --- /dev/null +++ b/lib/services/platform-service.ts @@ -0,0 +1,147 @@ +/// + +import path = require("path"); +import util = require("util"); +import helpers = require("./../common/helpers"); + +export class PlatformService implements IPlatformService { + private platformCapabilities: { [key: string]: IPlatformCapabilities } = { + ios: { + targetedOS: ['darwin'] + }, + android: { + } + }; + + private platformNames = []; + + constructor(private $errors: IErrors, + private $fs: IFileSystem, + private $projectService: IProjectService) { + this.platformNames = Object.keys(this.platformCapabilities); + } + + public getCapabilities(platform: string): IPlatformCapabilities { + return this.platformCapabilities[platform]; + } + + public addPlatforms(platforms: string[]): IFuture { + return (() => { + if(!platforms || platforms.length === 0) { + this.$errors.fail("No platform specified. Please specify a platform to add"); + } + + this.$projectService.ensureProject(); + + var platformsDir = this.$projectService.projectData.platformsDir; + this.$fs.ensureDirectoryExists(platformsDir).wait(); + + _.each(platforms, platform => { + this.addPlatform(platform.toLowerCase()).wait(); + }); + + }).future()(); + } + + private addPlatform(platform: string): IFuture { + return(() => { + platform = platform.split("@")[0]; + + this.validatePlatform(platform); + + var platformPath = path.join(this.$projectService.projectData.platformsDir, platform); + + // TODO: Check for version compatability if the platform is in format platform@version. This should be done in PR for semanting versioning + + if (this.$fs.exists(platformPath).wait()) { + this.$errors.fail("Platform %s already added", platform); + } + + // Copy platform specific files in platforms dir + this.$projectService.createPlatformSpecificProject(platform).wait(); + + }).future()(); + } + + public getInstalledPlatforms(): IFuture { + return(() => { + if(!this.$fs.exists(this.$projectService.projectData.platformsDir).wait()) { + return []; + } + + var subDirs = this.$fs.readDirectory(this.$projectService.projectData.platformsDir).wait(); + return _.filter(subDirs, p => { return this.platformNames.indexOf(p) > -1; }); + }).future()(); + } + + public getAvailablePlatforms(): IFuture { + return (() => { + var installedPlatforms = this.getInstalledPlatforms().wait(); + return _.filter(this.platformNames, p => { + return installedPlatforms.indexOf(p) < 0 && this.isPlatformSupportedForOS(p); // Only those not already installed + }); + }).future()(); + } + + public runPlatform(platform: string): IFuture { + return (() => { + + }).future()(); + } + + public preparePlatform(platform: string): IFuture { + return (() => { + platform = platform.toLowerCase(); + this.validatePlatform(platform); + var normalizedPlatformName = this.normalizePlatformName(platform); + + this.$projectService.prepareProject(normalizedPlatformName, this.platformNames).wait(); + }).future()(); + } + + public buildPlatform(platform: string): IFuture { + return (() => { + platform = platform.toLocaleLowerCase(); + this.validatePlatform(platform); + + this.$projectService.buildProject(platform).wait(); + }).future()(); + } + + private validatePlatform(platform: string): void { + if (!this.isValidPlatform(platform)) { + this.$errors.fail("Invalid platform %s. Valid platforms are %s.", platform, helpers.formatListOfNames(this.platformNames)); + } + + if (!this.isPlatformSupportedForOS(platform)) { + this.$errors.fail("Applications for platform %s can not be built on this OS - %s", platform, process.platform); + } + } + + private isValidPlatform(platform: string) { + return this.platformCapabilities[platform]; + } + + private isPlatformSupportedForOS(platform: string): boolean { + var platformCapabilities = this.getCapabilities(platform); + var targetedOS = platformCapabilities.targetedOS; + + if(!targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0) { + return true; + } + + return false; + } + + private normalizePlatformName(platform: string): string { + switch(platform) { + case "android": + return "Android"; + case "ios": + return "iOS"; + } + + return undefined; + } +} +$injector.register("platformService", PlatformService); \ No newline at end of file diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 5511e20d97..1aee2c152e 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -4,23 +4,59 @@ import path = require("path"); import options = require("./../options"); import shell = require("shelljs"); import osenv = require("osenv"); +import util = require("util"); +import helpers = require("./../common/helpers"); export class ProjectService implements IProjectService { - private static DEFAULT_ID = "com.telerik.tns.HelloWorld"; - private static DEFAULT_NAME = "HelloNativescript"; - private static APP_FOLDER_NAME = "app"; + private static DEFAULT_PROJECT_ID = "com.telerik.tns.HelloWorld"; + private static DEFAULT_PROJECT_NAME = "HelloNativescript"; + public static APP_FOLDER_NAME = "app"; + private static APP_RESOURCES_FOLDER_NAME = "App_Resources"; + public static PROJECT_FRAMEWORK_DIR = "framework"; + + public projectData: IProjectData = null; constructor(private $logger: ILogger, private $errors: IErrors, private $fs: IFileSystem, - private $projectTemplatesService: IProjectTemplatesService) { } + private $projectTemplatesService: IProjectTemplatesService, + private $androidProjectService: IPlatformProjectService, + private $iOSProjectService: IPlatformProjectService, + private $projectHelper: IProjectHelper, + private $config) { + this.projectData = this.getProjectData().wait(); + } + + private getProjectData(): IFuture { + return(() => { + var projectData: IProjectData = null; + var projectDir = this.$projectHelper.projectDir; + + if(projectDir) { + projectData = { + projectDir: projectDir, + platformsDir: path.join(projectDir, "platforms"), + projectFilePath: path.join(projectDir, this.$config.PROJECT_FILE_NAME) + }; + var projectFilePath = path.join(projectDir, this.$config.PROJECT_FILE_NAME); + + if (this.$fs.exists(projectFilePath).wait()) { + var fileContent = this.$fs.readJson(projectFilePath).wait(); + projectData.projectId = fileContent.id; + projectData.projectName = path.basename(projectDir); + } + } + + return projectData; + }).future()(); + } public createProject(projectName: string, projectId: string): IFuture { return(() => { var projectDir = path.resolve(options.path || "."); - projectId = projectId || ProjectService.DEFAULT_ID; - projectName = projectName || ProjectService.DEFAULT_NAME; + projectId = projectId || ProjectService.DEFAULT_PROJECT_ID; + projectName = projectName || ProjectService.DEFAULT_PROJECT_NAME; projectDir = path.join(projectDir, projectName); this.$fs.createDirectory(projectDir).wait(); @@ -34,7 +70,7 @@ export class ProjectService implements IProjectService { this.$errors.fail("Path already exists and is not empty %s", projectDir); } - this.$logger.trace("Creating a new NativeScript project with name %s and id at location", projectName, projectId, projectDir); + this.$logger.trace("Creating a new NativeScript project with name %s and id %s at location %s", projectName, projectId, projectDir); var appDirectory = path.join(projectDir, ProjectService.APP_FOLDER_NAME); var appPath: string = null; @@ -58,11 +94,11 @@ export class ProjectService implements IProjectService { appPath = defaultTemplatePath; } - this.createProjectCore(projectDir, appPath, false).wait(); + this.createProjectCore(projectDir, appPath, projectId, false).wait(); }).future()(); } - private createProjectCore(projectDir: string, appPath: string, symlink?: boolean): IFuture { + private createProjectCore(projectDir: string, appPath: string, projectId: string, symlink?: boolean): IFuture { return (() => { if(!this.$fs.exists(projectDir).wait()) { this.$fs.createDirectory(projectDir).wait(); @@ -74,18 +110,100 @@ export class ProjectService implements IProjectService { this.$fs.createDirectory(appDir).wait(); shell.cp('-R', path.join(appPath, "*"), appDir); } - this.createBasicProjectStructure(projectDir).wait(); + this.createBasicProjectStructure(projectDir, projectId).wait(); }).future()(); } - private createBasicProjectStructure(projectDir: string): IFuture { + private createBasicProjectStructure(projectDir: string, projectId: string): IFuture { return (() => { this.$fs.createDirectory(path.join(projectDir, "platforms")).wait(); this.$fs.createDirectory(path.join(projectDir, "tns_modules")).wait(); this.$fs.createDirectory(path.join(projectDir, "hooks")).wait(); + + var projectData = { id: projectId }; + this.$fs.writeFile(path.join(projectDir, this.$config.PROJECT_FILE_NAME), JSON.stringify(projectData)).wait(); + }).future()(); + } + + public createPlatformSpecificProject(platform: string): IFuture { + return(() => { + this.executePlatformSpecificAction(platform, "createProject").wait(); + }).future()(); + } + + public prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture { + return (() => { + var platform = normalizedPlatformName.toLowerCase(); + var assetsDirectoryPath = path.join(this.projectData.platformsDir, platform, "assets"); + var appResourcesDirectoryPath = path.join(assetsDirectoryPath, ProjectService.APP_FOLDER_NAME, ProjectService.APP_RESOURCES_FOLDER_NAME); + shell.cp("-r", path.join(this.projectData.projectDir, ProjectService.APP_FOLDER_NAME), assetsDirectoryPath); + + if(this.$fs.exists(appResourcesDirectoryPath).wait()) { + shell.cp("-r", path.join(appResourcesDirectoryPath, normalizedPlatformName, "*"), path.join(this.projectData.platformsDir, platform, "res")); + this.$fs.deleteDirectory(appResourcesDirectoryPath).wait(); + } + + var files = helpers.enumerateFilesInDirectorySync(path.join(assetsDirectoryPath, ProjectService.APP_FOLDER_NAME)); + var platformsAsString = platforms.join("|"); + + _.each(files, fileName => { + var platformInfo = ProjectService.parsePlatformSpecificFileName(path.basename(fileName), platformsAsString); + var shouldExcludeFile = platformInfo && platformInfo.platform !== platform; + if(shouldExcludeFile) { + this.$fs.deleteFile(fileName).wait(); + } else if(platformInfo && platformInfo.onDeviceName) { + this.$fs.rename(fileName, path.join(path.dirname(fileName), platformInfo.onDeviceName)).wait(); + } + }); + + }).future()(); + } + + private static parsePlatformSpecificFileName(fileName: string, platforms: string): any { + var regex = util.format("^(.+?)\.(%s)(\..+?)$", platforms); + var parsed = fileName.toLowerCase().match(new RegExp(regex, "i")); + if (parsed) { + return { + platform: parsed[2], + onDeviceName: parsed[1] + parsed[3] + }; + } + return undefined; + } + + public buildProject(platform: string): IFuture { + return (() => { + this.executePlatformSpecificAction(platform, "buildProject").wait(); + }).future()(); + } + + private executePlatformSpecificAction(platform, functionName: string): IFuture { + return (() => { + var platformProjectService = null; + switch (platform) { + case "android": + platformProjectService = this.$androidProjectService; + break; + case "ios": + platformProjectService = this.$iOSProjectService; + break; + } + + this.executeFunctionByName(functionName, platformProjectService, [this.projectData]).wait(); }).future()(); } + private executeFunctionByName(functionName, context , args: any[] ): IFuture { + return (() => { + var namespaces = functionName.split("."); + var func = namespaces.pop(); + for (var i = 0; i < namespaces.length; i++) { + context = context[namespaces[i]]; + } + return context[func].apply(context, args).wait(); + }).future()(); + } + private getCustomAppPath(): string { var customAppPath = options["copy-from"] || options["link-to"]; if(customAppPath) { @@ -100,5 +218,226 @@ export class ProjectService implements IProjectService { return customAppPath; } + + public ensureProject() { + if (this.projectData.projectDir === "" || !this.$fs.exists(this.projectData.projectFilePath).wait()) { + this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", process.cwd()); + } + } +} +$injector.register("projectService", ProjectService); + +class AndroidProjectService implements IPlatformProjectService { + private frameworkDir: string = null; + + constructor(private $fs: IFileSystem, + private $errors: IErrors, + private $logger: ILogger, + private $childProcess: IChildProcess, + private $projectTemplatesService: IProjectTemplatesService, + private $propertiesParser: IPropertiesParser) { } + + public createProject(projectData: IProjectData): IFuture { + return (() => { + this.frameworkDir = this.getFrameworkDir(projectData).wait(); + + var packageName = projectData.projectId; + var packageAsPath = packageName.replace(/\./g, path.sep); + var projectDir = path.join(projectData.projectDir, "platforms", "android"); + + this.validatePackageName(packageName); + this.validateProjectName(projectData.projectName); + + this.checkRequirements().wait(); + + var targetApi = this.getTarget().wait(); + + // Log the values for project + this.$logger.trace("Creating NativeScript project for the Android platform"); + this.$logger.trace("Path: %s", projectDir); + this.$logger.trace("Package: %s", projectData.projectId); + this.$logger.trace("Name: %s", projectData.projectName); + this.$logger.trace("Android target: %s", targetApi); + + this.$logger.out("Copying template files..."); + + shell.cp("-r", path.join(this.frameworkDir, "assets"), projectDir); + shell.cp("-r", path.join(this.frameworkDir, "gen"), projectDir); + shell.cp("-r", path.join(this.frameworkDir, "libs"), projectDir); + shell.cp("-r", path.join(this.frameworkDir, "res"), projectDir); + + shell.cp("-f", path.join(this.frameworkDir, ".project"), projectDir); + shell.cp("-f", path.join(this.frameworkDir, "AndroidManifest.xml"), projectDir); + + // Create src folder + var activityDir = path.join(projectDir, 'src', packageAsPath); + this.$fs.createDirectory(activityDir).wait(); + + this.$fs.deleteDirectory(path.join(projectData.platformsDir, "android", "node_modules")).wait(); + + // Interpolate the activity name and package + var stringsFilePath = path.join(projectDir, 'res', 'values', 'strings.xml'); + shell.sed('-i', /__NAME__/, projectData.projectName, stringsFilePath); + shell.sed('-i', /__TITLE_ACTIVITY__/, projectData.projectName, stringsFilePath); + shell.sed('-i', /__NAME__/, projectData.projectName, path.join(projectDir, '.project')); + shell.sed('-i', /__PACKAGE__/, packageName, path.join(projectDir, "AndroidManifest.xml")); + + this.runAndroidUpdate(projectDir, targetApi).wait(); + + this.$logger.out("Project successfully created."); + + }).future()(); + } + + public buildProject(projectData: IProjectData): IFuture { + return (() => { + var projectRoot = path.join(projectData.platformsDir, "android"); + var buildConfiguration = options.release || "--debug"; + var args = this.getAntArgs(buildConfiguration, projectRoot); + + switch(buildConfiguration) { + case "--debug": + args[0] = "debug"; + break; + case "--release": + args[0] = "release"; + break; + default: + this.$errors.fail("Build option %s not recognized", buildConfiguration); + } + + this.spawn('ant', args); + + }).future()(); + } + + 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 { + return (() => { + this.$childProcess.exec("android update project --subprojects --path " + projectPath + " --target " + targetApi).wait(); + }).future()(); + } + + 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 getFrameworkDir(projectData: IProjectData): IFuture { + return(() => { + var androidFrameworkPath = this.$projectTemplatesService.installAndroidFramework(path.join(projectData.platformsDir, "android")).wait(); + return path.join(androidFrameworkPath, "framework"); + }).future()(); + } + + private getTarget(): IFuture { + return (() => { + var projectPropertiesFilePath = path.join(this.frameworkDir, "project.properties"); + + if (this.$fs.exists(projectPropertiesFilePath).wait()) { + var properties = this.$propertiesParser.createEditor(projectPropertiesFilePath).wait(); + return properties.get("target"); + } + + return ""; + }).future()(); + } + + private checkAnt(): IFuture { + 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.") + } + return true; + }).future()(); + } + + private checkJava(): IFuture { + 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); + } + return true; + }).future()(); + } + + private checkAndroid(): IFuture { + return (() => { + var validTarget = this.getTarget().wait(); + try { + var output = 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"); + } + } + + 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]); + } + + return true; + }).future()(); + } + + private checkRequirements(): IFuture { + return (() => { + return this.checkAnt().wait() && this.checkAndroid().wait() && this.checkJava().wait(); + }).future()(); + } + } +$injector.register("androidProjectService", AndroidProjectService); + +class iOSProjectService implements IPlatformProjectService { + public createProject(projectData: IProjectData): IFuture { + return (() => { + + }).future()(); + } + + public buildProject(projectData: IProjectData): IFuture { + return (() => { + + }).future()(); + } } -$injector.register("projectService", ProjectService); \ No newline at end of file +$injector.register("iOSProjectService", iOSProjectService); \ No newline at end of file diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index 4ec6dbbec4..6e142c49cb 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -12,25 +12,33 @@ export class ProjectTemplatesService implements IProjectTemplatesService { private static NPM_DEFAULT_TEMPLATE_NAME = "tns-template-hello-world"; private static NPM_LOAD_FAILED = "Failed to retrieve nativescript hello world application. Please try again a little bit later."; + private static NPM_ANDROID_BRIDGE_NAME = "tns-android"; + public constructor(private $errors: IErrors, private $logger: ILogger, private $npm: INodePackageManager) { } public get defaultTemplatePath(): IFuture { - return this.getDefaultTemplatePath(); + return this.installNpmPackage(ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME); + } + + public installAndroidFramework(where?: string): IFuture { + return this.installNpmPackage(ProjectTemplatesService.NPM_ANDROID_BRIDGE_NAME, where); } - private getDefaultTemplatePath(): IFuture { + private installNpmPackage(packageName: string, where?: string): IFuture { return (() => { try { this.$npm.load().wait(); - this.$npm.install(npm.cache, ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME).wait(); + var location = where || npm.cache; + this.$npm.install(location, packageName).wait(); } catch (error) { this.$logger.debug(error); this.$errors.fail(ProjectTemplatesService.NPM_LOAD_FAILED); } - return path.join(npm.cache, "node_modules", ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME); + return path.join(location, "node_modules", packageName); + }).future()(); } } diff --git a/package.json b/package.json index a71e0f49d6..5f38af64b9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "underscore": "1.5.2", "unzip": "0.1.9", "yargs": "1.2.2", - "npm": "1.4.10" + "npm": "1.4.10", + "properties-parser": "0.2.3" }, "analyze": true, "devDependencies": {