feat(docs-infra): generate Angular CLI command reference (#25363)

PR Close #25363
This commit is contained in:
Pete Bacon Darwin
2018-09-14 10:05:57 +01:00
committed by Kara Erickson
parent 39a67548ac
commit f29b218060
28 changed files with 965 additions and 15 deletions

View File

@ -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');

View File

@ -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']);
});

View File

@ -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);
}
};
};

View File

@ -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 },
]);
});
});

View File

@ -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]);
}

View File

@ -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' },
]);
});
});

View File

@ -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');
}
};
};

View File

@ -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']);
});
});

View File

@ -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}`);
}

View File

@ -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' }),
]);
});
});
});

View File

@ -0,0 +1,6 @@
module.exports = function cliNegate() {
return {
name: 'cliNegate',
process: function(str) { return 'no' + str.charAt(0).toUpperCase() + str.slice(1); }
};
};

View File

@ -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');
});
});