зеркало из https://github.com/microsoft/rnx-kit.git
feat(build): introduce experimental `rnx-build` command (#1659)
This commit is contained in:
Родитель
8391972e9e
Коммит
9341c417e5
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@rnx-kit/build": minor
|
||||
---
|
||||
|
||||
@rnx-kit/build builds your app in the cloud
|
|
@ -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
|
|
@ -8,6 +8,7 @@
|
|||
.idea/
|
||||
Pods/
|
||||
coverage/
|
||||
/packages/*/*/rnx-build/
|
||||
/packages/*/bin/
|
||||
/packages/*/dist/
|
||||
/packages/*/lib/
|
||||
|
|
|
@ -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";
|
|
@ -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
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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче