UNPKG

voluptasmollitia

Version:
957 lines (910 loc) 33.7 kB
/** * @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'; 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!', PKG_JSON_DOES_NOT_EXIST = 'Module does not have a package.json file!', TYPINGS_FIELD_NOT_DEFINED = 'Module does not have typings field defined in its package.json!' } /** Contains a list of members by type. */ export interface MemberList { classes: string[]; functions: string[]; variables: string[]; enums: string[]; } /** Contains the dependencies and the size of their code for a single export. */ export interface ExportData { name: string; classes: string[]; functions: string[]; variables: string[]; enums: string[]; externals: { [key: string]: string[] }; size: number; sizeWithExtDeps: number; } export interface Report { name: string; symbols: ExportData[]; } /** * 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, // exclude all firebase dependencies and tslib external: id => id.startsWith('@firebase') || id === 'tslib' }); await externalDepsNotResolvedBundle.write({ file: externalDepsNotResolvedOutput, format: 'es' }); const dependencies: MemberList = extractDeclarations( externalDepsNotResolvedOutput, map ); const externalDepsResolvedOutputContent = fs.readFileSync( externalDepsResolvedOutput, 'utf-8' ); // Extract size of minified build const externalDepsNotResolvedOutputContent = fs.readFileSync( externalDepsNotResolvedOutput, 'utf-8' ); const externalDepsResolvedOutputContentMinimized = await terser.minify( externalDepsResolvedOutputContent, { format: { comments: false }, mangle: { toplevel: true }, compress: false } ); const externalDepsNotResolvedOutputContentMinimized = await terser.minify( externalDepsNotResolvedOutputContent, { format: { comments: false }, mangle: { toplevel: true }, compress: false } ); const exportData: ExportData = { name: '', classes: [], functions: [], variables: [], enums: [], externals: {}, size: 0, sizeWithExtDeps: 0 }; exportData.name = exportName; for (const key of Object.keys(dependencies) as Array<keyof MemberList>) { exportData[key] = dependencies[key]; } exportData.externals = extractExternalDependencies( externalDepsNotResolvedOutput ); exportData.size = Buffer.byteLength( externalDepsNotResolvedOutputContentMinimized.code!, 'utf-8' ); exportData.sizeWithExtDeps = Buffer.byteLength( externalDepsResolvedOutputContentMinimized.code!, 'utf-8' ); fs.unlinkSync(input); fs.unlinkSync(externalDepsNotResolvedOutput); fs.unlinkSync(externalDepsResolvedOutput); return exportData; } /** * 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: [] }; 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)) { // `const enum`s should not be analyzed. They do not add to bundle size and // creating a file that imports them causes an error during the rollup step. if ( // Identifies if this enum had a "const" modifier attached. !node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ConstKeyword) ) { 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 { const elements = variableDeclaration.name .elements as ts.NodeArray<ts.BindingElement>; elements.forEach((node: ts.BindingElement) => { 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) as Array< keyof MemberList >) { 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: [] }; 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; 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) as Array< keyof MemberList >) { 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) as Array<keyof 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: [] }; for (const key of Object.keys(memberList) as Array<keyof MemberList>) { memberList[key].forEach((element: string) => { if (map.has(element)) { newMemberList[map.get(element)! as keyof MemberList].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) as Array<keyof 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) as Array<keyof 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: Report, 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, JSON.stringify(report, null, 4)); } /** * * This functions writes generated json report(s) to a file of given directory */ export function writeReportToDirectory( report: Report, 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 ): { [key: string]: string[] } { 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: { [key: string]: string[] } = {}; externalsMap.forEach((value, key) => { externals[key.replace(/'/g, '')] = value; }); 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 */ export async function generateReportForModule( moduleLocation: string ): Promise<Report> { const packageJsonPath = `${moduleLocation}/package.json`; if (!fs.existsSync(packageJsonPath)) { throw new Error( `Firebase Module locates at ${moduleLocation}: ${ErrorCode.PKG_JSON_DOES_NOT_EXIST}` ); } const packageJson = JSON.parse( fs.readFileSync(packageJsonPath, { encoding: 'utf-8' }) ); // to exclude <modules>-types modules const TYPINGS: string = 'typings'; if (packageJson[TYPINGS]) { const dtsFile = `${moduleLocation}/${packageJson[TYPINGS]}`; const bundleLocation: string = retrieveBundleFileLocation(packageJson); if (!bundleLocation) { throw new Error(ErrorCode.BUNDLE_FILE_DOES_NOT_EXIST); } const bundleFile = `${moduleLocation}/${bundleLocation}`; const jsonReport: Report = await generateReport( packageJson.name, dtsFile, bundleFile ); return jsonReport; } throw new Error( `Firebase Module locates at: ${moduleLocation}: ${ErrorCode.TYPINGS_FIELD_NOT_DEFINED}` ); } /** * * @param pkgJson package.json of the module. * * This function implements a fallback of locating module's budle file. * It first looks at esm2017 field of package.json, then module field. Main * field at the last. * */ function retrieveBundleFileLocation(pkgJson: { [key: string]: string; }): string { if (pkgJson['esm2017']) { return pkgJson['esm2017']; } if (pkgJson['module']) { return pkgJson['module']; } if (pkgJson['main']) { return pkgJson['main']; } return ''; } /** * * 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) as Array<keyof MemberList>) { api[type].forEach((element: string) => { map.set(element, type); }); } return map; } /** * A recursive function that locates and generates reports for sub-modules */ async function traverseDirs( moduleLocation: string, // eslint-disable-next-line @typescript-eslint/ban-types executor: Function, level: number, levelLimit: number ): Promise<Report[]> { if (level > levelLimit) { return []; } const reports: Report[] = []; const report: Report = await executor(moduleLocation); if (report != null) { reports.push(report); } for (const name of fs.readdirSync(moduleLocation)) { const p = `${moduleLocation}/${name}`; const generateSizeAnalysisReportPkgJsonField: string = 'generate-size-analysis-report'; // submodules of a firebase module should set generate-size-analysis-report field of package.json to true // in order to be analyzed if ( fs.lstatSync(p).isDirectory() && fs.existsSync(`${p}/package.json`) && JSON.parse(fs.readFileSync(`${p}/package.json`, { encoding: 'utf-8' }))[ generateSizeAnalysisReportPkgJsonField ] ) { const subModuleReports: Report[] = await traverseDirs( p, executor, level + 1, levelLimit ); if (subModuleReports !== null && subModuleReports.length !== 0) { reports.push(...subModuleReports); } } } return reports; } /** * * 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( moduleName: string, publicApi: MemberList, jsFile: string, map: Map<string, string> ): Promise<Report> { const result: Report = { name: moduleName, symbols: [] }; for (const exp of publicApi.classes) { try { result.symbols.push(await extractDependenciesAndSize(exp, jsFile, map)); } catch (e) { console.log(e); } } for (const exp of publicApi.functions) { try { result.symbols.push(await extractDependenciesAndSize(exp, jsFile, map)); } catch (e) { console.log(e); } } for (const exp of publicApi.variables) { try { result.symbols.push(await extractDependenciesAndSize(exp, jsFile, map)); } catch (e) { console.log(e); } } for (const exp of publicApi.enums) { try { result.symbols.push(await extractDependenciesAndSize(exp, jsFile, map)); } catch (e) { console.log(e); } } return result; } /** * * This function generates a report from given dts file. * @param name a name to be displayed on the report. a module name if for a firebase module; a random name if for adhoc analysis. * @param dtsFile absolute path to the definition file of interest. * @param bundleFile absolute path to the bundle file of the given definition file. */ export async function generateReport( name: string, dtsFile: string, bundleFile: string ): Promise<Report> { 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(name, 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 * */ export async function generateReportForModules( moduleLocations: string[] ): Promise<Report[]> { const reportCollection: Report[] = []; 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. const reportsForModuleAndItsSubModule: Report[] = await traverseDirs( moduleLocation, generateReportForModule, 0, 1 ); if ( reportsForModuleAndItsSubModule !== null && reportsForModuleAndItsSubModule.length !== 0 ) { reportCollection.push(...reportsForModuleAndItsSubModule); } } return reportCollection; }