refactor(compiler-cli): Add a map of every symbols used inside a package (#57346)

This commit changes the structure of the API extraction files to include all symbols used inside a package.

The structure is a `Map`, Symbol => package
eg: 'ApplicationRef' => '@angular/core'

PR Close #57346
This commit is contained in:
Matthieu Riegler
2024-08-21 01:02:51 +02:00
committed by Andrew Kushnir
parent 93bdbbc812
commit 9814767d34
6 changed files with 190 additions and 6 deletions
@@ -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<string, string>} {
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);
}
@@ -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<string, string>} {
const entries: DocEntry[] = [];
const symbols = new Map<string, string>();
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. */
@@ -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<string, string> {
const importSpecifiers = new Map<string, string>();
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;
}
+1 -1
View File
@@ -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<string, string>} {
return this.compiler.getApiDocumentation(entryPoint);
}
@@ -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');
});
});
});
+8 -1
View File
@@ -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<string, string> {
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 {