booletwa
Version:
Generate TWA projects from a Web Manifest
268 lines (239 loc) • 9.97 kB
text/typescript
/*
* Copyright 2021 Google Inc. All Rights Reserved.
*
* 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 {GooglePlay, TwaManifest, asPlayStoreTrack} from '@bubblewrap/core';
import * as fs from 'fs';
import * as path from 'path';
import {TWA_MANIFEST_FILE_NAME} from '../constants';
import {Prompt, InquirerPrompt} from '../Prompt';
import {updateProject} from './shared';
import {enUS} from '../strings';
export interface PlayArgs {
track?: string;
serviceAccountFile?: string;
manifest?: string;
appBundleLocation?: string;
targetDirectory?: string;
retain?: number;
remove?: number;
list?: boolean;
add?: number;
}
// Default file path
const defaultSignedAppBundleFileName = 'app-release-bundle.aab';
type PlayCommand = 'publish' | 'retain' | 'versionCheck';
/**
* The Play class is the class that is used to communicate with the Google Play Store.
*/
class Play {
constructor(
private args: PlayArgs,
private googlePlay: GooglePlay,
private prompt: Prompt = new InquirerPrompt(),
) {}
/**
* @summary Can validate the largest version number vs twa-manifest.json and update
* to give x+1 version number.
* @return {number} The largest version number found in the play console.
*/
async getLargestVersion(twaManifest: TwaManifest): Promise<number> {
const versionCode = await this.googlePlay.performPlayOperation(
twaManifest.packageId, async (editId: string): Promise<number> => {
return await this.googlePlay.getLargestVersionCode(twaManifest.packageId, editId);
});
return versionCode;
}
/**
* Publishes the Android App Bundle to the user specified {@link PlayStoreTrack} from the
* {@link PlayArgs}.
* @return {boolean} Whether the publish command completes successfully or not.
*/
async publish(twaManifest: TwaManifest): Promise<boolean> {
// Validate that the publish value is listed in the available Tracks.
// If no value was supplied with publish we make it internal.
const userSelectedTrack = asPlayStoreTrack(this.args.track?.toLowerCase() || 'internal');
if (userSelectedTrack == null) {
this.prompt.printMessage(enUS.messageInvalidTrack);
return false;
}
const defaultPath = path.join(process.cwd(), defaultSignedAppBundleFileName);
const publishFilePath = this.args.appBundleLocation || defaultPath;
if (!fs.existsSync(publishFilePath)) {
throw new Error(`App Bundle not found on disk: ${publishFilePath}`);
}
const retainedBundles = twaManifest.retainedBundles;
await this.googlePlay.performPlayOperation(twaManifest.packageId,
async (editId: string): Promise<void> => {
return await this.googlePlay.publishBundle(
userSelectedTrack, publishFilePath, twaManifest.packageId, retainedBundles, editId);
}, true);
return true;
}
/**
* Updates the gradle file based on the updates to the twa-manifest.json file and warns the user
* that they need to call build again.
* @param {string} manifestFile - The path to the the TwaManifest JSON file.
* @param {string} appVersionName - Optional: Changes the string representation of the version.
*/
private async updateProjectAndWarn(manifestFile: string, appVersionName?: string): Promise<void> {
await updateProject(
true,
appVersionName || null,
this.prompt,
this.args.targetDirectory || process.cwd(),
manifestFile);
this.prompt.printMessage(enUS.messageCallBubblewrapBuild);
}
/**
* Runs the playRetain command. This handles the retaining of packages that are published on Play.
* @return {boolean} Returns whether or not the run command completed successfully.
*/
async runRetain(): Promise<boolean> {
const manifestFile = this.args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME);
const twaManifest = await TwaManifest.fromFile(manifestFile);
// bubblewrap playRetain --add 86
if (this.args.add) {
const versionToRetain = this.args.add;
// Validate an integer was supplied.
if (!Number.isInteger(versionToRetain)) {
throw new Error(enUS.versionRetainedNotAnInteger);
}
if (versionToRetain > twaManifest.appVersionCode) {
// Cannot retain a higher version as that would take precedence.
await this.prompt.printMessage(
enUS.versionToRetainHigherThanBuildVersion(
twaManifest.appVersionCode, versionToRetain));
}
// Validate that the version exists on the Play Servers.
const exists = await this.googlePlay.performPlayOperation(twaManifest.packageId,
async (editId: string): Promise<boolean> => {
return await this.googlePlay.versionExists(
twaManifest.packageId, versionToRetain, editId);
});
if (!exists) {
throw new Error(enUS.versionDoesNotExistOnServer);
}
const alreadyRetained = twaManifest.retainedBundles.find(
(version) => version == versionToRetain);
if (!alreadyRetained) {
twaManifest.retainedBundles.push(versionToRetain);
}
await twaManifest.saveToFile(manifestFile);
}
// bubblewrap playRetain --remove 86
if (this.args.remove) {
const versionToRemove = this.args.remove;
twaManifest.retainedBundles = twaManifest.retainedBundles.filter((obj) => {
return obj != versionToRemove;
});
await twaManifest.saveToFile(manifestFile);
}
// bubblewrap playRetain --list
if (this.args.list) {
twaManifest.retainedBundles.forEach((version) => {
this.prompt.printMessage(`${version}`);
});
}
return true;
}
/**
* Calls the publish workflow to the Play Store. This takes a user supplied track (or internal)
* and an appBundleLocation (defaults to the current directory) and uploads these artifacts to
* the Google Play Store.
* @returns whether the playPublish command completed successfully
*/
async runPlayPublish(): Promise<boolean> {
const manifestFile = this.args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME);
const twaManifest = await TwaManifest.fromFile(manifestFile);
const success = await this.publish(twaManifest);
if (!success) {
this.prompt.printMessage(enUS.messagePublishingWasNotSuccessful);
return false;
}
this.prompt.printMessage(enUS.messagePlayUploadSuccess);
return true;
}
/**
* Runs the version check workflow. If the published version is higher than that of the twaManifest,
* we assume that the version that exists locally is needs to be updated to a higher version.
*/
async runVersionCheck(): Promise<void> {
const manifestFile = this.args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME);
const twaManifest = await TwaManifest.fromFile(manifestFile);
// bubblewrap playVersionCheck
const version = await this.getLargestVersion(twaManifest);
if (version >= twaManifest.appVersionCode) {
const updateVersion = await this.prompt.promptConfirm(enUS.promptVersionMismatch(
twaManifest.appVersionCode.toString(), version.toString()), true);
if (updateVersion) {
if (twaManifest.appVersionCode.toString() == twaManifest.appVersionName) {
twaManifest.appVersionName = (version + 1).toString();
}
twaManifest.appVersionCode = version + 1;
await twaManifest.saveToFile(manifestFile);
await this.updateProjectAndWarn(manifestFile);
}
}
}
}
/**
* Validates that the service account JSON file exists.
* @param {string | undefined} path - The path the the JSON file.
* @return {boolean} Whether or not the JSON file exists.
*/
function validServiceAccountJsonFile(path: string | undefined): boolean {
if (path == undefined) {
return false;
}
if (!fs.existsSync(path)) {
return false;
}
return true;
}
async function setupGooglePlay(args: PlayArgs): Promise<GooglePlay> {
const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME);
const twaManifest = await TwaManifest.fromFile(manifestFile);
// Update the TWA-Manifest if service account is supplied
// bubblewrap play --serviceAccountFile="/path/to/service-account.json"
// --manifest="/path/twa-manifest.json"
if (args.serviceAccountFile) {
twaManifest.serviceAccountJsonFile = args.serviceAccountFile;
await twaManifest.saveToFile(manifestFile);
}
if (!twaManifest.serviceAccountJsonFile ||
!validServiceAccountJsonFile(twaManifest.serviceAccountJsonFile)) {
throw new Error(enUS.messageServiceAccountJSONMissing);
}
// Setup Google Play since we can confirm that the serviceAccountJsonFile is valid.
return new GooglePlay(twaManifest.serviceAccountJsonFile);
}
export async function play(parsedArgs: PlayArgs,
command: PlayCommand, prompt: Prompt = new InquirerPrompt()): Promise<boolean> {
// TODO(@nohe427): Remove after experimental
if (!await prompt.promptConfirm(enUS.promptExperimentalFeature, false)) {
return true;
}
const googlePlay = await setupGooglePlay(parsedArgs);
const play = new Play(parsedArgs, googlePlay, prompt);
switch (command) {
case 'publish':
return await play.runPlayPublish();
case 'versionCheck':
await play.runVersionCheck();
return true;
case 'retain':
return await play.runRetain();
}
}