diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9d5bdb6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +* +**/* \ No newline at end of file diff --git a/README.md b/README.md index 98a8754..77f2836 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,47 @@ `xdpm` is a command line tool that makes it easy to develop Adobe XD plugins. It is capable of the following tasks: -* `install`: copies one or more plugins in a develoment folder into Adobe XD's develop folder -* `watch`: Watches a plugin folder and copies it into Adobe XD whenever the contents change -* `validate`: Validates a plugin's manifest to ensure that it will be accepted by XD. -* `package`: Packages a plugin folder into an `xdx` file suitable for distribution -* `ls`: Lists plugins installed in Adobe XD in development mode. +- `bootstrap`: Creates a new plugin scaffold: headless, panel, react, modal. Optionally specify the name for your new plugin's directory. +- `install`: copies one or more plugins in a develoment folder into Adobe XD's develop folder +- `watch`: Watches a plugin folder and copies it into Adobe XD whenever the contents change +- `validate`: Validates a plugin's manifest to ensure that it will be accepted by XD. +- `package`: Packages a plugin folder into an `xdx` file suitable for distribution +- `ls`: Lists plugins installed in Adobe XD in development mode. ## Install -``` +```shell npm install -g @adobe/xdpm ``` If you've cloned the repository: -``` +```shell npm install npm link ``` -## Installing a plugin +## Bootstrapping a plugin + +any of the following: +```shell +xdpm bootstrap # Bootstrap a headless plugin +xdpm bootstrap my-panel # Bootstrap a headless plugin in dir ./my-panel +xdpm bootstrap panel # Bootstrap a panel plugin +xdpm bootstrap panel my-panel # Bootstrap a panel plugin in dir ./my-panel ``` + +Plugin type options: + +- headless (default) +- panel +- modal +- react + +## Installing a plugin + +```shell xdpm install # Install the current folder into Adobe XD xdpm install path/to/plugin # Install the specified folder into Adobe XD xdpm install -w release # Install to Adobe XD CC Release (`r` is also valid; default) @@ -38,7 +57,7 @@ If the plugin folder is not a valid XD plugin, you'll receive an error upon atte ## Watching a plugin -``` +```shell xdpm watch # Watch current folder and install changes into Adobe XD xdpm watch path/to/plugin # Watch the specified folder and install changes into Adobe XD xdpm watch -w release # Install to Adobe XD CC Release (`r` is also valid; default) @@ -50,7 +69,7 @@ When developing a plugin, you can work directly in Adobe XD's `develop` folder, ## Validating plugin manifests -``` +```shell xdpm validate [...folders] # Validate the manifests in the list of folders ``` @@ -58,7 +77,7 @@ You can validate that a manifest is correct using this command. Any errors found ## Packaging plugins -``` +```shell xdpm package [...folders] # Create Adobe XD package ``` @@ -68,7 +87,7 @@ When you're finished with a plugin, you can simply zip the folder and rename the ## Listing installed plugins -``` +```shell xdpm ls # List installed plugins in Adobe XD's `develop` folder xdpm ls -w release # List installed plugins in Adobe XD's `develop` folder (default) xdpm ls -w prerelease # List installed plugins in Adobe XD Prerelease `develop` folder @@ -78,7 +97,7 @@ You can install plugins that are currently installed in Adobe XD using `ls`. ## Help -``` +```shell Usage: xdpm [OPTIONS] [ARGS] @@ -111,4 +130,4 @@ Apache 2.0 You use this utility at your own risk. Under no circumstances shall Adobe be held liable for the use, misuse, or abuse of this utility. -Use of this utility means that you agree to Adobe's [Terms of Use](https://www.adobe.com/legal/terms.html) and the [Adobe Developer Additional Terms](https://wwwimages2.adobe.com/content/dam/acom/en/legal/servicetou/Adobe-Developer-Additional-Terms_en_US_20180605_2200.pdf). \ No newline at end of file +Use of this utility means that you agree to Adobe's [Terms of Use](https://www.adobe.com/legal/terms.html) and the [Adobe Developer Additional Terms](https://wwwimages2.adobe.com/content/dam/acom/en/legal/servicetou/Adobe-Developer-Additional-Terms_en_US_20180605_2200.pdf). diff --git a/commands/bootstrap.js b/commands/bootstrap.js new file mode 100644 index 0000000..38374c9 --- /dev/null +++ b/commands/bootstrap.js @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Adobe Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const shell = require("shelljs"); +const sanitize = require("sanitize-filename"); + +const repo = "https://github.com/AdobeXD/plugin-samples.git"; +const consoleLink = "https://console.adobe.io/plugins"; +const defaultDirname = "my-plugin"; +const sampleDirs = { + default: "quick-start", + headless: "quick-start", + panel: "quick-start-panel", + react: "quick-start-react", + modal: "ui-html" +}; + +function bootstrap(opts, args) { + shell.echo("Bootstrapping plugin:"); + + const sampleRepoDirname = + sampleDirs[args[0] || "default"] || sampleDirs.default; + const localDirname = getLocalDirname(args); + + if (!shell.which("git")) { + shell.echo( + "Sorry, 'git' must be set in your PATH for xdpm bootstrap to work, but wasn't found. Please check your PATH." + ); + shell.exit(1); + } + + // Clone from repo + shell.exec( + `git clone "${repo}" "${localDirname}"`, + (code, stdout, stderr) => { + if (code === 0) { + cleanupClone(sampleRepoDirname, localDirname); + } else { + shell.echo( + `xdpm was unable to retrieve the boilerplate code for your plugin` + ); + shell.echo(`See the following for more information:`); + shell.echo(`> ${stdout}`); + shell.echo(`> ${stderr}`); + + shell.exit(1); + } + } + ); +} + +function getLocalDirname(args) { + if (args[1]) return sanitize(args[1]); + if (args[0] && !sampleDirs[args[0]]) return sanitize(args[0]); + return defaultDirname; +} + +// Remove unneeded samples and git history +function cleanupClone(sampleRepoDirname, localDirname) { + const cdResult = shell.cd(`./${localDirname}`); + + if (cdResult.code === 0) { + shell.exec( + `git filter-branch --subdirectory-filter "${sampleRepoDirname}"`, + { silent: true } + ); + shell.rm("-rf", `.git`); + + shell.echo(`Plugin was sucessfully bootstrapped in ${localDirname}`); + shell.echo( + `Be sure to get your unique plugin ID at ${consoleLink} and include it in manifest.json` + ); + } else { + shell.echo(`Unable to find bootstrapped plugin directory ${localDirname}`); + + shell.exit(1); + } +} + +module.exports = { + bootstrap, + sampleDirs +}; diff --git a/commands/validate.js b/commands/validate.js index 1144166..ee136f2 100644 --- a/commands/validate.js +++ b/commands/validate.js @@ -15,10 +15,13 @@ */ const cli = require("cli"); +const shell = require("shelljs"); const path = require("path"); const getPluginMetadata = require("../lib/getPluginMetadata"); const validate = require("../lib/validate"); +const validateErrCode = 1; + /** * validates one or more plugins */ @@ -27,6 +30,19 @@ function validatePlugin(opts, args) { args.push("."); // assume we want to package the plugin in the cwd } + const results = getResults(args); + const errors = reportResults(opts, results); + + if (errors) { + cli.error(`Manifest validation failed. Exiting with code ${validateErrCode}`); + shell.exit(validateErrCode); + } + else { + shell.exit(0); + } +} + +function getResults(args) { const results = args.map(pluginToValidate => { const sourcePath = path.resolve(pluginToValidate); const result = { @@ -51,20 +67,32 @@ function validatePlugin(opts, args) { return result; }); + return results; +} + +/** + * Prints validation results for each tested plugin to command line. + * @param {*} opts + * @param {*} results + * @returns {boolean} True if there was an error for any of the tested plugin manifests. + */ +function reportResults(opts, results) { if (opts.json) { - cli.output(JSON.stringify(results)); - return results; - } + shell.echo(JSON.stringify(results)); + } + + let errors; results.forEach(result => { if (result.ok) { cli.ok(result.ok); } else { cli.error(result.error); + errors = true; } }); - return results; + return errors; } module.exports = validatePlugin; \ No newline at end of file diff --git a/index.js b/index.js index fa20f63..2776ce1 100755 --- a/index.js +++ b/index.js @@ -17,58 +17,77 @@ const cli = require("cli"); const package = require("./package.json"); +const sampleDirs = require("./commands/bootstrap").sampleDirs; +const sampleTypes = Object.keys(sampleDirs) + .filter(el => el !== "default") + .join(", "); cli.enable("status", "version"); cli.setApp(package.name, package.version); const commands = { - "install": "Install a plugin in development mode", - "ls": "List all plugins in development mode", - "package": "Package a plugin", - "validate": "Validate a plugin's manifest", - "watch": "Watch a plugin directory. If no directory is specified, `.` is assumed", + bootstrap: `Create a new plugin scaffold: ${sampleTypes}. Optionally specify the name for your new plugin's directory.`, + install: "Install a plugin in development mode", + ls: "List all plugins in development mode", + package: "Package a plugin", + validate: "Validate a plugin's manifest", + watch: + "Watch a plugin directory. If no directory is specified, `.` is assumed" }; const options = { - clean: ["c", "Clean before install", "bool", false], - json: ["j", "If true, indicates that JSON output should be generated", "bool", false], -// mode: ["m", "Indicates which plugin mode to use", ["d", "p"], "d"], - overwrite: ["o", "Allow overwriting plugins", "bool", false], - which: ["w", "Which Adobe XD instance to target", ["r", "p", "d", "release", "pre", "prerelease", "dev", "development"], "r"], + clean: ["c", "Clean before install", "bool", false], + json: [ + "j", + "If true, indicates that JSON output should be generated", + "bool", + false + ], + // mode: ["m", "Indicates which plugin mode to use", ["d", "p"], "d"], + overwrite: ["o", "Allow overwriting plugins", "bool", false], + which: [ + "w", + "Which Adobe XD instance to target", + ["r", "p", "d", "release", "pre", "prerelease", "dev", "development"], + "r" + ] }; const parsedOpts = cli.parse(options, commands); if (parsedOpts.json) { - cli.status = function() {}; + cli.status = function() {}; } else { - cli.info(`xdpm ${package.version} - XD Plugin Manager CLI`); - cli.info(`Use of this tool means you agree to the Adobe Terms of Use at + cli.info(`xdpm ${package.version} - XD Plugin Manager CLI`); + cli.info(`Use of this tool means you agree to the Adobe Terms of Use at https://www.adobe.com/legal/terms.html and the Developer Additional -Terms at https://wwwimages2.adobe.com/content/dam/acom/en/legal/servicetou/Adobe-Developer-Additional-Terms_en_US_20180605_2200.pdf.`) +Terms at https://wwwimages2.adobe.com/content/dam/acom/en/legal/servicetou/Adobe-Developer-Additional-Terms_en_US_20180605_2200.pdf.`); } const { command, args } = cli; if (parsedOpts.which) { - parsedOpts.which = parsedOpts.which[0]; + parsedOpts.which = parsedOpts.which[0]; } switch (command.toLowerCase()) { - case "ls": - require("./commands/ls")(parsedOpts, args); - break; - case "install": - require("./commands/install")(parsedOpts, args); - break; - case "watch": - require("./commands/watch")(parsedOpts, args); - break; - case "package": - require("./commands/package")(parsedOpts, args); - break; - case "validate": - require("./commands/validate")(parsedOpts, args); - break; + case "ls": + require("./commands/ls")(parsedOpts, args); + break; + case "install": + require("./commands/install")(parsedOpts, args); + break; + case "watch": + require("./commands/watch")(parsedOpts, args); + break; + case "package": + require("./commands/package")(parsedOpts, args); + break; + case "validate": + require("./commands/validate")(parsedOpts, args); + break; + case "bootstrap": + require("./commands/bootstrap").bootstrap(parsedOpts, args); + break; } diff --git a/lib/manifestSchema.js b/lib/manifestSchema.js index 0c3e62b..ef9b1e1 100644 --- a/lib/manifestSchema.js +++ b/lib/manifestSchema.js @@ -55,34 +55,69 @@ const manifestScema = { } }, uiEntryPoints: { - type: "array", - minItems: 1, - uniqueItems: true, - items: { - required: ["type", "label"], - properties: { - type: { - type: "string", - enum: ["menu", "panel"] - }, - label: { - type: ["string", "object"] - }, - commandId: { - type: "string" - }, - panelId: { - type: "string" - }, - shortcut: { - type: "object", + oneOf: [ + { + type: "array", + minItems: 1, + maxItems: 1, + uniqueItems: true, + items: { + required: ["type"], properties: { - mac: { type: "string" }, - win: { type: "string" } + type: { + type: "string", + enum: ["menu", "panel"] + }, + label: { + type: ["string", "object"] + }, + commandId: { + type: "string" + }, + panelId: { + type: "string" + }, + shortcut: { + type: "object", + properties: { + mac: { type: "string" }, + win: { type: "string" } + } + } + } + } + }, + { + type: "array", + minItems: 2, + uniqueItems: true, + items: { + required: ["type", "label"], + properties: { + type: { + type: "string", + enum: ["menu", "panel"] + }, + label: { + type: ["string", "object"] + }, + commandId: { + type: "string" + }, + panelId: { + type: "string" + }, + shortcut: { + type: "object", + properties: { + mac: { type: "string" }, + win: { type: "string" } + } + } } } } - } + ] } } }; diff --git a/package-lock.json b/package-lock.json index 1e77fb6..88f6cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@adobe/xdpm", - "version": "3.0.2", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -281,6 +281,14 @@ "path-parse": "^1.0.6" } }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "shelljs": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", @@ -299,6 +307,14 @@ "is-number": "^7.0.0" } }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -307,6 +323,11 @@ "punycode": "^2.1.0" } }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index b5fe315..db5d8de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/xdpm", - "version": "3.1.0", + "version": "4.0.0", "description": "Adobe XD CLI Plugin Manager Utility", "main": "index.js", "scripts": { @@ -32,6 +32,7 @@ "debounce": "^1.2.0", "ignore-walk": "^3.0.2", "mkdirp": "^0.5.1", + "sanitize-filename": "^1.6.3", "shelljs": "^0.8.3", "yazl": "^2.5.1" },