mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
Size Analysis Tool - Phase 1 (#3588)
* size analysis tool phase 1 * Create shy-bears-heal.md * Delete shy-bears-heal.md * enable toplevel mangling (#3591) * enforce -o command line flag * ignore size-analysis package in changeset * profile the tests * use smaller timeout * update package.json Co-authored-by: Feiyang <[email protected]> Co-authored-by: Feiyang1 <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Feiyang
Feiyang1
parent
77f3316821
commit
6e802da18e
@@ -17,7 +17,9 @@
|
||||
"@firebase/app-types-exp",
|
||||
"@firebase/functions-exp",
|
||||
"@firebase/functions-types-exp",
|
||||
"firebase-exp"
|
||||
"firebase-exp",
|
||||
"@firebase/changelog-generator",
|
||||
"firebase-size-analysis"
|
||||
],
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"onlyUpdatePeerDependentsWhenOutOfRange": true,
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"packages-exp/*",
|
||||
"integration/*"
|
||||
"integration/*",
|
||||
"repo-scripts/*"
|
||||
],
|
||||
"useWorkspaces": true
|
||||
}
|
||||
|
||||
+3
-2
@@ -49,7 +49,8 @@
|
||||
"size-report": "node scripts/report_binary_size.js",
|
||||
"api-report": "lerna run --scope @firebase/*-exp api-report",
|
||||
"docgen:exp": "ts-node-script scripts/exp/docgen.ts",
|
||||
"postinstall": "yarn --cwd repo-scripts/changelog-generator build"
|
||||
"postinstall": "yarn --cwd repo-scripts/changelog-generator build",
|
||||
"sa": "ts-node-script repo-scripts/size-analysis/analysis.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -154,4 +155,4 @@
|
||||
"pre-commit": "node tools/gitHooks/precommit.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: '../../config/.eslintrc.js',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
// to make vscode-eslint work with monorepo
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250
|
||||
tsconfigRootDir: __dirname
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
# Modular Export Binary Size Calculator
|
||||
|
||||
## Command Line Option
|
||||
|
||||
- `--version` Show version number [boolean]
|
||||
- `--inputModule, --im` The name of the module(s) to be analyzed. example: --inputModule "@firebase/functions-exp" "firebase/auth-exp" [array]
|
||||
- `--inputDtsFile, --if` Support for adhoc analysis. requires a path to dts file [string]
|
||||
- `--inputBundleFile, --ib` Support for adhoc analysis. requires a path to a bundle file [string]
|
||||
- `--output, -o` The location where report(s) will be generated, a directory path if module(s) are analyzed; a file path if ad hoc analysis is to be performed [string]
|
||||
- `--help ` Show help [boolean]
|
||||
|
||||
|
||||
|
||||
## Commands To Run The Tool
|
||||
|
||||
### Adhoc Support
|
||||
|
||||
$firebase-js-sdk/repo-scripts/size-analysis `ts-node-script analysis.ts --if <path to dts file> --ib <path to bundle file> -o <path to output FILE>`
|
||||
|
||||
### To Do Analysis On One to Many Firebase Modules
|
||||
|
||||
$firebase-js-sdk/repo-scripts/size-analysis `ts-node-script analysis.ts --im "@firebase/module1-exp" "@firebase/module2-exp" -o <path to output DIRECTORY>`
|
||||
|
||||
### To Do Analysis On All Firebase-Exp Modules
|
||||
|
||||
$firebase-js-sdk/repo-scripts/size-analysis `ts-node-script analysis.ts -o <path to output DIRECTORY>`
|
||||
|
||||
|
||||
@@ -0,0 +1,882 @@
|
||||
/**
|
||||
* @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 tmp from 'tmp';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as rollup from 'rollup';
|
||||
import * as terser from 'terser';
|
||||
import * as ts from 'typescript';
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import { deepCopy } from '@firebase/util';
|
||||
|
||||
const TYPINGS: string = 'typings';
|
||||
const BUNDLE: string = 'esm2017';
|
||||
export const enum ErrorCode {
|
||||
INVALID_FLAG_COMBINATION = 'Invalid command flag combinations!',
|
||||
BUNDLE_FILE_DOES_NOT_EXIST = 'Module does not have a bundle file!',
|
||||
DTS_FILE_DOES_NOT_EXIST = 'Module does not have a dts file!',
|
||||
OUTPUT_DIRECTORY_REQUIRED = 'An output directory is required but a file given!',
|
||||
OUTPUT_FILE_REQUIRED = 'An output file is required but a directory given!',
|
||||
INPUT_FILE_DOES_NOT_EXIST = 'Input file does not exist!',
|
||||
INPUT_DTS_FILE_DOES_NOT_EXIST = 'Input dts file does not exist!',
|
||||
INPUT_BUNDLE_FILE_DOES_NOT_EXIST = 'Input bundle file does not exist!',
|
||||
FILE_PARSING_ERROR = 'Failed to parse js file!'
|
||||
}
|
||||
export interface External {
|
||||
moduleName: string;
|
||||
symbols: string[];
|
||||
}
|
||||
|
||||
/** Contains a list of members by type. */
|
||||
export interface MemberList {
|
||||
classes: string[];
|
||||
functions: string[];
|
||||
variables: string[];
|
||||
enums: string[];
|
||||
externals: External[];
|
||||
}
|
||||
/** Contains the dependencies and the size of their code for a single export. */
|
||||
export interface ExportData {
|
||||
dependencies: MemberList;
|
||||
sizeInBytes: number;
|
||||
sizeInBytesWithExternalDeps: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions builds a simple JS app that only depends on the provided
|
||||
* export. It then uses Rollup to gather all top-level classes and functions
|
||||
* that that the export depends on.
|
||||
*
|
||||
* @param exportName The name of the export to verify
|
||||
* @param jsBundle The file name of the source bundle that contains the export
|
||||
* @return A list of dependencies for the given export
|
||||
*/
|
||||
export async function extractDependencies(
|
||||
exportName: string,
|
||||
jsBundle: string,
|
||||
map: Map<string, string>
|
||||
): Promise<MemberList> {
|
||||
const { dependencies } = await extractDependenciesAndSize(
|
||||
exportName,
|
||||
jsBundle,
|
||||
map
|
||||
);
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for extractDependencies that extracts the dependencies and the size
|
||||
* of the minified build.
|
||||
*/
|
||||
export async function extractDependenciesAndSize(
|
||||
exportName: string,
|
||||
jsBundle: string,
|
||||
map: Map<string, string>
|
||||
): Promise<ExportData> {
|
||||
const input = tmp.fileSync().name + '.js';
|
||||
const externalDepsResolvedOutput = tmp.fileSync().name + '.js';
|
||||
const externalDepsNotResolvedOutput = tmp.fileSync().name + '.js';
|
||||
|
||||
const exportStatement = `export { ${exportName} } from '${path.resolve(
|
||||
jsBundle
|
||||
)}';`;
|
||||
fs.writeFileSync(input, exportStatement);
|
||||
|
||||
// Run Rollup on the JavaScript above to produce a tree-shaken build
|
||||
const externalDepsResolvedBundle = await rollup.rollup({
|
||||
input,
|
||||
plugins: [
|
||||
resolve({
|
||||
mainFields: ['esm2017', 'module', 'main']
|
||||
}),
|
||||
commonjs()
|
||||
]
|
||||
});
|
||||
await externalDepsResolvedBundle.write({
|
||||
file: externalDepsResolvedOutput,
|
||||
format: 'es'
|
||||
});
|
||||
const externalDepsNotResolvedBundle = await rollup.rollup({
|
||||
input,
|
||||
external: id => id.startsWith('@firebase') // exclude all firebase dependencies
|
||||
});
|
||||
await externalDepsNotResolvedBundle.write({
|
||||
file: externalDepsNotResolvedOutput,
|
||||
format: 'es'
|
||||
});
|
||||
const dependencies: MemberList = extractDeclarations(
|
||||
externalDepsNotResolvedOutput,
|
||||
map
|
||||
);
|
||||
const externals: External[] = extractExternalDependencies(
|
||||
externalDepsNotResolvedOutput
|
||||
);
|
||||
dependencies.externals.push(...externals);
|
||||
|
||||
const externalDepsResolvedOutputContent = fs.readFileSync(
|
||||
externalDepsResolvedOutput,
|
||||
'utf-8'
|
||||
);
|
||||
// Extract size of minified build
|
||||
const externalDepsNotResolvedOutputContent = fs.readFileSync(
|
||||
externalDepsNotResolvedOutput,
|
||||
'utf-8'
|
||||
);
|
||||
const externalDepsResolvedOutputContentMinimized: terser.MinifyOutput = terser.minify(
|
||||
externalDepsResolvedOutputContent,
|
||||
{
|
||||
output: {
|
||||
comments: false
|
||||
},
|
||||
mangle: { toplevel: true },
|
||||
compress: false
|
||||
}
|
||||
);
|
||||
const externalDepsNotResolvedOutputContentMinimized: terser.MinifyOutput = terser.minify(
|
||||
externalDepsNotResolvedOutputContent,
|
||||
{
|
||||
output: {
|
||||
comments: false
|
||||
},
|
||||
mangle: { toplevel: true },
|
||||
compress: false
|
||||
}
|
||||
);
|
||||
|
||||
fs.unlinkSync(input);
|
||||
fs.unlinkSync(externalDepsNotResolvedOutput);
|
||||
fs.unlinkSync(externalDepsResolvedOutput);
|
||||
return {
|
||||
dependencies,
|
||||
sizeInBytes: Buffer.byteLength(
|
||||
externalDepsNotResolvedOutputContentMinimized.code!,
|
||||
'utf-8'
|
||||
),
|
||||
sizeInBytesWithExternalDeps: Buffer.byteLength(
|
||||
externalDepsResolvedOutputContentMinimized.code!,
|
||||
'utf-8'
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all function, class and variable declarations using the TypeScript
|
||||
* compiler API.
|
||||
* @param map maps every symbol listed in dts file to its type. eg: aVariable -> variable.
|
||||
* map is null when given filePath is a path to d.ts file.
|
||||
* map is populated when given filePath points to a .js bundle file.
|
||||
*
|
||||
* Examples of Various Type of Exports
|
||||
* FunctionDeclaration: export function aFunc(): string {...};
|
||||
* ClassDeclaration: export class aClass {};
|
||||
* EnumDeclaration: export enum aEnum {};
|
||||
* VariableDeclaration: export let aVariable: string; import * as tmp from 'tmp'; export declare const aVar: tmp.someType.
|
||||
* VariableStatement: export const aVarStatement: string = "string"; export const { a, b } = { a: 'a', b: 'b' };
|
||||
* ExportDeclaration:
|
||||
* named exports: export {foo, bar} from '...'; export {foo as foo1, bar} from '...'; export {LogLevel};
|
||||
* export everything: export * from '...';
|
||||
*/
|
||||
export function extractDeclarations(
|
||||
filePath: string,
|
||||
map?: Map<string, string>
|
||||
): MemberList {
|
||||
const program = ts.createProgram([filePath], { allowJs: true });
|
||||
const checker = program.getTypeChecker();
|
||||
|
||||
const sourceFile = program.getSourceFile(filePath);
|
||||
if (!sourceFile) {
|
||||
throw new Error(`${ErrorCode.FILE_PARSING_ERROR} ${filePath}`);
|
||||
}
|
||||
|
||||
let declarations: MemberList = {
|
||||
functions: [],
|
||||
classes: [],
|
||||
variables: [],
|
||||
enums: [],
|
||||
externals: []
|
||||
};
|
||||
const namespaceImportSet: Set<string> = new Set();
|
||||
// define a map here which is used to handle export statements that have no from clause.
|
||||
// As there is no from clause in such export statements, we retrieve symbol location by parsing the corresponding import
|
||||
// statements. We store the symbol and its defined location as key value pairs in the map.
|
||||
const importSymbolCurrentNameToModuleLocation: Map<
|
||||
string,
|
||||
string
|
||||
> = new Map();
|
||||
const importSymbolCurrentNameToOriginalName: Map<string, string> = new Map(); // key: current name value: original name
|
||||
const importModuleLocationToExportedSymbolsList: Map<
|
||||
string,
|
||||
MemberList
|
||||
> = new Map(); // key: module location, value: a list of all exported symbols of the module
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
if (ts.isFunctionDeclaration(node)) {
|
||||
declarations.functions.push(node.name!.text);
|
||||
} else if (ts.isClassDeclaration(node)) {
|
||||
declarations.classes.push(node.name!.text);
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
declarations.variables.push(node.name!.getText());
|
||||
} else if (ts.isEnumDeclaration(node)) {
|
||||
declarations.enums.push(node.name.escapedText.toString());
|
||||
} else if (ts.isVariableStatement(node)) {
|
||||
const variableDeclarations = node.declarationList.declarations;
|
||||
|
||||
variableDeclarations.forEach(variableDeclaration => {
|
||||
//variableDeclaration.name could be of Identifier type or of BindingPattern type
|
||||
// Identifier Example: export const a: string = "aString";
|
||||
if (ts.isIdentifier(variableDeclaration.name)) {
|
||||
declarations.variables.push(
|
||||
variableDeclaration.name.getText(sourceFile)
|
||||
);
|
||||
}
|
||||
// Binding Pattern Example: export const {a, b} = {a: 1, b: 1};
|
||||
else {
|
||||
variableDeclaration.name.elements.forEach(node => {
|
||||
declarations.variables.push(node.name.getText(sourceFile));
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (ts.isImportDeclaration(node) && node.importClause) {
|
||||
const symbol = checker.getSymbolAtLocation(node.moduleSpecifier);
|
||||
if (symbol && symbol.valueDeclaration) {
|
||||
const importFilePath = symbol.valueDeclaration.getSourceFile().fileName;
|
||||
// import { a, b } from '@firebase/dummy-exp'
|
||||
// import {a as A, b as B} from '@firebase/dummy-exp'
|
||||
if (
|
||||
node.importClause.namedBindings &&
|
||||
ts.isNamedImports(node.importClause.namedBindings)
|
||||
) {
|
||||
node.importClause.namedBindings.elements.forEach(each => {
|
||||
const symbolName: string = each.name.getText(sourceFile); // import symbol current name
|
||||
importSymbolCurrentNameToModuleLocation.set(
|
||||
symbolName,
|
||||
importFilePath
|
||||
);
|
||||
// if imported symbols are renamed, insert an entry to importSymbolCurrentNameToOriginalName Map
|
||||
// with key the current name, value the original name
|
||||
if (each.propertyName) {
|
||||
importSymbolCurrentNameToOriginalName.set(
|
||||
symbolName,
|
||||
each.propertyName.getText(sourceFile)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// import * as fs from 'fs'
|
||||
} else if (
|
||||
node.importClause.namedBindings &&
|
||||
ts.isNamespaceImport(node.importClause.namedBindings)
|
||||
) {
|
||||
const symbolName: string = node.importClause.namedBindings.name.getText(
|
||||
sourceFile
|
||||
);
|
||||
namespaceImportSet.add(symbolName);
|
||||
|
||||
// import a from '@firebase/dummy-exp'
|
||||
} else if (
|
||||
node.importClause.name &&
|
||||
ts.isIdentifier(node.importClause.name)
|
||||
) {
|
||||
const symbolName: string = node.importClause.name.getText(sourceFile);
|
||||
importSymbolCurrentNameToModuleLocation.set(
|
||||
symbolName,
|
||||
importFilePath
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// re-exports handler: handles cases like :
|
||||
// export {LogLevel};
|
||||
// export * from '..';
|
||||
// export {foo, bar} from '..';
|
||||
// export {foo as foo1, bar} from '...';
|
||||
else if (ts.isExportDeclaration(node)) {
|
||||
// this clause handles the export statements that have a from clause (referred to as moduleSpecifier in ts compiler).
|
||||
// examples are "export {foo as foo1, bar} from '...';"
|
||||
// and "export * from '..';"
|
||||
if (node.moduleSpecifier) {
|
||||
if (ts.isStringLiteral(node.moduleSpecifier)) {
|
||||
const reExportsWithFromClause: MemberList = handleExportStatementsWithFromClause(
|
||||
checker,
|
||||
node,
|
||||
node.moduleSpecifier.getText(sourceFile)
|
||||
);
|
||||
// concatenate re-exported MemberList with MemberList of the dts file
|
||||
for (const key of Object.keys(declarations)) {
|
||||
declarations[key].push(...reExportsWithFromClause[key]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// export {LogLevel};
|
||||
// exclusively handles named export statements that has no from clause.
|
||||
handleExportStatementsWithoutFromClause(
|
||||
node,
|
||||
importSymbolCurrentNameToModuleLocation,
|
||||
importSymbolCurrentNameToOriginalName,
|
||||
importModuleLocationToExportedSymbolsList,
|
||||
namespaceImportSet,
|
||||
declarations
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
declarations = dedup(declarations);
|
||||
|
||||
if (map) {
|
||||
declarations = mapSymbolToType(map, declarations);
|
||||
}
|
||||
|
||||
//Sort to ensure stable output
|
||||
Object.values(declarations).map(each => {
|
||||
each.sort();
|
||||
});
|
||||
return declarations;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param node compiler representation of an export statement
|
||||
*
|
||||
* This function exclusively handles export statements that have a from clause. The function uses checker argument to resolve
|
||||
* module name specified in from clause to its actual location. It then retrieves all exported symbols from the module.
|
||||
* If the statement is a named export, the function does an extra step, that is, filtering out the symbols that are not listed
|
||||
* in exportClause.
|
||||
*/
|
||||
function handleExportStatementsWithFromClause(
|
||||
checker: ts.TypeChecker,
|
||||
node: ts.ExportDeclaration,
|
||||
moduleName: string
|
||||
): MemberList {
|
||||
const symbol = checker.getSymbolAtLocation(node.moduleSpecifier);
|
||||
let declarations: MemberList = {
|
||||
functions: [],
|
||||
classes: [],
|
||||
variables: [],
|
||||
enums: [],
|
||||
externals: []
|
||||
};
|
||||
if (symbol && symbol.valueDeclaration) {
|
||||
const reExportFullPath = symbol.valueDeclaration.getSourceFile().fileName;
|
||||
// first step: always retrieve all exported symbols from the source location of the re-export.
|
||||
declarations = extractDeclarations(reExportFullPath);
|
||||
// if it's a named export statement, filter the MemberList to keep only those listed in exportClause.
|
||||
// named exports: eg: export {foo, bar} from '...'; and export {foo as foo1, bar} from '...';
|
||||
declarations = extractSymbolsFromNamedExportStatement(node, declarations);
|
||||
}
|
||||
// if the module name in the from clause cant be resolved to actual module location,
|
||||
// just extract symbols listed in the exportClause for named exports, put them in variables first, as
|
||||
// they will be categorized later using map argument.
|
||||
else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
||||
node.exportClause.elements.forEach(exportSpecifier => {
|
||||
declarations.variables.push(exportSpecifier.name.escapedText.toString());
|
||||
});
|
||||
}
|
||||
// handles the case when exporting * from a module whose location can't be resolved
|
||||
else {
|
||||
console.log(
|
||||
`The public API extraction of ${moduleName} is not complete, because it re-exports from ${moduleName} using * export but we couldn't resolve ${moduleName}`
|
||||
);
|
||||
}
|
||||
|
||||
return declarations;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node compiler representation of a named export statement
|
||||
* @param exportsFullList a list of all exported symbols retrieved from the location given in the export statement.
|
||||
*
|
||||
* This function filters on exportsFullList and keeps only those symbols that are listed in the given named export statement.
|
||||
*/
|
||||
function extractSymbolsFromNamedExportStatement(
|
||||
node: ts.ExportDeclaration,
|
||||
exportsFullList: MemberList
|
||||
): MemberList {
|
||||
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
||||
const actualExports: string[] = [];
|
||||
node.exportClause.elements.forEach(exportSpecifier => {
|
||||
const reExportedSymbol: string = extractOriginalSymbolName(
|
||||
exportSpecifier
|
||||
);
|
||||
// eg: export {foo as foo1 } from '...';
|
||||
// if export is renamed, replace with new name
|
||||
// reExportedSymbol: stores the original symbol name
|
||||
// exportSpecifier.name: stores the renamed symbol name
|
||||
if (isExportRenamed(exportSpecifier)) {
|
||||
actualExports.push(exportSpecifier.name.escapedText.toString());
|
||||
// reExportsMember stores all re-exported symbols in its orignal name. However, these re-exported symbols
|
||||
// could be renamed by the re-export. We want to show the renamed name of the symbols in the final analysis report.
|
||||
// Therefore, replaceAll simply replaces the original name of the symbol with the new name defined in re-export.
|
||||
replaceAll(
|
||||
exportsFullList,
|
||||
reExportedSymbol,
|
||||
exportSpecifier.name.escapedText.toString()
|
||||
);
|
||||
} else {
|
||||
actualExports.push(reExportedSymbol);
|
||||
}
|
||||
});
|
||||
// for named exports: requires a filter step which keeps only the symbols listed in the export statement.
|
||||
filterAllBy(exportsFullList, actualExports);
|
||||
}
|
||||
return exportsFullList;
|
||||
}
|
||||
/**
|
||||
* @param node compiler representation of a named export statement
|
||||
* @param importSymbolCurrentNameToModuleLocation a map with imported symbol current name as key and the resolved module location as value. (map is populated by parsing import statements)
|
||||
* @param importSymbolCurrentNameToOriginalName as imported symbols can be renamed, this map stores imported symbols current name and original name as key value pairs.
|
||||
* @param importModuleLocationToExportedSymbolsList a map that maps module location to a list of its exported symbols.
|
||||
* @param namespaceImportSymbolSet a set of namespace import symbols.
|
||||
* @param parentDeclarations a list of exported symbols extracted from the module so far
|
||||
* This function exclusively handles named export statements that has no from clause, i.e: statements like export {LogLevel};
|
||||
* first case: namespace export
|
||||
* example: import * as fs from 'fs'; export {fs};
|
||||
* The function checks if namespaceImportSymbolSet has a namespace import symbol that of the same name, append the symbol to declarations.variables if exists.
|
||||
*
|
||||
* second case: import then export
|
||||
* example: import {a} from '...'; export {a}
|
||||
* The function retrieves the location where the exported symbol is defined from the corresponding import statements.
|
||||
*
|
||||
* third case: declare first then export
|
||||
* examples: declare const apps: Map<string, number>; export { apps };
|
||||
* function foo(){} ; export {foo as bar};
|
||||
* The function parses export clause of the statement and replaces symbol with its current name (if the symbol is renamed) from the declaration argument.
|
||||
*/
|
||||
function handleExportStatementsWithoutFromClause(
|
||||
node: ts.ExportDeclaration,
|
||||
importSymbolCurrentNameToModuleLocation: Map<string, string>,
|
||||
importSymbolCurrentNameToOriginalName: Map<string, string>,
|
||||
importModuleLocationToExportedSymbolsList: Map<string, MemberList>,
|
||||
namespaceImportSymbolSet: Set<string>,
|
||||
parentDeclarations: MemberList
|
||||
): void {
|
||||
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
||||
node.exportClause.elements.forEach(exportSpecifier => {
|
||||
// export symbol could be renamed, we retrieve both its current/renamed name and original name
|
||||
const exportSymbolCurrentName = exportSpecifier.name.escapedText.toString();
|
||||
const exportSymbolOriginalName = extractOriginalSymbolName(
|
||||
exportSpecifier
|
||||
);
|
||||
// import * as fs from 'fs'; export {fs};
|
||||
if (namespaceImportSymbolSet.has(exportSymbolOriginalName)) {
|
||||
parentDeclarations.variables.push(exportSymbolOriginalName);
|
||||
replaceAll(
|
||||
parentDeclarations,
|
||||
exportSymbolOriginalName,
|
||||
exportSymbolCurrentName
|
||||
);
|
||||
}
|
||||
// handles import then exports
|
||||
// import {a as A , b as B} from '...'
|
||||
// export {A as AA , B as BB };
|
||||
else if (
|
||||
importSymbolCurrentNameToModuleLocation.has(exportSymbolOriginalName)
|
||||
) {
|
||||
const moduleLocation: string = importSymbolCurrentNameToModuleLocation.get(
|
||||
exportSymbolOriginalName
|
||||
);
|
||||
let reExportedSymbols: MemberList = null;
|
||||
if (importModuleLocationToExportedSymbolsList.has(moduleLocation)) {
|
||||
reExportedSymbols = deepCopy(
|
||||
importModuleLocationToExportedSymbolsList.get(moduleLocation)
|
||||
);
|
||||
} else {
|
||||
reExportedSymbols = extractDeclarations(
|
||||
importSymbolCurrentNameToModuleLocation.get(
|
||||
exportSymbolOriginalName
|
||||
)
|
||||
);
|
||||
importModuleLocationToExportedSymbolsList.set(
|
||||
moduleLocation,
|
||||
deepCopy(reExportedSymbols)
|
||||
);
|
||||
}
|
||||
|
||||
let nameToBeReplaced = exportSymbolOriginalName;
|
||||
// if current exported symbol is renamed in import clause. then we retrieve its original name from
|
||||
// importSymbolCurrentNameToOriginalName map
|
||||
if (
|
||||
importSymbolCurrentNameToOriginalName.has(exportSymbolOriginalName)
|
||||
) {
|
||||
nameToBeReplaced = importSymbolCurrentNameToOriginalName.get(
|
||||
exportSymbolOriginalName
|
||||
);
|
||||
}
|
||||
|
||||
filterAllBy(reExportedSymbols, [nameToBeReplaced]);
|
||||
// replace with new name
|
||||
replaceAll(
|
||||
reExportedSymbols,
|
||||
nameToBeReplaced,
|
||||
exportSymbolCurrentName
|
||||
);
|
||||
|
||||
// concatenate re-exported MemberList with MemberList of the dts file
|
||||
for (const key of Object.keys(parentDeclarations)) {
|
||||
parentDeclarations[key].push(...reExportedSymbols[key]);
|
||||
}
|
||||
}
|
||||
// handles declare first then export
|
||||
// declare const apps: Map<string, number>;
|
||||
// export { apps as apps1};
|
||||
// function a() {};
|
||||
// export {a};
|
||||
else {
|
||||
if (isExportRenamed(exportSpecifier)) {
|
||||
replaceAll(
|
||||
parentDeclarations,
|
||||
exportSymbolOriginalName,
|
||||
exportSymbolCurrentName
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To Make sure symbols of every category are unique.
|
||||
*/
|
||||
export function dedup(memberList: MemberList): MemberList {
|
||||
for (const key of Object.keys(memberList)) {
|
||||
const set: Set<string> = new Set(memberList[key]);
|
||||
memberList[key] = Array.from(set);
|
||||
}
|
||||
return memberList;
|
||||
}
|
||||
|
||||
export function mapSymbolToType(
|
||||
map: Map<string, string>,
|
||||
memberList: MemberList
|
||||
): MemberList {
|
||||
const newMemberList: MemberList = {
|
||||
functions: [],
|
||||
classes: [],
|
||||
variables: [],
|
||||
enums: [],
|
||||
externals: []
|
||||
};
|
||||
|
||||
for (const key of Object.keys(memberList)) {
|
||||
memberList[key].forEach(element => {
|
||||
if (map.has(element)) {
|
||||
newMemberList[map.get(element)].push(element);
|
||||
} else {
|
||||
newMemberList[key].push(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
return newMemberList;
|
||||
}
|
||||
|
||||
function extractOriginalSymbolName(
|
||||
exportSpecifier: ts.ExportSpecifier
|
||||
): string {
|
||||
// if symbol is renamed, then exportSpecifier.propertyName is not null and stores the orignal name, exportSpecifier.name stores the renamed name.
|
||||
// if symbol is not renamed, then exportSpecifier.propertyName is null, exportSpecifier.name stores the orignal name.
|
||||
if (exportSpecifier.propertyName) {
|
||||
return exportSpecifier.propertyName.escapedText.toString();
|
||||
}
|
||||
return exportSpecifier.name.escapedText.toString();
|
||||
}
|
||||
|
||||
function filterAllBy(memberList: MemberList, keep: string[]): void {
|
||||
for (const key of Object.keys(memberList)) {
|
||||
memberList[key] = memberList[key].filter(each => keep.includes(each));
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceAll(
|
||||
memberList: MemberList,
|
||||
original: string,
|
||||
current: string
|
||||
): void {
|
||||
for (const key of Object.keys(memberList)) {
|
||||
memberList[key] = replaceWith(memberList[key], original, current);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceWith(
|
||||
arr: string[],
|
||||
original: string,
|
||||
current: string
|
||||
): string[] {
|
||||
const rv: string[] = [];
|
||||
for (const each of arr) {
|
||||
if (each.localeCompare(original) === 0) {
|
||||
rv.push(current);
|
||||
} else {
|
||||
rv.push(each);
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
function isExportRenamed(exportSpecifier: ts.ExportSpecifier): boolean {
|
||||
return exportSpecifier.propertyName != null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This functions writes generated json report(s) to a file
|
||||
*/
|
||||
export function writeReportToFile(report: string, outputFile: string): void {
|
||||
if (fs.existsSync(outputFile) && !fs.lstatSync(outputFile).isFile()) {
|
||||
throw new Error(ErrorCode.OUTPUT_FILE_REQUIRED);
|
||||
}
|
||||
const directoryPath = path.dirname(outputFile);
|
||||
//for output file path like ./dir/dir1/dir2/file, we need to make sure parent dirs exist.
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
fs.mkdirSync(directoryPath, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(outputFile, report);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* This functions writes generated json report(s) to a file of given directory
|
||||
*/
|
||||
export function writeReportToDirectory(
|
||||
report: string,
|
||||
fileName: string,
|
||||
directoryPath: string
|
||||
): void {
|
||||
if (
|
||||
fs.existsSync(directoryPath) &&
|
||||
!fs.lstatSync(directoryPath).isDirectory()
|
||||
) {
|
||||
throw new Error(ErrorCode.OUTPUT_DIRECTORY_REQUIRED);
|
||||
}
|
||||
writeReportToFile(report, `${directoryPath}/${fileName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function extract unresolved external module symbols from bundle file import statements.
|
||||
*
|
||||
*/
|
||||
export function extractExternalDependencies(
|
||||
minimizedBundleFile: string
|
||||
): External[] {
|
||||
const program = ts.createProgram([minimizedBundleFile], { allowJs: true });
|
||||
|
||||
const sourceFile = program.getSourceFile(minimizedBundleFile);
|
||||
if (!sourceFile) {
|
||||
throw new Error(`${ErrorCode.FILE_PARSING_ERROR} ${minimizedBundleFile}`);
|
||||
}
|
||||
|
||||
const externalsMap: Map<string, string[]> = new Map();
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
if (ts.isImportDeclaration(node) && node.importClause) {
|
||||
const moduleName: string = node.moduleSpecifier.getText(sourceFile);
|
||||
if (!externalsMap.has(moduleName)) {
|
||||
externalsMap.set(moduleName, []);
|
||||
}
|
||||
|
||||
//import {a, b } from '@firebase/dummy-exp';
|
||||
// import {a as c, b } from '@firebase/dummy-exp';
|
||||
if (
|
||||
node.importClause.namedBindings &&
|
||||
ts.isNamedImports(node.importClause.namedBindings)
|
||||
) {
|
||||
node.importClause.namedBindings.elements.forEach(each => {
|
||||
// if imported symbol is renamed, we want its original name which is stored in propertyName
|
||||
if (each.propertyName) {
|
||||
externalsMap
|
||||
.get(moduleName)
|
||||
.push(each.propertyName.getText(sourceFile));
|
||||
} else {
|
||||
externalsMap.get(moduleName).push(each.name.getText(sourceFile));
|
||||
}
|
||||
});
|
||||
// import * as fs from 'fs'
|
||||
} else if (
|
||||
node.importClause.namedBindings &&
|
||||
ts.isNamespaceImport(node.importClause.namedBindings)
|
||||
) {
|
||||
externalsMap.get(moduleName).push('*');
|
||||
// import a from '@firebase/dummy-exp'
|
||||
} else if (
|
||||
node.importClause.name &&
|
||||
ts.isIdentifier(node.importClause.name)
|
||||
) {
|
||||
externalsMap.get(moduleName).push('default export');
|
||||
}
|
||||
}
|
||||
});
|
||||
const externals: External[] = [];
|
||||
|
||||
externalsMap.forEach((value, key) => {
|
||||
const external: External = {
|
||||
moduleName: key,
|
||||
symbols: value
|
||||
};
|
||||
externals.push(external);
|
||||
});
|
||||
return externals;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function generates a binary size report for the given module specified by the moduleLocation argument.
|
||||
* @param moduleLocation a path to location of a firebase module
|
||||
* @param outputDirectory a path to a directory where the reports will be written under.
|
||||
* @param writeFiles when true, will write reports to designated directory specified by outputDirectory.
|
||||
*/
|
||||
export async function generateReportForModule(
|
||||
moduleLocation: string,
|
||||
outputDirectory: string,
|
||||
writeFiles: boolean
|
||||
): Promise<string> {
|
||||
const packageJsonPath = `${moduleLocation}/package.json`;
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return;
|
||||
}
|
||||
const packageJson = require(packageJsonPath);
|
||||
// to exclude <modules>-types modules
|
||||
if (packageJson[TYPINGS]) {
|
||||
const dtsFile = `${moduleLocation}/${packageJson[TYPINGS]}`;
|
||||
if (!packageJson[BUNDLE]) {
|
||||
throw new Error(ErrorCode.BUNDLE_FILE_DOES_NOT_EXIST);
|
||||
}
|
||||
const bundleFile = `${moduleLocation}/${packageJson[BUNDLE]}`;
|
||||
const json = await generateReport(dtsFile, bundleFile);
|
||||
const fileName = `${path.basename(packageJson.name)}-dependency.json`;
|
||||
if (writeFiles) {
|
||||
writeReportToDirectory(json, fileName, path.resolve(outputDirectory));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* This function creates a map from a MemberList object which maps symbol names (key) listed
|
||||
* to its type (value)
|
||||
*/
|
||||
export function buildMap(api: MemberList): Map<string, string> {
|
||||
const map: Map<string, string> = new Map();
|
||||
for (const type of Object.keys(api)) {
|
||||
api[type].forEach(element => {
|
||||
map.set(element, type);
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* A recursive function that locates and generates reports for sub-modules
|
||||
*/
|
||||
function traverseDirs(
|
||||
moduleLocation: string,
|
||||
outputDirectory: string,
|
||||
writeFiles: boolean,
|
||||
executor,
|
||||
level: number,
|
||||
levelLimit: number
|
||||
): void {
|
||||
if (level > levelLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor(moduleLocation, outputDirectory, writeFiles);
|
||||
|
||||
for (const name of fs.readdirSync(moduleLocation)) {
|
||||
const p = `${moduleLocation}/${name}`;
|
||||
|
||||
if (fs.lstatSync(p).isDirectory()) {
|
||||
traverseDirs(
|
||||
p,
|
||||
outputDirectory,
|
||||
writeFiles,
|
||||
executor,
|
||||
level + 1,
|
||||
levelLimit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This functions generates the final json report for the module.
|
||||
* @param publicApi all symbols extracted from the input dts file.
|
||||
* @param jsFile a bundle file generated by rollup according to the input dts file.
|
||||
* @param map maps every symbol listed in publicApi to its type. eg: aVariable -> variable.
|
||||
*/
|
||||
export async function buildJsonReport(
|
||||
publicApi: MemberList,
|
||||
jsFile: string,
|
||||
map: Map<string, string>
|
||||
): Promise<string> {
|
||||
const result: { [key: string]: ExportData } = {};
|
||||
for (const exp of publicApi.classes) {
|
||||
result[exp] = await extractDependenciesAndSize(exp, jsFile, map);
|
||||
}
|
||||
for (const exp of publicApi.functions) {
|
||||
result[exp] = await extractDependenciesAndSize(exp, jsFile, map);
|
||||
}
|
||||
for (const exp of publicApi.variables) {
|
||||
result[exp] = await extractDependenciesAndSize(exp, jsFile, map);
|
||||
}
|
||||
|
||||
for (const exp of publicApi.enums) {
|
||||
result[exp] = await extractDependenciesAndSize(exp, jsFile, map);
|
||||
}
|
||||
return JSON.stringify(result, null, 4);
|
||||
}
|
||||
|
||||
export async function generateReport(
|
||||
dtsFile: string,
|
||||
bundleFile: string
|
||||
): Promise<string> {
|
||||
const resolvedDtsFile = path.resolve(dtsFile);
|
||||
const resolvedBundleFile = path.resolve(bundleFile);
|
||||
if (!fs.existsSync(resolvedDtsFile)) {
|
||||
throw new Error(ErrorCode.INPUT_DTS_FILE_DOES_NOT_EXIST);
|
||||
}
|
||||
if (!fs.existsSync(resolvedBundleFile)) {
|
||||
throw new Error(ErrorCode.INPUT_BUNDLE_FILE_DOES_NOT_EXIST);
|
||||
}
|
||||
const publicAPI = extractDeclarations(resolvedDtsFile);
|
||||
const map: Map<string, string> = buildMap(publicAPI);
|
||||
return buildJsonReport(publicAPI, bundleFile, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function recursively generates a binary size report for every module listed in moduleLocations array.
|
||||
*
|
||||
* @param moduleLocations an array of strings where each is a path to location of a firebase module
|
||||
* @param outputDirectory a path to a directory where the reports will be written under.
|
||||
* @param writeFiles when true, will write reports to designated directory specified by outputDirectory.
|
||||
*
|
||||
*
|
||||
*/
|
||||
export function generateReportForModules(
|
||||
moduleLocations: string[],
|
||||
outputDirectory: string,
|
||||
writeFiles: boolean
|
||||
): void {
|
||||
for (const moduleLocation of moduleLocations) {
|
||||
// we traverse the dir in order to include binaries for submodules, e.g. @firebase/firestore/memory
|
||||
// Currently we only traverse 1 level deep because we don't have any submodule deeper than that.
|
||||
traverseDirs(
|
||||
moduleLocation,
|
||||
outputDirectory,
|
||||
writeFiles,
|
||||
generateReportForModule,
|
||||
0,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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 { resolve } from 'path';
|
||||
import {
|
||||
generateReport,
|
||||
generateReportForModules,
|
||||
writeReportToFile,
|
||||
ErrorCode
|
||||
} from './analysis-helper';
|
||||
import { mapWorkspaceToPackages } from '../../scripts/release/utils/workspace';
|
||||
import { projectRoot } from '../../scripts/utils';
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
/**
|
||||
* Support Command Line Options
|
||||
* -- inputModule (optional) : can be left unspecified which results in running analysis on all exp modules.
|
||||
* can specify one to many module names seperated by space.
|
||||
* eg: --inputModule "@firebase/functions-exp" "firebase/auth-exp"
|
||||
*
|
||||
* -- inputDtsFile (optional) : adhoc support. Specify a path to dts file. Must enable -- inputBundleFile if this flag is specified.
|
||||
*
|
||||
* -- inputBundleFile (optional): adhoc support. Specify a path to bundle file. Must enable -- inputDtsFile if this flag is specified.
|
||||
*
|
||||
* --output (required): output directory or file where reports will be generated.
|
||||
* specify a directory if module(s) are analyzed
|
||||
* specify a file path if ad hoc analysis is to be performed
|
||||
*
|
||||
*/
|
||||
const argv = yargs
|
||||
.options({
|
||||
inputModule: {
|
||||
type: 'array',
|
||||
alias: 'im',
|
||||
desc:
|
||||
'The name of the module(s) to be analyzed. example: --inputModule "@firebase/functions-exp" "firebase/auth-exp"'
|
||||
},
|
||||
inputDtsFile: {
|
||||
type: 'string',
|
||||
alias: 'if',
|
||||
desc: 'support for adhoc analysis. requires a path to dts file'
|
||||
},
|
||||
inputBundleFile: {
|
||||
type: 'string',
|
||||
alias: 'ib',
|
||||
desc: 'support for adhoc analysis. requires a path to a bundle file'
|
||||
},
|
||||
output: {
|
||||
type: 'string',
|
||||
alias: 'o',
|
||||
required: true,
|
||||
desc:
|
||||
'The location where report(s) will be generated, a directory path if module(s) are analyzed; a file path if ad hoc analysis is to be performed'
|
||||
}
|
||||
})
|
||||
.help().argv;
|
||||
|
||||
/**
|
||||
* Entry Point of the Tool.
|
||||
* The function first checks if it's an adhoc run (by checking whether --inputDtsFile and --inputBundle are both enabled)
|
||||
* The function then checks whether --inputModule flag is specified; Run analysis on all modules if not, run analysis on selected modules if enabled.
|
||||
* Throw INVALID_FLAG_COMBINATION error if neither case fulfill.
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
// check if it's an adhoc run
|
||||
// adhoc run report can only be redirected to files
|
||||
if (argv.inputDtsFile && argv.inputBundleFile && argv.output) {
|
||||
const jsonReport = await generateReport(
|
||||
argv.inputDtsFile,
|
||||
argv.inputBundleFile
|
||||
);
|
||||
writeReportToFile(jsonReport, resolve(argv.output));
|
||||
} else if (!argv.inputDtsFile && !argv.inputBundleFile) {
|
||||
// retrieve All Module Names
|
||||
let allModulesLocation = await mapWorkspaceToPackages([
|
||||
`${projectRoot}/packages-exp/*`
|
||||
]);
|
||||
if (argv.inputModule) {
|
||||
allModulesLocation = allModulesLocation.filter(path => {
|
||||
const json = require(`${path}/package.json`);
|
||||
return argv.inputModule.includes(json.name);
|
||||
});
|
||||
}
|
||||
let writeFiles: boolean = false;
|
||||
if (argv.output) {
|
||||
writeFiles = true;
|
||||
}
|
||||
generateReportForModules(allModulesLocation, argv.output, writeFiles);
|
||||
} else {
|
||||
throw new Error(ErrorCode.INVALID_FLAG_COMBINATION);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "firebase-size-analysis",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "A template package for new firebase packages",
|
||||
"author": "Firebase <[email protected]> (https://firebase.google.com/)",
|
||||
"main": "dist/index.cjs.js",
|
||||
"esm2017": "dist/index.esm2017.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
|
||||
"lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
|
||||
"pretest": "tsc -p test/test-inputs && rollup -c",
|
||||
"test": "yarn type-check && TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha **/*.test.ts --config ../../config/mocharc.node.js --timeout 60000",
|
||||
"test:ci": "node ../../scripts/run_tests_in_ci.js",
|
||||
"type-check": "tsc -p . --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "3.9.7",
|
||||
"rollup": "2.23.0",
|
||||
"rollup-plugin-json": "4.0.0",
|
||||
"rollup-plugin-replace": "2.2.0",
|
||||
"rollup-plugin-typescript2": "0.27.1",
|
||||
"rollup-plugin-commonjs": "10.1.0",
|
||||
"rollup-plugin-node-resolve": "5.2.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@firebase/logger": "0.2.6",
|
||||
"@firebase/util": "0.3.0"
|
||||
},
|
||||
"repository": {
|
||||
"directory": "repo-scripts/size-analysis",
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/firebase-js-sdk.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/firebase/firebase-js-sdk/issues"
|
||||
},
|
||||
"nyc": {
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"reportDir": "./coverage/node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 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 typescriptPlugin from 'rollup-plugin-typescript2';
|
||||
import typescript from 'typescript';
|
||||
import pkg from './package.json';
|
||||
import json from 'rollup-plugin-json';
|
||||
|
||||
const deps = Object.keys(
|
||||
Object.assign({}, pkg.peerDependencies, pkg.dependencies)
|
||||
);
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'test/test-inputs/subsetExports.ts',
|
||||
output: [
|
||||
{
|
||||
file: 'test/test-inputs/dist/subsetExportsBundle.js',
|
||||
format: 'es',
|
||||
sourcemap: false
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
typescriptPlugin({
|
||||
typescript,
|
||||
tsconfigOverride: {
|
||||
compilerOptions: {
|
||||
target: 'es2017',
|
||||
module: 'es2015'
|
||||
}
|
||||
}
|
||||
}),
|
||||
json({
|
||||
preferConst: true
|
||||
})
|
||||
],
|
||||
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,494 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 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 { expect } from 'chai';
|
||||
|
||||
import {
|
||||
extractDeclarations,
|
||||
MemberList,
|
||||
dedup,
|
||||
mapSymbolToType,
|
||||
replaceAll,
|
||||
writeReportToFile,
|
||||
ErrorCode,
|
||||
writeReportToDirectory,
|
||||
External,
|
||||
extractExternalDependencies,
|
||||
buildMap
|
||||
} from '../analysis-helper';
|
||||
|
||||
import {
|
||||
getTestModuleDtsFilePath,
|
||||
getAssortedImportsJsFilePath,
|
||||
getSubsetExportsBundleFilePath
|
||||
} from './utils';
|
||||
import * as fs from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
describe('extractDeclarations on .d.ts file', () => {
|
||||
let testModuleDtsFile: string;
|
||||
let extractedDeclarations: MemberList;
|
||||
before(() => {
|
||||
const start = Date.now();
|
||||
testModuleDtsFile = getTestModuleDtsFilePath();
|
||||
extractedDeclarations = extractDeclarations(testModuleDtsFile);
|
||||
console.log('extractDeclarations on .d.ts file took ', Date.now() - start);
|
||||
});
|
||||
// export {tar as tarr, tar1 as tarr1} from '..'
|
||||
it('test export rename', () => {
|
||||
expect(extractedDeclarations.functions).to.include.members([
|
||||
'tarr',
|
||||
'tarr1'
|
||||
]);
|
||||
});
|
||||
// function foo() { }
|
||||
// export { foo as foo2 };
|
||||
it('test declare then export', () => {
|
||||
expect(extractedDeclarations.functions).to.include.members(['foo2']);
|
||||
expect(extractedDeclarations.classes).to.include.members(['Foo1']);
|
||||
});
|
||||
|
||||
it('test basic variable extractions', () => {
|
||||
expect(extractedDeclarations.variables).to.include.members([
|
||||
'basicVarDeclarationExport',
|
||||
'basicVarStatementExport',
|
||||
'reExportVarStatmentExport'
|
||||
]);
|
||||
});
|
||||
it('test re-exported variable extractions from same module - named re-exports', () => {
|
||||
expect(extractedDeclarations.variables).to.include.members([
|
||||
'basicVarDeclarationExportFar',
|
||||
'basicVarStatementExportFar',
|
||||
'reExportVarStatmentExportFar'
|
||||
]);
|
||||
});
|
||||
it('test re-exported variable extractions from same module - * re-exports', () => {
|
||||
expect(extractedDeclarations.variables).to.include.members([
|
||||
'basicVarDeclarationExportBar',
|
||||
'basicVarStatementExportBar',
|
||||
'reExportVarStatmentExportBar'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test basic function extractions', () => {
|
||||
expect(extractedDeclarations.functions).to.include.members([
|
||||
'basicFuncExportNoDependencies',
|
||||
'basicFuncExportVarDependencies',
|
||||
'basicFuncExportFuncDependencies',
|
||||
'basicFuncExportEnumDependencies',
|
||||
'basicFuncExternalDependencies'
|
||||
]);
|
||||
});
|
||||
it('test basic function de-duplication ', () => {
|
||||
expect(extractedDeclarations.functions).include.members([
|
||||
'basicUniqueFunc'
|
||||
]);
|
||||
|
||||
expect(
|
||||
extractedDeclarations.functions.filter(
|
||||
each => each.localeCompare('basicUniqueFunc') === 0
|
||||
).length
|
||||
).to.equal(1);
|
||||
});
|
||||
|
||||
it('test re-exported function extractions from same module - named re-exports', () => {
|
||||
expect(extractedDeclarations.functions).to.include.members([
|
||||
'basicFuncExportNoDependenciesFar',
|
||||
'basicFuncExportVarDependenciesFar',
|
||||
'basicFuncExportFuncDependenciesFar',
|
||||
'basicFuncExportEnumDependenciesFar',
|
||||
'basicFuncExternalDependenciesFar'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test re-exported function extractions from same module - * re-exports', () => {
|
||||
expect(extractedDeclarations.functions).to.include.members([
|
||||
'basicFuncExportNoDependenciesBar',
|
||||
'basicFuncExportVarDependenciesBar',
|
||||
'basicFuncExportFuncDependenciesBar',
|
||||
'basicFuncExportEnumDependenciesBar',
|
||||
'basicFuncExternalDependenciesBar'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test re-exported function de-duplication from same module ', () => {
|
||||
expect(extractedDeclarations.functions).include.members([
|
||||
'basicUniqueFuncFar'
|
||||
]);
|
||||
|
||||
expect(
|
||||
extractedDeclarations.functions.filter(
|
||||
each => each.localeCompare('basicUniqueFuncFar') === 0
|
||||
).length
|
||||
).to.equal(1);
|
||||
});
|
||||
|
||||
it('test basic class extractions', () => {
|
||||
expect(extractedDeclarations.classes).to.include.members([
|
||||
'BasicClassExport'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test re-exported class extractions from same module - named re-exports', () => {
|
||||
expect(extractedDeclarations.classes).to.include.members([
|
||||
'BasicClassExportFar'
|
||||
]);
|
||||
});
|
||||
it('test re-exported class extractions from same module - * re-exports', () => {
|
||||
expect(extractedDeclarations.classes).to.include.members([
|
||||
'BasicClassExportBar'
|
||||
]);
|
||||
});
|
||||
|
||||
it('test basic enum extractions', () => {
|
||||
expect(extractedDeclarations.enums).to.include.members(['BasicEnumExport']);
|
||||
});
|
||||
|
||||
it('test re-exported enum extractions from same module - named re-exports', () => {
|
||||
expect(extractedDeclarations.enums).to.include.members([
|
||||
'BasicEnumExportFar'
|
||||
]);
|
||||
});
|
||||
it('test re-exported enum extractions from same module - * re-exports', () => {
|
||||
expect(extractedDeclarations.enums).to.include.members([
|
||||
'BasicEnumExportBar'
|
||||
]);
|
||||
});
|
||||
// import {LogLevel as LogLevel1} from '@firebase/logger';
|
||||
// export {LogLevel1 as LogLevel2};
|
||||
it('test renamed import then renamed export', () => {
|
||||
expect(extractedDeclarations.enums).to.include.members(['LogLevel2']);
|
||||
});
|
||||
|
||||
//import { Logger } from "@firebase/logger";
|
||||
// export { Logger as Logger1 };
|
||||
it('test import then renamed export', () => {
|
||||
expect(extractedDeclarations.classes).to.include.members(['Logger1']);
|
||||
});
|
||||
|
||||
//import { setLogLevel } from "@firebase/logger";
|
||||
// export { setLogLevel };
|
||||
it('test import then export', () => {
|
||||
expect(extractedDeclarations.functions).to.include.members(['setLogLevel']);
|
||||
});
|
||||
|
||||
// import * as fs from 'fs'
|
||||
// import * as tmp from 'tmp'
|
||||
// export declare const aVar: tmp.FileOptions;
|
||||
// export { fs as fs1 };
|
||||
it('test namespace export', () => {
|
||||
expect(extractedDeclarations.variables).to.include.members(['fs1']);
|
||||
expect(extractedDeclarations.variables).to.not.include.members(['tmp']);
|
||||
expect(extractedDeclarations.variables).to.include.members(['aVar']);
|
||||
});
|
||||
});
|
||||
describe('extractDeclarations on js bundle file', () => {
|
||||
let subsetExportsBundleFile: string;
|
||||
let extractedDeclarations: MemberList;
|
||||
before(() => {
|
||||
const start = Date.now();
|
||||
const testModuleDtsFile: string = getTestModuleDtsFilePath();
|
||||
const map: Map<string, string> = buildMap(
|
||||
extractDeclarations(testModuleDtsFile)
|
||||
);
|
||||
subsetExportsBundleFile = getSubsetExportsBundleFilePath();
|
||||
extractedDeclarations = extractDeclarations(subsetExportsBundleFile, map);
|
||||
console.log(
|
||||
'extractDeclarations on js bundle file took ',
|
||||
Date.now() - start
|
||||
);
|
||||
});
|
||||
it('test variable extractions', () => {
|
||||
const variablesArray = ['aVar', 'fs1'];
|
||||
variablesArray.sort();
|
||||
expect(extractedDeclarations.variables).to.have.members(variablesArray);
|
||||
});
|
||||
|
||||
it('test functions extractions', () => {
|
||||
const functionsArray = [
|
||||
'tar',
|
||||
'tarr1',
|
||||
'basicFuncExportEnumDependencies',
|
||||
'd1',
|
||||
'd2',
|
||||
'd3',
|
||||
'basicFuncExportFuncDependenciesBar'
|
||||
];
|
||||
functionsArray.sort();
|
||||
expect(extractedDeclarations.functions).to.have.members(functionsArray);
|
||||
});
|
||||
|
||||
it('test enums extractions', () => {
|
||||
const enumsArray = [
|
||||
'BasicEnumExport',
|
||||
'LogLevel2',
|
||||
'BasicEnumExportBar',
|
||||
'BasicEnumExportFar'
|
||||
];
|
||||
enumsArray.sort();
|
||||
expect(extractedDeclarations.enums).to.have.members(enumsArray);
|
||||
});
|
||||
|
||||
it('test classes extractions', () => {
|
||||
const classesArray = ['Logger1'];
|
||||
classesArray.sort();
|
||||
expect(extractedDeclarations.classes).to.have.members(classesArray);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test dedup helper function', () => {
|
||||
it('test dedup with non-empty entries', () => {
|
||||
let memberList: MemberList = {
|
||||
functions: ['aFunc', 'aFunc', 'bFunc', 'cFunc'],
|
||||
classes: ['aClass', 'bClass', 'aClass', 'cClass'],
|
||||
variables: ['aVar', 'bVar', 'cVar', 'aVar'],
|
||||
enums: ['aEnum', 'bEnum', 'cEnum', 'dEnum'],
|
||||
externals: []
|
||||
};
|
||||
memberList = dedup(memberList);
|
||||
|
||||
expect(memberList.functions).to.have.length(3);
|
||||
expect(memberList.classes).to.have.length(3);
|
||||
expect(memberList.variables).to.have.length(3);
|
||||
expect(memberList.enums).to.have.length(4);
|
||||
expect(
|
||||
memberList.functions.filter(each => each.localeCompare('aFunc') === 0)
|
||||
.length
|
||||
).to.equal(1);
|
||||
expect(
|
||||
memberList.classes.filter(each => each.localeCompare('aClass') === 0)
|
||||
.length
|
||||
).to.equal(1);
|
||||
expect(
|
||||
memberList.variables.filter(each => each.localeCompare('aVar') === 0)
|
||||
.length
|
||||
).to.equal(1);
|
||||
expect(
|
||||
memberList.enums.filter(each => each.localeCompare('aEnum') === 0).length
|
||||
).to.equal(1);
|
||||
});
|
||||
|
||||
it('test dedup with empty entries', () => {
|
||||
let memberList: MemberList = {
|
||||
functions: [],
|
||||
classes: [],
|
||||
variables: ['aVar', 'bVar', 'cVar', 'aVar'],
|
||||
enums: [],
|
||||
externals: []
|
||||
};
|
||||
memberList = dedup(memberList);
|
||||
expect(memberList.functions).to.have.length(0);
|
||||
expect(memberList.classes).to.have.length(0);
|
||||
expect(memberList.enums).to.have.length(0);
|
||||
expect(memberList.variables).to.have.length(3);
|
||||
|
||||
expect(
|
||||
memberList.variables.filter(each => each.localeCompare('aVar') === 0)
|
||||
.length
|
||||
).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test replaceAll helper function', () => {
|
||||
it('test replaceAll with multiple occurences of an element', () => {
|
||||
const memberList: MemberList = {
|
||||
functions: ['aFunc', 'aFunc', 'bFunc', 'cFunc'],
|
||||
classes: ['aClass', 'bClass', 'aClass', 'cClass'],
|
||||
variables: ['aVar', 'bVar', 'cVar', 'aVar'],
|
||||
enums: ['aEnum', 'bEnum', 'cEnum', 'dEnum'],
|
||||
externals: []
|
||||
};
|
||||
const original: string = 'aFunc';
|
||||
const replaceTo: string = 'replacedFunc';
|
||||
replaceAll(memberList, original, replaceTo);
|
||||
expect(memberList.functions).to.not.include.members([original]);
|
||||
expect(memberList.functions).to.include.members([replaceTo]);
|
||||
expect(memberList.functions).to.have.length(4);
|
||||
expect(
|
||||
memberList.functions.filter(each => each.localeCompare(original) === 0)
|
||||
.length
|
||||
).to.equal(0);
|
||||
expect(
|
||||
memberList.functions.filter(each => each.localeCompare(replaceTo) === 0)
|
||||
.length
|
||||
).to.equal(2);
|
||||
});
|
||||
|
||||
it('test replaceAll with single occurence of an element', () => {
|
||||
const memberList: MemberList = {
|
||||
functions: ['aFunc', 'aFunc', 'bFunc', 'cFunc'],
|
||||
classes: ['aClass', 'bClass', 'aClass', 'cClass'],
|
||||
variables: ['aVar', 'bVar', 'cVar', 'aVar'],
|
||||
enums: ['aEnum', 'bEnum', 'cEnum', 'dEnum'],
|
||||
externals: []
|
||||
};
|
||||
const replaceTo: string = 'replacedClass';
|
||||
const original: string = 'bClass';
|
||||
replaceAll(memberList, original, replaceTo);
|
||||
expect(memberList.classes).to.not.include.members([original]);
|
||||
expect(memberList.classes).to.include.members([replaceTo]);
|
||||
expect(memberList.classes).to.have.length(4);
|
||||
expect(
|
||||
memberList.classes.filter(each => each.localeCompare(original) === 0)
|
||||
.length
|
||||
).to.equal(0);
|
||||
expect(
|
||||
memberList.classes.filter(each => each.localeCompare(replaceTo) === 0)
|
||||
.length
|
||||
).to.equal(1);
|
||||
});
|
||||
|
||||
it('test replaceAll with zero occurence of an element', () => {
|
||||
const memberList: MemberList = {
|
||||
functions: ['aFunc', 'aFunc', 'bFunc', 'cFunc'],
|
||||
classes: ['aClass', 'bClass', 'aClass', 'cClass'],
|
||||
variables: ['aVar', 'bVar', 'cVar', 'aVar'],
|
||||
enums: ['aEnum', 'bEnum', 'cEnum', 'dEnum'],
|
||||
externals: []
|
||||
};
|
||||
const replaceTo: string = 'replacedEnum';
|
||||
const original: string = 'eEnum';
|
||||
replaceAll(memberList, original, replaceTo);
|
||||
expect(memberList.enums).to.not.include.members([original, replaceTo]);
|
||||
expect(memberList.enums).to.have.length(4);
|
||||
expect(
|
||||
memberList.enums.filter(each => each.localeCompare(original) === 0).length
|
||||
).to.equal(0);
|
||||
expect(
|
||||
memberList.enums.filter(each => each.localeCompare(replaceTo) === 0)
|
||||
.length
|
||||
).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test mapSymbolToType helper function', () => {
|
||||
it('test if function correctly categorizes symbols that are misplaced', () => {
|
||||
let memberList: MemberList = {
|
||||
functions: ['aVar', 'bFunc', 'cFunc'],
|
||||
classes: ['bClass', 'cClass'],
|
||||
variables: ['aClass', 'bVar', 'cVar', 'aEnum'],
|
||||
enums: ['bEnum', 'cEnum', 'dEnum', 'aFunc'],
|
||||
externals: []
|
||||
};
|
||||
|
||||
const map: Map<string, string> = new Map([
|
||||
['aFunc', 'functions'],
|
||||
['bFunc', 'functions'],
|
||||
['aClass', 'classes'],
|
||||
['bClass', 'classes'],
|
||||
['aVar', 'variables'],
|
||||
['bVar', 'variables'],
|
||||
['aEnum', 'enums']
|
||||
]);
|
||||
|
||||
memberList = mapSymbolToType(map, memberList);
|
||||
|
||||
expect(memberList.functions).to.have.members(['aFunc', 'bFunc', 'cFunc']);
|
||||
expect(memberList.functions).to.not.include.members(['aVar']);
|
||||
expect(memberList.classes).to.have.members(['aClass', 'bClass', 'cClass']);
|
||||
expect(memberList.variables).to.not.include.members(['aClass', 'aEnum']);
|
||||
expect(memberList.variables).to.have.members(['aVar', 'bVar', 'cVar']);
|
||||
expect(memberList.enums).to.have.members([
|
||||
'aEnum',
|
||||
'bEnum',
|
||||
'cEnum',
|
||||
'dEnum'
|
||||
]);
|
||||
expect(memberList.enums).to.not.include.members(['aFunc']);
|
||||
|
||||
expect(memberList.functions).to.have.length(3);
|
||||
expect(memberList.classes).to.have.length(3);
|
||||
expect(memberList.variables).to.have.length(3);
|
||||
expect(memberList.enums).to.have.length(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test writeReportToFile helper function', () => {
|
||||
it('should throw error when given path exists and points to directory', () => {
|
||||
const aDir = resolve('./a-dir/a-sub-dir');
|
||||
fs.mkdirSync(aDir, { recursive: true });
|
||||
expect(() => writeReportToFile('content', aDir)).to.throw(
|
||||
ErrorCode.OUTPUT_FILE_REQUIRED
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw error when given path does not pre-exist', () => {
|
||||
const aPathToFile = resolve('./a-dir/a-sub-dir/a-file');
|
||||
expect(() => writeReportToFile('content', aPathToFile)).to.not.throw();
|
||||
fs.unlinkSync(aPathToFile);
|
||||
});
|
||||
after(() => {
|
||||
fs.rmdirSync('a-dir/a-sub-dir');
|
||||
fs.rmdirSync('a-dir', { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test writeReportToDirectory helper function', () => {
|
||||
it('should throw error when given path exists and points to a file', () => {
|
||||
const aDir = resolve('./a-dir/a-sub-dir');
|
||||
fs.mkdirSync(aDir, { recursive: true });
|
||||
const aFile = `a-file`;
|
||||
const aPathToFile = `${aDir}/${aFile}`;
|
||||
fs.writeFileSync(aPathToFile, 'content');
|
||||
expect(() =>
|
||||
writeReportToDirectory('content', aFile, aPathToFile)
|
||||
).to.throw(ErrorCode.OUTPUT_DIRECTORY_REQUIRED);
|
||||
});
|
||||
|
||||
it('should not throw error when given path does not pre-exist', () => {
|
||||
const aDir = resolve('./a-dir/a-sub-dir');
|
||||
const aFile = `a-file`;
|
||||
expect(() => writeReportToDirectory('content', aFile, aDir)).to.not.throw();
|
||||
});
|
||||
after(() => {
|
||||
fs.unlinkSync(`${resolve('./a-dir/a-sub-dir')}/a-file`);
|
||||
fs.rmdirSync('a-dir/a-sub-dir');
|
||||
fs.rmdirSync('a-dir', { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('test extractExternalDependencies helper function', () => {
|
||||
it('should correctly extract all symbols listed in import statements', () => {
|
||||
const assortedImports: string = getAssortedImportsJsFilePath();
|
||||
const externals: External[] = extractExternalDependencies(assortedImports);
|
||||
const barFilter: External[] = externals.filter(
|
||||
each => each.moduleName.localeCompare("'./bar'") === 0
|
||||
);
|
||||
expect(barFilter.length).to.equal(1);
|
||||
expect(barFilter[0].symbols).to.have.members([
|
||||
'basicFuncExternalDependenciesBar',
|
||||
'basicFuncExportEnumDependenciesBar',
|
||||
'BasicClassExportBar' // extract original name if renamed
|
||||
]);
|
||||
const loggerFilter: External[] = externals.filter(
|
||||
each => each.moduleName.localeCompare("'@firebase/logger'") === 0
|
||||
);
|
||||
expect(loggerFilter.length).to.equal(0);
|
||||
|
||||
const fsFilter: External[] = externals.filter(
|
||||
each => each.moduleName.localeCompare("'fs'") === 0
|
||||
);
|
||||
expect(fsFilter.length).to.equal(1);
|
||||
expect(fsFilter[0].symbols).to.have.members(['*']); // namespace export
|
||||
|
||||
const defaultExportFilter: External[] = externals.filter(
|
||||
each => each.moduleName.localeCompare("'@firebase/app'") === 0
|
||||
);
|
||||
expect(defaultExportFilter.length).to.equal(1);
|
||||
expect(defaultExportFilter[0].symbols).to.have.members(['default export']); // default export
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @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 '@firebase/logger';
|
||||
import * as fs from 'fs';
|
||||
import {
|
||||
basicFuncExportEnumDependenciesBar,
|
||||
basicFuncExternalDependenciesBar
|
||||
} from './bar';
|
||||
import { BasicClassExportBar as BasicClassExportBarRenamed } from './bar';
|
||||
import defaultExport from '@firebase/app';
|
||||
console.log(
|
||||
fs,
|
||||
basicFuncExportEnumDependenciesBar,
|
||||
basicFuncExternalDependenciesBar,
|
||||
BasicClassExportBarRenamed,
|
||||
defaultExport
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @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 { LogLevel } from '@firebase/logger';
|
||||
|
||||
export let basicVarDeclarationExportBar: string;
|
||||
export const basicVarStatementExportBar = 'basicVarStatementExportBar';
|
||||
export const reExportVarStatmentExportBar = LogLevel;
|
||||
|
||||
export enum BasicEnumExportBar {
|
||||
DEBUG = 0,
|
||||
VERBOSE = 1,
|
||||
INFO = 2,
|
||||
WARN = 3,
|
||||
ERROR = 4,
|
||||
SILENT = 5
|
||||
}
|
||||
|
||||
export class BasicClassExportBar {}
|
||||
|
||||
export function basicFuncExportNoDependenciesBar(): string {
|
||||
return 'basicFuncExportNoDependenciesBar';
|
||||
}
|
||||
export function basicFuncExportVarDependenciesBar(): string {
|
||||
return basicVarStatementExportBar;
|
||||
}
|
||||
|
||||
function d1(): string {
|
||||
return 'd1';
|
||||
}
|
||||
function d2(): string {
|
||||
return 'd2';
|
||||
}
|
||||
function d3(): string {
|
||||
return d1() + d2();
|
||||
}
|
||||
export function basicFuncExportFuncDependenciesBar(): string {
|
||||
return d3();
|
||||
}
|
||||
|
||||
export function basicFuncExportEnumDependenciesBar(): BasicEnumExportBar {
|
||||
return BasicEnumExportBar.DEBUG;
|
||||
}
|
||||
|
||||
export function basicFuncExternalDependenciesBar(): LogLevel {
|
||||
return LogLevel.ERROR;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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 { LogLevel } from '@firebase/logger';
|
||||
|
||||
export let basicVarDeclarationExportFar: string;
|
||||
export const basicVarStatementExportFar = 'basicVarStatementExportFar';
|
||||
export const reExportVarStatmentExportFar = name;
|
||||
|
||||
export enum BasicEnumExportFar {
|
||||
DEBUG = 0,
|
||||
VERBOSE = 1,
|
||||
INFO = 2,
|
||||
WARN = 3,
|
||||
ERROR = 4,
|
||||
SILENT = 5
|
||||
}
|
||||
|
||||
export class BasicClassExportFar {}
|
||||
|
||||
export function basicFuncExportNoDependenciesFar(): string {
|
||||
return 'basicFuncExportNoDependenciesFar';
|
||||
}
|
||||
export function basicFuncExportVarDependenciesFar(): string {
|
||||
return basicVarStatementExportFar;
|
||||
}
|
||||
|
||||
function d1(): string {
|
||||
return 'd1';
|
||||
}
|
||||
function d2(): string {
|
||||
return 'd2';
|
||||
}
|
||||
function d3(): string {
|
||||
return d1() + d2();
|
||||
}
|
||||
export function basicFuncExportFuncDependenciesFar(): string {
|
||||
return d3();
|
||||
}
|
||||
|
||||
export function basicFuncExportEnumDependenciesFar(): BasicEnumExportFar {
|
||||
return BasicEnumExportFar.DEBUG;
|
||||
}
|
||||
|
||||
export function basicFuncExternalDependenciesFar(): LogLevel {
|
||||
return LogLevel.ERROR;
|
||||
}
|
||||
|
||||
export function basicUniqueFuncFar(
|
||||
x: Array<{ suit: string; card: number }>
|
||||
): number;
|
||||
export function basicUniqueFuncFar(x: number): { suit: string; card: number };
|
||||
export function basicUniqueFuncFar(
|
||||
x: number | Array<{ suit: string; card: number }>
|
||||
) {
|
||||
if (typeof x === 'object') {
|
||||
const pickedCard = Math.floor(Math.random() * x.length);
|
||||
return pickedCard;
|
||||
}
|
||||
// Otherwise just let them pick the card
|
||||
else if (typeof x === 'number') {
|
||||
return { suit: 'a', card: x % 13 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @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 { LogLevel as LogLevel1, Logger, setLogLevel } from '@firebase/logger';
|
||||
import * as fs from 'fs';
|
||||
export { fs as fs1 };
|
||||
import * as tmp from 'tmp';
|
||||
export const aVar: tmp.FileOptions = {};
|
||||
// wildcard export
|
||||
export * from './bar';
|
||||
// named export
|
||||
export {
|
||||
BasicEnumExportFar,
|
||||
BasicClassExportFar,
|
||||
basicFuncExportNoDependenciesFar,
|
||||
basicFuncExportVarDependenciesFar,
|
||||
basicFuncExportFuncDependenciesFar,
|
||||
basicFuncExportEnumDependenciesFar,
|
||||
basicFuncExternalDependenciesFar,
|
||||
basicUniqueFuncFar,
|
||||
basicVarDeclarationExportFar,
|
||||
basicVarStatementExportFar,
|
||||
reExportVarStatmentExportFar
|
||||
} from './far';
|
||||
export let basicVarDeclarationExport: string;
|
||||
export const basicVarStatementExport = 'basicVarStatementExport';
|
||||
export const reExportVarStatmentExport = LogLevel1;
|
||||
|
||||
export enum BasicEnumExport {
|
||||
DEBUG = 0,
|
||||
VERBOSE = 1,
|
||||
INFO = 2,
|
||||
WARN = 3,
|
||||
ERROR = 4,
|
||||
SILENT = 5
|
||||
}
|
||||
|
||||
export class BasicClassExport {}
|
||||
|
||||
export function basicFuncExportNoDependencies(): string {
|
||||
return 'basicFuncExportNoDependencies';
|
||||
}
|
||||
export function basicFuncExportVarDependencies(): string {
|
||||
return basicVarStatementExport;
|
||||
}
|
||||
|
||||
function d1(): string {
|
||||
return 'd1';
|
||||
}
|
||||
function d2(): string {
|
||||
return 'd2';
|
||||
}
|
||||
function d3(): string {
|
||||
return d1() + d2();
|
||||
}
|
||||
export function basicFuncExportFuncDependencies(): string {
|
||||
return d3();
|
||||
}
|
||||
|
||||
export function basicFuncExportEnumDependencies(): BasicEnumExport {
|
||||
return BasicEnumExport.DEBUG;
|
||||
}
|
||||
|
||||
export function basicFuncExternalDependencies(): LogLevel1 {
|
||||
return LogLevel1.WARN;
|
||||
}
|
||||
|
||||
export function basicUniqueFunc(
|
||||
x: Array<{ suit: string; card: number }>
|
||||
): number;
|
||||
export function basicUniqueFunc(x: number): { suit: string; card: number };
|
||||
export function basicUniqueFunc(
|
||||
x: number | Array<{ suit: string; card: number }>
|
||||
): number | object {
|
||||
if (typeof x === 'object') {
|
||||
const pickedCard = Math.floor(Math.random() * x.length);
|
||||
return pickedCard;
|
||||
}
|
||||
// Otherwise just let them pick the card
|
||||
else if (typeof x === 'number') {
|
||||
return { suit: 'a', card: x % 13 };
|
||||
}
|
||||
}
|
||||
|
||||
const appsNotExported: Map<string, number> = new Map();
|
||||
const apps: Map<string, number> = new Map();
|
||||
export { apps };
|
||||
|
||||
class FooNotExported {}
|
||||
class Foo {}
|
||||
export { Foo as Foo1 };
|
||||
|
||||
function foo(x: string): string {
|
||||
return x;
|
||||
}
|
||||
export { foo as foo2 };
|
||||
|
||||
export {};
|
||||
|
||||
export { tar as tarr, tar1 as tarr1 } from './tar';
|
||||
// re-export from firebase external module
|
||||
export { LogLevel1 as LogLevel2 };
|
||||
|
||||
export { Logger as Logger1 };
|
||||
|
||||
export { setLogLevel };
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export {
|
||||
aVar,
|
||||
LogLevel2,
|
||||
Logger1,
|
||||
tarr1,
|
||||
fs1,
|
||||
basicFuncExportEnumDependencies,
|
||||
basicFuncExportFuncDependenciesBar
|
||||
} from './index';
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export function tar(name: string): string {
|
||||
return name;
|
||||
}
|
||||
|
||||
export function tar1(name: string): string {
|
||||
return tar(name);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"importHelpers": true,
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "es6",
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export function getTestModuleDtsFilePath(): string {
|
||||
return `${__dirname}/test-inputs/dist/index.d.ts`;
|
||||
}
|
||||
export function getSubsetExportsBundleFilePath(): string {
|
||||
return `${__dirname}/test-inputs/dist/subsetExportsBundle.js`;
|
||||
}
|
||||
export function getAssortedImportsJsFilePath(): string {
|
||||
return `${__dirname}/test-inputs/dist/assortedImports.js`;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "es5",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user