Skip to content

Commit e5665f2

Browse files
Merge pull request #3542 from NativeScript/vladimirov/templates-v2
feat: Allow templates to be full applications
2 parents e0f81b6 + cd3f6cb commit e5665f2

File tree

7 files changed

+310
-111
lines changed

7 files changed

+310
-111
lines changed

lib/constants.ts

+16
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,16 @@ 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+
}
204+
205+
export class Hooks {
206+
public static createProject = "createProject";
207+
}

lib/definitions/project.d.ts

+38-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
/**
2-
* Describes available settings when creating new NativeScript application.
3-
*/
4-
interface IProjectSettings {
1+
interface IProjectName {
2+
projectName: string;
3+
}
4+
5+
interface IProjectSettingsBase extends IProjectName {
56
/**
67
* Name of the newly created application.
78
*/
89
projectName: string;
910

11+
/**
12+
* Defines whether the `npm install` command should be executed with `--ignore-scripts` option.
13+
* When it is passed, all scripts (postinstall for example) will not be executed.
14+
*/
15+
ignoreScripts?: boolean;
16+
1017
/**
1118
* Selected template from which to create the project. If not specified, defaults to hello-world template.
1219
* Template can be any npm package, local dir, github url, .tgz file.
@@ -19,7 +26,19 @@ interface IProjectSettings {
1926
* Application identifier for the newly created application. If not specified, defaults to org.nativescript.<projectName>.
2027
*/
2128
appId?: string;
29+
}
30+
31+
/**
32+
* Describes information passed to project creation hook (createProject).
33+
*/
34+
interface IProjectCreationSettings extends IProjectSettingsBase, IProjectDir {
35+
36+
}
2237

38+
/**
39+
* Describes available settings when creating new NativeScript application.
40+
*/
41+
interface IProjectSettings extends IProjectSettingsBase {
2342
/**
2443
* Path where the project will be created. If not specified, defaults to current working dir.
2544
*/
@@ -29,17 +48,8 @@ interface IProjectSettings {
2948
* Defines if invalid application name can be used for project creation.
3049
*/
3150
force?: boolean;
32-
33-
/**
34-
* Defines whether the `npm install` command should be executed with `--ignore-scripts` option.
35-
* When it is passed, all scripts (postinstall for example) will not be executed.
36-
*/
37-
ignoreScripts?: boolean;
3851
}
3952

40-
interface IProjectName {
41-
projectName: string;
42-
}
4353

4454
interface ICreateProjectData extends IProjectDir, IProjectName {
4555

@@ -201,6 +211,11 @@ interface IImageDefinitionsStructure {
201211
android: IImageDefinitionGroup;
202212
}
203213

214+
interface ITemplateData {
215+
templatePath: string;
216+
templateVersion: string;
217+
}
218+
204219
/**
205220
* Describes working with templates.
206221
*/
@@ -211,9 +226,17 @@ interface IProjectTemplatesService {
211226
* In case templateName is a special word, validated from us (for ex. typescript), resolve the real template name and add it to npm cache.
212227
* In any other cases try to `npm install` the specified templateName to temp directory.
213228
* @param {string} templateName The name of the template.
214-
* @return {string} Path to the directory where extracted template can be found.
229+
* @return {ITemplateData} Data describing the template - location where it is installed and its NativeScript version.
230+
*/
231+
prepareTemplate(templateName: string, projectDir: string): Promise<ITemplateData>;
232+
233+
/**
234+
* Gives information for the nativescript specific version of the template, for example v1, v2, etc.
235+
* Defaults to v1 in case there's no version specified.
236+
* @param {string} templatePath Full path to the template.
237+
* @returns {string} The version, for example v1 or v2.
215238
*/
216-
prepareTemplate(templateName: string, projectDir: string): Promise<string>;
239+
getTemplateVersion(templatePath: string): string;
217240
}
218241

219242
interface IPlatformProjectServiceBase {

lib/services/project-service.ts

+103-30
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import * as constants from "../constants";
22
import * as path from "path";
33
import * as shelljs from "shelljs";
44
import { exported } from "../common/decorators";
5+
import { Hooks } from "../constants";
56

67
export class ProjectService implements IProjectService {
78

8-
constructor(private $npm: INodePackageManager,
9+
constructor(private $hooksService: IHooksService,
10+
private $npm: INodePackageManager,
911
private $errors: IErrors,
1012
private $fs: IFileSystem,
1113
private $logger: ILogger,
12-
private $projectData: IProjectData,
1314
private $projectDataService: IProjectDataService,
1415
private $projectHelper: IProjectHelper,
1516
private $projectNameService: IProjectNameService,
@@ -37,17 +38,26 @@ export class ProjectService implements IProjectService {
3738
this.$errors.fail("Path already exists and is not empty %s", projectDir);
3839
}
3940

40-
const projectId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);
41-
this.createPackageJson(projectDir, projectId);
42-
43-
this.$logger.trace(`Creating a new NativeScript project with name ${projectName} and id ${projectId} at location ${projectDir}`);
41+
const appId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);
42+
this.createPackageJson(projectDir, appId);
43+
this.$logger.trace(`Creating a new NativeScript project with name ${projectName} and id ${appId} at location ${projectDir}`);
4444
if (!selectedTemplate) {
4545
selectedTemplate = constants.RESERVED_TEMPLATE_NAMES["default"];
4646
}
4747

48+
const projectCreationData = await this.createProjectCore({ template: selectedTemplate, projectDir, ignoreScripts: projectOptions.ignoreScripts, appId: appId, projectName });
49+
50+
this.$logger.printMarkdown("Project `%s` was successfully created.", projectCreationData.projectName);
51+
52+
return projectCreationData;
53+
}
54+
55+
private async createProjectCore(projectCreationSettings: IProjectCreationSettings): Promise<ICreateProjectData> {
56+
const { template, projectDir, appId, projectName, ignoreScripts } = projectCreationSettings;
57+
4858
try {
49-
const templatePath = await this.$projectTemplatesService.prepareTemplate(selectedTemplate, projectDir);
50-
await this.extractTemplate(projectDir, templatePath);
59+
const { templatePath, templateVersion } = await this.$projectTemplatesService.prepareTemplate(template, projectDir);
60+
await this.extractTemplate(projectDir, templatePath, templateVersion);
5161

5262
await this.ensureAppResourcesExist(projectDir);
5363

@@ -57,23 +67,33 @@ export class ProjectService implements IProjectService {
5767
await this.$npmInstallationManager.install(constants.TNS_CORE_MODULES_NAME, projectDir, { dependencyType: "save" });
5868
}
5969

60-
this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); //merging dependencies from template (dev && prod)
61-
this.removeMergedDependencies(projectDir, templatePackageJsonData);
70+
if (templateVersion === constants.TemplateVersions.v1) {
71+
this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); // merging dependencies from template (dev && prod)
72+
this.removeMergedDependencies(projectDir, templatePackageJsonData);
73+
}
6274

75+
const templatePackageJson = this.$fs.readJson(path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME));
76+
77+
// Install devDependencies and execute all scripts:
6378
await this.$npm.install(projectDir, projectDir, {
6479
disableNpmInstall: false,
6580
frameworkPath: null,
66-
ignoreScripts: projectOptions.ignoreScripts
81+
ignoreScripts
6782
});
6883

69-
const templatePackageJson = this.$fs.readJson(path.join(templatePath, "package.json"));
7084
await this.$npm.uninstall(templatePackageJson.name, { save: true }, projectDir);
85+
if (templateVersion === constants.TemplateVersions.v2) {
86+
this.alterPackageJsonData(projectDir, appId);
87+
}
7188
} catch (err) {
7289
this.$fs.deleteDirectory(projectDir);
7390
throw err;
7491
}
7592

76-
this.$logger.printMarkdown("Project `%s` was successfully created.", projectName);
93+
this.$hooksService.executeAfterHooks(Hooks.createProject, {
94+
hookArgs: projectCreationSettings
95+
});
96+
7797
return { projectName, projectDir };
7898
}
7999

@@ -100,21 +120,34 @@ export class ProjectService implements IProjectService {
100120
return null;
101121
}
102122

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

106-
const appDestinationPath = this.$projectData.getAppDirectoryPath(projectDir);
107-
this.$fs.createDirectory(appDestinationPath);
126+
this.$logger.trace(`Template version is ${templateVersion}`);
127+
let destinationDir = "";
128+
switch (templateVersion) {
129+
case constants.TemplateVersions.v1:
130+
const projectData = this.$projectDataService.getProjectData(projectDir);
131+
const appDestinationPath = projectData.getAppDirectoryPath(projectDir);
132+
this.$fs.createDirectory(appDestinationPath);
133+
destinationDir = appDestinationPath;
134+
break;
135+
case constants.TemplateVersions.v2:
136+
default:
137+
destinationDir = projectDir;
138+
break;
139+
}
108140

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

112144
this.$fs.createDirectory(path.join(projectDir, "platforms"));
113145
}
114146

115147
private async ensureAppResourcesExist(projectDir: string): Promise<void> {
116-
const appPath = this.$projectData.getAppDirectoryPath(projectDir),
117-
appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
148+
const projectData = this.$projectDataService.getProjectData(projectDir);
149+
const appPath = projectData.getAppDirectoryPath(projectDir);
150+
const appResourcesDestinationPath = projectData.getAppResourcesDirectoryPath(projectDir);
118151

119152
if (!this.$fs.exists(appResourcesDestinationPath)) {
120153
this.$fs.createDirectory(appResourcesDestinationPath);
@@ -128,10 +161,24 @@ export class ProjectService implements IProjectService {
128161
ignoreScripts: false
129162
});
130163

131-
const defaultTemplateAppResourcesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME,
132-
defaultTemplateName, constants.APP_RESOURCES_FOLDER_NAME);
164+
const defaultTemplatePath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, defaultTemplateName);
165+
const defaultTemplateVersion = this.$projectTemplatesService.getTemplateVersion(defaultTemplatePath);
166+
167+
let defaultTemplateAppResourcesPath: string = null;
168+
switch (defaultTemplateVersion) {
169+
case constants.TemplateVersions.v1:
170+
defaultTemplateAppResourcesPath = path.join(projectDir,
171+
constants.NODE_MODULES_FOLDER_NAME,
172+
defaultTemplateName,
173+
constants.APP_RESOURCES_FOLDER_NAME);
174+
break;
175+
case constants.TemplateVersions.v2:
176+
default:
177+
const defaultTemplateProjectData = this.$projectDataService.getProjectData(defaultTemplatePath);
178+
defaultTemplateAppResourcesPath = defaultTemplateProjectData.appResourcesDirectoryPath;
179+
}
133180

134-
if (this.$fs.exists(defaultTemplateAppResourcesPath)) {
181+
if (defaultTemplateAppResourcesPath && this.$fs.exists(defaultTemplateAppResourcesPath)) {
135182
shelljs.cp('-R', defaultTemplateAppResourcesPath, appPath);
136183
}
137184

@@ -140,7 +187,8 @@ export class ProjectService implements IProjectService {
140187
}
141188

142189
private removeMergedDependencies(projectDir: string, templatePackageJsonData: any): void {
143-
const extractedTemplatePackageJsonPath = path.join(this.$projectData.getAppDirectoryPath(projectDir), constants.PACKAGE_JSON_FILE_NAME);
190+
const appDirectoryPath = this.$projectDataService.getProjectData(projectDir).appDirectoryPath;
191+
const extractedTemplatePackageJsonPath = path.join(appDirectoryPath, constants.PACKAGE_JSON_FILE_NAME);
144192
for (const key in templatePackageJsonData) {
145193
if (constants.PackageJsonKeysToKeep.indexOf(key) === -1) {
146194
delete templatePackageJsonData[key];
@@ -188,13 +236,38 @@ export class ProjectService implements IProjectService {
188236
private createPackageJson(projectDir: string, projectId: string): void {
189237
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
190238

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-
});
239+
this.$fs.writeJson(projectFilePath, this.packageJsonDefaultData);
240+
241+
this.setAppId(projectDir, projectId);
242+
}
243+
244+
private get packageJsonDefaultData(): IStringDictionary {
245+
return {
246+
description: "NativeScript Application",
247+
license: "SEE LICENSE IN <your-license-filename>",
248+
readme: "NativeScript Application",
249+
repository: "<fill-your-repository-here>"
250+
};
251+
}
252+
253+
private alterPackageJsonData(projectDir: string, projectId: string): void {
254+
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);
255+
256+
const packageJsonData = this.$fs.readJson(projectFilePath);
257+
258+
// Remove the metadata keys from the package.json
259+
let updatedPackageJsonData = _.omitBy<any, any>(packageJsonData, (value: any, key: string) => _.startsWith(key, "_"));
260+
updatedPackageJsonData = _.merge(updatedPackageJsonData, this.packageJsonDefaultData);
261+
262+
if (updatedPackageJsonData.nativescript && updatedPackageJsonData.nativescript.templateVersion) {
263+
delete updatedPackageJsonData.nativescript.templateVersion;
264+
}
265+
266+
this.$fs.writeJson(projectFilePath, updatedPackageJsonData);
267+
this.setAppId(projectDir, projectId);
268+
}
197269

270+
private setAppId(projectDir: string, projectId: string): void {
198271
this.$projectDataService.setNSValue(projectDir, "id", projectId);
199272
}
200273
}

0 commit comments

Comments
 (0)