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:
committed by
Alex Rickabaugh
parent
23f6b76d1a
commit
85f5cb45d2
@@ -22,7 +22,7 @@ describe('processNgModuleDocs processor', () => {
|
||||
expect(processor.$runAfter).toEqual(['extractDecoratedClassesProcessor', 'computeIdsProcessor']);
|
||||
});
|
||||
|
||||
it('should non-arrayNgModule options to arrays', () => {
|
||||
it('should convert non-array NgModule options to arrays', () => {
|
||||
const docs = [{
|
||||
docType: 'ngmodule',
|
||||
ngmoduleOptions: {
|
||||
@@ -39,15 +39,15 @@ describe('processNgModuleDocs processor', () => {
|
||||
|
||||
it('should link directive/pipe docs with their NgModule docs (sorted by id)', () => {
|
||||
const aliasMap = injector.get('aliasMap');
|
||||
const directiveOptions = {selector: 'some-selector'};
|
||||
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {}};
|
||||
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {}};
|
||||
const directive1 = { docType: 'directive', id: 'Directive1', ngModules: ['NgModule1'], directiveOptions};
|
||||
const directive2 = { docType: 'directive', id: 'Directive2', ngModules: ['NgModule2'], directiveOptions};
|
||||
const directive3 = { docType: 'directive', id: 'Directive3', ngModules: ['NgModule1', 'NgModule2'], directiveOptions};
|
||||
const pipe1 = { docType: 'pipe', id: 'Pipe1', ngModules: ['NgModule1']};
|
||||
const pipe2 = { docType: 'pipe', id: 'Pipe2', ngModules: ['NgModule2']};
|
||||
const pipe3 = { docType: 'pipe', id: 'Pipe3', ngModules: ['NgModule1', 'NgModule2']};
|
||||
const directiveOptions = { selector: 'some-selector' };
|
||||
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModule1'], ngmoduleOptions: {} };
|
||||
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModule2'], ngmoduleOptions: {} };
|
||||
const directive1 = { docType: 'directive', id: 'Directive1', ngModules: ['NgModule1'], directiveOptions };
|
||||
const directive2 = { docType: 'directive', id: 'Directive2', ngModules: ['NgModule2'], directiveOptions };
|
||||
const directive3 = { docType: 'directive', id: 'Directive3', ngModules: ['NgModule1', 'NgModule2'], directiveOptions };
|
||||
const pipe1 = { docType: 'pipe', id: 'Pipe1', ngModules: ['NgModule1'] };
|
||||
const pipe2 = { docType: 'pipe', id: 'Pipe2', ngModules: ['NgModule2'] };
|
||||
const pipe3 = { docType: 'pipe', id: 'Pipe3', ngModules: ['NgModule1', 'NgModule2'] };
|
||||
|
||||
aliasMap.addDoc(ngModule1);
|
||||
aliasMap.addDoc(ngModule2);
|
||||
@@ -67,22 +67,138 @@ describe('processNgModuleDocs processor', () => {
|
||||
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(() => {
|
||||
processor.$process([{ docType: 'directive', id: 'AbstractDir', directiveOptions: {} }]);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(() => {
|
||||
processor.$process([{ docType: 'directive', id: 'AbstractDir',
|
||||
directiveOptions: {selector: undefined} }]);
|
||||
processor.$process([{
|
||||
docType: 'directive', id: 'AbstractDir',
|
||||
directiveOptions: { selector: undefined }
|
||||
}]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should error if a pipe/directive does not have a `@ngModule` tag', () => {
|
||||
const log = injector.get('log');
|
||||
expect(() => {
|
||||
processor.$process([{ docType: 'directive', id: 'Directive1',
|
||||
directiveOptions: {selector: 'dir1'} }]);
|
||||
processor.$process([{
|
||||
docType: 'directive', id: 'Directive1',
|
||||
directiveOptions: { selector: 'dir1' }
|
||||
}]);
|
||||
}).toThrowError('Failed to process NgModule relationships.');
|
||||
expect(log.error).toHaveBeenCalledWith(
|
||||
'"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', () => {
|
||||
const log = injector.get('log');
|
||||
expect(() => {
|
||||
processor.$process([{ docType: 'directive', id: 'Directive1', ngModules: ['MissingNgModule'],
|
||||
directiveOptions: {selector: 'dir1'} }]);
|
||||
processor.$process([{
|
||||
docType: 'directive', id: 'Directive1', ngModules: ['MissingNgModule'],
|
||||
directiveOptions: { selector: 'dir1' }
|
||||
}]);
|
||||
}).toThrowError('Failed to process NgModule relationships.');
|
||||
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(() => {
|
||||
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['MissingNgModule'] }]);
|
||||
}).toThrowError('Failed to process NgModule relationships.');
|
||||
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', () => {
|
||||
const aliasMap = injector.get('aliasMap');
|
||||
const log = injector.get('log');
|
||||
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModuleAlias'], ngmoduleOptions: {}};
|
||||
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModuleAlias'], ngmoduleOptions: {}};
|
||||
const ngModule1 = { docType: 'ngmodule', id: 'NgModule1', aliases: ['NgModuleAlias'], ngmoduleOptions: {} };
|
||||
const ngModule2 = { docType: 'ngmodule', id: 'NgModule2', aliases: ['NgModuleAlias'], ngmoduleOptions: {} };
|
||||
aliasMap.addDoc(ngModule1);
|
||||
aliasMap.addDoc(ngModule2);
|
||||
|
||||
expect(() => {
|
||||
processor.$process([{
|
||||
docType: 'directive', id: 'Directive1', ngModules: ['NgModuleAlias'],
|
||||
directiveOptions: {selector: 'dir1'} }]);
|
||||
directiveOptions: { selector: 'dir1' }
|
||||
}]);
|
||||
}).toThrowError('Failed to process NgModule relationships.');
|
||||
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(() => {
|
||||
processor.$process([{ docType: 'pipe', id: 'Pipe1', ngModules: ['NgModuleAlias'] }]);
|
||||
}).toThrowError('Failed to process NgModule relationships.');
|
||||
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};
|
||||
}
|
||||
Reference in New Issue
Block a user