diff --git a/packages/firebase/firestore/index.min.ts b/packages/firebase/firestore/index.min.ts new file mode 100644 index 00000000000..17fa0a30387 --- /dev/null +++ b/packages/firebase/firestore/index.min.ts @@ -0,0 +1,18 @@ +/** + * @license + * 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. + */ + +import '../../firestore/dist/index.esm.min'; diff --git a/packages/firebase/rollup.config.js b/packages/firebase/rollup.config.js index ffd0085410f..7d5805c2f6a 100644 --- a/packages/firebase/rollup.config.js +++ b/packages/firebase/rollup.config.js @@ -28,6 +28,38 @@ import pkg from './package.json'; import appPkg from './app/package.json'; +function createUmdOutputConfig(output) { + return { + file: output, + format: 'umd', + sourcemap: true, + extend: true, + name: GLOBAL_NAME, + globals: { + '@firebase/app': GLOBAL_NAME + }, + + /** + * use iife to avoid below error in the old Safari browser + * SyntaxError: Functions cannot be declared in a nested block in strict mode + * https://github.com/firebase/firebase-js-sdk/issues/1228 + * + */ + intro: ` + try { + (function() {`, + outro: ` + }).apply(this, arguments); + } catch(err) { + console.error(err); + throw new Error( + 'Cannot instantiate ${output} - ' + + 'be sure to load firebase-app.js first.' + ); + }` + }; +} + const plugins = [ sourcemaps(), resolveModule(), @@ -101,36 +133,7 @@ const componentBuilds = pkg.components }, { input: `${component}/index.ts`, - output: { - file: `firebase-${component}.js`, - format: 'umd', - sourcemap: true, - extend: true, - name: GLOBAL_NAME, - globals: { - '@firebase/app': GLOBAL_NAME - }, - - /** - * use iife to avoid below error in the old Safari browser - * SyntaxError: Functions cannot be declared in a nested block in strict mode - * https://github.com/firebase/firebase-js-sdk/issues/1228 - * - */ - - intro: ` - try { - (function() {`, - outro: ` - }).apply(this, arguments); - } catch(err) { - console.error(err); - throw new Error( - 'Cannot instantiate firebase-${component} - ' + - 'be sure to load firebase-app.js first.' - ); - }` - }, + output: createUmdOutputConfig(`firebase-${component}.js`), plugins: [...plugins, uglify()], external: ['@firebase/app'] } @@ -138,6 +141,13 @@ const componentBuilds = pkg.components }) .reduce((a, b) => a.concat(b), []); +const firestoreMinifiedBuild = { + input: `firestore/index.min.ts`, + output: createUmdOutputConfig(`firebase-firestore-min.js`), + plugins: [...plugins, uglify()], + external: ['@firebase/app'] +}; + /** * Complete Package Builds */ @@ -239,4 +249,9 @@ const completeBuilds = [ } ]; -export default [...appBuilds, ...componentBuilds, ...completeBuilds]; +export default [ + ...appBuilds, + ...componentBuilds, + firestoreMinifiedBuild, + ...completeBuilds +]; diff --git a/packages/firestore/.npmignore b/packages/firestore/.npmignore index 91da21bdd05..89d8480e19b 100644 --- a/packages/firestore/.npmignore +++ b/packages/firestore/.npmignore @@ -2,9 +2,10 @@ /src test .idea +.cache # Files not needed by end users gulpfile.js index.ts karma.conf.js -tsconfig.json \ No newline at end of file +tsconfig.json diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json new file mode 100644 index 00000000000..8f0adbef84b --- /dev/null +++ b/packages/firestore/externs.json @@ -0,0 +1,27 @@ +{ + "externs" : [ + "node_modules/typescript/lib/lib.es5.d.ts", + "node_modules/typescript/lib/lib.dom.d.ts", + "node_modules/typescript/lib/lib.es2015.promise.d.ts", + "node_modules/typescript/lib/lib.es2015.symbol.d.ts", + "node_modules/typescript/lib/lib.es2015.iterable.d.ts", + "node_modules/typescript/lib/lib.es2015.collection.d.ts", + "node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts", + "node_modules/typescript/lib/lib.es2015.core.d.ts", + "node_modules/typescript/lib/lib.es2017.object.d.ts", + "packages/app-types/index.d.ts", + "packages/app-types/private.d.ts", + "packages/auth-interop-types/index.d.ts", + "packages/firestore-types/index.d.ts", + "packages/firebase/index.d.ts", + "packages/component/dist/src/component.d.ts", + "packages/component/dist/src/provider.d.ts", + "packages/component/dist/src/component_container.d.ts", + "packages/logger/dist/src/logger.d.ts", + "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/local/indexeddb_schema.d.ts", + "packages/firestore/dist/lib/src/local/shared_client_state_schema.d.ts" + ] +} diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 23326595667..2f62e82571a 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -4,6 +4,7 @@ "description": "", "author": "Firebase (https://firebase.google.com/)", "scripts": { + "prebuild": "tsc -d --declarationDir dist/lib --emitDeclarationOnly && tsc -m es2015 --moduleResolution node scripts/*.ts ", "build": "rollup -c", "build:console": "node tools/console.build.js", "dev": "rollup -c -w", @@ -24,8 +25,11 @@ }, "main": "dist/index.node.cjs.js", "browser": "dist/index.cjs.js", + "browserMinified": "dist/index.cjs.min.js", "module": "dist/index.esm.js", + "moduleMinified": "dist/index.esm.min.js", "esm2017": "dist/index.esm2017.js", + "esm2017Minified": "dist/index.esm2017.min.js", "license": "Apache-2.0", "files": [ "dist" @@ -48,8 +52,10 @@ "protobufjs": "6.8.8", "rollup": "1.27.9", "rollup-plugin-copy-assets": "1.1.0", + "rollup-plugin-json": "5.1.3", "rollup-plugin-node-resolve": "5.2.0", "rollup-plugin-replace": "2.2.0", + "rollup-plugin-terser": "5.1.2", "rollup-plugin-typescript2": "0.25.3", "typescript": "3.7.3" }, diff --git a/packages/firestore/rollup.config.js b/packages/firestore/rollup.config.js index be88c8c83da..8a934ad2c2f 100644 --- a/packages/firestore/rollup.config.js +++ b/packages/firestore/rollup.config.js @@ -15,27 +15,73 @@ * limitations under the License. */ +import * as path from 'path'; + import json from 'rollup-plugin-json'; import typescriptPlugin from 'rollup-plugin-typescript2'; import replace from 'rollup-plugin-replace'; import copy from 'rollup-plugin-copy-assets'; import typescript from 'typescript'; +import { terser } from 'rollup-plugin-terser'; + +import { renameInternals } from './scripts/rename-internals'; +import { extractPublicIdentifiers } from './scripts/extract-api'; import pkg from './package.json'; +import { externs } from './externs.json'; const deps = Object.keys( Object.assign({}, pkg.peerDependencies, pkg.dependencies) ); +// Extract all identifiers used in external APIs (to be used as a blacklist by +// the SDK minifier). +const externsPaths = externs.map(p => path.resolve(__dirname, '../../', p)); +const publicIdentifiers = extractPublicIdentifiers(externsPaths); +const transformers = [ + service => ({ + before: [ + renameInternals(service.getProgram(), { + publicIdentifiers, + prefix: '__PRIVATE_' + }) + ], + after: [] + }) +]; + +const terserOptions = { + output: { + comments: false + }, + mangle: { + properties: { + regex: /^__PRIVATE_/ + } + } +}; + /** * ES5 Builds */ const es5BuildPlugins = [ typescriptPlugin({ - typescript + typescript, + cacheRoot: './.cache/es5/' }), json() ]; +const es5MinifiedBuildPlugins = [ + typescriptPlugin({ + typescript, + transformers, + cacheRoot: './.cache/es5.min/' + }), + json(), + terser(terserOptions), + replace({ delimiters: ['', ''], '__PRIVATE_': ''}) +]; + const es5Builds = [ /** * Node.js Build @@ -69,6 +115,15 @@ const es5Builds = [ ], plugins: es5BuildPlugins, external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + }, + { + input: 'index.ts', + output: [ + { file: pkg.browserMinified, format: 'cjs', sourcemap: true }, + { file: pkg.moduleMinified, format: 'es', sourcemap: true } + ], + plugins: es5MinifiedBuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; @@ -82,11 +137,28 @@ const es2017BuildPlugins = [ compilerOptions: { target: 'es2017' } - } + }, + cacheRoot: './.cache/es2017/' }), json({ preferConst: true }) ]; +const es2017MinifiedBuildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + }, + cacheRoot: './.cache/es2017.min/', + transformers + }), + json({ preferConst: true }), + terser(terserOptions), + replace({ delimiters: ['', ''], '__PRIVATE_': ''}) +]; + const es2017Builds = [ /** * Browser Build @@ -100,6 +172,16 @@ const es2017Builds = [ }, plugins: es2017BuildPlugins, external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + }, + { + input: 'index.ts', + output: { + file: pkg.esm2017Minified, + format: 'es', + sourcemap: true + }, + plugins: es2017MinifiedBuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) } ]; diff --git a/packages/firestore/scripts/extract-api.ts b/packages/firestore/scripts/extract-api.ts new file mode 100644 index 00000000000..0bdb46be9b1 --- /dev/null +++ b/packages/firestore/scripts/extract-api.ts @@ -0,0 +1,60 @@ +/** + * @license + * 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. + */ + +import * as ts from 'typescript'; +import * as fs from 'fs'; + +function extractIdentifiersFromNodeAndChildren( + node: ts.Node, + symbols: Set +): void { + if (ts.isIdentifier(node)) { + symbols.add(node.escapedText.toString()); + } + + ts.forEachChild(node, (childNode: ts.Node) => + extractIdentifiersFromNodeAndChildren(childNode, symbols) + ); +} + +/** + * Traverses TypeScript type definition files and returns the list of referenced + * identifiers. + */ +export function extractPublicIdentifiers(filePaths: string[]): Set { + const publicIdentifiers = new Set(); + + for (const filePath of filePaths) { + const contents = fs.readFileSync(filePath, { encoding: 'UTF-8' }); + const sourceFile = ts.createSourceFile( + filePath, + contents, + ts.ScriptTarget.ES2015 + ); + + const identifiers = new Set(); + ts.forEachChild(sourceFile, (childNode: ts.Node) => + extractIdentifiersFromNodeAndChildren(childNode, identifiers) + ); + + identifiers.forEach(api => { + publicIdentifiers.add(api); + }); + } + + return publicIdentifiers; +} diff --git a/packages/firestore/scripts/rename-internals.ts b/packages/firestore/scripts/rename-internals.ts new file mode 100644 index 00000000000..54d7135865f --- /dev/null +++ b/packages/firestore/scripts/rename-internals.ts @@ -0,0 +1,83 @@ +/** + * @license + * 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. + */ + +import * as ts from 'typescript'; + +// `undefined` is treated as an identifier by TSC, but not part of any externs. +const blacklist = ['undefined']; + +/** + * Processes TypeScript source files and renames all identifiers that do not + * reference the public API. + */ +class RenameInternals { + constructor( + private readonly publicApi: Set, + private readonly prefix: string + ) {} + + visitNodeAndChildren( + node: T, + context: ts.TransformationContext + ): T { + return ts.visitEachChild( + this.visitNode(node), + (childNode: ts.Node) => this.visitNodeAndChildren(childNode, context), + context + ) as T; + } + + visitNode(node: ts.Node): ts.Node { + if (ts.isIdentifier(node)) { + const name = node.escapedText.toString(); + if (!this.publicApi.has(name) + && blacklist.indexOf(node.escapedText.toString()) === -1) { + return ts.createIdentifier(this.prefix + name); + } + } + + return node; + } +} + +const DEFAULT_PREFIX = '_'; + +export interface SDKMinifierOptions { + /** List of identifiers that are not to be minified. */ + publicIdentifiers: Set; + /** + * A prefix to append to all identifiers that are not referencing the Public + * API. Defauls to '_'. + */ + prefix?: string; +} + +/** + * A TypeScript transformer that minifies existing source files. All identifiers + * are minified unless listed in `config.publicIdentifiers`. + */ +export function renameInternals( + program: ts.Program, + config: SDKMinifierOptions +): ts.TransformerFactory { + const prefix = config.prefix ?? DEFAULT_PREFIX; + + const renamer = new RenameInternals(config.publicIdentifiers, prefix); + return (context: ts.TransformationContext) => (file: ts.SourceFile) => { + return renamer.visitNodeAndChildren(file, context); + }; +}