From f29b218060addd07513d3690555075c7b7b11a05 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Fri, 14 Sep 2018 10:05:57 +0100 Subject: [PATCH] feat(docs-infra): generate Angular CLI command reference (#25363) PR Close #25363 --- aio/content/cli-src/.gitignore | 1 + aio/content/cli-src/package.json | 5 + aio/content/cli/index.md | 83 ++++++ aio/content/navigation.json | 11 + aio/package.json | 5 +- aio/src/app/navigation/navigation.service.ts | 7 +- aio/src/styles/0-base/_typography.scss | 11 - aio/src/styles/2-modules/_cli-pages.scss | 7 + aio/src/styles/2-modules/_modules-dir.scss | 1 + .../transforms/angular.io-package/index.js | 3 +- .../cli-docs-package/extract-cli-commands.js | 7 + .../transforms/cli-docs-package/index.js | 48 ++++ .../processors/filterHiddenCommands.js | 9 + .../processors/filterHiddenCommands.spec.js | 40 +++ .../processors/processCliCommands.js | 68 +++++ .../processors/processCliCommands.spec.js | 264 ++++++++++++++++++ .../processors/processCliContainerDoc.js | 11 + .../processors/processCliContainerDoc.spec.js | 23 ++ .../cli-docs-package/readers/cli-command.js | 47 ++++ .../readers/cli-command.spec.js | 124 ++++++++ .../cli-docs-package/rendering/cliNegate.js | 6 + .../rendering/cliNegate.spec.js | 17 ++ .../templates/cli/cli-command.template.html | 18 ++ .../templates/cli/cli-container.template.html | 23 ++ .../templates/cli/include/cli-breadcrumb.html | 16 ++ .../templates/cli/include/cli-header.html | 4 + .../transforms/templates/cli/lib/cli.html | 111 ++++++++ aio/yarn.lock | 10 + 28 files changed, 965 insertions(+), 15 deletions(-) create mode 100644 aio/content/cli-src/.gitignore create mode 100644 aio/content/cli-src/package.json create mode 100644 aio/content/cli/index.md create mode 100644 aio/src/styles/2-modules/_cli-pages.scss create mode 100644 aio/tools/transforms/cli-docs-package/extract-cli-commands.js create mode 100644 aio/tools/transforms/cli-docs-package/index.js create mode 100644 aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.js create mode 100644 aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.spec.js create mode 100644 aio/tools/transforms/cli-docs-package/processors/processCliCommands.js create mode 100644 aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js create mode 100644 aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.js create mode 100644 aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.spec.js create mode 100644 aio/tools/transforms/cli-docs-package/readers/cli-command.js create mode 100644 aio/tools/transforms/cli-docs-package/readers/cli-command.spec.js create mode 100644 aio/tools/transforms/cli-docs-package/rendering/cliNegate.js create mode 100644 aio/tools/transforms/cli-docs-package/rendering/cliNegate.spec.js create mode 100644 aio/tools/transforms/templates/cli/cli-command.template.html create mode 100644 aio/tools/transforms/templates/cli/cli-container.template.html create mode 100644 aio/tools/transforms/templates/cli/include/cli-breadcrumb.html create mode 100644 aio/tools/transforms/templates/cli/include/cli-header.html create mode 100644 aio/tools/transforms/templates/cli/lib/cli.html diff --git a/aio/content/cli-src/.gitignore b/aio/content/cli-src/.gitignore new file mode 100644 index 0000000000..7255efa0bc --- /dev/null +++ b/aio/content/cli-src/.gitignore @@ -0,0 +1 @@ +yarn.lock \ No newline at end of file diff --git a/aio/content/cli-src/package.json b/aio/content/cli-src/package.json new file mode 100644 index 0000000000..d5bcd7f65c --- /dev/null +++ b/aio/content/cli-src/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@angular/cli": "https://github.com/angular/cli-builds#master" + } +} diff --git a/aio/content/cli/index.md b/aio/content/cli/index.md new file mode 100644 index 0000000000..a775146254 --- /dev/null +++ b/aio/content/cli/index.md @@ -0,0 +1,83 @@ +

CLI Command Reference

+ +The Angular CLI is a command-line tool that you use to initialize, develop, scaffold, and maintain Angular applications. + +## Getting Started + +### Installing Angular CLI + +The current version of Angular CLI is 6.x. + +* Both the CLI and the projects that you generate with the tool have dependencies that require Node 8.9 or higher, together with NPM 5.5.1 or higher. +* Install the CLI using npm: + `npm install -g @angular/cli` +* The CLI is an open-source tool: + https://github.com/angular/angular-cli/tree/master/packages/angular/cli + +For details about changes between versions, and information about updating from previous releases, see the Releases tab on GitHub. + +### Basic workflow + +Invoke the tool on the command line through the ng executable. Online help is available on the command line: + +``` +> ng help Lists commands with short descriptions +> ng --help Lists options for a command. +``` + +To create, build, and serve a new, basic Angular project on a development server, use the following commands: + +``` +cd +ng new my-project +cd my-project +ng serve +``` + +In your browser, open http://localhost:4200/ to see the new app run. + +### Workspaces and project files + +Angular 6 introduced the workspace directory structure for Angular apps. A workspace defines a project. A project can contain multiple apps, as well as libraries that can be used in any of the apps. + +Some commands (such as build) must be executed from within a workspace folder, and others (such as new) must be executed from outside any workspace. This requirement is called out in the description of each command where it applies.The `new` command creates a [workspace](guide/glossary#workspace) to contain [projects](guide/glossary#project). A project can be an app or a library, and a workspace can contain multiple apps and libraries. + +A newly generated app project contains the source files for a root module, with a root component and template, which you can edit directly, or add to and modify using CLI commands. Use the generate command to add new files for additional components and services, and code for new pipes, directives, and so on. + +* Commands such as `add` and `generate`, that create or operate on apps and libraries, must be executed from within a workspace folder. +* Apps in a workspace can use libraries in the same workspace. +* Each project has a `src` folder that contains the logic, data, and assets. + See an example of the [file structure](guide/quickstart#project-file-review) in [Getting Started](guide/quickstart). + +When you use the `serve` command to build an app, the server automatically rebuilds the app and reloads the page when you change any of the source files. + +### Configuring the CLI + +Configuration files let you customize your project. The CLI configuration file, angular.json, is created at the top level of the project folder. This is where you can set CLI defaults for your project, and specify which files to include when the CLI builds the project. + +The CLI config command lets you set and retrieve configuration values from the command line, or you can edit the angular.json file directly. + +* See the complete schema for angular.json. +* Learn more about configuration options for Angular (link to new guide?) + +### Command options and arguments + +All commands and some options have aliases, as listed in the descriptions. Option names are prefixed with a double dash (--), but arguments and option aliases are not. + +Typically, the name of a generated artifact can be given as an argument to the command or specified with the --name option. Most commands have additional options. + +Command syntax is shown as follows: + +``` +ng commandNameOrAlias [options] +``` + +Options take either string or Boolean arguments. Defaults are shown in bold for Boolean or enumerated values, and are given with the description. For example: + +``` + --optionNameOrAlias= + --optionNameOrAlias=true|false + --optionNameOrAlias=allowedValue1|allowedValue2|allowedValue3 +``` + +Boolean options can also be expressed with a prefix `no-` to indicate a value of false. For example, `--no-prod` is equivalent to `--prod=false`. \ No newline at end of file diff --git a/aio/content/navigation.json b/aio/content/navigation.json index bad9227eea..6852d02de3 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -592,6 +592,17 @@ "tooltip": "Details of the Angular classes and values.", "url": "api" }, + { + "title": "CLI Commands", + "tooltip": "Angular CLI command reference.", + "children": [ + { + "title": "Using the CLI", + "tooltip": "An overview of how to use the CLI tool", + "url": "cli" + } + ] + }, { "url": "guide/change-log", "title": "Change Log", diff --git a/aio/package.json b/aio/package.json index a02221b510..bb2c139299 100644 --- a/aio/package.json +++ b/aio/package.json @@ -18,13 +18,14 @@ "build-for": "yarn ~~build --configuration", "prebuild-local": "yarn setup-local", "build-local": "yarn ~~build --configuration=stable", + "extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint", "test": "yarn check-env && ng test", "pree2e": "yarn check-env && yarn update-webdriver", "e2e": "ng e2e --no-webdriver-update", "presetup": "yarn --cwd .. install && yarn install --frozen-lockfile && yarn ~~check-env && yarn ~~clean-generated && yarn boilerplate:remove", "setup": "yarn aio-use-npm && yarn example-use-npm", - "postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn docs", + "postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn extract-cli-command-docs && yarn docs", "presetup-local": "yarn presetup", "setup-local": "yarn aio-use-local && yarn example-use-local", "postsetup-local": "yarn postsetup", @@ -127,6 +128,8 @@ "jasmine-spec-reporter": "^4.1.0", "jasmine-ts": "^0.2.1", "jsdom": "^9.12.0", + "json-schema-traverse": "^0.4.1", + "json5": "^1.0.1", "karma": "^1.7.0", "karma-chrome-launcher": "^2.1.1", "karma-cli": "^1.0.1", diff --git a/aio/src/app/navigation/navigation.service.ts b/aio/src/app/navigation/navigation.service.ts index dbee4e367c..3b42083b56 100644 --- a/aio/src/app/navigation/navigation.service.ts +++ b/aio/src/app/navigation/navigation.service.ts @@ -95,8 +95,11 @@ export class NavigationService { this.location.currentPath, (navMap, url) => { - const urlKey = url.startsWith('api/') ? 'api' : url; - return navMap.get(urlKey) || { '' : { view: '', url: urlKey, nodes: [] }}; + const matchSpecialUrls = /^api|^cli/.exec(url); + if (matchSpecialUrls) { + url = matchSpecialUrls[0]; + } + return navMap.get(url) || { '' : { view: '', url: url, nodes: [] }}; }) .pipe(publishReplay(1)); (currentNodes as ConnectableObservable).connect(); diff --git a/aio/src/styles/0-base/_typography.scss b/aio/src/styles/0-base/_typography.scss index b83a0604b0..57e0a196b5 100755 --- a/aio/src/styles/0-base/_typography.scss +++ b/aio/src/styles/0-base/_typography.scss @@ -135,17 +135,6 @@ th { text-align: left; } -p > code, li > code, td > code, th > code { - font-family: $code-font; - font-size: 85%; - color: $darkgray; - letter-spacing: 0; - line-height: 1; - padding: 2px 0; - background-color: $backgroundgray; - border-radius: 4px; -} - code { font-family: $code-font; font-size: 90%; diff --git a/aio/src/styles/2-modules/_cli-pages.scss b/aio/src/styles/2-modules/_cli-pages.scss new file mode 100644 index 0000000000..a8462aa91b --- /dev/null +++ b/aio/src/styles/2-modules/_cli-pages.scss @@ -0,0 +1,7 @@ +.cli-name, .cli-default { + font-weight: bold; +} + +.cli-option-syntax { + white-space: pre; +} diff --git a/aio/src/styles/2-modules/_modules-dir.scss b/aio/src/styles/2-modules/_modules-dir.scss index 3584f91dc7..2d8089f5e0 100644 --- a/aio/src/styles/2-modules/_modules-dir.scss +++ b/aio/src/styles/2-modules/_modules-dir.scss @@ -8,6 +8,7 @@ @import 'buttons'; @import 'callout'; @import 'card'; + @import 'cli-pages'; @import 'code'; @import 'contribute'; @import 'contributor'; diff --git a/aio/tools/transforms/angular.io-package/index.js b/aio/tools/transforms/angular.io-package/index.js index bd3d196739..f2b818f855 100644 --- a/aio/tools/transforms/angular.io-package/index.js +++ b/aio/tools/transforms/angular.io-package/index.js @@ -9,11 +9,12 @@ const Package = require('dgeni').Package; const gitPackage = require('dgeni-packages/git'); const apiPackage = require('../angular-api-package'); const contentPackage = require('../angular-content-package'); +const cliDocsPackage = require('../cli-docs-package'); const { extname, resolve } = require('canonical-path'); const { existsSync } = require('fs'); const { SRC_PATH } = require('../config'); -module.exports = new Package('angular.io', [gitPackage, apiPackage, contentPackage]) +module.exports = new Package('angular.io', [gitPackage, apiPackage, contentPackage, cliDocsPackage]) // This processor relies upon the versionInfo. See below... .processor(require('./processors/processNavigationMap')) diff --git a/aio/tools/transforms/cli-docs-package/extract-cli-commands.js b/aio/tools/transforms/cli-docs-package/extract-cli-commands.js new file mode 100644 index 0000000000..fc755c8c9e --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/extract-cli-commands.js @@ -0,0 +1,7 @@ +const shelljs = require('shelljs'); +const {resolve} = require('canonical-path'); +const {CONTENTS_PATH} = require('../config'); + +shelljs.cd(resolve(CONTENTS_PATH, 'cli-src')); +shelljs.exec('git clean -Xfd'); +shelljs.exec('yarn install'); diff --git a/aio/tools/transforms/cli-docs-package/index.js b/aio/tools/transforms/cli-docs-package/index.js new file mode 100644 index 0000000000..8579efb354 --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/index.js @@ -0,0 +1,48 @@ +const {resolve} = require('canonical-path'); +const Package = require('dgeni').Package; +const basePackage = require('../angular-base-package'); +const contentPackage = require('../content-package'); +const {CONTENTS_PATH, TEMPLATES_PATH, requireFolder} = require('../config'); + + +// Define the dgeni package for generating the docs +module.exports = new Package('cli-docs', [basePackage, contentPackage]) + +// Register the services and file readers +.factory(require('./readers/cli-command')) + +// Register the processors +.processor(require('./processors/processCliContainerDoc')) +.processor(require('./processors/processCliCommands')) +.processor(require('./processors/filterHiddenCommands')) + +// Configure file reading +.config(function(readFilesProcessor, cliCommandFileReader) { + const CLI_SOURCE_PATH = resolve(CONTENTS_PATH, 'cli-src/node_modules/@angular/cli/help'); + readFilesProcessor.fileReaders.push(cliCommandFileReader); + readFilesProcessor.sourceFiles = readFilesProcessor.sourceFiles.concat([ + { + basePath: CLI_SOURCE_PATH, + include: resolve(CLI_SOURCE_PATH, '*.json'), + fileReader: 'cliCommandFileReader' + }, + { + basePath: CONTENTS_PATH, + include: resolve(CONTENTS_PATH, 'cli/**'), + fileReader: 'contentFileReader' + }, + ]); +}) + +.config(function(templateFinder, templateEngine, getInjectables) { + // Where to find the templates for the CLI doc rendering + templateFinder.templateFolders.unshift(resolve(TEMPLATES_PATH, 'cli')); + // Add in templating filters and tags + templateEngine.filters = templateEngine.filters.concat(getInjectables(requireFolder(__dirname, './rendering'))); +}) + + +.config(function(convertToJsonProcessor, postProcessHtml) { + convertToJsonProcessor.docTypes = convertToJsonProcessor.docTypes.concat(['cli-command', 'cli-overview']); + postProcessHtml.docTypes = postProcessHtml.docTypes.concat(['cli-command', 'cli-overview']); +}); diff --git a/aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.js b/aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.js new file mode 100644 index 0000000000..ebf3c6df58 --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.js @@ -0,0 +1,9 @@ +module.exports = function filterHiddenCommands() { + return { + $runAfter: ['files-read'], + $runBefore: ['processCliContainerDoc'], + $process(docs) { + return docs.filter(doc => doc.docType !== 'cli-command' || doc.hidden !== true); + } + }; +}; diff --git a/aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.spec.js b/aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.spec.js new file mode 100644 index 0000000000..b3b570a3de --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/processors/filterHiddenCommands.spec.js @@ -0,0 +1,40 @@ +const testPackage = require('../../helpers/test-package'); +const processorFactory = require('./filterHiddenCommands'); +const Dgeni = require('dgeni'); + +describe('filterHiddenCommands processor', () => { + + it('should be available on the injector', () => { + const dgeni = new Dgeni([testPackage('cli-docs-package')]); + const injector = dgeni.configureInjector(); + const processor = injector.get('filterHiddenCommands'); + expect(processor.$process).toBeDefined(); + }); + + it('should run after the correct processor', () => { + const processor = processorFactory(); + expect(processor.$runAfter).toEqual(['files-read']); + }); + + it('should run before the correct processor', () => { + const processor = processorFactory(); + expect(processor.$runBefore).toEqual(['processCliContainerDoc']); + }); + + it('should remove CLI command docs that are hidden', () => { + const processor = processorFactory(); + const filtered = processor.$process([ + { docType: 'cli-command', id: 'one' }, + { docType: 'cli-command', id: 'two', hidden: true }, + { docType: 'cli-command', id: 'three', hidden: false }, + { docType: 'other-doc', id: 'four', hidden: true }, + { docType: 'other-doc', id: 'five', hidden: false }, + ]); + expect(filtered).toEqual([ + { docType: 'cli-command', id: 'one' }, + { docType: 'cli-command', id: 'three', hidden: false }, + { docType: 'other-doc', id: 'four', hidden: true }, + { docType: 'other-doc', id: 'five', hidden: false }, + ]); + }); +}); diff --git a/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js new file mode 100644 index 0000000000..3df3095ade --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.js @@ -0,0 +1,68 @@ +module.exports = function processCliCommands() { + return { + $runAfter: ['extra-docs-added'], + $runBefore: ['rendering-docs'], + $process(docs) { + const navigationDoc = docs.find(doc => doc.docType === 'navigation-json'); + const navigationNode = navigationDoc && navigationDoc.data['SideNav'].find(node => node.title === 'CLI Commands'); + + docs.forEach(doc => { + if (doc.docType === 'cli-command') { + doc.names = collectNames(doc.name, doc.commandAliases); + + // Recursively process the options + processOptions(doc, doc.options); + + // Add to navigation doc + if (navigationNode) { + navigationNode.children.push({ url: doc.path, title: `ng ${doc.name}` }); + } + } + }); + } + }; +}; + +function processOptions(container, options) { + container.positionalOptions = []; + container.namedOptions = []; + + options.forEach(option => { + + if (option.type === 'boolean' && option.default === undefined) { + option.default = false; + } + + // Ignore any hidden options + if (option.hidden) { return; } + + option.types = option.types || [option.type]; + option.names = collectNames(option.name, option.aliases); + + // Now work out what kind of option it is: positional/named + if (option.positional !== undefined) { + container.positionalOptions[option.positional] = option; + } else { + container.namedOptions.push(option); + } + + // Recurse if there are subcommands + if (option.subcommands) { + option.subcommands = getValues(option.subcommands); + option.subcommands.forEach(subcommand => { + subcommand.names = collectNames(subcommand.name, subcommand.aliases); + processOptions(subcommand, subcommand.options); + }); + } + }); + + container.namedOptions.sort((a, b) => a.name > b.name ? 1 : -1); +} + +function collectNames(name, aliases) { + return [name].concat(aliases); +} + +function getValues(obj) { + return Object.keys(obj).map(key => obj[key]); +} diff --git a/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js new file mode 100644 index 0000000000..cb80985ae2 --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/processors/processCliCommands.spec.js @@ -0,0 +1,264 @@ +const testPackage = require('../../helpers/test-package'); +const processorFactory = require('./processCliCommands'); +const Dgeni = require('dgeni'); + +describe('processCliCommands processor', () => { + + it('should be available on the injector', () => { + const dgeni = new Dgeni([testPackage('cli-docs-package')]); + const injector = dgeni.configureInjector(); + const processor = injector.get('processCliCommands'); + expect(processor.$process).toBeDefined(); + }); + + it('should run after the correct processor', () => { + const processor = processorFactory(); + expect(processor.$runAfter).toEqual(['extra-docs-added']); + }); + + it('should run before the correct processor', () => { + const processor = processorFactory(); + expect(processor.$runBefore).toEqual(['rendering-docs']); + }); + + it('should collect the names (name + aliases)', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: ['alias1', 'alias2'], + options: [], + }; + processor.$process([doc]); + expect(doc.names).toEqual(['name', 'alias1', 'alias2']); + }); + + describe('options', () => { + it('should remove the hidden options', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [ + { name: 'option1' }, + { name: 'option2', hidden: true }, + { name: 'option3' }, + { name: 'option4', hidden: true }, + ], + }; + processor.$process([doc]); + expect(doc.namedOptions).toEqual([ + jasmine.objectContaining({ name: 'option1' }), + jasmine.objectContaining({ name: 'option3' }), + ]); + }); + + it('should collect the non-hidden positional and named options', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [ + { name: 'named1' }, + { name: 'positional1', positional: 0}, + { name: 'named2', hidden: true }, + { name: 'positional2', hidden: true, positional: 1}, + ], + }; + processor.$process([doc]); + expect(doc.positionalOptions).toEqual([ + jasmine.objectContaining({ name: 'positional1', positional: 0}), + ]); + expect(doc.namedOptions).toEqual([ + jasmine.objectContaining({ name: 'named1' }), + ]); + }); + + it('should sort the named options into order by name', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [ + { name: 'c' }, + { name: 'a' }, + { name: 'b' }, + ], + }; + processor.$process([doc]); + expect(doc.namedOptions).toEqual([ + jasmine.objectContaining({ name: 'a' }), + jasmine.objectContaining({ name: 'b' }), + jasmine.objectContaining({ name: 'c' }), + ]); + }); + }); + + describe('subcommands', () => { + it('should convert subcommands hash into a collection', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [{ + name: 'supercommand', + subcommands: { + subcommand1: { + name: 'subcommand1', + options: [ + { name: 'subcommand1-option1' }, + { name: 'subcommand1-option2' }, + ], + }, + subcommand2: { + name: 'subcommand2', + options: [ + { name: 'subcommand2-option1' }, + { name: 'subcommand2-option2' }, + ], + } + }, + }], + }; + processor.$process([doc]); + expect(doc.options[0].subcommands).toEqual([ + jasmine.objectContaining({ name: 'subcommand1' }), + jasmine.objectContaining({ name: 'subcommand2' }), + ]); + }); + + it('should remove the hidden subcommand options', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [{ + name: 'supercommand', + subcommands: { + subcommand1: { + name: 'subcommand1', + options: [ + { name: 'subcommand1-option1' }, + { name: 'subcommand1-option2', hidden: true }, + ], + }, + subcommand2: { + name: 'subcommand2', + options: [ + { name: 'subcommand2-option1', hidden: true }, + { name: 'subcommand2-option2' }, + ], + } + }, + }], + }; + processor.$process([doc]); + expect(doc.options[0].subcommands[0].namedOptions).toEqual([ + jasmine.objectContaining({ name: 'subcommand1-option1' }), + ]); + expect(doc.options[0].subcommands[1].namedOptions).toEqual([ + jasmine.objectContaining({ name: 'subcommand2-option2' }), + ]); + }); + + it('should collect the non-hidden positional arguments and named options', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [{ + name: 'supercommand', + subcommands: { + subcommand1: { + name: 'subcommand1', + options: [ + { name: 'subcommand1-option1' }, + { name: 'subcommand1-option2', positional: 0 }, + ], + }, + subcommand2: { + name: 'subcommand2', + options: [ + { name: 'subcommand2-option1', hidden: true }, + { name: 'subcommand2-option2', hidden: true, positional: 1 }, + ], + } + }, + }], + }; + processor.$process([doc]); + expect(doc.options[0].subcommands[0].positionalOptions).toEqual([ + jasmine.objectContaining({ name: 'subcommand1-option2', positional: 0}), + ]); + expect(doc.options[0].subcommands[0].namedOptions).toEqual([ + jasmine.objectContaining({ name: 'subcommand1-option1' }), + ]); + + expect(doc.options[0].subcommands[1].positionalOptions).toEqual([]); + expect(doc.options[0].subcommands[1].namedOptions).toEqual([]); + }); + + it('should sort the named subcommand options into order by name', () => { + const processor = processorFactory(); + const doc = { + docType: 'cli-command', + name: 'name', + commandAliases: [], + options: [{ + name: 'supercommand', + subcommands: { + subcommand1: { + name: 'subcommand1', + options: [ + { name: 'c' }, + { name: 'a' }, + { name: 'b' }, + ] + } + } + }], + }; + processor.$process([doc]); + expect(doc.options[0].subcommands[0].namedOptions).toEqual([ + jasmine.objectContaining({ name: 'a' }), + jasmine.objectContaining({ name: 'b' }), + jasmine.objectContaining({ name: 'c' }), + ]); + }); + }); + + it('should add the command to the CLI node in the navigation doc', () => { + const processor = processorFactory(); + const command = { + docType: 'cli-command', + name: 'command1', + commandAliases: ['alias1', 'alias2'], + options: [], + path: 'cli/command1', + }; + const navigation = { + docType: 'navigation-json', + data: { + SideNav: [ + { url: 'some/page', title: 'Some Page' }, + { url: 'cli', title: 'CLI Commands', children: [ + { url: 'cli', title: 'Using the CLI' }, + ]}, + { url: 'other/page', title: 'Other Page' }, + ] + } + }; + processor.$process([command, navigation]); + expect(navigation.data.SideNav[1].title).toEqual('CLI Commands'); + expect(navigation.data.SideNav[1].children).toEqual([ + { url: 'cli', title: 'Using the CLI' }, + { url: 'cli/command1', title: 'ng command1' }, + ]); + }); +}); diff --git a/aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.js b/aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.js new file mode 100644 index 0000000000..f33eccbb2e --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.js @@ -0,0 +1,11 @@ +module.exports = function processCliContainerDoc() { + return { + $runAfter: ['extra-docs-added'], + $runBefore: ['rendering-docs'], + $process(docs) { + const cliDoc = docs.find(doc => doc.id === 'cli/index'); + cliDoc.id = 'cli-container'; + cliDoc.commands = docs.filter(doc => doc.docType === 'cli-command'); + } + }; +}; diff --git a/aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.spec.js b/aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.spec.js new file mode 100644 index 0000000000..038c1892d1 --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/processors/processCliContainerDoc.spec.js @@ -0,0 +1,23 @@ +const testPackage = require('../../helpers/test-package'); +const processorFactory = require('./processCliContainerDoc'); +const Dgeni = require('dgeni'); + +describe('processCliContainerDoc processor', () => { + + it('should be available on the injector', () => { + const dgeni = new Dgeni([testPackage('cli-docs-package')]); + const injector = dgeni.configureInjector(); + const processor = injector.get('processCliContainerDoc'); + expect(processor.$process).toBeDefined(); + }); + + it('should run after the correct processor', () => { + const processor = processorFactory(); + expect(processor.$runAfter).toEqual(['extra-docs-added']); + }); + + it('should run before the correct processor', () => { + const processor = processorFactory(); + expect(processor.$runBefore).toEqual(['rendering-docs']); + }); +}); diff --git a/aio/tools/transforms/cli-docs-package/readers/cli-command.js b/aio/tools/transforms/cli-docs-package/readers/cli-command.js new file mode 100644 index 0000000000..615f1fab62 --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/readers/cli-command.js @@ -0,0 +1,47 @@ +/** + * This file reader will pull the contents from a cli command json file + * + * The doc will initially have the form: + * ``` + * { + * startingLine: 1, + * ... + * } + * ``` + */ +module.exports = function cliCommandFileReader(log) { + const json5 = require('json5'); + return { + name: 'cliCommandFileReader', + defaultPattern: /\.json$/, + getDocs(fileInfo) { + try { + const doc = json5.parse(fileInfo.content); + const name = fileInfo.baseName; + const path = `cli/${name}`; + // We return a single element array because content files only contain one document + const result = Object.assign(doc, { + content: doc.description, + docType: 'cli-command', + startingLine: 1, + id: `cli-${doc.name}`, + commandAliases: doc.aliases || [], + aliases: computeAliases(doc), + path, + outputPath: `${path}.json`, + breadCrumbs: [ + { text: 'CLI', path: 'cli' }, + { text: name, path }, + ] + }); + return [result]; + } catch (e) { + log.warn(`Failed to read cli command file: "${fileInfo.relativePath}" - ${e.message}`); + } + } + }; +}; + +function computeAliases(doc) { + return [doc.name].concat(doc.aliases || []).map(alias => `cli-${alias}`); +} \ No newline at end of file diff --git a/aio/tools/transforms/cli-docs-package/readers/cli-command.spec.js b/aio/tools/transforms/cli-docs-package/readers/cli-command.spec.js new file mode 100644 index 0000000000..4136eed79e --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/readers/cli-command.spec.js @@ -0,0 +1,124 @@ +const cliCommandReaderFactory = require('./cli-command'); +const reader = cliCommandReaderFactory(); + +const content = ` +{ + "name": "add", + "description": "Add support for a library to your project.", + "longDescription": "Add support for a library in your project, for example adding \`@angular/pwa\` which would configure\\nyour project for PWA support.\\n", + "hidden": false, + "type": "custom", + "options": [ + { + "name": "collection", + "description": "The package to be added.", + "type": "string", + "required": false, + "aliases": [], + "hidden": false, + "positional": 0 + }, + { + "name": "help", + "description": "Shows a help message.", + "type": "boolean", + "required": false, + "aliases": [], + "hidden": false + }, + { + "name": "helpJson", + "description": "Shows the metadata associated with each flags, in JSON format.", + "type": "boolean", + "required": false, + "aliases": [], + "hidden": false + } + ], + "aliases": ['a'], + "scope": "in" +} +`; + +const fileInfo = {content, baseName: 'add'}; + +describe('cli-command reader', () => { + describe('getDocs', () => { + it('should return an array containing a single doc', () => { + const docs = reader.getDocs(fileInfo); + expect(docs.length).toEqual(1); + }); + + it('should return a cli-command doc', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0]).toEqual(jasmine.objectContaining({ + id: 'cli-add', + docType: 'cli-command', + })); + }); + + it('should extract the name from the fileInfo', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].name).toEqual('add'); + }); + + it('should compute the id and aliases', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].id).toEqual('cli-add'); + expect(docs[0].aliases).toEqual(['cli-add', 'cli-a']); + }); + + it('should compute the path and outputPath', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].path).toEqual('cli/add'); + expect(docs[0].outputPath).toEqual('cli/add.json'); + }); + + it('should compute the bread crumbs', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].breadCrumbs).toEqual([ + { text: 'CLI', path: 'cli' }, + { text: 'add', path: 'cli/add' }, + ]); + }); + + it('should start at line 1', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].startingLine).toEqual(1); + }); + + it('should extract the short description into the content', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].content).toEqual('Add support for a library to your project.'); + }); + + it('should extract the long description', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].longDescription).toEqual('Add support for a library in your project, for example adding `@angular/pwa` which would configure\nyour project for PWA support.\n'); + }); + + it('should extract the command type', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].type).toEqual('custom'); + }); + + it('should extract the command scope', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].scope).toEqual('in'); + }); + + it('should extract the command aliases', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].commandAliases).toEqual(['a']); + }); + + it('should extract the options', () => { + const docs = reader.getDocs(fileInfo); + expect(docs[0].options).toEqual([ + jasmine.objectContaining({ name: 'collection' }), + jasmine.objectContaining({ name: 'help' }), + jasmine.objectContaining({ name: 'helpJson' }), + ]); + }); + }); +}); diff --git a/aio/tools/transforms/cli-docs-package/rendering/cliNegate.js b/aio/tools/transforms/cli-docs-package/rendering/cliNegate.js new file mode 100644 index 0000000000..a895be67dd --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/rendering/cliNegate.js @@ -0,0 +1,6 @@ +module.exports = function cliNegate() { + return { + name: 'cliNegate', + process: function(str) { return 'no' + str.charAt(0).toUpperCase() + str.slice(1); } + }; +}; \ No newline at end of file diff --git a/aio/tools/transforms/cli-docs-package/rendering/cliNegate.spec.js b/aio/tools/transforms/cli-docs-package/rendering/cliNegate.spec.js new file mode 100644 index 0000000000..15b26199e6 --- /dev/null +++ b/aio/tools/transforms/cli-docs-package/rendering/cliNegate.spec.js @@ -0,0 +1,17 @@ +var factory = require('./cliNegate'); + +describe('cliNegate filter', function() { + var filter; + + beforeEach(function() { filter = factory(); }); + + it('should be called "cliNegate"', function() { expect(filter.name).toEqual('cliNegate'); }); + + it('should make the first char uppercase and add `no` to the front', function() { + expect(filter.process('abc')).toEqual('noAbc'); + }); + + it('should make leave the rest of the chars alone', function() { + expect(filter.process('abCdE')).toEqual('noAbCdE'); + }); +}); \ No newline at end of file diff --git a/aio/tools/transforms/templates/cli/cli-command.template.html b/aio/tools/transforms/templates/cli/cli-command.template.html new file mode 100644 index 0000000000..c6c489a679 --- /dev/null +++ b/aio/tools/transforms/templates/cli/cli-command.template.html @@ -0,0 +1,18 @@ +{% import 'lib/cli.html' as cli %} + +
+ {% include 'include/cli-breadcrumb.html' %} + {% include 'include/cli-header.html' %} + + + +
+ {$ doc.shortDescription | marked $} + {$ doc.description | marked $} + {$ cli.renderSyntax(doc) $} + {$ cli.renderArguments(doc.positionalOptions, 2) $} + {$ cli.renderNamedOptions(doc.namedOptions, 2) $} + {$ cli.renderSubcommands(doc) $} + {$ doc.longDescription | marked $} +
+
diff --git a/aio/tools/transforms/templates/cli/cli-container.template.html b/aio/tools/transforms/templates/cli/cli-container.template.html new file mode 100644 index 0000000000..b9bf21596a --- /dev/null +++ b/aio/tools/transforms/templates/cli/cli-container.template.html @@ -0,0 +1,23 @@ +
+{$ doc.description | marked $} +
+ +

Command Overview

+ + + + + + + + + +{% for command in doc.commands %} + + + + + +{% endfor %} + +
CommandAliasDescription
{$ command.name $}{% for alias in command.commandAliases %}{$ alias $} {% endfor %}{$ command.description | marked $}
\ No newline at end of file diff --git a/aio/tools/transforms/templates/cli/include/cli-breadcrumb.html b/aio/tools/transforms/templates/cli/include/cli-breadcrumb.html new file mode 100644 index 0000000000..00f748f565 --- /dev/null +++ b/aio/tools/transforms/templates/cli/include/cli-breadcrumb.html @@ -0,0 +1,16 @@ +{% set comma = joiner(',') %} +{% set slash = joiner('/') %} + + diff --git a/aio/tools/transforms/templates/cli/include/cli-header.html b/aio/tools/transforms/templates/cli/include/cli-header.html new file mode 100644 index 0000000000..a02041d0e3 --- /dev/null +++ b/aio/tools/transforms/templates/cli/include/cli-header.html @@ -0,0 +1,4 @@ + +
+

ng {$ doc.name $}

+
diff --git a/aio/tools/transforms/templates/cli/lib/cli.html b/aio/tools/transforms/templates/cli/lib/cli.html new file mode 100644 index 0000000000..4255d314c6 --- /dev/null +++ b/aio/tools/transforms/templates/cli/lib/cli.html @@ -0,0 +1,111 @@ +{% macro renderSyntax(container, prefix) -%} +{% for name in container.names %} +ng {%if prefix %}{$ prefix $} {% endif %}{$ name $} + {%- for arg in container.positionalOptions %} <{$ arg.name $}>{% endfor %} + {%- if container.namedOptions.length %} [options]{% endif -%} + +{% endfor %} +{% endmacro %} + +{% macro renderArguments(arguments, level = 2) %} +{% if arguments.length %} +Arguments + + + + + + + + + {% for option in arguments %} + + + + + {% endfor %} + +
ArgumentDescription
{$ option.name $} + {$ option.description | marked $} + {% if option.subcommands.length -%} +

This option can take one of the following sub-commands:

+

+ {%- endif %} +
+{% endif %} +{% endmacro %} + +{% macro renderNamedOptions(options, level = 2) %} +{% if options.length %} +Options + + + + + + + + + {% for option in options %} + + + + + {% endfor %} + +
OptionDescription
+ {% for type in option.types -%} + {% for alias in option.names -%} + {$ renderOption(option.name, alias, type, option.default, option.enum) $} + {% if not loop.last %}
{% endif %} + {% endfor -%} + {% if not loop.last %}
{% endif %} + {% endfor -%} +
+ {$ option.description | marked $} +
+{% endif %} +{% endmacro %} + +{% macro bold(isBold, contents) -%} +{$ contents $} +{%- endmacro -%} + +{%- macro renderValues(values, default) -%} +{% for value in values %}{$ bold(value==default, value) $}{% if not loop.last %}|{% endif %}{% endfor %} +{%- endmacro -%} + +{%- macro renderOption(name, alias, type, default, values) -%} +{% set prefix = '--' if (name === alias and name.length > 1) else '-' %} +{%- if type === 'boolean' -%} +{%- if not values.length %}{$ prefix $}{$ alias $}={$ renderValues([true, false], default) $} +{% endif -%} +{$ prefix $}{$ bold(default, alias) $}|{$ prefix $}{$ bold(not default, alias | cliNegate) $} +{%- elif values.length -%} +{$ prefix $}{$ alias $}={$ renderValues(values, default) $} +{%- elif type === 'string' -%} +{$ prefix $}{$ alias $}={$ name $} +{%- else -%} +{$ prefix $}{$ alias $} +{%- endif -%} +{%- endmacro -%} + +{%- macro renderSubcommands(container) -%} +{% for command in container.positionalOptions %}{% if command.subcommands.length %} +

{$ command.name | title $} commands

+{% for subcommand in command.subcommands %} +

{$ subcommand.name $}

+{% for name in container.names %} +{$ renderSyntax(subcommand, name) $} +{% endfor %} +{$ subcommand.description | marked $} +{# for now we assume that commands do not have further sub-commands #} +{$ renderArguments(subcommand.positionalOptions, 4) $} +{$ renderNamedOptions(subcommand.namedOptions, 4) $} +{% endfor %} +{% endif %}{% endfor %} +{%- endmacro -%} \ No newline at end of file diff --git a/aio/yarn.lock b/aio/yarn.lock index c3e52ea2ec..5dab946fb1 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -6102,6 +6102,10 @@ json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -6124,6 +6128,12 @@ json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + dependencies: + minimist "^1.2.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"