Skip to content

Commit a9ee3de

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 a9ee3de

6 files changed

Lines changed: 192 additions & 69 deletions

File tree

lib/constants.ts

Lines changed: 12 additions & 0 deletions
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 74 additions & 22 deletions
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,32 @@ 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 appPath = this.$projectData.getAppDirectoryPath(projectDir);
134+
const appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
117135

118136
if (!this.$fs.exists(appResourcesDestinationPath)) {
119137
this.$fs.createDirectory(appResourcesDestinationPath);
@@ -127,11 +145,20 @@ export class ProjectService implements IProjectService {
127145
ignoreScripts: false
128146
});
129147

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

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

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

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

248+
private setAppId(projectDir: string, projectId: string): void {
197249
this.$projectDataService.setNSValue(projectDir, "id", projectId);
198250
}
199251
}

lib/services/project-templates-service.ts

Lines changed: 36 additions & 5 deletions
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)