diff --git a/.github/scripts/verify_package.sh b/.github/scripts/verify_package.sh index f8c37b4db0..2b295a63b9 100755 --- a/.github/scripts/verify_package.sh +++ b/.github/scripts/verify_package.sh @@ -53,7 +53,7 @@ trap cleanup EXIT # Copy package and test sources into working directory cp "${PKG_NAME}" "${WORK_DIR}" -cp -r test/integration/typescript/* "${WORK_DIR}" +cp -r test/integration/postcheck/* "${WORK_DIR}" cp test/resources/mock.key.json "${WORK_DIR}" # Enter work dir @@ -68,6 +68,10 @@ npm install -S "${PKG_NAME}" echo "> tsc -p tsconfig.json" ./node_modules/.bin/tsc -p tsconfig.json -MOCHA_CLI="./node_modules/.bin/mocha -r ts-node/register" -echo "> $MOCHA_CLI src/*.test.ts" -$MOCHA_CLI src/*.test.ts +MOCHA_CLI="./node_modules/.bin/mocha" +TS_MOCHA_CLI="${MOCHA_CLI} -r ts-node/register" +echo "> $TS_MOCHA_CLI typescript/*.test.ts" +$TS_MOCHA_CLI typescript/*.test.ts + +echo "> $MOCHA_CLI esm/*.js" +$MOCHA_CLI esm/*.js diff --git a/entrypoints.json b/entrypoints.json new file mode 100644 index 0000000000..3c2d6c1e0c --- /dev/null +++ b/entrypoints.json @@ -0,0 +1,51 @@ +{ + "firebase-admin": { + "legacy": true, + "typings": "./lib/default-namespace.d.ts", + "dist": "./lib/index.js" + }, + "firebase-admin/app": { + "typings": "./lib/app/index.d.ts", + "dist": "./lib/app/index.js" + }, + "firebase-admin/auth": { + "typings": "./lib/auth/index.d.ts", + "dist": "./lib/auth/index.js" + }, + "firebase-admin/database": { + "typings": "./lib/database/index.d.ts", + "dist": "./lib/database/index.js" + }, + "firebase-admin/firestore": { + "typings": "./lib/firestore/index.d.ts", + "dist": "./lib/firestore/index.js" + }, + "firebase-admin/instance-id": { + "typings": "./lib/instance-id/index.d.ts", + "dist": "./lib/instance-id/index.js" + }, + "firebase-admin/messaging": { + "typings": "./lib/messaging/index.d.ts", + "dist": "./lib/messaging/index.js" + }, + "firebase-admin/machine-learning": { + "typings": "./lib/machine-learning/index.d.ts", + "dist": "./lib/machine-learning/index.js" + }, + "firebase-admin/project-management": { + "typings": "./lib/project-management/index.d.ts", + "dist": "./lib/project-management/index.js" + }, + "firebase-admin/security-rules": { + "typings": "./lib/security-rules/index.d.ts", + "dist": "./lib/security-rules/index.js" + }, + "firebase-admin/storage": { + "typings": "./lib/storage/index.d.ts", + "dist": "./lib/storage/index.js" + }, + "firebase-admin/remote-config": { + "typings": "./lib/remote-config/index.d.ts", + "dist": "./lib/remote-config/index.js" + } +} diff --git a/generate-esm-wrapper.js b/generate-esm-wrapper.js new file mode 100644 index 0000000000..3d47c709d1 --- /dev/null +++ b/generate-esm-wrapper.js @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2021 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. + */ + +const path = require('path'); +const fs = require('mz/fs'); + +async function main() { + const entryPoints = require('./entrypoints.json'); + for (const entryPoint in entryPoints) { + const info = entryPoints[entryPoint]; + if (info.legacy) { + continue; + } + + await generateEsmWrapper(entryPoint, info.dist); + } +} + +async function generateEsmWrapper(entryPoint, source) { + console.log(`Generating ESM wrapper for ${entryPoint}`); + const target = getTarget(entryPoint); + const output = getEsmOutput(source, target); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(target, output); + await fs.writeFile('./lib/esm/package.json', JSON.stringify({type: 'module'})); +} + +function getTarget(entryPoint) { + const child = entryPoint.replace('firebase-admin/', ''); + return `./lib/esm/${child}/index.js`; +} + +function getEsmOutput(source, target) { + const sourcePath = path.resolve(source); + const cjsSource = require.resolve(sourcePath); + const keys = getExports(cjsSource); + const targetPath = path.resolve(target); + const importPath = getImportPath(targetPath, cjsSource); + + let output = `import mod from ${JSON.stringify(importPath)};`; + output += '\n\n'; + for (const key of keys) { + output += `export const ${key} = mod.${key};\n`; + } + + return output; +} + +function getImportPath(from, to) { + const fromDir = path.dirname(from); + return path.relative(fromDir, to).replace(/\\/g, '/'); +} + +function getExports(cjsSource) { + const mod = require(cjsSource); + const keys = new Set(Object.getOwnPropertyNames(mod)); + keys.delete('__esModule'); + return [...keys].sort(); +} + +(async () => { + try { + await main(); + } catch (err) { + console.log(err); + process.exit(1); + } +})(); diff --git a/generate-reports.js b/generate-reports.js index 4d92f64563..129c06e542 100644 --- a/generate-reports.js +++ b/generate-reports.js @@ -31,38 +31,24 @@ const { local: localMode } = yargs // API Extractor configuration file. const config = require('./api-extractor.json'); -// List of module entry points. We generate a separate report for each entry point. -const entryPoints = { - 'firebase-admin': './lib/default-namespace.d.ts', - 'firebase-admin/app': './lib/app/index.d.ts', - 'firebase-admin/auth': './lib/auth/index.d.ts', - 'firebase-admin/database': './lib/database/index.d.ts', - 'firebase-admin/firestore': './lib/firestore/index.d.ts', - 'firebase-admin/instance-id': './lib/instance-id/index.d.ts', - 'firebase-admin/messaging': './lib/messaging/index.d.ts', - 'firebase-admin/machine-learning': './lib/machine-learning/index.d.ts', - 'firebase-admin/project-management': './lib/project-management/index.d.ts', - 'firebase-admin/security-rules': './lib/security-rules/index.d.ts', - 'firebase-admin/storage': './lib/storage/index.d.ts', - 'firebase-admin/remote-config': './lib/remote-config/index.d.ts', -}; - const tempConfigFile = 'api-extractor.tmp'; async function generateReports() { - for (const key in entryPoints) { - await generateReportForEntryPoint(key); + const entryPoints = require('./entrypoints.json'); + for (const entryPoint in entryPoints) { + const filePath = entryPoints[entryPoint].typings; + await generateReportForEntryPoint(entryPoint, filePath); } } -async function generateReportForEntryPoint(key) { - console.log(`\nGenerating API report for ${key}`) +async function generateReportForEntryPoint(entryPoint, filePath) { + console.log(`\nGenerating API report for ${entryPoint}`) console.log('========================================================\n'); - const safeName = key.replace('/', '.'); + const safeName = entryPoint.replace('/', '.'); console.log('Updating configuration for entry point...'); config.apiReport.reportFileName = `${safeName}.api.md`; - config.mainEntryPointFilePath = entryPoints[key]; + config.mainEntryPointFilePath = filePath; console.log(`Report file name: ${config.apiReport.reportFileName}`); console.log(`Entry point declaration: ${config.mainEntryPointFilePath}`); await fs.writeFile(tempConfigFile, JSON.stringify(config)); diff --git a/gulpfile.js b/gulpfile.js index 07184a93f9..a056ca2bc9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -41,7 +41,7 @@ var paths = { test: [ 'test/**/*.ts', - '!test/integration/typescript/src/example*.ts', + '!test/integration/postcheck/typescript/*.ts', ], build: 'lib/', diff --git a/package.json b/package.json index 107f5d0382..d75168d7ff 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "build": "gulp build", "build:tests": "gulp compile_test", - "prepare": "npm run build", + "prepare": "npm run build && npm run esm-wrap", "lint": "run-p lint:src lint:test", "test": "run-s lint test:unit", "integration": "run-s build test:integration", @@ -22,7 +22,8 @@ "lint:test": "eslint test/ --ext .ts", "apidocs": "node docgen/generate-docs.js --api node", "api-extractor": "node generate-reports.js", - "api-extractor:local": "npm run build && node generate-reports.js --local" + "api-extractor:local": "npm run build && node generate-reports.js --local", + "esm-wrap": "node generate-esm-wrapper.js" }, "nyc": { "extension": [ @@ -94,17 +95,50 @@ }, "exports": { ".": "./lib/index.js", - "./app": "./lib/app/index.js", - "./auth": "./lib/auth/index.js", - "./database": "./lib/database/index.js", - "./firestore": "./lib/firestore/index.js", - "./instance-id": "./lib/instance-id/index.js", - "./machine-learning": "./lib/machine-learning/index.js", - "./messaging": "./lib/messaging/index.js", - "./project-management": "./lib/project-management/index.js", - "./remote-config": "./lib/remote-config/index.js", - "./security-rules": "./lib/security-rules/index.js", - "./storage": "./lib/storage/index.js" + "./app": { + "require": "./lib/app/index.js", + "import": "./lib/esm/app/index.js" + }, + "./auth": { + "require": "./lib/auth/index.js", + "import": "./lib/esm/auth/index.js" + }, + "./database": { + "require": "./lib/database/index.js", + "import": "./lib/esm/database/index.js" + }, + "./firestore": { + "require": "./lib/firestore/index.js", + "import": "./lib/esm/firestore/index.js" + }, + "./instance-id": { + "require": "./lib/instance-id/index.js", + "import": "./lib/esm/instance-id/index.js" + }, + "./machine-learning": { + "require": "./lib/machine-learning/index.js", + "import": "./lib/esm/machine-learning/index.js" + }, + "./messaging": { + "require": "./lib/messaging/index.js", + "import": "./lib/esm/messaging/index.js" + }, + "./project-management": { + "require": "./lib/project-management/index.js", + "import": "./lib/esm/project-management/index.js" + }, + "./remote-config": { + "require": "./lib/remote-config/index.js", + "import": "./lib/esm/remote-config/index.js" + }, + "./security-rules": { + "require": "./lib/security-rules/index.js", + "import": "./lib/esm/security-rules/index.js" + }, + "./storage": { + "require": "./lib/storage/index.js", + "import": "./lib/esm/storage/index.js" + } }, "dependencies": { "@firebase/database": "^0.8.1", diff --git a/test/integration/postcheck/esm/example.test.js b/test/integration/postcheck/esm/example.test.js new file mode 100644 index 0000000000..323b226982 --- /dev/null +++ b/test/integration/postcheck/esm/example.test.js @@ -0,0 +1,121 @@ +/*! + * @license + * Copyright 2021 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 { expect } from 'chai'; + +import { cert, deleteApp, initializeApp } from 'firebase-admin/app'; +import { getAuth, Auth } from 'firebase-admin/auth'; +import { getDatabase, getDatabaseWithUrl, ServerValue } from 'firebase-admin/database'; +import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; +import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; +import { getMessaging, Messaging } from 'firebase-admin/messaging'; +import { getProjectManagement, ProjectManagement } from 'firebase-admin/project-management'; +import { getRemoteConfig, RemoteConfig } from 'firebase-admin/remote-config'; +import { getSecurityRules, SecurityRules } from 'firebase-admin/security-rules'; +import { getStorage, Storage } from 'firebase-admin/storage'; + +describe('ESM entry points', () => { + let app; + + before(() => { + app = initializeApp({ + credential: cert('mock.key.json'), + databaseURL: 'https://mock.firebaseio.com' + }, 'TestApp'); + }); + + after(() => { + return deleteApp(app); + }); + + it('Should return an initialized App', () => { + expect(app.name).to.equal('TestApp'); + }); + + it('Should return an Auth client', () => { + const client = getAuth(app); + expect(client).to.be.instanceOf(Auth); + }); + + it('Should return a Messaging client', () => { + const client = getMessaging(app); + expect(client).to.be.instanceOf(Messaging); + }); + + it('Should return a ProjectManagement client', () => { + const client = getProjectManagement(app); + expect(client).to.be.instanceOf(ProjectManagement); + }); + + it('Should return a SecurityRules client', () => { + const client = getSecurityRules(app); + expect(client).to.be.instanceOf(SecurityRules); + }); + + it('Should return a Database client', () => { + const db = getDatabase(app); + expect(db).to.be.not.undefined; + expect(typeof db.getRules).to.equal('function'); + }); + + it('Should return a Database client for URL', () => { + const db = getDatabaseWithUrl('https://other-mock.firebaseio.com', app); + expect(db).to.be.not.undefined; + expect(typeof db.getRules).to.equal('function'); + }); + + it('Should return a Database ServerValue', () => { + expect(ServerValue.increment(1)).to.be.not.undefined; + }); + + it('Should return a Cloud Storage client', () => { + const storage = getStorage(app); + expect(storage).to.be.instanceOf(Storage) + const bucket = storage.bucket('TestBucket'); + expect(bucket.name).to.equal('TestBucket'); + }); + + it('Should return a Firestore client', () => { + const firestore = getFirestore(app); + expect(firestore).to.be.instanceOf(Firestore); + }); + + it('Should return a Firestore FieldValue', () => { + expect(FieldValue.increment(1)).to.be.not.undefined; + }); + + it('Should return a DocumentReference', () => { + const ref = getFirestore(app).collection('test').doc(); + expect(ref).to.be.instanceOf(DocumentReference); + }); + + it('Should return an InstanceId client', () => { + const client = getInstanceId(app); + expect(client).to.be.instanceOf(InstanceId); + }); + + it('Should return a MachineLearning client', () => { + const client = getMachineLearning(app); + expect(client).to.be.instanceOf(MachineLearning); + }); + + it('Should return a RemoteConfig client', () => { + const client = getRemoteConfig(app); + expect(client).to.be.instanceOf(RemoteConfig); + }); +}); diff --git a/test/integration/postcheck/esm/package.json b/test/integration/postcheck/esm/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/test/integration/postcheck/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/integration/typescript/package.json b/test/integration/postcheck/package.json similarity index 63% rename from test/integration/typescript/package.json rename to test/integration/postcheck/package.json index 8f467c2a41..6345381702 100644 --- a/test/integration/typescript/package.json +++ b/test/integration/postcheck/package.json @@ -1,5 +1,5 @@ { - "name": "firebase-admin-typescript-test", + "name": "firebase-admin-postcheck", "version": "1.0.0", "description": "Firebase Admin SDK post package test cases", "license": "Apache-2.0", @@ -8,12 +8,12 @@ "url": "https://github.com/firebase/firebase-admin-node" }, "devDependencies": { - "@types/chai": "^3.4.35", + "@types/chai": "^4.0.0", "@types/mocha": "^2.2.48", - "@types/node": "^8.10.59", - "chai": "^3.5.0", - "mocha": "^5.2.0", - "ts-node": "^3.3.0", + "@types/node": "^10.10.0", + "chai": "^4.2.0", + "mocha": "^8.0.0", + "ts-node": "^9.0.0", "typescript": "^3.7.3" } } diff --git a/test/integration/typescript/tsconfig.json b/test/integration/postcheck/tsconfig.json similarity index 89% rename from test/integration/typescript/tsconfig.json rename to test/integration/postcheck/tsconfig.json index 6820c83387..85001669ec 100644 --- a/test/integration/typescript/tsconfig.json +++ b/test/integration/postcheck/tsconfig.json @@ -11,6 +11,6 @@ ] }, "files": [ - "src/example.ts" + "./typescript/example.ts" ] } diff --git a/test/integration/typescript/src/example-modular.test.ts b/test/integration/postcheck/typescript/example-modular.test.ts similarity index 100% rename from test/integration/typescript/src/example-modular.test.ts rename to test/integration/postcheck/typescript/example-modular.test.ts diff --git a/test/integration/typescript/src/example.test.ts b/test/integration/postcheck/typescript/example.test.ts similarity index 100% rename from test/integration/typescript/src/example.test.ts rename to test/integration/postcheck/typescript/example.test.ts diff --git a/test/integration/typescript/src/example.ts b/test/integration/postcheck/typescript/example.ts similarity index 100% rename from test/integration/typescript/src/example.ts rename to test/integration/postcheck/typescript/example.ts