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'], | ||||
|     skipAbstractDirectives: true, | ||||
|     $process(docs) { | ||||
|       // Match all the directives/pipes to their module
 | ||||
|       const errors = []; | ||||
|       docs.forEach(doc => { | ||||
| 
 | ||||
|       for (const doc of docs) { | ||||
|         if (this.exportDocTypes.indexOf(doc.docType) !== -1) { | ||||
|           const options = doc[`${doc.docType}Options`]; | ||||
| 
 | ||||
|           // 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; | ||||
|           }); | ||||
|           // Match all the directives/pipes to their module
 | ||||
|           this.processNgModuleExportDoc(doc, errors); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|         if (doc.docType === 'class') { | ||||
|           this.processInjectableDoc(doc, errors); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (errors.length) { | ||||
|         errors.forEach(error => log.error(error)); | ||||
|         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') { | ||||
|           Object.keys(doc.ngmoduleOptions).forEach(key => { | ||||
|             const value = doc.ngmoduleOptions[key]; | ||||
|             if (value && !Array.isArray(value)) { | ||||
|               doc.ngmoduleOptions[key] = [value]; | ||||
|             } | ||||
|           }); | ||||
|           this.exportDocTypes.forEach(type => { | ||||
|             const containerName = getContainerName(type); | ||||
|             const container = doc[containerName]; | ||||
|             if (container) { | ||||
|               container.sort(byId); | ||||
|             } | ||||
|           }); | ||||
|           convertAllPropertiesToArrays(doc.ngmoduleOptions); | ||||
|           this.sortExportDocContainers(doc); | ||||
|           this.processNgModuleProviders(doc); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * Associate the `exportDoc` that is expected to have been exported from an `NgModule` with its | ||||
|      * `NgModule` doc. | ||||
|      */ | ||||
|     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) { | ||||
|   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) { | ||||
|   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']); | ||||
|   }); | ||||
| 
 | ||||
|   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}; | ||||
| } | ||||
| @ -1,9 +1,11 @@ | ||||
| {% import "lib/ngmodule.html" as ngModuleHelpers -%} | ||||
| {% extends 'export-base.template.html' -%} | ||||
| 
 | ||||
| {% block overview %} | ||||
|   {% include "includes/class-overview.html" %} | ||||
| {% endblock %} | ||||
| {% block details %} | ||||
|   {$ ngModuleHelpers.ngModuleList(doc.ngModules, 'Provided in') $} | ||||
|   {% include "includes/description.html" %} | ||||
|   {% include "includes/class-members.html" %} | ||||
| {% endblock %} | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| {% import "lib/memberHelpers.html" as memberHelpers -%} | ||||
| {% import "lib/ngmodule.html" as ngModuleHelpers -%} | ||||
| {% extends 'class.template.html' -%} | ||||
| 
 | ||||
| {% block overview %}{% endblock %} | ||||
| 
 | ||||
| {% block details -%} | ||||
|   {% include "includes/ngmodule.html" %} | ||||
|   {$ ngModuleHelpers.ngModuleList(doc.ngModules, 'Exported from') $} | ||||
|   {% include "includes/selectors.html" %} | ||||
| 
 | ||||
|   {$ 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/ngmodule.html" as ngModuleHelpers -%} | ||||
| {% import "lib/paramList.html" as params -%} | ||||
| 
 | ||||
| <section class="{$ doc.docType $}-overview"> | ||||
| @ -10,7 +11,7 @@ | ||||
|       {%- if param.isOptional or param.defaultValue !== undefined %} ]{% endif %} | ||||
|     {%- endfor %} }}</code-example> | ||||
| 
 | ||||
|   {% include "includes/ngmodule.html" %} | ||||
|   {$ ngModuleHelpers.ngModuleList(doc.ngModules, 'Exported from') $} | ||||
| 
 | ||||
|   {% if doc.valueParam.type %} | ||||
|   <h2>Input value</h2> | ||||
|  | ||||
							
								
								
									
										18
									
								
								aio/tools/transforms/templates/api/lib/ngmodule.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								aio/tools/transforms/templates/api/lib/ngmodule.html
									
									
									
									
									
										Normal file
									
								
							| @ -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') $} | ||||
| 
 | ||||
|   {% if doc.ngmoduleOptions.providers %} | ||||
|   {$ renderTable(doc.ngmoduleOptions.providers, 'providers', 'Providers', 'Provider') $} | ||||
|   {% if doc.providers %} | ||||
|   {$ renderTable(doc.providers, 'providers', 'Providers', 'Provider') $} | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {% if doc.directives.length %} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user