mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca718d8da9 | ||
|
|
3e7b849ad1 | ||
|
|
2ec646659d | ||
|
|
c33e3d8a4c | ||
|
|
d65c180746 | ||
|
|
754af79e0c | ||
|
|
3733cc4cf8 | ||
|
|
9e5c1740b4 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.1"
|
||||
"version": "0.1.2"
|
||||
}
|
||||
+13
-13
@@ -21,7 +21,7 @@ $ npm install -g eas-cli
|
||||
$ eas COMMAND
|
||||
running command...
|
||||
$ eas (-v|--version|version)
|
||||
eas-cli/0.1.1 darwin-x64 node-v14.15.0
|
||||
eas-cli/0.1.2 darwin-x64 node-v14.15.0
|
||||
$ eas --help [COMMAND]
|
||||
USAGE
|
||||
$ eas COMMAND
|
||||
@@ -58,7 +58,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/login.ts)_
|
||||
_See code: [build/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -72,7 +72,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/logout.ts)_
|
||||
_See code: [build/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -86,7 +86,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/account/view.ts)_
|
||||
_See code: [build/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/account/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -105,7 +105,7 @@ OPTIONS
|
||||
--wait Wait for build(s) to complete
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/index.ts)_
|
||||
_See code: [build/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/index.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -120,7 +120,7 @@ OPTIONS
|
||||
--allow-experimental Enable experimental configuration steps.
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/configure.ts)_
|
||||
_See code: [build/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -136,7 +136,7 @@ OPTIONS
|
||||
--status=(in-queue|in-progress|errored|finished)
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/list.ts)_
|
||||
_See code: [build/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/list.ts)_
|
||||
|
||||
## `eas build:view [BUILDID]`
|
||||
|
||||
@@ -147,7 +147,7 @@ USAGE
|
||||
$ eas build:view [BUILDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/build/view.ts)_
|
||||
_See code: [build/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/build/view.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -158,7 +158,7 @@ USAGE
|
||||
$ eas credentials
|
||||
```
|
||||
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/credentials.ts)_
|
||||
_See code: [build/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -169,7 +169,7 @@ USAGE
|
||||
$ eas device:create
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/create.ts)_
|
||||
_See code: [build/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/device/create.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -183,7 +183,7 @@ OPTIONS
|
||||
--apple-team-id=apple-team-id
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/list.ts)_
|
||||
_See code: [build/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -194,7 +194,7 @@ USAGE
|
||||
$ eas device:view [UDID]
|
||||
```
|
||||
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/device/view.ts)_
|
||||
_See code: [build/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/device/view.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -291,5 +291,5 @@ EXAMPLES
|
||||
and provide its App ID
|
||||
```
|
||||
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.1.1/build/commands/submit.ts)_
|
||||
_See code: [build/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.1.2/build/commands/submit.ts)_
|
||||
<!-- commandsstop -->
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -14,7 +14,7 @@
|
||||
"@expo/eas-build-job": "0.1.4",
|
||||
"@expo/eas-json": "^0.1.0-alpha.20",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.10",
|
||||
"@expo/plist": "^0.0.11",
|
||||
"@expo/plugin-warn-if-update-available": "^1.7.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
@@ -39,6 +39,7 @@
|
||||
"keychain": "^1.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"md5-file": "^5.0.0",
|
||||
"mime": "^2.4.7",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
@@ -70,6 +71,7 @@
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/node": "^12",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
@@ -83,6 +85,7 @@
|
||||
"axios": "^0.21.0",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"mockdate": "^3.0.2",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.9.7",
|
||||
"wonka": "^4.0.14"
|
||||
|
||||
@@ -20,6 +20,8 @@ export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
label: 'Status',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case BuildStatus.IN_QUEUE:
|
||||
return chalk.blue('in queue');
|
||||
case BuildStatus.IN_PROGRESS:
|
||||
return chalk.blue('in progress');
|
||||
case BuildStatus.FINISHED:
|
||||
@@ -38,7 +40,7 @@ export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
{
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
if (build.status === BuildStatus.IN_PROGRESS) {
|
||||
if (build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS) {
|
||||
return '<in progress>';
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ export default function formatBuild(build: Build, { accountName }: Options) {
|
||||
{
|
||||
label: 'Finished at',
|
||||
value:
|
||||
build.status === BuildStatus.IN_PROGRESS
|
||||
build.status === BuildStatus.IN_QUEUE || build.status === BuildStatus.IN_PROGRESS
|
||||
? '<in progress>'
|
||||
: new Date(build.updatedAt).toLocaleString(),
|
||||
},
|
||||
|
||||
@@ -4,19 +4,28 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { apiClient } from '../../api';
|
||||
import { configureAsync } from '../../build/configure';
|
||||
import { createCommandContextAsync } from '../../build/context';
|
||||
import { buildAsync } from '../../build/create';
|
||||
import { AnalyticsEvent, RequestedPlatform } from '../../build/types';
|
||||
import { AnalyticsEvent, Build as BuildType, RequestedPlatform } from '../../build/types';
|
||||
import Analytics from '../../build/utils/analytics';
|
||||
import formatBuild from '../../build/utils/formatBuild';
|
||||
import { isGitStatusCleanAsync } from '../../build/utils/repository';
|
||||
import { learnMore } from '../../log';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import log, { learnMore } from '../../log';
|
||||
import {
|
||||
isEasEnabledForProjectAsync,
|
||||
warnEasUnavailable,
|
||||
} from '../../project/isEasEnabledForProject';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class Build extends Command {
|
||||
@@ -48,7 +57,7 @@ export default class Build extends Command {
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = this.parse(Build);
|
||||
await ensureLoggedInAsync();
|
||||
const user = await ensureLoggedInAsync();
|
||||
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
if (!flags.platform && nonInteractive) {
|
||||
@@ -66,6 +75,17 @@ export default class Build extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const accountHasPendingBuilds = await ensureNoPendingBuildsExistAsync({
|
||||
accountName,
|
||||
platform,
|
||||
projectId,
|
||||
user,
|
||||
});
|
||||
if (accountHasPendingBuilds) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureProjectConfiguredAsync(projectDir);
|
||||
|
||||
const trackingCtx = {
|
||||
@@ -89,6 +109,64 @@ export default class Build extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureNoPendingBuildsExistAsync({
|
||||
user,
|
||||
platform,
|
||||
accountName,
|
||||
projectId,
|
||||
}: {
|
||||
user: Actor;
|
||||
platform: RequestedPlatform;
|
||||
accountName: string;
|
||||
projectId: string;
|
||||
}): Promise<boolean> {
|
||||
// allow expo admins to run as many builds as they wish
|
||||
if (user.isExpoAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const appPlatforms = toAppPlatforms(platform);
|
||||
const maybePendingBuilds = await Promise.all(
|
||||
appPlatforms.map(appPlatform => BuildQuery.getPendingBuildIdAsync(accountName, appPlatform))
|
||||
);
|
||||
const pendingBuilds = maybePendingBuilds.filter(i => i !== null);
|
||||
if (pendingBuilds.length > 0) {
|
||||
log.newLine();
|
||||
log.error(
|
||||
'Your other builds are still pending. Wait for them to complete before running this command again.'
|
||||
);
|
||||
log.newLine();
|
||||
const results = await Promise.all(
|
||||
pendingBuilds.map(pendingBuild => {
|
||||
return apiClient.get<{ data: BuildType }>(
|
||||
`projects/${projectId}/builds/${pendingBuild?.id}`,
|
||||
{
|
||||
responseType: 'json',
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
log(formatBuild(result.body.data, { accountName }));
|
||||
}
|
||||
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function toAppPlatforms(requestedPlatform: RequestedPlatform): AppPlatform[] {
|
||||
if (requestedPlatform === RequestedPlatform.All) {
|
||||
return [AppPlatform.Android, AppPlatform.Ios];
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return [AppPlatform.Android];
|
||||
} else {
|
||||
return [AppPlatform.Ios];
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForPlatformAsync(): Promise<RequestedPlatform> {
|
||||
const { platform } = await promptAsync({
|
||||
type: 'select',
|
||||
|
||||
@@ -80,9 +80,7 @@ export class AccountResolver {
|
||||
)} account?`,
|
||||
});
|
||||
|
||||
if (useProjectAccount) {
|
||||
return projectAccount;
|
||||
}
|
||||
return useProjectAccount ? projectAccount : undefined;
|
||||
}
|
||||
|
||||
private async promptForAccountAsync(): Promise<Account> {
|
||||
|
||||
@@ -57,7 +57,7 @@ export type RootQuery = {
|
||||
builds: BuildQuery;
|
||||
clientBuilds: ClientBuildQuery;
|
||||
project: ProjectQuery;
|
||||
/** Search for users and apps */
|
||||
/** Search for Snacks */
|
||||
search: Array<SearchResult>;
|
||||
/** @deprecated Use 'search' root field. */
|
||||
searchUsersAndApps: Array<Maybe<SearchResult>>;
|
||||
@@ -156,6 +156,7 @@ export type Account = {
|
||||
/** @deprecated Build packs are no longer supported */
|
||||
availableBuilds?: Maybe<Scalars['Int']>;
|
||||
unlimitedBuilds?: Maybe<Scalars['Boolean']>;
|
||||
/** @deprecated No longer needed */
|
||||
subscriptionChangesPending?: Maybe<Scalars['Boolean']>;
|
||||
/** @deprecated Build packs are no longer supported */
|
||||
willAutoRenewBuilds?: Maybe<Scalars['Boolean']>;
|
||||
@@ -170,7 +171,7 @@ export type Account = {
|
||||
apps?: Maybe<Array<Maybe<App>>>;
|
||||
/** Build Jobs associated with this account */
|
||||
buildJobs?: Maybe<Array<Maybe<BuildJob>>>;
|
||||
/** (Turtle v2) Builds associated with this account */
|
||||
/** (EAS Build) Builds associated with this account */
|
||||
builds?: Maybe<Array<Maybe<Build>>>;
|
||||
/** Owning User of this account if personal account */
|
||||
owner?: Maybe<User>;
|
||||
@@ -236,6 +237,7 @@ export type AccountBuildsArgs = {
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
status?: Maybe<BuildStatus>;
|
||||
platform?: Maybe<AppPlatform>;
|
||||
};
|
||||
|
||||
|
||||
@@ -372,6 +374,7 @@ export type App = Project & {
|
||||
/** @deprecated Field no longer supported */
|
||||
releases: Array<Maybe<AppRelease>>;
|
||||
builds?: Maybe<Array<Maybe<Build>>>;
|
||||
buildJobs: Array<Maybe<BuildJob>>;
|
||||
/** @deprecated Field no longer supported */
|
||||
users?: Maybe<Array<Maybe<User>>>;
|
||||
/** @deprecated Field no longer supported */
|
||||
@@ -427,6 +430,14 @@ export type AppBuildsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppBuildJobsArgs = {
|
||||
offset: Scalars['Int'];
|
||||
limit: Scalars['Int'];
|
||||
status?: Maybe<BuildStatus>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppIosAppCredentialsArgs = {
|
||||
filter?: Maybe<IosAppCredentialsFilter>;
|
||||
@@ -676,7 +687,7 @@ export enum BuildStatus {
|
||||
Finished = 'FINISHED'
|
||||
}
|
||||
|
||||
/** Represents a Turtle v2 build */
|
||||
/** Represents an EAS Build */
|
||||
export type Build = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'Build';
|
||||
id: Scalars['ID'];
|
||||
@@ -696,6 +707,7 @@ export type Build = ActivityTimelineProjectActivity & {
|
||||
appVersion?: Maybe<Scalars['String']>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
metrics?: Maybe<BuildMetrics>;
|
||||
distribution?: Maybe<DistributionType>;
|
||||
};
|
||||
|
||||
@@ -711,11 +723,65 @@ export type BuildArtifacts = {
|
||||
xcodeBuildLogsUrl?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type BuildMetrics = {
|
||||
__typename?: 'BuildMetrics';
|
||||
buildQueueTime?: Maybe<Scalars['Int']>;
|
||||
buildDuration?: Maybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
export enum DistributionType {
|
||||
Store = 'STORE',
|
||||
Internal = 'INTERNAL'
|
||||
}
|
||||
|
||||
/** Represents an Standalone App build job */
|
||||
export type BuildJob = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'BuildJob';
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
app?: Maybe<App>;
|
||||
user?: Maybe<User>;
|
||||
release?: Maybe<AppRelease>;
|
||||
config?: Maybe<Scalars['JSON']>;
|
||||
artifacts?: Maybe<BuildArtifact>;
|
||||
logs?: Maybe<BuildLogs>;
|
||||
created?: Maybe<Scalars['DateTime']>;
|
||||
updated?: Maybe<Scalars['DateTime']>;
|
||||
fullExperienceName?: Maybe<Scalars['String']>;
|
||||
status?: Maybe<BuildJobStatus>;
|
||||
expirationDate?: Maybe<Scalars['DateTime']>;
|
||||
platform: AppPlatform;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type BuildArtifact = {
|
||||
__typename?: 'BuildArtifact';
|
||||
url: Scalars['String'];
|
||||
manifestPlistUrl?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type BuildLogs = {
|
||||
__typename?: 'BuildLogs';
|
||||
url?: Maybe<Scalars['String']>;
|
||||
format?: Maybe<BuildJobLogsFormat>;
|
||||
};
|
||||
|
||||
export enum BuildJobLogsFormat {
|
||||
Raw = 'RAW',
|
||||
Json = 'JSON'
|
||||
}
|
||||
|
||||
export enum BuildJobStatus {
|
||||
Pending = 'PENDING',
|
||||
Started = 'STARTED',
|
||||
InProgress = 'IN_PROGRESS',
|
||||
Errored = 'ERRORED',
|
||||
Finished = 'FINISHED',
|
||||
SentToQueue = 'SENT_TO_QUEUE'
|
||||
}
|
||||
|
||||
export type IosAppCredentialsFilter = {
|
||||
appleAppIdentifierId?: Maybe<Scalars['String']>;
|
||||
};
|
||||
@@ -978,54 +1044,6 @@ export type Update = ActivityTimelineProjectActivity & {
|
||||
releaseName: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum BuildJobStatus {
|
||||
Pending = 'PENDING',
|
||||
Started = 'STARTED',
|
||||
InProgress = 'IN_PROGRESS',
|
||||
Errored = 'ERRORED',
|
||||
Finished = 'FINISHED',
|
||||
SentToQueue = 'SENT_TO_QUEUE'
|
||||
}
|
||||
|
||||
/** Represents an Standalone App build job */
|
||||
export type BuildJob = ActivityTimelineProjectActivity & {
|
||||
__typename?: 'BuildJob';
|
||||
id: Scalars['ID'];
|
||||
actor?: Maybe<Actor>;
|
||||
activityTimestamp: Scalars['DateTime'];
|
||||
app?: Maybe<App>;
|
||||
user?: Maybe<User>;
|
||||
release?: Maybe<AppRelease>;
|
||||
config?: Maybe<Scalars['JSON']>;
|
||||
artifacts?: Maybe<BuildArtifact>;
|
||||
logs?: Maybe<BuildLogs>;
|
||||
created?: Maybe<Scalars['DateTime']>;
|
||||
updated?: Maybe<Scalars['DateTime']>;
|
||||
fullExperienceName?: Maybe<Scalars['String']>;
|
||||
status?: Maybe<BuildJobStatus>;
|
||||
expirationDate?: Maybe<Scalars['DateTime']>;
|
||||
platform: AppPlatform;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type BuildArtifact = {
|
||||
__typename?: 'BuildArtifact';
|
||||
url: Scalars['String'];
|
||||
manifestPlistUrl?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type BuildLogs = {
|
||||
__typename?: 'BuildLogs';
|
||||
url?: Maybe<Scalars['String']>;
|
||||
format?: Maybe<BuildJobLogsFormat>;
|
||||
};
|
||||
|
||||
export enum BuildJobLogsFormat {
|
||||
Raw = 'RAW',
|
||||
Json = 'JSON'
|
||||
}
|
||||
|
||||
export type UserPermission = {
|
||||
__typename?: 'UserPermission';
|
||||
permissions?: Maybe<Array<Maybe<Permission>>>;
|
||||
@@ -1229,7 +1247,10 @@ export type ProjectPublicData = {
|
||||
|
||||
export type BuildJobQuery = {
|
||||
__typename?: 'BuildJobQuery';
|
||||
/** get all build jobs by an optional filter */
|
||||
/**
|
||||
* get all build jobs by an optional filter
|
||||
* @deprecated Prefer Account.buildJobs or App.buildJobs
|
||||
*/
|
||||
all: Array<Maybe<BuildJob>>;
|
||||
byId: BuildJob;
|
||||
};
|
||||
@@ -1251,7 +1272,7 @@ export type BuildJobQueryByIdArgs = {
|
||||
|
||||
export type BuildQuery = {
|
||||
__typename?: 'BuildQuery';
|
||||
/** Look up Turtle v2 build by build id */
|
||||
/** Look up EAS Build by build ID */
|
||||
byId: Build;
|
||||
/**
|
||||
* Get all builds for a specific app.
|
||||
@@ -2560,8 +2581,11 @@ export type AppSearchResult = SearchResult & {
|
||||
/** Represents a search result for a user */
|
||||
export type UserSearchResult = SearchResult & {
|
||||
__typename?: 'UserSearchResult';
|
||||
/** @deprecated Field no longer supported */
|
||||
id: Scalars['ID'];
|
||||
/** @deprecated Field no longer supported */
|
||||
rank?: Maybe<Scalars['Int']>;
|
||||
/** @deprecated Field no longer supported */
|
||||
user: User;
|
||||
};
|
||||
|
||||
@@ -2579,84 +2603,35 @@ export enum CacheControlScope {
|
||||
}
|
||||
|
||||
|
||||
export type AccountByNameQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
export type GetSignedUploadMutationMutationVariables = Exact<{
|
||||
contentTypes: Array<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountByNameQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { account: (
|
||||
{ __typename?: 'AccountQuery' }
|
||||
& { byName: (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id'>
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type AppleDevicesByAccountNameQueryVariables = Exact<{
|
||||
accountId: Scalars['ID'];
|
||||
identifier: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AppleDevicesByAccountNameQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { appleTeam: (
|
||||
{ __typename?: 'AppleTeamQuery' }
|
||||
& { byAppleTeamIdentifier?: Maybe<(
|
||||
{ __typename?: 'AppleTeam' }
|
||||
& Pick<AppleTeam, 'id' | 'appleTeamName'>
|
||||
& { appleDevices?: Maybe<Array<Maybe<(
|
||||
{ __typename?: 'AppleDevice' }
|
||||
& Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled'>
|
||||
)>>> }
|
||||
export type GetSignedUploadMutationMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { asset: (
|
||||
{ __typename?: 'AssetMutation' }
|
||||
& { getSignedAssetUploadSpecifications?: Maybe<(
|
||||
{ __typename?: 'GetSignedAssetUploadSpecificationsResult' }
|
||||
& Pick<GetSignedAssetUploadSpecificationsResult, 'specifications'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type AppleDevicesByIdentifierQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
identifier: Scalars['String'];
|
||||
export type PublishMutationMutationVariables = Exact<{
|
||||
publishUpdateGroupInput?: Maybe<PublishUpdateGroupInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AppleDevicesByIdentifierQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { account: (
|
||||
{ __typename?: 'AccountQuery' }
|
||||
& { byName: (
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id'>
|
||||
& { appleDevices?: Maybe<Array<Maybe<(
|
||||
{ __typename?: 'AppleDevice' }
|
||||
& Pick<AppleDevice, 'id' | 'identifier' | 'name' | 'deviceClass' | 'enabled'>
|
||||
& { appleTeam: (
|
||||
{ __typename?: 'AppleTeam' }
|
||||
& Pick<AppleTeam, 'id' | 'appleTeamIdentifier' | 'appleTeamName'>
|
||||
) }
|
||||
)>>> }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type AppleTeamsByAccountNameQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AppleTeamsByAccountNameQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { account: (
|
||||
{ __typename?: 'AccountQuery' }
|
||||
& { byName: (
|
||||
{ __typename?: 'Account' }
|
||||
& { appleTeams: Array<Maybe<(
|
||||
{ __typename?: 'AppleTeam' }
|
||||
& Pick<AppleTeam, 'id' | 'appleTeamName' | 'appleTeamIdentifier'>
|
||||
)>> }
|
||||
) }
|
||||
export type PublishMutationMutation = (
|
||||
{ __typename?: 'RootMutation' }
|
||||
& { updateRelease: (
|
||||
{ __typename?: 'UpdateReleaseMutation' }
|
||||
& { publishUpdateGroup: Array<Maybe<(
|
||||
{ __typename?: 'Update' }
|
||||
& Pick<Update, 'updateGroup'>
|
||||
)>> }
|
||||
) }
|
||||
);
|
||||
|
||||
@@ -2704,6 +2679,29 @@ export type BuildsForAppQueryQuery = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type PendingBuildsForAccountAndPlatformQueryVariables = Exact<{
|
||||
accountName: Scalars['String'];
|
||||
platform: AppPlatform;
|
||||
}>;
|
||||
|
||||
|
||||
export type PendingBuildsForAccountAndPlatformQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { account: (
|
||||
{ __typename?: 'AccountQuery' }
|
||||
& { byName: (
|
||||
{ __typename?: 'Account' }
|
||||
& { inQueueBuilds?: Maybe<Array<Maybe<(
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id' | 'platform'>
|
||||
)>>>, inProgressBuilds?: Maybe<Array<Maybe<(
|
||||
{ __typename?: 'Build' }
|
||||
& Pick<Build, 'id' | 'platform'>
|
||||
)>>> }
|
||||
) }
|
||||
) }
|
||||
);
|
||||
|
||||
export type Unnamed_1_QueryVariables = Exact<{
|
||||
username: Scalars['String'];
|
||||
slug: Scalars['String'];
|
||||
@@ -2724,6 +2722,22 @@ export type Unnamed_1_Query = (
|
||||
) }
|
||||
);
|
||||
|
||||
export type GetAssetMetadataQueryQueryVariables = Exact<{
|
||||
storageKeys: Array<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAssetMetadataQueryQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { asset: (
|
||||
{ __typename?: 'AssetQuery' }
|
||||
& { metadata: Array<(
|
||||
{ __typename?: 'AssetMetadataResult' }
|
||||
& Pick<AssetMetadataResult, 'storageKey' | 'status'>
|
||||
)> }
|
||||
) }
|
||||
);
|
||||
|
||||
export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -2731,14 +2745,14 @@ export type CurrentUserQuery = (
|
||||
{ __typename?: 'RootQuery' }
|
||||
& { meActor?: Maybe<(
|
||||
{ __typename: 'User' }
|
||||
& Pick<User, 'username' | 'id'>
|
||||
& Pick<User, 'username' | 'id' | 'isExpoAdmin'>
|
||||
& { accounts?: Maybe<Array<Maybe<(
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id' | 'name'>
|
||||
)>>> }
|
||||
) | (
|
||||
{ __typename: 'Robot' }
|
||||
& Pick<Robot, 'firstName' | 'id'>
|
||||
& Pick<Robot, 'firstName' | 'id' | 'isExpoAdmin'>
|
||||
& { accounts?: Maybe<Array<Maybe<(
|
||||
{ __typename?: 'Account' }
|
||||
& Pick<Account, 'id' | 'name'>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { PublishUpdateGroupInput, Update } from '../generated';
|
||||
|
||||
const PublishMutation = {
|
||||
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ asset: { getSignedAssetUploadSpecifications: { specifications: string[] } } },
|
||||
{ contentTypes: string[] }
|
||||
>(
|
||||
gql`
|
||||
mutation GetSignedUploadMutation($contentTypes: [String!]!) {
|
||||
asset {
|
||||
getSignedAssetUploadSpecifications(assetContentTypes: $contentTypes) {
|
||||
specifications
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
contentTypes,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.asset.getSignedAssetUploadSpecifications;
|
||||
},
|
||||
|
||||
async publishUpdateGroupAsync(
|
||||
publishUpdateGroupInput: PublishUpdateGroupInput
|
||||
): Promise<Pick<Update, 'updateGroup'>> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<
|
||||
{ updateRelease: { publishUpdateGroup: Pick<Update, 'updateGroup'>[] } },
|
||||
{ publishUpdateGroupInput: PublishUpdateGroupInput }
|
||||
>(
|
||||
gql`
|
||||
mutation PublishMutation($publishUpdateGroupInput: PublishUpdateGroupInput) {
|
||||
updateRelease {
|
||||
publishUpdateGroup(publishUpdateGroupInput: $publishUpdateGroupInput) {
|
||||
updateGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ publishUpdateGroupInput }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateRelease.publishUpdateGroup[0];
|
||||
},
|
||||
};
|
||||
|
||||
export { PublishMutation };
|
||||
@@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { Build } from '../generated';
|
||||
import { AppPlatform, Build } from '../generated';
|
||||
|
||||
type Filters = Partial<Pick<Build, 'platform' | 'status'>> & {
|
||||
offset?: number;
|
||||
@@ -9,6 +9,7 @@ type Filters = Partial<Pick<Build, 'platform' | 'status'>> & {
|
||||
};
|
||||
|
||||
type BuildQueryResult = Pick<Build, 'platform' | 'artifacts'>;
|
||||
type PendingBuildQueryResult = Pick<Build, 'id' | 'platform'>;
|
||||
|
||||
const BuildQuery = {
|
||||
async byIdAsync(buildId: string): Promise<BuildQueryResult> {
|
||||
@@ -70,6 +71,60 @@ const BuildQuery = {
|
||||
|
||||
return data.builds.allForApp;
|
||||
},
|
||||
|
||||
async getPendingBuildIdAsync(
|
||||
accountName: string,
|
||||
platform: AppPlatform
|
||||
): Promise<PendingBuildQueryResult | null> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{
|
||||
account: {
|
||||
byName: {
|
||||
inQueueBuilds: PendingBuildQueryResult[];
|
||||
inProgressBuilds: PendingBuildQueryResult[];
|
||||
};
|
||||
};
|
||||
}>(
|
||||
gql`
|
||||
query PendingBuildsForAccountAndPlatform(
|
||||
$accountName: String!
|
||||
$platform: AppPlatform!
|
||||
) {
|
||||
account {
|
||||
byName(accountName: $accountName) {
|
||||
inQueueBuilds: builds(
|
||||
offset: 0
|
||||
limit: 1
|
||||
platform: $platform
|
||||
status: IN_QUEUE
|
||||
) {
|
||||
id
|
||||
platform
|
||||
}
|
||||
inProgressBuilds: builds(
|
||||
offset: 0
|
||||
limit: 1
|
||||
platform: $platform
|
||||
status: IN_PROGRESS
|
||||
) {
|
||||
id
|
||||
platform
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountName, platform }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const pendingBuilds = [
|
||||
...data.account.byName.inProgressBuilds,
|
||||
...data.account.byName.inQueueBuilds,
|
||||
];
|
||||
return pendingBuilds.length > 0 ? pendingBuilds[0] : null;
|
||||
},
|
||||
};
|
||||
|
||||
export { BuildQuery };
|
||||
@@ -0,0 +1,32 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../client';
|
||||
import { AssetMetadataResult } from '../generated';
|
||||
|
||||
const PublishQuery = {
|
||||
async getAssetMetadataAsync(storageKeys: string[]): Promise<AssetMetadataResult[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<{ asset: { metadata: AssetMetadataResult[] } }, { storageKeys: string[] }>(
|
||||
gql`
|
||||
query GetAssetMetadataQuery($storageKeys: [String!]!) {
|
||||
asset {
|
||||
metadata(storageKeys: $storageKeys) {
|
||||
storageKey
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
storageKeys,
|
||||
},
|
||||
{ requestPolicy: 'network-only' } // Since we reptitively query this to monitor the asset upload, we need to ensure it is not cached.
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.asset.metadata;
|
||||
},
|
||||
};
|
||||
|
||||
export { PublishQuery };
|
||||
@@ -23,6 +23,7 @@ const UserQuery = {
|
||||
id
|
||||
name
|
||||
}
|
||||
isExpoAdmin
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
import fs from 'fs';
|
||||
import mockdate from 'mockdate';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AssetMetadataStatus } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
import {
|
||||
Platforms,
|
||||
TIMEOUT_LIMIT,
|
||||
buildUpdateInfoGroupAsync,
|
||||
collectAssets,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getBase64URLEncoding,
|
||||
getStorageKey,
|
||||
getStorageKeyForAssetAsync,
|
||||
guessContentTypeFromExtension,
|
||||
resolveInputDirectory,
|
||||
uploadAssetsAsync,
|
||||
} from '../publish';
|
||||
|
||||
jest.mock('../../uploads');
|
||||
jest.mock('fs');
|
||||
|
||||
const dummyFileBuffer = Buffer.from('dummy-file');
|
||||
fs.mkdirSync(path.resolve(), { recursive: true });
|
||||
fs.writeFileSync(path.resolve('md5-hash-of-file'), dummyFileBuffer);
|
||||
|
||||
describe(guessContentTypeFromExtension, () => {
|
||||
it('returns the correct content type for jpg', () => {
|
||||
expect(guessContentTypeFromExtension('jpg')).toBe('image/jpeg');
|
||||
});
|
||||
it('returns application/octet-stream when an ext is not recognized', () => {
|
||||
expect(guessContentTypeFromExtension('does-not-exist')).toBe('application/octet-stream');
|
||||
});
|
||||
it('returns application/octet-stream when an ext is undefined', () => {
|
||||
expect(guessContentTypeFromExtension(undefined)).toBe('application/octet-stream');
|
||||
});
|
||||
});
|
||||
|
||||
describe(getBase64URLEncoding, () => {
|
||||
it('computes the correct encoding', () => {
|
||||
expect(getBase64URLEncoding(Buffer.from('test-string'))).toBe('dGVzdC1zdHJpbmc');
|
||||
});
|
||||
});
|
||||
|
||||
describe(getStorageKey, () => {
|
||||
it('computes the correct key', () => {
|
||||
const key = getStorageKey('image/jpeg', 'blibblab');
|
||||
expect(key).toBe('j0iiW9hDbR2HKoH1nCxsKRM6QIZVtZ__2ssOiOcxlAs');
|
||||
});
|
||||
it('uses the null separator to distinguish unequal keys', () => {
|
||||
const keyOne = getStorageKey('image/jpeg', 'blibblab');
|
||||
const keyTwo = getStorageKey('image', '/jpegblibblab');
|
||||
expect(keyOne).not.toBe(keyTwo);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getStorageKeyForAssetAsync, () => {
|
||||
const pathLocation = uuidv4();
|
||||
fs.writeFileSync(pathLocation, Buffer.from('I am pretending to be a jpeg'));
|
||||
it('returns the correct key', async () => {
|
||||
const asset = {
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: pathLocation,
|
||||
};
|
||||
expect(await getStorageKeyForAssetAsync(asset)).toBe(
|
||||
'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(convertAssetToUpdateInfoGroupFormatAsync, () => {
|
||||
it('resolves to the correct value', async () => {
|
||||
const path = uuidv4();
|
||||
fs.writeFileSync(path, 'I am pretending to be a jpeg');
|
||||
const type = 'jpg';
|
||||
const asset = {
|
||||
type,
|
||||
contentType: 'image/jpeg',
|
||||
path,
|
||||
};
|
||||
await expect(convertAssetToUpdateInfoGroupFormatAsync(asset)).resolves.toEqual({
|
||||
bundleKey: `c939e759656f577c058f445bfb19182e.${type}`,
|
||||
contentType: 'image/jpeg',
|
||||
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(buildUpdateInfoGroupAsync, () => {
|
||||
const androidBundlePath = uuidv4();
|
||||
const assetPath = uuidv4();
|
||||
fs.writeFileSync(androidBundlePath, 'I am a js bundle');
|
||||
fs.writeFileSync(assetPath, 'I am pretending to be a jpeg');
|
||||
|
||||
it('returns the correct value', async () => {
|
||||
await expect(
|
||||
buildUpdateInfoGroupAsync({
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).resolves.toEqual({
|
||||
android: {
|
||||
assets: [
|
||||
{
|
||||
bundleKey: 'c939e759656f577c058f445bfb19182e.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
fileHash: 'tzD6J-OQZaHCKnL3GHWV9RbnrpyojnagiOE7r3mSkU4',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'fo8Y08LktVk6qLtGbn8GRWpOUyD13ABMUnbtRCN1L7Y',
|
||||
},
|
||||
],
|
||||
|
||||
launchAsset: {
|
||||
bundleKey: 'ec0dd14670aae108f99a810df9c1482c.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
fileHash: 'KEw79FnKTLOyVbRT1SlohSTjPe5e8FpULy2ST-I5BUg',
|
||||
storageBucket: 'update-assets-production',
|
||||
storageKey: 'aC9N6RZlcHoIYjIsoJd2KUcigBKy98RHvZacDyPNjCQ',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(resolveInputDirectory, () => {
|
||||
it('returns the correct distRoot path', () => {
|
||||
const customDirectoryName = path.resolve(uuidv4());
|
||||
fs.mkdirSync(customDirectoryName, { recursive: true });
|
||||
expect(resolveInputDirectory(customDirectoryName)).toBe(customDirectoryName);
|
||||
});
|
||||
it('throws an error if the path does not exist', () => {
|
||||
const nonExistentPath = path.resolve(uuidv4());
|
||||
expect(() => {
|
||||
resolveInputDirectory(nonExistentPath);
|
||||
}).toThrow(`${nonExistentPath} does not exist. Please create it with your desired bundler.`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(collectAssets, () => {
|
||||
it('builds an update info group', () => {
|
||||
const fakeHash = 'md5-hash-of-jpg';
|
||||
const fakeJson = { bundledAssets: [`asset_${fakeHash}.jpg`] };
|
||||
const bundles = { android: 'android-bundle-code', ios: 'ios-bundle-code' };
|
||||
const inputDir = uuidv4();
|
||||
|
||||
const userDefinedAssets = [
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: path.resolve(`${inputDir}/assets/${fakeHash}`),
|
||||
},
|
||||
];
|
||||
|
||||
const bundleDir = path.resolve(`${inputDir}/bundles`);
|
||||
const assetDir = path.resolve(`${inputDir}/assets`);
|
||||
fs.mkdirSync(bundleDir, { recursive: true });
|
||||
fs.mkdirSync(assetDir, { recursive: true });
|
||||
Platforms.forEach(platform => {
|
||||
fs.writeFileSync(
|
||||
path.resolve(`${inputDir}/${platform}-index.json`),
|
||||
JSON.stringify(fakeJson)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.resolve(`${inputDir}/bundles/${platform}-randomHash.js`),
|
||||
bundles[platform]
|
||||
);
|
||||
});
|
||||
fs.writeFileSync(path.resolve(`${inputDir}/assets/${fakeHash}`), dummyFileBuffer);
|
||||
|
||||
expect(collectAssets(inputDir)).toEqual({
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/android-randomHash.js`),
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(`${inputDir}/bundles/ios-randomHash.js`),
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(filterOutAssetsThatAlreadyExistAsync, () => {
|
||||
it('gets a missing asset', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
return [
|
||||
{
|
||||
storageKey: 'blah',
|
||||
status: AssetMetadataStatus.DoesNotExist,
|
||||
__typename: 'AssetMetadataResult',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
expect(
|
||||
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
|
||||
).toBe(1);
|
||||
});
|
||||
it('ignores an asset that exists', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
return [
|
||||
{
|
||||
storageKey: 'blah',
|
||||
status: AssetMetadataStatus.Exists,
|
||||
__typename: 'AssetMetadataResult',
|
||||
},
|
||||
];
|
||||
});
|
||||
expect(
|
||||
await (await filterOutAssetsThatAlreadyExistAsync([{ storageKey: 'blah' } as any])).length
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe(uploadAssetsAsync, () => {
|
||||
const publishBundles = {
|
||||
android: {
|
||||
code: 'android bundle code',
|
||||
assets: [{ files: ['md5-hash-of-file'], type: 'jpg' }],
|
||||
map: 'dummy-string',
|
||||
},
|
||||
ios: {
|
||||
code: 'ios bundle code',
|
||||
assets: [{ files: ['md5-hash-of-file'], type: 'jpg' }],
|
||||
map: 'dummy-string',
|
||||
},
|
||||
};
|
||||
|
||||
const androidBundlePath = uuidv4();
|
||||
const iosBundlePath = uuidv4();
|
||||
const dummyFilePath = uuidv4();
|
||||
const userDefinedPath = uuidv4();
|
||||
fs.writeFileSync(androidBundlePath, publishBundles.android.code);
|
||||
fs.writeFileSync(iosBundlePath, publishBundles.ios.code);
|
||||
fs.writeFileSync(dummyFilePath, dummyFileBuffer);
|
||||
fs.writeFileSync(userDefinedPath, 'I am an octet stream');
|
||||
|
||||
const userDefinedAsset = {
|
||||
type: 'bundle',
|
||||
contentType: 'application/octet-stream',
|
||||
path: userDefinedPath,
|
||||
};
|
||||
|
||||
const assetsForUpdateInfoGroup = {
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
},
|
||||
};
|
||||
jest.spyOn(PublishMutation, 'getUploadURLsAsync').mockImplementation(async () => {
|
||||
return { specifications: ['{}', '{}', '{}'] };
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
it('throws an error if the upload exceeds TIMEOUT_LIMIT', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status = AssetMetadataStatus.DoesNotExist;
|
||||
mockdate.set(Date.now() + TIMEOUT_LIMIT + 1);
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).rejects.toThrow(
|
||||
'Asset upload timed out. Please try again.'
|
||||
);
|
||||
});
|
||||
it('resolves if the assets are already uploaded', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status = AssetMetadataStatus.Exists;
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toBe(undefined);
|
||||
});
|
||||
it('resolves if the assets are eventually uploaded', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status =
|
||||
Date.now() === 0 ? AssetMetadataStatus.DoesNotExist : AssetMetadataStatus.Exists;
|
||||
mockdate.set(Date.now() + 1);
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Platform } from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import { uniqBy } from 'lodash';
|
||||
import mime from 'mime';
|
||||
import path from 'path';
|
||||
|
||||
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
|
||||
import { PublishMutation } from '../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../graphql/queries/PublishQuery';
|
||||
import { PresignedPost, uploadWithPresignedPostAsync } from '../uploads';
|
||||
|
||||
export const TIMEOUT_LIMIT = 60_000; // 1 minute
|
||||
const STORAGE_BUCKET = getStorageBucket();
|
||||
export const Platforms: PublishPlatform[] = ['android', 'ios']; // TODO-JJ allow users to specify this in app.js
|
||||
|
||||
function getStorageBucket(): string {
|
||||
if (process.env.EXPO_STAGING || process.env.EXPO_LOCAL) {
|
||||
return 'update-assets-staging';
|
||||
} else {
|
||||
return 'update-assets-production';
|
||||
}
|
||||
}
|
||||
|
||||
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
|
||||
export type RawAsset = {
|
||||
type: string;
|
||||
contentType: string;
|
||||
path: string;
|
||||
};
|
||||
type CollectedAssets = {
|
||||
[platform in PublishPlatform]?: {
|
||||
launchAsset: RawAsset;
|
||||
assets: RawAsset[];
|
||||
};
|
||||
};
|
||||
|
||||
type ManifestFragment = {
|
||||
launchAsset: PartialManifestAsset;
|
||||
assets: PartialManifestAsset[];
|
||||
};
|
||||
type UpdateInfoGroup = {
|
||||
[key in PublishPlatform]: ManifestFragment;
|
||||
};
|
||||
|
||||
export function guessContentTypeFromExtension(ext?: string): string {
|
||||
return mime.getType(ext ?? '') ?? 'application/octet-stream'; // unrecognized extension
|
||||
}
|
||||
|
||||
export function getBase64URLEncoding(buffer: Buffer): string {
|
||||
const base64 = buffer.toString('base64');
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* The storage key is used to store the asset in GCS
|
||||
*/
|
||||
export function getStorageKey(contentType: string, contentHash: string): string {
|
||||
const nullSeparator = Buffer.alloc(1);
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(contentType)
|
||||
.update(nullSeparator)
|
||||
.update(contentHash)
|
||||
.digest();
|
||||
return getBase64URLEncoding(hash);
|
||||
}
|
||||
|
||||
async function calculateFileHashAsync(filePath: string, algorithm: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createReadStream(filePath).on('error', reject);
|
||||
const hash = file.pipe(crypto.createHash(algorithm)).on('error', reject);
|
||||
hash.on('finish', () => resolve(hash.read()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function that computes an assets storage key starting from its buffer.
|
||||
*/
|
||||
export async function getStorageKeyForAssetAsync(asset: RawAsset): Promise<string> {
|
||||
const fileHash = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
return getStorageKey(asset.contentType, fileHash);
|
||||
}
|
||||
|
||||
export async function convertAssetToUpdateInfoGroupFormatAsync(
|
||||
asset: RawAsset
|
||||
): Promise<PartialManifestAsset> {
|
||||
const fileHash = getBase64URLEncoding(await calculateFileHashAsync(asset.path, 'sha256'));
|
||||
const contentType = asset.contentType;
|
||||
const storageKey = getStorageKey(contentType, fileHash);
|
||||
const bundleKey = [
|
||||
(await calculateFileHashAsync(asset.path, 'md5')).toString('hex'),
|
||||
asset.type,
|
||||
].join('.');
|
||||
|
||||
return {
|
||||
fileHash,
|
||||
contentType,
|
||||
storageBucket: STORAGE_BUCKET,
|
||||
storageKey,
|
||||
bundleKey,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildUpdateInfoGroupAsync(assets: CollectedAssets): Promise<UpdateInfoGroup> {
|
||||
let platform: PublishPlatform;
|
||||
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
|
||||
for (platform in assets) {
|
||||
updateInfoGroup[platform] = {
|
||||
launchAsset: await convertAssetToUpdateInfoGroupFormatAsync(assets[platform]?.launchAsset!),
|
||||
assets: await Promise.all(
|
||||
(assets[platform]?.assets ?? []).map(convertAssetToUpdateInfoGroupFormatAsync)
|
||||
),
|
||||
};
|
||||
}
|
||||
return updateInfoGroup as UpdateInfoGroup;
|
||||
}
|
||||
|
||||
export function resolveInputDirectory(customInputDirectory: string): string {
|
||||
const distRoot = path.resolve(customInputDirectory);
|
||||
if (!fs.existsSync(distRoot)) {
|
||||
throw new Error(`${distRoot} does not exist. Please create it with your desired bundler.`);
|
||||
}
|
||||
return distRoot;
|
||||
}
|
||||
|
||||
export function collectUserDefinedAssets(distRoot: string): RawAsset[] {
|
||||
const assetRoot = path.join(distRoot, 'assets');
|
||||
const assetPointers = Platforms.map(platform => {
|
||||
const assetJsonPath = path.join(distRoot, `${platform}-index.json`);
|
||||
return JsonFile.read(assetJsonPath).bundledAssets;
|
||||
}).flat();
|
||||
|
||||
return [...new Set(assetPointers)].map(pointer => {
|
||||
const [filename, ext] = new String(pointer ?? '').split('_').pop()?.split('.') ?? [];
|
||||
if (!filename) {
|
||||
throw new Error(
|
||||
'There was an error locating all of the bundler defined assets. Please rerun the bundler and then retry publishing.'
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: ext ?? '',
|
||||
contentType: guessContentTypeFromExtension(ext),
|
||||
path: path.join(assetRoot, filename),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function collectBundles(distRoot: string): { [key: string]: RawAsset } {
|
||||
const bundleRoot = path.join(distRoot, 'bundles');
|
||||
const bundlePaths = Object.fromEntries(
|
||||
fs.readdirSync(bundleRoot).map(name => [name.split('-')[0], path.join(bundleRoot, name)])
|
||||
);
|
||||
|
||||
const bundleBuffers: { [key: string]: RawAsset } = {};
|
||||
Platforms.forEach(platform => {
|
||||
bundleBuffers[platform] = {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: bundlePaths[platform],
|
||||
};
|
||||
});
|
||||
return bundleBuffers;
|
||||
}
|
||||
|
||||
export function collectAssets(inputDir: string): CollectedAssets {
|
||||
const distRoot = resolveInputDirectory(inputDir);
|
||||
const assets = collectUserDefinedAssets(distRoot);
|
||||
const bundles = collectBundles(distRoot);
|
||||
const assetsFinal: CollectedAssets = {};
|
||||
|
||||
for (const platform of Platforms) {
|
||||
assetsFinal[platform] = {
|
||||
launchAsset: bundles[platform],
|
||||
assets,
|
||||
};
|
||||
}
|
||||
|
||||
return assetsFinal;
|
||||
}
|
||||
|
||||
export async function filterOutAssetsThatAlreadyExistAsync(
|
||||
uniqueAssetsWithStorageKey: (RawAsset & { storageKey: string })[]
|
||||
): Promise<(RawAsset & { storageKey: string })[]> {
|
||||
const assetMetadata = await PublishQuery.getAssetMetadataAsync(
|
||||
uniqueAssetsWithStorageKey.map(asset => asset.storageKey)
|
||||
);
|
||||
const missingAssetKeys = assetMetadata
|
||||
.filter(result => result.status !== AssetMetadataStatus.Exists)
|
||||
.map(result => result.storageKey);
|
||||
|
||||
const missingAssets = uniqueAssetsWithStorageKey.filter(asset => {
|
||||
return missingAssetKeys.includes(asset.storageKey);
|
||||
});
|
||||
return missingAssets;
|
||||
}
|
||||
|
||||
export async function uploadAssetsAsync(assetsForUpdateInfoGroup: CollectedAssets): Promise<void> {
|
||||
let assets: RawAsset[] = [];
|
||||
let platform: keyof CollectedAssets;
|
||||
for (platform in assetsForUpdateInfoGroup) {
|
||||
assets = [
|
||||
...assets,
|
||||
assetsForUpdateInfoGroup[platform]!.launchAsset,
|
||||
...assetsForUpdateInfoGroup[platform]!.assets,
|
||||
];
|
||||
}
|
||||
|
||||
const uniqueAssets = uniqBy<
|
||||
RawAsset & {
|
||||
storageKey: string;
|
||||
}
|
||||
>(
|
||||
await Promise.all(
|
||||
assets.map(async asset => {
|
||||
return {
|
||||
...asset,
|
||||
storageKey: await getStorageKeyForAssetAsync(asset),
|
||||
};
|
||||
})
|
||||
),
|
||||
'storageKey'
|
||||
);
|
||||
|
||||
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(uniqueAssets);
|
||||
const { specifications } = await PublishMutation.getUploadURLsAsync(
|
||||
missingAssets.map(ma => ma.contentType)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
missingAssets.map((missingAsset, i) => {
|
||||
const presignedPost: PresignedPost = JSON.parse(specifications[i]);
|
||||
return uploadWithPresignedPostAsync(fs.createReadStream(missingAsset.path), presignedPost);
|
||||
})
|
||||
);
|
||||
|
||||
// Wait up to TIMEOUT_LIMIT for assets to be uploaded and processed
|
||||
const start = Date.now();
|
||||
let timeout = 1;
|
||||
while (missingAssets.length > 0) {
|
||||
const timeoutPromise = new Promise(resolve => setTimeout(resolve, timeout * 1000)); // linear backoff
|
||||
missingAssets = await filterOutAssetsThatAlreadyExistAsync(missingAssets);
|
||||
await timeoutPromise; // await after filterOutAssetsThatAlreadyExistAsync for easy mocking with jest.runAllTimers
|
||||
timeout += 1;
|
||||
|
||||
if (Date.now() - start > TIMEOUT_LIMIT) {
|
||||
throw new Error('Asset upload timed out. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
[SubmissionErrorCode.IOS_OLD_VERSION_CODE_ERROR]:
|
||||
"You've already submitted this version of the app.\n" +
|
||||
'Versions are identified by Build Numbers (expo.ios.buildNumber in app.json).\n' +
|
||||
"If you're submitting an Expo project built with EAS Build, increment the build number in app.json and build the project again." +
|
||||
"If you're submitting an Expo project built with EAS Build, increment the build number in app.json and build the project again.\n" +
|
||||
`${learnMore('https://expo.fyi/bumping-ios-build-number')}.`,
|
||||
[SubmissionErrorCode.IOS_UNKNOWN_ERROR]:
|
||||
"We couldn't figure out what went wrong. Please see logs to learn more.",
|
||||
|
||||
@@ -57,8 +57,8 @@ async function obtainS3PresignedPostAsync(
|
||||
export async function uploadWithPresignedPostAsync(
|
||||
stream: Readable | Buffer,
|
||||
presignedPost: PresignedPost,
|
||||
handleProgressEvent: ProgressHandler
|
||||
) {
|
||||
handleProgressEvent?: ProgressHandler
|
||||
): Promise<string> {
|
||||
const form = new FormData();
|
||||
for (const [fieldKey, fieldValue] of Object.entries(presignedPost.fields)) {
|
||||
form.append(fieldKey, fieldValue);
|
||||
@@ -66,17 +66,21 @@ export async function uploadWithPresignedPostAsync(
|
||||
form.append('file', stream);
|
||||
const formHeaders = form.getHeaders();
|
||||
let uploadPromise = got.post(presignedPost.url, { body: form, headers: { ...formHeaders } });
|
||||
|
||||
if (handleProgressEvent) {
|
||||
uploadPromise = uploadPromise.on('uploadProgress', progress =>
|
||||
handleProgressEvent({ progress })
|
||||
);
|
||||
try {
|
||||
const response = await uploadPromise;
|
||||
handleProgressEvent({ isComplete: true });
|
||||
return String(response.headers.location);
|
||||
} catch (error) {
|
||||
handleProgressEvent({ isComplete: true, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await uploadPromise;
|
||||
handleProgressEvent({ isComplete: true });
|
||||
return String(response.headers.location);
|
||||
} catch (error) {
|
||||
handleProgressEvent({ isComplete: true, error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
const response = await uploadPromise;
|
||||
return String(response.headers.location);
|
||||
}
|
||||
@@ -50,10 +50,12 @@ async function isGitInstalledAsync(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getBranchNameAsync(): Promise<string | undefined> {
|
||||
async function getBranchNameAsync(): Promise<string | null> {
|
||||
try {
|
||||
return (await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout;
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getLastCommitMessageAsync(): Promise<string | null> {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitReturns": true,
|
||||
"outDir": "build",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
|
||||
@@ -3686,6 +3686,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18"
|
||||
integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==
|
||||
|
||||
"@types/mime@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
|
||||
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
@@ -9258,6 +9263,11 @@ mime@^2.4.1, mime@^2.4.4:
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
|
||||
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
|
||||
|
||||
mime@^2.4.7:
|
||||
version "2.4.7"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
|
||||
integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
|
||||
|
||||
mimic-fn@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
|
||||
@@ -9397,6 +9407,11 @@ mkdirp@^0.5.0, mkdirp@^0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
mockdate@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.2.tgz#a5a7bb5820da617747af424d7a4dcb22c6c03d79"
|
||||
integrity sha512-ldfYSUW1ocqSHGTK6rrODUiqAFPGAg0xaHqYJ5tvj1hQyFsjuHpuWgWFTZWwDVlzougN/s2/mhDr8r5nY5xDpA==
|
||||
|
||||
modify-values@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
|
||||
|
||||
Reference in new issue
Block a user