Skip to content

Commit c3e16d6

Browse files
Merge pull request #4614 from NativeScript/vladimirov/fix-plugin-create
fix: plugin create command needs more args for plugin seed
2 parents 2376eea + 936cdff commit c3e16d6

File tree

6 files changed

+103
-15
lines changed

6 files changed

+103
-15
lines changed

docs/man_pages/lib-management/plugin-create.md

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Create from a custom plugin seed | `$ tns plugin create <Plugin Repository Name>
2828
* `--path` - Specifies the directory where you want to create the project, if different from the current directory.
2929
* `--username` - Specifies the Github username, which will be used to build the URLs in the plugin's package.json file.
3030
* `--pluginName` - Used to set the default file and class names in the plugin source.
31+
* `--includeTypeScriptDemo` - Specifies if TypeScript demo should be created. Default value is `y` (i.e. `demo` will be created), in case you do not want to create this demo, pass `--includeTypeScriptDemo=n`
32+
* `--includeAngularDemo` - Specifies if Angular demo should be created. Default value is `y` (i.e. `demo-angular` will be created), in case you do not want to create this demo, pass `--includeAngularDemo=n`
3133
* `--template` - Specifies the custom seed archive, which you want to use to create your plugin. If `--template` is not set, the NativeScript CLI creates the plugin from the default NativeScript Plugin Seed. `<Template>` can be a URL or a local path to a `.tar.gz` file with the contents of a seed repository.<% if(isHtml) { %> This must be a clone of the [NativeScript Plugin Seed](https://github.com/NativeScript/nativescript-plugin-seed) and must contain a `src` directory with a package.json file and a script at `src/scripts/postclone.js`. After the archive is extracted, the postclone script will be executed with the username (`gitHubUsername`) and plugin name (`pluginName`) parameters given to the `tns plugin create` command prompts. For more information, visit the default plugin seed repository and [examine the source script](https://github.com/NativeScript/nativescript-plugin-seed/blob/master/src/scripts/postclone.js) there. Examples:
3234

3335
* Using a local file:

lib/commands/plugin/create-plugin.ts

+27-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export class CreatePluginCommand implements ICommand {
55
public allowedParameters: ICommandParameter[] = [];
66
public userMessage = "What is your GitHub username?\n(will be used to update the Github URLs in the plugin's package.json)";
77
public nameMessage = "What will be the name of your plugin?\n(use lowercase characters and dashes only)";
8+
public includeTypeScriptDemoMessage = 'Do you want to include a "TypeScript NativeScript" application linked with your plugin to make development easier?';
9+
public includeAngularDemoMessage = 'Do you want to include an "Angular NativeScript" application linked with your plugin to make development easier?';
810
public pathAlreadyExistsMessageTemplate = "Path already exists and is not empty %s";
911
constructor(private $options: IOptions,
1012
private $errors: IErrors,
@@ -62,15 +64,25 @@ export class CreatePluginCommand implements ICommand {
6264

6365
const gitHubUsername = await this.getGitHubUsername(config.username);
6466
const pluginNameSource = await this.getPluginNameSource(config.pluginName, pluginRepoName);
67+
const includeTypescriptDemo = await this.getShouldIncludeDemoResult(config.includeTypeScriptDemo, this.includeTypeScriptDemoMessage);
68+
const includeAngularDemo = await this.getShouldIncludeDemoResult(config.includeAngularDemo, this.includeAngularDemoMessage);
6569

66-
if (!isInteractive() && (!config.username || !config.pluginName)) {
67-
this.$logger.printMarkdown("Using default values for Github user and/or plugin name since your shell is not interactive.");
70+
if (!isInteractive() && (!config.username || !config.pluginName || !config.includeAngularDemo || !config.includeTypeScriptDemo)) {
71+
this.$logger.printMarkdown("Using default values for plugin creation options since your shell is not interactive.");
6872
}
6973

7074
// run postclone script manually and kill it if it takes more than 10 sec
7175
const pathToPostCloneScript = path.join("scripts", "postclone");
72-
const params = [pathToPostCloneScript, `gitHubUsername=${gitHubUsername}`, `pluginName=${pluginNameSource}`, "initGit=y"];
73-
const outputScript = (await this.$childProcess.spawnFromEvent(process.execPath, params, "close", { cwd, timeout: 10000 }));
76+
const params = [
77+
pathToPostCloneScript,
78+
`gitHubUsername=${gitHubUsername}`,
79+
`pluginName=${pluginNameSource}`,
80+
"initGit=y",
81+
`includeTypeScriptDemo=${includeTypescriptDemo}`,
82+
`includeAngularDemo=${includeAngularDemo}`
83+
];
84+
85+
const outputScript = (await this.$childProcess.spawnFromEvent(process.execPath, params, "close", { stdio: "inherit", cwd, timeout: 10000 }));
7486
if (outputScript && outputScript.stdout) {
7587
this.$logger.printMarkdown(outputScript.stdout);
7688
}
@@ -101,7 +113,7 @@ export class CreatePluginCommand implements ICommand {
101113
}
102114
}
103115

104-
private async getGitHubUsername(gitHubUsername: string) {
116+
private async getGitHubUsername(gitHubUsername: string): Promise<string> {
105117
if (!gitHubUsername) {
106118
gitHubUsername = "NativeScriptDeveloper";
107119
if (isInteractive()) {
@@ -112,7 +124,7 @@ export class CreatePluginCommand implements ICommand {
112124
return gitHubUsername;
113125
}
114126

115-
private async getPluginNameSource(pluginNameSource: string, pluginRepoName: string) {
127+
private async getPluginNameSource(pluginNameSource: string, pluginRepoName: string): Promise<string> {
116128
if (!pluginNameSource) {
117129
// remove nativescript- prefix for naming plugin files
118130
const prefix = 'nativescript-';
@@ -124,6 +136,15 @@ export class CreatePluginCommand implements ICommand {
124136

125137
return pluginNameSource;
126138
}
139+
140+
private async getShouldIncludeDemoResult(includeDemoOption: string, message: string): Promise<string> {
141+
let shouldIncludeDemo = !!includeDemoOption;
142+
if (!includeDemoOption && isInteractive()) {
143+
shouldIncludeDemo = await this.$prompter.confirm(message, () => { return true; });
144+
}
145+
146+
return shouldIncludeDemo ? "y" : "n";
147+
}
127148
}
128149

129150
$injector.registerCommand(["plugin|create"], CreatePluginCommand);

lib/declarations.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ interface IPort {
493493
interface IPluginSeedOptions {
494494
username: string;
495495
pluginName: string;
496+
includeTypeScriptDemo: string;
497+
includeAngularDemo: string;
496498
}
497499

498500
interface IAndroidBundleOptions {

lib/options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export class Options {
120120
background: { type: OptionType.String, hasSensitiveValue: false },
121121
username: { type: OptionType.String, hasSensitiveValue: true },
122122
pluginName: { type: OptionType.String, hasSensitiveValue: false },
123+
includeTypeScriptDemo: { type: OptionType.String, hasSensitiveValue: false },
124+
includeAngularDemo: { type: OptionType.String, hasSensitiveValue: false },
123125
hmr: { type: OptionType.Boolean, hasSensitiveValue: false },
124126
collection: { type: OptionType.String, alias: "c", hasSensitiveValue: false },
125127
json: { type: OptionType.Boolean, hasSensitiveValue: false },

test/plugin-create.ts

+60-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const dummyProjectName = "dummyProjectName";
1919
const dummyArgs = [dummyProjectName];
2020
const dummyUser = "devUsername";
2121
const dummyName = "devPlugin";
22+
const createDemoProjectAnswer = true;
23+
const creteDemoProjectOption = "y";
2224
const dummyPacote: IPacoteOutput = { packageName: "", destinationDirectory: "" };
2325

2426
function createTestInjector() {
@@ -104,48 +106,99 @@ describe("Plugin create command tests", () => {
104106
await createPluginCommand.execute(dummyArgs);
105107
});
106108

107-
it("should pass when only project name is set with prompts in interactive shell.", async () => {
109+
it("should pass when all options are set with prompts in interactive shell.", async () => {
108110
const prompter = testInjector.resolve("$prompter");
109111
const strings: IDictionary<string> = {};
112+
const confirmQuestions: IDictionary<boolean> = {};
110113
strings[createPluginCommand.userMessage] = dummyUser;
111114
strings[createPluginCommand.nameMessage] = dummyName;
115+
confirmQuestions[createPluginCommand.includeTypeScriptDemoMessage] = createDemoProjectAnswer;
116+
confirmQuestions[createPluginCommand.includeAngularDemoMessage] = createDemoProjectAnswer;
112117

113118
prompter.expect({
114-
strings: strings
119+
strings: strings,
120+
confirmQuestions
115121
});
116122
await createPluginCommand.execute(dummyArgs);
117123
prompter.assert();
118124
});
119125

120-
it("should pass with project name and username set with one prompt in interactive shell.", async () => {
126+
it("should pass when username is passed with command line option and all other options are populated with prompts in interactive shell.", async () => {
121127
options.username = dummyUser;
122128
const prompter = testInjector.resolve("$prompter");
123129
const strings: IDictionary<string> = {};
130+
const confirmQuestions: IDictionary<boolean> = {};
124131
strings[createPluginCommand.nameMessage] = dummyName;
132+
confirmQuestions[createPluginCommand.includeTypeScriptDemoMessage] = createDemoProjectAnswer;
133+
confirmQuestions[createPluginCommand.includeAngularDemoMessage] = createDemoProjectAnswer;
125134

126135
prompter.expect({
127-
strings: strings
136+
strings: strings,
137+
confirmQuestions
128138
});
129139
await createPluginCommand.execute(dummyArgs);
130140
prompter.assert();
131141
});
132142

133-
it("should pass with project name and pluginName set with one prompt in interactive shell.", async () => {
143+
it("should pass when plugin name is passed with command line option and all other options are populated with prompts in interactive shell.", async () => {
134144
options.pluginName = dummyName;
135145
const prompter = testInjector.resolve("$prompter");
136146
const strings: IDictionary<string> = {};
147+
const confirmQuestions: IDictionary<boolean> = {};
148+
137149
strings[createPluginCommand.userMessage] = dummyUser;
150+
confirmQuestions[createPluginCommand.includeTypeScriptDemoMessage] = createDemoProjectAnswer;
151+
confirmQuestions[createPluginCommand.includeAngularDemoMessage] = createDemoProjectAnswer;
138152

139153
prompter.expect({
140-
strings: strings
154+
strings,
155+
confirmQuestions
141156
});
142157
await createPluginCommand.execute(dummyArgs);
143158
prompter.assert();
144159
});
145160

146-
it("should pass with project name, username and pluginName set with no prompt in interactive shell.", async () => {
161+
it("should pass when includeTypeScriptDemo is passed with command line option and all other options are populated with prompts in interactive shell.", async () => {
162+
options.includeTypeScriptDemo = creteDemoProjectOption;
163+
const prompter = testInjector.resolve("$prompter");
164+
const strings: IDictionary<string> = {};
165+
const confirmQuestions: IDictionary<boolean> = {};
166+
strings[createPluginCommand.userMessage] = dummyUser;
167+
strings[createPluginCommand.nameMessage] = dummyName;
168+
confirmQuestions[createPluginCommand.includeAngularDemoMessage] = createDemoProjectAnswer;
169+
170+
prompter.expect({
171+
strings: strings,
172+
confirmQuestions
173+
});
174+
await createPluginCommand.execute(dummyArgs);
175+
prompter.assert();
176+
});
177+
178+
it("should pass when includeAngularDemo is passed with command line option and all other options are populated with prompts in interactive shell.", async () => {
179+
options.includeAngularDemo = creteDemoProjectOption;
180+
const prompter = testInjector.resolve("$prompter");
181+
const strings: IDictionary<string> = {};
182+
const confirmQuestions: IDictionary<boolean> = {};
183+
184+
strings[createPluginCommand.userMessage] = dummyUser;
185+
strings[createPluginCommand.nameMessage] = dummyName;
186+
confirmQuestions[createPluginCommand.includeTypeScriptDemoMessage] = createDemoProjectAnswer;
187+
188+
prompter.expect({
189+
strings: strings,
190+
confirmQuestions
191+
});
192+
await createPluginCommand.execute(dummyArgs);
193+
prompter.assert();
194+
});
195+
196+
it("should pass with all options passed through command line opts with no prompt in interactive shell.", async () => {
147197
options.username = dummyUser;
148198
options.pluginName = dummyName;
199+
options.includeTypeScriptDemo = creteDemoProjectOption;
200+
options.includeAngularDemo = creteDemoProjectOption;
201+
149202
await createPluginCommand.execute(dummyArgs);
150203
});
151204

test/stubs.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -571,13 +571,15 @@ export class PrompterStub implements IPrompter {
571571
private passwords: IDictionary<string> = {};
572572
private answers: IDictionary<string> = {};
573573
private questionChoices: IDictionary<any[]> = {};
574+
private confirmQuestions: IDictionary<boolean> = {};
574575

575-
expect(options?: { strings?: IDictionary<string>, passwords?: IDictionary<string>, answers?: IDictionary<string>, questionChoices?: IDictionary<any[]> }) {
576+
expect(options?: { strings?: IDictionary<string>, passwords?: IDictionary<string>, answers?: IDictionary<string>, questionChoices?: IDictionary<any[]>, confirmQuestions?: IDictionary<boolean> }) {
576577
if (options) {
577578
this.strings = options.strings || this.strings;
578579
this.passwords = options.passwords || this.passwords;
579580
this.answers = options.answers || this.answers;
580581
this.questionChoices = options.questionChoices || this.questionChoices;
582+
this.confirmQuestions = options.confirmQuestions || this.confirmQuestions;
581583
}
582584
}
583585

@@ -607,7 +609,10 @@ export class PrompterStub implements IPrompter {
607609
return result;
608610
}
609611
async confirm(message: string, defaultAction?: () => boolean): Promise<boolean> {
610-
throw unreachable();
612+
chai.assert.ok(message in this.confirmQuestions, `PrompterStub didn't expect to be asked for: ${message}`);
613+
const result = this.confirmQuestions[message];
614+
delete this.confirmQuestions[message];
615+
return result;
611616
}
612617
dispose(): void {
613618
throw unreachable();
@@ -620,6 +625,9 @@ export class PrompterStub implements IPrompter {
620625
for (const key in this.passwords) {
621626
throw unexpected(`PrompterStub was instructed to reply with "${this.passwords[key]}" to a "${key}" password request, but was never asked!`);
622627
}
628+
for (const key in this.confirmQuestions) {
629+
throw unexpected(`PrompterStub was instructed to reply with "${this.confirmQuestions[key]}" to a "${key}" confirm question, but was never asked!`);
630+
}
623631
}
624632
}
625633

0 commit comments

Comments
 (0)