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.
This commit is contained in:
parent
a0c6d44e18
commit
4e10faf1eb
|
@ -8,5 +8,5 @@ yarn-error.log
|
|||
# Ignore generated content
|
||||
/dist
|
||||
/tmp
|
||||
/src/content/docs
|
||||
/src/content
|
||||
/.sass-cache
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<md-sidenav-container class="sidenav-container">
|
||||
|
||||
<md-sidenav #sidenav class="sidenav" [opened]="isSideBySide" [mode] = "isSideBySide ? 'side' : 'over'">
|
||||
<div class="version-info vertical-menu-item">{{ (versionInfo | async)?.full }}</div>
|
||||
<aio-top-menu *ngIf="!isSideBySide" class="small" [nodes]="(navigationViews | async)?.TopBar" [homeImageUrl]="homeImageUrl"></aio-top-menu>
|
||||
<aio-nav-menu [nodes]="(navigationViews | async)?.SideNav" [selectedNodes]="selectedNodes | async"></aio-nav-menu>
|
||||
</md-sidenav>
|
||||
|
@ -16,5 +17,4 @@
|
|||
<aio-search-results #searchResults></aio-search-results>
|
||||
<aio-doc-viewer [doc]="currentDocument | async" (docRendered)="onDocRendered($event)"></aio-doc-viewer>
|
||||
</section>
|
||||
|
||||
</md-sidenav-container>
|
|
@ -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<DocumentContents>;
|
||||
navigationViews: Observable<NavigationViews>;
|
||||
selectedNodes: Observable<NavigationNode[]>;
|
||||
versionInfo: Observable<VersionInfo>;
|
||||
|
||||
@ViewChildren('searchBox, searchResults', { read: ElementRef })
|
||||
searchElements: QueryList<ElementRef>;
|
||||
|
@ -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() {
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<NavigationViews>;
|
||||
|
||||
/**
|
||||
* The current version of doc-app that we are running
|
||||
*/
|
||||
versionInfo: Observable<VersionInfo>;
|
||||
|
||||
/**
|
||||
* An observable array of nodes that indicate which nodes in the `navigationViews` match the current URL location
|
||||
*/
|
||||
selectedNodes = this.getSelectedNodes();
|
||||
selectedNodes: Observable<NavigationNode[]>;
|
||||
|
||||
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<NavigationViews> {
|
||||
const navigationViews = this.http.get(navigationPath)
|
||||
.map(res => res.json() as NavigationViews)
|
||||
private fetchNavigationInfo(): Observable<NavigationResponse> {
|
||||
const navigationInfo = this.http.get(navigationPath)
|
||||
.map(res => res.json() as NavigationResponse)
|
||||
.publishLast();
|
||||
navigationInfo.connect();
|
||||
return navigationInfo;
|
||||
}
|
||||
|
||||
private getVersionInfo(navigationInfo: Observable<NavigationResponse>) {
|
||||
const versionInfo = navigationInfo.map(response => response['__versionInfo']).publishReplay(1);
|
||||
versionInfo.connect();
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
private getNavigationViews(navigationInfo: Observable<NavigationResponse>) {
|
||||
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<NavigationViews>) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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'}
|
||||
];
|
||||
})
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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']
|
||||
}];
|
||||
}
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue