From 16f13b7b783b02018e63f7368845cf1de065fc28 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Mon, 16 Nov 2015 21:22:00 +0200 Subject: [PATCH 1/2] Add support for different templates Add support for --template option when creating new project. The value for `--template` can be anything that you can `npm install`. For example valid calls are: * `tns create app1 --template tns-template-hello-world` * `tns create app1 --template https://github.com/NativeScript/template-hello-world-ts/tarball/master` * `tns create app1 --template ../myTemplate` In case you use: `tns create app1 --template typescript` or `tns create app1 --template tsc`, CLI will try to install `tns-template-hello-world-ts` template. In case you use `tns create app1 --template typescript@1.2.0` we will install version 1.2.0 of `tns-template-hello-world-ts`. When a custom template is used, CLI will extend its App_Resources with the ones from default template in case any of them is missing. Update npm version in order to support .git urls for --template option. --- lib/commands/create-project.ts | 5 +- lib/common | 2 +- lib/definitions/project.d.ts | 18 ++- lib/npm-installation-manager.ts | 3 +- lib/services/project-service.ts | 63 +++++++-- lib/services/project-templates-service.ts | 94 ++++++++++++- package.json | 2 +- test/project-service.ts | 155 +++++++++++++++++++--- test/project-templates-service.ts | 134 +++++++++++++++++++ test/stubs.ts | 6 +- 10 files changed, 447 insertions(+), 35 deletions(-) create mode 100644 test/project-templates-service.ts diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index 44c297fa8d..14683a192b 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -26,13 +26,14 @@ export class CreateProjectCommand implements ICommand { constructor(private $projectService: IProjectService, private $errors: IErrors, private $logger: ILogger, - private $projectNameValidator: IProjectNameValidator) { } + private $projectNameValidator: IProjectNameValidator, + private $options: ICommonOptions) { } public enableHooks = false; execute(args: string[]): IFuture { return (() => { - this.$projectService.createProject(args[0]).wait(); + this.$projectService.createProject(args[0], this.$options.template).wait(); }).future()(); } diff --git a/lib/common b/lib/common index 66516b388a..ed9af3330c 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 66516b388a771dc24018d56367aa5c6f1cbe945d +Subproject commit ed9af3330c8c340faf217376d6f6d51916d0bf36 diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index df3d0022ab..825a99573d 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -1,6 +1,6 @@ interface IProjectService { - createProject(projectName: string): IFuture; + createProject(projectName: string, selectedTemplate?: string): IFuture; } interface IProjectData { @@ -22,8 +22,24 @@ interface IProjectDataService { removeDependency(dependencyName: string): IFuture; } +/** + * Describes working with templates. + */ interface IProjectTemplatesService { + /** + * Defines the path where unpacked default template can be found. + */ defaultTemplatePath: IFuture; + + /** + * Prepares template for project creation. + * In case templateName is not provided, use defaultTemplatePath. + * In case templateName is a special word, validated from us (for ex. typescript), resolve the real template name and add it to npm cache. + * In any other cases try to `npm install` the specified templateName to temp directory. + * @param {string} templateName The name of the template. + * @return {string} Path to the directory where extracted template can be found. + */ + prepareTemplate(templateName: string): IFuture; } interface IPlatformProjectServiceBase { diff --git a/lib/npm-installation-manager.ts b/lib/npm-installation-manager.ts index 677b4cc258..656f0da30b 100644 --- a/lib/npm-installation-manager.ts +++ b/lib/npm-installation-manager.ts @@ -18,7 +18,8 @@ export class NpmInstallationManager implements INpmInstallationManager { private packageSpecificDirectories: IStringDictionary = { "tns-android": constants.PROJECT_FRAMEWORK_FOLDER_NAME, "tns-ios": constants.PROJECT_FRAMEWORK_FOLDER_NAME, - "tns-template-hello-world": constants.APP_RESOURCES_FOLDER_NAME + "tns-template-hello-world": constants.APP_RESOURCES_FOLDER_NAME, + "tns-template-hello-world-ts": constants.APP_RESOURCES_FOLDER_NAME }; constructor(private $npm: INodePackageManager, diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 0db9290824..a0c28a4efb 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -4,7 +4,7 @@ import * as constants from "../constants"; import * as osenv from "osenv"; import * as path from "path"; -import * as shell from "shelljs"; +import * as shelljs from "shelljs"; export class ProjectService implements IProjectService { @@ -18,7 +18,7 @@ export class ProjectService implements IProjectService { private $projectTemplatesService: IProjectTemplatesService, private $options: IOptions) { } - public createProject(projectName: string): IFuture { + public createProject(projectName: string, selectedTemplate?: string): IFuture { return(() => { if (!projectName) { this.$errors.fail("You must specify when creating a new project."); @@ -51,7 +51,6 @@ export class ProjectService implements IProjectService { let appDirectory = path.join(projectDir, constants.APP_FOLDER_NAME); let appPath: string = null; - if (customAppPath) { this.$logger.trace("Using custom app from %s", customAppPath); @@ -68,25 +67,71 @@ export class ProjectService implements IProjectService { 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"); - let defaultTemplatePath = this.$projectTemplatesService.defaultTemplatePath.wait(); - this.$logger.trace("Copying NativeScript hello world application into %s", appDirectory); + let defaultTemplatePath = this.$projectTemplatesService.prepareTemplate(selectedTemplate).wait(); + this.$logger.trace(`Copying application from '${defaultTemplatePath}' into '${appDirectory}'.`); appPath = defaultTemplatePath; } try { this.createProjectCore(projectDir, appPath, projectId).wait(); + //update dependencies and devDependencies of newly created project with data from template + this.mergeProjectAndTemplateProperties(projectDir, appPath).wait(); + this.updateAppResourcesDir(appDirectory).wait(); + // Delete app/package.json file, its just causing confusion. + // Also its dependencies and devDependencies are already merged in project's package.json. + this.$fs.deleteFile(path.join(projectDir, constants.APP_FOLDER_NAME, constants.PACKAGE_JSON_FILE_NAME)).wait(); } catch (err) { this.$fs.deleteDirectory(projectDir).wait(); throw err; } - this.$logger.out("Project %s was successfully created", projectName); }).future()(); } + private mergeProjectAndTemplateProperties(projectDir: string, templatePath: string): IFuture { + return (() => { + let projectPackageJsonPath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); + let projectPackageJsonData = this.$fs.readJson(projectPackageJsonPath).wait(); + this.$logger.trace("Initial project package.json data: ", projectPackageJsonData); + let templatePackageJsonData = this.$fs.readJson(path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME)).wait(); + if(projectPackageJsonData.dependencies || templatePackageJsonData.dependencies) { + projectPackageJsonData.dependencies = this.mergeDependencies(projectPackageJsonData.dependencies, templatePackageJsonData.dependencies); + } + + if(projectPackageJsonData.devDependencies || templatePackageJsonData.devDependencies) { + projectPackageJsonData.devDependencies = this.mergeDependencies(projectPackageJsonData.devDependencies, templatePackageJsonData.devDependencies); + } + + this.$logger.trace("New project package.json data: ", projectPackageJsonData); + this.$fs.writeJson(projectPackageJsonPath, projectPackageJsonData).wait(); + }).future()(); + } + + private updateAppResourcesDir(appDirectory: string): IFuture { + return (() => { + let defaultAppResourcesDir = path.join(this.$projectTemplatesService.defaultTemplatePath.wait(), constants.APP_RESOURCES_FOLDER_NAME); + let targetAppResourcesDir = path.join(appDirectory, constants.APP_RESOURCES_FOLDER_NAME); + this.$logger.trace(`Updating AppResources values from ${defaultAppResourcesDir} to ${targetAppResourcesDir}`); + shelljs.cp("-R", path.join(defaultAppResourcesDir, "*"), targetAppResourcesDir); + }).future()(); + } + + private mergeDependencies(projectDependencies: IStringDictionary, templateDependencies: IStringDictionary): IStringDictionary { + // Cast to any when logging as logger thinks it can print only string. + // Cannot use toString() because we want to print the whole objects, not [Object object] + this.$logger.trace("Merging dependencies, projectDependencies are: ", projectDependencies, " templateDependencies are: ", templateDependencies); + projectDependencies = projectDependencies || {}; + _.extend(projectDependencies, templateDependencies || {}); + let sortedDeps: IStringDictionary = {}; + let dependenciesNames = _.keys(projectDependencies).sort(); + _.each(dependenciesNames, (key: string) => { + sortedDeps[key] = projectDependencies[key]; + }); + this.$logger.trace("Sorted merged dependencies are: ", sortedDeps); + return sortedDeps; + } + private createProjectCore(projectDir: string, appSourcePath: string, projectId: string): IFuture { return (() => { this.$fs.ensureDirectoryExists(projectDir).wait(); @@ -97,7 +142,7 @@ export class ProjectService implements IProjectService { if(this.$options.symlink) { this.$fs.symlink(appSourcePath, appDestinationPath).wait(); } else { - shell.cp('-R', path.join(appSourcePath, "*"), appDestinationPath); + shelljs.cp('-R', path.join(appSourcePath, "*"), appDestinationPath); } this.createBasicProjectStructure(projectDir, projectId).wait(); diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index 7d3d11d566..9c23c5c9d7 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -1,13 +1,101 @@ /// "use strict"; +import * as path from "path"; +import * as temp from "temp"; +import * as constants from "../constants"; +import {EOL} from "os"; +temp.track(); export class ProjectTemplatesService implements IProjectTemplatesService { - private static NPM_DEFAULT_TEMPLATE_NAME = "tns-template-hello-world"; + private static RESERVED_TEMPLATE_NAMES: IStringDictionary = { + "default": "tns-template-hello-world", + "tsc": "tns-template-hello-world-ts", + "typescript": "tns-template-hello-world-ts" + }; - public constructor(private $npmInstallationManager: INpmInstallationManager) { } + public constructor(private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $npm: INodePackageManager, + private $npmInstallationManager: INpmInstallationManager) { } public get defaultTemplatePath(): IFuture { - return this.$npmInstallationManager.install(ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME); + return this.prepareNativeScriptTemplate(ProjectTemplatesService.RESERVED_TEMPLATE_NAMES["default"]); + } + + public prepareTemplate(originalTemplateName: string): IFuture { + return ((): string => { + let realTemplatePath: string; + if(originalTemplateName) { + let templateName = originalTemplateName.toLowerCase(); + + // support @ syntax + let [name, version] = templateName.split("@"); + if(ProjectTemplatesService.RESERVED_TEMPLATE_NAMES[name]) { + realTemplatePath = this.prepareNativeScriptTemplate(ProjectTemplatesService.RESERVED_TEMPLATE_NAMES[name], version).wait(); + } else { + let tempDir = temp.mkdirSync("nativescript-template-dir"); + try { + // Use the original template name, specified by user as it may be case-sensitive. + this.$npm.install(originalTemplateName, tempDir, {production: true, silent: true}).wait(); + } catch(err) { + this.$logger.trace(err); + this.$errors.failWithoutHelp(`Unable to use template ${originalTemplateName}. Make sure you've specified valid name, github URL or path to local dir.` + + `${EOL}Error is: ${err.message}.`); + } + + realTemplatePath = this.getTemplatePathFromTempDir(tempDir).wait(); + } + } else { + realTemplatePath = this.defaultTemplatePath.wait(); + } + + if(realTemplatePath) { + this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME)).wait(); + return realTemplatePath; + } + + this.$errors.failWithoutHelp("Unable to find the template in temp directory. " + + `Please open an issue at https://github.com/NativeScript/nativescript-cli/issues and send the output of the same command executed with --log trace.`); + }).future()(); + } + + /** + * Install verified NativeScript template in the npm cache. + * The "special" here is that npmInstallationManager will check current CLI version and will instal best matching version of the template. + * For example in case CLI is version 10.12.8, npmInstallationManager will try to find latest 10.12.x version of the template. + * @param {string} templateName The name of the verified NativeScript template. + * @param {string} version The version of the template specified by user. + * @return {string} Path to the directory where the template is installed. + */ + private prepareNativeScriptTemplate(templateName: string, version?: string): IFuture { + this.$logger.trace(`Using NativeScript verified template: ${templateName} with version ${version}.`); + return this.$npmInstallationManager.install(templateName, {version: version}); + } + + private getTemplatePathFromTempDir(tempDir: string): IFuture { + return ((): string => { + let templatePath: string; + let tempDirContents = this.$fs.readDirectory(tempDir).wait(); + this.$logger.trace(`TempDir contents: ${tempDirContents}.`); + + // We do not know the name of the package that will be installed, so after installation to temp dir, + // there should be node_modules dir there and its only subdir should be our package. + // In case there's some other dir instead of node_modules, consider it as our package. + if(tempDirContents && tempDirContents.length === 1) { + let tempDirSubdir = _.first(tempDirContents); + if(tempDirSubdir === constants.NODE_MODULES_FOLDER_NAME) { + let templateDirName = _.first(this.$fs.readDirectory(path.join(tempDir, constants.NODE_MODULES_FOLDER_NAME)).wait()); + if(templateDirName) { + templatePath = path.join(tempDir, tempDirSubdir, templateDirName); + } + } else { + templatePath = path.join(tempDir, tempDirSubdir); + } + } + + return templatePath; + }).future()(); } } $injector.register("projectTemplatesService", ProjectTemplatesService); diff --git a/package.json b/package.json index 8531b29067..6821b321d7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mute-stream": "0.0.5", "node-inspector": "https://github.com/NativeScript/node-inspector/tarball/v0.7.4.0", "node-uuid": "1.4.3", - "npm": "2.6.1", + "npm": "2.14.12", "open": "0.0.5", "osenv": "0.1.3", "plist": "1.1.0", diff --git a/test/project-service.ts b/test/project-service.ts index 5ecc2bce4a..1ba47eb539 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -20,6 +20,7 @@ import * as helpers from "../lib/common/helpers"; import {assert} from "chai"; import {Options} from "../lib/options"; import {HostInfo} from "../lib/common/host-info"; +import {ProjectTemplatesService} from "../lib/services/project-templates-service"; let mockProjectNameValidator = { validate: () => { return true; } @@ -34,9 +35,9 @@ class ProjectIntegrationTest { this.createTestInjector(); } - public createProject(projectName: string): IFuture { + public createProject(projectName: string, template?: string): IFuture { let projectService = this.testInjector.resolve("projectService"); - return projectService.createProject(projectName); + return projectService.createProject(projectName, template); } public getNpmPackagePath(packageName: string): IFuture { @@ -59,7 +60,7 @@ class ProjectIntegrationTest { }).future()(); } - public assertProject(tempFolder: string, projectName: string, appId: string): IFuture { + public assertProject(tempFolder: string, projectName: string, appId: string, projectSourceDirectory?: string): IFuture { return (() => { let fs: IFileSystem = this.testInjector.resolve("fs"); let projectDir = path.join(tempFolder, projectName); @@ -67,7 +68,7 @@ class ProjectIntegrationTest { let platformsDirectoryPath = path.join(projectDir, "platforms"); let tnsProjectFilePath = path.join(projectDir, "package.json"); let tnsModulesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, constants.TNS_CORE_MODULES_NAME); - + let packageJsonContent = fs.readJson(tnsProjectFilePath).wait(); let options = this.testInjector.resolve("options"); assert.isTrue(fs.exists(appDirectoryPath).wait()); @@ -78,21 +79,38 @@ class ProjectIntegrationTest { assert.isFalse(fs.isEmptyDir(appDirectoryPath).wait()); assert.isTrue(fs.isEmptyDir(platformsDirectoryPath).wait()); - let actualAppId = fs.readJson(tnsProjectFilePath).wait()["nativescript"].id; + let actualAppId = packageJsonContent["nativescript"].id; let expectedAppId = appId; assert.equal(actualAppId, expectedAppId); - let tnsCoreModulesRecord = fs.readJson(tnsProjectFilePath).wait()["dependencies"][constants.TNS_CORE_MODULES_NAME]; + let tnsCoreModulesRecord = packageJsonContent["dependencies"][constants.TNS_CORE_MODULES_NAME]; assert.isTrue(tnsCoreModulesRecord !== null); - let actualFiles = fs.enumerateFilesInDirectorySync(options.copyFrom); - let expectedFiles = fs.enumerateFilesInDirectorySync(appDirectoryPath); - - assert.equal(actualFiles.length, expectedFiles.length); - _.each(actualFiles, file => { - let relativeToProjectDir = helpers.getRelativeToRootPath(options.copyFrom, file); - assert.isTrue(fs.exists(path.join(appDirectoryPath, relativeToProjectDir)).wait()); + let sourceDir = projectSourceDirectory || options.copyFrom; + + let expectedFiles = fs.enumerateFilesInDirectorySync(sourceDir); + let actualFiles = fs.enumerateFilesInDirectorySync(appDirectoryPath); + assert.isTrue(actualFiles.length >= (expectedFiles.length - 1), "Files in created project must be at least as files in app dir (without package.json)."); + _.each(expectedFiles, file => { + let relativeToProjectDir = helpers.getRelativeToRootPath(sourceDir, file); + if(path.basename(file) === "package.json") { + assert.isFalse(fs.exists(path.join(appDirectoryPath, relativeToProjectDir)).wait()); + } else { + assert.isTrue(fs.exists(path.join(appDirectoryPath, relativeToProjectDir)).wait()); + } }); + + // assert dependencies and devDependencies are copied from template to real project + let sourcePackageJsonContent = fs.readJson(path.join(sourceDir, "package.json")).wait(); + let missingDeps = _.difference(_.keys(sourcePackageJsonContent.dependencies), _.keys(packageJsonContent.dependencies)); + let missingDevDeps = _.difference(_.keys(sourcePackageJsonContent.devDependencies), _.keys(packageJsonContent.devDependencies)); + assert.deepEqual(missingDeps, [], `All dependencies from template must be copied to project's package.json. Missing ones are: ${missingDeps.join(", ")}.`); + assert.deepEqual(missingDevDeps, [], `All devDependencies from template must be copied to project's package.json. Missing ones are: ${missingDevDeps.join(", ")}.`); + + // assert App_Resources are prepared correctly + let appResourcesDir = path.join(appDirectoryPath, "App_Resources"); + let appResourcesContents = fs.readDirectory(appResourcesDir).wait(); + assert.deepEqual(appResourcesContents, ["Android", "iOS"], "Project's app/App_Resources must contain Android and iOS directories."); }).future()(); } @@ -107,7 +125,7 @@ class ProjectIntegrationTest { this.testInjector.register('logger', stubs.LoggerStub); this.testInjector.register("projectService", ProjectServiceLib.ProjectService); this.testInjector.register("projectHelper", ProjectHelperLib.ProjectHelper); - this.testInjector.register("projectTemplatesService", stubs.ProjectTemplatesService); + this.testInjector.register("projectTemplatesService", ProjectTemplatesService); this.testInjector.register("projectNameValidator", mockProjectNameValidator); this.testInjector.register("fs", FileSystem); @@ -126,8 +144,15 @@ class ProjectIntegrationTest { describe("Project Service Tests", () => { describe("project service integration tests", () => { + let pathToDefaultTemplate: string; + before(() => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let projectTemplatesService: IProjectTemplatesService = projectIntegrationTest.testInjector.resolve("projectTemplatesService"); + pathToDefaultTemplate = projectTemplatesService.defaultTemplatePath.wait(); + }); + it("creates valid project from default template", () => { - let projectIntegrationTest = new ProjectIntegrationTest(); + let projectIntegrationTest = new ProjectIntegrationTest(); let tempFolder = temp.mkdirSync("project"); let projectName = "myapp"; let options = projectIntegrationTest.testInjector.resolve("options"); @@ -138,8 +163,106 @@ describe("Project Service Tests", () => { projectIntegrationTest.createProject(projectName).wait(); projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp").wait(); }); + + it("creates valid project from default template when --template default is specified", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("project"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + projectIntegrationTest.createProject(projectName, "default").wait(); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", pathToDefaultTemplate).wait(); + }); + + it("creates valid project from default template when --template default@version is specified", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("project"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + let projectTemplatesService: IProjectTemplatesService = projectIntegrationTest.testInjector.resolve("projectTemplatesService"); + projectIntegrationTest.createProject(projectName, "default@1.4.0").wait(); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectTemplatesService.prepareTemplate("default@1.4.0").wait()).wait(); + }); + + /* Uncomment when tns-template-hello-world-ts is public + it("creates valid project from typescript template", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("projectTypescript"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + projectIntegrationTest.createProject(projectName, "typescript").wait(); + + let projectTemplatesService: IProjectTemplatesService = projectIntegrationTest.testInjector.resolve("projectTemplatesService"); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectTemplatesService.prepareTemplate("typescript").wait()).wait(); + }); + + it("creates valid project from tsc template", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("projectTsc"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + projectIntegrationTest.createProject(projectName, "tsc").wait(); + + let projectTemplatesService: IProjectTemplatesService = projectIntegrationTest.testInjector.resolve("projectTemplatesService"); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectTemplatesService.prepareTemplate("tsc").wait()).wait(); + }); + */ + + it("creates valid project from local directory template", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("projectLocalDir"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + let tempDir = temp.mkdirSync("template"); + let fs: IFileSystem = projectIntegrationTest.testInjector.resolve("fs"); + fs.writeJson(path.join(tempDir, "package.json"), { + name: "myCustomTemplate", + version: "1.0.0", + dependencies: { + "lodash": "3.10.1" + }, + devDependencies: { + "minimist": "1.2.0" + } + }).wait(); + + projectIntegrationTest.createProject(projectName, tempDir).wait(); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", tempDir).wait(); + }); + + it("creates valid project from tarball", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("projectLocalDir"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + projectIntegrationTest.createProject(projectName, "https://github.com/NativeScript/template-hello-world/tarball/master").wait(); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", pathToDefaultTemplate).wait(); + }); + + it("creates valid project from git url", () => { + let projectIntegrationTest = new ProjectIntegrationTest(); + let tempFolder = temp.mkdirSync("projectLocalDir"); + let projectName = "myapp"; + let options = projectIntegrationTest.testInjector.resolve("options"); + + options.path = tempFolder; + projectIntegrationTest.createProject(projectName, "https://github.com/NativeScript/template-hello-world.git").wait(); + projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", pathToDefaultTemplate).wait(); + }); + it("creates valid project with specified id from default template", () => { - let projectIntegrationTest = new ProjectIntegrationTest(); + let projectIntegrationTest = new ProjectIntegrationTest(); let tempFolder = temp.mkdirSync("project1"); let projectName = "myapp"; let options = projectIntegrationTest.testInjector.resolve("options"); diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts new file mode 100644 index 0000000000..e4c0c6f91b --- /dev/null +++ b/test/project-templates-service.ts @@ -0,0 +1,134 @@ +/// +"use strict"; + +import {Yok} from "../lib/common/yok"; +import * as stubs from "./stubs"; +import {ProjectTemplatesService} from "../lib/services/project-templates-service"; +import * as assert from "assert"; +import Future = require("fibers/future"); +import * as path from "path"; + +let isDeleteDirectoryCalledForNodeModulesDir = false; +let expectedTemplatePath = "templatePath"; +let nativeScriptValidatedTemplatePath = "nsValidatedTemplatePath"; + +function createTestInjector(configuration?: {shouldNpmInstallThrow: boolean, npmInstallationDirContents: string[], npmInstallationDirNodeModulesContents: string[]}): IInjector { + let injector = new Yok(); + injector.register("errors", stubs.ErrorsStub); + injector.register("logger", stubs.LoggerStub); + injector.register("fs", { + readDirectory: (dirPath: string) => { + if(dirPath.toLowerCase().indexOf("node_modules") !== -1) { + return Future.fromResult(configuration.npmInstallationDirNodeModulesContents); + } + return Future.fromResult(configuration.npmInstallationDirContents); + }, + + deleteDirectory: (directory: string) => { + if(directory.indexOf("node_modules") !== -1) { + isDeleteDirectoryCalledForNodeModulesDir = true; + } + return Future.fromResult(); + } + + }); + injector.register("npm", { + install: (packageName: string, pathToSave: string, config?: any) => { + return (() => { + if(configuration.shouldNpmInstallThrow) { + throw new Error("NPM install throws error."); + } + + return "sample result"; + }).future()(); + } + }); + + injector.register("npmInstallationManager", { + install: (packageName: string, options?: INpmInstallOptions) => { + return Future.fromResult(nativeScriptValidatedTemplatePath); + } + }); + + injector.register("projectTemplatesService", ProjectTemplatesService); + + return injector; +} + +describe("project-templates-service", () => { + let testInjector: IInjector; + let projectTemplatesService: IProjectTemplatesService; + beforeEach(() => { + isDeleteDirectoryCalledForNodeModulesDir = false; + }); + + describe("prepareTemplate", () => { + describe("throws error", () =>{ + it("when npm install fails", () => { + testInjector = createTestInjector({shouldNpmInstallThrow: true, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: null}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + assert.throws(() => projectTemplatesService.prepareTemplate("invalidName").wait()); + }); + + it("when after npm install the temp directory does not have any content", () => { + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: null}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + assert.throws(() => projectTemplatesService.prepareTemplate("validName").wait()); + }); + + it("when after npm install the temp directory has more than one subdir", () => { + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: ["dir1", "dir2"], npmInstallationDirNodeModulesContents: []}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + assert.throws(() => projectTemplatesService.prepareTemplate("validName").wait()); + }); + + it("when after npm install the temp directory has only node_modules directory and there's nothing inside node_modules", () => { + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: ["node_modules"], npmInstallationDirNodeModulesContents: []}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + assert.throws(() => projectTemplatesService.prepareTemplate("validName").wait()); + }); + }); + + describe("returns correct path to template", () => { + it("when after npm install the temp directory has only one subdir and it is not node_modules", () =>{ + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: [expectedTemplatePath], npmInstallationDirNodeModulesContents: []}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + let actualPathToTemplate = projectTemplatesService.prepareTemplate("validName").wait(); + assert.strictEqual(path.basename(actualPathToTemplate), expectedTemplatePath); + assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); + }); + + it("when after npm install the temp directory has only one subdir and it is node_modules", () =>{ + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: ["node_modules"], npmInstallationDirNodeModulesContents: [expectedTemplatePath]}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + let actualPathToTemplate = projectTemplatesService.prepareTemplate("validName").wait(); + assert.strictEqual(path.basename(actualPathToTemplate), expectedTemplatePath); + assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); + }); + + it("when reserved template name is used", () =>{ + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: []}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + let actualPathToTemplate = projectTemplatesService.prepareTemplate("typescript").wait(); + assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); + assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); + }); + + it("when reserved template name is used (case-insensitive test)", () =>{ + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: []}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + let actualPathToTemplate = projectTemplatesService.prepareTemplate("tYpEsCriPT").wait(); + assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); + assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); + }); + + it("uses defaultTemplate when undefined is passed as parameter", () =>{ + testInjector = createTestInjector({shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: []}); + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + let actualPathToTemplate = projectTemplatesService.prepareTemplate(undefined).wait(); + assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); + assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); + }); + }); + }); +}); diff --git a/test/stubs.ts b/test/stubs.ts index 4d645811c5..be34db8dde 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -184,7 +184,7 @@ export class ErrorsStub implements IErrors { } failWithoutHelp(message: string, ...args: any[]): void { - throw new Error(); + throw new Error(message); } beginCommand(action:() => IFuture, printHelpCommand: () => IFuture): IFuture { @@ -386,6 +386,10 @@ export class ProjectTemplatesService implements IProjectTemplatesService { get defaultTemplatePath(): IFuture { return Future.fromResult(""); } + + prepareTemplate(templateName: string): IFuture { + return Future.fromResult(""); + } } export class HooksServiceStub implements IHooksService { From 3ad78727c80ad1a0fe6c0c16b68081f52dfc6080 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Mon, 30 Nov 2015 11:22:00 +0200 Subject: [PATCH 2/2] Install all dependencies on create When `tns create ` is executed, install all dependencies, so typescript compilation will be enabled immediately when using tsc template. --- lib/services/project-service.ts | 30 ++++++++++++++++++------------ test/project-service.ts | 2 -- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index a0c28a4efb..48a18df987 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -80,6 +80,7 @@ export class ProjectService implements IProjectService { // Delete app/package.json file, its just causing confusion. // Also its dependencies and devDependencies are already merged in project's package.json. this.$fs.deleteFile(path.join(projectDir, constants.APP_FOLDER_NAME, constants.PACKAGE_JSON_FILE_NAME)).wait(); + this.$npm.install(projectDir, projectDir, { "ignore-scripts": this.$options.ignoreScripts }).wait(); } catch (err) { this.$fs.deleteDirectory(projectDir).wait(); throw err; @@ -91,20 +92,25 @@ export class ProjectService implements IProjectService { private mergeProjectAndTemplateProperties(projectDir: string, templatePath: string): IFuture { return (() => { - let projectPackageJsonPath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); - let projectPackageJsonData = this.$fs.readJson(projectPackageJsonPath).wait(); - this.$logger.trace("Initial project package.json data: ", projectPackageJsonData); - let templatePackageJsonData = this.$fs.readJson(path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME)).wait(); - if(projectPackageJsonData.dependencies || templatePackageJsonData.dependencies) { - projectPackageJsonData.dependencies = this.mergeDependencies(projectPackageJsonData.dependencies, templatePackageJsonData.dependencies); - } + let templatePackageJsonPath = path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME); + if(this.$fs.exists(templatePackageJsonPath).wait()) { + let projectPackageJsonPath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME); + let projectPackageJsonData = this.$fs.readJson(projectPackageJsonPath).wait(); + this.$logger.trace("Initial project package.json data: ", projectPackageJsonData); + let templatePackageJsonData = this.$fs.readJson(templatePackageJsonPath).wait(); + if(projectPackageJsonData.dependencies || templatePackageJsonData.dependencies) { + projectPackageJsonData.dependencies = this.mergeDependencies(projectPackageJsonData.dependencies, templatePackageJsonData.dependencies); + } - if(projectPackageJsonData.devDependencies || templatePackageJsonData.devDependencies) { - projectPackageJsonData.devDependencies = this.mergeDependencies(projectPackageJsonData.devDependencies, templatePackageJsonData.devDependencies); - } + if(projectPackageJsonData.devDependencies || templatePackageJsonData.devDependencies) { + projectPackageJsonData.devDependencies = this.mergeDependencies(projectPackageJsonData.devDependencies, templatePackageJsonData.devDependencies); + } - this.$logger.trace("New project package.json data: ", projectPackageJsonData); - this.$fs.writeJson(projectPackageJsonPath, projectPackageJsonData).wait(); + this.$logger.trace("New project package.json data: ", projectPackageJsonData); + this.$fs.writeJson(projectPackageJsonPath, projectPackageJsonData).wait(); + } else { + this.$logger.trace(`Template ${templatePath} does not have ${constants.PACKAGE_JSON_FILE_NAME} file.`); + } }).future()(); } diff --git a/test/project-service.ts b/test/project-service.ts index 1ba47eb539..a464cfea4c 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -187,7 +187,6 @@ describe("Project Service Tests", () => { projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectTemplatesService.prepareTemplate("default@1.4.0").wait()).wait(); }); - /* Uncomment when tns-template-hello-world-ts is public it("creates valid project from typescript template", () => { let projectIntegrationTest = new ProjectIntegrationTest(); let tempFolder = temp.mkdirSync("projectTypescript"); @@ -213,7 +212,6 @@ describe("Project Service Tests", () => { let projectTemplatesService: IProjectTemplatesService = projectIntegrationTest.testInjector.resolve("projectTemplatesService"); projectIntegrationTest.assertProject(tempFolder, projectName, "org.nativescript.myapp", projectTemplatesService.prepareTemplate("tsc").wait()).wait(); }); - */ it("creates valid project from local directory template", () => { let projectIntegrationTest = new ProjectIntegrationTest();