diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 7cf338d9b1..e7084481c1 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -2,10 +2,14 @@ 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.requireCommand("create", "./commands/create-project"); diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 0000000000..804e955644 --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1,8 @@ +/// + +export var APP_FOLDER_NAME = "app"; +export var DEFAULT_PROJECT_ID = "com.telerik.tns.HelloWorld"; +export var DEFAULT_PROJECT_NAME = "HelloNativescript"; +export var APP_RESOURCES_FOLDER_NAME = "App_Resources"; +export var PROJECT_FRAMEWORK_FOLDER_NAME = "framework"; + diff --git a/lib/declarations.ts b/lib/declarations.ts index ea88f0bcdd..55adf5b3db 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -1,7 +1,7 @@ interface INodePackageManager { cache: string; load(config?: any): IFuture; - install(where: string, what: string): IFuture; + install(packageName: string, pathToSave?: string): IFuture; } interface IPropertiesParser { diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 0a774170bb..a0fce8fbe9 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -7,6 +7,16 @@ interface IPlatformService { buildPlatform(platform: string): IFuture; } -interface IPlatformCapabilities { +interface IPlatformData { + frameworkPackageName: string; + platformProjectService: IPlatformProjectService; + projectRoot: string; + normalizedPlatformName: string; targetedOS?: string[]; -} \ No newline at end of file +} + +interface IPlatformsData { + platformsNames: string[]; + getPlatformData(platform: string): IPlatformData; +} + diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index a0eb4d8cfb..ed7d44936b 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -1,26 +1,25 @@ 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; + projectName: string; platformsDir: string; projectFilePath: string; projectId?: string; - projectName?: string; } interface IProjectTemplatesService { defaultTemplatePath: IFuture; - installAndroidFramework(whereToInstall: string): IFuture +} + +interface IPlatformProjectService { + validate(): IFuture; + createProject(projectRoot: string, frameworkDir: string): IFuture; + interpolateData(projectRoot: string): void; + afterCreateProject(projectRoot: string): void; + prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture; + buildProject(projectRoot: string): IFuture; } \ No newline at end of file diff --git a/lib/definitions/shelljs.d.ts b/lib/definitions/shelljs.d.ts index 0b84163555..518e2063e2 100644 --- a/lib/definitions/shelljs.d.ts +++ b/lib/definitions/shelljs.d.ts @@ -1,5 +1,6 @@ declare module "shelljs" { function cp(arg: string, sourcePath: string, destinationPath: string): void; + 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; diff --git a/lib/node-package-manager.ts b/lib/node-package-manager.ts index 1f0c815e71..a13132516e 100644 --- a/lib/node-package-manager.ts +++ b/lib/node-package-manager.ts @@ -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; } @@ -21,7 +27,21 @@ export class NodePackageManager implements INodePackageManager { return future; } - public install(where: string, what: string): IFuture { + public install(packageName: string, pathToSave?: string): IFuture { + return (() => { + var action = (packageName: string) => { + pathToSave = pathToSave || npm.cache; + this.installCore(pathToSave, packageName).wait(); + }; + + this.tryExecuteAction(action, packageName).wait(); + + return path.join(pathToSave, "node_modules", packageName); + + }).future()(); + } + + private installCore(where: string, what: string): IFuture { var future = new Future(); npm.commands["install"](where, what, (err, data) => { if(err) { @@ -32,5 +52,17 @@ export class NodePackageManager implements INodePackageManager { }); return future; } + + private tryExecuteAction(action: (...args: any[]) => void, ...args: any[]): IFuture { + 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()(); + } } $injector.register("npm", NodePackageManager); diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts new file mode 100644 index 0000000000..d289d7cf07 --- /dev/null +++ b/lib/services/android-project-service.ts @@ -0,0 +1,212 @@ +/// +import path = require("path"); +import shell = require("shelljs"); +import util = require("util"); +import options = require("./../options"); +import helpers = require("./../common/helpers"); +import constants = require("./../constants"); + +class AndroidProjectService implements IPlatformProjectService { + constructor(private $fs: IFileSystem, + private $errors: IErrors, + private $logger: ILogger, + private $childProcess: IChildProcess, + private $projectData: IProjectData, + private $propertiesParser: IPropertiesParser) { } + + public validate(): IFuture { + return (() => { + this.validatePackageName(this.$projectData.projectId); + this.validateProjectName(this.$projectData.projectName); + + this.checkAnt().wait() && this.checkAndroid().wait() && this.checkJava().wait(); + }).future()(); + } + + public createProject(projectRoot: string, frameworkDir: string): IFuture { + return (() => { + this.validateAndroidTarget(frameworkDir); // We need framework to be installed to validate android target so we can't call this method in validate() + + var paths = "assets gen libs res".split(' ').map(p => path.join(frameworkDir, p)); + shell.cp("-r", paths, projectRoot); + + paths = ".project AndroidManifest.xml project.properties".split(' ').map(p => path.join(frameworkDir, p)); + shell.cp("-f", paths, projectRoot); + + // Create src folder + var packageName = this.$projectData.projectId; + var packageAsPath = packageName.replace(/\./g, path.sep); + var activityDir = path.join(projectRoot, 'src', packageAsPath); + this.$fs.createDirectory(activityDir).wait(); + + }).future()(); + } + + 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 afterCreateProject(projectRoot: string) { + var targetApi = this.getTarget(projectRoot).wait(); + this.$logger.trace("Android target: %s", targetApi); + this.runAndroidUpdate(projectRoot, targetApi).wait(); + } + + 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, constants.APP_FOLDER_NAME, constants.APP_RESOURCES_FOLDER_NAME); + shell.cp("-r", path.join(this.$projectData.projectDir, constants.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, constants.APP_FOLDER_NAME)); + var platformsAsString = platforms.join("|"); + + _.each(files, fileName => { + var platformInfo = AndroidProjectService.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()(); + } + + public buildProject(projectRoot: string): IFuture { + return (() => { + var buildConfiguration = options.release ? "release" : "debug"; + var args = this.getAntArgs(buildConfiguration, projectRoot); + 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 (() => { + var args = [ + "--path", projectPath, + "--target", targetApi + ]; + + this.spawn("android update project", args); + }).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 validateAndroidTarget(frameworkDir: string) { + var validTarget = this.getTarget(frameworkDir).wait(); + 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]); + } + } + + private getTarget(projectRoot: string): IFuture { + 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()(); + } + + 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.") + } + }).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); + } + }).future()(); + } + + private checkAndroid(): IFuture { + 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()(); + } + + 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; + } +} +$injector.register("androidProjectService", AndroidProjectService); \ No newline at end of file diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts new file mode 100644 index 0000000000..6aacd045b7 --- /dev/null +++ b/lib/services/ios-project-service.ts @@ -0,0 +1,35 @@ +/// + +class IOSProjectService implements IPlatformProjectService { + public validate(): IFuture { + return (() => { + }).future()(); + } + + public interpolateData(): void { + + } + + public afterCreateProject(): void { + + } + + public createProject(): IFuture { + return (() => { + + }).future()(); + } + + public prepareProject(normalizedPlatformName: string, platforms: string[]): IFuture { + return (() => { + + }).future()(); + } + + public buildProject(): IFuture { + return (() => { + + }).future()(); + } +} +$injector.register("iOSProjectService", IOSProjectService); \ No newline at end of file diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 39214865dc..065b99f6b1 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -1,28 +1,53 @@ /// import path = require("path"); +import shell = require("shelljs"); import util = require("util"); +import constants = require("./../constants"); import helpers = require("./../common/helpers"); -export class PlatformService implements IPlatformService { - private platformCapabilities: { [key: string]: IPlatformCapabilities } = { - ios: { - targetedOS: ['darwin'] - }, - android: { +class PlatformsData implements IPlatformsData { + private platformsData = {}; + + constructor($projectData: IProjectData, + $androidProjectService: IPlatformProjectService, + $iOSProjectService: IPlatformProjectService) { + + this.platformsData = { + ios: { + frameworkPackageName: "tns-ios", + normalizedPlatformName: "iOS", + platformProjectService: $iOSProjectService, + projectRoot: "", + targetedOS: ['darwin'] + }, + android: { + frameworkPackageName: "tns-android", + normalizedPlatformName: "Android", + platformProjectService: $androidProjectService, + projectRoot: path.join($projectData.platformsDir, "android") + } } - }; + } - private platformNames = []; + public get platformsNames() { + return Object.keys(this.platformsData); + } - constructor(private $errors: IErrors, - private $fs: IFileSystem, - private $projectService: IProjectService) { - this.platformNames = Object.keys(this.platformCapabilities); + public getPlatformData(platform): IPlatformData { + return this.platformsData[platform]; } +} +$injector.register("platformsData", PlatformsData); - public getCapabilities(platform: string): IPlatformCapabilities { - return this.platformCapabilities[platform]; +export class PlatformService implements IPlatformService { + constructor(private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $npm: INodePackageManager, + private $projectService: IProjectService, + private $projectData: IProjectData, + private $platformsData: IPlatformsData) { } public addPlatforms(platforms: string[]): IFuture { @@ -33,7 +58,7 @@ export class PlatformService implements IPlatformService { this.$projectService.ensureProject(); - var platformsDir = this.$projectService.projectData.platformsDir; + var platformsDir = this.$projectData.platformsDir; this.$fs.ensureDirectoryExists(platformsDir).wait(); _.each(platforms, platform => { @@ -49,7 +74,7 @@ export class PlatformService implements IPlatformService { this.validatePlatform(platform); - var platformPath = path.join(this.$projectService.projectData.platformsDir, platform); + var platformPath = path.join(this.$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 @@ -58,44 +83,66 @@ export class PlatformService implements IPlatformService { } // Copy platform specific files in platforms dir - this.$projectService.createPlatformSpecificProject(platform).wait(); + var platformData = this.$platformsData.getPlatformData(platform); + var platformProjectService = platformData.platformProjectService; + + platformProjectService.validate().wait(); + + // Log the values for project + this.$logger.trace("Creating NativeScript project for the %s platform", platform); + this.$logger.trace("Path: %s", platformData.projectRoot); + this.$logger.trace("Package: %s", this.$projectData.projectId); + this.$logger.trace("Name: %s", this.$projectData.projectName); + + this.$logger.out("Copying template files..."); + + // get path to downloaded framework package + var frameworkDir = this.$npm.install(this.$platformsData.getPlatformData(platform).frameworkPackageName, + path.join(this.$projectData.platformsDir, platform)).wait(); + frameworkDir = path.join(frameworkDir, constants.PROJECT_FRAMEWORK_FOLDER_NAME); + + platformProjectService.createProject(platformData.projectRoot, frameworkDir).wait(); + + // Need to remove unneeded node_modules folder + this.$fs.deleteDirectory(path.join("../", frameworkDir)).wait(); + + platformProjectService.interpolateData(platformData.projectRoot); + platformProjectService.afterCreateProject(platformData.projectRoot); + + this.$logger.out("Project successfully created."); }).future()(); } public getInstalledPlatforms(): IFuture { return(() => { - if(!this.$fs.exists(this.$projectService.projectData.platformsDir).wait()) { + if(!this.$fs.exists(this.$projectData.platformsDir).wait()) { return []; } - var subDirs = this.$fs.readDirectory(this.$projectService.projectData.platformsDir).wait(); - return _.filter(subDirs, p => { return this.platformNames.indexOf(p) > -1; }); + var subDirs = this.$fs.readDirectory(this.$projectData.platformsDir).wait(); + return _.filter(subDirs, p => this.$platformsData.platformsNames.indexOf(p) > -1); }).future()(); } public getAvailablePlatforms(): IFuture { return (() => { var installedPlatforms = this.getInstalledPlatforms().wait(); - return _.filter(this.platformNames, p => { + return _.filter(this.$platformsData.platformsNames, 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(); + var platformData = this.$platformsData.getPlatformData(platform); + var platformProjectService = platformData.platformProjectService; + + platformProjectService.prepareProject(platformData.normalizedPlatformName, this.$platformsData.platformsNames).wait(); }).future()(); } @@ -104,13 +151,21 @@ export class PlatformService implements IPlatformService { platform = platform.toLocaleLowerCase(); this.validatePlatform(platform); - this.$projectService.buildProject(platform).wait(); + var platformData = this.$platformsData.getPlatformData(platform); + platformData.platformProjectService.buildProject(platformData.projectRoot).wait(); + this.$logger.out("Project successfully built"); + }).future()(); + } + + public runPlatform(platform: string): IFuture { + return (() => { + }).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)); + this.$errors.fail("Invalid platform %s. Valid platforms are %s.", platform, helpers.formatListOfNames(this.$platformsData.platformsNames)); } if (!this.isPlatformSupportedForOS(platform)) { @@ -119,12 +174,11 @@ export class PlatformService implements IPlatformService { } private isValidPlatform(platform: string) { - return this.platformCapabilities[platform]; + return this.$platformsData.getPlatformData(platform); } private isPlatformSupportedForOS(platform: string): boolean { - var platformCapabilities = this.getCapabilities(platform); - var targetedOS = platformCapabilities.targetedOS; + var targetedOS = this.$platformsData.getPlatformData(platform).targetedOS; if(!targetedOS || targetedOS.indexOf("*") >= 0 || targetedOS.indexOf(process.platform) >= 0) { return true; @@ -132,16 +186,5 @@ export class PlatformService implements IPlatformService { 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 +$injector.register("platformService", PlatformService); diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 1aee2c152e..8d709b1a2b 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -1,62 +1,61 @@ /// -import path = require("path"); import options = require("./../options"); -import shell = require("shelljs"); import osenv = require("osenv"); +import path = require("path"); +import shell = require("shelljs"); import util = require("util"); +import constants = require("./../constants"); import helpers = require("./../common/helpers"); -export class ProjectService implements IProjectService { - 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"; +class ProjectData implements IProjectData { + public projectDir: string; + public platformsDir: string; + public projectFilePath: string; + public projectId: string; + public projectName: string; - public projectData: IProjectData = null; - - constructor(private $logger: ILogger, - private $errors: IErrors, - private $fs: IFileSystem, - private $projectTemplatesService: IProjectTemplatesService, - private $androidProjectService: IPlatformProjectService, - private $iOSProjectService: IPlatformProjectService, + constructor(private $fs: IFileSystem, private $projectHelper: IProjectHelper, private $config) { - this.projectData = this.getProjectData().wait(); + this.initializeProjectData().wait(); } - private getProjectData(): IFuture { + private initializeProjectData(): 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); + this.projectDir = projectDir; + this.projectName = path.basename(projectDir); + this.platformsDir = path.join(projectDir, "platforms"); + this.projectFilePath = path.join(projectDir, this.$config.PROJECT_FILE_NAME); + + if (this.$fs.exists(this.projectFilePath).wait()) { + var fileContent = this.$fs.readJson(this.projectFilePath).wait(); + this.projectId = fileContent.id; } } - return projectData; - }).future()(); + }).future()(); } +} +$injector.register("projectData", ProjectData); + +export class ProjectService implements IProjectService { + constructor(private $logger: ILogger, + private $errors: IErrors, + private $fs: IFileSystem, + private $projectTemplatesService: IProjectTemplatesService, + private $projectData: IProjectData, + private $config) { } public createProject(projectName: string, projectId: string): IFuture { return(() => { var projectDir = path.resolve(options.path || "."); - projectId = projectId || ProjectService.DEFAULT_PROJECT_ID; - projectName = projectName || ProjectService.DEFAULT_PROJECT_NAME; + projectId = projectId || constants.DEFAULT_PROJECT_ID; + projectName = projectName || constants.DEFAULT_PROJECT_NAME; projectDir = path.join(projectDir, projectName); this.$fs.createDirectory(projectDir).wait(); @@ -72,7 +71,7 @@ export class ProjectService implements IProjectService { 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 appDirectory = path.join(projectDir, constants.APP_FOLDER_NAME); var appPath: string = null; if(customAppPath) { @@ -106,7 +105,7 @@ export class ProjectService implements IProjectService { if(symlink) { // TODO: Implement support for symlink the app folder instead of copying } else { - var appDir = path.join(projectDir, ProjectService.APP_FOLDER_NAME); + var appDir = path.join(projectDir, constants.APP_FOLDER_NAME); this.$fs.createDirectory(appDir).wait(); shell.cp('-R', path.join(appPath, "*"), appDir); } @@ -125,89 +124,10 @@ export class ProjectService implements IProjectService { }).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) { - if(customAppPath.indexOf("http") >= 0) { + if(customAppPath.indexOf("http://") === 0) { this.$errors.fail("Only local paths for custom app are supported."); } @@ -220,224 +140,10 @@ export class ProjectService implements IProjectService { } public ensureProject() { - if (this.projectData.projectDir === "" || !this.$fs.exists(this.projectData.projectFilePath).wait()) { + 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("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 6e142c49cb..e70bfe69f0 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -10,36 +10,11 @@ import Future = require("fibers/future"); 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 constructor(private $npm: INodePackageManager) { } public get defaultTemplatePath(): IFuture { - return this.installNpmPackage(ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME); - } - - public installAndroidFramework(where?: string): IFuture { - return this.installNpmPackage(ProjectTemplatesService.NPM_ANDROID_BRIDGE_NAME, where); - } - - private installNpmPackage(packageName: string, where?: string): IFuture { - return (() => { - try { - this.$npm.load().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(location, "node_modules", packageName); - - }).future()(); + return this.$npm.install(ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME); } } $injector.register("projectTemplatesService", ProjectTemplatesService); \ No newline at end of file diff --git a/package.json b/package.json index 5f38af64b9..bdc41a8f67 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,11 @@ "unzip": "0.1.9", "yargs": "1.2.2", "npm": "1.4.10", - "properties-parser": "0.2.3" + "properties-parser": "0.2.3", + "watchr": "2.4.11", + "rimraf": "2.2.6", + "mkdirp": "0.3.5", + "shelljs": "0.3.0" }, "analyze": true, "devDependencies": {