Skip to content

fix: fix project creation with github url passed as template #4124

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 2 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ interface INodePackageManager {
*/
view(packageName: string, config: Object): Promise<any>;

/**
* Checks if the specified string is name of a packaged published in the NPM registry.
* @param {string} packageName The string to be checked.
* @return {Promise<boolean>} True if the specified string is a registered package name, false otherwise.
*/
isRegistered(packageName: string): Promise<boolean>;

/**
* Separates the package name and version from a specified fullPackageName.
* @param {string} fullPackageName The full name of the package like [email protected].
* @return {INpmPackageNameParts} An object containing the separated package name and version.
*/
getPackageNameParts(fullPackageName: string): INpmPackageNameParts

/**
* Returns the full name of an npm package based on the provided name and version.
* @param {INpmPackageNameParts} packageNameParts An object containing the package name and version.
* @return {string} The full name of the package like [email protected].
*/
getPackageFullName(packageNameParts: INpmPackageNameParts): string

/**
* Searches for a package.
* @param {string[]} filter Keywords with which to perform the search.
Expand Down Expand Up @@ -59,6 +80,7 @@ interface INpmInstallationManager {
getLatestVersion(packageName: string): Promise<string>;
getNextVersion(packageName: string): Promise<string>;
getLatestCompatibleVersion(packageName: string, referenceVersion?: string): Promise<string>;
getLatestCompatibleVersionSafe(packageName: string, referenceVersion?: string): Promise<string>;
getInspectorFromCache(inspectorNpmPackageName: string, projectDir: string): Promise<string>;
}

Expand Down Expand Up @@ -353,6 +375,11 @@ interface INpmsPackageData {
maintainers: INpmsUser[];
}

interface INpmPackageNameParts {
name: string;
version: string;
}

interface IUsername {
username: string;
}
Expand Down
56 changes: 53 additions & 3 deletions lib/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,46 @@ export class NodePackageManager implements INodePackageManager {
return JSON.parse(viewResult);
}

public async isRegistered(packageName: string): Promise<boolean> {
if (this.isURL(packageName) || this.$fs.exists(packageName) || this.isTgz(packageName)) {
return false;
}

try {
const viewResult = await this.view(packageName, { name: true });

// `npm view nonExistingPackageName` will return `nativescript`
// if executed in the root dir of the CLI (npm 6.4.1)
const packageNameRegex = new RegExp(packageName, "i");
const isProperResult = packageNameRegex.test(viewResult);

return isProperResult;
} catch (e) {
return false;
}
}

public getPackageNameParts(fullPackageName: string): INpmPackageNameParts {
// support <reserved_name>@<version> syntax, for example [email protected]
// support <scoped_package_name>@<version> syntax, for example @nativescript/[email protected]
const lastIndexOfAtSign = fullPackageName.lastIndexOf("@");
let version = "";
let templateName = "";
if (lastIndexOfAtSign > 0) {
templateName = fullPackageName.substr(0, lastIndexOfAtSign).toLowerCase();
version = fullPackageName.substr(lastIndexOfAtSign + 1);
}

return {
name: templateName || fullPackageName,
version: version
}
}

public getPackageFullName(packageNameParts: INpmPackageNameParts): string {
return packageNameParts.version ? `${packageNameParts.name}@${packageNameParts.version}` : packageNameParts.name;
}

public async searchNpms(keyword: string): Promise<INpmsResult> {
// TODO: Fix the generation of url - in case it contains @ or / , the call may fail.
const httpRequestResult = await this.$httpClient.httpRequest(`https://api.npms.io/v2/search?q=keywords:${keyword}`);
Expand All @@ -136,6 +176,16 @@ export class NodePackageManager implements INodePackageManager {
return path.join(cachePath.trim(), CACACHE_DIRECTORY_NAME);
}

private isTgz(packageName: string): boolean {
return packageName.indexOf(".tgz") >= 0;
}

private isURL(str: string): boolean {
const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
const url = new RegExp(urlRegex, 'i');
return str.length < 2083 && url.test(str);
}

private getNpmExecutableName(): string {
let npmExecutableName = "npm";

Expand All @@ -153,7 +203,7 @@ export class NodePackageManager implements INodePackageManager {
array.push(`--${flag}`);
array.push(`${config[flag]}`);
} else if (config[flag]) {
if (flag === "dist-tags" || flag === "versions") {
if (flag === "dist-tags" || flag === "versions" || flag === "name") {
array.push(` ${flag}`);
continue;
}
Expand All @@ -171,8 +221,8 @@ export class NodePackageManager implements INodePackageManager {
// TODO: Add tests for this functionality
try {
const originalOutput: INpmInstallCLIResult | INpm5InstallCliResult = JSON.parse(npmDryRunInstallOutput);
const npm5Output = <INpm5InstallCliResult> originalOutput;
const npmOutput = <INpmInstallCLIResult> originalOutput;
const npm5Output = <INpm5InstallCliResult>originalOutput;
const npmOutput = <INpmInstallCLIResult>originalOutput;
let name: string;
_.forOwn(npmOutput.dependencies, (peerDependency: INpmPeerDependencyInfo, key: string) => {
if (!peerDependency.required && !peerDependency.peerMissing) {
Expand Down
30 changes: 13 additions & 17 deletions lib/npm-installation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ export class NpmInstallationManager implements INpmInstallationManager {
return maxSatisfying || latestVersion;
}

public async getLatestCompatibleVersionSafe(packageName: string, referenceVersion?: string): Promise<string> {
let version = "";
const canGetVersionFromNpm = await this.$npm.isRegistered(packageName);
if (canGetVersionFromNpm) {
version = await this.getLatestCompatibleVersion(packageName, referenceVersion);
}

return version;
}

public async install(packageToInstall: string, projectDir: string, opts?: INpmInstallOptions): Promise<any> {
try {
const pathToSave = projectDir;
Expand Down Expand Up @@ -100,6 +110,7 @@ export class NpmInstallationManager implements INpmInstallationManager {
if (this.$fs.exists(pathToInspector)) {
return true;
}

return false;
}

Expand All @@ -109,28 +120,13 @@ export class NpmInstallationManager implements INpmInstallationManager {
packageName = possiblePackageName;
}

// check if the packageName is url or local file and if it is, let npm install deal with the version
if (this.isURL(packageName) || this.$fs.exists(packageName) || this.isTgz(packageName)) {
version = null;
} else {
version = version || await this.getLatestCompatibleVersion(packageName);
}

version = version || await this.getLatestCompatibleVersionSafe(packageName);
const installResultInfo = await this.npmInstall(packageName, pathToSave, version, dependencyType);
const installedPackageName = installResultInfo.name;

const pathToInstalledPackage = path.join(pathToSave, "node_modules", installedPackageName);
return pathToInstalledPackage;
}

private isTgz(packageName: string): boolean {
return packageName.indexOf(".tgz") >= 0;
}

private isURL(str: string): boolean {
const urlRegex = '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
const url = new RegExp(urlRegex, 'i');
return str.length < 2083 && url.test(str);
return pathToInstalledPackage;
}

private async npmInstall(packageName: string, pathToSave: string, version: string, dependencyType: string): Promise<INpmInstallResultInfo> {
Expand Down
35 changes: 13 additions & 22 deletions lib/services/project-templates-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,33 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
private $logger: ILogger,
private $npmInstallationManager: INpmInstallationManager,
private $pacoteService: IPacoteService,
private $errors: IErrors) { }
private $errors: IErrors,
private $npm: INodePackageManager) { }

public async prepareTemplate(originalTemplateName: string, projectDir: string): Promise<ITemplateData> {
if (!originalTemplateName) {
originalTemplateName = constants.RESERVED_TEMPLATE_NAMES["default"];
public async prepareTemplate(templateValue: string, projectDir: string): Promise<ITemplateData> {
if (!templateValue) {
templateValue = constants.RESERVED_TEMPLATE_NAMES["default"];
}

// support <reserved_name>@<version> syntax, for example [email protected]
// support <scoped_package_name>@<version> syntax, for example @nativescript/[email protected]
const lastIndexOfAtSign = originalTemplateName.lastIndexOf("@");
let name = originalTemplateName;
let version = "";
if (lastIndexOfAtSign > 0) {
name = originalTemplateName.substr(0, lastIndexOfAtSign);
version = originalTemplateName.substr(lastIndexOfAtSign + 1);
}
const templateNameParts = this.$npm.getPackageNameParts(templateValue);
templateValue = constants.RESERVED_TEMPLATE_NAMES[templateNameParts.name] || templateNameParts.name;

const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name;
if (!this.$fs.exists(templateName)) {
version = version || await this.$npmInstallationManager.getLatestCompatibleVersion(templateName);
}
let version = templateNameParts.version || await this.$npmInstallationManager.getLatestCompatibleVersionSafe(templateValue);
const fullTemplateName = this.$npm.getPackageFullName({ name: templateValue, version: version });

const fullTemplateName = version ? `${templateName}@${version}` : templateName;
const templatePackageJsonContent = await this.getTemplatePackageJsonContent(fullTemplateName);
const templateVersion = await this.getTemplateVersion(fullTemplateName);

let templatePath = null;
if (templateVersion === constants.TemplateVersions.v1) {
templatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
templatePath = await this.prepareNativeScriptTemplate(templateValue, version, projectDir);
// this removes dependencies from templates so they are not copied to app folder
this.$fs.deleteDirectory(path.join(templatePath, constants.NODE_MODULES_FOLDER_NAME));
}

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

const templateNameToBeTracked = this.getTemplateNameToBeTracked(templateName, templatePackageJsonContent);
const templateNameToBeTracked = this.getTemplateNameToBeTracked(templateValue, templatePackageJsonContent);
if (templateNameToBeTracked) {
await this.$analyticsService.trackEventActionInGoogleAnalytics({
action: constants.TrackActionNames.CreateProject,
Expand All @@ -61,7 +52,7 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
});
}

return { templateName, templatePath, templateVersion, templatePackageJsonContent, version };
return { templateName: templateValue, templatePath, templateVersion, templatePackageJsonContent, version };
}

private async getTemplateVersion(templateName: string): Promise<string> {
Expand Down
88 changes: 88 additions & 0 deletions test/node-package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Yok } from "../lib/common/yok";
import * as stubs from "./stubs";
import { assert } from "chai";
import { NodePackageManager } from "../lib/node-package-manager";

function createTestInjector(configuration: {
} = {}): IInjector {
const injector = new Yok();
injector.register("hostInfo", {});
injector.register("errors", stubs.ErrorsStub);
injector.register("logger", stubs.LoggerStub);
injector.register("childProcess", stubs.ChildProcessStub);
injector.register("httpClient", {});
injector.register("fs", stubs.FileSystemStub);
injector.register("npm", NodePackageManager);

return injector;
}

describe.only("node-package-manager", () => {

describe("getPackageNameParts", () => {
[
{
name: "should return both name and version when valid fullName passed",
templateFullName: "[email protected]",
expectedVersion: "1.0.0",
expectedName: "some-template",
},
{
name: "should return both name and version when valid fullName with scope passed",
templateFullName: "@nativescript/[email protected]",
expectedVersion: "1.0.0",
expectedName: "@nativescript/some-template",
},
{
name: "should return only name when version is not specified and the template is scoped",
templateFullName: "@nativescript/some-template",
expectedVersion: "",
expectedName: "@nativescript/some-template",
},
{
name: "should return only name when version is not specified",
templateFullName: "some-template",
expectedVersion: "",
expectedName: "some-template",
}
].forEach(testCase => {
it(testCase.name, async () => {
const testInjector = createTestInjector();
const npm = testInjector.resolve<NodePackageManager>("npm");
const templateNameParts = await npm.getPackageNameParts(testCase.templateFullName);
assert.strictEqual(templateNameParts.name, testCase.expectedName);
assert.strictEqual(templateNameParts.version, testCase.expectedVersion);
});
});
});

describe("getPackageFullName", () => {
[
{
name: "should return name and version when specified",
templateName: "some-template",
templateVersion: "1.0.0",
expectedFullName: "[email protected]",
},
{
name: "should return only the github url when no version specified",
templateName: "https://github.com/NativeScript/template-drawer-navigation-ng#master",
templateVersion: "",
expectedFullName: "https://github.com/NativeScript/template-drawer-navigation-ng#master",
},
{
name: "should return only the name when no version specified",
templateName: "some-template",
templateVersion: "",
expectedFullName: "some-template",
}
].forEach(testCase => {
it(testCase.name, async () => {
const testInjector = createTestInjector();
const npm = testInjector.resolve<NodePackageManager>("npm");
const templateFullName = await npm.getPackageFullName({ name: testCase.templateName, version: testCase.templateVersion });
assert.strictEqual(templateFullName, testCase.expectedFullName);
});
});
});
});
Loading