diff --git a/packages/build-tools/package.json b/packages/build-tools/package.json index 7a52582d..e579a19a 100644 --- a/packages/build-tools/package.json +++ b/packages/build-tools/package.json @@ -34,9 +34,7 @@ "fs-extra": "^9.0.0", "node-forge": "^0.9.1", "nullthrows": "^1.1.1", - "plist": "^3.0.1", - "uuid": "^3.3.3", - "xml2js": "^0.4.23" + "plist": "^3.0.1" }, "devDependencies": { "@types/fs-extra": "^9.0.1", diff --git a/packages/build-tools/src/__mocks__/fs.ts b/packages/build-tools/src/__mocks__/fs.ts index 7b6f1561..8314c390 100644 --- a/packages/build-tools/src/__mocks__/fs.ts +++ b/packages/build-tools/src/__mocks__/fs.ts @@ -3,5 +3,8 @@ import { fs } from 'memfs'; // `temp-dir` dependency of `tempy` is using `fs.realpathSync('/tmp')` // on import to verify existence of tmp directory fs.mkdirSync('/tmp'); +if (process.env.TMPDIR) { + fs.mkdirSync(process.env.TMPDIR, { recursive: true }); +} module.exports = fs; diff --git a/packages/build-tools/src/android/__tests__/expoUpdates.test.ts b/packages/build-tools/src/android/__tests__/expoUpdates.test.ts new file mode 100644 index 00000000..acd5583f --- /dev/null +++ b/packages/build-tools/src/android/__tests__/expoUpdates.test.ts @@ -0,0 +1,156 @@ +import path from 'path'; + +import fs from 'fs-extra'; +import { AndroidConfig } from '@expo/config-plugins'; + +import { + AndroidMetadataName, + androidGetNativelyDefinedClassicReleaseChannelAsync, + androidSetChannelNativelyAsync, + androidSetClassicReleaseChannelNativelyAsync, +} from '../expoUpdates'; + +jest.mock('fs'); + +const channel = 'main'; +const noMetadataAndroidManifest = ` + + + + + + + + + + + + + + + +`; +describe(androidSetClassicReleaseChannelNativelyAsync, () => { + test('sets the release channel', async () => { + const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-'); + fs.ensureDirSync(reactNativeProjectDirectory); + const releaseChannel = 'default'; + const ctx = { + reactNativeProjectDirectory, + job: { releaseChannel }, + logger: { info: () => {} }, + }; + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, 'android')); + const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync( + reactNativeProjectDirectory + ); + const manifestDirectory = path.dirname(manifestPath); + + fs.ensureDirSync(manifestDirectory); + fs.writeFileSync(manifestPath, Buffer.from(noMetadataAndroidManifest)); + const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + expect( + AndroidConfig.Manifest.getMainApplicationMetaDataValue(androidManifest, 'releaseChannel') + ).toBe(null); + + await androidSetClassicReleaseChannelNativelyAsync(ctx as any); + + const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + expect( + AndroidConfig.Manifest.getMainApplicationMetaDataValue( + newAndroidManifest, + AndroidMetadataName.RELEASE_CHANNEL + ) + ).toBe(releaseChannel); + }); +}); +describe(androidSetChannelNativelyAsync, () => { + it('sets the channel', async () => { + const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-'); + fs.ensureDirSync(reactNativeProjectDirectory); + const ctx = { + reactNativeProjectDirectory, + job: { updates: { channel } }, + logger: { info: () => {} }, + }; + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, 'android')); + const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync( + reactNativeProjectDirectory + ); + const manifestDirectory = path.dirname(manifestPath); + + fs.ensureDirSync(manifestDirectory); + fs.writeFileSync(manifestPath, noMetadataAndroidManifest); + + const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + expect( + AndroidConfig.Manifest.getMainApplicationMetaDataValue( + androidManifest, + AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY + ) + ).toBe(null); + + await androidSetChannelNativelyAsync(ctx as any); + + const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue( + newAndroidManifest, + AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY + ); + expect(newValue).toBeDefined(); + expect(JSON.parse(newValue!)).toEqual({ 'expo-channel-name': channel }); + }); +}); +describe(androidGetNativelyDefinedClassicReleaseChannelAsync, () => { + it('gets the natively defined release channel', async () => { + const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-'); + fs.ensureDirSync(reactNativeProjectDirectory); + const releaseChannel = 'default'; + const ctx = { + reactNativeProjectDirectory, + logger: { info: () => {} }, + }; + + const releaseChannelInAndroidManifest = ` + + + + + + + + + + + + + `; + fs.ensureDirSync(path.join(reactNativeProjectDirectory, 'android')); + const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync( + reactNativeProjectDirectory + ); + const manifestDirectory = path.dirname(manifestPath); + + fs.ensureDirSync(manifestDirectory); + fs.writeFileSync(manifestPath, releaseChannelInAndroidManifest); + + const nativelyDefinedReleaseChannel = await androidGetNativelyDefinedClassicReleaseChannelAsync( + ctx as any + ); + expect(nativelyDefinedReleaseChannel).toBe(releaseChannel); + }); +}); diff --git a/packages/build-tools/src/android/expoUpdates.ts b/packages/build-tools/src/android/expoUpdates.ts new file mode 100644 index 00000000..f28f8b98 --- /dev/null +++ b/packages/build-tools/src/android/expoUpdates.ts @@ -0,0 +1,81 @@ +import assert from 'assert'; + +import fs from 'fs-extra'; +import { AndroidConfig } from '@expo/config-plugins'; +import { Job } from '@expo/eas-build-job'; + +import { BuildContext } from '../context'; + +export enum AndroidMetadataName { + UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY', + RELEASE_CHANNEL = 'expo.modules.updates.EXPO_RELEASE_CHANNEL', +} + +export async function androidSetChannelNativelyAsync(ctx: BuildContext): Promise { + assert(ctx.job.updates?.channel, 'updates.channel must be defined'); + + const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync( + ctx.reactNativeProjectDirectory + ); + + if (!(await fs.pathExists(manifestPath))) { + throw new Error(`Couldn't find Android manifest at ${manifestPath}`); + } + + const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest); + const stringifiedUpdatesRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue( + androidManifest, + AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY + ); + AndroidConfig.Manifest.addMetaDataItemToMainApplication( + mainApp, + AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY, + JSON.stringify({ + ...JSON.parse(stringifiedUpdatesRequestHeaders ?? '{}'), + 'expo-channel-name': ctx.job.updates.channel, + }), + 'value' + ); + await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest); +} + +export async function androidSetClassicReleaseChannelNativelyAsync( + ctx: BuildContext +): Promise { + assert(ctx.job.releaseChannel, 'releaseChannel must be defined'); + + const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync( + ctx.reactNativeProjectDirectory + ); + if (!(await fs.pathExists(manifestPath))) { + throw new Error(`Couldn't find Android manifest at ${manifestPath}`); + } + + const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest); + AndroidConfig.Manifest.addMetaDataItemToMainApplication( + mainApp, + AndroidMetadataName.RELEASE_CHANNEL, + ctx.job.releaseChannel, + 'value' + ); + await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest); +} + +export async function androidGetNativelyDefinedClassicReleaseChannelAsync( + ctx: BuildContext +): Promise { + const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync( + ctx.reactNativeProjectDirectory + ); + if (!(await fs.pathExists(manifestPath))) { + return null; + } + + const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath); + return AndroidConfig.Manifest.getMainApplicationMetaDataValue( + androidManifest, + AndroidMetadataName.RELEASE_CHANNEL + ); +} diff --git a/packages/build-tools/src/android/releaseChannel.ts b/packages/build-tools/src/android/releaseChannel.ts deleted file mode 100644 index d171b4c9..00000000 --- a/packages/build-tools/src/android/releaseChannel.ts +++ /dev/null @@ -1,91 +0,0 @@ -import path from 'path'; - -import * as xml from 'xml2js'; -import fs from 'fs-extra'; - -const RELEASE_CHANNEL = 'expo.modules.updates.EXPO_RELEASE_CHANNEL'; - -async function updateReleaseChannel( - reactNativeProjectDirectory: string, - releaseChannel: string -): Promise { - const manifestPath = path.join( - reactNativeProjectDirectory, - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml' - ); - - if (!(await fs.pathExists(manifestPath))) { - throw new Error(`Couldn't find Android manifest at ${manifestPath}`); - } - - const manifestContent = await fs.readFile(manifestPath, 'utf8'); - const manifest = await xml.parseStringPromise(manifestContent); - - const mainApplication = manifest?.manifest?.application?.find( - (e: any) => e?.['$']?.['android:name'] === '.MainApplication' - ); - - if (!mainApplication) { - throw new Error(`Couldn't find '.MainApplication' in the manifest at ${manifestPath}`); - } - - const newItem = { - $: { - 'android:name': RELEASE_CHANNEL, - 'android:value': releaseChannel, - }, - }; - - if (mainApplication['meta-data']) { - const existingMetaDataItem = mainApplication['meta-data'].find( - (e: any) => e.$['android:name'] === RELEASE_CHANNEL - ); - - if (existingMetaDataItem) { - existingMetaDataItem.$['android:value'] = releaseChannel; - } else { - mainApplication['meta-data'].push(newItem); - } - } else { - mainApplication['meta-data'] = [newItem]; - } - - const manifestXml = new xml.Builder().buildObject(manifest); - await fs.writeFile(manifestPath, manifestXml); -} - -async function getReleaseChannel(reactNativeProjectDirectory: string): Promise { - const manifestPath = path.join( - reactNativeProjectDirectory, - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml' - ); - - if (!(await fs.pathExists(manifestPath))) { - throw new Error(`Couldn't find Android manifest at ${manifestPath}`); - } - - const manifestContent = await fs.readFile(manifestPath, 'utf8'); - const manifest = await xml.parseStringPromise(manifestContent); - - const mainApplication = manifest?.manifest?.application?.find( - (e: any) => e?.['$']?.['android:name'] === '.MainApplication' - ); - - if (!mainApplication?.['meta-data']) { - return; - } - const existingMetaDataItem = mainApplication['meta-data'].find( - (e: any) => e.$['android:name'] === RELEASE_CHANNEL - ); - return existingMetaDataItem?.$?.['android:value']; -} - -export { getReleaseChannel, updateReleaseChannel }; diff --git a/packages/build-tools/src/builders/androidGeneric.ts b/packages/build-tools/src/builders/androidGeneric.ts index f196b501..7b549b09 100644 --- a/packages/build-tools/src/builders/androidGeneric.ts +++ b/packages/build-tools/src/builders/androidGeneric.ts @@ -4,10 +4,9 @@ import { BuildContext } from '../context'; import { setup } from '../utils/project'; import { findBuildArtifacts } from '../utils/buildArtifacts'; import { Hook, runHookIfPresent } from '../utils/hooks'; -import { getReleaseChannel, updateReleaseChannel } from '../android/releaseChannel'; import { restoreCredentials } from '../android/credentials'; import { runGradleCommand, ensureLFLineEndingsInGradlewScript } from '../android/gradle'; -import { configureExpoUpdatesIfInstalled } from '../generic/expoUpdates'; +import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates'; export default async function androidGenericBuilder( ctx: BuildContext @@ -33,7 +32,7 @@ export default async function androidGenericBuilder( } await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => { - await configureExpoUpdatesIfInstalled(ctx, { getReleaseChannel, updateReleaseChannel }); + await configureExpoUpdatesIfInstalledAsync(ctx); }); await ctx.runBuildPhase(BuildPhase.RUN_GRADLEW, async () => { diff --git a/packages/build-tools/src/builders/androidManaged.ts b/packages/build-tools/src/builders/androidManaged.ts index 5cd146fd..9620c62e 100644 --- a/packages/build-tools/src/builders/androidManaged.ts +++ b/packages/build-tools/src/builders/androidManaged.ts @@ -2,11 +2,10 @@ import { AndroidConfig } from '@expo/config-plugins'; import { Android, BuildPhase } from '@expo/eas-build-job'; import { ManagedBuildContext } from '../managed/context'; -import { configureExpoUpdatesIfInstalled } from '../managed/expoUpdates'; +import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates'; import { setup } from '../utils/project'; import { findSingleBuildArtifact } from '../utils/buildArtifacts'; import { Hook, runHookIfPresent } from '../utils/hooks'; -import { updateReleaseChannel } from '../android/releaseChannel'; import { restoreCredentials } from '../android/credentials'; import { runGradleCommand } from '../android/gradle'; @@ -33,7 +32,7 @@ export default async function androidManagedBuilder( }); } await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => { - await configureExpoUpdatesIfInstalled(ctx, updateReleaseChannel); + await configureExpoUpdatesIfInstalledAsync(ctx); }); await ctx.runBuildPhase(BuildPhase.RUN_GRADLEW, async () => { diff --git a/packages/build-tools/src/builders/iosGeneric.ts b/packages/build-tools/src/builders/iosGeneric.ts index 6097fbca..f818f2be 100644 --- a/packages/build-tools/src/builders/iosGeneric.ts +++ b/packages/build-tools/src/builders/iosGeneric.ts @@ -4,12 +4,11 @@ import { BuildContext } from '../context'; import { setup } from '../utils/project'; import { findBuildArtifacts } from '../utils/buildArtifacts'; import { Hook, runHookIfPresent } from '../utils/hooks'; -import { updateReleaseChannel, getReleaseChannel } from '../ios/releaseChannel'; import CredentialsManager from '../ios/credentials/manager'; import { configureXcodeProject } from '../ios/configure'; import { runFastlaneGym } from '../ios/fastlane'; import { installPods } from '../ios/pod'; -import { configureExpoUpdatesIfInstalled } from '../generic/expoUpdates'; +import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates'; export default async function iosGenericBuilder( ctx: BuildContext @@ -40,7 +39,7 @@ export default async function iosGenericBuilder( } await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => { - await configureExpoUpdatesIfInstalled(ctx, { getReleaseChannel, updateReleaseChannel }); + await configureExpoUpdatesIfInstalledAsync(ctx); }); await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => { diff --git a/packages/build-tools/src/builders/iosManaged.ts b/packages/build-tools/src/builders/iosManaged.ts index 470cf2a9..ac8c55e1 100644 --- a/packages/build-tools/src/builders/iosManaged.ts +++ b/packages/build-tools/src/builders/iosManaged.ts @@ -4,11 +4,10 @@ import { IOSConfig } from '@expo/config-plugins'; import { BuildPhase, Ios } from '@expo/eas-build-job'; import { ManagedBuildContext } from '../managed/context'; -import { configureExpoUpdatesIfInstalled } from '../managed/expoUpdates'; +import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates'; import { setup } from '../utils/project'; import { findSingleBuildArtifact } from '../utils/buildArtifacts'; import { Hook, runHookIfPresent } from '../utils/hooks'; -import { updateReleaseChannel } from '../ios/releaseChannel'; import { configureXcodeProject } from '../ios/configure'; import CredentialsManager from '../ios/credentials/manager'; import { runFastlaneGym } from '../ios/fastlane'; @@ -47,7 +46,7 @@ export default async function iosManagedBuilder( } await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => { - await configureExpoUpdatesIfInstalled(ctx, updateReleaseChannel); + await configureExpoUpdatesIfInstalledAsync(ctx); }); await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => { diff --git a/packages/build-tools/src/generic/expoUpdates.ts b/packages/build-tools/src/generic/expoUpdates.ts deleted file mode 100644 index 419e6d39..00000000 --- a/packages/build-tools/src/generic/expoUpdates.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Ios, Android, Platform } from '@expo/eas-build-job'; - -import { BuildContext } from '../context'; -import isExpoUpdatesInstalledAsync from '../utils/isExpoUpdatesInstalled'; - -type GenericJob = Ios.GenericJob | Android.GenericJob; - -export async function configureExpoUpdatesIfInstalled( - ctx: BuildContext, - { - getReleaseChannel, - updateReleaseChannel, - }: { - getReleaseChannel: (dir: string) => Promise; - updateReleaseChannel: (dir: string, releaseChannel: string) => Promise; - } -): Promise { - if (await isExpoUpdatesInstalledAsync(ctx.reactNativeProjectDirectory)) { - if (ctx.job.releaseChannel) { - const configFile = - ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist'; - ctx.logger.info( - `Setting the release channel in '${configFile}' to '${ctx.job.releaseChannel}'` - ); - await updateReleaseChannel(ctx.reactNativeProjectDirectory, ctx.job.releaseChannel); - } else { - const channel = await getReleaseChannel(ctx.reactNativeProjectDirectory); - if (!channel || channel === 'default') { - ctx.logger.info(`Using default release channel for 'expo-updates' (default)`); - } else { - ctx.logger.info(`Using the release channel pre-configured in native project (${channel})`); - ctx.logger.warn('Please add the "releaseChannel" field to your build profile (eas.json)'); - } - } - } -} diff --git a/packages/build-tools/src/ios/__tests__/expoUpdates.test.ts b/packages/build-tools/src/ios/__tests__/expoUpdates.test.ts new file mode 100644 index 00000000..2c9b99d8 --- /dev/null +++ b/packages/build-tools/src/ios/__tests__/expoUpdates.test.ts @@ -0,0 +1,136 @@ +import path from 'path'; + +import fs from 'fs-extra'; +import plist from '@expo/plist'; +import { IOSConfig } from '@expo/config-plugins'; + +import { + iosGetNativelyDefinedClassicReleaseChannelAsync, + IosMetadataName, + iosSetChannelNativelyAsync, + iosSetClassicReleaseChannelNativelyAsync, +} from '../../ios/expoUpdates'; + +jest.mock('fs'); + +const noItemsExpoPlist = ` + + + + + +`; +const channel = 'main'; + +describe(iosSetClassicReleaseChannelNativelyAsync, () => { + test('sets the release channel', async () => { + const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-'); + fs.ensureDirSync(reactNativeProjectDirectory); + const releaseChannel = 'default'; + const ctx = { + reactNativeProjectDirectory, + job: { releaseChannel }, + logger: { info: () => {} }, + }; + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/test/')); + fs.writeFileSync( + path.join(reactNativeProjectDirectory, '/ios/test/AppDelegate.m'), + Buffer.from('placeholder') + ); + + const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory); + const expoPlistDirectory = path.dirname(expoPlistPath); + + fs.ensureDirSync(expoPlistDirectory); + fs.writeFileSync(expoPlistPath, noItemsExpoPlist); + + await iosSetClassicReleaseChannelNativelyAsync(ctx as any); + + const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8'); + expect(plist.parse(newExpoPlist)[IosMetadataName.RELEASE_CHANNEL]).toEqual(releaseChannel); + }); +}); + +describe(iosSetChannelNativelyAsync, () => { + it('sets the channel', async () => { + const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-'); + fs.ensureDirSync(reactNativeProjectDirectory); + const ctx = { + reactNativeProjectDirectory, + job: { updates: { channel } }, + logger: { info: () => {} }, + }; + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/')); + fs.writeFileSync( + path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/project.pbxproj'), + Buffer.from('placeholder') + ); + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/test/')); + fs.writeFileSync( + path.join(reactNativeProjectDirectory, '/ios/test/AppDelegate.m'), + Buffer.from('placeholder') + ); + + const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory); + const expoPlistDirectory = path.dirname(expoPlistPath); + + fs.ensureDirSync(expoPlistDirectory); + fs.writeFileSync(expoPlistPath, noItemsExpoPlist); + + await iosSetChannelNativelyAsync(ctx as any); + + const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8'); + expect( + plist.parse(newExpoPlist)[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] + ).toEqual({ 'expo-channel-name': channel }); + }); +}); + +describe(iosGetNativelyDefinedClassicReleaseChannelAsync, () => { + it('gets the natively defined release channel', async () => { + const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-'); + fs.ensureDirSync(reactNativeProjectDirectory); + const releaseChannel = 'default'; + const ctx = { + reactNativeProjectDirectory, + logger: { info: () => {} }, + }; + + const releaseChannelInPlist = ` + + + + + ${IosMetadataName.RELEASE_CHANNEL} + ${releaseChannel} + + `; + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/')); + fs.writeFileSync( + path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/project.pbxproj'), + Buffer.from('placeholder') + ); + + fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/test/')); + fs.writeFileSync( + path.join(reactNativeProjectDirectory, '/ios/test/AppDelegate.m'), + Buffer.from('placeholder') + ); + + const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory); + const expoPlistDirectory = path.dirname(expoPlistPath); + + fs.ensureDirSync(expoPlistDirectory); + fs.writeFileSync(expoPlistPath, releaseChannelInPlist); + + const nativelyDefinedReleaseChannel = await iosGetNativelyDefinedClassicReleaseChannelAsync( + ctx as any + ); + + expect(nativelyDefinedReleaseChannel).toBe(releaseChannel); + }); +}); diff --git a/packages/build-tools/src/ios/expoUpdates.ts b/packages/build-tools/src/ios/expoUpdates.ts new file mode 100644 index 00000000..ace05019 --- /dev/null +++ b/packages/build-tools/src/ios/expoUpdates.ts @@ -0,0 +1,70 @@ +import assert from 'assert'; + +import { IOSConfig } from '@expo/config-plugins'; +import fs from 'fs-extra'; +import plist from '@expo/plist'; +import { Job } from '@expo/eas-build-job'; + +import { BuildContext } from '../context'; + +export enum IosMetadataName { + UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'EXUpdatesRequestHeaders', + RELEASE_CHANNEL = 'EXUpdatesReleaseChannel', +} + +export async function iosSetChannelNativelyAsync(ctx: BuildContext): Promise { + assert(ctx.job.updates?.channel, 'updates.channel must be defined'); + + const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory); + + if (!(await fs.pathExists(expoPlistPath))) { + throw new Error(`${expoPlistPath} does no exist`); + } + + const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8'); + const items: Record> = plist.parse(expoPlistContent); + items[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] = { + ...((items[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] as Record< + string, + string + >) ?? {}), + 'expo-channel-name': ctx.job.updates.channel, + }; + const expoPlist = plist.build(items); + + await fs.writeFile(expoPlistPath, expoPlist); +} + +export async function iosSetClassicReleaseChannelNativelyAsync( + ctx: BuildContext +): Promise { + assert(ctx.job.releaseChannel, 'releaseChannel must be defined'); + + const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory); + + if (!(await fs.pathExists(expoPlistPath))) { + throw new Error(`${expoPlistPath} does not exist`); + } + + const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8'); + const items: Record> = plist.parse(expoPlistContent); + items[IosMetadataName.RELEASE_CHANNEL] = ctx.job.releaseChannel; + const expoPlist = plist.build(items); + + await fs.writeFile(expoPlistPath, expoPlist); +} + +export async function iosGetNativelyDefinedClassicReleaseChannelAsync( + ctx: BuildContext +): Promise { + const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory); + if (!(await fs.pathExists(expoPlistPath))) { + return null; + } + const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8'); + const parsedPlist = plist.parse(expoPlistContent); + if (!parsedPlist) { + return null; + } + return parsedPlist[IosMetadataName.RELEASE_CHANNEL] ?? null; +} diff --git a/packages/build-tools/src/ios/releaseChannel.ts b/packages/build-tools/src/ios/releaseChannel.ts deleted file mode 100644 index 2f5cda2d..00000000 --- a/packages/build-tools/src/ios/releaseChannel.ts +++ /dev/null @@ -1,71 +0,0 @@ -import path from 'path'; - -import plist from '@expo/plist'; -import fg from 'fast-glob'; -import fs from 'fs-extra'; - -async function updateReleaseChannel( - reactNativeProjectDirectory: string, - releaseChannel: string -): Promise { - const pbxprojPaths = await fg('ios/*/project.pbxproj', { cwd: reactNativeProjectDirectory }); - - const pbxprojPath = pbxprojPaths.length > 0 ? pbxprojPaths[0] : undefined; - - if (!pbxprojPath) { - throw new Error(`Couldn't find an iOS project at '${reactNativeProjectDirectory}'`); - } - - const xcodeprojPath = path.resolve(pbxprojPath, '..'); - const expoPlistPath = path.resolve( - reactNativeProjectDirectory, - 'ios', - path.basename(xcodeprojPath).replace(/\.xcodeproj$/, ''), - 'Supporting', - 'Expo.plist' - ); - - let items: Record = {}; - - if (await fs.pathExists(expoPlistPath)) { - const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8'); - items = plist.parse(expoPlistContent); - } - - items.EXUpdatesReleaseChannel = releaseChannel; - - const expoPlist = plist.build(items); - - if (!(await fs.pathExists(path.dirname(expoPlistPath)))) { - await fs.mkdirp(path.dirname(expoPlistPath)); - } - - await fs.writeFile(expoPlistPath, expoPlist); -} - -async function getReleaseChannel(reactNativeProjectDirectory: string): Promise { - const pbxprojPaths = await fg('ios/*/project.pbxproj', { cwd: reactNativeProjectDirectory }); - - const pbxprojPath = pbxprojPaths.length > 0 ? pbxprojPaths[0] : undefined; - - if (!pbxprojPath) { - throw new Error(`Couldn't find an iOS project at '${reactNativeProjectDirectory}'`); - } - - const xcodeprojPath = path.resolve(pbxprojPath, '..'); - const expoPlistPath = path.resolve( - reactNativeProjectDirectory, - 'ios', - path.basename(xcodeprojPath).replace(/\.xcodeproj$/, ''), - 'Supporting', - 'Expo.plist' - ); - - if (!(await fs.pathExists(expoPlistPath))) { - return; - } - const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8'); - return plist.parse(expoPlistContent)?.EXUpdatesReleaseChannel; -} - -export { updateReleaseChannel, getReleaseChannel }; diff --git a/packages/build-tools/src/managed/expoUpdates.ts b/packages/build-tools/src/managed/expoUpdates.ts deleted file mode 100644 index c6f36349..00000000 --- a/packages/build-tools/src/managed/expoUpdates.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Platform } from '@expo/eas-build-job'; - -import isExpoUpdatesInstalledAsync from '../utils/isExpoUpdatesInstalled'; - -import { ManagedBuildContext, ManagedJob } from './context'; - -export async function configureExpoUpdatesIfInstalled( - ctx: ManagedBuildContext, - updateReleaseChannel: (dir: string, releaseChannel: string) => Promise -): Promise { - if (await isExpoUpdatesInstalledAsync(ctx.reactNativeProjectDirectory)) { - if (ctx.job.releaseChannel) { - const configFile = - ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist'; - ctx.logger.info( - `Setting the release channel in '${configFile}' to '${ctx.job.releaseChannel}'` - ); - await updateReleaseChannel(ctx.reactNativeProjectDirectory, ctx.job.releaseChannel); - } else { - ctx.logger.info(`Using default release channel for 'expo-updates' (default)`); - } - } -} diff --git a/packages/build-tools/src/utils/__tests__/expoUpdates.test.ts b/packages/build-tools/src/utils/__tests__/expoUpdates.test.ts new file mode 100644 index 00000000..c8f6cb50 --- /dev/null +++ b/packages/build-tools/src/utils/__tests__/expoUpdates.test.ts @@ -0,0 +1,104 @@ +import { Platform } from '@expo/eas-build-job'; + +import { ManagedBuildContext, ManagedJob } from '../../managed/context'; +import * as expoUpdates from '../expoUpdates'; +import isExpoUpdatesInstalledAsync from '../isExpoUpdatesInstalled'; + +jest.mock('../isExpoUpdatesInstalled', () => jest.fn()); +jest.mock('fs'); + +describe(expoUpdates.configureExpoUpdatesIfInstalledAsync, () => { + beforeAll(() => { + jest.restoreAllMocks(); + }); + it('aborts if expo-updates is not installed', async () => { + (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(false); + jest.spyOn(expoUpdates, 'configureEASExpoUpdatesAsync'); + jest.spyOn(expoUpdates, 'configureClassicExpoUpdatesAsync'); + + await expoUpdates.configureExpoUpdatesIfInstalledAsync({ + job: { Platform: Platform.IOS }, + } as any); + + expect(expoUpdates.configureEASExpoUpdatesAsync).not.toBeCalled(); + expect(expoUpdates.configureClassicExpoUpdatesAsync).not.toBeCalled(); + expect(isExpoUpdatesInstalledAsync).toHaveBeenCalledTimes(1); + }); + + it('configures for EAS if the updates.channel field is set', async () => { + (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true); + jest.spyOn(expoUpdates, 'configureEASExpoUpdatesAsync').mockImplementation(); + jest.spyOn(expoUpdates, 'configureClassicExpoUpdatesAsync'); + + const managedCtx: ManagedBuildContext = { + job: { updates: { channel: 'main' }, Platform: Platform.IOS }, + } as any; + await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx); + + expect(expoUpdates.configureEASExpoUpdatesAsync).toBeCalledTimes(1); + expect(expoUpdates.configureClassicExpoUpdatesAsync).not.toBeCalled(); + expect(isExpoUpdatesInstalledAsync).toHaveBeenCalledTimes(1); + }); + + it('configures for classic updates if the updates.channel field is not set', async () => { + (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true); + jest.spyOn(expoUpdates, 'configureEASExpoUpdatesAsync'); + jest.spyOn(expoUpdates, 'configureClassicExpoUpdatesAsync').mockImplementation(); + + const managedCtx: ManagedBuildContext = { + job: { platform: Platform.IOS }, + logger: { info: () => {} }, + } as any; + await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx); + + expect(expoUpdates.configureEASExpoUpdatesAsync).not.toBeCalled(); + expect(expoUpdates.configureClassicExpoUpdatesAsync).toBeCalledTimes(1); + expect(isExpoUpdatesInstalledAsync).toHaveBeenCalledTimes(1); + }); +}); + +describe(expoUpdates.configureClassicExpoUpdatesAsync, () => { + beforeAll(() => { + jest.restoreAllMocks(); + }); + it('sets the release channel if it is supplied in ctx.job.releaseChannel', async () => { + (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true); + jest.spyOn(expoUpdates, 'setClassicReleaseChannelNativelyAsync').mockImplementation(); + + const managedCtx: ManagedBuildContext = { + job: { releaseChannel: 'default', platform: Platform.IOS }, + } as any; + await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx); + + expect(expoUpdates.setClassicReleaseChannelNativelyAsync).toBeCalledTimes(1); + }); + it('searches for the natively defined releaseChannel if it is not supplied by ctx.job.releaseChannel', async () => { + (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true); + jest.spyOn(expoUpdates, 'getNativelyDefinedClassicReleaseChannelAsync').mockImplementation(); + + const managedCtx: ManagedBuildContext = { + job: { platform: Platform.IOS }, + logger: { info: () => {}, warn: () => {} }, + } as any; + await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx); + + expect(expoUpdates.getNativelyDefinedClassicReleaseChannelAsync).toBeCalledTimes(1); + }); + it('uses the default release channel if the releaseChannel is not defined in ctx.job.releaseChannel nor natively.', async () => { + (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true); + jest + .spyOn(expoUpdates, 'getNativelyDefinedClassicReleaseChannelAsync') + .mockImplementation(async () => { + return null; + }); + + const infoLogger = jest.fn(); + const managedCtx: ManagedBuildContext = { + job: { platform: Platform.IOS }, + logger: { info: infoLogger }, + } as any; + await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx); + + expect(infoLogger).toBeCalledWith(`Using default release channel for 'expo-updates' (default)`); + }); +}); diff --git a/packages/build-tools/src/utils/expoUpdates.ts b/packages/build-tools/src/utils/expoUpdates.ts new file mode 100644 index 00000000..60a50441 --- /dev/null +++ b/packages/build-tools/src/utils/expoUpdates.ts @@ -0,0 +1,136 @@ +import assert from 'assert'; + +import { Ios, Android, Platform, Job } from '@expo/eas-build-job'; + +import { + androidSetChannelNativelyAsync, + androidSetClassicReleaseChannelNativelyAsync, + androidGetNativelyDefinedClassicReleaseChannelAsync, +} from '../android/expoUpdates'; +import { + iosSetChannelNativelyAsync, + iosSetClassicReleaseChannelNativelyAsync, + iosGetNativelyDefinedClassicReleaseChannelAsync, +} from '../ios/expoUpdates'; +import { BuildContext } from '../context'; + +import isExpoUpdatesInstalledAsync from './isExpoUpdatesInstalled'; +export type GenericJob = Ios.GenericJob | Android.GenericJob; + +/** + * Used for when Expo Updates is pointed at an EAS server. + * @param ctx + * @param platform + */ +export const setChannelNativelyAsync = async (ctx: BuildContext): Promise => { + assert(ctx.job.updates?.channel, 'updates.channel must be defined'); + const newUpdateRequestHeaders: Record = { + 'expo-channel-name': ctx.job.updates.channel, + }; + + const configFile = ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist'; + ctx.logger.info( + `Setting the update response headers in '${configFile}' to '${JSON.stringify( + newUpdateRequestHeaders + )}'` + ); + + switch (ctx.job.platform) { + case Platform.ANDROID: { + await androidSetChannelNativelyAsync(ctx); + return; + } + case Platform.IOS: { + await iosSetChannelNativelyAsync(ctx); + return; + } + default: + throw new Error(`Platform is not supported.`); + } +}; + +/** + * Used for classic Expo Updates + * @param ctx + * @param platform + */ +export const setClassicReleaseChannelNativelyAsync = async ( + ctx: BuildContext +): Promise => { + assert(ctx.job.releaseChannel, 'releaseChannel must be defined'); + + const configFile = ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist'; + ctx.logger.info(`Setting the release channel in '${configFile}' to '${ctx.job.releaseChannel}'`); + + switch (ctx.job.platform) { + case Platform.ANDROID: { + await androidSetClassicReleaseChannelNativelyAsync(ctx); + return; + } + case Platform.IOS: { + await iosSetClassicReleaseChannelNativelyAsync(ctx); + return; + } + default: + throw new Error(`Platform is not supported.`); + } +}; + +/** + * Used for classic Expo Updates + * @param ctx + * @param platform + */ +export const getNativelyDefinedClassicReleaseChannelAsync = async ( + ctx: BuildContext +): Promise => { + switch (ctx.job.platform) { + case Platform.ANDROID: { + return androidGetNativelyDefinedClassicReleaseChannelAsync(ctx); + } + case Platform.IOS: { + return iosGetNativelyDefinedClassicReleaseChannelAsync(ctx); + } + default: + throw new Error(`Platform is not supported.`); + } +}; + +export const configureClassicExpoUpdatesAsync = async (ctx: BuildContext): Promise => { + if (ctx.job.releaseChannel) { + await setClassicReleaseChannelNativelyAsync(ctx); + } else { + /** + * If releaseChannel is not defined: + * 1. Try to infer it from the native value. + * 2. If it is not set, fallback to 'default'. + */ + const releaseChannel = await getNativelyDefinedClassicReleaseChannelAsync(ctx); + if (releaseChannel) { + ctx.logger.info( + `Using the release channel pre-configured in native project (${releaseChannel})` + ); + ctx.logger.warn('Please add the "releaseChannel" field to your build profile (eas.json)'); + } else { + ctx.logger.info(`Using default release channel for 'expo-updates' (default)`); + } + } +}; + +export const configureEASExpoUpdatesAsync = async (ctx: BuildContext): Promise => { + await setChannelNativelyAsync(ctx); +}; + +export const configureExpoUpdatesIfInstalledAsync = async ( + ctx: BuildContext +): Promise => { + if (!(await isExpoUpdatesInstalledAsync(ctx.reactNativeProjectDirectory))) { + return; + } + + if (ctx.job.updates?.channel) { + await configureEASExpoUpdatesAsync(ctx); + } else { + await configureClassicExpoUpdatesAsync(ctx); + } +}; diff --git a/packages/eas-build-job/src/__tests__/android.test.ts b/packages/eas-build-job/src/__tests__/android.test.ts index a3c2af71..b54ffc0e 100644 --- a/packages/eas-build-job/src/__tests__/android.test.ts +++ b/packages/eas-build-job/src/__tests__/android.test.ts @@ -123,4 +123,44 @@ describe('Android.ManagedJobSchema', () => { ); expect(value).not.toMatchObject(managedJob); }); + test('validates channel', () => { + const managedJob = { + secrets, + type: Workflow.MANAGED, + platform: Platform.ANDROID, + updates: { + channel: 'main', + }, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'http://localhost:3000', + }, + projectRootDirectory: '.', + }; + + const { value, error } = Android.ManagedJobSchema.validate(managedJob, joiOptions); + expect(value).toMatchObject(managedJob); + expect(error).toBeFalsy(); + }); + test('fails when both releaseChannel and updates.channel are defined', () => { + const managedJob = { + secrets, + type: Workflow.MANAGED, + platform: Platform.ANDROID, + releaseChannel: 'default', + updates: { + channel: 'main', + }, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'http://localhost:3000', + }, + projectRootDirectory: '.', + }; + + const { error } = Android.ManagedJobSchema.validate(managedJob, joiOptions); + expect(error?.message).toBe( + '"value" contains a conflict between optional exclusive peers [releaseChannel, updates.channel]' + ); + }); }); diff --git a/packages/eas-build-job/src/__tests__/ios.test.ts b/packages/eas-build-job/src/__tests__/ios.test.ts index 6e9262c4..1cf14cd1 100644 --- a/packages/eas-build-job/src/__tests__/ios.test.ts +++ b/packages/eas-build-job/src/__tests__/ios.test.ts @@ -132,4 +132,50 @@ describe('Ios.ManagedJobSchema', () => { ); expect(value).not.toMatchObject(managedJob); }); + test('validates channel', () => { + const managedJob = { + secrets: { + buildCredentials, + }, + type: Workflow.MANAGED, + platform: Platform.IOS, + updates: { + channel: 'main', + }, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'http://localhost:3000', + }, + projectRootDirectory: '.', + distribution: 'store', + }; + + const { value, error } = Ios.ManagedJobSchema.validate(managedJob, joiOptions); + expect(value).toMatchObject(managedJob); + expect(error).toBeFalsy(); + }); + test('fails when both releaseChannel and updates.channel are defined', () => { + const managedJob = { + secrets: { + buildCredentials, + }, + type: Workflow.MANAGED, + platform: Platform.IOS, + releaseChannel: 'default', + updates: { + channel: 'main', + }, + projectArchive: { + type: ArchiveSourceType.URL, + url: 'http://localhost:3000', + }, + projectRootDirectory: '.', + distribution: 'store', + }; + + const { error } = Ios.ManagedJobSchema.validate(managedJob, joiOptions); + expect(error?.message).toBe( + '"value" contains a conflict between optional exclusive peers [releaseChannel, updates.channel]' + ); + }); }); diff --git a/packages/eas-build-job/src/android.ts b/packages/eas-build-job/src/android.ts index b6bf7e6f..fe4eec40 100644 --- a/packages/eas-build-job/src/android.ts +++ b/packages/eas-build-job/src/android.ts @@ -57,6 +57,9 @@ interface BaseJob { platform: Platform.ANDROID; projectRootDirectory: string; releaseChannel?: string; + updates?: { + channel?: string; + }; secrets: { buildCredentials?: { keystore: Keystore; @@ -72,13 +75,16 @@ const BaseJobSchema = Joi.object({ platform: Joi.string().valid(Platform.ANDROID).required(), projectRootDirectory: Joi.string().required(), releaseChannel: Joi.string(), + updates: Joi.object({ + channel: Joi.string(), + }), secrets: Joi.object({ buildCredentials: Joi.object({ keystore: KeystoreSchema.required() }), environmentSecrets: EnvSchema, }).required(), builderEnvironment: BuilderEnvironmentSchema, cache: CacheSchema.default(), -}); +}).oxor('releaseChannel', 'updates.channel'); export interface GenericJob extends BaseJob { type: Workflow.GENERIC; diff --git a/packages/eas-build-job/src/ios.ts b/packages/eas-build-job/src/ios.ts index 516414fd..0031bb90 100644 --- a/packages/eas-build-job/src/ios.ts +++ b/packages/eas-build-job/src/ios.ts @@ -76,6 +76,9 @@ interface BaseJob { platform: Platform.IOS; projectRootDirectory: string; releaseChannel?: string; + updates?: { + channel?: string; + }; distribution?: DistributionType; secrets: { buildCredentials?: BuildCredentials; @@ -85,19 +88,24 @@ interface BaseJob { cache: Cache; } -const BaseJobSchema = Joi.object().keys({ - projectArchive: ArchiveSourceSchema.required(), - platform: Joi.string().valid(Platform.IOS).required(), - projectRootDirectory: Joi.string().required(), - releaseChannel: Joi.string(), - distribution: Joi.string().valid('store', 'internal', 'simulator'), - secrets: Joi.object({ - buildCredentials: BuildCredentialsSchema, - environmentSecrets: EnvSchema, - }).required(), - builderEnvironment: BuilderEnvironmentSchema, - cache: CacheSchema.default(), -}); +const BaseJobSchema = Joi.object() + .keys({ + projectArchive: ArchiveSourceSchema.required(), + platform: Joi.string().valid(Platform.IOS).required(), + projectRootDirectory: Joi.string().required(), + releaseChannel: Joi.string(), + updates: Joi.object({ + channel: Joi.string(), + }), + distribution: Joi.string().valid('store', 'internal', 'simulator'), + secrets: Joi.object({ + buildCredentials: BuildCredentialsSchema, + environmentSecrets: EnvSchema, + }).required(), + builderEnvironment: BuilderEnvironmentSchema, + cache: CacheSchema.default(), + }) + .oxor('releaseChannel', 'updates.channel'); export interface GenericJob extends BaseJob { type: Workflow.GENERIC; diff --git a/packages/eas-build-job/src/metadata.ts b/packages/eas-build-job/src/metadata.ts index 27e5664f..0f56b0b4 100644 --- a/packages/eas-build-job/src/metadata.ts +++ b/packages/eas-build-job/src/metadata.ts @@ -50,11 +50,17 @@ export type Metadata = { sdkVersion?: string; /** - * Release channel (for expo-updates) - * It's undefined if the expo-updates package is not installed for the project. + * Release channel (for classic expo-updates) + * It's undefined if the classic expo-updates package is not installed for the project. */ releaseChannel?: string; + /** + * Channel (for Expo Updates when it is configured for for use with EAS) + * It's undefined if the expo-updates package is not configured for use with EAS. + */ + channel?: string; + /** * Distribution type * Indicates whether this is a build for store, internal distribution, or simulator (iOS). @@ -99,6 +105,7 @@ export const MetadataSchema = Joi.object({ credentialsSource: Joi.string().valid('local', 'remote'), sdkVersion: Joi.string(), releaseChannel: Joi.string(), + channel: Joi.string(), appName: Joi.string(), appIdentifier: Joi.string(), buildProfile: Joi.string(), diff --git a/yarn.lock b/yarn.lock index 679caf74..18a16598 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15225,7 +15225,7 @@ uuid@^2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0: +uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==