diff --git a/.pullapprove.yml b/.pullapprove.yml index fd8a6293b2..31e2b222a6 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -1232,6 +1232,7 @@ groups: 'goldens/public-api/**', 'CHANGELOG.md', 'docs/NAMING.md', + 'aio/content/errors/*.md', 'aio/content/guide/glossary.md', 'aio/content/guide/styleguide.md', 'aio/content/examples/styleguide/**', diff --git a/aio/content/errors/index.md b/aio/content/errors/index.md new file mode 100644 index 0000000000..ecff113c4a --- /dev/null +++ b/aio/content/errors/index.md @@ -0,0 +1 @@ +# Errors List \ No newline at end of file diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 9ed124c84e..ed1348a342 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -56,35 +56,35 @@ "tooltip": "Set up your environment and learn basic concepts", "children": [ { - "title": "Try it", - "tooltip": "Examine and work with a ready-made sample app, with no setup.", - "children": [ - { - "url": "start", - "title": "Getting started", - "tooltip": "Take a look at Angular's component model, template syntax, and component communication." - }, - { - "url": "start/start-routing", - "title": "Adding navigation", - "tooltip": "Navigate among different page views using the browser's URL." - }, - { - "url": "start/start-data", - "title": "Managing Data", - "tooltip": "Use services and access external data via HTTP." - }, - { - "url": "start/start-forms", - "title": "Using Forms for User Input", - "tooltip": "Learn about fetching and managing data from users with forms." - }, - { - "url": "start/start-deployment", - "title": "Deploying an application", - "tooltip": "Move to local development, or deploy your application to Firebase or your own server." - } - ] + "title": "Try it", + "tooltip": "Examine and work with a ready-made sample app, with no setup.", + "children": [ + { + "url": "start", + "title": "Getting started", + "tooltip": "Take a look at Angular's component model, template syntax, and component communication." + }, + { + "url": "start/start-routing", + "title": "Adding navigation", + "tooltip": "Navigate among different page views using the browser's URL." + }, + { + "url": "start/start-data", + "title": "Managing Data", + "tooltip": "Use services and access external data via HTTP." + }, + { + "url": "start/start-forms", + "title": "Using Forms for User Input", + "tooltip": "Learn about fetching and managing data from users with forms." + }, + { + "url": "start/start-deployment", + "title": "Deploying an application", + "tooltip": "Move to local development, or deploy your application to Firebase or your own server." + } + ] }, { "url": "guide/setup-local", @@ -916,6 +916,17 @@ "tooltip": "Details of the Angular packages, classes, interfaces, and other types.", "url": "api" }, + { + "title": "Error Reference", + "tooltip": "Details of the errors that can be thrown by Angular.", + "children": [ + { + "title": "Overview", + "url": "errors", + "hidden": true + } + ] + }, { "title": "Example applications", "tooltip": "List of all of the example applications in the Angular documentation.", diff --git a/aio/src/styles/1-layouts/_top-menu.scss b/aio/src/styles/1-layouts/_top-menu.scss index b6ad206fd5..7493e3ef48 100644 --- a/aio/src/styles/1-layouts/_top-menu.scss +++ b/aio/src/styles/1-layouts/_top-menu.scss @@ -52,6 +52,7 @@ aio-shell.folder-api mat-toolbar.mat-toolbar, aio-shell.folder-cli mat-toolbar.mat-toolbar, aio-shell.folder-docs mat-toolbar.mat-toolbar, aio-shell.folder-guide mat-toolbar.mat-toolbar, +aio-shell.folder-errors mat-toolbar.mat-toolbar, aio-shell.folder-start mat-toolbar.mat-toolbar, aio-shell.folder-tutorial mat-toolbar.mat-toolbar { @media (min-width: $showTopMenuWidth) { diff --git a/aio/src/styles/2-modules/_errors.scss b/aio/src/styles/2-modules/_errors.scss new file mode 100644 index 0000000000..d4d1a5c9bd --- /dev/null +++ b/aio/src/styles/2-modules/_errors.scss @@ -0,0 +1,59 @@ + +.error-list { + display: grid; + list-style: none; + padding: 0; + overflow: hidden; + + @media screen and (max-width: 600px) { + margin: 0 0 0 -8px; + } + + li { + @include font-size(14); + margin: 8px 0; + @include line-height(14); + padding: 0; + float: left; + overflow: hidden; + min-width: 220px; + text-overflow: ellipsis; + white-space: nowrap; + + .symbol { + margin-right: 8px; + + &.runtime { + background: $green-500; + } + + &.compiler { + background: $blue-500; + } + } + + .symbol.runtime:before { + content: "R"; + } + + .symbol.compiler:before { + content: "C"; + } + + a { + color: $blue-grey-600; + display: inline-block; + @include line-height(16); + padding: 0 16px 0; + text-decoration: none; + transition: all .3s; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + background: $blue-grey-50; + color: $blue-500; + } + } + } +} \ No newline at end of file diff --git a/aio/src/styles/2-modules/_modules-dir.scss b/aio/src/styles/2-modules/_modules-dir.scss index 76302968a5..d55755d566 100644 --- a/aio/src/styles/2-modules/_modules-dir.scss +++ b/aio/src/styles/2-modules/_modules-dir.scss @@ -15,6 +15,7 @@ @import 'deploy-theme'; @import 'details'; @import 'edit-page-cta'; + @import 'errors'; @import 'features'; @import 'filetree'; @import 'heading-anchors'; diff --git a/aio/tools/transforms/angular-api-package/index.js b/aio/tools/transforms/angular-api-package/index.js index 8b6e065ed2..6e2d571c88 100644 --- a/aio/tools/transforms/angular-api-package/index.js +++ b/aio/tools/transforms/angular-api-package/index.js @@ -17,7 +17,6 @@ module.exports = // Register the processors .processor(require('./processors/mergeParameterInfo')) .processor(require('./processors/processPseudoClasses')) - .processor(require('./processors/splitDescription')) .processor(require('./processors/convertPrivateClassesToInterfaces')) .processor(require('./processors/generateApiListDoc')) .processor(require('./processors/addNotYetDocumentedProperty')) diff --git a/aio/tools/transforms/angular-base-package/index.js b/aio/tools/transforms/angular-base-package/index.js index 4fcb0c4631..ab7020c6d3 100644 --- a/aio/tools/transforms/angular-base-package/index.js +++ b/aio/tools/transforms/angular-base-package/index.js @@ -32,6 +32,7 @@ module.exports = new Package('angular-base', [ .processor(require('./processors/copyContentAssets')) .processor(require('./processors/renderLinkInfo')) .processor(require('./processors/checkContentRules')) + .processor(require('./processors/splitDescription')) // overrides base packageInfo and returns the one for the 'angular/angular' repo. .factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); }) diff --git a/aio/tools/transforms/angular-api-package/processors/splitDescription.js b/aio/tools/transforms/angular-base-package/processors/splitDescription.js similarity index 100% rename from aio/tools/transforms/angular-api-package/processors/splitDescription.js rename to aio/tools/transforms/angular-base-package/processors/splitDescription.js diff --git a/aio/tools/transforms/angular-api-package/processors/splitDescription.spec.js b/aio/tools/transforms/angular-base-package/processors/splitDescription.spec.js similarity index 100% rename from aio/tools/transforms/angular-api-package/processors/splitDescription.spec.js rename to aio/tools/transforms/angular-base-package/processors/splitDescription.spec.js diff --git a/aio/tools/transforms/angular-errors-package/index.js b/aio/tools/transforms/angular-errors-package/index.js new file mode 100644 index 0000000000..961b727739 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/index.js @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const path = 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'); + +const errorPackage = new Package('angular-errors', [basePackage, contentPackage]); +errorPackage.factory(require('./readers/error')) + .processor(require('./processors/processErrorDocs')) + .processor(require('./processors/processErrorsContainerDoc')) + + // Where do we find the error documentation files? + .config(function(readFilesProcessor, errorFileReader) { + readFilesProcessor.fileReaders.push(errorFileReader); + readFilesProcessor.sourceFiles = readFilesProcessor.sourceFiles.concat([ + { + basePath: CONTENTS_PATH, + include: CONTENTS_PATH + '/errors/**/*.md', + exclude: CONTENTS_PATH + '/errors/index.md', + fileReader: 'errorFileReader' + }, + { + basePath: CONTENTS_PATH, + include: CONTENTS_PATH + '/errors/index.md', + fileReader: 'contentFileReader' + }, + ]); + }) + + // Here we compute the `id`, `code`, `aliases`, `path` and `outputPath` for the `error` docs. + // * The `id` is the same as the `path` (the source path with the `.md` stripped off). + // * The `code` is the id without any containing paths (currently all errors must be on the top + // level). + // * The `aliases` are used for automatic code linking and search terms. + .config(function(computeIdsProcessor, computePathsProcessor) { + computeIdsProcessor.idTemplates.push({ + docTypes: ['error'], + getId: function(doc) { + return doc.fileInfo + .relativePath + // strip off the extension + .replace(/\.\w*$/, ''); + }, + getAliases: function(doc) { + doc.code = path.basename(doc.id); + return [doc.id, doc.code]; + } + }); + + computePathsProcessor.pathTemplates = computePathsProcessor.pathTemplates.concat([ + { + docTypes: ['error'], + getPath: (doc) => doc.id, + outputPathTemplate: '${path}.json', + }, + ]); + }) + + // Configure jsdoc-style tag parsing + .config(function(parseTagsProcessor, getInjectables) { + // Load up all the tag definitions in the tag-defs folder + parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat( + getInjectables(requireFolder(__dirname, './tag-defs'))); + }) + + // The templates that define how the `error` and `error-container` doc-types are rendered are + // found in the `TEMPLATES_PATH/error` directory. + .config(function(templateFinder) { + templateFinder.templateFolders.unshift(path.resolve(TEMPLATES_PATH, 'error')); + }) + + // The AIO application expects content files to be provided as JSON files that it requests via + // HTTP. So here we tell the `convertToJsonProcessor` to include docs of type `error` in those + // that it converts. + .config(function(convertToJsonProcessor, postProcessHtml) { + convertToJsonProcessor.docTypes.push('error'); + postProcessHtml.docTypes.push('error'); + }); + +module.exports = errorPackage; \ No newline at end of file diff --git a/aio/tools/transforms/angular-errors-package/processors/processErrorDocs.js b/aio/tools/transforms/angular-errors-package/processors/processErrorDocs.js new file mode 100644 index 0000000000..ca661fcfd2 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/processors/processErrorDocs.js @@ -0,0 +1,55 @@ +module.exports = function processErrorDocs(createDocMessage) { + return { + $runAfter: ['extra-docs-added'], + $runBefore: ['rendering-docs'], + $process(docs) { + const navigationDoc = docs.find(doc => doc.docType === 'navigation-json'); + const errorsNode = navigationDoc && findErrorsNode(navigationDoc.data['SideNav']); + + if (!errorsNode) { + throw new Error(createDocMessage( + 'Missing `errors` url - This node is needed as a place to insert the generated errors docs.', + navigationDoc)); + } + + docs.forEach(doc => { + if (doc.docType === 'error') { + // Add to navigation doc + const title = `${doc.code}: ${doc.name}`; + errorsNode.children.push({url: doc.path, title: title, tooltip: doc.name}); + } + }); + }, + }; +}; + +/** + * Look for the `errors` navigation node. It is the node whose first child has `url: 'errors'`. + * (NOTE: Using the URL instead of the title, because it is more robust.) + * + * We will "recursively" check all navigation nodes and their children (in breadth-first order), + * until we find the `errors` node. Keep a list of nodes lists to check. + * (NOTE: Each item in the list is a LIST of nodes.) + */ +function findErrorsNode(nodes) { + const nodesList = [nodes]; + + while (nodesList.length > 0) { + // Get the first item from the list of nodes lists. + const currentNodes = nodesList.shift(); + const errorsNode = currentNodes.find(isErrorsNode); + + // One of the nodes in `currentNodes` was the `errors` node. Return it. + if (errorsNode) return errorsNode; + + // The `errors` node is not in `currentNodes`. Check each node's children (if any). + currentNodes.forEach(node => node.children && nodesList.push(node.children)); + } + + // We checked all navigation nodes and their children and did not find the `errors` node. + return undefined; +} + +function isErrorsNode(node) { + return node.children && node.children.length && node.children[0].url === 'errors'; +} diff --git a/aio/tools/transforms/angular-errors-package/processors/processErrorDocs.spec.js b/aio/tools/transforms/angular-errors-package/processors/processErrorDocs.spec.js new file mode 100644 index 0000000000..908f0003a6 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/processors/processErrorDocs.spec.js @@ -0,0 +1,125 @@ +const testPackage = require('../../helpers/test-package'); +const Dgeni = require('dgeni'); + +describe('processErrorDocs processor', () => { + let dgeni, injector, processor, createDocMessage; + + beforeEach(() => { + dgeni = new Dgeni([testPackage('angular-errors-package')]); + injector = dgeni.configureInjector(); + processor = injector.get('processErrorDocs'); + createDocMessage = injector.get('createDocMessage'); + }); + + it('should be available on the injector', () => { + expect(processor.$process).toBeDefined(); + }); + + it('should run after the correct processor', () => { + expect(processor.$runAfter).toEqual(['extra-docs-added']); + }); + + it('should run before the correct processor', () => { + expect(processor.$runBefore).toEqual(['rendering-docs']); + }); + + it('should add the error to the `errors` node in the navigation doc if there is a top level node with a `errors` url', + () => { + const errorDoc = { + docType: 'error', + name: 'error1', + code: '888', + path: 'errors/error1', + }; + const navigation = { + docType: 'navigation-json', + data: { + SideNav: [ + {url: 'some/page', title: 'Some Page'}, + { + title: 'Errors', + children: [{'title': 'Overview', 'url': 'errors'}], + }, + {url: 'other/page', title: 'Other Page'}, + ], + }, + }; + processor.$process([errorDoc, navigation]); + expect(navigation.data.SideNav[1].title).toEqual('Errors'); + expect(navigation.data.SideNav[1].children).toEqual([ + {url: 'errors', title: 'Overview'}, + {url: 'errors/error1', title: '888: error1', tooltip: 'error1'}, + ]); + }); + + it('should detect the `errors` node if it is nested in another node', () => { + const errorDoc = { + docType: 'error', + name: 'error1', + code: '888', + path: 'errors/error1', + }; + const navigation = { + docType: 'navigation-json', + data: { + SideNav: [ + {url: 'some/page', title: 'Some Page'}, + { + title: 'Errors Grandparent', + children: [ + {url: 'some/nested/page', title: 'Some Nested Page'}, + { + title: 'Errors Parent', + children: [ + {url: 'some/more/nested/page', title: 'Some More Nested Page'}, + { + title: 'Errors', + children: [{'title': 'Overview', 'url': 'errors'}], + }, + {url: 'other/more/nested/page', title: 'Other More Nested Page'}, + ], + }, + {url: 'other/nested/page', title: 'Other Nested Page'}, + ], + }, + {url: 'other/page', title: 'Other Page'}, + ], + }, + }; + + processor.$process([errorDoc, navigation]); + + const errorsContainerNode = navigation.data.SideNav[1].children[1].children[1]; + expect(errorsContainerNode.title).toEqual('Errors'); + expect(errorsContainerNode.children).toEqual([ + {url: 'errors', title: 'Overview'}, + {url: 'errors/error1', title: '888: error1', tooltip: 'error1'}, + ]); + }); + + it('should complain if there is no child with `errors` url', () => { + const errorDoc = { + docType: 'error', + name: 'error1', + code: '888', + path: 'errors/error1', + }; + const navigation = { + docType: 'navigation-json', + data: { + SideNav: [ + {url: 'some/page', title: 'Some Page'}, { + title: 'Errors', + tooltip: 'Angular Error reference', + children: [{'title': 'Overview', 'url': 'not-errors'}] + }, + {url: 'other/page', title: 'Other Page'} + ] + } + }; + expect(() => processor.$process([errorDoc, navigation])) + .toThrowError(createDocMessage( + 'Missing `errors` url - This node is needed as a place to insert the generated errors docs.', + navigation)); + }); +}); diff --git a/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js b/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js new file mode 100644 index 0000000000..876674beff --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.js @@ -0,0 +1,11 @@ +module.exports = function processErrorsContainerDoc() { + return { + $runAfter: ['extra-docs-added'], + $runBefore: ['rendering-docs'], + $process(docs) { + const errorsDoc = docs.find(doc => doc.id === 'errors/index'); + errorsDoc.id = 'errors-container'; + errorsDoc.errors = docs.filter(doc => doc.docType === 'error'); + } + }; +}; diff --git a/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.spec.js b/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.spec.js new file mode 100644 index 0000000000..ef8cb17276 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/processors/processErrorsContainerDoc.spec.js @@ -0,0 +1,22 @@ +const testPackage = require('../../helpers/test-package'); +const processorFactory = require('./processErrorsContainerDoc'); +const Dgeni = require('dgeni'); + +describe('processErrorsContainerDoc processor', () => { + it('should be available on the injector', () => { + const dgeni = new Dgeni([testPackage('angular-errors-package')]); + const injector = dgeni.configureInjector(); + const processor = injector.get('processErrorsContainerDoc'); + 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/angular-errors-package/readers/error.js b/aio/tools/transforms/angular-errors-package/readers/error.js new file mode 100644 index 0000000000..1a974ba02a --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/readers/error.js @@ -0,0 +1,23 @@ +/** + * @dgService + * @description + * This file reader will pull the contents from a text file (by default .md) + * + * The doc will initially have the form: + * ``` + * { + * docType: 'error', + * content: 'the content of the file', + * } + * ``` + */ +module.exports = function errorFileReader() { + return { + name: 'errorFileReader', + defaultPattern: /\.md$/, + getDocs: function(fileInfo) { + // We return a single element array because content files only contain one document + return [{docType: 'error', content: fileInfo.content}]; + } + }; +}; diff --git a/aio/tools/transforms/angular-errors-package/readers/error.spec.js b/aio/tools/transforms/angular-errors-package/readers/error.spec.js new file mode 100644 index 0000000000..bfc07b2073 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/readers/error.spec.js @@ -0,0 +1,44 @@ +const Dgeni = require('dgeni'); +const path = require('canonical-path'); +const testPackage = require('../../helpers/test-package'); + +describe('errorFileReader', () => { + let dgeni, injector, fileReader; + + beforeEach(() => { + dgeni = new Dgeni([testPackage('angular-errors-package', false)]); + injector = dgeni.configureInjector(); + fileReader = injector.get('errorFileReader'); + }); + + function createFileInfo(file, content, basePath) { + return { + fileReader: fileReader.name, + filePath: file, + baseName: path.basename(file, path.extname(file)), + extension: path.extname(file).replace(/^\./, ''), + basePath: basePath, + relativePath: path.relative(basePath, file), + content: content + }; + } + + describe('defaultPattern', () => { + it('should match .md files', () => { + expect(fileReader.defaultPattern.test('abc.md')).toBeTruthy(); + expect(fileReader.defaultPattern.test('abc.js')).toBeFalsy(); + }); + }); + + + describe('getDocs', () => { + it('should return an object containing info about the file and its contents', () => { + const fileInfo = createFileInfo( + 'project/path/modules/someModule/foo/docs/subfolder/bar.md', 'A load of content', + 'project/path'); + expect(fileReader.getDocs(fileInfo)).toEqual([ + {docType: 'error', content: 'A load of content'} + ]); + }); + }); +}); diff --git a/aio/tools/transforms/angular-errors-package/tag-defs/category.js b/aio/tools/transforms/angular-errors-package/tag-defs/category.js new file mode 100644 index 0000000000..51619bc782 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/tag-defs/category.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'category'}; +}; diff --git a/aio/tools/transforms/angular-errors-package/tag-defs/debugging.js b/aio/tools/transforms/angular-errors-package/tag-defs/debugging.js new file mode 100644 index 0000000000..c6e46e7054 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/tag-defs/debugging.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'debugging'}; +}; diff --git a/aio/tools/transforms/angular-errors-package/tag-defs/shortDescription.js b/aio/tools/transforms/angular-errors-package/tag-defs/shortDescription.js new file mode 100644 index 0000000000..60a54b99c9 --- /dev/null +++ b/aio/tools/transforms/angular-errors-package/tag-defs/shortDescription.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'shortDescription'}; +}; diff --git a/aio/tools/transforms/angular.io-package/index.js b/aio/tools/transforms/angular.io-package/index.js index 1e4b50ea4d..139937e839 100644 --- a/aio/tools/transforms/angular.io-package/index.js +++ b/aio/tools/transforms/angular.io-package/index.js @@ -9,12 +9,13 @@ const Package = require('dgeni').Package; const gitPackage = require('dgeni-packages/git'); const apiPackage = require('../angular-api-package'); const contentPackage = require('../angular-content-package'); +const errorsPackage = require('../angular-errors-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, cliDocsPackage]) +module.exports = new Package('angular.io', [gitPackage, apiPackage, contentPackage, cliDocsPackage, errorsPackage]) // This processor relies upon the versionInfo. See below... .processor(require('./processors/processNavigationMap')) diff --git a/aio/tools/transforms/templates/error/error.template.html b/aio/tools/transforms/templates/error/error.template.html new file mode 100644 index 0000000000..514b8f1c32 --- /dev/null +++ b/aio/tools/transforms/templates/error/error.template.html @@ -0,0 +1,18 @@ +{% import "lib/githubLinks.html" as github -%} + +
{$ error.code $}: {$ error.name $}
+
+