Skip to content

Commit 1983062

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 ae30c84 commit 1983062

File tree

6 files changed

+231
-88
lines changed

6 files changed

+231
-88
lines changed

lib/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export const enum DebugTools {
137137
export const enum TrackActionNames {
138138
Build = "Build",
139139
CreateProject = "Create project",
140+
UsingTemplate = "Using Template",
140141
Debug = "Debug",
141142
Deploy = "Deploy",
142143
LiveSync = "LiveSync",
@@ -145,6 +146,8 @@ export const enum TrackActionNames {
145146
CheckEnvironmentRequirements = "Check Environment Requirements"
146147
}
147148

149+
export const AnalyticsEventLabelDelimiter = "__";
150+
148151
export const enum BuildStates {
149152
Clean = "Clean",
150153
Incremental = "Incremental"
@@ -189,3 +192,12 @@ export class SubscribeForNewsletterMessages {
189192
public static ReviewPrivacyPolicyMsg = `You can review the Progress Software Privacy Policy at \`${PROGRESS_PRIVACY_POLICY_URL}\``;
190193
public static PromptMsg = "Input your e-mail address to agree".green + " or " + "leave empty to decline".red.bold + ":";
191194
}
195+
196+
export class TemplateVersions {
197+
public static v1 = "v1";
198+
public static v2 = "v2";
199+
}
200+
201+
export class ProjectTemplateErrors {
202+
public static InvalidTemplateVersionStringFormat = "The template '%s' has a NativeScript version '%s' that is not supported. Unable to create project from it.";
203+
}

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;
@@ -100,21 +107,32 @@ export class ProjectService implements IProjectService {
100107
return null;
101108
}
102109

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

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

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

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

115133
private async ensureAppResourcesExist(projectDir: string): Promise<void> {
116-
const appPath = this.$projectData.getAppDirectoryPath(projectDir),
117-
appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
134+
const appPath = this.$projectData.getAppDirectoryPath(projectDir);
135+
const appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
118136

119137
if (!this.$fs.exists(appResourcesDestinationPath)) {
120138
this.$fs.createDirectory(appResourcesDestinationPath);
@@ -128,11 +146,20 @@ export class ProjectService implements IProjectService {
128146
ignoreScripts: false
129147
});
130148

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

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

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

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

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

lib/services/project-templates-service.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,71 @@
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],
1719
version = data[1];
1820

1921
const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name;
2022

21-
const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
23+
const templatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
2224

2325
await this.$analyticsService.track("Template used for project creation", templateName);
2426

25-
const templateNameToBeTracked = this.getTemplateNameToBeTracked(templateName, realTemplatePath);
27+
const templateNameToBeTracked = this.getTemplateNameToBeTracked(templateName, templatePath);
28+
const templateVersion = this.getTemplateVersion(templatePath);
2629
if (templateNameToBeTracked) {
2730
await this.$analyticsService.trackEventActionInGoogleAnalytics({
2831
action: constants.TrackActionNames.CreateProject,
2932
isForDevice: null,
3033
additionalData: templateNameToBeTracked
3134
});
35+
36+
await this.$analyticsService.trackEventActionInGoogleAnalytics({
37+
action: constants.TrackActionNames.UsingTemplate,
38+
additionalData: `${templateNameToBeTracked}${constants.AnalyticsEventLabelDelimiter}${templateVersion}`
39+
});
3240
}
3341

3442
// this removes dependencies from templates so they are not copied to app folder
35-
this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME));
43+
this.$fs.deleteDirectory(path.join(templatePath, constants.NODE_MODULES_FOLDER_NAME));
44+
45+
return { templatePath, templateVersion };
46+
}
47+
48+
private getTemplateVersion(templatePath: string): string {
49+
this.$logger.trace(`Checking the NativeScript version of the template located at ${templatePath}.`);
50+
const pathToPackageJson = path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME);
51+
if (this.$fs.exists(pathToPackageJson)) {
52+
const packageJsonContent = this.$fs.readJson(pathToPackageJson);
53+
const templateVersionFromPackageJson: string = packageJsonContent && packageJsonContent.nativescript && packageJsonContent.nativescript.templateVersion;
54+
55+
if (templateVersionFromPackageJson) {
56+
this.$logger.trace(`The template ${templatePath} has version ${templateVersionFromPackageJson}.`);
57+
58+
if (_.values(constants.TemplateVersions).indexOf(templateVersionFromPackageJson) === -1) {
59+
this.$errors.failWithoutHelp(format(constants.ProjectTemplateErrors.InvalidTemplateVersionStringFormat, templatePath, templateVersionFromPackageJson));
60+
}
61+
62+
return templateVersionFromPackageJson;
63+
}
64+
}
3665

37-
return realTemplatePath;
66+
const defaultVersion = constants.TemplateVersions.v1;
67+
this.$logger.trace(`The template ${templatePath} does not specify version or we were unable to find out the version. Using default one ${defaultVersion}`);
68+
return defaultVersion;
3869
}
3970

4071
/**

0 commit comments

Comments
 (0)