Skip to content

feat: Allow templates to be full applications #3542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const enum DebugTools {
export const enum TrackActionNames {
Build = "Build",
CreateProject = "Create project",
UsingTemplate = "Using Template",
Debug = "Debug",
Deploy = "Deploy",
LiveSync = "LiveSync",
Expand All @@ -145,6 +146,8 @@ export const enum TrackActionNames {
CheckEnvironmentRequirements = "Check Environment Requirements"
}

export const AnalyticsEventLabelDelimiter = "__";

export const enum BuildStates {
Clean = "Clean",
Incremental = "Incremental"
Expand Down Expand Up @@ -189,3 +192,16 @@ export class SubscribeForNewsletterMessages {
public static ReviewPrivacyPolicyMsg = `You can review the Progress Software Privacy Policy at \`${PROGRESS_PRIVACY_POLICY_URL}\``;
public static PromptMsg = "Input your e-mail address to agree".green + " or " + "leave empty to decline".red.bold + ":";
}

export class TemplateVersions {
public static v1 = "v1";
public static v2 = "v2";
}

export class ProjectTemplateErrors {
public static InvalidTemplateVersionStringFormat = "The template '%s' has a NativeScript version '%s' that is not supported. Unable to create project from it.";
}

export class Hooks {
public static createProject = "createProject";
}
53 changes: 38 additions & 15 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
/**
* Describes available settings when creating new NativeScript application.
*/
interface IProjectSettings {
interface IProjectName {
projectName: string;
}

interface IProjectSettingsBase extends IProjectName {
/**
* Name of the newly created application.
*/
projectName: string;

/**
* Defines whether the `npm install` command should be executed with `--ignore-scripts` option.
* When it is passed, all scripts (postinstall for example) will not be executed.
*/
ignoreScripts?: boolean;

/**
* Selected template from which to create the project. If not specified, defaults to hello-world template.
* Template can be any npm package, local dir, github url, .tgz file.
Expand All @@ -19,7 +26,19 @@ interface IProjectSettings {
* Application identifier for the newly created application. If not specified, defaults to org.nativescript.<projectName>.
*/
appId?: string;
}

/**
* Describes information passed to project creation hook (createProject).
*/
interface IProjectCreationSettings extends IProjectSettingsBase, IProjectDir {

}

/**
* Describes available settings when creating new NativeScript application.
*/
interface IProjectSettings extends IProjectSettingsBase {
/**
* Path where the project will be created. If not specified, defaults to current working dir.
*/
Expand All @@ -29,17 +48,8 @@ interface IProjectSettings {
* Defines if invalid application name can be used for project creation.
*/
force?: boolean;

/**
* Defines whether the `npm install` command should be executed with `--ignore-scripts` option.
* When it is passed, all scripts (postinstall for example) will not be executed.
*/
ignoreScripts?: boolean;
}

interface IProjectName {
projectName: string;
}

interface ICreateProjectData extends IProjectDir, IProjectName {

Expand Down Expand Up @@ -201,6 +211,11 @@ interface IImageDefinitionsStructure {
android: IImageDefinitionGroup;
}

interface ITemplateData {
templatePath: string;
templateVersion: string;
}

/**
* Describes working with templates.
*/
Expand All @@ -211,9 +226,17 @@ interface IProjectTemplatesService {
* In case templateName is a special word, validated from us (for ex. typescript), resolve the real template name and add it to npm cache.
* In any other cases try to `npm install` the specified templateName to temp directory.
* @param {string} templateName The name of the template.
* @return {string} Path to the directory where extracted template can be found.
* @return {ITemplateData} Data describing the template - location where it is installed and its NativeScript version.
*/
prepareTemplate(templateName: string, projectDir: string): Promise<ITemplateData>;

/**
* Gives information for the nativescript specific version of the template, for example v1, v2, etc.
* Defaults to v1 in case there's no version specified.
* @param {string} templatePath Full path to the template.
* @returns {string} The version, for example v1 or v2.
*/
prepareTemplate(templateName: string, projectDir: string): Promise<string>;
getTemplateVersion(templatePath: string): string;
}

interface IPlatformProjectServiceBase {
Expand Down
133 changes: 103 additions & 30 deletions lib/services/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import * as constants from "../constants";
import * as path from "path";
import * as shelljs from "shelljs";
import { exported } from "../common/decorators";
import { Hooks } from "../constants";

export class ProjectService implements IProjectService {

constructor(private $npm: INodePackageManager,
constructor(private $hooksService: IHooksService,
private $npm: INodePackageManager,
private $errors: IErrors,
private $fs: IFileSystem,
private $logger: ILogger,
private $projectData: IProjectData,
private $projectDataService: IProjectDataService,
private $projectHelper: IProjectHelper,
private $projectNameService: IProjectNameService,
Expand Down Expand Up @@ -37,17 +38,26 @@ export class ProjectService implements IProjectService {
this.$errors.fail("Path already exists and is not empty %s", projectDir);
}

const projectId = projectOptions.appId || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);
this.createPackageJson(projectDir, projectId);

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

const projectCreationData = await this.createProjectCore({ template: selectedTemplate, projectDir, ignoreScripts: projectOptions.ignoreScripts, appId: appId, projectName });

this.$logger.printMarkdown("Project `%s` was successfully created.", projectCreationData.projectName);

return projectCreationData;
}

private async createProjectCore(projectCreationSettings: IProjectCreationSettings): Promise<ICreateProjectData> {
const { template, projectDir, appId, projectName, ignoreScripts } = projectCreationSettings;

try {
const templatePath = await this.$projectTemplatesService.prepareTemplate(selectedTemplate, projectDir);
await this.extractTemplate(projectDir, templatePath);
const { templatePath, templateVersion } = await this.$projectTemplatesService.prepareTemplate(template, projectDir);
await this.extractTemplate(projectDir, templatePath, templateVersion);

await this.ensureAppResourcesExist(projectDir);

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

this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); //merging dependencies from template (dev && prod)
this.removeMergedDependencies(projectDir, templatePackageJsonData);
if (templateVersion === constants.TemplateVersions.v1) {
this.mergeProjectAndTemplateProperties(projectDir, templatePackageJsonData); // merging dependencies from template (dev && prod)
this.removeMergedDependencies(projectDir, templatePackageJsonData);
}

const templatePackageJson = this.$fs.readJson(path.join(templatePath, constants.PACKAGE_JSON_FILE_NAME));

// Install devDependencies and execute all scripts:
await this.$npm.install(projectDir, projectDir, {
disableNpmInstall: false,
frameworkPath: null,
ignoreScripts: projectOptions.ignoreScripts
ignoreScripts
});

const templatePackageJson = this.$fs.readJson(path.join(templatePath, "package.json"));
await this.$npm.uninstall(templatePackageJson.name, { save: true }, projectDir);
if (templateVersion === constants.TemplateVersions.v2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (templateVersion !== constants.TemplateVersions.v2) will allow bumping versions without having to alter code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot be sure what will be required in the next versions, so I prefer keeping the strictness here - for v2 do this. In case we decide to use v3 and to make it work in a similar manner , we'll alter the code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it more likely that v3 will be derived from v2 structure rather than from the one of v1.

this.alterPackageJsonData(projectDir, appId);
}
} catch (err) {
this.$fs.deleteDirectory(projectDir);
throw err;
}

this.$logger.printMarkdown("Project `%s` was successfully created.", projectName);
this.$hooksService.executeAfterHooks(Hooks.createProject, {
hookArgs: projectCreationSettings
});

return { projectName, projectDir };
}

Expand All @@ -100,21 +120,34 @@ export class ProjectService implements IProjectService {
return null;
}

private async extractTemplate(projectDir: string, realTemplatePath: string): Promise<void> {
private async extractTemplate(projectDir: string, realTemplatePath: string, templateVersion: string): Promise<void> {
this.$fs.ensureDirectoryExists(projectDir);

const appDestinationPath = this.$projectData.getAppDirectoryPath(projectDir);
this.$fs.createDirectory(appDestinationPath);
this.$logger.trace(`Template version is ${templateVersion}`);
let destinationDir = "";
switch (templateVersion) {
case constants.TemplateVersions.v1:
const projectData = this.$projectDataService.getProjectData(projectDir);
const appDestinationPath = projectData.getAppDirectoryPath(projectDir);
this.$fs.createDirectory(appDestinationPath);
destinationDir = appDestinationPath;
break;
case constants.TemplateVersions.v2:
default:
destinationDir = projectDir;
break;
}

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

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

private async ensureAppResourcesExist(projectDir: string): Promise<void> {
const appPath = this.$projectData.getAppDirectoryPath(projectDir),
appResourcesDestinationPath = this.$projectData.getAppResourcesDirectoryPath(projectDir);
const projectData = this.$projectDataService.getProjectData(projectDir);
const appPath = projectData.getAppDirectoryPath(projectDir);
const appResourcesDestinationPath = projectData.getAppResourcesDirectoryPath(projectDir);

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

const defaultTemplateAppResourcesPath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME,
defaultTemplateName, constants.APP_RESOURCES_FOLDER_NAME);
const defaultTemplatePath = path.join(projectDir, constants.NODE_MODULES_FOLDER_NAME, defaultTemplateName);
const defaultTemplateVersion = this.$projectTemplatesService.getTemplateVersion(defaultTemplatePath);

let defaultTemplateAppResourcesPath: string = null;
switch (defaultTemplateVersion) {
case constants.TemplateVersions.v1:
defaultTemplateAppResourcesPath = path.join(projectDir,
constants.NODE_MODULES_FOLDER_NAME,
defaultTemplateName,
constants.APP_RESOURCES_FOLDER_NAME);
break;
case constants.TemplateVersions.v2:
default:
const defaultTemplateProjectData = this.$projectDataService.getProjectData(defaultTemplatePath);
defaultTemplateAppResourcesPath = defaultTemplateProjectData.appResourcesDirectoryPath;
}

if (this.$fs.exists(defaultTemplateAppResourcesPath)) {
if (defaultTemplateAppResourcesPath && this.$fs.exists(defaultTemplateAppResourcesPath)) {
shelljs.cp('-R', defaultTemplateAppResourcesPath, appPath);
}

Expand All @@ -140,7 +187,8 @@ export class ProjectService implements IProjectService {
}

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

this.$fs.writeJson(projectFilePath, {
"description": "NativeScript Application",
"license": "SEE LICENSE IN <your-license-filename>",
"readme": "NativeScript Application",
"repository": "<fill-your-repository-here>"
});
this.$fs.writeJson(projectFilePath, this.packageJsonDefaultData);

this.setAppId(projectDir, projectId);
}

private get packageJsonDefaultData(): IStringDictionary {
return {
description: "NativeScript Application",
license: "SEE LICENSE IN <your-license-filename>",
readme: "NativeScript Application",
repository: "<fill-your-repository-here>"
};
}

private alterPackageJsonData(projectDir: string, projectId: string): void {
const projectFilePath = path.join(projectDir, this.$staticConfig.PROJECT_FILE_NAME);

const packageJsonData = this.$fs.readJson(projectFilePath);

// Remove the metadata keys from the package.json
let updatedPackageJsonData = _.omitBy<any, any>(packageJsonData, (value: any, key: string) => _.startsWith(key, "_"));
updatedPackageJsonData = _.merge(updatedPackageJsonData, this.packageJsonDefaultData);

if (updatedPackageJsonData.nativescript && updatedPackageJsonData.nativescript.templateVersion) {
delete updatedPackageJsonData.nativescript.templateVersion;
}

this.$fs.writeJson(projectFilePath, updatedPackageJsonData);
this.setAppId(projectDir, projectId);
}

private setAppId(projectDir: string, projectId: string): void {
this.$projectDataService.setNSValue(projectDir, "id", projectId);
}
}
Expand Down
Loading