diff --git a/.gitignore b/.gitignore index 69dce8499d..73461007f0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,15 +23,8 @@ pids logs results scratch/ -.idea/workspace.xml -.idea/tasks.xml -.idea/watcherTasks.xml - +.idea/ test-reports.xml npm-debug.log node_modules -resources/App_Resources -resources/Cordova -resources/ItemTemplates -resources/ProjectTemplates diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index e7a290a099..8cc63b833d 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -1,4 +1,10 @@ -global._ = require("underscore"); -global.$injector = require("./common/lib/yok").injector; +require("./common/bootstrap"); $injector.require("nativescript-cli", "./nativescript-cli"); + +$injector.require("projectService", "./services/project-service"); +$injector.require("projectTemplatesService", "./services/project-templates-service"); + +$injector.requireCommand("create", "./commands/create-project-command"); + +$injector.require("npm", "./node-package-manager"); diff --git a/lib/commands/create-project-command.ts b/lib/commands/create-project-command.ts new file mode 100644 index 0000000000..4fe0df3b2c --- /dev/null +++ b/lib/commands/create-project-command.ts @@ -0,0 +1,12 @@ +/// + +export class CreateProjectCommand implements ICommand { + constructor(private $projectService: IProjectService) { } + + execute(args: string[]): IFuture { + return (() => { + this.$projectService.createProject(args[0], args[1]).wait(); + }).future()(); + } +} +$injector.registerCommand("create", CreateProjectCommand); diff --git a/lib/common b/lib/common index a4294787f9..33c883f1ab 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit a4294787f9ce49b5de608ed02cfc08124594ab32 +Subproject commit 33c883f1abae2a5e5486a87f11df1d75e00e1dc8 diff --git a/lib/declarations.ts b/lib/declarations.ts new file mode 100644 index 0000000000..f8bbfab6e1 --- /dev/null +++ b/lib/declarations.ts @@ -0,0 +1,5 @@ +interface INodePackageManager { + cache: string; + load(config?: any): IFuture; + install(where: string, what: string): IFuture; +} \ No newline at end of file diff --git a/lib/definitions/npm.d.ts b/lib/definitions/npm.d.ts new file mode 100644 index 0000000000..ce70940eab --- /dev/null +++ b/lib/definitions/npm.d.ts @@ -0,0 +1,5 @@ + declare module "npm" { + var cache: string; + var commands: any[]; + function load(config: Object, callback: (err: any, data: any) => void); +} \ No newline at end of file diff --git a/lib/definitions/osenv.d.ts b/lib/definitions/osenv.d.ts new file mode 100644 index 0000000000..e02de9be32 --- /dev/null +++ b/lib/definitions/osenv.d.ts @@ -0,0 +1,3 @@ +declare module "osenv" { + function home(); +} \ No newline at end of file diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts new file mode 100644 index 0000000000..20934af393 --- /dev/null +++ b/lib/definitions/project.d.ts @@ -0,0 +1,7 @@ +interface IProjectService { + createProject(projectName: string, projectId: string): IFuture; +} + +interface IProjectTemplatesService { + defaultTemplatePath: IFuture; +} \ No newline at end of file diff --git a/lib/definitions/shelljs.d.ts b/lib/definitions/shelljs.d.ts new file mode 100644 index 0000000000..99ea4dc36e --- /dev/null +++ b/lib/definitions/shelljs.d.ts @@ -0,0 +1,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); +} \ No newline at end of file diff --git a/lib/nativescript-cli.ts b/lib/nativescript-cli.ts index bbb1df0206..e2600aa613 100644 --- a/lib/nativescript-cli.ts +++ b/lib/nativescript-cli.ts @@ -1,10 +1,16 @@ /// import Fiber = require("fibers"); +import Future = require("fibers/future"); +import path = require("path"); require("./bootstrap"); +require("./options"); -var fiber = Fiber(() => { -}); -global.__main_fiber__ = fiber; // leak fiber to prevent it from being GC'd and thus corrupting V8 -fiber.run(); \ No newline at end of file +import errors = require("./common/errors"); +errors.installUncaughtExceptionListener(); + +$injector.register("config", {"CI_LOGGER": false}); + +var dispatcher = $injector.resolve("dispatcher"); +dispatcher.runMainFiber(); diff --git a/lib/node-package-manager.ts b/lib/node-package-manager.ts new file mode 100644 index 0000000000..4a12295114 --- /dev/null +++ b/lib/node-package-manager.ts @@ -0,0 +1,36 @@ +/// + +import npm = require("npm"); +import Future = require("fibers/future"); +import shell = require("shelljs"); + +export class NodePackageManager implements INodePackageManager { + public get cache(): string { + return npm.cache; + } + + public load(config?: any): IFuture { + var future = new Future(); + npm.load(config, (err) => { + if(err) { + future.throw(err); + } else { + future.return(); + } + }); + return future; + } + + public install(where: string, what: string): IFuture { + var future = new Future(); + npm.commands["install"](where, what, (err, data) => { + if(err) { + future.throw(err); + } else { + future.return(data); + } + }); + return future; + } +} +$injector.register("npm", NodePackageManager); \ No newline at end of file diff --git a/lib/options.ts b/lib/options.ts new file mode 100644 index 0000000000..56a48f873d --- /dev/null +++ b/lib/options.ts @@ -0,0 +1,29 @@ +/// + +import path = require("path"); +import helpers = require("./common/helpers"); +import osenv = require("osenv"); + +var knownOpts:any = { + "log" : String, + "verbose" : Boolean, + "path" : String, + "copy-from": String, + "link-to": String, + "version": Boolean, + "help": Boolean + }, + shorthands = { + "v" : "verbose", + "p" : "path" + }; + +var defaultProfileDir = path.join(osenv.home(), ".nativescript-cli"); +var parsed = helpers.getParsedOptions(knownOpts, shorthands, defaultProfileDir); + +Object.keys(parsed).forEach((opt) => exports[opt] = parsed[opt]); + +exports.knownOpts = knownOpts; + +declare var exports:any; +export = exports; \ No newline at end of file diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts new file mode 100644 index 0000000000..5511e20d97 --- /dev/null +++ b/lib/services/project-service.ts @@ -0,0 +1,104 @@ +/// + +import path = require("path"); +import options = require("./../options"); +import shell = require("shelljs"); +import osenv = require("osenv"); + +export class ProjectService implements IProjectService { + private static DEFAULT_ID = "com.telerik.tns.HelloWorld"; + private static DEFAULT_NAME = "HelloNativescript"; + private static APP_FOLDER_NAME = "app"; + + constructor(private $logger: ILogger, + private $errors: IErrors, + private $fs: IFileSystem, + private $projectTemplatesService: IProjectTemplatesService) { } + + public createProject(projectName: string, projectId: string): IFuture { + return(() => { + var projectDir = path.resolve(options.path || "."); + + projectId = projectId || ProjectService.DEFAULT_ID; + projectName = projectName || ProjectService.DEFAULT_NAME; + + projectDir = path.join(projectDir, projectName); + this.$fs.createDirectory(projectDir).wait(); + + var customAppPath = this.getCustomAppPath(); + if(customAppPath) { + customAppPath = path.resolve(customAppPath); + } + + if(this.$fs.exists(projectDir).wait() && !this.$fs.isEmptyDir(projectDir).wait()) { + 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); + + var appDirectory = path.join(projectDir, ProjectService.APP_FOLDER_NAME); + var appPath: string = null; + + if(customAppPath) { + this.$logger.trace("Using custom app from %s", customAppPath); + + // Make sure that the source app/ is not a direct ancestor of a target app/ + var relativePathFromSourceToTarget = path.relative(customAppPath, appDirectory); + var doesRelativePathGoUpAtLeastOneDir = relativePathFromSourceToTarget.split(path.sep)[0] == ".."; + if(!doesRelativePathGoUpAtLeastOneDir) { + this.$errors.fail("Project dir %s must not be created at/inside the template used to create the project %s.", projectDir, customAppPath); + } + this.$logger.trace("Copying custom app into %s", appDirectory); + appPath = customAppPath; + } else { + // No custom app - use nativescript hello world application + this.$logger.trace("Using NativeScript hello world application"); + var defaultTemplatePath = this.$projectTemplatesService.defaultTemplatePath.wait(); + this.$logger.trace("Copying Nativescript hello world application into %s", appDirectory); + appPath = defaultTemplatePath; + } + + this.createProjectCore(projectDir, appPath, false).wait(); + }).future()(); + } + + private createProjectCore(projectDir: string, appPath: string, symlink?: boolean): IFuture { + return (() => { + if(!this.$fs.exists(projectDir).wait()) { + this.$fs.createDirectory(projectDir).wait(); + } + if(symlink) { + // TODO: Implement support for symlink the app folder instead of copying + } else { + var appDir = path.join(projectDir, ProjectService.APP_FOLDER_NAME); + this.$fs.createDirectory(appDir).wait(); + shell.cp('-R', path.join(appPath, "*"), appDir); + } + this.createBasicProjectStructure(projectDir).wait(); + }).future()(); + } + + private createBasicProjectStructure(projectDir: 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(); + }).future()(); + } + + private getCustomAppPath(): string { + var customAppPath = options["copy-from"] || options["link-to"]; + if(customAppPath) { + if(customAppPath.indexOf("http") >= 0) { + this.$errors.fail("Only local paths for custom app are supported."); + } + + if(customAppPath.substr(0, 1) === '~') { + customAppPath = path.join(osenv.home(), customAppPath.substr(1)); + } + } + + return customAppPath; + } +} +$injector.register("projectService", ProjectService); \ No newline at end of file diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts new file mode 100644 index 0000000000..4ec6dbbec4 --- /dev/null +++ b/lib/services/project-templates-service.ts @@ -0,0 +1,37 @@ +/// + +import util = require("util"); +import path = require("path"); +import shell = require("shelljs"); +import npm = require("npm"); +var options = require("./../options"); +var helpers = require("./../common/helpers"); +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."; + + public constructor(private $errors: IErrors, + private $logger: ILogger, + private $npm: INodePackageManager) { } + + public get defaultTemplatePath(): IFuture { + return this.getDefaultTemplatePath(); + } + + private getDefaultTemplatePath(): IFuture { + return (() => { + try { + this.$npm.load().wait(); + this.$npm.install(npm.cache, ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME).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); + }).future()(); + } +} +$injector.register("projectTemplatesService", ProjectTemplatesService); \ No newline at end of file diff --git a/package.json b/package.json index 7723e21b9b..a71e0f49d6 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,15 @@ ], "dependencies": { "fibers": "https://github.com/icenium/node-fibers/tarball/master", + "filesize": "2.0.3", + "progress-stream": "0.5.0", "log4js": "0.6.9", + "osenv": "0.1.0", "tabtab": "https://github.com/tailsu/node-tabtab/tarball/master", "underscore": "1.5.2", "unzip": "0.1.9", - "yargs": "1.2.2" + "yargs": "1.2.2", + "npm": "1.4.10" }, "analyze": true, "devDependencies": {