mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
773 lines
24 KiB
TypeScript
773 lines
24 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2020 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import * as yargs from 'yargs';
|
|
import * as ts from 'typescript';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { ESLint } from 'eslint';
|
|
|
|
/**
|
|
* Prunes a DTS file based on three main rules:
|
|
* - Top level types are only included if they are also exported.
|
|
* - Underscore-prefixed members of class and interface types are stripped.
|
|
* - Constructors are made private or protected if marked with
|
|
* `@hideconstructor`/`@hideconstructor protected`.
|
|
*
|
|
* This function is meant to operate on DTS files generated by API extractor
|
|
* and extracts out the API that is relevant for third-party SDK consumers.
|
|
*
|
|
* @param inputLocation The file path to the .d.ts produced by API explorer.
|
|
* @param outputLocation The output location for the pruned .d.ts file.
|
|
*/
|
|
export function pruneDts(
|
|
inputLocation: string,
|
|
outputLocation: string,
|
|
otherExportFileLocations: string[] = []
|
|
): void {
|
|
const compilerOptions = {};
|
|
const host = ts.createCompilerHost(compilerOptions);
|
|
const program = ts.createProgram(
|
|
[inputLocation, ...otherExportFileLocations],
|
|
compilerOptions,
|
|
host
|
|
);
|
|
const printer: ts.Printer = ts.createPrinter();
|
|
const sourceFile = program.getSourceFile(inputLocation)!;
|
|
const otherExportSourceFiles = otherExportFileLocations
|
|
.map(otherFileLocation => program.getSourceFile(otherFileLocation))
|
|
.filter(value => value !== undefined) as ts.SourceFile[];
|
|
|
|
const result: ts.TransformationResult<ts.SourceFile> =
|
|
ts.transform<ts.SourceFile>(sourceFile, [
|
|
dropPrivateApiTransformer.bind(
|
|
null,
|
|
program,
|
|
host,
|
|
otherExportSourceFiles
|
|
)
|
|
]);
|
|
const transformedSourceFile: ts.SourceFile = result.transformed[0];
|
|
let content = printer.printFile(transformedSourceFile);
|
|
|
|
// Preserve leading license comment if updateSourceFile dropped it from statement #0
|
|
const leadingComments = ts.getLeadingCommentRanges(
|
|
sourceFile.getFullText(),
|
|
0
|
|
);
|
|
if (leadingComments && leadingComments.length > 0) {
|
|
const firstComment = sourceFile
|
|
.getFullText()
|
|
.slice(leadingComments[0].pos, leadingComments[0].end);
|
|
if (
|
|
firstComment.includes('@license') &&
|
|
!content.trimStart().startsWith('/**\n * @license') &&
|
|
!content.trimStart().startsWith('/*\n * @license')
|
|
) {
|
|
content = `${firstComment}\n` + content;
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(outputLocation, content);
|
|
}
|
|
|
|
export async function addBlankLines(outputLocation: string): Promise<void> {
|
|
const eslint = new ESLint({
|
|
fix: true,
|
|
overrideConfig: {
|
|
parserOptions: {
|
|
ecmaVersion: 2017,
|
|
sourceType: 'module',
|
|
tsconfigRootDir: __dirname,
|
|
project: ['./tsconfig.eslint.json']
|
|
},
|
|
env: {
|
|
es6: true
|
|
},
|
|
plugins: ['@typescript-eslint'],
|
|
parser: '@typescript-eslint/parser',
|
|
rules: {
|
|
'unused-imports/no-unused-imports-ts': ['off'],
|
|
// add blank lines after imports. Otherwise removeUnusedImports() will remove the comment
|
|
// of the first item after the import block
|
|
'padding-line-between-statements': [
|
|
'error',
|
|
{ 'blankLine': 'always', 'prev': 'import', 'next': '*' }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
const results = await eslint.lintFiles(outputLocation);
|
|
await ESLint.outputFixes(results);
|
|
}
|
|
|
|
export async function removeUnusedImports(
|
|
outputLocation: string
|
|
): Promise<void> {
|
|
const eslint = new ESLint({
|
|
fix: true,
|
|
overrideConfig: {
|
|
parserOptions: {
|
|
ecmaVersion: 2017,
|
|
sourceType: 'module',
|
|
tsconfigRootDir: __dirname,
|
|
project: ['./tsconfig.eslint.json']
|
|
},
|
|
env: {
|
|
es6: true
|
|
},
|
|
plugins: ['unused-imports', '@typescript-eslint'],
|
|
parser: '@typescript-eslint/parser',
|
|
rules: {
|
|
'unused-imports/no-unused-imports-ts': ['error']
|
|
}
|
|
}
|
|
});
|
|
const results = await eslint.lintFiles(outputLocation);
|
|
await ESLint.outputFixes(results);
|
|
}
|
|
|
|
/** Determines whether the provided identifier should be hidden. */
|
|
function hasPrivatePrefix(name: ts.Identifier): boolean {
|
|
// Identifiers that are prefixed with an underscore are not included in
|
|
// the public API.
|
|
return !!name.escapedText?.toString().startsWith('_');
|
|
}
|
|
|
|
/** Returns whether type identified by `name` is exported. */
|
|
function isExported(
|
|
typeChecker: ts.TypeChecker,
|
|
sourceFile: ts.SourceFile,
|
|
name: ts.Identifier
|
|
): boolean {
|
|
const declarations =
|
|
typeChecker.getSymbolAtLocation(name)?.declarations ?? [];
|
|
|
|
// Check is this is a public symbol (e.g. part of the DOM library)
|
|
const isTypeScriptType = declarations.find(
|
|
d => d.getSourceFile().fileName.indexOf('typescript/lib') != -1
|
|
);
|
|
const isImported = declarations.find(d => ts.isImportSpecifier(d));
|
|
if (isTypeScriptType || isImported) {
|
|
return true;
|
|
}
|
|
|
|
// Check is this is part of the exported symbols of the SDK module
|
|
const allExportedSymbols = typeChecker.getExportsOfModule(
|
|
typeChecker.getSymbolAtLocation(sourceFile)!
|
|
);
|
|
return !!allExportedSymbols.find(s => s.name === name.escapedText);
|
|
}
|
|
|
|
/**
|
|
* Replaces an existing constructor implementation if the constructor is marked
|
|
* with the JSDod tag `@hideconstructor`. The replaced constructor can either
|
|
* have `private` visibility` or `protected`. To generate a protected
|
|
* constructor, specify `@hideconstructor protected`.
|
|
*
|
|
* Returns either the modified constructor or the existing constructor if no
|
|
* modification was needed.
|
|
*/
|
|
function maybeHideConstructor(
|
|
node: ts.ConstructorDeclaration
|
|
): ts.ConstructorDeclaration {
|
|
const hideConstructorTag = ts
|
|
.getJSDocTags(node)
|
|
?.find(t => t.tagName.escapedText === 'hideconstructor');
|
|
|
|
if (hideConstructorTag) {
|
|
const hideConstructorModifier = ts.factory.createModifier(
|
|
hideConstructorTag.comment === 'protected'
|
|
? ts.SyntaxKind.ProtectedKeyword
|
|
: ts.SyntaxKind.PrivateKeyword
|
|
);
|
|
return ts.factory.createConstructorDeclaration(
|
|
[...(ts.getModifiers(node) ?? []), hideConstructorModifier],
|
|
[],
|
|
undefined
|
|
);
|
|
} else {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Examines `extends` and `implements` clauses and removes or replaces them if
|
|
* they refer to a non-exported type. When an export is removed, all members
|
|
* from the removed class are merged into the provided class or interface
|
|
* declaration.
|
|
*
|
|
* @example
|
|
* Input:
|
|
* class Foo {
|
|
* foo: string;
|
|
* }
|
|
* export class Bar extends Foo {}
|
|
*
|
|
* Output:
|
|
* export class Bar {
|
|
* foo: string;
|
|
* }
|
|
*/
|
|
function prunePrivateImports<
|
|
T extends ts.InterfaceDeclaration | ts.ClassDeclaration
|
|
>(
|
|
program: ts.Program,
|
|
host: ts.CompilerHost,
|
|
sourceFile: ts.SourceFile,
|
|
node: T
|
|
): T {
|
|
const typeChecker = program.getTypeChecker();
|
|
|
|
// The list of heritage clauses after all private symbols are removed.
|
|
const prunedHeritageClauses: ts.HeritageClause[] = [];
|
|
// Additional members that are copied from the private symbols into the public
|
|
// symbols
|
|
const inheritedInterfaceProperties: ts.TypeElement[] = [];
|
|
const inheritedClassMembers: ts.ClassElement[] = [];
|
|
for (const heritageClause of node.heritageClauses || []) {
|
|
const exportedTypes: ts.ExpressionWithTypeArguments[] = [];
|
|
for (const type of heritageClause.types) {
|
|
if (
|
|
ts.isIdentifier(type.expression) &&
|
|
isExported(typeChecker, sourceFile, type.expression)
|
|
) {
|
|
exportedTypes.push(type);
|
|
} else {
|
|
// Hide the type we are inheriting from and merge its declarations
|
|
// into the current class.
|
|
const privateType = typeChecker.getTypeAtLocation(type);
|
|
if (ts.isClassDeclaration(node)) {
|
|
inheritedClassMembers.push(
|
|
...convertPropertiesForEnclosingDeclaration<
|
|
ts.ClassDeclaration,
|
|
ts.ClassElement
|
|
>(program, host, sourceFile, privateType.getProperties(), node)
|
|
);
|
|
} else if (ts.isInterfaceDeclaration(node)) {
|
|
inheritedInterfaceProperties.push(
|
|
...convertPropertiesForEnclosingDeclaration<
|
|
ts.InterfaceDeclaration,
|
|
ts.TypeElement
|
|
>(program, host, sourceFile, privateType.getProperties(), node)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (exportedTypes.length > 0) {
|
|
prunedHeritageClauses.push(
|
|
ts.factory.updateHeritageClause(heritageClause, exportedTypes)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (ts.isClassDeclaration(node)) {
|
|
const modifiersAndDecorators = [
|
|
...(ts.getModifiers(node) ?? []),
|
|
...(ts.getDecorators(node) ?? [])
|
|
];
|
|
return ts.factory.updateClassDeclaration(
|
|
node,
|
|
modifiersAndDecorators,
|
|
node.name,
|
|
node.typeParameters,
|
|
prunedHeritageClauses,
|
|
[...node.members, ...inheritedClassMembers]
|
|
) as T;
|
|
} else if (ts.isInterfaceDeclaration(node)) {
|
|
return ts.factory.updateInterfaceDeclaration(
|
|
node,
|
|
node.modifiers,
|
|
node.name,
|
|
node.typeParameters,
|
|
prunedHeritageClauses,
|
|
[...node.members, ...inheritedInterfaceProperties]
|
|
) as T;
|
|
} else {
|
|
throw new Error('Only classes or interfaces are supported');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterates over the provided symbols and returns named declarations for these
|
|
* symbols if they are missing from `currentInterface`. This allows us to merge
|
|
* interface hierarchies for interfaces whose inherited properties are not part of the
|
|
* public API.
|
|
*
|
|
* This method relies on a private API in TypeScript's `codefix` package.
|
|
*/
|
|
function convertPropertiesForEnclosingDeclaration<
|
|
T extends ts.ClassDeclaration | ts.InterfaceDeclaration,
|
|
U extends ts.ClassElement | ts.TypeElement
|
|
>(
|
|
program: ts.Program,
|
|
host: ts.CompilerHost,
|
|
sourceFile: ts.SourceFile,
|
|
parentClassSymbols: ts.Symbol[],
|
|
currentDeclaration: T
|
|
): U[] {
|
|
const newMembers: U[] = [];
|
|
// The `codefix` package is not public but it does exactly what we want. It's
|
|
// the same package that is used by VSCode to fill in missing members, which
|
|
// is what we are using it for in this script. `codefix` handles missing
|
|
// properties, methods and correctly deduces generics.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(ts as any).codefix.createMissingMemberNodes(
|
|
currentDeclaration,
|
|
parentClassSymbols,
|
|
sourceFile,
|
|
{ program, host },
|
|
/* userPreferences= */ {},
|
|
/* importAdder= */ undefined,
|
|
(missingMember: U) => {
|
|
const originalSymbol = parentClassSymbols.find(
|
|
symbol =>
|
|
symbol.escapedName ==
|
|
(missingMember.name as ts.Identifier).escapedText
|
|
);
|
|
if (originalSymbol) {
|
|
const jsDocComment = extractJSDocComment(originalSymbol, newMembers);
|
|
if (jsDocComment) {
|
|
ts.setSyntheticLeadingComments(missingMember, [
|
|
{
|
|
kind: ts.SyntaxKind.MultiLineCommentTrivia,
|
|
text: `*\n${jsDocComment}\n`,
|
|
hasTrailingNewLine: true,
|
|
pos: -1,
|
|
end: -1
|
|
}
|
|
]);
|
|
}
|
|
|
|
newMembers.push(missingMember);
|
|
}
|
|
}
|
|
);
|
|
return newMembers;
|
|
}
|
|
|
|
/** Extracts the JSDoc comment from `symbol`. */
|
|
function extractJSDocComment(
|
|
symbol: ts.Symbol,
|
|
alreadyAddedMembers: ts.Node[]
|
|
): string | null {
|
|
const overloadCount = alreadyAddedMembers.filter(
|
|
node =>
|
|
ts.isClassElement(node) &&
|
|
(node.name as ts.Identifier).escapedText == symbol.name
|
|
).length;
|
|
|
|
// Extract the comment from the overload that we are currently processing.
|
|
let targetIndex = 0;
|
|
const commentParts = symbol
|
|
.getDocumentationComment(undefined)
|
|
.filter(symbol => {
|
|
// Overload comments are separated by line breaks.
|
|
if (symbol.kind == 'lineBreak') {
|
|
++targetIndex;
|
|
return false;
|
|
} else {
|
|
return overloadCount == targetIndex;
|
|
}
|
|
});
|
|
|
|
const comment = commentParts
|
|
.map(commentPart => {
|
|
if (commentPart.kind === 'linkText') {
|
|
/**
|
|
* Some links will be spread across more than one line. For example:
|
|
* {@link
|
|
* DocumentReference}
|
|
* In this case, we want to remove the leading ' * ' without removing the newline.
|
|
*/
|
|
const leadingStar = /(?!\n)\s*\* /;
|
|
/**
|
|
* For some reason, this text will also have a trailing space (the above example would give
|
|
* us 'DocumentReference '), so we trim that from the end of the string.
|
|
*/
|
|
const trailingSpaces = / *$/;
|
|
|
|
return commentPart.text
|
|
.replace(leadingStar, '\n')
|
|
.replace(trailingSpaces, '');
|
|
} else {
|
|
return commentPart.text;
|
|
}
|
|
})
|
|
.join('');
|
|
|
|
if (!comment || !symbol.declarations) {
|
|
return null;
|
|
}
|
|
|
|
const declaration = symbol.declarations[overloadCount];
|
|
const jsDocTags = ts
|
|
.getJSDocTags(declaration)
|
|
.map(tag =>
|
|
tag
|
|
.getFullText()
|
|
.split('\n')
|
|
.map(line => line.replace('*', '').trim())
|
|
.filter(line => line !== '') // Remove empty lines
|
|
.map(text => text.replace(/s*\* /, '')) // Remove the '*' prefix from all lines
|
|
.join('\n')
|
|
)
|
|
.join('\n');
|
|
const maybeNewline = jsDocTags?.length > 0 ? '\n' : '';
|
|
|
|
const commentAndTags = `${comment}\n${maybeNewline}${jsDocTags}`.trim();
|
|
const formattedJSDocComment = commentAndTags
|
|
.split('\n')
|
|
.map(line => ` * ${line.trim()}`)
|
|
.join('\n');
|
|
|
|
return formattedJSDocComment;
|
|
}
|
|
|
|
/**
|
|
* Replaces input types of public APIs that consume non-exported types, which
|
|
* allows us to exclude private types from the pruned definitions. Returns the
|
|
* the name of the exported API or undefined if no type is found.
|
|
*
|
|
* @example
|
|
* Input:
|
|
* class PrivateFoo {}
|
|
* export class PublicFoo extends PrivateFoo {}
|
|
* export function doFoo(foo: PrivateFoo);
|
|
*
|
|
* Output:
|
|
* export class PublicFoo {}
|
|
* export function doFoo(foo: PublicFoo);
|
|
*/
|
|
function extractExportedSymbol(
|
|
typeChecker: ts.TypeChecker,
|
|
sourceFile: ts.SourceFile,
|
|
typeName: ts.Node
|
|
): ts.Symbol | undefined {
|
|
if (!ts.isIdentifier(typeName)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (isExported(typeChecker, sourceFile, typeName)) {
|
|
// Don't replace the type reference if the type is already part of the
|
|
// public API.
|
|
return undefined;
|
|
}
|
|
|
|
const localSymbolName = typeName.escapedText;
|
|
const allExportedSymbols = typeChecker.getExportsOfModule(
|
|
typeChecker.getSymbolAtLocation(sourceFile)!
|
|
);
|
|
|
|
// Examine all exported types and check if they extend or implement the
|
|
// provided local type. If so, we can use the exported type in lieu of the
|
|
// private type.
|
|
|
|
// Short circuit if the local types is already part of the public types.
|
|
for (const symbol of allExportedSymbols) {
|
|
if (symbol.name === localSymbolName) {
|
|
return symbol;
|
|
}
|
|
}
|
|
|
|
// See if there is an exported symbol that extends this private symbol.
|
|
// In this case, we can safely use the public symbol instead.
|
|
for (const symbol of allExportedSymbols) {
|
|
if (symbol.declarations) {
|
|
for (const declaration of symbol.declarations) {
|
|
if (
|
|
ts.isClassDeclaration(declaration) ||
|
|
ts.isInterfaceDeclaration(declaration)
|
|
) {
|
|
for (const heritageClause of declaration.heritageClauses || []) {
|
|
for (const type of heritageClause.types || []) {
|
|
if (ts.isIdentifier(type.expression)) {
|
|
const subclassName = type.expression.escapedText;
|
|
if (subclassName === localSymbolName) {
|
|
// TODO: We may need to change this to return a Union type if
|
|
// more than one public type corresponds to the private type.
|
|
return symbol;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no symbol was found that extends the private symbol, check the reverse.
|
|
// We might find an exported symbol in the inheritance chain of the local
|
|
// symbol. Note that this is not always safe as we might replace the local
|
|
// symbol with a less restrictive type.
|
|
const localSymbol = typeChecker.getSymbolAtLocation(typeName);
|
|
if (localSymbol?.declarations) {
|
|
for (const declaration of localSymbol.declarations) {
|
|
if (
|
|
ts.isClassDeclaration(declaration) ||
|
|
ts.isInterfaceDeclaration(declaration)
|
|
) {
|
|
for (const heritageClause of declaration.heritageClauses || []) {
|
|
for (const type of heritageClause.types || []) {
|
|
if (ts.isIdentifier(type.expression)) {
|
|
if (isExported(typeChecker, sourceFile, type.expression)) {
|
|
return typeChecker.getSymbolAtLocation(type.expression);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function findExternalExport(
|
|
typeChecker: ts.TypeChecker,
|
|
sourceFile: ts.SourceFile,
|
|
node:
|
|
| ts.InterfaceDeclaration
|
|
| ts.ClassDeclaration
|
|
| ts.TypeAliasDeclaration
|
|
| ts.EnumDeclaration,
|
|
otherExportSourceFiles: ts.SourceFile[]
|
|
): ts.SourceFile | undefined {
|
|
if (!node.name) return undefined;
|
|
|
|
const localSymbolName = node.name.text;
|
|
|
|
for (const otherExportSourceFile of otherExportSourceFiles) {
|
|
const otherExportedSymbols = typeChecker.getExportsOfModule(
|
|
typeChecker.getSymbolAtLocation(otherExportSourceFile)!
|
|
);
|
|
|
|
for (const symbol of otherExportedSymbols) {
|
|
// TODO: ideally this would compare definitions to handle the case
|
|
// of name collisions with different definitions. However, this
|
|
// implementation currently does not handle function exports,
|
|
// which is the only place we expect name collisions.
|
|
if (symbol.name === localSymbolName) {
|
|
return otherExportSourceFile;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function dropPrivateApiTransformer(
|
|
program: ts.Program,
|
|
host: ts.CompilerHost,
|
|
otherExportSourceFiles: ts.SourceFile[],
|
|
context: ts.TransformationContext
|
|
): ts.Transformer<ts.SourceFile> {
|
|
const typeChecker = program.getTypeChecker();
|
|
|
|
return (sourceFile: ts.SourceFile) => {
|
|
const imports: Record<string, Array<string>> = {};
|
|
|
|
// Get exported symbols
|
|
const directExportedSymbols = typeChecker.getExportsOfModule(
|
|
typeChecker.getSymbolAtLocation(sourceFile)!
|
|
);
|
|
// Map exported symbols to aliases.
|
|
// For the statement `export { X as Y };`, this list would contain a symbol
|
|
// for `X`.
|
|
const aliasedExportedSymbols = directExportedSymbols
|
|
.map(symbol =>
|
|
symbol.flags & ts.SymbolFlags.Alias
|
|
? typeChecker.getAliasedSymbol(symbol)
|
|
: undefined
|
|
)
|
|
.filter(symbol => symbol !== undefined);
|
|
|
|
function ensureImportsForFile(filename: string): Array<string> {
|
|
let importsForFile = imports[filename];
|
|
if (!importsForFile) {
|
|
importsForFile = [];
|
|
imports[filename] = importsForFile;
|
|
}
|
|
return importsForFile;
|
|
}
|
|
|
|
function visit(node: ts.Node): ts.Node {
|
|
if (
|
|
ts.isInterfaceDeclaration(node) ||
|
|
ts.isClassDeclaration(node) ||
|
|
ts.isFunctionDeclaration(node) ||
|
|
ts.isVariableStatement(node) ||
|
|
ts.isTypeAliasDeclaration(node) ||
|
|
ts.isModuleDeclaration(node) ||
|
|
ts.isEnumDeclaration(node)
|
|
) {
|
|
// Remove any types that are not exported.
|
|
// First we check the modifiers for the symbol `export function X`. If
|
|
// the export keyword is found, the symbol is modified.
|
|
// Second we check if the symbol has an alias that is exported elsewhere,
|
|
// for example: `function X; export { X as Y }`. If the alias is
|
|
// exported elsewhere, then we also have to keep the symbol.
|
|
if (
|
|
!ts
|
|
.getModifiers(node)
|
|
?.find(m => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
) {
|
|
// Try to get a symbol for this node.
|
|
const symbol =
|
|
'name' in node && node.name
|
|
? typeChecker.getSymbolAtLocation(node.name)
|
|
: undefined;
|
|
// Check if that symbol is in the list of aliased exported symbols.
|
|
// If it is, we keep the symbol. Otherwise, we remove the symbol.
|
|
if (!symbol || !aliasedExportedSymbols.includes(symbol)) {
|
|
// NO-OP block to keep the condition readable
|
|
return ts.factory.createNotEmittedStatement(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
ts.isInterfaceDeclaration(node) ||
|
|
ts.isClassDeclaration(node) ||
|
|
ts.isTypeAliasDeclaration(node) ||
|
|
ts.isEnumDeclaration(node)
|
|
) {
|
|
// Remove any types that are exported externally
|
|
const externalExportFile = findExternalExport(
|
|
typeChecker,
|
|
sourceFile,
|
|
node,
|
|
otherExportSourceFiles
|
|
);
|
|
if (externalExportFile && node.name) {
|
|
ensureImportsForFile(
|
|
path.relative(
|
|
path.dirname(sourceFile.fileName),
|
|
externalExportFile.fileName
|
|
)
|
|
).push(node.name.text);
|
|
return ts.factory.createNotEmittedStatement(node);
|
|
}
|
|
}
|
|
|
|
if (ts.isConstructorDeclaration(node)) {
|
|
// Replace internal constructors with private constructors.
|
|
return maybeHideConstructor(node);
|
|
} else if (
|
|
ts.isClassDeclaration(node) ||
|
|
ts.isInterfaceDeclaration(node)
|
|
) {
|
|
// Remove any imports that reference internal APIs, while retaining
|
|
// their public members.
|
|
return prunePrivateImports(program, host, sourceFile, node);
|
|
} else if (
|
|
ts.isPropertyDeclaration(node) ||
|
|
ts.isMethodDeclaration(node) ||
|
|
ts.isGetAccessor(node)
|
|
) {
|
|
// Remove any class and interface members that are prefixed with
|
|
// underscores.
|
|
if (hasPrivatePrefix(node.name as ts.Identifier)) {
|
|
const notEmittedStatement =
|
|
ts.factory.createNotEmittedStatement(node);
|
|
return notEmittedStatement;
|
|
}
|
|
} else if (ts.isTypeReferenceNode(node)) {
|
|
// For public types that refer internal types, find a public type that
|
|
// we can refer to instead.
|
|
const publicName = extractExportedSymbol(
|
|
typeChecker,
|
|
sourceFile,
|
|
node.typeName
|
|
);
|
|
return publicName
|
|
? ts.factory.updateTypeReferenceNode(
|
|
node,
|
|
ts.factory.createIdentifier(publicName.name),
|
|
node.typeArguments
|
|
)
|
|
: node;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function visitNodeAndChildren<T extends ts.Node>(node: T): T {
|
|
return ts.visitEachChild(
|
|
visit(node),
|
|
childNode => visitNodeAndChildren(childNode),
|
|
context
|
|
) as T;
|
|
}
|
|
const result = visitNodeAndChildren(sourceFile);
|
|
|
|
const moreImports: ts.ImportDeclaration[] = [];
|
|
for (let filename in imports) {
|
|
const importSpecifiers: ts.ImportSpecifier[] = [];
|
|
for (let identifier of imports[filename]) {
|
|
importSpecifiers.push(
|
|
ts.factory.createImportSpecifier(
|
|
false,
|
|
undefined,
|
|
ts.factory.createIdentifier(identifier)
|
|
)
|
|
);
|
|
}
|
|
let outFileName = filename.startsWith('.') ? filename : `./${filename}`;
|
|
outFileName = outFileName.replace('.d.ts', '');
|
|
const importDeclaration = ts.factory.createImportDeclaration(
|
|
[],
|
|
ts.factory.createImportClause(
|
|
true,
|
|
undefined,
|
|
ts.factory.createNamedImports(importSpecifiers)
|
|
),
|
|
ts.factory.createStringLiteral(outFileName, true)
|
|
);
|
|
|
|
moreImports.push(importDeclaration);
|
|
}
|
|
|
|
return ts.factory.updateSourceFile(
|
|
result,
|
|
[...moreImports, ...result.statements],
|
|
true
|
|
);
|
|
};
|
|
}
|
|
|
|
const argv = yargs
|
|
.options({
|
|
input: {
|
|
type: 'string',
|
|
desc: 'The location of the index.ts file'
|
|
},
|
|
output: {
|
|
type: 'string',
|
|
desc: 'The location for the index.d.ts file'
|
|
}
|
|
})
|
|
.parseSync();
|
|
|
|
if (argv.input && argv.output) {
|
|
console.log('Removing private exports...');
|
|
pruneDts(argv.input, argv.output);
|
|
console.log('Removing unused imports...');
|
|
removeUnusedImports(argv.output).then(() => console.log('Done.'));
|
|
}
|