Skip to content

Commit 0edeec5

Browse files
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 [email protected]` 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.
1 parent 9dcd3b4 commit 0edeec5

10 files changed

+447
-35
lines changed

lib/commands/create-project.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ export class CreateProjectCommand implements ICommand {
2626
constructor(private $projectService: IProjectService,
2727
private $errors: IErrors,
2828
private $logger: ILogger,
29-
private $projectNameValidator: IProjectNameValidator) { }
29+
private $projectNameValidator: IProjectNameValidator,
30+
private $options: ICommonOptions) { }
3031

3132
public enableHooks = false;
3233

3334
execute(args: string[]): IFuture<void> {
3435
return (() => {
35-
this.$projectService.createProject(args[0]).wait();
36+
this.$projectService.createProject(args[0], this.$options.template).wait();
3637
}).future<void>()();
3738
}
3839

lib/definitions/project.d.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
interface IProjectService {
3-
createProject(projectName: string): IFuture<void>;
3+
createProject(projectName: string, selectedTemplate?: string): IFuture<void>;
44
}
55

66
interface IProjectData {
@@ -22,8 +22,24 @@ interface IProjectDataService {
2222
removeDependency(dependencyName: string): IFuture<void>;
2323
}
2424

25+
/**
26+
* Describes working with templates.
27+
*/
2528
interface IProjectTemplatesService {
29+
/**
30+
* Defines the path where unpacked default template can be found.
31+
*/
2632
defaultTemplatePath: IFuture<string>;
33+
34+
/**
35+
* Prepares template for project creation.
36+
* In case templateName is not provided, use defaultTemplatePath.
37+
* In case templateName is a special word, validated from us (for ex. typescript), resolve the real template name and add it to npm cache.
38+
* In any other cases try to `npm install` the specified templateName to temp directory.
39+
* @param {string} templateName The name of the template.
40+
* @return {string} Path to the directory where extracted template can be found.
41+
*/
42+
prepareTemplate(templateName: string): IFuture<string>;
2743
}
2844

2945
interface IPlatformProjectServiceBase {

lib/npm-installation-manager.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export class NpmInstallationManager implements INpmInstallationManager {
1818
private packageSpecificDirectories: IStringDictionary = {
1919
"tns-android": constants.PROJECT_FRAMEWORK_FOLDER_NAME,
2020
"tns-ios": constants.PROJECT_FRAMEWORK_FOLDER_NAME,
21-
"tns-template-hello-world": constants.APP_RESOURCES_FOLDER_NAME
21+
"tns-template-hello-world": constants.APP_RESOURCES_FOLDER_NAME,
22+
"tns-template-hello-world-ts": constants.APP_RESOURCES_FOLDER_NAME
2223
};
2324

2425
constructor(private $npm: INodePackageManager,

lib/services/project-service.ts

+54-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import * as constants from "../constants";
55
import * as osenv from "osenv";
66
import * as path from "path";
7-
import * as shell from "shelljs";
7+
import * as shelljs from "shelljs";
88

99
export class ProjectService implements IProjectService {
1010

@@ -18,7 +18,7 @@ export class ProjectService implements IProjectService {
1818
private $projectTemplatesService: IProjectTemplatesService,
1919
private $options: IOptions) { }
2020

21-
public createProject(projectName: string): IFuture<void> {
21+
public createProject(projectName: string, selectedTemplate?: string): IFuture<void> {
2222
return(() => {
2323
if (!projectName) {
2424
this.$errors.fail("You must specify <App name> when creating a new project.");
@@ -51,7 +51,6 @@ export class ProjectService implements IProjectService {
5151

5252
let appDirectory = path.join(projectDir, constants.APP_FOLDER_NAME);
5353
let appPath: string = null;
54-
5554
if (customAppPath) {
5655
this.$logger.trace("Using custom app from %s", customAppPath);
5756

@@ -68,25 +67,71 @@ export class ProjectService implements IProjectService {
6867
this.$logger.trace("Copying custom app into %s", appDirectory);
6968
appPath = customAppPath;
7069
} else {
71-
// No custom app - use nativescript hello world application
72-
this.$logger.trace("Using NativeScript hello world application");
73-
let defaultTemplatePath = this.$projectTemplatesService.defaultTemplatePath.wait();
74-
this.$logger.trace("Copying NativeScript hello world application into %s", appDirectory);
70+
let defaultTemplatePath = this.$projectTemplatesService.prepareTemplate(selectedTemplate).wait();
71+
this.$logger.trace(`Copying application from '${defaultTemplatePath}' into '${appDirectory}'.`);
7572
appPath = defaultTemplatePath;
7673
}
7774

7875
try {
7976
this.createProjectCore(projectDir, appPath, projectId).wait();
77+
//update dependencies and devDependencies of newly created project with data from template
78+
this.mergeProjectAndTemplateProperties(projectDir, appPath).wait();
79+
this.updateAppResourcesDir(appDirectory).wait();
80+
// Delete app/package.json file, its just causing confusion.
81+
// Also its dependencies and devDependencies are already merged in project's package.json.
82+
this.$fs.deleteFile(path.join(projectDir, constants.APP_FOLDER_NAME, constants.PACKAGE_JSON_FILE_NAME)).wait();
8083
} catch (err) {
8184
this.$fs.deleteDirectory(projectDir).wait();
8285
throw err;
8386
}
84-
8587
this.$logger.out("Project %s was successfully created", projectName);
8688

8789
}).future<void>()();
8890
}
8991

92+
private mergeProjectAndTemplateProperties(projectDir: string, templatePath: string): IFuture<void> {
93+
return (() => {
94+
let projectPackageJsonPath = path.join(projectDir, constants.PACKAGE_JSON_FILE_NAME);
95+
let projectPackageJsonData = this.$fs.readJson(projectPackageJsonPath).wait();
96+
this.$logger.trace("Initial project package.json data: ", projectPackageJsonData);
97+
let templatePackageJsonData = this.$fs.readJson(path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME)).wait();
98+
if(projectPackageJsonData.dependencies || templatePackageJsonData.dependencies) {
99+
projectPackageJsonData.dependencies = this.mergeDependencies(projectPackageJsonData.dependencies, templatePackageJsonData.dependencies);
100+
}
101+
102+
if(projectPackageJsonData.devDependencies || templatePackageJsonData.devDependencies) {
103+
projectPackageJsonData.devDependencies = this.mergeDependencies(projectPackageJsonData.devDependencies, templatePackageJsonData.devDependencies);
104+
}
105+
106+
this.$logger.trace("New project package.json data: ", projectPackageJsonData);
107+
this.$fs.writeJson(projectPackageJsonPath, projectPackageJsonData).wait();
108+
}).future<void>()();
109+
}
110+
111+
private updateAppResourcesDir(appDirectory: string): IFuture<void> {
112+
return (() => {
113+
let defaultAppResourcesDir = path.join(this.$projectTemplatesService.defaultTemplatePath.wait(), constants.APP_RESOURCES_FOLDER_NAME);
114+
let targetAppResourcesDir = path.join(appDirectory, constants.APP_RESOURCES_FOLDER_NAME);
115+
this.$logger.trace(`Updating AppResources values from ${defaultAppResourcesDir} to ${targetAppResourcesDir}`);
116+
shelljs.cp("-R", path.join(defaultAppResourcesDir, "*"), targetAppResourcesDir);
117+
}).future<void>()();
118+
}
119+
120+
private mergeDependencies(projectDependencies: IStringDictionary, templateDependencies: IStringDictionary): IStringDictionary {
121+
// Cast to any when logging as logger thinks it can print only string.
122+
// Cannot use toString() because we want to print the whole objects, not [Object object]
123+
this.$logger.trace("Merging dependencies, projectDependencies are: ", <any>projectDependencies, " templateDependencies are: ", <any>templateDependencies);
124+
projectDependencies = projectDependencies || {};
125+
_.extend(projectDependencies, templateDependencies || {});
126+
let sortedDeps: IStringDictionary = {};
127+
let dependenciesNames = _.keys(projectDependencies).sort();
128+
_.each(dependenciesNames, (key: string) => {
129+
sortedDeps[key] = projectDependencies[key];
130+
});
131+
this.$logger.trace("Sorted merged dependencies are: ", <any>sortedDeps);
132+
return sortedDeps;
133+
}
134+
90135
private createProjectCore(projectDir: string, appSourcePath: string, projectId: string): IFuture<void> {
91136
return (() => {
92137
this.$fs.ensureDirectoryExists(projectDir).wait();
@@ -97,7 +142,7 @@ export class ProjectService implements IProjectService {
97142
if(this.$options.symlink) {
98143
this.$fs.symlink(appSourcePath, appDestinationPath).wait();
99144
} else {
100-
shell.cp('-R', path.join(appSourcePath, "*"), appDestinationPath);
145+
shelljs.cp('-R', path.join(appSourcePath, "*"), appDestinationPath);
101146
}
102147

103148
this.createBasicProjectStructure(projectDir, projectId).wait();
+91-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,101 @@
11
///<reference path="../.d.ts"/>
22
"use strict";
3+
import * as path from "path";
4+
import * as temp from "temp";
5+
import * as constants from "../constants";
6+
import {EOL} from "os";
7+
temp.track();
38

49
export class ProjectTemplatesService implements IProjectTemplatesService {
5-
private static NPM_DEFAULT_TEMPLATE_NAME = "tns-template-hello-world";
10+
private static RESERVED_TEMPLATE_NAMES: IStringDictionary = {
11+
"default": "tns-template-hello-world",
12+
"tsc": "tns-template-hello-world-ts",
13+
"typescript": "tns-template-hello-world-ts"
14+
};
615

7-
public constructor(private $npmInstallationManager: INpmInstallationManager) { }
16+
public constructor(private $errors: IErrors,
17+
private $fs: IFileSystem,
18+
private $logger: ILogger,
19+
private $npm: INodePackageManager,
20+
private $npmInstallationManager: INpmInstallationManager) { }
821

922
public get defaultTemplatePath(): IFuture<string> {
10-
return this.$npmInstallationManager.install(ProjectTemplatesService.NPM_DEFAULT_TEMPLATE_NAME);
23+
return this.prepareNativeScriptTemplate(ProjectTemplatesService.RESERVED_TEMPLATE_NAMES["default"]);
24+
}
25+
26+
public prepareTemplate(originalTemplateName: string): IFuture<string> {
27+
return ((): string => {
28+
let realTemplatePath: string;
29+
if(originalTemplateName) {
30+
let templateName = originalTemplateName.toLowerCase();
31+
32+
// support <reserved_name>@<version> syntax
33+
let [name, version] = templateName.split("@");
34+
if(ProjectTemplatesService.RESERVED_TEMPLATE_NAMES[name]) {
35+
realTemplatePath = this.prepareNativeScriptTemplate(ProjectTemplatesService.RESERVED_TEMPLATE_NAMES[name], version).wait();
36+
} else {
37+
let tempDir = temp.mkdirSync("nativescript-template-dir");
38+
try {
39+
// Use the original template name, specified by user as it may be case-sensitive.
40+
this.$npm.install(originalTemplateName, tempDir, {production: true, silent: true}).wait();
41+
} catch(err) {
42+
this.$logger.trace(err);
43+
this.$errors.failWithoutHelp(`Unable to use template ${originalTemplateName}. Make sure you've specified valid name, github URL or path to local dir.` +
44+
`${EOL}Error is: ${err.message}.`);
45+
}
46+
47+
realTemplatePath = this.getTemplatePathFromTempDir(tempDir).wait();
48+
}
49+
} else {
50+
realTemplatePath = this.defaultTemplatePath.wait();
51+
}
52+
53+
if(realTemplatePath) {
54+
this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME)).wait();
55+
return realTemplatePath;
56+
}
57+
58+
this.$errors.failWithoutHelp("Unable to find the template in temp directory. " +
59+
`Please open an issue at https://github.com/NativeScript/nativescript-cli/issues and send the output of the same command executed with --log trace.`);
60+
}).future<string>()();
61+
}
62+
63+
/**
64+
* Install verified NativeScript template in the npm cache.
65+
* The "special" here is that npmInstallationManager will check current CLI version and will instal best matching version of the template.
66+
* For example in case CLI is version 10.12.8, npmInstallationManager will try to find latest 10.12.x version of the template.
67+
* @param {string} templateName The name of the verified NativeScript template.
68+
* @param {string} version The version of the template specified by user.
69+
* @return {string} Path to the directory where the template is installed.
70+
*/
71+
private prepareNativeScriptTemplate(templateName: string, version?: string): IFuture<string> {
72+
this.$logger.trace(`Using NativeScript verified template: ${templateName} with version ${version}.`);
73+
return this.$npmInstallationManager.install(templateName, {version: version});
74+
}
75+
76+
private getTemplatePathFromTempDir(tempDir: string): IFuture<string> {
77+
return ((): string => {
78+
let templatePath: string;
79+
let tempDirContents = this.$fs.readDirectory(tempDir).wait();
80+
this.$logger.trace(`TempDir contents: ${tempDirContents}.`);
81+
82+
// We do not know the name of the package that will be installed, so after installation to temp dir,
83+
// there should be node_modules dir there and its only subdir should be our package.
84+
// In case there's some other dir instead of node_modules, consider it as our package.
85+
if(tempDirContents && tempDirContents.length === 1) {
86+
let tempDirSubdir = _.first(tempDirContents);
87+
if(tempDirSubdir === constants.NODE_MODULES_FOLDER_NAME) {
88+
let templateDirName = _.first(this.$fs.readDirectory(path.join(tempDir, constants.NODE_MODULES_FOLDER_NAME)).wait());
89+
if(templateDirName) {
90+
templatePath = path.join(tempDir, tempDirSubdir, templateDirName);
91+
}
92+
} else {
93+
templatePath = path.join(tempDir, tempDirSubdir);
94+
}
95+
}
96+
97+
return templatePath;
98+
}).future<string>()();
1199
}
12100
}
13101
$injector.register("projectTemplatesService", ProjectTemplatesService);

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"mute-stream": "0.0.5",
5454
"node-inspector": "https://github.com/NativeScript/node-inspector/tarball/v0.7.4.0",
5555
"node-uuid": "1.4.3",
56-
"npm": "2.6.1",
56+
"npm": "2.14.12",
5757
"open": "0.0.5",
5858
"osenv": "0.1.3",
5959
"plist": "1.1.0",

0 commit comments

Comments
 (0)