-
-
Notifications
You must be signed in to change notification settings - Fork 197
feat: add create plugin command #3800
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<% if (isJekyll) { %>--- | ||
title: tns plugin create | ||
position: 1 | ||
---<% } %> | ||
# tns plugin create | ||
|
||
Usage | Synopsis | ||
---|--- | ||
Create a new plugin | `$ tns plugin create <Plugin Repository Name> [--path <Directory>]` | ||
|
||
Creates a new project for NativeScript plugin development. The project uses the [NativeScript Plugin Seed](https://github.com/NativeScript/nativescript-plugin-seed) as a base and contains the following directories: | ||
|
||
* `src` - source code of the plugin | ||
* `demo` - simple NativeScript application used to test and show plugin features | ||
* `publish` - shell scripts used to build and pack the plugin source code and publish it in [NPM](https://www.npmjs.com/) | ||
|
||
The project is setup for easy commit in Github, which is why the command will ask you for your Github username. | ||
<% if(isHtml) { %>Before starting to code your first plugin, you can visit the NativeScript documentation page for [building plugins](https://docs.nativescript.org/plugins/building-plugins#step-2-set-up-a-development-workflow) or the [plugin seed repository](https://github.com/NativeScript/nativescript-plugin-seed/blob/master/README.md).<% } %> | ||
|
||
### Options | ||
|
||
* `--path` - Specifies the directory where you want to create the project, if different from the current directory. | ||
* `--username` - Specifies the Github username, which will be used to build the URLs in the plugin's package.json file. | ||
* `--pluginName` - Used to set the default file and class names in the plugin source. | ||
|
||
### Attributes | ||
|
||
* `<Plugin Repository Name>` is the name of repository where your plugin will reside. A directory with the same name will be created. For example: `nativescript-awesome-list`. If a directory with the name already exists and is not empty, the plugin create command will fail. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import * as path from "path"; | ||
import { isInteractive } from "../../common/helpers"; | ||
|
||
export class CreatePluginCommand implements ICommand { | ||
public allowedParameters: ICommandParameter[] = []; | ||
public userMessage = "What is your GitHub username?\n(will be used to update the Github URLs in the plugin's package.json)"; | ||
public nameMessage = ""; | ||
constructor(private $options: IOptions, | ||
private $errors: IErrors, | ||
private $terminalSpinnerService: ITerminalSpinnerService, | ||
private $logger: ILogger, | ||
private $pacoteService: IPacoteService, | ||
private $fs: IFileSystem, | ||
private $childProcess: IChildProcess, | ||
private $prompter: IPrompter) { } | ||
|
||
public async execute(args: string[]): Promise<void> { | ||
const pluginRepoName = args[0]; | ||
const pathToProject = this.$options.path; | ||
const selectedPath = path.resolve(pathToProject || "."); | ||
const projectDir = path.join(selectedPath, pluginRepoName); | ||
|
||
this.$logger.printMarkdown("Downloading the latest version of NativeScript Plugin Seed..."); | ||
await this.downloadPackage(projectDir); | ||
|
||
this.$logger.printMarkdown("Executing initial plugin configuration script..."); | ||
await this.setupSeed(projectDir, pluginRepoName); | ||
|
||
this.$logger.printMarkdown("Solution for `%s` was successfully created.", pluginRepoName); | ||
} | ||
|
||
public async canExecute(args: string[]): Promise<boolean> { | ||
if (!args[0]) { | ||
this.$errors.fail("You must specify the plugin repository name."); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private async setupSeed(projectDir: string, pluginRepoName: string): Promise<void> { | ||
const config = this.$options; | ||
const spinner = this.$terminalSpinnerService.createSpinner(); | ||
const cwd = path.join(projectDir, "src"); | ||
try { | ||
spinner.start(); | ||
await this.$childProcess.exec("npm i", { cwd: cwd }); | ||
} finally { | ||
spinner.stop(); | ||
} | ||
|
||
let gitHubUsername = config.username; | ||
if (!gitHubUsername) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe you can extract part of this method to a separate methods, which will make the code easier to read, for example: const gitHubUsername = await this.getGitHubUsernam(config.username);
const pluginNameSource = await this.getPluginNameSource(config.pluginName); |
||
gitHubUsername = "NativeScriptDeveloper"; | ||
if (isInteractive()) { | ||
gitHubUsername = await this.$prompter.getString(this.userMessage, { allowEmpty: false, defaultAction: () => { return gitHubUsername; } }); | ||
} | ||
} | ||
|
||
let pluginNameSource = config.pluginName; | ||
if (!pluginNameSource) { | ||
// remove nativescript- prefix for naming plugin files | ||
const prefix = 'nativescript-'; | ||
pluginNameSource = pluginRepoName.toLowerCase().startsWith(prefix) ? pluginRepoName.slice(prefix.length, pluginRepoName.length) : pluginRepoName; | ||
if (isInteractive()) { | ||
pluginNameSource = await this.$prompter.getString(this.nameMessage, { allowEmpty: false, defaultAction: () => { return pluginNameSource; } }); | ||
} | ||
} | ||
|
||
if (!isInteractive() && (!config.username || !config.pluginName)) { | ||
this.$logger.printMarkdown("Using default values for Github user and/or plugin name since your shell is not interactive."); | ||
} | ||
|
||
const params = `gitHubUsername=${gitHubUsername} pluginName=${pluginNameSource} initGit=y`; | ||
// run postclone script manually and kill it if it takes more than 10 sec | ||
const outputScript = (await this.$childProcess.exec(`node scripts/postclone ${params}`, { cwd: cwd, timeout: 10000 })); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could cause issues in case any of the parameters has a space in it. I recommend using const pathToPostCloneScript = path.join("scripts", "postclone");
const params = [ pathToPostCloneScript, `gitHubUsername=${gitHubUsername}`, `pluginName=${pluginNameSource}`, "initGit=y"];
await this.$childProcess.spawnFromEvent(process.execPath, params, "close", { cwd, timeout: 10000 }); |
||
this.$logger.printMarkdown(outputScript); | ||
} | ||
|
||
private async downloadPackage(projectDir: string): Promise<void> { | ||
this.$fs.createDirectory(projectDir); | ||
|
||
if (this.$fs.exists(projectDir) && !this.$fs.isEmptyDir(projectDir)) { | ||
this.$errors.fail("Path already exists and is not empty %s", projectDir); | ||
} | ||
|
||
const spinner = this.$terminalSpinnerService.createSpinner(); | ||
const packageToInstall = "https://github.com/NativeScript/nativescript-plugin-seed/archive/master.tar.gz"; | ||
try { | ||
spinner.start(); | ||
await this.$pacoteService.extractPackage(packageToInstall, projectDir); | ||
} catch (err) { | ||
this.$fs.deleteDirectory(projectDir); | ||
throw err; | ||
} finally { | ||
spinner.stop(); | ||
} | ||
} | ||
} | ||
|
||
$injector.registerCommand(["plugin|create"], CreatePluginCommand); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,9 @@ export class Options extends commonOptionsLibPath.OptionsBase { | |
inspector: { type: OptionType.Boolean }, | ||
clean: { type: OptionType.Boolean }, | ||
watch: { type: OptionType.Boolean, default: true }, | ||
background: { type: OptionType.String } | ||
background: { type: OptionType.String }, | ||
username: {type: OptionType.String}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you please format this changes ( |
||
pluginName: {type: OptionType.String} | ||
}, | ||
$errors, $staticConfig, $settingsService); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { Yok } from "../lib/common/yok"; | ||
import * as stubs from "./stubs"; | ||
import { CreatePluginCommand } from "../lib/commands/plugin/create-plugin"; | ||
import { assert } from "chai"; | ||
import helpers = require("../lib/common/helpers"); | ||
|
||
const originalIsInteractive = helpers.isInteractive; | ||
const dummyArgs = ["dummyProjectName"]; | ||
const dummyUser = "devUsername"; | ||
const dummyName = "devPlugin"; | ||
|
||
function createTestInjector() { | ||
const testInjector = new Yok(); | ||
|
||
testInjector.register("injector", testInjector); | ||
testInjector.register("errors", stubs.ErrorsStub); | ||
testInjector.register("logger", stubs.LoggerStub); | ||
testInjector.register("childProcess", stubs.ChildProcessStub); | ||
testInjector.register("prompter", new stubs.PrompterStub()); | ||
testInjector.register("fs", stubs.FileSystemStub); | ||
testInjector.register("options", { | ||
username: undefined, | ||
pluginName: undefined | ||
}); | ||
|
||
testInjector.register("terminalSpinnerService", { | ||
createSpinner: () => ({ | ||
start: (): void => undefined, | ||
stop: (): void => undefined, | ||
message: (): void => undefined | ||
}) | ||
}); | ||
|
||
testInjector.register("pacoteService", { | ||
manifest: () => Promise.resolve(), | ||
extractPackage: () => Promise.resolve() | ||
}); | ||
|
||
testInjector.register("createCommand", CreatePluginCommand); | ||
|
||
return testInjector; | ||
} | ||
|
||
describe("Plugin create command tests", () => { | ||
let testInjector: IInjector; | ||
let options: IOptions; | ||
let createPluginCommand: CreatePluginCommand; | ||
|
||
beforeEach(() => { | ||
helpers.isInteractive = () => true; | ||
testInjector = createTestInjector(); | ||
options = testInjector.resolve("$options"); | ||
createPluginCommand = testInjector.resolve("$createCommand"); | ||
}); | ||
|
||
afterEach(() => { | ||
helpers.isInteractive = originalIsInteractive; | ||
}); | ||
|
||
describe("#CreatePluginCommand", () => { | ||
it("should fail when project name is not set.", async () => { | ||
assert.isRejected(createPluginCommand.canExecute([])); | ||
}); | ||
|
||
it("should pass when only project name is set in non-interactive shell.", async () => { | ||
helpers.isInteractive = () => false; | ||
await createPluginCommand.execute(dummyArgs); | ||
}); | ||
|
||
it("should pass when only project name is set with prompts in interactive shell.", async () => { | ||
const prompter = testInjector.resolve("$prompter"); | ||
const strings: IDictionary<string> = {}; | ||
strings[createPluginCommand.userMessage] = dummyUser; | ||
strings[createPluginCommand.nameMessage] = dummyName; | ||
|
||
prompter.expect({ | ||
strings: strings | ||
}); | ||
await createPluginCommand.execute(dummyArgs); | ||
prompter.assert(); | ||
}); | ||
|
||
it("should pass with project name and username set with one prompt in interactive shell.", async () => { | ||
options.username = dummyUser; | ||
const prompter = testInjector.resolve("$prompter"); | ||
const strings: IDictionary<string> = {}; | ||
strings[createPluginCommand.nameMessage] = dummyName; | ||
|
||
prompter.expect({ | ||
strings: strings | ||
}); | ||
await createPluginCommand.execute(dummyArgs); | ||
prompter.assert(); | ||
}); | ||
|
||
it("should pass with project name and pluginName set with one prompt in interactive shell.", async () => { | ||
options.pluginName = dummyName; | ||
const prompter = testInjector.resolve("$prompter"); | ||
const strings: IDictionary<string> = {}; | ||
strings[createPluginCommand.userMessage] = dummyUser; | ||
|
||
prompter.expect({ | ||
strings: strings | ||
}); | ||
await createPluginCommand.execute(dummyArgs); | ||
prompter.assert(); | ||
}); | ||
|
||
it("should pass with project name, username and pluginName set with no prompt in interactive shell.", async () => { | ||
options.username = dummyUser; | ||
options.pluginName = dummyName; | ||
await createPluginCommand.execute(dummyArgs); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using childProcess.exec, you can use the
$npm
:https://github.com/NativeScript/nativescript-cli/blob/master/lib/node-package-manager.ts#L18
For example: