Skip to content

Commit d050c7d

Browse files
feat: Allow templates to be full applications
Currently when a project is created the content of the template is placed inside the `app` directory of the newly created project. This leads to some issues when you want to support more complex scenarios, for example it is difficult to add configuration file (like nsconfig.json or webpack.config.js) in the root of the project. The suggested solution to allow templates to be the full application is to check the template from which the application is created. In case the template contains a nativescript key and templateVersion property in it, check its value. In case it is v1, use the old way, i.e. place the content of the template in the app directory of the created project. In case it is v2 place the content of the template at the root of the application. In case it is anything else - throw an error. In case it is missing, use v1 as default. The solution ensures backwards compatiblity with existing templates and allows creating new types of templates.
1 parent 6d88123 commit d050c7d

File tree

6 files changed

+191
-69
lines changed

6 files changed

+191
-69
lines changed

lib/constants.ts

+12
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export const enum DebugTools {
133133
export const enum TrackActionNames {
134134
Build = "Build",
135135
CreateProject = "Create project",
136+
UsingTemplate = "Using Template",
136137
Debug = "Debug",
137138
Deploy = "Deploy",
138139
LiveSync = "LiveSync",
@@ -141,6 +142,8 @@ export const enum TrackActionNames {
141142
CheckEnvironmentRequirements = "Check Environment Requirements"
142143
}
143144

145+
export const AnalyticsEventLabelDelimiter = "__";
146+
144147
export const enum BuildStates {
145148
Clean = "Clean",
146149
Incremental = "Incremental"
@@ -170,3 +173,12 @@ export class AssetConstants {
170173
public static defaultScale = 1;
171174
public static defaultOverlayImageScale = 0.8;
172175
}
176+
177+
export class TemplateVersions {
178+
public static v1 = "v1";
179+
public static v2 = "v2";
180+
}
181+
182+
export class ProjectTemplateErrors {
183+
public static InvalidTemplateVersionStringFormat = "The template '%s' has a NativeScript version '%s' that is not supported. Unable to create project from it.";
184+
}

lib/definitions/project.d.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ interface IImageDefinitionsStructure {
201201
android: IImageDefinitionGroup;
202202
}
203203

204+
interface ITemplateData {
205+
templatePath: string;
206+
templateVersion: string;
207+
}
208+
204209
/**
205210
* Describes working with templates.
206211
*/
@@ -211,9 +216,9 @@ interface IProjectTemplatesService {
211216
* In case templateName is a special word, validated from us (for ex. typescript), resolve the real template name and add it to npm cache.
212217
* In any other cases try to `npm install` the specified templateName to temp directory.
213218
* @param {string} templateName The name of the template.
214-
* @return {string} Path to the directory where extracted template can be found.
219+
* @return {ITemplateData} Data describing the template - location where it is installed and its NativeScript version.
215220
*/
216-
prepareTemplate(templateName: string, projectDir: string): Promise<string>;
221+
prepareTemplate(templateName: string, projectDir: string): Promise<ITemplateData>;
217222
}
218223

219224
interface IPlatformProjectServiceBase {

lib/services/project-service.ts

+73-22
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export class ProjectService implements IProjectService {
4646
}
4747

4848
try {
49-
const templatePath = await this.$projectTemplatesService.prepareTemplate(selectedTemplate, projectDir);
50-
await this.extractTemplate(projectDir, templatePath);
49+
const { templatePath, templateVersion } = await this.$projectTemplatesService.prepareTemplate(selectedTemplate, projectDir);
50+
await this.extractTemplate(projectDir, templatePath, templateVersion);
5151

5252
await this.ensureAppResourcesExist(projectDir);
5353

@@ -57,17 +57,24 @@ export class ProjectService implements IProjectService {
5757
await this.$npmInstallationManager.install(constants.TNS_CORE_MODULES_NAME, projectDir, { dependencyType: "save" });
5858
}
5959

60-
this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); //merging dependencies from template (dev && prod)
61-
this.removeMergedDependencies(projectDir, templatePackageJsonData);
60+
if (templateVersion === constants.TemplateVersions.v1) {
61+
this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); // merging dependencies from template (dev && prod)
62+
this.removeMergedDependencies(projectDir, templatePackageJsonData);
63+
}
64+
65+
const templatePackageJson = this.$fs.readJson(path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME));
6266

67+
// Install devDependencies and execute all scripts:
6368
await this.$npm.install(projectDir, projectDir, {
6469
disableNpmInstall: false,
6570
frameworkPath: null,
6671
ignoreScripts: projectOptions.ignoreScripts
6772
});
6873

69-
const templatePackageJson = this.$fs.readJson(path.join(templatePath, "package.json"));
7074
await this.$npm.uninstall(templatePackageJson.name, { save: true }, projectDir);
75+
if (templateVersion === constants.TemplateVersions.v2) {
76+
this.alterPackageJsonData(projectDir, projectId);
77+
}
7178
} catch (err) {
7279
this.$fs.deleteDirectory(projectDir);
7380
throw err;
@@ -99,21 +106,31 @@ export class ProjectService implements IProjectService {
99106
return null;
100107
}
101108

102-
private async extractTemplate(projectDir: string, realTemplatePath: string): Promise<void> {
109+
private async extractTemplate(projectDir: string, realTemplatePath: string, templateVersion: string): Promise<void> {
103110
this.$fs.ensureDirectoryExists(projectDir);
104111

105-
const appDestinationPath = this.$projectData.getAppDirectoryPath(projectDir);
106-
this.$fs.createDirectory(appDestinationPath);
112+
this.$logger.trace(`Template version is ${templateVersion}`);
113+
let destinationDir = "";
114+
switch (templateVersion) {
115+
case constants.TemplateVersions.v2:
116+
destinationDir = projectDir;
117+
break;
118+
case constants.TemplateVersions.v1:
119+
default:
120+
const appDestinationPath = this.$projectData.getAppDirectoryPath(projectDir);
121+
this.$fs.createDirectory(appDestinationPath);
122+
destinationDir = appDestinationPath;
123+
break;
124+
}
107125

108-
this.$logger.trace(`Copying application from '${realTemplatePath}' into '${appDestinationPath}'.`);
109-
shelljs.cp('-R', path.join(realTemplatePath, "*"), appDestinationPath);
126+
this.$logger.trace(`Copying application from '${realTemplatePath}' into '${destinationDir}'.`);
127+
shelljs.cp('-R', path.join(realTemplatePath, "*"), destinationDir);
110128

111129
this.$fs.createDirectory(path.join(projectDir, "platforms"));
112130
}
113131

114132
private async ensureAppResourcesExist(projectDir: string): Promise<void> {
115-
const appPath = this.$projectData.getAppDirectoryPath(projectDir),
116-
appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
133+
const appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
117134

118135
if (!this.$fs.exists(appResourcesDestinationPath)) {
119136
this.$fs.createDirectory(appResourcesDestinationPath);
@@ -127,11 +144,20 @@ export class ProjectService implements IProjectService {
127144
ignoreScripts: false
128145
});
129146

130-
const defaultTemplateAppResourcesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME,
131-
defaultTemplateName, constants.APP_RESOURCES_FOLDER_NAME);
147+
const obsoleteAppResourcesPath = path.join(projectDir,
148+
constants.NODE_MODULES_FOLDER_NAME,
149+
defaultTemplateName,
150+
constants.APP_RESOURCES_FOLDER_NAME);
151+
152+
const defaultTemplateAppResourcesPath = path.join(projectDir,
153+
constants.NODE_MODULES_FOLDER_NAME,
154+
defaultTemplateName,
155+
constants.APP_FOLDER_NAME,
156+
constants.APP_RESOURCES_FOLDER_NAME);
132157

133-
if (this.$fs.exists(defaultTemplateAppResourcesPath)) {
134-
shelljs.cp('-R', defaultTemplateAppResourcesPath, appPath);
158+
const pathToAppResources = this.$fs.exists(defaultTemplateAppResourcesPath) ? defaultTemplateAppResourcesPath : obsoleteAppResourcesPath;
159+
if (this.$fs.exists(pathToAppResources)) {
160+
shelljs.cp('-R', pathToAppResources, appResourcesDestinationPath);
135161
}
136162

137163
await this.$npm.uninstall(defaultTemplateName, { save: true }, projectDir);
@@ -187,13 +213,38 @@ export class ProjectService implements IProjectService {
187213
private createPackageJson(projectDir: string, projectId: string): void {
188214
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
189215

190-
this.$fs.writeJson(projectFilePath, {
191-
"description": "NativeScript Application",
192-
"license": "SEE LICENSE IN <your-license-filename>",
193-
"readme": "NativeScript Application",
194-
"repository": "<fill-your-repository-here>"
195-
});
216+
this.$fs.writeJson(projectFilePath, this.packageJsonDefaultData);
217+
218+
this.setAppId(projectDir, projectId);
219+
}
220+
221+
private get packageJsonDefaultData(): IStringDictionary {
222+
return {
223+
description: "NativeScript Application",
224+
license: "SEE LICENSE IN <your-license-filename>",
225+
readme: "NativeScript Application",
226+
repository: "<fill-your-repository-here>"
227+
};
228+
}
229+
230+
private alterPackageJsonData(projectDir: string, projectId: string): void {
231+
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
232+
233+
const packageJsonData = this.$fs.readJson(projectFilePath);
234+
235+
// Remove the metadata keys from the package.json
236+
let updatedPackageJsonData = _.omitBy<any, any>(packageJsonData, (value: any, key: string) => _.startsWith(key, "_"));
237+
updatedPackageJsonData = _.merge(updatedPackageJsonData, this.packageJsonDefaultData);
238+
239+
if (updatedPackageJsonData.nativescript && updatedPackageJsonData.nativescript.templateVersion) {
240+
delete updatedPackageJsonData.nativescript.templateVersion;
241+
}
242+
243+
this.$fs.writeJson(projectFilePath, updatedPackageJsonData);
244+
this.setAppId(projectDir, projectId);
245+
}
196246

247+
private setAppId(projectDir: string, projectId: string): void {
197248
this.$projectDataService.setNSValue(projectDir, "id", projectId);
198249
}
199250
}

lib/services/project-templates-service.ts

+36-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import * as path from "path";
22
import * as temp from "temp";
33
import * as constants from "../constants";
4+
import { format } from "util";
45
temp.track();
56

67
export class ProjectTemplatesService implements IProjectTemplatesService {
78

89
public constructor(private $analyticsService: IAnalyticsService,
910
private $fs: IFileSystem,
1011
private $logger: ILogger,
11-
private $npmInstallationManager: INpmInstallationManager) { }
12+
private $npmInstallationManager: INpmInstallationManager,
13+
private $errors: IErrors) { }
1214

13-
public async prepareTemplate(originalTemplateName: string, projectDir: string): Promise<string> {
15+
public async prepareTemplate(originalTemplateName: string, projectDir: string): Promise<ITemplateData> {
1416
// support <reserved_name>@<version> syntax
1517
const data = originalTemplateName.split("@"),
1618
name = data[0],
@@ -26,12 +28,41 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
2628
additionalData: templateName
2729
});
2830

29-
const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
31+
const templatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
3032

3133
// this removes dependencies from templates so they are not copied to app folder
32-
this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME));
34+
this.$fs.deleteDirectory(path.join(templatePath, constants.NODE_MODULES_FOLDER_NAME));
3335

34-
return realTemplatePath;
36+
const templateVersion = this.getTemplateVersion(templatePath);
37+
await this.$analyticsService.trackEventActionInGoogleAnalytics({
38+
action: constants.TrackActionNames.UsingTemplate,
39+
additionalData: `${templateName}${constants.AnalyticsEventLabelDelimiter}${templateVersion}`
40+
});
41+
42+
return { templatePath, templateVersion };
43+
}
44+
45+
private getTemplateVersion(templatePath: string): string {
46+
this.$logger.trace(`Checking the NativeScript version of the template located at ${templatePath}.`);
47+
const pathToPackageJson = path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME);
48+
if (this.$fs.exists(pathToPackageJson)) {
49+
const packageJsonContent = this.$fs.readJson(pathToPackageJson);
50+
const templateVersionFromPackageJson: string = packageJsonContent && packageJsonContent.nativescript && packageJsonContent.nativescript.templateVersion;
51+
52+
if (templateVersionFromPackageJson) {
53+
this.$logger.trace(`The template ${templatePath} has version ${templateVersionFromPackageJson}.`);
54+
55+
if (_.values(constants.TemplateVersions).indexOf(templateVersionFromPackageJson) === -1) {
56+
this.$errors.failWithoutHelp(format(constants.ProjectTemplateErrors.InvalidTemplateVersionStringFormat, templatePath, templateVersionFromPackageJson));
57+
}
58+
59+
return templateVersionFromPackageJson;
60+
}
61+
}
62+
63+
const defaultVersion = constants.TemplateVersions.v1;
64+
this.$logger.trace(`The template ${templatePath} does not specify version. Using default one ${defaultVersion}`);
65+
return defaultVersion;
3566
}
3667

3768
/**

0 commit comments

Comments
 (0)