Compare commits

...
34 changed files with 362 additions and 128 deletions

No files matched your search

+1
View File
@@ -15,6 +15,7 @@
"@expo/config-types": "50.0.0", "@expo/config-types": "50.0.0",
"@expo/eas-build-job": "1.0.70", "@expo/eas-build-job": "1.0.70",
"@expo/eas-json": "7.1.3", "@expo/eas-json": "7.1.3",
"@expo/fingerprint": "0.6.0",
"@expo/json-file": "8.2.37", "@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0", "@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33", "@expo/osascript": "2.0.33",
@@ -24,7 +24,9 @@ export async function syncProjectConfigurationAsync({
localAutoIncrement?: AndroidVersionAutoIncrement; localAutoIncrement?: AndroidVersionAutoIncrement;
vcsClient: Client; vcsClient: Client;
}): Promise<void> { }): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
@@ -100,7 +100,9 @@ export async function maybeResolveVersionsAsync(
buildProfile: BuildProfile<Platform.ANDROID>, buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client vcsClient: Client
): Promise<{ appVersion?: string; appBuildVersion?: string }> { ): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir); const buildGradle = await getAppBuildGradleAsync(projectDir);
try { try {
+3 -1
View File
@@ -61,7 +61,9 @@ export async function createBuildContextAsync<T extends Platform>({
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env }); const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug; const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId); const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
const accountId = account.id; const accountId = account.id;
const runFromCI = getenv.boolish('CI', false); const runFromCI = getenv.boolish('CI', false);
const developmentClient = const developmentClient =
@@ -22,7 +22,9 @@ export async function syncProjectConfigurationAsync({
localAutoIncrement?: IosVersionAutoIncrement; localAutoIncrement?: IosVersionAutoIncrement;
vcsClient: Client; vcsClient: Client;
}): Promise<void> { }): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
+6 -2
View File
@@ -114,7 +114,9 @@ export async function readShortVersionAsync(
buildSettings: XCBuildConfiguration['buildSettings'], buildSettings: XCBuildConfiguration['buildSettings'],
vcsClient: Client vcsClient: Client
): Promise<string | undefined> { ): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings); const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
@@ -136,7 +138,9 @@ export async function readBuildNumberAsync(
buildSettings: XCBuildConfiguration['buildSettings'], buildSettings: XCBuildConfiguration['buildSettings'],
vcsClient: Client vcsClient: Client
): Promise<string | undefined> { ): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings); const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return ( return (
@@ -43,7 +43,11 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
); );
const workflowPerPlatformList = await Promise.all( const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform, vcsClient)) platformsToCheck.map(platform =>
resolveWorkflowAsync(projectDir, platform, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})
)
); );
Log.newLine(); Log.newLine();
@@ -107,7 +107,9 @@ export async function makeProjectTarballAsync(
startTimer(compressTimerLabel); startTimer(compressTimerLabel);
try { try {
await vcsClient.makeShallowCopyAsync(shallowClonePath); await vcsClient.makeShallowCopyAsync(shallowClonePath, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [ await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.', '.',
]); ]);
+3 -1
View File
@@ -32,7 +32,9 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if ( if (
(await fs.pathExists(absGoogleServicesFilePath)) && (await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) || (!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath)))) (await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath), {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})))
) { ) {
Log.warn( Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.` `File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
@@ -72,14 +72,18 @@ export default class BuildConfigure extends EasCommand {
// configure expo-updates // configure expo-updates
if (expoUpdatesIsInstalled) { if (expoUpdatesIsInstalled) {
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) { if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(projectDir, exp); await syncAndroidUpdatesConfigurationAsync(projectDir, exp);
} }
} }
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) { if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(vcsClient, projectDir, exp); await syncIosUpdatesConfigurationAsync(vcsClient, projectDir, exp);
} }
@@ -94,7 +94,9 @@ export default class BuildInspect extends EasCommand {
if (flags.stage === InspectStage.ARCHIVE) { if (flags.stage === InspectStage.ARCHIVE) {
await vcsClient.ensureRepoExistsAsync(); await vcsClient.ensureRepoExistsAsync();
await vcsClient.makeShallowCopyAsync(tmpWorkingdir); await vcsClient.makeShallowCopyAsync(tmpWorkingdir, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory); await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else { } else {
try { try {
@@ -109,7 +109,9 @@ export default class BuildVersionSyncView extends EasCommand {
toAppPlatform(profileInfo.platform), toAppPlatform(profileInfo.platform),
applicationIdentifier applicationIdentifier
); );
const workflow = await resolveWorkflowAsync(projectDir, profileInfo.platform, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, profileInfo.platform, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (!remoteVersions?.buildVersion) { if (!remoteVersions?.buildVersion) {
Log.warn( Log.warn(
`Skipping versions sync for ${platformDisplayName}. There are no versions configured on Expo servers, use "eas build:version:set" or run a build to initialize it.` `Skipping versions sync for ${platformDisplayName}. There are no versions configured on Expo servers, use "eas build:version:set" or run a build to initialize it.`
+6 -2
View File
@@ -50,8 +50,12 @@ export default class Diagnostics extends EasCommand {
} }
private async printWorkflowAsync(projectDir: string, vcsClient: Client): Promise<void> { private async printWorkflowAsync(projectDir: string, vcsClient: Client): Promise<void> {
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (androidWorkflow === iosWorkflow) { if (androidWorkflow === iosWorkflow) {
Log.log(` Project workflow: ${androidWorkflow}`); Log.log(` Project workflow: ${androidWorkflow}`);
@@ -359,12 +359,13 @@ export default class UpdatePublish extends EasCommand {
throw e; throw e;
} }
const runtimeVersions = await getRuntimeVersionObjectAsync( const runtimeVersions = await getRuntimeVersionObjectAsync({
exp, exp,
realizedPlatforms, platforms: realizedPlatforms,
projectDir, projectDir,
vcsClient vcsClient,
); nonInteractive,
});
const runtimeToPlatformMapping = const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions); getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
@@ -201,12 +201,13 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
const gitCommitHash = await vcsClient.getCommitHashAsync(); const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync(); const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const runtimeVersions = await getRuntimeVersionObjectAsync( const runtimeVersions = await getRuntimeVersionObjectAsync({
exp, exp,
realizedPlatforms, platforms: realizedPlatforms,
projectDir, projectDir,
vcsClient vcsClient,
); nonInteractive,
});
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups']; let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start(); const publishSpinner = ora('Publishing...').start();
@@ -23,12 +23,16 @@ describe(resolveWorkflowAsync, () => {
projectDir projectDir
); );
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient)).resolves.toBe( await expect(
Workflow.GENERIC resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
); useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
await expect(resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient)).resolves.toBe( })
Workflow.GENERIC ).resolves.toBe(Workflow.GENERIC);
); await expect(
resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})
).resolves.toBe(Workflow.GENERIC);
}); });
test('bare workflow for single platform', async () => { test('bare workflow for single platform', async () => {
@@ -39,12 +43,16 @@ describe(resolveWorkflowAsync, () => {
projectDir projectDir
); );
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient)).resolves.toBe( await expect(
Workflow.MANAGED resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
); useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
await expect(resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient)).resolves.toBe( })
Workflow.GENERIC ).resolves.toBe(Workflow.MANAGED);
); await expect(
resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})
).resolves.toBe(Workflow.GENERIC);
}); });
test('android/ios directories are ignored', async () => { test('android/ios directories are ignored', async () => {
@@ -57,12 +65,16 @@ describe(resolveWorkflowAsync, () => {
projectDir projectDir
); );
await expect(resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient)).resolves.toBe( await expect(
Workflow.MANAGED resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
); useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
await expect(resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient)).resolves.toBe( })
Workflow.MANAGED ).resolves.toBe(Workflow.MANAGED);
); await expect(
resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})
).resolves.toBe(Workflow.MANAGED);
}); });
}); });
@@ -80,7 +92,11 @@ describe(resolveWorkflowPerPlatformAsync, () => {
projectDir projectDir
); );
await expect(resolveWorkflowPerPlatformAsync(projectDir, vcsClient)).resolves.toEqual({ await expect(
resolveWorkflowPerPlatformAsync(projectDir, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})
).resolves.toEqual({
android: Workflow.GENERIC, android: Workflow.GENERIC,
ios: Workflow.GENERIC, ios: Workflow.GENERIC,
}); });
@@ -35,7 +35,9 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync({
exp: ExpoConfig; exp: ExpoConfig;
vcsClient: Client; vcsClient: Client;
}): Promise<string> { }): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects'); assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
try { try {
@@ -101,7 +103,9 @@ export async function getApplicationIdAsync(
vcsClient: Client, vcsClient: Client,
gradleContext?: GradleBuildContext gradleContext?: GradleBuildContext
): Promise<string> { ): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp); warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
@@ -16,7 +16,9 @@ export async function resolveGradleBuildContextAsync(
buildProfile: BuildProfile<Platform.ANDROID>, buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client vcsClient: Client
): Promise<GradleBuildContext | undefined> { ): Promise<GradleBuildContext | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
try { try {
if (buildProfile.gradleCommand) { if (buildProfile.gradleCommand) {
@@ -36,7 +36,9 @@ export async function getApplicationIdentifierAsync({
}): Promise<string> { }): Promise<string> {
if (platform === Platform.ANDROID) { if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>; const profile = buildProfile as BuildProfile<Platform.ANDROID>;
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.MANAGED) { if (workflow === Workflow.MANAGED) {
return await ensureApplicationIdIsDefinedForManagedProjectAsync({ return await ensureApplicationIdIsDefinedForManagedProjectAsync({
@@ -51,7 +53,9 @@ export async function getApplicationIdentifierAsync({
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile, vcsClient); const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile, vcsClient);
return await getApplicationIdAsync(projectDir, exp, vcsClient, gradleContext); return await getApplicationIdAsync(projectDir, exp, vcsClient, gradleContext);
} else { } else {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
const profile = buildProfile as BuildProfile<Platform.IOS>; const profile = buildProfile as BuildProfile<Platform.IOS>;
if (workflow === Workflow.MANAGED) { if (workflow === Workflow.MANAGED) {
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync({ return await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
@@ -31,7 +31,11 @@ export async function validateCustomBuildConfigAsync({
`Custom build configuration file ${chalk.bold(relativeConfigPath)} does not exist.` `Custom build configuration file ${chalk.bold(relativeConfigPath)} does not exist.`
); );
} }
if (await vcsClient.isFileIgnoredAsync(relativeConfigPath)) { if (
await vcsClient.isFileIgnoredAsync(relativeConfigPath, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
})
) {
throw new Error( throw new Error(
`Custom build configuration file ${chalk.bold( `Custom build configuration file ${chalk.bold(
relativeConfigPath relativeConfigPath
@@ -28,7 +28,9 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync({
exp: ExpoConfig; exp: ExpoConfig;
vcsClient: Client; vcsClient: Client;
}): Promise<string> { }): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects'); assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
try { try {
@@ -55,7 +57,9 @@ export async function getBundleIdentifierAsync(
vcsClient: Client, vcsClient: Client,
xcodeContext?: { targetName?: string; buildConfiguration?: string } xcodeContext?: { targetName?: string; buildConfiguration?: string }
): Promise<string> { ): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp); warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(projectDir, exp);
@@ -28,7 +28,9 @@ export async function getManagedApplicationTargetEntitlementsAsync(
projectRoot: projectDir, projectRoot: projectDir,
platforms: ['ios'], platforms: ['ios'],
introspect: true, introspect: true,
ignoreExistingNativeFiles: await hasIgnoredIosProjectAsync(projectDir, vcsClient), ignoreExistingNativeFiles: await hasIgnoredIosProjectAsync(projectDir, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
}),
}); });
return expWithMods.ios?.entitlements || {}; return expWithMods.ios?.entitlements || {};
} finally { } finally {
+3 -1
View File
@@ -24,7 +24,9 @@ export async function resolveXcodeBuildContextAsync(
}: { exp: ExpoConfig; projectDir: string; nonInteractive: boolean; vcsClient: Client }, }: { exp: ExpoConfig; projectDir: string; nonInteractive: boolean; vcsClient: Client },
buildProfile: BuildProfile<Platform.IOS> buildProfile: BuildProfile<Platform.IOS>
): Promise<XcodeBuildContext> { ): Promise<XcodeBuildContext> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
const buildScheme = const buildScheme =
buildProfile.scheme ?? buildProfile.scheme ??
+3 -1
View File
@@ -145,7 +145,9 @@ export async function resolveBareProjectTargetsAsync({
} }
export async function resolveTargetsAsync(opts: ResolveTargetOptions): Promise<Target[]> { export async function resolveTargetsAsync(opts: ResolveTargetOptions): Promise<Target[]> {
const workflow = await resolveWorkflowAsync(opts.projectDir, Platform.IOS, opts.vcsClient); const workflow = await resolveWorkflowAsync(opts.projectDir, Platform.IOS, opts.vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
return await resolveBareProjectTargetsAsync(opts); return await resolveBareProjectTargetsAsync(opts);
} else if (workflow === Workflow.MANAGED) { } else if (workflow === Workflow.MANAGED) {
+114 -38
View File
@@ -1,6 +1,7 @@
import { ExpoConfig, Platform } from '@expo/config'; import { ExpoConfig, Platform } from '@expo/config';
import { Updates } from '@expo/config-plugins'; import { Updates } from '@expo/config-plugins';
import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job'; import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job';
import * as Fingerprint from '@expo/fingerprint';
import JsonFile from '@expo/json-file'; import JsonFile from '@expo/json-file';
import assert from 'assert'; import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
@@ -8,7 +9,6 @@ import crypto from 'crypto';
import fs from 'fs-extra'; import fs from 'fs-extra';
import Joi from 'joi'; import Joi from 'joi';
import mime from 'mime'; import mime from 'mime';
import nullthrows from 'nullthrows';
import path from 'path'; import path from 'path';
import promiseLimit from 'promise-limit'; import promiseLimit from 'promise-limit';
@@ -22,7 +22,7 @@ import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery'; import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log, { learnMore } from '../log'; import Log, { learnMore } from '../log';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform'; import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts'; import { confirmAsync, promptAsync } from '../prompts';
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync'; import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils'; import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads'; import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
@@ -675,47 +675,123 @@ export function getRequestedPlatform(
} }
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */ /** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
export async function getRuntimeVersionObjectAsync( export async function getRuntimeVersionObjectAsync({
exp: ExpoConfig, exp,
platforms: Platform[], platforms,
projectDir: string, projectDir,
vcsClient: Client vcsClient,
): Promise<{ platform: string; runtimeVersion: string }[]> { nonInteractive,
}: {
exp: ExpoConfig;
platforms: Platform[];
projectDir: string;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<{ platform: string; runtimeVersion: string }[]> {
const runtimeVersionObjects: { platform: string; runtimeVersion: string }[] = [];
// calculate these serially since in some cases (fingerprint) we need to prompt the user to confirm that
// the runtime version is ready to be calculated
for (const platform of platforms) { for (const platform of platforms) {
if (platform === 'web') { runtimeVersionObjects.push({
continue; platform,
} runtimeVersion: await getRuntimeVersionForPlatformAsync({
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object'; exp,
if (isPolicy) { platform,
const isManaged = projectDir,
(await resolveWorkflowAsync(projectDir, platform as EASBuildJobPlatform, vcsClient)) === vcsClient,
Workflow.MANAGED; nonInteractive,
if (!isManaged) { }),
throw new Error( });
`You're currently using the bare workflow, where runtime version policies are not supported. You must set your runtime version manually. For example, define your runtime version as "1.0.0", not {"policy": "appVersion"} in your app config. ${learnMore( }
'https://docs.expo.dev/eas-update/runtime-versions'
)}` return runtimeVersionObjects;
); }
async function getRuntimeVersionForPlatformAsync({
exp,
platform,
projectDir,
vcsClient,
nonInteractive,
}: {
exp: ExpoConfig;
platform: Platform;
projectDir: string;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<string> {
if (platform === 'web') {
return 'UNVERSIONED';
}
const runtimeVersion = exp[platform]?.runtimeVersion ?? exp.runtimeVersion;
if (typeof runtimeVersion === 'object') {
const workflowIgnoringEASIgnoreToMatchExpoUpdatesDetection = await resolveWorkflowAsync(
projectDir,
platform as EASBuildJobPlatform,
vcsClient,
{ useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: false }
);
const policy = runtimeVersion.policy;
if (policy === 'fingerprintExperimental') {
if (workflowIgnoringEASIgnoreToMatchExpoUpdatesDetection === Workflow.GENERIC) {
if (nonInteractive) {
// log to inform the user that the fingerprint has been calculated
Log.warn(
`Calculating native fingerprint for platform ${platform} using current state of the "${platform}" directory.`
);
} else {
// prompt the user to confirm that their native projects are in a state that is ready
// for the fingerprint to be calculated
const confirm = await confirmAsync({
message: `Calculating native fingerprint for platform ${platform} using current state of the "${platform}" directory. Do you want to continue?`,
});
if (!confirm) {
throw new Error('Aborted');
}
}
return await Fingerprint.createProjectHashAsync(projectDir, {
platforms: [platform],
});
} else if (workflowIgnoringEASIgnoreToMatchExpoUpdatesDetection === Workflow.MANAGED) {
// ignore everything in native directories to ensure fingerprint is the same
// no matter whether project has been prebuilt
return await Fingerprint.createProjectHashAsync(projectDir, {
platforms: [platform],
ignorePaths: ['/android/**/*', '/ios/**/*'],
});
} else {
throw new Error('Unknown workflow');
} }
} }
const workflow = await resolveWorkflowAsync(
projectDir,
platform as EASBuildJobPlatform,
vcsClient,
{ useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true }
);
if (workflow !== Workflow.MANAGED) {
throw new Error(
`You're currently using the bare workflow, where runtime version policies are not supported. You must set your runtime version manually. For example, define your runtime version as "1.0.0", not {"policy": "appVersion"} in your app config. ${learnMore(
'https://docs.expo.dev/eas-update/runtime-versions'
)}`
);
}
} }
return await Promise.all( const resolvedRuntimeVersion = await Updates.getRuntimeVersionAsync(projectDir, exp, platform);
[...new Set(platforms)].map(async platform => { if (!resolvedRuntimeVersion) {
if (platform === 'web') { throw new Error(
return { platform: 'web', runtimeVersion: 'UNVERSIONED' }; `Unable to determine runtime version for ${
} requestedPlatformDisplayNames[platform]
return { }. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
platform, );
runtimeVersion: nullthrows( }
await Updates.getRuntimeVersionAsync(projectDir, exp, platform),
`Unable to determine runtime version for ${ return resolvedRuntimeVersion;
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
})
);
} }
export function getRuntimeToPlatformMappingFromRuntimeVersions( export function getRuntimeToPlatformMappingFromRuntimeVersions(
+16 -7
View File
@@ -8,7 +8,10 @@ import { Client } from '../vcs/vcs';
export async function resolveWorkflowAsync( export async function resolveWorkflowAsync(
projectDir: string, projectDir: string,
platform: Platform, platform: Platform,
vcsClient: Client vcsClient: Client,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<Workflow> { ): Promise<Workflow> {
let platformWorkflowMarkers: string[]; let platformWorkflowMarkers: string[];
try { try {
@@ -27,7 +30,7 @@ export async function resolveWorkflowAsync(
for (const marker of platformWorkflowMarkers) { for (const marker of platformWorkflowMarkers) {
if ( if (
(await fs.pathExists(marker)) && (await fs.pathExists(marker)) &&
!(await vcsClient.isFileIgnoredAsync(path.relative(vcsRootPath, marker))) !(await vcsClient.isFileIgnoredAsync(path.relative(vcsRootPath, marker), options))
) { ) {
return Workflow.GENERIC; return Workflow.GENERIC;
} }
@@ -37,24 +40,30 @@ export async function resolveWorkflowAsync(
export async function resolveWorkflowPerPlatformAsync( export async function resolveWorkflowPerPlatformAsync(
projectDir: string, projectDir: string,
vcsClient: Client vcsClient: Client,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<Record<Platform, Workflow>> { ): Promise<Record<Platform, Workflow>> {
const [android, ios] = await Promise.all([ const [android, ios] = await Promise.all([
resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient), resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient, options),
resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient), resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient, options),
]); ]);
return { android, ios }; return { android, ios };
} }
export async function hasIgnoredIosProjectAsync( export async function hasIgnoredIosProjectAsync(
projectDir: string, projectDir: string,
vcsClient: Client vcsClient: Client,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<boolean> { ): Promise<boolean> {
const vcsRootPath = path.normalize(await vcsClient.getRootPathAsync()); const vcsRootPath = path.normalize(await vcsClient.getRootPathAsync());
try { try {
const pbxProjectPath = IOSConfig.Paths.getPBXProjectPath(projectDir); const pbxProjectPath = IOSConfig.Paths.getPBXProjectPath(projectDir);
return await vcsClient.isFileIgnoredAsync(path.relative(vcsRootPath, pbxProjectPath)); return await vcsClient.isFileIgnoredAsync(path.relative(vcsRootPath, pbxProjectPath), options);
} finally { } finally {
return false; return false;
} }
+3 -1
View File
@@ -384,7 +384,9 @@ export async function ensureEASUpdateIsConfiguredAsync({
return; return;
} }
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient); const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient, {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true,
});
const { projectChanged, exp: expWithUpdates } = const { projectChanged, exp: expWithUpdates } =
await ensureEASUpdatesIsConfiguredInExpoConfigAsync({ await ensureEASUpdatesIsConfiguredInExpoConfigAsync({
exp: expMaybeWithoutUpdates, exp: expMaybeWithoutUpdates,
@@ -18,7 +18,7 @@ describe(Ignore, () => {
'/root' '/root'
); );
const ignore = new Ignore('/root'); const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true });
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(true); expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false); expect(ignore.ignores('bbb')).toBe(false);
@@ -36,7 +36,7 @@ describe(Ignore, () => {
'/root' '/root'
); );
const ignore = new Ignore('/root'); const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true });
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(false); expect(ignore.ignores('aaa')).toBe(false);
expect(ignore.ignores('bbb')).toBe(false); expect(ignore.ignores('bbb')).toBe(false);
@@ -44,6 +44,25 @@ describe(Ignore, () => {
expect((ignore as any).ignoreMapping.map((i: any) => i[0])).toEqual(['', '']); expect((ignore as any).ignoreMapping.map((i: any) => i[0])).toEqual(['', '']);
}); });
it('uses .gitignore files if .easignore is present but useEASIgnoreIfAvailableWhenEvaluatingFileIgnores is false', async () => {
vol.fromJSON(
{
'.gitignore': 'aaa',
'.easignore': 'ccc',
'dir/.gitignore': 'bbb',
},
'/root'
);
const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: false });
await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('ccc')).toBe(false);
expect(ignore.ignores('dir/aaa')).toBe(true);
expect(ignore.ignores('dir/bbb')).toBe(true);
});
it('ignores .gitignore files in node_modules content', async () => { it('ignores .gitignore files in node_modules content', async () => {
vol.fromJSON( vol.fromJSON(
{ {
@@ -54,7 +73,7 @@ describe(Ignore, () => {
'/root' '/root'
); );
const ignore = new Ignore('/root'); const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true });
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
expect(ignore.ignores('aaa')).toBe(true); expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false); expect(ignore.ignores('bbb')).toBe(false);
@@ -72,7 +91,7 @@ describe(Ignore, () => {
'/root' '/root'
); );
const ignore = new Ignore('/root'); const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true });
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
expect(ignore.ignores('dir/ccc')).toBe(true); expect(ignore.ignores('dir/ccc')).toBe(true);
}); });
@@ -80,7 +99,7 @@ describe(Ignore, () => {
it('ignores .git', async () => { it('ignores .git', async () => {
vol.fromJSON({}, '/root'); vol.fromJSON({}, '/root');
const ignore = new Ignore('/root'); const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true });
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
expect(ignore.ignores('.git')).toBe(true); expect(ignore.ignores('.git')).toBe(true);
}); });
@@ -93,7 +112,7 @@ describe(Ignore, () => {
'/root' '/root'
); );
const ignore = new Ignore('/root'); const ignore = new Ignore('/root', { useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: true });
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
expect(() => ignore.ignores('dir/test')).not.toThrowError(); expect(() => ignore.ignores('dir/test')).not.toThrowError();
}); });
+12 -2
View File
@@ -120,7 +120,12 @@ export default class GitClient extends Client {
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim(); return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
} }
public async makeShallowCopyAsync(destinationPath: string): Promise<void> { public async makeShallowCopyAsync(
destinationPath: string,
_options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<void> {
if (await this.hasUncommittedChangesAsync()) { if (await this.hasUncommittedChangesAsync()) {
// it should already be checked before this function is called, but in case it wasn't // it should already be checked before this function is called, but in case it wasn't
// we want to ensure that any changes were introduced by call to `setGitCaseSensitivityAsync` // we want to ensure that any changes were introduced by call to `setGitCaseSensitivityAsync`
@@ -207,7 +212,12 @@ export default class GitClient extends Client {
); );
} }
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> { public override async isFileIgnoredAsync(
filePath: string,
_options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<boolean> {
try { try {
await spawnAsync('git', ['check-ignore', '-q', filePath], { await spawnAsync('git', ['check-ignore', '-q', filePath], {
cwd: path.normalize(await this.getRootPathAsync()), cwd: path.normalize(await this.getRootPathAsync()),
@@ -15,16 +15,26 @@ export default class GitNoCommitClient extends GitClient {
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim(); return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
} }
public override async makeShallowCopyAsync(destinationPath: string): Promise<void> { public override async makeShallowCopyAsync(
destinationPath: string,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<void> {
// normalize converts C:/some/path to C:\some\path on windows // normalize converts C:/some/path to C:\some\path on windows
const srcPath = path.normalize(await this.getRootPathAsync()); const srcPath = path.normalize(await this.getRootPathAsync());
await makeShallowCopyAsync(srcPath, destinationPath); await makeShallowCopyAsync(srcPath, destinationPath, options);
} }
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> { public override async isFileIgnoredAsync(
filePath: string,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<boolean> {
// normalize converts C:/some/path to C:\some\path on windows // normalize converts C:/some/path to C:\some\path on windows
const rootPath = path.normalize(await this.getRootPathAsync()); const rootPath = path.normalize(await this.getRootPathAsync());
const ignore = new Ignore(rootPath); const ignore = new Ignore(rootPath, options);
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
return ignore.ignores(filePath); return ignore.ignores(filePath);
} }
+14 -4
View File
@@ -6,13 +6,23 @@ export default class NoVcsClient extends Client {
return getRootPath(); return getRootPath();
} }
public async makeShallowCopyAsync(destinationPath: string): Promise<void> { public async makeShallowCopyAsync(
destinationPath: string,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<void> {
const srcPath = getRootPath(); const srcPath = getRootPath();
await makeShallowCopyAsync(srcPath, destinationPath); await makeShallowCopyAsync(srcPath, destinationPath, options);
} }
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> { public override async isFileIgnoredAsync(
const ignore = new Ignore(getRootPath()); filePath: string,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<boolean> {
const ignore = new Ignore(getRootPath(), options);
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
return ignore.ignores(filePath); return ignore.ignores(filePath);
} }
+23 -10
View File
@@ -32,16 +32,23 @@ export function getRootPath(): string {
export class Ignore { export class Ignore {
private ignoreMapping: (readonly [string, SingleFileIgnore])[] = []; private ignoreMapping: (readonly [string, SingleFileIgnore])[] = [];
constructor(private rootDir: string) {} constructor(
private readonly rootDir: string,
private readonly options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
) {}
public async initIgnoreAsync(): Promise<void> { public async initIgnoreAsync(): Promise<void> {
const easIgnorePath = path.join(this.rootDir, EASIGNORE_FILENAME); if (this.options.useEASIgnoreIfAvailableWhenEvaluatingFileIgnores) {
if (await fs.pathExists(easIgnorePath)) { const easIgnorePath = path.join(this.rootDir, EASIGNORE_FILENAME);
this.ignoreMapping = [ if (await fs.pathExists(easIgnorePath)) {
['', createIgnore().add(DEFAULT_IGNORE)], this.ignoreMapping = [
['', createIgnore().add(await fs.readFile(easIgnorePath, 'utf-8'))], ['', createIgnore().add(DEFAULT_IGNORE)],
]; ['', createIgnore().add(await fs.readFile(easIgnorePath, 'utf-8'))],
return; ];
return;
}
} }
const ignoreFilePaths = ( const ignoreFilePaths = (
await fg(`**/${GITIGNORE_FILENAME}`, { await fg(`**/${GITIGNORE_FILENAME}`, {
@@ -74,8 +81,14 @@ export class Ignore {
} }
} }
export async function makeShallowCopyAsync(src: string, dst: string): Promise<void> { export async function makeShallowCopyAsync(
const ignore = new Ignore(src); src: string,
dst: string,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<void> {
const ignore = new Ignore(src, options);
await ignore.initIgnoreAsync(); await ignore.initIgnoreAsync();
await fs.copy(src, dst, { await fs.copy(src, dst, {
filter: (srcFilePath: string) => { filter: (srcFilePath: string) => {
+12 -2
View File
@@ -3,7 +3,12 @@ export abstract class Client {
// destination, folder created this way will be uploaded "as is", so implementation should skip // destination, folder created this way will be uploaded "as is", so implementation should skip
// anything that is not committed to the repository. Most optimal solution is to create shallow clone // anything that is not committed to the repository. Most optimal solution is to create shallow clone
// using tooling provided by specific VCS, that respects all ignore rules // using tooling provided by specific VCS, that respects all ignore rules
public abstract makeShallowCopyAsync(destinationPath: string): Promise<void>; public abstract makeShallowCopyAsync(
destinationPath: string,
options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<void>;
// Find root of the repository. // Find root of the repository.
// //
@@ -75,7 +80,12 @@ export abstract class Client {
// //
// @param filePath has to be a relative normalized path pointing to a file // @param filePath has to be a relative normalized path pointing to a file
// located under the root of the repository // located under the root of the repository
public async isFileIgnoredAsync(_filePath: string): Promise<boolean> { public async isFileIgnoredAsync(
_filePath: string,
_options: {
useEASIgnoreIfAvailableWhenEvaluatingFileIgnores: boolean;
}
): Promise<boolean> {
return false; return false;
} }
Generated
+1 -1
View File
@@ -1470,7 +1470,7 @@
joi "^17.11.0" joi "^17.11.0"
semver "^7.5.4" semver "^7.5.4"
"@expo/fingerprint@^0.6.0": "@expo/[email protected]", "@expo/fingerprint@^0.6.0":
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.6.0.tgz#77366934673d4ecea37284109b4dd67f9e6a7487" resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.6.0.tgz#77366934673d4ecea37284109b4dd67f9e6a7487"
integrity sha512-KfpoVRTMwMNJ/Cf5o+Ou8M/Y0EGSTqK+rbi70M2Y0K2qgWNfMJ1gm6sYO9uc8lcTr7YSYM1Rme3dk7QXhpScNA== integrity sha512-KfpoVRTMwMNJ/Cf5o+Ou8M/Y0EGSTqK+rbi70M2Y0K2qgWNfMJ1gm6sYO9uc8lcTr7YSYM1Rme3dk7QXhpScNA==