voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
518 lines (457 loc) • 14.1 kB
text/typescript
/**
* @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 { spawn, exec } from 'child-process-promise';
import ora from 'ora';
import { createPromptModule } from 'inquirer';
import { projectRoot, readPackageJson } from '../utils';
import simpleGit from 'simple-git/promise';
import { mapWorkspaceToPackages } from '../release/utils/workspace';
import { inc } from 'semver';
import { writeFile as _writeFile, rmdirSync, existsSync } from 'fs';
import { resolve } from 'path';
import { promisify } from 'util';
import chalk from 'chalk';
import Listr from 'listr';
import {
prepare as prepareFirestoreForRelease,
createFirestoreCompatProject
} from './prepare-firestore-for-exp-release';
import {
createStorageCompatProject,
prepare as prepareStorageForRelease
} from './prepare-storage-for-exp-release';
import {
createDatabaseCompatProject,
prepare as prepareDatabaseForRelease
} from './prepare-database-for-exp-release';
import * as yargs from 'yargs';
import { addCompatToFirebasePkgJson } from './prepare-util';
const prompt = createPromptModule();
const argv = yargs
.options({
dryRun: {
type: 'boolean',
default: false
}
})
.help().argv;
const writeFile = promisify(_writeFile);
const git = simpleGit(projectRoot);
const FIREBASE_UMBRELLA_PACKAGE_NAME = 'firebase-exp';
async function publishExpPackages({ dryRun }: { dryRun: boolean }) {
try {
/**
* Welcome to the firebase release CLI!
*/
console.log(
`Welcome to the Firebase Exp Packages release CLI! dryRun: ${dryRun}`
);
/**
* Update fields in package.json and create compat packages (e.g. packages-exp/firestore-compat)
*/
await prepareFirestoreForRelease();
await prepareStorageForRelease();
await prepareDatabaseForRelease();
// path to exp packages
let packagePaths = await mapWorkspaceToPackages([
`${projectRoot}/packages-exp/*`
]);
packagePaths.push(`${projectRoot}/packages/firestore`);
packagePaths.push(`${projectRoot}/packages/storage`);
packagePaths.push(`${projectRoot}/packages/database`);
packagePaths.push(`${projectRoot}/packages/firestore/compat`);
packagePaths.push(`${projectRoot}/packages/storage/compat`);
packagePaths.push(`${projectRoot}/packages/database/compat`);
/**
* Bumps the patch version of firebase-exp package regardless if there is any update
* since the last release. This simplifies the script and works fine for exp packages.
*
* We do this before the build so the registerVersion will grab the correct
* versions from package.json for platform logging.
*/
const versions = await getNewVersions(packagePaths);
await updatePackageJsons(packagePaths, versions, {
// Changing the package names here will break the build process.
// It will be done after the build.
removeExpInName: false,
updateVersions: true,
makePublic: true
});
/**
* build packages except for the umbrella package (firebase) which will be built after firestore/storage/database compat packages are created
*/
await buildPackages();
/**
* Create compat packages for Firestore, Database and Storage
*/
await createFirestoreCompatProject();
await createStorageCompatProject();
await createDatabaseCompatProject();
/**
* Add firestore-compat, database-compat and storage-compat to the dependencies array of firebase-exp
*/
await addCompatToFirebasePkgJson([
'@firebase/firestore-compat',
'@firebase/storage-compat',
'@firebase/database-compat'
]);
/**
* build firebase
*/
await buildFirebasePackage();
/**
* Removes -exp in package names because we will publish them using
* the existing package names under a special release tag (firebase@exp).
*/
await updatePackageJsons(packagePaths, versions, {
removeExpInName: true,
updateVersions: false,
makePublic: false
});
let versionCheckMessage =
'\r\nAre you sure these are the versions you want to publish?\r\n';
for (const [pkgName, version] of versions) {
versionCheckMessage += `${pkgName} : ${version}\n`;
}
const { versionCheck } = await prompt([
{
type: 'confirm',
name: 'versionCheck',
message: versionCheckMessage,
default: false
}
]);
if (!versionCheck) {
throw new Error('Version check failed');
}
// publish all packages under packages-exp plus firestore, storage and database
const packagesToPublish = await mapWorkspaceToPackages([
`${projectRoot}/packages-exp/*`
]);
packagesToPublish.push(`${projectRoot}/packages/firestore`);
packagesToPublish.push(`${projectRoot}/packages/storage`);
packagesToPublish.push(`${projectRoot}/packages/database`);
/**
* Release packages to NPM
*/
await publishToNpm(packagesToPublish, dryRun);
/**
* reset the working tree to recover package names with -exp in the package.json files,
* then bump patch version of firebase-exp (the umbrella package) only
*/
const firebaseExpVersion = new Map<string, string>();
firebaseExpVersion.set(
FIREBASE_UMBRELLA_PACKAGE_NAME,
versions.get(FIREBASE_UMBRELLA_PACKAGE_NAME)
);
const firebaseExpPath = packagePaths.filter(p =>
p.includes(FIREBASE_UMBRELLA_PACKAGE_NAME)
);
const { resetWorkingTree } = await prompt([
{
type: 'confirm',
name: 'resetWorkingTree',
message: 'Do you want to reset the working tree?',
default: true
}
]);
if (resetWorkingTree) {
await resetWorkingTreeAndBumpVersions(
firebaseExpPath,
firebaseExpVersion
);
} else {
process.exit(0);
}
/**
* Do not push to remote if it's a dryrun
*/
if (!dryRun) {
const { shouldCommitAndPush } = await prompt([
{
type: 'confirm',
name: 'shouldCommitAndPush',
message:
'Do you want to commit and push the exp version update to remote?',
default: true
}
]);
/**
* push to github
*/
if (shouldCommitAndPush) {
await commitAndPush(versions);
}
}
} catch (err) {
/**
* Log any errors that happened during the process
*/
console.error(err);
/**
* Exit with an error code
*/
process.exit(1);
}
}
/**
* The order of build is important
*/
async function buildPackages() {
const spinner = ora(' Building Packages').start();
// Build dependencies
await spawn(
'yarn',
[
'lerna',
'run',
'--scope',
'@firebase/util',
'--scope',
'@firebase/component',
'--scope',
'@firebase/logger',
'--scope',
'@firebase/webchannel-wrapper',
'build'
],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
// Build exp and compat packages except for firebase-exp
await spawn(
'yarn',
[
'lerna',
'run',
'--scope',
'@firebase/*-exp',
'--scope',
'@firebase/*-compat',
'build:release'
],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
// Build exp packages developed in place
// Firestore and firestore-compat
await spawn(
'yarn',
['lerna', 'run', '--scope', '@firebase/firestore', 'prebuild'],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
await spawn(
'yarn',
['lerna', 'run', '--scope', '@firebase/firestore', 'build:exp:release'],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
// Storage
await spawn(
'yarn',
['lerna', 'run', '--scope', '@firebase/storage', 'build:exp:release'],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
// Database
await spawn(
'yarn',
['lerna', 'run', '--scope', '@firebase/database', 'build:exp:release'],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
// remove packages/installations/dist, otherwise packages that depend on packages-exp/installations-exp (e.g. Perf, FCM)
// will incorrectly reference packages/installations.
const installationsDistDirPath = resolve(
projectRoot,
'packages/installations/dist'
);
if (existsSync(installationsDistDirPath)) {
rmdirSync(installationsDistDirPath, { recursive: true });
}
spinner.stopAndPersist({
symbol: '✅'
});
}
async function buildFirebasePackage() {
const spinner = ora(' Building firebase').start();
// Build firebase-exp
await spawn(
'yarn',
['lerna', 'run', '--scope', 'firebase-exp', 'build:release'],
{
cwd: projectRoot,
stdio: 'inherit'
}
);
spinner.stopAndPersist({
symbol: '✅'
});
}
async function getNewVersions(packagePaths: string[]) {
// get package name -> next version mapping
const versions = new Map();
for (const path of packagePaths) {
const { version, name } = await readPackageJson(path);
// increment firebase-exp as a v9 prerelease, e.g. 9.0.0-beta.0 -> 9.0.0-beta.1
if (name === FIREBASE_UMBRELLA_PACKAGE_NAME) {
const nextVersion = inc(version, 'prerelease');
versions.set(name, nextVersion);
} else {
// create individual packages version
// we can't use minor version for them because most of them
// are still in the pre-major version officially.
const nextVersion = `${version}-exp.${await getCurrentSha()}`;
versions.set(name, nextVersion);
}
}
return versions;
}
async function publishToNpm(packagePaths: string[], dryRun = false) {
const taskArray = await Promise.all(
packagePaths.map(async pp => {
const { version, name } = await readPackageJson(pp);
return {
title: `📦 ${name}@${version}`,
task: () => publishPackage(pp, dryRun)
};
})
);
const tasks = new Listr(taskArray, {
concurrent: false,
exitOnError: false
});
console.log('\r\nPublishing Packages to NPM:');
return tasks.run();
}
async function publishPackage(packagePath: string, dryRun: boolean) {
const args = ['publish', '--access', 'public', '--tag', 'beta'];
if (dryRun) {
args.push('--dry-run');
}
await spawn('npm', args, { cwd: packagePath });
}
async function resetWorkingTreeAndBumpVersions(
packagePaths: string[],
versions: Map<string, string>
) {
console.log('Resetting working tree');
await git.checkout('.');
await updatePackageJsons(packagePaths, versions, {
removeExpInName: false,
updateVersions: true,
makePublic: false
});
}
async function updatePackageJsons(
packagePaths: string[],
versions: Map<string, string>,
{
removeExpInName,
updateVersions,
makePublic
}: {
removeExpInName: boolean;
updateVersions: boolean;
makePublic: boolean;
}
) {
for (const path of packagePaths) {
const packageJsonPath = `${path}/package.json`;
const packageJson = await readPackageJson(path);
// update version
if (updateVersions) {
const nextVersion = versions.get(packageJson.name);
console.log(
chalk`Updating {blue ${packageJson.name}} from {green ${packageJson.version}} to {green ${nextVersion}}`
);
packageJson.version = nextVersion;
}
// remove -exp in the package name
if (removeExpInName) {
const cleanName = removeExpInPackageName(packageJson.name);
console.log(
chalk`Renaming {blue ${packageJson.name}} to {blue ${cleanName}}`
);
packageJson.name = cleanName;
// update dep version and remove -exp in dep names
// don't care about devDependencies because they are irrelavant when using the package
const depTypes = ['dependencies', 'peerDependencies'];
for (const depType of depTypes) {
const dependencies = packageJson[depType] || {};
const newDependenciesObj: { [key: string]: string } = {};
for (const d of Object.keys(dependencies)) {
const dNextVersion = versions.get(d);
const nameWithoutExp = removeExpInPackageName(d);
if (!dNextVersion) {
newDependenciesObj[nameWithoutExp] = dependencies[d];
} else {
newDependenciesObj[nameWithoutExp] = dNextVersion;
}
}
packageJson[depType] = newDependenciesObj;
}
}
// set private to false
if (makePublic) {
packageJson.private = false;
}
// update package.json files
await writeFile(
packageJsonPath,
`${JSON.stringify(packageJson, null, 2)}\n`,
{ encoding: 'utf-8' }
);
}
}
async function commitAndPush(versions: Map<string, string>) {
await exec('git add packages-exp/firebase-exp/package.json yarn.lock');
const firebaseExpVersion = versions.get(FIREBASE_UMBRELLA_PACKAGE_NAME);
await exec(`git commit -m "Publish firebase ${firebaseExpVersion || ''}"`);
let { stdout: currentBranch, stderr } = await exec(
`git rev-parse --abbrev-ref HEAD`
);
currentBranch = currentBranch.trim();
await exec(`git push origin ${currentBranch} --no-verify -u`, {
cwd: projectRoot
});
}
function removeExpInPackageName(name: string) {
const regex = /^(.*firebase.*)-exp(.*)$/g;
const captures = regex.exec(name);
if (!captures) {
return name;
}
return `${captures[1]}${captures[2]}`;
}
async function getCurrentSha() {
return (await git.revparse(['--short', 'HEAD'])).trim();
}
publishExpPackages(argv);