diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index fb2575848a3..f1659137bd1 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -902,7 +902,7 @@ export class NgCompiler { * @param entryPoint Path to the entry point for the package for which API * docs should be extracted. */ - getApiDocumentation(entryPoint: string): DocEntry[] { + getApiDocumentation(entryPoint: string): {entries: DocEntry[]; symbols: Map} { const compilation = this.ensureAnalyzed(); const checker = this.inputProgram.getTypeChecker(); const docsExtractor = new DocsExtractor(checker, compilation.metaReader); @@ -918,7 +918,7 @@ export class NgCompiler { } // TODO: Technically the current directory is not the root dir. - // Should probably be derived from the config. + // Should probably be derived from the config. const rootDir = this.inputProgram.getCurrentDirectory(); return docsExtractor.extractAll(entryPointSourceFile, rootDir); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts index b584e27bde9..f882598ba25 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts @@ -27,9 +27,13 @@ import { isInitializerApiFunction, } from './initializer_api_function_extractor'; import {extractTypeAlias} from './type_alias_extractor'; +import {getImportedSymbols} from './import_extractor'; type DeclarationWithExportName = readonly [string, ts.Declaration]; +/** The modules here doesn't expose symbols publicly as they are considered private */ +const privateModules = new Set(['core/primitives/signals', 'core/primitives/event-dispatch']); + /** * Extracts all information from a source file that may be relevant for generating * public API documentation. @@ -46,8 +50,12 @@ export class DocsExtractor { * * @param sourceFile The file from which to extract documentable entries. */ - extractAll(sourceFile: ts.SourceFile, rootDir: string): DocEntry[] { + extractAll( + sourceFile: ts.SourceFile, + rootDir: string, + ): {entries: DocEntry[]; symbols: Map} { const entries: DocEntry[] = []; + const symbols = new Map(); const exportedDeclarations = this.getExportedDeclarations(sourceFile); for (const [exportName, node] of exportedDeclarations) { @@ -62,6 +70,20 @@ export class DocsExtractor { // We want the real source file of the declaration. const realSourceFile = node.getSourceFile(); + /** + * `sourceFile` is generaly a `export * from './public_api';` with no imports. + * It is necessary to pick-up every import from the real source files. + * This allows to include symbols from other packages (like @angular/core) + * By doing this, the generation remains independant from other packages + */ + const importedSymbols = getImportedSymbols(realSourceFile); + importedSymbols.forEach((moduleName, symbolName) => { + if (privateModules.has(moduleName) || symbolName.startsWith('ɵ')) { + return; + } + symbols.set(symbolName, moduleName); + }); + // Set the source code references for the extracted entry. (entry as DocEntryWithSourceInfo).source = { filePath: getRelativeFilePath(realSourceFile, rootDir), @@ -77,7 +99,7 @@ export class DocsExtractor { } } - return entries; + return {entries, symbols}; } /** Extract the doc entry for a single declaration. */ diff --git a/packages/compiler-cli/src/ngtsc/docs/src/import_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/import_extractor.ts new file mode 100644 index 00000000000..8dcb0c83a58 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/docs/src/import_extractor.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import ts from 'typescript'; + +/** + * For a given SourceFile, it extracts all imported symbols from other Angular packages. + * + * @returns a map Symbol => Package, eg: ApplicationRef => @angular/core + */ +export function getImportedSymbols(sourceFile: ts.SourceFile): Map { + const importSpecifiers = new Map(); + + function visit(node: ts.Node) { + if (ts.isImportDeclaration(node)) { + let moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, ''); + + if (moduleSpecifier.startsWith('@angular')) { + const namedBindings = node.importClause?.namedBindings; + + if (namedBindings && ts.isNamedImports(namedBindings)) { + namedBindings.elements.forEach((importSpecifier) => { + const importName = importSpecifier.name.text; + const importAlias = importSpecifier.propertyName + ? importSpecifier.propertyName.text + : undefined; + + importSpecifiers.set(importAlias ?? importName, moduleSpecifier); + }); + } + } + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + + return importSpecifiers; +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index ee250afdae6..bd928ab04f5 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -399,7 +399,7 @@ export class NgtscProgram implements api.Program { * @param entryPoint Path to the entry point for the package for which API * docs should be extracted. */ - getApiDocumentation(entryPoint: string): DocEntry[] { + getApiDocumentation(entryPoint: string): {entries: DocEntry[]; symbols: Map} { return this.compiler.getApiDocumentation(entryPoint); } diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/import_extractor_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/import_extractor_spec.ts new file mode 100644 index 00000000000..e98f07f8963 --- /dev/null +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/import_extractor_spec.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; +import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; + +import {NgtscTestEnvironment} from '../env'; + +const testFiles = loadStandardTestFiles({fakeCommon: true}); + +runInEachFileSystem(() => { + let env!: NgtscTestEnvironment; + + describe('ngtsc function docs extraction', () => { + beforeEach(() => { + env = NgtscTestEnvironment.setup(testFiles); + env.tsconfig(); + }); + + it('should extract imported symbols from other angular packages', () => { + env.write( + 'index.ts', + ` + import {ApplicationRef} from '@angular/core'; + import {FormGroup} from '@angular/forms'; + + export function getApp(): ApplicationRef { + } + + export function getForm(): FormGroup { + } + `, + ); + + const symbols = env.driveDocsExtractionForSymbols('index.ts'); + + expect(symbols.size).toBe(2); + expect(symbols.get('ApplicationRef')).toBe('@angular/core'); + expect(symbols.get('FormGroup')).toBe('@angular/forms'); + }); + + it('should not extract imported symbols from non angular packages', () => { + env.write( + 'index.ts', + ` + import {ApplicationRef} from '@not-angular/core'; + import {FormGroup} from '@angular/forms'; + + export function getApp(): ApplicationRef { + } + + export function getForm(): FormGroup { + } + `, + ); + + const symbols = env.driveDocsExtractionForSymbols('index.ts'); + + expect(symbols.size).toBe(1); + expect(symbols.get('FormGroup')).toBe('@angular/forms'); + }); + + it('should not extract private symbols', () => { + env.write( + 'index.ts', + ` + import {ɵSafeHtml} from '@angular/core'; + import {FormGroup} from '@angular/forms'; + + export function getApp(): ApplicationRef { + } + + export function getForm(): FormGroup { + } + `, + ); + + const symbols = env.driveDocsExtractionForSymbols('index.ts'); + + expect(symbols.size).toBe(1); + expect(symbols.get('FormGroup')).toBe('@angular/forms'); + }); + + it('should not extract symbols from private packages', () => { + env.write( + 'index.ts', + ` + import {REACTIVE_NODE} from '@core/primitives/signals'; + import {FormGroup} from '@angular/forms'; + + export function getApp(): ApplicationRef { + } + + export function getForm(): FormGroup { + } + `, + ); + + const symbols = env.driveDocsExtractionForSymbols('index.ts'); + + expect(symbols.size).toBe(1); + expect(symbols.get('FormGroup')).toBe('@angular/forms'); + }); + }); +}); diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index d6f6c02ac27..419e130c902 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -321,7 +321,14 @@ export class NgtscTestEnvironment { const {rootNames, options} = readNgcCommandLineAndConfiguration(this.commandLineArgs); const host = createCompilerHost({options}); const program = createProgram({rootNames, host, options}); - return (program as NgtscProgram).getApiDocumentation(entryPoint); + return (program as NgtscProgram).getApiDocumentation(entryPoint).entries; + } + + driveDocsExtractionForSymbols(entryPoint: string): Map { + const {rootNames, options} = readNgcCommandLineAndConfiguration(this.commandLineArgs); + const host = createCompilerHost({options}); + const program = createProgram({rootNames, host, options}); + return (program as NgtscProgram).getApiDocumentation(entryPoint).symbols; } driveXi18n(format: string, outputFileName: string, locale: string | null = null): void {