build(docs-infra): associate `providedIn` injectables with their NgModule (#41960)
Such injectables were not appearing in the providers lists of their NgModule. This commit updates the doc-gen to support associating these automatically. Further, it also allows developers to mark other injectables that are provided in an NgModule with a reference to the NgModule where they are provided. The commit also does a refactoring of the `processNgModuleDocs` dgeni processor code, to make it easier to maintain. Fixes #41203 PR Close #41960
This commit is contained in:
parent
23f6b76d1a
commit
85f5cb45d2
|
@ -5,77 +5,222 @@ module.exports = function processNgModuleDocs(getDocFromAlias, createDocMessage,
|
||||||
exportDocTypes: ['directive', 'pipe'],
|
exportDocTypes: ['directive', 'pipe'],
|
||||||
skipAbstractDirectives: true,
|
skipAbstractDirectives: true,
|
||||||
$process(docs) {
|
$process(docs) {
|
||||||
// Match all the directives/pipes to their module
|
|
||||||
const errors = [];
|
const errors = [];
|
||||||
docs.forEach(doc => {
|
|
||||||
|
for (const doc of docs) {
|
||||||
if (this.exportDocTypes.indexOf(doc.docType) !== -1) {
|
if (this.exportDocTypes.indexOf(doc.docType) !== -1) {
|
||||||
const options = doc[`${doc.docType}Options`];
|
// Match all the directives/pipes to their module
|
||||||
|
this.processNgModuleExportDoc(doc, errors);
|
||||||
// Directives without a selector are considered abstract and do
|
|
||||||
// not need to be part of any `@NgModule`.
|
|
||||||
if (this.skipAbstractDirectives && doc.docType === 'directive' && !options.selector) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doc.ngModules || doc.ngModules.length === 0) {
|
|
||||||
errors.push(createDocMessage(`"${doc.id}" has no @ngModule tag. Docs of type "${doc.docType}" must have this tag.`, doc));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.ngModules.forEach((ngModule, index) => {
|
|
||||||
|
|
||||||
const ngModuleDocs = getDocFromAlias(ngModule, doc);
|
|
||||||
|
|
||||||
if (ngModuleDocs.length === 0) {
|
|
||||||
errors.push(createDocMessage(`"@ngModule ${ngModule}" does not match a public NgModule`, doc));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ngModuleDocs.length > 1) {
|
|
||||||
errors.push(createDocMessage(`"@ngModule ${ngModule}" is ambiguous. Matches: ${ngModuleDocs.map(d => d.id).join(', ')}`, doc));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ngModuleDoc = ngModuleDocs[0];
|
|
||||||
const containerName = getContainerName(doc.docType);
|
|
||||||
const container = ngModuleDoc[containerName] = ngModuleDoc[containerName] || [];
|
|
||||||
container.push(doc);
|
|
||||||
|
|
||||||
doc.ngModules[index] = ngModuleDoc;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (doc.docType === 'class') {
|
||||||
|
this.processInjectableDoc(doc, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
errors.forEach(error => log.error(error));
|
errors.forEach(error => log.error(error));
|
||||||
throw new Error('Failed to process NgModule relationships.');
|
throw new Error('Failed to process NgModule relationships.');
|
||||||
}
|
}
|
||||||
|
|
||||||
docs.forEach(doc => {
|
// Update the NgModule docs after we have associated the directives/pipes/injectables docs.
|
||||||
|
for (const doc of docs) {
|
||||||
if (doc.docType === 'ngmodule') {
|
if (doc.docType === 'ngmodule') {
|
||||||
Object.keys(doc.ngmoduleOptions).forEach(key => {
|
convertAllPropertiesToArrays(doc.ngmoduleOptions);
|
||||||
const value = doc.ngmoduleOptions[key];
|
this.sortExportDocContainers(doc);
|
||||||
if (value && !Array.isArray(value)) {
|
this.processNgModuleProviders(doc);
|
||||||
doc.ngmoduleOptions[key] = [value];
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
this.exportDocTypes.forEach(type => {
|
|
||||||
const containerName = getContainerName(type);
|
/**
|
||||||
const container = doc[containerName];
|
* Associate the `exportDoc` that is expected to have been exported from an `NgModule` with its
|
||||||
if (container) {
|
* `NgModule` doc.
|
||||||
container.sort(byId);
|
*/
|
||||||
}
|
processNgModuleExportDoc(exportDoc, errors) {
|
||||||
});
|
const options = exportDoc[`${exportDoc.docType}Options`];
|
||||||
|
|
||||||
|
// Directives without a selector are considered abstract and do not need to be part of
|
||||||
|
// any `@NgModule`.
|
||||||
|
if (this.skipAbstractDirectives && exportDoc.docType === 'directive' && !options.selector) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exportDoc.ngModules || exportDoc.ngModules.length === 0) {
|
||||||
|
errors.push(createDocMessage(
|
||||||
|
`"${exportDoc.id}" has no @ngModule tag. Docs of type "${exportDoc.docType}" must have this tag.`,
|
||||||
|
exportDoc));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportDoc.ngModules.forEach((ngModule, index) => {
|
||||||
|
const ngModuleDoc = getNgModule(ngModule, exportDoc, errors);
|
||||||
|
if (ngModuleDoc !== null) {
|
||||||
|
const containerName = getContainerName(exportDoc.docType);
|
||||||
|
const container = ngModuleDoc[containerName] = ngModuleDoc[containerName] || [];
|
||||||
|
container.push(exportDoc);
|
||||||
|
exportDoc.ngModules[index] = ngModuleDoc;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associate the given `injectableDoc` with an NgModule doc if it is provided on an `NgModule`.
|
||||||
|
*/
|
||||||
|
processInjectableDoc(injectableDoc, errors) {
|
||||||
|
const ngModules = [];
|
||||||
|
|
||||||
|
if (Array.isArray(injectableDoc.ngModules)) {
|
||||||
|
for (const ngModule of injectableDoc.ngModules) {
|
||||||
|
if (isWrappedInQuotes(ngModule)) {
|
||||||
|
// `ngModule` is wrapped in quotes, so it will be one of `'any'`, `'root'` or `'platform'`
|
||||||
|
// and is not associated with a specific NgModule. So just use the string.
|
||||||
|
ngModules.push(ngModule.slice(1, -1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Convert any `@ngModule` JSDOC tags to actual NgModule docs.
|
||||||
|
// Don't add this doc to the NgModule doc, since this should already be in the `providers`
|
||||||
|
// property of the `@NgModule()` decorator.
|
||||||
|
const ngModuleDoc = getNgModule(ngModule, injectableDoc, errors);
|
||||||
|
if (ngModuleDoc !== null) {
|
||||||
|
ngModules.push(ngModuleDoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for `providedIn` property on `@Injectable()`.
|
||||||
|
for (const decorator of injectableDoc.decorators || []) {
|
||||||
|
if (decorator.name === 'Injectable' && decorator.argumentInfo[0]) {
|
||||||
|
const providedIn = decorator.argumentInfo[0].providedIn;
|
||||||
|
this.processProvidedIn(providedIn, injectableDoc, ngModules, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for `providedIn` property on an `ɵprov` static property
|
||||||
|
if (injectableDoc.symbol?.exports.has('ɵprov')) {
|
||||||
|
const declaration = injectableDoc.symbol?.exports.get('ɵprov')?.valueDeclaration;
|
||||||
|
const properties = declaration?.initializer?.arguments?.[0]?.properties;
|
||||||
|
const providedInProp = properties?.find(prop => prop.name.text === 'providedIn');
|
||||||
|
const providedInNode = providedInProp?.initializer;
|
||||||
|
if (providedInNode) {
|
||||||
|
const providedIn = providedInNode.getSourceFile().text.slice(providedInNode.pos, providedInNode.end).trim();
|
||||||
|
this.processProvidedIn(providedIn, injectableDoc, ngModules, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ngModules.length > 0) {
|
||||||
|
injectableDoc.ngModules = ngModules;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
processProvidedIn(providedIn, injectableDoc, ngModules, errors) {
|
||||||
|
if (typeof providedIn !== 'string') {
|
||||||
|
// `providedIn` is not a string, which means that this is not a tree-shakable provider
|
||||||
|
// that needs associating with an NgModule.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isWrappedInQuotes(providedIn)) {
|
||||||
|
// `providedIn` is wrapped in quotes, so it will be one of `'root'` or `'platform'` and
|
||||||
|
// is not associated with a specific NgModule. So just use the string.
|
||||||
|
ngModules.push(providedIn.slice(1, -1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `providedIn` ought to reference a public NgModule
|
||||||
|
const ngModuleDoc = getNgModule(providedIn, injectableDoc, errors);
|
||||||
|
if (ngModuleDoc === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = ngModuleDoc.providers = ngModuleDoc.providers || [];
|
||||||
|
container.push(injectableDoc);
|
||||||
|
ngModules.push(ngModuleDoc);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the arrays containing the docs exported from the `ngModuleDoc` are sorted.
|
||||||
|
*/
|
||||||
|
sortExportDocContainers(ngModuleDoc) {
|
||||||
|
for (const type of this.exportDocTypes) {
|
||||||
|
const container = ngModuleDoc[getContainerName(type)];
|
||||||
|
if (Array.isArray(container)) {
|
||||||
|
container.sort(byId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the providers of the `ngModuleDoc`.
|
||||||
|
*
|
||||||
|
* Some providers come from the `@NgModule({providers: ...})` decoration.
|
||||||
|
* Other providers come from the `@Injectable({providedIn: ...})` decoration.
|
||||||
|
*/
|
||||||
|
processNgModuleProviders(ngModuleDoc) {
|
||||||
|
// Add providers from the NgModule decorator.
|
||||||
|
const providers = ngModuleDoc.ngmoduleOptions.providers || [];
|
||||||
|
// And also add those associated via the `Injectable` `providedIn` property.
|
||||||
|
if (Array.isArray(ngModuleDoc.providers)) {
|
||||||
|
for (const provider of ngModuleDoc.providers) {
|
||||||
|
providers.push(`{ provide: ${provider.name}, useClass: ${provider.name} }`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providers.length > 0) {
|
||||||
|
ngModuleDoc.providers = providers;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getNgModule(ngModuleId, doc, errors) {
|
||||||
|
const ngModuleDocs = getDocFromAlias(ngModuleId, doc);
|
||||||
|
|
||||||
|
if (ngModuleDocs.length === 0) {
|
||||||
|
errors.push(
|
||||||
|
createDocMessage(`The referenced "${ngModuleId}" does not match a public NgModule`, doc));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ngModuleDocs.length > 1) {
|
||||||
|
errors.push(createDocMessage(
|
||||||
|
`The referenced "${ngModuleId}" is ambiguous. Matches: ${ngModuleDocs.map(d => d.id).join(', ')}`,
|
||||||
|
doc));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ngModuleDocs[0];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the name of the array that will hold items of this type in the NgModule document.
|
||||||
|
*/
|
||||||
function getContainerName(docType) {
|
function getContainerName(docType) {
|
||||||
return docType + 's';
|
return docType + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparison function for sorting docs associated with an NgModule.
|
||||||
|
*
|
||||||
|
* This is used to sort docs by their id.
|
||||||
|
*/
|
||||||
function byId(a, b) {
|
function byId(a, b) {
|
||||||
return a.id > b.id ? 1 : -1;
|
return a.id > b.id ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert all the values of properties on the `obj` to arrays, if not already.
|
||||||
|
*/
|
||||||
|
function convertAllPropertiesToArrays(obj) {
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
obj[key] = [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the `str` is wrapped in single or double quotes.
|
||||||
|
*/
|
||||||
|
function isWrappedInQuotes(str) {
|
||||||
|
return /^['"].+['"]$/.test(str);
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe('processNgModuleDocs processor', () => {
|
||||||
expect(processor.$runAfter).toEqual(['extractDecoratedClassesProcessor', 'computeIdsProcessor']);
|
expect(processor.$runAfter).toEqual(['extractDecoratedClassesProcessor', 'computeIdsProcessor']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should non-arrayNgModule options to arrays', () => {
|
it('should convert non-array NgModule options to arrays', () => {
|
||||||
const docs = [{
|
const docs = [{
|
||||||
docType: 'ngmodule',
|
docType: 'ngmodule',
|
||||||
ngmoduleOptions: {
|
ngmoduleOptions: {
|
||||||
|
@ -39,15 +39,15 @@ describe('processNgModuleDocs processor', () => {
|
||||||
|
|
||||||
it('should link directive/pipe docs with their NgModule docs (sorted by id)', () => {
|
it('should link directive/pipe docs with their NgModule docs (sorted by id)', () => {
|
||||||
const aliasMap = injector.get('aliasMap');
|
const aliasMap = injector.get('aliasMap');
|
||||||
const directiveOptions = {selector: 'some-selector'};
|
const directiveOptions = { selector: 'some-selector' };
|
||||||
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {}};
|
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {} };
|
||||||
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {}};
|
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {} };
|
||||||
const directive1 = { docType: 'directive', id: 'Directive1', ngModules: ['NgModule1'], directiveOptions};
|
const directive1 = { docType: 'directive', id: 'Directive1', ngModules: ['NgModule1'], directiveOptions };
|
||||||
const directive2 = { docType: 'directive', id: 'Directive2', ngModules: ['NgModule2'], directiveOptions};
|
const directive2 = { docType: 'directive', id: 'Directive2', ngModules: ['NgModule2'], directiveOptions };
|
||||||
const directive3 = { docType: 'directive', id: 'Directive3', ngModules: ['NgModule1', 'NgModule2'], directiveOptions};
|
const directive3 = { docType: 'directive', id: 'Directive3', ngModules: ['NgModule1', 'NgModule2'], directiveOptions };
|
||||||
const pipe1 = { docType: 'pipe', id: 'Pipe1', ngModules: ['NgModule1']};
|
const pipe1 = { docType: 'pipe', id: 'Pipe1', ngModules: ['NgModule1'] };
|
||||||
const pipe2 = { docType: 'pipe', id: 'Pipe2', ngModules: ['NgModule2']};
|
const pipe2 = { docType: 'pipe', id: 'Pipe2', ngModules: ['NgModule2'] };
|
||||||
const pipe3 = { docType: 'pipe', id: 'Pipe3', ngModules: ['NgModule1', 'NgModule2']};
|
const pipe3 = { docType: 'pipe', id: 'Pipe3', ngModules: ['NgModule1', 'NgModule2'] };
|
||||||
|
|
||||||
aliasMap.addDoc(ngModule1);
|
aliasMap.addDoc(ngModule1);
|
||||||
aliasMap.addDoc(ngModule2);
|
aliasMap.addDoc(ngModule2);
|
||||||
|
@ -67,22 +67,138 @@ describe('processNgModuleDocs processor', () => {
|
||||||
expect(pipe3.ngModules).toEqual([ngModule1, ngModule2]);
|
expect(pipe3.ngModules).toEqual([ngModule1, ngModule2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not error if an abstract directove does not have a `@ngModule` tag', () => {
|
it('should link classes that have a `providedIn` property on an @Injectable decorator that references a known NgModule doc', () => {
|
||||||
|
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {} };
|
||||||
|
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {} };
|
||||||
|
const injectable1 = { docType: 'class', name: 'Injectable1', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: '\'root\'' }] }] };
|
||||||
|
const injectable2 = { docType: 'class', name: 'Injectable2', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: '\'platform\'' }] }] };
|
||||||
|
const injectable3 = { docType: 'class', name: 'Injectable3', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: '"root"' }] }] };
|
||||||
|
const injectable4 = { docType: 'class', name: 'Injectable4', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: '"platform"' }] }] };
|
||||||
|
const injectable5 = { docType: 'class', name: 'Injectable5', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: 'NgModule1' }] }] };
|
||||||
|
const injectable6 = { docType: 'class', name: 'Injectable6', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: 'NgModule2' }] }] };
|
||||||
|
const injectable7 = { docType: 'class', name: 'Injectable7' };
|
||||||
|
const nonInjectable = { docType: 'class', name: 'nonInjectable' };
|
||||||
|
|
||||||
|
const aliasMap = injector.get('aliasMap');
|
||||||
|
aliasMap.addDoc(ngModule1);
|
||||||
|
aliasMap.addDoc(ngModule2);
|
||||||
|
processor.$process([ngModule1, ngModule2, injectable1, injectable2, injectable3, injectable4, injectable5, injectable6, injectable7, nonInjectable]);
|
||||||
|
|
||||||
|
expect(ngModule1.providers).toEqual(['{ provide: Injectable5, useClass: Injectable5 }']);
|
||||||
|
expect(ngModule2.providers).toEqual(['{ provide: Injectable6, useClass: Injectable6 }']);
|
||||||
|
|
||||||
|
expect(injectable1.ngModules).toEqual(['root']);
|
||||||
|
expect(injectable2.ngModules).toEqual(['platform']);
|
||||||
|
expect(injectable3.ngModules).toEqual(['root']);
|
||||||
|
expect(injectable4.ngModules).toEqual(['platform']);
|
||||||
|
expect(injectable5.ngModules).toEqual([ngModule1]);
|
||||||
|
expect(injectable6.ngModules).toEqual([ngModule2]);
|
||||||
|
expect(injectable7.ngModules).toBeUndefined();
|
||||||
|
expect(nonInjectable.ngModules).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should link classes that have a `providedIn` property on a ɵprov static that references a known NgModule doc', () => {
|
||||||
|
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {} };
|
||||||
|
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {} };
|
||||||
|
const injectable1 = { docType: 'class', name: 'Injectable1', symbol: createSymbolWithProvider('\'root\'') };
|
||||||
|
const injectable2 = { docType: 'class', name: 'Injectable2', symbol: createSymbolWithProvider('\'platform\'') };
|
||||||
|
const injectable3 = { docType: 'class', name: 'Injectable3', symbol: createSymbolWithProvider('"root"') };
|
||||||
|
const injectable4 = { docType: 'class', name: 'Injectable4', symbol: createSymbolWithProvider('"platform"') };
|
||||||
|
const injectable5 = { docType: 'class', name: 'Injectable5', symbol: createSymbolWithProvider('NgModule1') };
|
||||||
|
const injectable6 = { docType: 'class', name: 'Injectable6', symbol: createSymbolWithProvider('NgModule2') };
|
||||||
|
const injectable7 = { docType: 'class', name: 'Injectable7' };
|
||||||
|
const nonInjectable = { docType: 'class', name: 'nonInjectable' };
|
||||||
|
|
||||||
|
const aliasMap = injector.get('aliasMap');
|
||||||
|
aliasMap.addDoc(ngModule1);
|
||||||
|
aliasMap.addDoc(ngModule2);
|
||||||
|
processor.$process([ngModule1, ngModule2, injectable1, injectable2, injectable3, injectable4, injectable5, injectable6, injectable7, nonInjectable]);
|
||||||
|
|
||||||
|
expect(ngModule1.providers).toEqual(['{ provide: Injectable5, useClass: Injectable5 }']);
|
||||||
|
expect(ngModule2.providers).toEqual(['{ provide: Injectable6, useClass: Injectable6 }']);
|
||||||
|
|
||||||
|
expect(injectable1.ngModules).toEqual(['root']);
|
||||||
|
expect(injectable2.ngModules).toEqual(['platform']);
|
||||||
|
expect(injectable3.ngModules).toEqual(['root']);
|
||||||
|
expect(injectable4.ngModules).toEqual(['platform']);
|
||||||
|
expect(injectable5.ngModules).toEqual([ngModule1]);
|
||||||
|
expect(injectable6.ngModules).toEqual([ngModule2]);
|
||||||
|
expect(injectable7.ngModules).toBeUndefined();
|
||||||
|
expect(nonInjectable.ngModules).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should link injectables that are marked with `@ngModule` JSDOC tags', () => {
|
||||||
|
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {} };
|
||||||
|
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: { providers: ['PROVIDER'] } };
|
||||||
|
const injectable1 = { docType: 'class', name: 'Injectable1', ngModules: ['NgModule1'] };
|
||||||
|
const injectable2 = { docType: 'class', name: 'Injectable2', ngModules: ['NgModule2'] };
|
||||||
|
const injectable3 = { docType: 'class', name: 'Injectable3' };
|
||||||
|
const nonInjectable = { docType: 'class', name: 'nonInjectable' };
|
||||||
|
|
||||||
|
const aliasMap = injector.get('aliasMap');
|
||||||
|
aliasMap.addDoc(ngModule1);
|
||||||
|
aliasMap.addDoc(ngModule2);
|
||||||
|
processor.$process([ngModule1, ngModule2, injectable1, injectable2, injectable3, nonInjectable]);
|
||||||
|
|
||||||
|
// Should not update the NgModule docs in this case.
|
||||||
|
expect(ngModule1.providers).toBeUndefined();
|
||||||
|
expect(ngModule2.providers).toEqual(['PROVIDER']);
|
||||||
|
|
||||||
|
expect(injectable1.ngModules).toEqual([ngModule1]);
|
||||||
|
expect(injectable2.ngModules).toEqual([ngModule2]);
|
||||||
|
expect(injectable3.ngModules).toBeUndefined();
|
||||||
|
expect(nonInjectable.ngModules).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if an injectable that has a `providedIn` property that references an unknown NgModule doc', () => {
|
||||||
|
const log = injector.get('log');
|
||||||
|
const injectable = { docType: 'class', name: 'Injectable1', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: 'NgModuleRef' }] }] };
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
processor.$process([injectable]);
|
||||||
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
|
'The referenced "NgModuleRef" does not match a public NgModule - doc "Injectable1" (class) ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if an injectable that has a `providedIn` property that references an ambiguous NgModule doc', () => {
|
||||||
|
const log = injector.get('log');
|
||||||
|
|
||||||
|
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModuleRef'], ngmoduleOptions: {} };
|
||||||
|
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModuleRef'], ngmoduleOptions: {} };
|
||||||
|
const injectable = { docType: 'class', name: 'Injectable1', decorators: [{ name: 'Injectable', argumentInfo: [{ providedIn: 'NgModuleRef' }] }] };
|
||||||
|
|
||||||
|
const aliasMap = injector.get('aliasMap');
|
||||||
|
aliasMap.addDoc(ngModule1);
|
||||||
|
aliasMap.addDoc(ngModule2);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
processor.$process([injectable]);
|
||||||
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
|
'The referenced "NgModuleRef" is ambiguous. Matches: NgModule1, NgModule2 - doc "Injectable1" (class) ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not error if an abstract directive does not have a `@ngModule` tag', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{ docType: 'directive', id: 'AbstractDir', directiveOptions: {} }]);
|
processor.$process([{ docType: 'directive', id: 'AbstractDir', directiveOptions: {} }]);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{ docType: 'directive', id: 'AbstractDir',
|
processor.$process([{
|
||||||
directiveOptions: {selector: undefined} }]);
|
docType: 'directive', id: 'AbstractDir',
|
||||||
|
directiveOptions: { selector: undefined }
|
||||||
|
}]);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if a pipe/directive does not have a `@ngModule` tag', () => {
|
it('should error if a pipe/directive does not have a `@ngModule` tag', () => {
|
||||||
const log = injector.get('log');
|
const log = injector.get('log');
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{ docType: 'directive', id: 'Directive1',
|
processor.$process([{
|
||||||
directiveOptions: {selector: 'dir1'} }]);
|
docType: 'directive', id: 'Directive1',
|
||||||
|
directiveOptions: { selector: 'dir1' }
|
||||||
|
}]);
|
||||||
}).toThrowError('Failed to process NgModule relationships.');
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
expect(log.error).toHaveBeenCalledWith(
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
'"Directive1" has no @ngModule tag. Docs of type "directive" must have this tag. - doc "Directive1" (directive) ');
|
'"Directive1" has no @ngModule tag. Docs of type "directive" must have this tag. - doc "Directive1" (directive) ');
|
||||||
|
@ -97,39 +213,66 @@ describe('processNgModuleDocs processor', () => {
|
||||||
it('should error if a pipe/directive has an @ngModule tag that does not match an NgModule doc', () => {
|
it('should error if a pipe/directive has an @ngModule tag that does not match an NgModule doc', () => {
|
||||||
const log = injector.get('log');
|
const log = injector.get('log');
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{ docType: 'directive', id: 'Directive1', ngModules: ['MissingNgModule'],
|
processor.$process([{
|
||||||
directiveOptions: {selector: 'dir1'} }]);
|
docType: 'directive', id: 'Directive1', ngModules: ['MissingNgModule'],
|
||||||
|
directiveOptions: { selector: 'dir1' }
|
||||||
|
}]);
|
||||||
}).toThrowError('Failed to process NgModule relationships.');
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
expect(log.error).toHaveBeenCalledWith(
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
'"@ngModule MissingNgModule" does not match a public NgModule - doc "Directive1" (directive) ');
|
'The referenced "MissingNgModule" does not match a public NgModule - doc "Directive1" (directive) ');
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['MissingNgModule'] }]);
|
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['MissingNgModule'] }]);
|
||||||
}).toThrowError('Failed to process NgModule relationships.');
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
expect(log.error).toHaveBeenCalledWith(
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
'"@ngModule MissingNgModule" does not match a public NgModule - doc "Pipe1" (pipe) ');
|
'The referenced "MissingNgModule" does not match a public NgModule - doc "Pipe1" (pipe) ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if a pipe/directive has an @ngModule tag that matches more than one NgModule doc', () => {
|
it('should error if a pipe/directive has an @ngModule tag that matches more than one NgModule doc', () => {
|
||||||
const aliasMap = injector.get('aliasMap');
|
const aliasMap = injector.get('aliasMap');
|
||||||
const log = injector.get('log');
|
const log = injector.get('log');
|
||||||
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModuleAlias'], ngmoduleOptions: {}};
|
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModuleAlias'], ngmoduleOptions: {} };
|
||||||
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModuleAlias'], ngmoduleOptions: {}};
|
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModuleAlias'], ngmoduleOptions: {} };
|
||||||
aliasMap.addDoc(ngModule1);
|
aliasMap.addDoc(ngModule1);
|
||||||
aliasMap.addDoc(ngModule2);
|
aliasMap.addDoc(ngModule2);
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{
|
processor.$process([{
|
||||||
docType: 'directive', id: 'Directive1', ngModules: ['NgModuleAlias'],
|
docType: 'directive', id: 'Directive1', ngModules: ['NgModuleAlias'],
|
||||||
directiveOptions: {selector: 'dir1'} }]);
|
directiveOptions: { selector: 'dir1' }
|
||||||
|
}]);
|
||||||
}).toThrowError('Failed to process NgModule relationships.');
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
expect(log.error).toHaveBeenCalledWith(
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
'"@ngModule NgModuleAlias" is ambiguous. Matches: NgModule1, NgModule2 - doc "Directive1" (directive) ');
|
'The referenced "NgModuleAlias" is ambiguous. Matches: NgModule1, NgModule2 - doc "Directive1" (directive) ');
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['NgModuleAlias'] }]);
|
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['NgModuleAlias'] }]);
|
||||||
}).toThrowError('Failed to process NgModule relationships.');
|
}).toThrowError('Failed to process NgModule relationships.');
|
||||||
expect(log.error).toHaveBeenCalledWith(
|
expect(log.error).toHaveBeenCalledWith(
|
||||||
'"@ngModule NgModuleAlias" is ambiguous. Matches: NgModule1, NgModule2 - doc "Pipe1" (pipe) ');
|
'The referenced "NgModuleAlias" is ambiguous. Matches: NgModule1, NgModule2 - doc "Pipe1" (pipe) ');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function simulates a TS AST node for the code:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* static ɵprov = ɵɵdefineInjectable({
|
||||||
|
* providedIn: 'xxxx',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function createSymbolWithProvider(providedIn) {
|
||||||
|
const initializer = {
|
||||||
|
pos: 0,
|
||||||
|
end: providedIn.length,
|
||||||
|
getSourceFile() {
|
||||||
|
return { text: providedIn };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const valueDeclaration = { initializer: { arguments: [{ properties: [ { name: { text: 'providedIn' }, initializer } ] } ] } };
|
||||||
|
const exportMap = new Map();
|
||||||
|
exportMap.set('ɵprov', {valueDeclaration});
|
||||||
|
return {exports: exportMap};
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
|
{% import "lib/ngmodule.html" as ngModuleHelpers -%}
|
||||||
{% extends 'export-base.template.html' -%}
|
{% extends 'export-base.template.html' -%}
|
||||||
|
|
||||||
{% block overview %}
|
{% block overview %}
|
||||||
{% include "includes/class-overview.html" %}
|
{% include "includes/class-overview.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block details %}
|
{% block details %}
|
||||||
|
{$ ngModuleHelpers.ngModuleList(doc.ngModules, 'Provided in') $}
|
||||||
{% include "includes/description.html" %}
|
{% include "includes/description.html" %}
|
||||||
{% include "includes/class-members.html" %}
|
{% include "includes/class-members.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
{% import "lib/memberHelpers.html" as memberHelpers -%}
|
{% import "lib/memberHelpers.html" as memberHelpers -%}
|
||||||
|
{% import "lib/ngmodule.html" as ngModuleHelpers -%}
|
||||||
{% extends 'class.template.html' -%}
|
{% extends 'class.template.html' -%}
|
||||||
|
|
||||||
{% block overview %}{% endblock %}
|
{% block overview %}{% endblock %}
|
||||||
|
|
||||||
{% block details -%}
|
{% block details -%}
|
||||||
{% include "includes/ngmodule.html" %}
|
{$ ngModuleHelpers.ngModuleList(doc.ngModules, 'Exported from') $}
|
||||||
{% include "includes/selectors.html" %}
|
{% include "includes/selectors.html" %}
|
||||||
|
|
||||||
{$ memberHelpers.renderDirectiveProperties(doc, 'Properties') $}
|
{$ memberHelpers.renderDirectiveProperties(doc, 'Properties') $}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
{% if doc.ngModules.length == 1 %}<h2>NgModule</h2>{% else %}<h2>NgModules</h2>{% endif %}
|
|
||||||
<ul class="ngmodule-list">
|
|
||||||
{% for ngModule in doc.ngModules %}
|
|
||||||
<li>
|
|
||||||
<a href="{$ ngModule.path $}">
|
|
||||||
<code-example language="ts" hideCopy="true" class="no-box">{$ ngModule.name | escape $}</code-example>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% import "lib/memberHelpers.html" as memberHelpers -%}
|
{% import "lib/memberHelpers.html" as memberHelpers -%}
|
||||||
|
{% import "lib/ngmodule.html" as ngModuleHelpers -%}
|
||||||
{% import "lib/paramList.html" as params -%}
|
{% import "lib/paramList.html" as params -%}
|
||||||
|
|
||||||
<section class="{$ doc.docType $}-overview">
|
<section class="{$ doc.docType $}-overview">
|
||||||
|
@ -10,7 +11,7 @@
|
||||||
{%- if param.isOptional or param.defaultValue !== undefined %} ]{% endif %}
|
{%- if param.isOptional or param.defaultValue !== undefined %} ]{% endif %}
|
||||||
{%- endfor %} }}</code-example>
|
{%- endfor %} }}</code-example>
|
||||||
|
|
||||||
{% include "includes/ngmodule.html" %}
|
{$ ngModuleHelpers.ngModuleList(doc.ngModules, 'Exported from') $}
|
||||||
|
|
||||||
{% if doc.valueParam.type %}
|
{% if doc.valueParam.type %}
|
||||||
<h2>Input value</h2>
|
<h2>Input value</h2>
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
{% macro ngModuleList(ngModules, heading) %}
|
||||||
|
{% if ngModules and ngModules.length > 0 %}
|
||||||
|
<h2>{$ heading $}</h2>
|
||||||
|
<ul>
|
||||||
|
{% for ngModule in ngModules %}
|
||||||
|
<li>
|
||||||
|
{% if ngModule.path %}
|
||||||
|
<a href="{$ ngModule.path $}">
|
||||||
|
<code-example language="ts" hideCopy="true" class="no-box">{$ ngModule.name | escape $}</code-example>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<code-example language="ts" hideCopy="true" class="no-box">'{$ ngModule | escape $}'</code-example>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
|
@ -69,8 +69,8 @@
|
||||||
|
|
||||||
{$ memberHelpers.renderMethodDetails(versionInfo, doc.methods, 'instance-methods', 'instance-method', 'Methods') $}
|
{$ memberHelpers.renderMethodDetails(versionInfo, doc.methods, 'instance-methods', 'instance-method', 'Methods') $}
|
||||||
|
|
||||||
{% if doc.ngmoduleOptions.providers %}
|
{% if doc.providers %}
|
||||||
{$ renderTable(doc.ngmoduleOptions.providers, 'providers', 'Providers', 'Provider') $}
|
{$ renderTable(doc.providers, 'providers', 'Providers', 'Provider') $}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if doc.directives.length %}
|
{% if doc.directives.length %}
|
||||||
|
|
Loading…
Reference in New Issue