diff --git a/package.json b/package.json index 9006f8c0980..32003d73928 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,8 @@ "integration/*" ], "devDependencies": { + "@microsoft/api-documenter": "7.7.20", + "@microsoft/api-extractor": "7.7.13", "@types/chai": "4.2.11", "@types/chai-as-promised": "7.1.2", "@types/long": "4.0.1", @@ -65,6 +67,8 @@ "@types/node": "12.12.37", "@types/sinon": "9.0.0", "@types/sinon-chai": "3.2.4", + "@types/tmp": "0.2.0", + "@types/yargs": "15.0.4", "@typescript-eslint/eslint-plugin": "2.30.0", "@typescript-eslint/eslint-plugin-tslint": "2.30.0", "@typescript-eslint/parser": "2.30.0", @@ -132,9 +136,7 @@ "typescript": "3.8.3", "watch": "1.0.2", "webpack": "4.43.0", - "yargs": "15.3.1", - "@microsoft/api-extractor": "7.7.13", - "@microsoft/api-documenter": "7.7.20" + "yargs": "15.3.1" }, "husky": { "hooks": { diff --git a/packages/firestore/exp/index.d.ts b/packages/firestore/exp/index.d.ts new file mode 100644 index 00000000000..982b12e255c --- /dev/null +++ b/packages/firestore/exp/index.d.ts @@ -0,0 +1,19 @@ +/** + * @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 foo(): string; +export function bar(): string; diff --git a/packages/firestore/exp/index.node.ts b/packages/firestore/exp/index.node.ts new file mode 100644 index 00000000000..ac36767cfe2 --- /dev/null +++ b/packages/firestore/exp/index.node.ts @@ -0,0 +1,18 @@ +/** + * @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 { foo, bar } from './src/api/foobar'; diff --git a/packages/firestore/exp/src/api/foobar.ts b/packages/firestore/exp/src/api/foobar.ts new file mode 100644 index 00000000000..328241108fb --- /dev/null +++ b/packages/firestore/exp/src/api/foobar.ts @@ -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 foo(): string { + return bar(); +} + +export function bar(): string { + return 'bar'; +} diff --git a/packages/firestore/exp/test/deps/dependencies.json b/packages/firestore/exp/test/deps/dependencies.json new file mode 100644 index 00000000000..e348f877ca0 --- /dev/null +++ b/packages/firestore/exp/test/deps/dependencies.json @@ -0,0 +1,23 @@ +{ + "bar": { + "dependencies": { + "functions": [ + "bar" + ], + "classes": [], + "variables": [] + }, + "sizeInBytes": 39 + }, + "foo": { + "dependencies": { + "functions": [ + "bar", + "foo" + ], + "classes": [], + "variables": [] + }, + "sizeInBytes": 67 + } +} diff --git a/packages/firestore/exp/test/deps/verify_dependencies.test.ts b/packages/firestore/exp/test/deps/verify_dependencies.test.ts new file mode 100644 index 00000000000..ffebded12bd --- /dev/null +++ b/packages/firestore/exp/test/deps/verify_dependencies.test.ts @@ -0,0 +1,45 @@ +/** + * @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 { expect } from 'chai'; + +import { extractDependencies } from '../../../../../scripts/exp/extract-deps.helpers'; + +import * as dependencies from './dependencies.json'; +import * as pkg from '../../../package.json'; +import { forEach } from '../../../src/util/obj'; + +describe('Dependencies', () => { + forEach(dependencies, (api, { dependencies }) => { + it(api, () => { + return extractDependencies(api, pkg.exp).then(extractedDependencies => { + expect(extractedDependencies.classes).to.have.members( + dependencies.classes, + 'for classes' + ); + expect(extractedDependencies.functions).to.have.members( + dependencies.functions, + 'for functions' + ); + expect(extractedDependencies.variables).to.have.members( + dependencies.variables, + 'for variables' + ); + }); + }); + }); +}); diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index c5c690cc031..d597bfb77a9 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -23,8 +23,8 @@ "packages/webchannel-wrapper/src/index.d.ts", "packages/util/dist/src/environment.d.ts", "packages/firestore/src/protos/firestore_proto_api.d.ts", - "packages/firestore/dist/lib/src/util/error.d.ts", - "packages/firestore/dist/lib/src/local/indexeddb_schema.d.ts", - "packages/firestore/dist/lib/src/local/shared_client_state_schema.d.ts" + "packages/firestore/src/util/error.ts", + "packages/firestore/src/local/indexeddb_schema.ts", + "packages/firestore/src/local/shared_client_state_schema.ts" ] } diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 665ad2fef68..0635fdf8e46 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -7,15 +7,20 @@ "description": "The Cloud Firestore component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "scripts": { - "prebuild": "tsc -d --downlevelIteration --declarationDir dist/lib --emitDeclarationOnly && tsc -m es2015 --moduleResolution node scripts/*.ts ", + "prebuild": "tsc -m es2015 --moduleResolution node scripts/*.ts ", "build": "rollup -c rollup.config.es2017.js && rollup -c rollup.config.es5.js", "build:deps": "lerna run --scope @firebase/'{app,firestore}' --include-dependencies build", "build:console": "node tools/console.build.js", + "build:exp": "rollup -c rollup.config.exp.js", "predev": "yarn prebuild", "dev": "rollup -c -w", "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "prettier": "prettier --write '*.ts' '*.js' 'src/**/*.js' 'test/**/*.js' 'src/**/*.ts' 'test/**/*.ts'", + "prettier": "prettier --write '*.ts' '*.js' 'exp/**/*.ts' 'src/**/*.js' 'test/**/*.js' 'src/**/*.ts' 'test/**/*.ts'", + "pregendeps:exp": "yarn build:exp", + "gendeps:exp": "../../scripts/exp/extract-deps.sh --types ./exp/index.d.ts --bundle ./dist/exp/index.js --output ./exp/test/deps/dependencies.json", + "pretest:exp": "yarn build:exp", + "test:exp": "TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'exp/test/**/*.test.ts' --file exp/index.node.ts --config ../../config/mocharc.node.js", "test": "run-s lint test:all", "test:ci": "node ../../scripts/run_tests_in_ci.js", "test:all": "run-p test:browser test:travis test:minified", @@ -34,6 +39,7 @@ "browser": "dist/index.cjs.js", "module": "dist/index.esm.js", "esm2017": "dist/index.esm2017.js", + "exp": "dist/exp/index.js", "license": "Apache-2.0", "files": [ "dist", diff --git a/packages/firestore/rollup.config.exp.js b/packages/firestore/rollup.config.exp.js new file mode 100644 index 00000000000..912f2a943a1 --- /dev/null +++ b/packages/firestore/rollup.config.exp.js @@ -0,0 +1,52 @@ +/** + * @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 typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; + +import { resolveNodeExterns } from './rollup.shared'; + +import pkg from './package.json'; + +const defaultPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + }, + clean: true + }) +]; + +const nodeBuilds = [ + { + input: './exp/index.node.ts', + output: { + file: pkg.exp, + format: 'es' + }, + plugins: defaultPlugins, + external: resolveNodeExterns, + treeshake: { + tryCatchDeoptimization: false + } + } +]; + +export default [...nodeBuilds]; diff --git a/packages/firestore/scripts/extract-api.ts b/packages/firestore/scripts/extract-api.ts index 7e520a4dfbc..49dee323669 100644 --- a/packages/firestore/scripts/extract-api.ts +++ b/packages/firestore/scripts/extract-api.ts @@ -32,6 +32,17 @@ function extractIdentifiersFromNodeAndChildren( ); } +/** Generates the "d.ts" content for `fileName`. */ +function extractTypeDeclaration(fileName: string): string { + let result: string; + const compilerOptions = { declaration: true, emitDeclarationOnly: true }; + const host = ts.createCompilerHost(compilerOptions); + host.writeFile = (_: string, contents: string) => (result = contents); + const program = ts.createProgram([fileName], compilerOptions, host); + program.emit(); + return result!; +} + /** * Traverses TypeScript type definition files and returns the list of referenced * identifiers. @@ -41,12 +52,21 @@ export function extractPublicIdentifiers(filePaths: string[]): Set { for (const filePath of filePaths) { const contents = fs.readFileSync(filePath, { encoding: 'UTF-8' }); - const sourceFile = ts.createSourceFile( + let sourceFile = ts.createSourceFile( filePath, contents, ts.ScriptTarget.ES2015 ); + if (!sourceFile.isDeclarationFile) { + const dtsSource = extractTypeDeclaration(filePath); + sourceFile = ts.createSourceFile( + filePath.replace('.ts', '.d.ts'), + dtsSource, + ts.ScriptTarget.ES2015 + ); + } + const identifiers = new Set(); ts.forEachChild(sourceFile, (childNode: ts.Node) => extractIdentifiersFromNodeAndChildren(childNode, identifiers) diff --git a/scripts/exp/extract-deps.helpers.ts b/scripts/exp/extract-deps.helpers.ts new file mode 100644 index 00000000000..4df429b8102 --- /dev/null +++ b/scripts/exp/extract-deps.helpers.ts @@ -0,0 +1,128 @@ +/** + * @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'; + +/** Contains a list of members by type. */ +export type MemberList = { + classes: string[]; + functions: string[]; + variables: string[]; +}; +/** Contains the dependencies and the size of their code for a single export. */ +export type ExportData = { dependencies: MemberList; sizeInBytes: 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 +): Promise { + const { dependencies } = await extractDependenciesAndSize( + exportName, + jsBundle + ); + return dependencies; +} + +/** + * Helper for extractDependencies that extracts the dependencies and the size + * of the minified build. + */ +export async function extractDependenciesAndSize( + exportName: string, + jsBundle: string +): Promise { + const input = tmp.fileSync().name + '.js'; + const output = tmp.fileSync().name + '.js'; + + // JavaScript content that exports a single API from the bundle + const beforeContent = `export { ${exportName} } from '${path.resolve( + jsBundle + )}';`; + fs.writeFileSync(input, beforeContent); + + // Run Rollup on the JavaScript above to produce a tree-shaken build + const bundle = await rollup.rollup({ + input, + external: id => id.startsWith('@firebase/') + }); + await bundle.write({ file: output, format: 'es' }); + + const dependencies = extractDeclarations(output); + + // Extract size of minified build + const afterContent = fs.readFileSync(output, 'utf-8'); + const { code } = terser.minify(afterContent, { + output: { + comments: false + }, + mangle: false, + compress: false + }); + + fs.unlinkSync(input); + fs.unlinkSync(output); + + return { dependencies, sizeInBytes: Buffer.byteLength(code!, 'utf-8') }; +} + +/** + * Extracts all function, class and variable declarations using the TypeScript + * compiler API. + */ +export function extractDeclarations(jsFile: string): MemberList { + const program = ts.createProgram([jsFile], { allowJs: true }); + const sourceFile = program.getSourceFile(jsFile); + if (!sourceFile) { + throw new Error('Failed to parse file: ' + jsFile); + } + + const declarations: MemberList = { + functions: [], + classes: [], + variables: [] + }; + 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()); + } + }); + + // Sort to ensure stable output + declarations.functions.sort(); + declarations.classes.sort(); + declarations.variables.sort(); + + return declarations; +} diff --git a/scripts/exp/extract-deps.sh b/scripts/exp/extract-deps.sh new file mode 100755 index 00000000000..410fe120f15 --- /dev/null +++ b/scripts/exp/extract-deps.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Copyright 2020 Google Inc. +# +# 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. + +# extract-deps --- Invokes extract-deps using TS_NODE + +set -o nounset +set -o errexit + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +NPM_BIN_DIR="$(npm bin)" +TSNODE="$NPM_BIN_DIR/ts-node" +GENERATE_DEPS_JS="$DIR/extract-deps.ts" + +export TS_NODE_CACHE=NO +export TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}' +export TS_NODE_PROJECT="$DIR/../../config/tsconfig.base.json" + +$TSNODE $GENERATE_DEPS_JS "$@" diff --git a/scripts/exp/extract-deps.ts b/scripts/exp/extract-deps.ts new file mode 100644 index 00000000000..4a32d3f6f3c --- /dev/null +++ b/scripts/exp/extract-deps.ts @@ -0,0 +1,71 @@ +/** + * @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 fs from 'fs'; +import * as path from 'path'; +import * as yargs from 'yargs'; +import { + ExportData, + extractDependenciesAndSize, + extractDeclarations, + MemberList +} from './extract-deps.helpers'; + +// Small command line app that builds a JSON file with a list of the +// dependencies for each individual exported type. This can then be used to +// verify dependencies in tree-shakeable SDKs. + +const argv = yargs.options({ + types: { + type: 'string', + demandOption: true, + desc: 'The location of the index.d.ts file that describes the Public API' + }, + bundle: { + type: 'string', + demandOption: true, + desc: + 'The location of transpiled JavaScript bundle that contains all code for the SDK' + }, + output: { + type: 'string', + demandOption: true, + desc: 'The location to write the JSON output to' + } +}).argv; + +async function buildJson( + publicApi: MemberList, + jsFile: string +): Promise { + const result: { [key: string]: ExportData } = {}; + for (const exp of publicApi.classes) { + result[exp] = await extractDependenciesAndSize(exp, jsFile); + } + for (const exp of publicApi.functions) { + result[exp] = await extractDependenciesAndSize(exp, jsFile); + } + for (const exp of publicApi.variables) { + result[exp] = await extractDependenciesAndSize(exp, jsFile); + } + return JSON.stringify(result, null, 4); +} + +const publicApi = extractDeclarations(path.resolve(argv.types)); +buildJson(publicApi, path.resolve(argv.bundle)).then(json => { + fs.writeFileSync(path.resolve(argv.output), json); +}); diff --git a/yarn.lock b/yarn.lock index 6a0bb5a6e2a..1591ae97d02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2255,11 +2255,28 @@ resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== +"@types/tmp@0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c" + integrity sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ== + "@types/tough-cookie@*": version "4.0.0" resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + +"@types/yargs@15.0.4": + version "15.0.4" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" + integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg== + dependencies: + "@types/yargs-parser" "*" + "@types/yauzl@^2.9.1": version "2.9.1" resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"