From 4e10faf1eb94778112434a84b06a86194e1aed35 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 20 Mar 2017 22:23:20 +0000 Subject: [PATCH] build(aio): add version into navigation.json The navigation.json is now passed through the dgeni pipeline. The source file has been moved to `aio/content/navigation.json` but the generated file will now appear where the original source file was found, `aio/src/content/navigation.json`. Everything inside `aio/src/content` is now generated and ignored by git. The `processNavigationMap` processor in this commit adds the current version information to the navigation.json file and verifies the relative urls in the file map to real documents. The navigationService exposes the versionInfo as an observable, which the AppComponent renders at the top of the sidenav. --- aio/.gitignore | 2 +- aio/{src => }/content/navigation.json | 0 aio/src/app/app.component.html | 2 +- aio/src/app/app.component.ts | 4 +- .../app/navigation/navigation.service.spec.ts | 21 +++++- aio/src/app/navigation/navigation.service.ts | 72 ++++++++++++++++--- aio/src/styles/1-layouts/_sidenav.scss | 4 ++ aio/transforms/angular.io-package/index.js | 14 +++- .../processors/processNavigationMap.js | 50 +++++++++++++ .../angular.io-package/readers/navigation.js | 19 +++++ 10 files changed, 172 insertions(+), 16 deletions(-) rename aio/{src => }/content/navigation.json (100%) create mode 100644 aio/transforms/angular.io-package/processors/processNavigationMap.js create mode 100644 aio/transforms/angular.io-package/readers/navigation.js diff --git a/aio/.gitignore b/aio/.gitignore index 0a503cdc1e..2f721086ca 100644 --- a/aio/.gitignore +++ b/aio/.gitignore @@ -8,5 +8,5 @@ yarn-error.log # Ignore generated content /dist /tmp -/src/content/docs +/src/content /.sass-cache diff --git a/aio/src/content/navigation.json b/aio/content/navigation.json similarity index 100% rename from aio/src/content/navigation.json rename to aio/content/navigation.json diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index b65a14ec00..9f17aa2949 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -8,6 +8,7 @@ +
{{ (versionInfo | async)?.full }}
@@ -16,5 +17,4 @@ -
\ No newline at end of file diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index 688d7a6cee..8e63ac7cf9 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -5,7 +5,7 @@ import { GaService } from 'app/shared/ga.service'; import { LocationService } from 'app/shared/location.service'; import { DocumentService, DocumentContents } from 'app/documents/document.service'; import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component'; -import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service'; +import { NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service'; import { SearchService } from 'app/search/search.service'; import { SearchResultsComponent } from 'app/search/search-results/search-results.component'; import { AutoScrollService } from 'app/shared/auto-scroll.service'; @@ -24,6 +24,7 @@ export class AppComponent implements OnInit { currentDocument: Observable; navigationViews: Observable; selectedNodes: Observable; + versionInfo: Observable; @ViewChildren('searchBox, searchResults', { read: ElementRef }) searchElements: QueryList; @@ -45,6 +46,7 @@ export class AppComponent implements OnInit { locationService.currentUrl.subscribe(url => gaService.locationChanged(url)); this.navigationViews = navigationService.navigationViews; this.selectedNodes = navigationService.selectedNodes; + this.versionInfo = navigationService.versionInfo; } ngOnInit() { diff --git a/aio/src/app/navigation/navigation.service.spec.ts b/aio/src/app/navigation/navigation.service.spec.ts index e2aae81f9f..104a4fe821 100644 --- a/aio/src/app/navigation/navigation.service.spec.ts +++ b/aio/src/app/navigation/navigation.service.spec.ts @@ -1,7 +1,7 @@ import { ReflectiveInjector } from '@angular/core'; import { Http, ConnectionBackend, RequestOptions, BaseRequestOptions, Response, ResponseOptions } from '@angular/http'; import { MockBackend } from '@angular/http/testing'; -import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service'; +import { NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service'; import { LocationService } from 'app/shared/location.service'; import { MockLocationService } from 'testing/location.service'; import { Logger } from 'app/shared/logger.service'; @@ -125,4 +125,23 @@ describe('NavigationService', () => { expect(currentNodes).toEqual([]); }); }); + + describe('versionInfo', () => { + let service: NavigationService, versionInfo: VersionInfo; + + beforeEach(() => { + service = injector.get(NavigationService); + service.versionInfo.subscribe(info => versionInfo = info); + + const backend = injector.get(ConnectionBackend); + backend.connectionsArray[0].mockRespond(createResponse({ + ['__versionInfo']: { raw: '4.0.0' } + })); + }); + + it('should extract the version info', () => { + const backend = injector.get(ConnectionBackend); + expect(versionInfo).toEqual({ raw: '4.0.0' }); + }); + }); }); diff --git a/aio/src/app/navigation/navigation.service.ts b/aio/src/app/navigation/navigation.service.ts index de5d4a500a..3b7db011af 100644 --- a/aio/src/app/navigation/navigation.service.ts +++ b/aio/src/app/navigation/navigation.service.ts @@ -12,28 +12,62 @@ import { LocationService } from 'app/shared/location.service'; import { NavigationNode } from './navigation-node'; export { NavigationNode } from './navigation-node'; +export type NavigationResponse = {'__versionInfo': VersionInfo } & { [name: string]: NavigationNode[] }; + export interface NavigationViews { [name: string]: NavigationNode[]; } +export interface NavigationMap { + [url: string]: NavigationNode; +} + +export interface VersionInfo { + raw: string; + major: number; + minor: number; + patch: number; + prerelease: string[]; + build: string; + version: string; + codeName: string; + isSnapshot: boolean; + full: string; + branch: string; + commitSHA: string; +} + const navigationPath = 'content/navigation.json'; + @Injectable() export class NavigationService { /** * An observable collection of NavigationNode trees, which can be used to render navigational menus */ - navigationViews = this.fetchNavigationViews(); + navigationViews: Observable; + + /** + * The current version of doc-app that we are running + */ + versionInfo: Observable; + /** * An observable array of nodes that indicate which nodes in the `navigationViews` match the current URL location */ - selectedNodes = this.getSelectedNodes(); + selectedNodes: Observable; - constructor(private http: Http, private location: LocationService, private logger: Logger) { } + constructor(private http: Http, private location: LocationService, private logger: Logger) { + const navigationInfo = this.fetchNavigationInfo(); + // The version information is packaged inside the navigation response to save us an extra request. + this.versionInfo = this.getVersionInfo(navigationInfo); + this.navigationViews = this.getNavigationViews(navigationInfo); + this.selectedNodes = this.getSelectedNodes(this.navigationViews); + } /** - * Get an observable that fetches the `NavigationViews` from the server. + * Get an observable that fetches the `NavigationResponse` from the server. * We create an observable by calling `http.get` but then publish it to share the result * among multiple subscribers, without triggering new requests. * We use `publishLast` because once the http request is complete the request observable completes. @@ -43,10 +77,22 @@ export class NavigationService { * another request to the server. * We are not storing the subscription from connecting as we do not expect this service to be destroyed. */ - private fetchNavigationViews(): Observable { - const navigationViews = this.http.get(navigationPath) - .map(res => res.json() as NavigationViews) + private fetchNavigationInfo(): Observable { + const navigationInfo = this.http.get(navigationPath) + .map(res => res.json() as NavigationResponse) .publishLast(); + navigationInfo.connect(); + return navigationInfo; + } + + private getVersionInfo(navigationInfo: Observable) { + const versionInfo = navigationInfo.map(response => response['__versionInfo']).publishReplay(1); + versionInfo.connect(); + return versionInfo; + } + + private getNavigationViews(navigationInfo: Observable) { + const navigationViews = navigationInfo.map(response => unpluck(response, '__versionInfo')).publishReplay(1); navigationViews.connect(); return navigationViews; } @@ -57,9 +103,9 @@ export class NavigationService { * URL change before they receive an emission. * See above for discussion of using `connect`. */ - private getSelectedNodes() { + private getSelectedNodes(navigationViews: Observable) { const selectedNodes = combineLatest( - this.navigationViews.map(this.computeUrlToNodesMap), + navigationViews.map(this.computeUrlToNodesMap), this.location.currentUrl, (navMap, url) => navMap[url] || []) .publishReplay(1); @@ -74,7 +120,7 @@ export class NavigationService { * @param navigation A collection of navigation nodes that are to be mapped */ private computeUrlToNodesMap(navigation: NavigationViews) { - const navMap = {}; + const navMap: NavigationMap = {}; Object.keys(navigation).forEach(key => navigation[key].forEach(node => walkNodes(node))); return navMap; @@ -90,3 +136,9 @@ export class NavigationService { } } } + +function unpluck(obj: any, property: string) { + const result = Object.assign({}, obj); + delete result[property]; + return result; +} diff --git a/aio/src/styles/1-layouts/_sidenav.scss b/aio/src/styles/1-layouts/_sidenav.scss index 7bdb1654d7..26637827b1 100644 --- a/aio/src/styles/1-layouts/_sidenav.scss +++ b/aio/src/styles/1-layouts/_sidenav.scss @@ -162,3 +162,7 @@ .level-1:not(.expanded) .material-icons, .level-2:not(.expanded) .material-icons { @include rotate(0deg); } + +.version-info { + border: 3px $blue solid; +} \ No newline at end of file diff --git a/aio/transforms/angular.io-package/index.js b/aio/transforms/angular.io-package/index.js index da97986215..757ebbfe36 100644 --- a/aio/transforms/angular.io-package/index.js +++ b/aio/transforms/angular.io-package/index.js @@ -50,10 +50,13 @@ module.exports = .processor(require('./processors/filterPrivateDocs')) .processor(require('./processors/filterIgnoredDocs')) .processor(require('./processors/fixInternalDocumentLinks')) + .processor(require('./processors/processNavigationMap')) // overrides base packageInfo and returns the one for the 'angular/angular' repo. .factory('packageInfo', function() { return require(path.resolve(PROJECT_ROOT, 'package.json')); }) + .factory(require('./readers/navigation')) + .config(function(checkAnchorLinksProcessor, log) { // TODO: re-enable checkAnchorLinksProcessor.$enabled = false; @@ -61,12 +64,13 @@ module.exports = // Where do we get the source files? .config(function( - readTypeScriptModules, readFilesProcessor, collectExamples, generateKeywordsProcessor) { + readTypeScriptModules, readFilesProcessor, collectExamples, generateKeywordsProcessor, navigationFileReader) { // API files are typescript readTypeScriptModules.basePath = API_SOURCE_PATH; readTypeScriptModules.ignoreExportsMatching = [/^_/]; readTypeScriptModules.hidePrivateMembers = true; + readFilesProcessor.fileReaders.push(navigationFileReader) readTypeScriptModules.sourceFiles = [ 'common/index.ts', 'common/testing/index.ts', @@ -117,6 +121,11 @@ module.exports = include: CONTENTS_PATH + '/examples/**/*', fileReader: 'exampleFileReader' }, + { + basePath: CONTENTS_PATH, + include: CONTENTS_PATH + '/navigation.json', + fileReader: 'navigationFileReader' + }, ]; collectExamples.exampleFolders = ['examples', 'examples']; @@ -242,7 +251,8 @@ module.exports = outputPathTemplate: '${path}' }, {docTypes: ['example-region'], getOutputPath: function() {}}, - {docTypes: ['content'], pathTemplate: '${id}', outputPathTemplate: '${path}.json'} + {docTypes: ['content'], pathTemplate: '${id}', outputPathTemplate: '${path}.json'}, + {docTypes: ['navigation-map'], pathTemplate: '${id}', outputPathTemplate: '../${id}.json'} ]; }) diff --git a/aio/transforms/angular.io-package/processors/processNavigationMap.js b/aio/transforms/angular.io-package/processors/processNavigationMap.js new file mode 100644 index 0000000000..e625f9d683 --- /dev/null +++ b/aio/transforms/angular.io-package/processors/processNavigationMap.js @@ -0,0 +1,50 @@ +module.exports = function processNavigationMap(versionInfo, log) { + return { + $runAfter: ['paths-computed'], + $runBefore: ['rendering-docs'], + $process: function(docs) { + + const navigationDoc = docs.find(doc => doc.docType === 'navigation-map'); + + if (!navigationDoc) { + throw new Error( + 'Missing navigation map document (docType="navigation-map").' + + 'Did you forget to add it to the readFileProcessor?'); + } + + // Verify that all the navigation paths are to valid docs + const pathMap = {}; + docs.forEach(doc => pathMap[doc.path] = true); + const errors = walk(navigationDoc.data, pathMap, []); + + if (errors.length) { + log.error(`Navigation doc: ${navigationDoc.fileInfo.relativePath} contains invalid urls`); + console.log(errors); + // TODO(petebd): fail if there are errors: throw new Error('processNavigationMap failed'); + } + + // Add in the version data in a "secret" field to be extracted in the docs app + navigationDoc.data['__versionInfo'] = versionInfo.currentVersion; + } + } +}; + +function walk(node, map, path) { + let errors = []; + for(const key in node) { + const child = node[key]; + if (key === 'url') { + const url = child.replace(/#.*$/, ''); // strip hash + if (isRelative(url) && !map[url]) { + errors.push({ path: path.join('.'), url }); + } + } else if (typeof child !== 'string') { + errors = errors.concat(walk(child, map, path.concat([key]))); + } + } + return errors; +} + +function isRelative(url) { + return !/^(https?:)?\/\//.test(url); +} \ No newline at end of file diff --git a/aio/transforms/angular.io-package/readers/navigation.js b/aio/transforms/angular.io-package/readers/navigation.js new file mode 100644 index 0000000000..43a82f19bb --- /dev/null +++ b/aio/transforms/angular.io-package/readers/navigation.js @@ -0,0 +1,19 @@ +/** + * Read in the navigation JSON + */ +module.exports = function navigationFileReader() { + return { + name: 'navigationFileReader', + getDocs: function(fileInfo) { + + // We return a single element array because content files only contain one document + return [{ + docType: 'navigation-map', + data: JSON.parse(fileInfo.content), + template: 'json-doc.template.json', + id: 'navigation', + aliases: ['navigation', 'navigation.json'] + }]; + } + }; +}; \ No newline at end of file