Skip to content

Commit e3c2b10

Browse files
Merge pull request #1642 from NativeScript/milanov/fix-create-project-name-validation
Added check if project name starts with number when creating new proj…
2 parents 3f5fd84 + ff2ae76 commit e3c2b10

6 files changed

+283
-6
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ $injector.require("androidProjectService", "./services/android-project-service")
1212
$injector.require("iOSProjectService", "./services/ios-project-service");
1313

1414
$injector.require("projectTemplatesService", "./services/project-templates-service");
15+
$injector.require("projectNameService", "./services/project-name-service");
1516
$injector.require("tnsModulesService", "./services/tns-modules-service");
1617

1718
$injector.require("platformsData", "./platforms-data");

lib/declarations.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -159,23 +159,23 @@ interface IAndroidToolsInfo {
159159
* @param {any} options Defines if the warning messages should treated as error and if the targetSdk value should be validated as well.
160160
* @return {boolean} True if there are detected issues, false otherwise.
161161
*/
162-
validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture<boolean>;
162+
validateInfo(options?: { showWarningsAsErrors: boolean, validateTargetSdk: boolean }): IFuture<boolean>;
163163

164164
/**
165165
* Validates the information about required JAVA version.
166166
* @param {string} installedJavaVersion The JAVA version that will be checked.
167167
* @param {any} options Defines if the warning messages should treated as error.
168168
* @return {boolean} True if there are detected issues, false otherwise.
169169
*/
170-
validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture<boolean>;
170+
validateJavacVersion(installedJavaVersion: string, options?: { showWarningsAsErrors: boolean }): IFuture<boolean>;
171171

172172
/**
173173
* Returns the path to `android` executable. It should be `$ANDROID_HOME/tools/android`.
174174
* In case ANDROID_HOME is not defined, check if `android` is part of $PATH.
175175
* @param {any} options Defines if the warning messages should treated as error.
176176
* @return {string} Path to the `android` executable.
177177
*/
178-
getPathToAndroidExecutable(options?: {showWarningsAsErrors: boolean}): IFuture<string>;
178+
getPathToAndroidExecutable(options?: { showWarningsAsErrors: boolean }): IFuture<string>;
179179

180180
/**
181181
* Gets the path to `adb` executable from ANDROID_HOME. It should be `$ANDROID_HOME/platform-tools/adb` in case it exists.
@@ -256,3 +256,16 @@ interface IXmlValidator {
256256
*/
257257
getXmlFileErrors(sourceFile: string): IFuture<string>;
258258
}
259+
260+
/**
261+
* Describes methods for project name.
262+
*/
263+
interface IProjectNameService {
264+
/**
265+
* Ensures the passed project name is valid. If the project name is not valid prompts for actions.
266+
* @param {string} project name to be checked.
267+
* @param {IOptions} current command options.
268+
* @return {IFuture<strng>} returns the selected name of the project.
269+
*/
270+
ensureValidName(projectName: string, validateOptions?: {force: boolean}): IFuture<string>;
271+
}

lib/services/project-name-service.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use strict";
2+
3+
import { isInteractive } from "../common/helpers";
4+
5+
export class ProjectNameService implements IProjectNameService {
6+
constructor(private $projectNameValidator: IProjectNameValidator,
7+
private $errors: IErrors,
8+
private $logger: ILogger,
9+
private $prompter: IPrompter) { }
10+
11+
public ensureValidName(projectName: string, validateOptions?: { force: boolean }): IFuture<string> {
12+
return (() => {
13+
if (validateOptions && validateOptions.force) {
14+
return projectName;
15+
}
16+
17+
if (!this.$projectNameValidator.validate(projectName)) {
18+
return this.promptForNewName(projectName, validateOptions).wait();
19+
}
20+
21+
if (!this.checkIfNameStartsWithLetter(projectName)) {
22+
if (!isInteractive()) {
23+
this.$errors.fail("The project name does not start with letter and will fail to build for Android. If You want to create project with this name add --force to the create command.");
24+
return;
25+
}
26+
27+
return this.promptForNewName(projectName, validateOptions).wait();
28+
}
29+
30+
return projectName;
31+
}).future<string>()();
32+
}
33+
34+
private checkIfNameStartsWithLetter(projectName: string): boolean {
35+
let startsWithLetterExpression = /^[a-zA-Z]/;
36+
return startsWithLetterExpression.test(projectName);
37+
}
38+
39+
private promptForNewName(projectName: string, validateOptions?: { force: boolean }): IFuture<string> {
40+
return (() => {
41+
if (this.promptForForceNameConfirm().wait()) {
42+
return projectName;
43+
}
44+
45+
let newProjectName = this.$prompter.getString("Enter the new project name:").wait();
46+
return this.ensureValidName(newProjectName, validateOptions).wait();
47+
}).future<string>()();
48+
}
49+
50+
private promptForForceNameConfirm(): IFuture<boolean> {
51+
return (() => {
52+
this.$logger.warn("The project name does not start with letter and will fail to build for Android.");
53+
54+
return this.$prompter.confirm("Do you want to create the project with this name?").wait();
55+
}).future<boolean>()();
56+
}
57+
}
58+
59+
$injector.register("projectNameService", ProjectNameService);

lib/services/project-service.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class ProjectService implements IProjectService {
1414
private $logger: ILogger,
1515
private $projectDataService: IProjectDataService,
1616
private $projectHelper: IProjectHelper,
17-
private $projectNameValidator: IProjectNameValidator,
17+
private $projectNameService: IProjectNameService,
1818
private $projectTemplatesService: IProjectTemplatesService,
1919
private $options: IOptions) { }
2020

@@ -23,7 +23,8 @@ export class ProjectService implements IProjectService {
2323
if (!projectName) {
2424
this.$errors.fail("You must specify <App name> when creating a new project.");
2525
}
26-
this.$projectNameValidator.validate(projectName);
26+
27+
projectName = this.$projectNameService.ensureValidName(projectName, {force: this.$options.force}).wait();
2728

2829
let projectId = this.$options.appid || this.$projectHelper.generateDefaultAppId(projectName, constants.DEFAULT_APP_IDENTIFIER_PREFIX);
2930

test/project-name-service.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/// <reference path=".d.ts" />
2+
"use strict";
3+
4+
import {Yok} from "../lib/common/yok";
5+
import {ProjectNameService} from "../lib/services/project-name-service";
6+
import {assert} from "chai";
7+
import {ErrorsStub, LoggerStub} from "./stubs";
8+
import Future = require("fibers/future");
9+
10+
let mockProjectNameValidator = {
11+
validate: () => true
12+
};
13+
14+
let dummyString: string = "dummyString";
15+
16+
function createTestInjector(): IInjector {
17+
let testInjector: IInjector;
18+
19+
testInjector = new Yok();
20+
testInjector.register("projectNameService", ProjectNameService);
21+
testInjector.register("projectNameValidator", mockProjectNameValidator);
22+
testInjector.register("errors", ErrorsStub);
23+
testInjector.register("logger", LoggerStub);
24+
testInjector.register("prompter", {
25+
confirm: (message: string): IFuture<boolean> => Future.fromResult(true),
26+
getString: (message: string): IFuture<string> => Future.fromResult(dummyString)
27+
});
28+
29+
return testInjector;
30+
}
31+
32+
describe("Project Name Service Tests", () => {
33+
let testInjector: IInjector;
34+
let projectNameService: IProjectNameService;
35+
let validProjectName = "valid";
36+
let invalidProjectName = "1invalid";
37+
38+
beforeEach(() => {
39+
testInjector = createTestInjector();
40+
projectNameService = testInjector.resolve("projectNameService");
41+
});
42+
43+
it("returns correct name when valid name is entered", () => {
44+
let actualProjectName = projectNameService.ensureValidName(validProjectName).wait();
45+
46+
assert.deepEqual(actualProjectName, validProjectName);
47+
});
48+
49+
it("returns correct name when invalid name is entered several times and then valid name is entered", () => {
50+
let prompter = testInjector.resolve("prompter");
51+
prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(false);
52+
53+
let incorrectInputsLimit = 5;
54+
let incorrectInputsCount = 0;
55+
56+
prompter.getString = (message: string): IFuture<string> => {
57+
return (() => {
58+
if (incorrectInputsCount < incorrectInputsLimit) {
59+
incorrectInputsCount++;
60+
61+
return invalidProjectName;
62+
} else {
63+
return validProjectName;
64+
}
65+
}).future<string>()();
66+
};
67+
68+
let actualProjectName = projectNameService.ensureValidName(invalidProjectName).wait();
69+
70+
assert.deepEqual(actualProjectName, validProjectName);
71+
});
72+
73+
it("returns the invalid name when invalid name is entered and --force flag is present", () => {
74+
let actualProjectName = projectNameService.ensureValidName(validProjectName, { force: true }).wait();
75+
76+
assert.deepEqual(actualProjectName, validProjectName);
77+
});
78+
});

test/project-service.ts

+126-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as stubs from "./stubs";
66
import * as constants from "./../lib/constants";
77
import {ChildProcess} from "../lib/common/child-process";
88
import * as ProjectServiceLib from "../lib/services/project-service";
9+
import {ProjectNameService} from "../lib/services/project-name-service";
910
import * as ProjectDataServiceLib from "../lib/services/project-data-service";
1011
import * as ProjectDataLib from "../lib/project-data";
1112
import * as ProjectHelperLib from "../lib/common/project-helper";
@@ -21,11 +22,16 @@ import {assert} from "chai";
2122
import {Options} from "../lib/options";
2223
import {HostInfo} from "../lib/common/host-info";
2324
import {ProjectTemplatesService} from "../lib/services/project-templates-service";
25+
import Future = require("fibers/future");
2426

2527
let mockProjectNameValidator = {
26-
validate: () => { return true; }
28+
validate: () => true
2729
};
2830

31+
let dummyString: string = "dummyString";
32+
let hasPromptedForString = false;
33+
let originalIsInteractive = helpers.isInteractive;
34+
2935
temp.track();
3036

3137
class ProjectIntegrationTest {
@@ -121,6 +127,7 @@ class ProjectIntegrationTest {
121127
this.testInjector.register("errors", stubs.ErrorsStub);
122128
this.testInjector.register('logger', stubs.LoggerStub);
123129
this.testInjector.register("projectService", ProjectServiceLib.ProjectService);
130+
this.testInjector.register("projectNameService", ProjectNameService);
124131
this.testInjector.register("projectHelper", ProjectHelperLib.ProjectHelper);
125132
this.testInjector.register("projectTemplatesService", ProjectTemplatesService);
126133
this.testInjector.register("projectNameValidator", mockProjectNameValidator);
@@ -136,6 +143,15 @@ class ProjectIntegrationTest {
136143

137144
this.testInjector.register("options", Options);
138145
this.testInjector.register("hostInfo", HostInfo);
146+
this.testInjector.register("prompter", {
147+
confirm: (message: string): IFuture<boolean> => Future.fromResult(true),
148+
getString: (message: string): IFuture<string> => {
149+
return (() => {
150+
hasPromptedForString = true;
151+
return dummyString;
152+
}).future<string>()();
153+
}
154+
});
139155
}
140156
}
141157

@@ -299,6 +315,115 @@ describe("Project Service Tests", () => {
299315
projectIntegrationTest.createProject(projectName).wait();
300316
projectIntegrationTest.assertProject(tempFolder, projectName, options.appid).wait();
301317
});
318+
319+
describe("project name validation tests", () => {
320+
let validProjectName = "valid";
321+
let invalidProjectName = "1invalid";
322+
let projectIntegrationTest: ProjectIntegrationTest;
323+
let tempFolder: string;
324+
let options: IOptions;
325+
let prompter: IPrompter;
326+
327+
beforeEach(() => {
328+
hasPromptedForString = false;
329+
helpers.isInteractive = () => true;
330+
projectIntegrationTest = new ProjectIntegrationTest();
331+
tempFolder = temp.mkdirSync("project");
332+
options = projectIntegrationTest.testInjector.resolve("options");
333+
prompter = projectIntegrationTest.testInjector.resolve("prompter");
334+
});
335+
336+
afterEach(() => {
337+
helpers.isInteractive = originalIsInteractive;
338+
});
339+
340+
it("creates project when is interactive and incorrect name is specified and the --force option is set", () => {
341+
let projectName = invalidProjectName;
342+
343+
options.force = true;
344+
options.path = tempFolder;
345+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
346+
347+
projectIntegrationTest.createProject(projectName).wait();
348+
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
349+
});
350+
351+
it("creates project when is interactive and incorrect name is specified and the user confirms to use the incorrect name", () => {
352+
let projectName = invalidProjectName;
353+
prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(true);
354+
355+
options.path = tempFolder;
356+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
357+
358+
projectIntegrationTest.createProject(projectName).wait();
359+
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
360+
});
361+
362+
it("prompts for new name when is interactive and incorrect name is specified and the user does not confirm to use the incorrect name", () => {
363+
let projectName = invalidProjectName;
364+
365+
prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(false);
366+
367+
options.path = tempFolder;
368+
369+
projectIntegrationTest.createProject(projectName).wait();
370+
assert.isTrue(hasPromptedForString);
371+
});
372+
373+
it("creates project when is interactive and incorrect name is specified and the user does not confirm to use the incorrect name and enters incorrect name again several times and then enters correct name", () => {
374+
let projectName = invalidProjectName;
375+
376+
prompter.confirm = (message: string): IFuture<boolean> => Future.fromResult(false);
377+
378+
let incorrectInputsLimit = 5;
379+
let incorrectInputsCount = 0;
380+
381+
prompter.getString = (message: string): IFuture<string> => {
382+
return (() => {
383+
if (incorrectInputsCount < incorrectInputsLimit) {
384+
incorrectInputsCount++;
385+
}
386+
else {
387+
hasPromptedForString = true;
388+
389+
return validProjectName;
390+
}
391+
392+
return projectName;
393+
}).future<string>()();
394+
};
395+
396+
options.path = tempFolder;
397+
398+
projectIntegrationTest.createProject(projectName).wait();
399+
assert.isTrue(hasPromptedForString);
400+
});
401+
402+
it("does not create project when is not interactive and incorrect name is specified", () => {
403+
let projectName = invalidProjectName;
404+
helpers.isInteractive = () => false;
405+
406+
options.force = false;
407+
options.path = tempFolder;
408+
409+
assert.throws(() => {
410+
projectIntegrationTest.createProject(projectName).wait();
411+
});
412+
});
413+
414+
it("creates project when is not interactive and incorrect name is specified and the --force option is set", () => {
415+
let projectName = invalidProjectName;
416+
helpers.isInteractive = () => false;
417+
418+
options.force = true;
419+
options.path = tempFolder;
420+
421+
projectIntegrationTest.createProject(projectName).wait();
422+
options.copyFrom = projectIntegrationTest.getNpmPackagePath("tns-template-hello-world").wait();
423+
projectIntegrationTest.assertProject(tempFolder, projectName, `org.nativescript.${projectName}`).wait();
424+
});
425+
});
426+
302427
});
303428
});
304429

0 commit comments

Comments
 (0)