Skip to content

Commit 40c3d1d

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.
1 parent 691c9f9 commit 40c3d1d

File tree

7 files changed

+155
-16
lines changed

7 files changed

+155
-16
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

+52-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,69 @@ 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();
8080
} catch (err) {
8181
this.$fs.deleteDirectory(projectDir).wait();
8282
throw err;
8383
}
84-
8584
this.$logger.out("Project %s was successfully created", projectName);
8685

8786
}).future<void>()();
8887
}
8988

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

103146
this.createBasicProjectStructure(projectDir, projectId).wait();
+76-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,87 @@
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+
temp.track();
37

48
export class ProjectTemplatesService implements IProjectTemplatesService {
59
private static NPM_DEFAULT_TEMPLATE_NAME = "tns-template-hello-world";
10+
private static RESERVED_TEMPLATE_NAMES: IStringDictionary = {
11+
"typescript": "tns-template-hello-world-ts",
12+
"tsc": "tns-template-hello-world-ts"
13+
};
614

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

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

test/stubs.ts

+4
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,10 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
386386
get defaultTemplatePath(): IFuture<string> {
387387
return Future.fromResult("");
388388
}
389+
390+
prepareTemplate(templateName: string): IFuture<string> {
391+
return Future.fromResult("");
392+
}
389393
}
390394

391395
export class HooksServiceStub implements IHooksService {

0 commit comments

Comments
 (0)