feat(build): introduce experimental `rnx-build` command (#1659)

This commit is contained in:
Tommy Nguyen 2022-06-21 10:15:50 +02:00 коммит произвёл GitHub
Родитель 8391972e9e
Коммит 9341c417e5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 1230 добавлений и 14 удалений

Просмотреть файл

@ -0,0 +1,5 @@
---
"@rnx-kit/build": minor
---
@rnx-kit/build builds your app in the cloud

81
.github/workflows/rnx-build.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,81 @@
name: rnx-build
on:
workflow_dispatch:
inputs:
packageManager:
description: "Supported package managers are `npm`, `yarn`, `pnpm` (v6.10+)"
required: true
default: "yarn"
platform:
description: "Supported platforms are `android`, `ios`, `macos`, `windows`"
required: true
projectRoot:
description: "Root of the project"
required: true
jobs:
build-android:
name: "Build Android"
if: ${{ github.event.inputs.platform == 'android' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3.3.0
with:
distribution: temurin
java-version: 11
- name: Set up Node 16
uses: actions/setup-node@v3.3.0
with:
node-version: 16
cache: ${{ github.event.inputs.packageManager }}
- name: Install npm dependencies
run: ${{ github.event.inputs.packageManager }} install
- name: Build Android app
uses: gradle/gradle-build-action@v2.2.0
with:
gradle-version: wrapper
arguments: --no-daemon clean assembleDebug
build-root-directory: ${{ github.event.inputs.projectRoot }}/android
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: android-artifact
path: ${{ github.event.inputs.projectRoot }}/android/app/build/outputs/apk/debug/app-debug.apk
build-ios:
name: "Build iOS"
if: ${{ github.event.inputs.platform == 'ios' }}
runs-on: macos-11
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Node 16
uses: actions/setup-node@v3.3.0
with:
node-version: 16
cache: ${{ github.event.inputs.packageManager }}
- name: Install npm dependencies
run: ${{ github.event.inputs.packageManager }} install
- name: Install Pods
run: pod install --project-directory=ios
working-directory: ${{ github.event.inputs.projectRoot }}
- name: Build iOS app
run: |
device_name='iPhone 13'
device=$(xcrun simctl list devices "${device_name}" available | grep "${device_name} (")
re='\(([-0-9A-Fa-f]+)\)'
[[ $device =~ $re ]] || exit 1
xcworkspace=$(find . -maxdepth 1 -name '*.xcworkspace' -type d | head -1)
xcodebuild -workspace ${xcworkspace} -scheme ReactTestApp -destination "id=${BASH_REMATCH[1]}" -configuration Debug -derivedDataPath DerivedData CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO build
working-directory: ${{ github.event.inputs.projectRoot }}/ios
- name: Prepare build artifact
run: |
tar -cvf ios-artifact.tar -C DerivedData/Build/Products/Debug-iphonesimulator ReactTestApp.app
shasum --algorithm 512 ios-artifact.tar
working-directory: ${{ github.event.inputs.projectRoot }}/ios
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: ios-artifact
path: ${{ github.event.inputs.projectRoot }}/ios/ios-artifact.tar

1
.gitignore поставляемый
Просмотреть файл

@ -8,6 +8,7 @@
.idea/
Pods/
coverage/
/packages/*/*/rnx-build/
/packages/*/bin/
/packages/*/dist/
/packages/*/lib/

70
incubator/build/README.md Normal file
Просмотреть файл

@ -0,0 +1,70 @@
# @rnx-kit/build
[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
[![npm version](https://img.shields.io/npm/v/@rnx-kit/build)](https://www.npmjs.com/package/@rnx-kit/build)
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
### This tool is EXPERIMENTAL - USE WITH CAUTION
🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧
An experimental tool for building your native apps in the cloud.
## Requirements
🚧 TODO: Reduce the number of requirements
- GitHub hosted repository
- [Android Studio](https://developer.android.com/studio)
- [Node.js](https://nodejs.org/en/download/) LTS 14.15 or greater
- [Xcode](https://developer.apple.com/xcode/)
## Usage
🚧 TODO: Not quite ready for general consumption
At the moment, running these two commands should trigger an iOS build, install
it in a simulator, and launch the app.
```sh
yarn build
yarn rnx-build
```
## Contributors' Notes
### TODO
- [x] Figure out how to push/restore local Git state
- [x] Figure out how to trigger workflow
- [x] Figure out how to fetch job state
- [x] Figure out how to push artifacts
- [x] Android
- [x] iOS
- [x] Figure out how to download artifacts
- [x] Android
- [x] iOS
- [ ] Figure out how to install artifacts
- [x] Android emulator
- [ ] Android device
- [x] iOS simulator
- [ ] iOS device
- [x] Miscellaneous cleanup
- [x] Implement proper CLI
- [x] Download build artifacts to platform specific folders
- [x] iOS: Detect workspace to build
- [ ] Cancel build job when user ctrl+c in the terminal
- [ ] Figure out appropriate storage for auth tokens
- [ ] Add `init` or `install` command to copy the correct workflow file to
user's repo
- [ ] Figure out how to install artifacts with QR code
- [ ] Figure out caching
- [ ] Figure out how to skip native build when cached
- [ ] Implement support for macOS
- [ ] Implement support for Windows
### Open Questions
- Can we avoid depending on Android SDK?
- Can we avoid depending on Xcode?

Просмотреть файл

@ -0,0 +1,53 @@
{
"private": true,
"name": "@rnx-kit/build",
"version": "0.0.0",
"description": "EXPERIMENTAL - USE WITH CAUTION - New package called build",
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/incubator/build#readme",
"license": "MIT",
"author": {
"name": "Microsoft Open Source",
"email": "microsoftopensource@users.noreply.github.com"
},
"files": [
"lib/*"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
"bin": {
"rnx-build": "lib/index.js"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/rnx-kit",
"directory": "incubator/build"
},
"scripts": {
"build": "rnx-kit-scripts build",
"format": "rnx-kit-scripts format",
"lint": "rnx-kit-scripts lint",
"rnx-build": "node lib/index.js"
},
"dependencies": {
"@octokit/core": "^3.6.0",
"@octokit/plugin-rest-endpoint-methods": "^5.14.0",
"ora": "^5.4.1",
"pkg-dir": "^5.0.0",
"yargs": "^16.0.0",
"yauzl": "2.10.0"
},
"devDependencies": {
"@rnx-kit/scripts": "*",
"@types/yauzl": "2.10.0"
},
"engines": {
"node": ">=14.15"
},
"eslintConfig": {
"extends": "@rnx-kit/eslint-config"
},
"jest": {
"preset": "@rnx-kit/scripts"
},
"experimental": true
}

Просмотреть файл

@ -0,0 +1,28 @@
import * as fs from "node:fs";
import * as path from "node:path";
import yauzl from "yauzl";
export function extract(filename: string): Promise<string> {
return new Promise((resolve, reject) => {
const destination = path.dirname(filename);
yauzl.open(filename, { lazyEntries: true }, (err, zipFile) => {
if (err) {
reject(err);
}
zipFile.readEntry();
zipFile.on("entry", (entry) => {
zipFile.openReadStream(entry, (err, readStream) => {
if (err) {
reject(err);
}
const entryFilename = path.join(destination, entry.fileName);
readStream
.pipe(fs.createWriteStream(entryFilename))
.once("finish", () => resolve(entryFilename));
});
});
});
});
}

Просмотреть файл

@ -0,0 +1,20 @@
export function idle(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function withRetries<R>(
func: () => Promise<R>,
retries: number,
counter = 0
): Promise<R> {
try {
return await func();
} catch (e) {
if (retries === 1) {
throw e;
}
}
await idle(Math.pow(2, counter) * 1000);
return withRetries(func, retries - 1, counter + 1);
}

Просмотреть файл

@ -0,0 +1,68 @@
import ora from "ora";
import { extract } from "./archive";
import { deleteBranch, pushCurrentChanges } from "./git";
import { installAndLaunchApk } from "./platforms/android";
import { installAndLaunchApp } from "./platforms/ios";
import type { BuildParams, Remote, RepositoryInfo } from "./types";
export async function startBuild(
remote: Remote,
repoInfo: RepositoryInfo,
inputs: BuildParams
): Promise<number> {
const spinner = ora();
if (!remote.isSetUp(spinner)) {
return 1;
}
spinner.start("Creating build branch");
const upstream = "origin";
const buildBranch = await pushCurrentChanges(upstream);
if (!buildBranch) {
return 1;
}
spinner.succeed(`Created build branch ${buildBranch}`);
const cleanUp = async () => {
spinner.start(`Deleting ${buildBranch}`);
await deleteBranch(buildBranch, upstream);
spinner.succeed(`Deleted ${buildBranch}`);
};
const onSignal = () => {
spinner.fail();
cleanUp().then(() => process.exit(1));
};
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
spinner.start("Queueing build");
try {
const context = { ...repoInfo, ref: buildBranch };
const artifactFile = await remote.build(context, inputs, spinner);
spinner.start("Extracting build artifact");
const buildArtifact = await extract(artifactFile);
spinner.succeed(`Extracted ${buildArtifact}`);
switch (inputs.platform) {
case "android":
await installAndLaunchApk(buildArtifact, undefined, spinner);
break;
case "ios":
await installAndLaunchApp(buildArtifact, undefined, spinner);
break;
default:
break;
}
} catch (e) {
spinner.fail();
await cleanUp();
throw e;
}
await cleanUp();
return 0;
}

Просмотреть файл

@ -0,0 +1,48 @@
import type { SpawnSyncReturns } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
type SpawnResult = Pick<
SpawnSyncReturns<string>,
"stdout" | "stderr" | "status"
>;
type Command = (...args: string[]) => Promise<SpawnResult>;
type CommandSync = (...args: string[]) => SpawnResult;
export function ensure(result: SpawnResult, message = result.stderr): string {
if (result.status !== 0) {
throw new Error(message);
}
return result.stdout;
}
export function makeCommand(command: string, options = {}): Command {
return (...args: string[]) => {
return new Promise((resolve) => {
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const cmd = spawn(command, args, options);
cmd.stdout.on("data", (data) => {
stdout.push(data);
});
cmd.stderr.on("data", (data) => {
stderr.push(data);
});
cmd.on("close", (status) => {
resolve({
stdout: Buffer.concat(stdout).toString().trim(),
stderr: Buffer.concat(stderr).toString().trim(),
status,
});
});
});
};
}
export function makeCommandSync(command: string): CommandSync {
return (...args: string[]) => spawnSync(command, args, { encoding: "utf-8" });
}

Просмотреть файл

@ -0,0 +1,9 @@
import * as os from "node:os";
import * as path from "node:path";
export const MAX_ATTEMPTS = 8;
export const MAX_DOWNLOAD_ATTEMPTS = 3;
export const BUILD_ID = "rnx-build";
export const USER_CONFIG_FILE = path.join(os.homedir(), `.${BUILD_ID}.json`);
export const WORKFLOW_ID = BUILD_ID + ".yml";

111
incubator/build/src/git.ts Normal file
Просмотреть файл

@ -0,0 +1,111 @@
import { makeCommand, makeCommandSync } from "./command";
import { BUILD_ID } from "./constants";
const git = makeCommand("git");
const gitSync = makeCommandSync("git");
function commitStagedChanges(): () => void {
if (gitSync("diff", "--staged", "--exit-code").status === 0) {
return () => undefined;
}
gitSync("commit", "--message", `"[${BUILD_ID}] changes to be committed"`);
return () => gitSync("reset", "--soft", "@^");
}
function commitUnstagedChanges(): () => void {
if (gitSync("diff", "--exit-code").status === 0) {
return () => undefined;
}
gitSync("add", "--all");
gitSync(
"commit",
"--message",
`"[${BUILD_ID}] changes not staged for commit"`
);
return () => gitSync("reset", "--mixed", "@^");
}
function ensureDoubleDigit(n: number): string {
return n < 10 ? "0" + n : n.toString();
}
function generateBranchName(): string {
const currentBranch = gitSync("branch", "--show-current").stdout.trim();
const sha = gitSync("rev-list", "-1", "HEAD").stdout.substring(0, 8);
const now = new Date();
const year = now.getUTCFullYear();
const month = ensureDoubleDigit(now.getUTCMonth());
const date = ensureDoubleDigit(now.getUTCDate());
const hours = ensureDoubleDigit(now.getUTCHours());
const minutes = ensureDoubleDigit(now.getUTCMinutes());
const seconds = ensureDoubleDigit(now.getUTCSeconds());
const milliseconds = now.getUTCMilliseconds();
return `${BUILD_ID}/${currentBranch}/${year}${month}${date}-${hours}${minutes}${seconds}.${milliseconds}-${sha}`;
}
/**
* Deletes the specified branch/tag.
* @param ref The branch/tag to delete
* @param upstream Deletes the upstream (tracking) reference if specified
*/
export async function deleteBranch(
ref: string,
upstream?: string
): Promise<void> {
gitSync("branch", "--delete", "--force", ref);
if (upstream) {
await git("push", upstream, ":" + ref);
}
}
/**
* Returns the remote URL.
* @param upstream Upstream (tracking) reference
* @returns Remote URL
*/
export function getRemoteUrl(upstream = "origin"): string {
return gitSync("remote", "get-url", upstream).stdout.trim();
}
/**
* Returns the path to the root of the repository.
* @returns Path to the root of the repository
*/
export function getRepositoryRoot(): string {
return gitSync("rev-parse", "--show-toplevel").stdout.trim();
}
/**
* Push current changes to remote.
* @param upstream Upstream (tracking) reference
* @returns The name of the branch if successfully pushed to remote
*/
export async function pushCurrentChanges(
upstream: string
): Promise<string | false> {
const buildBranch = generateBranchName();
// The order of the following operations is important. We want to push
// everything that the user currently has, even untracked files, then restore
// everything to its original state, including staged files. And it's
// important that we immediately do so as soon as the build branch is created.
// This is to avoid leaving things in a weird state should anything go wrong
// while pushing to upstream.
const restoreStagedChanges = commitStagedChanges();
const restoreUnstagedChanges = commitUnstagedChanges();
gitSync("branch", buildBranch);
restoreUnstagedChanges();
restoreStagedChanges();
const { status } = await git("push", "--set-upstream", upstream, buildBranch);
if (status !== 0) {
deleteBranch(buildBranch);
return false;
}
return buildBranch;
}

Просмотреть файл

@ -0,0 +1,46 @@
import * as path from "path";
import pkgDir from "pkg-dir";
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";
import { startBuild } from "./build";
import { getRepositoryRoot } from "./git";
import * as github from "./remotes/github";
import type { BuildParams, Platform } from "./types";
export function build(params: BuildParams): Promise<number> {
const githubRepo = github.getRepositoryInfo();
if (githubRepo) {
return startBuild(github, githubRepo, params);
}
return Promise.reject("Unsupported repository");
}
function main(): void {
const repoRoot = getRepositoryRoot();
const params = yargs(hideBin(process.argv))
.option("platform", {
alias: "p",
type: "string",
description:
"Supported platforms are `android`, `ios`, `macos`, `windows`",
choices: ["android", "ios", "macos", "windows"],
required: true,
})
.option("project-root", {
type: "string",
description: "Root of project",
default: path.relative(repoRoot, pkgDir.sync() || process.cwd()),
coerce: (value) => {
// `projectRoot` needs to be relative to repository root
return path.relative(repoRoot, path.resolve(process.cwd(), value));
},
}).argv;
build({
platform: params.platform as Platform,
projectRoot: params["project-root"],
});
}
main();

Просмотреть файл

@ -0,0 +1,151 @@
import { spawn } from "node:child_process";
import { existsSync as fileExists } from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { Ora } from "ora";
import { idle } from "../async";
import { ensure, makeCommand, makeCommandSync } from "../command";
const ANDROID_HOME = process.env.ANDROID_HOME || "";
const ADB_PATH = path.join(ANDROID_HOME, "platform-tools", "adb");
const EMULATOR_PATH = path.join(ANDROID_HOME, "emulator", "emulator");
const adb = makeCommand(ADB_PATH);
async function getBuildToolsPath(): Promise<string> {
const buildToolsInstallPath = path.join(ANDROID_HOME, "build-tools");
if (!fileExists(buildToolsInstallPath)) {
throw new Error("Could not find Android SDK Build-Tools");
}
const versions = await fs.readdir(buildToolsInstallPath);
let latestVersion = "0.0.0";
let maxValue = 0;
for (const version of versions) {
const [major, minor, patch] = version.split(".");
const value =
parseInt(major, 10) * 10000 +
parseInt(minor, 10) * 100 +
parseInt(patch, 10);
if (maxValue < value) {
latestVersion = version;
}
}
return path.join(buildToolsInstallPath, latestVersion);
}
async function getDevices(): Promise<[string, string][]> {
const { stdout } = await adb("devices");
return stdout
.split("\n")
.splice(1)
.map((device) => {
const [name, state] = device.split("\t");
return [name, state];
});
}
async function getEmulators(): Promise<string[]> {
const emulator = makeCommand(EMULATOR_PATH);
const result = await emulator("-list-avds");
return ensure(result)
.split("\n")
.map((device) => device.trim())
.filter(Boolean);
}
async function getPackageName(apk: string): Promise<[string, string]> {
const buildToolsPath = await getBuildToolsPath();
const aapt = makeCommandSync(path.join(buildToolsPath, "aapt2"));
const { stdout } = aapt("dump", "badging", apk);
const packageMatch = stdout.match(/package: name='(.*?)'/);
if (!packageMatch) {
throw new Error("Could not find package name");
}
const activityMatch = stdout.match(/launchable-activity: name='(.*?)'/);
if (!activityMatch) {
throw new Error("Could not find launchable activity");
}
return [packageMatch[1], activityMatch[1]];
}
async function install(filename: string, packageName: string): Promise<void> {
const { stderr, status } = await adb("install", filename);
if (status !== 0) {
if (stderr.includes("device offline")) {
await idle(1000);
return install(filename, packageName);
} else if (stderr.includes("INSTALL_FAILED_UPDATE_INCOMPATIBLE")) {
await adb("uninstall", packageName);
return install(filename, packageName);
}
throw new Error(stderr);
}
}
async function launchEmulator(emulatorName: string): Promise<void> {
spawn(EMULATOR_PATH, ["@" + emulatorName], {
detached: true,
stdio: "ignore",
}).unref();
while (emulatorName) {
const devices = await getDevices();
if (devices.some(([, state]) => state === "device")) {
break;
}
await idle(1000);
}
}
function start(packageName: string, activityName: string) {
return adb("shell", "am", "start", "-n", `${packageName}/${activityName}`);
}
export async function installAndLaunchApk(
apk: string,
emulatorName: string | undefined,
spinner: Ora
): Promise<void> {
if (!ANDROID_HOME) {
spinner.warn(
"ANDROID_HOME is not set and is required to install and launch APKs"
);
return;
}
if (emulatorName) {
spinner.start(`Booting Android emulator @${emulatorName}`);
await launchEmulator(emulatorName);
spinner.succeed(`Booted @${emulatorName}`);
} else {
const devices = await getDevices();
if (devices.some(([, state]) => state === "device")) {
spinner.info("An Android device is already connected");
} else {
const emulators = await getEmulators();
const emulatorName = emulators[0];
spinner.start(`Booting Android emulator @${emulatorName}`);
await launchEmulator(emulatorName);
spinner.succeed(`Booted @${emulatorName}`);
}
}
const [packageName, activityName] = await getPackageName(apk);
spinner.start(`Installing ${apk}`);
await install(apk, packageName);
spinner.succeed(`Installed ${apk}`);
spinner.start(`Starting ${packageName}`);
await start(packageName, activityName);
spinner.succeed(`Started ${packageName}`);
}

Просмотреть файл

@ -0,0 +1,131 @@
import * as path from "node:path";
import type { Ora } from "ora";
import { idle } from "../async";
import { ensure, makeCommand, makeCommandSync } from "../command";
type SimDevice = {
name: string;
state: "Booted" | "Shutdown";
deviceTypeIdentifier: string;
isAvailable: boolean;
udid: string;
logPath: string;
dataPathSize: number;
dataPath: string;
availabilityError?: string;
};
type Devices = {
devices: Record<string, SimDevice[]>;
};
const open = makeCommand("open");
const xcrun = makeCommand("xcrun");
async function bootSimulator({ udid }: SimDevice): Promise<void> {
ensure(await xcrun("simctl", "boot", udid));
while (udid) {
await idle(1000);
const dev = await getDevice(udid);
if (dev.state === "Booted") {
break;
}
}
}
async function getAvailableDevices(
search = "iPhone"
): Promise<Record<string, SimDevice[]>> {
const result = ensure(
await xcrun("simctl", "list", "--json", "devices", search, "available")
);
const { devices } = JSON.parse(result) as Devices;
return devices;
}
function getDeveloperDirectory(): string | undefined {
const xcodeSelect = makeCommandSync("xcode-select");
const { stdout, status } = xcodeSelect("--print-path");
return status === 0 ? stdout.trim() : undefined;
}
async function getDevice(deviceName: string | undefined): Promise<SimDevice> {
const devices = await getAvailableDevices(deviceName);
const runtime = Object.keys(devices)
.sort()
.reverse()
.find((runtime) => devices[runtime].length !== 0);
if (!runtime) {
throw new Error("Could not find an appropriate simulator");
}
const deviceList = devices[runtime];
return deviceList[deviceList.length - 1];
}
async function install(app: string, { udid }: SimDevice): Promise<void> {
ensure(await xcrun("simctl", "install", udid, app));
}
async function launch(app: string, { udid }: SimDevice): Promise<void> {
const plutil = makeCommand("plutil");
const plistResult = await plutil(
"-convert",
"json",
"-o",
"-",
path.join(app, "Info.plist")
);
const plist = ensure(plistResult, `Failed to parse 'Info.plist' of '${app}'`);
const { CFBundleIdentifier } = JSON.parse(plist);
ensure(await xcrun("simctl", "launch", udid, CFBundleIdentifier));
}
async function untar(archive: string): Promise<string> {
const buildDir = path.dirname(archive);
const tar = makeCommand("tar", { cwd: buildDir });
const filename = path.basename(archive);
const list = ensure(await tar("tf", filename));
const m = list.match(/(.*?)\//);
if (!m) {
throw new Error(`Failed to determine content of ${archive}`);
}
ensure(await tar("xf", filename));
return path.join(buildDir, m[1]);
}
export async function installAndLaunchApp(
archive: string,
deviceName: string | undefined,
spinner: Ora
): Promise<void> {
const developerDir = getDeveloperDirectory();
if (!developerDir) {
spinner.warn("Xcode is required to install and launch apps");
return;
}
const device = await getDevice(deviceName);
if (device.state === "Booted") {
spinner.info(`${device.name} simulator is already connected`);
} else {
spinner.start(`Booting ${device.name} simulator`);
await bootSimulator(device);
spinner.succeed(`Booted ${device.name} simulator`);
}
await open(path.join(developerDir, "Applications", "Simulator.app"));
const app = await untar(archive);
spinner.start(`Installing ${app}`);
await install(app, device);
spinner.succeed(`Installed ${app}`);
spinner.start(`Launching ${app}`);
await launch(app, device);
spinner.succeed(`Launched ${app}`);
}

Просмотреть файл

@ -0,0 +1,230 @@
import { Octokit } from "@octokit/core";
import type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import {
existsSync as fileExists,
readFileSync as fileReadSync,
} from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import ora from "ora";
import { idle, withRetries } from "../async";
import {
BUILD_ID,
MAX_ATTEMPTS,
MAX_DOWNLOAD_ATTEMPTS,
USER_CONFIG_FILE,
WORKFLOW_ID,
} from "../constants";
import { getRemoteUrl, getRepositoryRoot } from "../git";
import type {
BuildParams,
Context,
RepositoryInfo,
UserConfig,
} from "../types";
type GitHubClient = Octokit & ReturnType<typeof restEndpointMethods>;
type WorkflowRunId =
RestEndpointMethodTypes["actions"]["listJobsForWorkflowRun"]["parameters"];
type WorkflowRunsParams =
RestEndpointMethodTypes["actions"]["listWorkflowRuns"]["parameters"];
const octokit: () => GitHubClient = (() => {
let client: GitHubClient | undefined = undefined;
return () => {
if (!client) {
const RestClient = Octokit.plugin(restEndpointMethods);
client = new RestClient({ auth: getPersonalAccessToken() });
}
return client;
};
})();
async function downloadArtifact(
runId: WorkflowRunId,
{ platform, projectRoot }: BuildParams
): Promise<string> {
const artifacts = await withRetries(async () => {
const { data } = await octokit().rest.actions.listWorkflowRunArtifacts(
runId
);
if (data.total_count === 0) {
throw new Error("No artifacts were uploaded");
}
return data.artifacts;
}, MAX_DOWNLOAD_ATTEMPTS);
const data = await withRetries(async () => {
const { data } = await octokit().rest.actions.downloadArtifact({
owner: runId.owner,
repo: runId.repo,
artifact_id: artifacts[0].id,
archive_format: "zip",
});
return data as ArrayBuffer;
}, MAX_DOWNLOAD_ATTEMPTS);
const buildDir = path.join(
getRepositoryRoot(),
projectRoot,
platform,
BUILD_ID
);
const filename = path.join(buildDir, artifacts[0].name + ".zip");
await fs.mkdir(buildDir, { recursive: true, mode: 0o755 });
await fs.writeFile(filename, Buffer.from(data));
return filename;
}
function getPersonalAccessToken(): string | undefined {
if (process.env.GITHUB_TOKEN) {
return process.env.GITHUB_TOKEN;
}
const content = fileReadSync(USER_CONFIG_FILE, { encoding: "utf-8" });
const config: Partial<UserConfig> = JSON.parse(content);
return config?.tokens?.github;
}
async function getWorkflowRunId(
params: WorkflowRunsParams
): Promise<WorkflowRunId> {
const run_id = await withRetries(async () => {
const result = await octokit().rest.actions.listWorkflowRuns(params);
if (result.data.total_count === 0) {
throw new Error("Failed to get workflow run id");
}
return result.data.workflow_runs[0].id;
}, MAX_ATTEMPTS);
return {
owner: params.owner,
repo: params.repo,
run_id,
};
}
async function watchWorkflowRun(
runId: WorkflowRunId,
spinner: ora.Ora
): Promise<void> {
spinner.start("Starting build");
while (runId) {
await idle(1000);
const result = await octokit().rest.actions.listJobsForWorkflowRun(runId);
const job = result.data.jobs.find((job) => job.conclusion !== "skipped");
if (!job) {
continue;
}
const { status, conclusion, steps } = job;
if (status === "completed") {
switch (conclusion) {
case "failure":
spinner.fail();
break;
case "success":
spinner.succeed("Build succeeded");
break;
default:
spinner.fail(`Build ${conclusion}`);
break;
}
break;
}
const currentStep = steps?.find((step) => step.status !== "completed");
if (currentStep) {
spinner.text = currentStep.name;
}
}
}
/**
* Returns name and owner of repository if upstream is GitHub.
* @param upstream Upstream (tracking) reference; defaults to "origin"
* @returns Name and owner of repository if upstream is GitHub; otherwise `undefined`
*/
export function getRepositoryInfo(
upstream = "origin"
): RepositoryInfo | undefined {
const remoteUrl = getRemoteUrl(upstream);
const m = remoteUrl.match(/github.com[/:](.*?)\/(.*?)\.git/);
if (!m) {
return undefined;
}
return { owner: m[1], repo: m[2] };
}
export function isSetUp(spinner: ora.Ora): boolean {
const workflowFile = path.join(
getRepositoryRoot(),
".github",
"workflows",
WORKFLOW_ID
);
if (!fileExists(workflowFile)) {
spinner.fail("The workflow for `rnx-build` needs to be committed first");
return false;
}
if (!getPersonalAccessToken()) {
const exampleConfig: UserConfig = {
tokens: {
github: "token",
},
};
const example = JSON.stringify(exampleConfig);
spinner.fail(
`Missing personal access token for GitHub. Please create one, and put it in \`${USER_CONFIG_FILE}\`, e.g.: \`${example}\`.`
);
spinner.fail(
"For how to create a personal access token, see: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
);
return false;
}
return true;
}
export async function build(
{ owner, repo, ref }: Context,
inputs: BuildParams,
spinner: ora.Ora
): Promise<string> {
await octokit().rest.actions.createWorkflowDispatch({
owner,
repo,
workflow_id: WORKFLOW_ID,
ref,
inputs,
});
const workflowRunId = await getWorkflowRunId({
owner,
repo,
workflow_id: WORKFLOW_ID,
branch: ref,
event: "workflow_dispatch",
per_page: 1,
exclude_pull_requests: true,
});
spinner.succeed(
`Build queued: https://github.com/${owner}/${repo}/actions/runs/${workflowRunId.run_id}`
);
await watchWorkflowRun(workflowRunId, spinner);
spinner.start("Downloading build artifact");
const artifactFile = await downloadArtifact(workflowRunId, inputs);
spinner.succeed(`Build artifact saved to ${artifactFile}`);
return artifactFile;
}

Просмотреть файл

@ -0,0 +1,29 @@
import type { Ora } from "ora";
export type Platform = "android" | "ios" | "macos" | "windows";
export type RepositoryInfo = {
owner: string;
repo: string;
};
export type UserConfig = {
tokens: {
github?: string;
};
};
export type BuildParams = {
projectRoot: string;
platform: Platform;
[key: string]: string;
};
export type Context = RepositoryInfo & {
ref: string;
};
export type Remote = {
isSetUp(spinner: Ora): boolean;
build(context: Context, inputs: BuildParams, spinner: Ora): Promise<string>;
};

Просмотреть файл

@ -0,0 +1,4 @@
{
"extends": "@rnx-kit/scripts/tsconfig-shared.json",
"include": ["src"]
}

Просмотреть файл

@ -277,14 +277,14 @@ PODS:
- React-jsi (= 0.66.4)
- React-logger (= 0.66.4)
- React-perflogger (= 0.66.4)
- ReactTestApp-DevSupport (1.3.8):
- ReactTestApp-DevSupport (1.3.10):
- React-Core
- React-jsi
- ReactTestApp-MSAL (1.0.1):
- ReactTestApp-MSAL (1.0.2):
- MSAL
- RNXAuth
- ReactTestApp-Resources (1.0.0-dev)
- RNXAuth (0.1.1):
- RNXAuth (0.1.2):
- React-Core
- SwiftLint (0.46.2)
- Yoga (1.14.0)
@ -437,10 +437,10 @@ SPEC CHECKSUMS:
React-RCTVibration: e3ffca672dd3772536cb844274094b0e2c31b187
React-runtimeexecutor: dec32ee6f2e2a26e13e58152271535fadff5455a
ReactCommon: 57b69f6383eafcbd7da625bfa6003810332313c4
ReactTestApp-DevSupport: 1075d13c9f78457e4e7365dffb30f131e5b16a5b
ReactTestApp-MSAL: 01ce59a7dccd3b099f6de76d1bc0b1ea9b24d88a
ReactTestApp-DevSupport: 6275cb3b001d9d971f8328580f757409a1c1cf64
ReactTestApp-MSAL: 2fe4334f2e18008e6cc5323bb57d583f9b43b0d8
ReactTestApp-Resources: 74a1cf509f4e7962b16361ea4e73cba3648fff5d
RNXAuth: 60c26d03e192279f9880da69e2876cc90786769d
RNXAuth: e393a0547142ae4c3bdd2ba299cd9dfff4192e32
SwiftLint: 6bc52a21f0fd44cab9aa2dc8e534fb9f5e3ec507
Yoga: e7dc4e71caba6472ff48ad7d234389b91dadc280

Просмотреть файл

@ -147,7 +147,7 @@ THE SOFTWARE.
"https://spdx.org/licenses/MIT.html",
],
"name": "yargs",
"path": "~/packages/third-party-notices/node_modules/yargs",
"path": "~/node_modules/yargs",
"version": "1.2.3-fixedVersionForTesting",
},
Object {

145
yarn.lock
Просмотреть файл

@ -295,7 +295,7 @@
dependencies:
"@babel/types" "^7.16.7"
"@babel/helper-member-expression-to-functions@^7.16.7", "@babel/helper-member-expression-to-functions@^7.17.7":
"@babel/helper-member-expression-to-functions@^7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4"
integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==
@ -653,7 +653,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.3"
"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.16.7", "@babel/plugin-syntax-flow@^7.17.12", "@babel/plugin-syntax-flow@^7.2.0":
"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.17.12", "@babel/plugin-syntax-flow@^7.2.0":
version "7.17.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.17.12.tgz#23d852902acd19f42923fca9d0f196984d124e73"
integrity sha512-B8QIgBvkIG6G2jgsOHQUist7Sm0EBLDCx8sen072IwqNuzMegZNXrYnSv77cYzA8mLDZAfQYqsLIhimiP1s2HQ==
@ -688,7 +688,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.16.7", "@babel/plugin-syntax-jsx@^7.17.12":
"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.17.12":
version "7.17.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.17.12.tgz#834035b45061983a491f60096f61a2e7c5674a47"
integrity sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog==
@ -839,7 +839,7 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.16.7", "@babel/plugin-transform-flow-strip-types@^7.17.12":
"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.17.12":
version "7.17.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.17.12.tgz#5e070f99a4152194bd9275de140e83a92966cab3"
integrity sha512-g8cSNt+cHCpG/uunPQELdq/TeV3eg1OLJYwxypwHtAWo9+nErH3lQx9CSO2uI9lF74A0mR0t4KoMjs1snSgnTw==
@ -1257,7 +1257,7 @@
"@babel/parser" "^7.16.7"
"@babel/types" "^7.16.7"
"@babel/traverse@^7.0.0", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.18.5", "@babel/traverse@^7.7.2":
"@babel/traverse@^7.0.0", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.18.5", "@babel/traverse@^7.7.2":
version "7.18.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd"
integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==
@ -2713,6 +2713,85 @@
dependencies:
nx "14.3.6"
"@octokit/auth-token@^2.4.4":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36"
integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==
dependencies:
"@octokit/types" "^6.0.3"
"@octokit/core@^3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085"
integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==
dependencies:
"@octokit/auth-token" "^2.4.4"
"@octokit/graphql" "^4.5.8"
"@octokit/request" "^5.6.3"
"@octokit/request-error" "^2.0.5"
"@octokit/types" "^6.0.3"
before-after-hook "^2.2.0"
universal-user-agent "^6.0.0"
"@octokit/endpoint@^6.0.1":
version "6.0.12"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658"
integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==
dependencies:
"@octokit/types" "^6.0.3"
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
"@octokit/graphql@^4.5.8":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3"
integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==
dependencies:
"@octokit/request" "^5.6.0"
"@octokit/types" "^6.0.3"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^12.1.0":
version "12.1.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.1.0.tgz#a68b60e969f26dee0eb7d127c65a84967f2d3a6e"
integrity sha512-kQzJh3ZUv3lDpi6M+uekMRHULvf9DlWoI1XgKN6nPeGDzkSgtkhVq1MMz3bFKQ6H6GbdC3ZqG/a6VzKhIx0VeA==
"@octokit/plugin-rest-endpoint-methods@^5.14.0":
version "5.14.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.14.0.tgz#758e01ac40998e607feaea7f80220c69990814ae"
integrity sha512-MRnMs4Dcm1OSaz/g/RLr4YY9otgysS7vN5SUkHGd7t+R8323cHsHFoEWHYPSmgUC0BieHRhvnCRWb4i3Pl+Lgg==
dependencies:
"@octokit/types" "^6.35.0"
deprecation "^2.3.1"
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==
dependencies:
"@octokit/types" "^6.0.3"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.6.0", "@octokit/request@^5.6.3":
version "5.6.3"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0"
integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==
dependencies:
"@octokit/endpoint" "^6.0.1"
"@octokit/request-error" "^2.1.0"
"@octokit/types" "^6.16.1"
is-plain-object "^5.0.0"
node-fetch "^2.6.7"
universal-user-agent "^6.0.0"
"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.35.0":
version "6.35.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.35.0.tgz#11cd9a679c32b4a6c36459ae2ec3ac4de0104f71"
integrity sha512-DhLfdUuv3H37u6jBDfkwamypx3HflHg29b26nbA6iVFYkAlZ5cMEtu/9pQoihGnQE5M7jJFnNo25Rr1UwQNF8Q==
dependencies:
"@octokit/openapi-types" "^12.1.0"
"@parcel/watcher@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b"
@ -2751,7 +2830,7 @@
hermes-profile-transformer "^0.0.6"
ip "^1.1.5"
"@react-native-community/cli-platform-android@^6.0.0", "@react-native-community/cli-platform-android@^6.2.0", "@react-native-community/cli-platform-android@^6.3.0":
"@react-native-community/cli-platform-android@^6.0.0", "@react-native-community/cli-platform-android@^6.3.0":
version "6.3.0"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-6.3.0.tgz#ab7d156bd69a392493323eeaba839a874c0e201f"
integrity sha512-d5ufyYcvrZoHznYm5bjBXaiHIJv552t5gYtQpnUsxBhHSQ8QlaNmlLUyeSPRDfOw4ND9b0tPHqs4ufwx6vp/fQ==
@ -3976,6 +4055,13 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yauzl@2.10.0":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@^5.0.0":
version "5.28.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.28.0.tgz#6204ac33bdd05ab27c7f77960f1023951115d403"
@ -5051,6 +5137,11 @@ batch@0.6.1:
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
before-after-hook@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
better-opn@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
@ -5260,6 +5351,11 @@ buffer-alloc@^1.1.0:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
@ -6546,6 +6642,11 @@ deprecated-react-native-prop-types@^2.3.0:
invariant "*"
prop-types "*"
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
deps-regex@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deps-regex/-/deps-regex-0.1.4.tgz#518667b7691460a5e7e0a341be76eb7ce8090184"
@ -7723,6 +7824,13 @@ fbjs@^3.0.0, fbjs@^3.0.1:
setimmediate "^1.0.5"
ua-parser-js "^0.7.30"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
dependencies:
pend "~1.2.0"
fecha@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce"
@ -9110,7 +9218,7 @@ is-ci@^3.0.1:
dependencies:
ci-info "^3.2.0"
is-core-module@^2.2.0, is-core-module@^2.4.0, is-core-module@^2.8.1, is-core-module@^2.9.0:
is-core-module@^2.2.0, is-core-module@^2.4.0, is-core-module@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
@ -9273,6 +9381,11 @@ is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
@ -12554,6 +12667,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@ -15736,6 +15854,11 @@ unist-util-visit@2.0.3, unist-util-visit@^2.0.0, unist-util-visit@^2.0.1, unist-
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
universal-user-agent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
universalify@^0.1.0, universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@ -16577,6 +16700,14 @@ yargs@^17.1.1, yargs@^17.4.0:
y18n "^5.0.5"
yargs-parser "^21.0.0"
yauzl@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"