mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
329ddccd61 | ||
|
|
1db7281e8b | ||
|
|
1e8a0acabe | ||
|
|
6bfb8af57f | ||
|
|
231f29ecf1 |
No files matched your search
+2
-5
@@ -8,19 +8,18 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add screenshots and previews support to `metadata:push` and `metadata:pull`. ([#3301](https://github.com/expo/eas-cli/pull/3301) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Auto-generate `ios.bundleIdentifier` and `android.package` in non-interactive mode when not set in app config. ([#3399](https://github.com/expo/eas-cli/pull/3399) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
- [eas-cli] Add `--non-interactive` flag to `metadata:push` and `metadata:pull` commands with ASC API Key auth support. ([#3548](https://github.com/expo/eas-cli/pull/3548) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Fix workflow:logs for builds using built-in EAS build steps. ([#3523](https://github.com/expo/eas-cli/pull/3523) by [@douglowder](https://github.com/douglowder))
|
||||
- [build-tools][worker] Read Expo app config with `expo config` CLI invocation before falling back to `@expo/config` ([#3536](https://github.com/expo/eas-cli/pull/3536) by [@kitten](https://github.com/kitten))
|
||||
- Fix workflow detection for tracked `.gitignore`d native projects when `requireCommit` is disabled. ([#3561](https://github.com/expo/eas-cli/pull/3561) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Bump apple-utils. ([#3545](https://github.com/expo/eas-cli/pull/3545) by [@quinlanj](https://github.com/quinlanj))
|
||||
- [eas-cli][build-tools][worker] Bump `@expo/config` and `@expo/config-plugins`. ([#3527](https://github.com/expo/eas-cli/pull/3527) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [18.4.0](https://github.com/expo/eas-cli/releases/tag/v18.4.0) - 2026-03-16
|
||||
|
||||
@@ -65,8 +64,6 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
- Remove --environment flag requirement for update:configure. ([#3440](https://github.com/expo/eas-cli/pull/3440) by [@douglowder](https://github.com/douglowder))
|
||||
- Fix login spinner interfering with prompts in `eas go` command when not logged in. ([#3451](https://github.com/expo/eas-cli/pull/3451) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [18.0.6](https://github.com/expo/eas-cli/releases/tag/v18.0.6) - 2026-02-27
|
||||
|
||||
## [18.0.5](https://github.com/expo/eas-cli/releases/tag/v18.0.5) - 2026-02-25
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"test": "yarn jest-unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.16",
|
||||
"@expo/apple-utils": "2.1.15",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/eas-json": "18.4.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
|
||||
@@ -315,42 +315,6 @@
|
||||
"liveEdits": true,
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"screenshots": {
|
||||
"type": "object",
|
||||
"description": "Screenshots for this locale, organized by display type (e.g., APP_IPHONE_67, APP_IPAD_PRO_129)",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"previews": {
|
||||
"type": "object",
|
||||
"description": "Video previews for this locale, organized by display type (e.g., IPHONE_67, IPAD_PRO_129)",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Video file path (relative to project root)"
|
||||
},
|
||||
"previewFrameTimeCode": {
|
||||
"type": "string",
|
||||
"description": "Optional preview frame time code (e.g., '00:05:00' for 5 seconds)"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@ async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): P
|
||||
},
|
||||
{
|
||||
configPath: 'exp.notification.icon',
|
||||
pngPath: (ctx.exp as Record<string, any>).notification?.icon,
|
||||
pngPath: ctx.exp.notification?.icon,
|
||||
},
|
||||
];
|
||||
await validatePNGsAsync(pngs);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import uniq from '../../../utils/expodash/uniq';
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { removeDatePrecision } from '../../utils/date';
|
||||
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';
|
||||
import { AppleMetadata } from '../types';
|
||||
|
||||
type PartialExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
|
||||
|
||||
@@ -211,14 +211,4 @@ export class AppleConfigReader {
|
||||
// TODO: add attachment
|
||||
};
|
||||
}
|
||||
|
||||
/** Get screenshots configuration for a specific locale */
|
||||
public getScreenshots(locale: string): AppleScreenshots | null {
|
||||
return this.schema.info?.[locale]?.screenshots ?? null;
|
||||
}
|
||||
|
||||
/** Get video previews configuration for a specific locale */
|
||||
public getPreviews(locale: string): ApplePreviews | null {
|
||||
return this.schema.info?.[locale]?.previews ?? null;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { AppleMetadata, ApplePreviews, AppleScreenshots } from '../types';
|
||||
import { AppleMetadata } from '../types';
|
||||
|
||||
/**
|
||||
* Serializes the Apple ASC entities into the metadata configuration schema.
|
||||
@@ -186,21 +186,6 @@ export class AppleConfigWriter {
|
||||
// TODO: add attachment
|
||||
};
|
||||
}
|
||||
|
||||
/** Set screenshots for a specific locale */
|
||||
public setScreenshots(locale: string, screenshots: AppleScreenshots): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
|
||||
this.schema.info[locale].screenshots =
|
||||
Object.keys(screenshots).length > 0 ? screenshots : undefined;
|
||||
}
|
||||
|
||||
/** Set video previews for a specific locale */
|
||||
public setPreviews(locale: string, previews: ApplePreviews): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
this.schema.info[locale] = this.schema.info[locale] ?? { title: '' };
|
||||
this.schema.info[locale].previews = Object.keys(previews).length > 0 ? previews : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to convert `T | null` to `T | undefined`, required for the entity properties */
|
||||
|
||||
@@ -4,25 +4,16 @@ import type { AgeRatingData } from './tasks/age-rating';
|
||||
import type { AppInfoData } from './tasks/app-info';
|
||||
import type { AppReviewData } from './tasks/app-review-detail';
|
||||
import type { AppVersionData } from './tasks/app-version';
|
||||
import type { PreviewsData } from './tasks/previews';
|
||||
import type { ScreenshotsData } from './tasks/screenshots';
|
||||
|
||||
/**
|
||||
* The fully prepared apple data, used within the `downloadAsync` or `uploadAsync` tasks.
|
||||
* It contains references to each individual models, to either upload or download App Store data.
|
||||
*/
|
||||
export type AppleData = { app: App; projectDir: string } & AppInfoData &
|
||||
AppVersionData &
|
||||
AgeRatingData &
|
||||
AppReviewData &
|
||||
ScreenshotsData &
|
||||
PreviewsData;
|
||||
export type AppleData = { app: App } & AppInfoData & AppVersionData & AgeRatingData & AppReviewData;
|
||||
|
||||
/**
|
||||
* The unprepared partial apple data, used within the `prepareAsync` tasks.
|
||||
* It contains a reference to the app, each task should populate the necessary data.
|
||||
* If an entity fails to prepare the data, individual tasks should raise errors about the missing data.
|
||||
*/
|
||||
export type PartialAppleData = { app: App; projectDir: string } & Partial<
|
||||
Omit<AppleData, 'app' | 'projectDir'>
|
||||
>;
|
||||
export type PartialAppleData = { app: App } & Partial<Omit<AppleData, 'app'>>;
|
||||
@@ -1,14 +1,8 @@
|
||||
import { truthy } from '../../../utils/expodash/filter';
|
||||
import { IssueRule } from '../../config/issue';
|
||||
import { AppleInfo } from '../types';
|
||||
|
||||
/** Only check text properties that may contain restricted words */
|
||||
type AppleInfoTextProperty = 'title' | 'subtitle' | 'description' | 'keywords';
|
||||
const RESTRICTED_PROPERTIES: AppleInfoTextProperty[] = [
|
||||
'title',
|
||||
'subtitle',
|
||||
'description',
|
||||
'keywords',
|
||||
];
|
||||
const RESTRICTED_PROPERTIES: (keyof AppleInfo)[] = ['title', 'subtitle', 'description', 'keywords'];
|
||||
const RESTRICTED_WORDS = {
|
||||
beta: 'Apple restricts the word "beta" and synonyms implying incomplete functionality.',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AgeRatingDeclaration, AppInfo, Rating } from '@expo/apple-utils';
|
||||
import { AgeRatingDeclaration, AppStoreVersion, Rating } from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
@@ -12,13 +12,13 @@ jest.mock('../../config/writer');
|
||||
|
||||
describe(AgeRatingTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('loads age rating from app info', async () => {
|
||||
it('loads age rating from app version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppInfo.type}/stub-id/ageRatingDeclaration`)
|
||||
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`)
|
||||
.reply(200, require('./fixtures/appStoreVersions/get-ageRatingDeclaration-200.json'));
|
||||
|
||||
const context: any = {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AgeRatingTask().prepareAsync({ context });
|
||||
@@ -26,16 +26,6 @@ describe(AgeRatingTask, () => {
|
||||
expect(context.ageRating).toBeInstanceOf(AgeRatingDeclaration);
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('skips when info is not available', async () => {
|
||||
const context: any = {
|
||||
info: undefined,
|
||||
};
|
||||
|
||||
await new AgeRatingTask().prepareAsync({ context });
|
||||
|
||||
expect(context.ageRating).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
@@ -64,13 +54,13 @@ describe(AgeRatingTask, () => {
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when age rating is not loaded', async () => {
|
||||
await expect(
|
||||
new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({}),
|
||||
context: { ageRating: undefined } as any,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
it('aborts when age rating is not loaded', async () => {
|
||||
const promise = new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({}),
|
||||
context: { ageRating: undefined } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('rating not initialized');
|
||||
});
|
||||
|
||||
it('skips updating age rating when not configured', async () => {
|
||||
|
||||
@@ -23,7 +23,6 @@ describe(AppInfoTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppInfoTask().prepareAsync({ context });
|
||||
|
||||
@@ -72,20 +72,20 @@ describe(AppReviewDetailTask, () => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('skips when version is not loaded', async () => {
|
||||
await expect(
|
||||
new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: '[email protected]',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {} as any,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: 'review@example.com',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {} as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('version not init');
|
||||
});
|
||||
|
||||
it('updates review details when loaded', async () => {
|
||||
|
||||
@@ -44,7 +44,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
|
||||
@@ -84,7 +83,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
|
||||
@@ -120,7 +118,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask().prepareAsync({ context });
|
||||
@@ -156,7 +153,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask().prepareAsync({ context });
|
||||
@@ -198,7 +194,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ version: '2.0' }).prepareAsync({ context });
|
||||
@@ -248,7 +243,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
};
|
||||
|
||||
await new AppVersionTask({ version: '3.0' }).prepareAsync({ context });
|
||||
@@ -377,7 +371,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
};
|
||||
|
||||
@@ -406,7 +399,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
versionPhasedRelease: null, // Not enabled yet
|
||||
};
|
||||
@@ -425,7 +417,6 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
// Enabled, and not completed yet
|
||||
versionPhasedRelease: new AppStoreVersionPhasedRelease(
|
||||
@@ -449,9 +440,8 @@ describe(AppVersionTask, () => {
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
projectDir: '/test/project',
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
// Enabled, and completed
|
||||
// Enabled, and not completed yet
|
||||
versionPhasedRelease: new AppStoreVersionPhasedRelease(
|
||||
requestContext,
|
||||
'APP_STORE_VERSION_PHASED_RELEASE_1',
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
import {
|
||||
AppPreview,
|
||||
AppPreviewSet,
|
||||
AppStoreVersionLocalization,
|
||||
PreviewType,
|
||||
} from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { PartialAppleData } from '../../data';
|
||||
import { PreviewsTask } from '../previews';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
const mockFetch = jest.fn();
|
||||
jest.mock('../../../../fetch', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: any[]) => mockFetch(...args),
|
||||
}));
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
jest.spyOn(fs, 'statSync').mockReturnValue({ size: 10240 } as any);
|
||||
jest.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined);
|
||||
jest.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
describe(PreviewsTask, () => {
|
||||
beforeEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('prepareAsync', () => {
|
||||
it('initializes empty preview sets when no locales are available', async () => {
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: undefined,
|
||||
};
|
||||
|
||||
await new PreviewsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.previewSets).toBeDefined();
|
||||
expect(context.previewSets!.size).toBe(0);
|
||||
});
|
||||
|
||||
it('fetches preview sets for each locale', async () => {
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersionLocalization.type}/LOC_1/${AppPreviewSet.type}`)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: 'PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: [locale],
|
||||
};
|
||||
|
||||
await new PreviewsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.previewSets).toBeDefined();
|
||||
expect(context.previewSets!.size).toBe(1);
|
||||
expect(context.previewSets!.get('en-US')).toBeDefined();
|
||||
expect(context.previewSets!.get('en-US')!.has(PreviewType.IPHONE_67)).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('skips when preview sets are not prepared', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { previewSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('skips locales with no preview sets', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('downloads previews and sets config with simple path', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const preview = new AppPreview(requestContext, 'PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
videoUrl: 'https://example.com/demo.mp4',
|
||||
assetDeliveryState: {
|
||||
state: 'COMPLETE',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(preview, 'getVideoUrl').mockReturnValue('https://example.com/demo.mp4');
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
const previewSet = new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [preview],
|
||||
} as any);
|
||||
previewTypeMap.set(PreviewType.IPHONE_67, previewSet);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', previewTypeMap);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-video-data')),
|
||||
});
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).toBeCalledWith('en-US', {
|
||||
[PreviewType.IPHONE_67]: 'store/apple/preview/en-US/IPHONE_67/demo.mp4',
|
||||
});
|
||||
});
|
||||
|
||||
it('downloads previews with previewFrameTimeCode as object config', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const preview = new AppPreview(requestContext, 'PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
videoUrl: 'https://example.com/demo.mp4',
|
||||
assetDeliveryState: {
|
||||
state: 'COMPLETE',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.spyOn(preview, 'getVideoUrl').mockReturnValue('https://example.com/demo.mp4');
|
||||
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
const previewSet = new AppPreviewSet(requestContext, 'PSET_1', {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [preview],
|
||||
} as any);
|
||||
previewTypeMap.set(PreviewType.IPHONE_67, previewSet);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', previewTypeMap);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-video-data')),
|
||||
});
|
||||
|
||||
await new PreviewsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setPreviews).toBeCalledWith('en-US', {
|
||||
[PreviewType.IPHONE_67]: {
|
||||
path: 'store/apple/preview/en-US/IPHONE_67/demo.mp4',
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when preview sets are not prepared', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: { previewSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips when no locales are configured', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets: new Map(),
|
||||
versionLocales: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips locales without previews in config', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
// No previews
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets: new Map(),
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should complete without uploading
|
||||
});
|
||||
|
||||
it('uploads new preview for configured locale with string path', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: './previews/demo.mp4',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const newPreview = new AppPreview(requestContext, 'NEW_PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: null,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Create preview set
|
||||
.post(`/v1/${AppPreviewSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock AppPreview.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(newPreview);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(AppPreview.uploadAsync).toHaveBeenCalledWith(
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
id: 'NEW_PSET_1',
|
||||
filePath: '/test/project/previews/demo.mp4',
|
||||
waitForProcessing: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uploads new preview with object config including previewFrameTimeCode', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
previews: {
|
||||
IPHONE_67: {
|
||||
path: './previews/demo.mp4',
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const newPreview = new AppPreview(requestContext, 'NEW_PV_1', {
|
||||
fileName: 'demo.mp4',
|
||||
fileSize: 10240,
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Create preview set
|
||||
.post(`/v1/${AppPreviewSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_PSET_1',
|
||||
type: AppPreviewSet.type,
|
||||
attributes: {
|
||||
previewType: PreviewType.IPHONE_67,
|
||||
appPreviews: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock AppPreview.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppPreview, 'uploadAsync').mockResolvedValue(newPreview);
|
||||
|
||||
const previewSets = new Map();
|
||||
previewSets.set('en-US', new Map());
|
||||
|
||||
await new PreviewsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
previewSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(AppPreview.uploadAsync).toHaveBeenCalledWith(
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
id: 'NEW_PSET_1',
|
||||
filePath: '/test/project/previews/demo.mp4',
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: '00:05:00',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,303 +0,0 @@
|
||||
import {
|
||||
AppScreenshot,
|
||||
AppScreenshotSet,
|
||||
AppStoreVersionLocalization,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { PartialAppleData } from '../../data';
|
||||
import { ScreenshotsTask } from '../screenshots';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
const mockFetch = jest.fn();
|
||||
jest.mock('../../../../fetch', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: any[]) => mockFetch(...args),
|
||||
}));
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
jest.spyOn(fs, 'statSync').mockReturnValue({ size: 1024 } as any);
|
||||
jest.spyOn(fs.promises, 'mkdir').mockResolvedValue(undefined);
|
||||
jest.spyOn(fs.promises, 'writeFile').mockResolvedValue(undefined);
|
||||
|
||||
describe(ScreenshotsTask, () => {
|
||||
beforeEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('prepareAsync', () => {
|
||||
it('initializes empty screenshot sets when no locales are available', async () => {
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: undefined,
|
||||
};
|
||||
|
||||
await new ScreenshotsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.screenshotSets).toBeDefined();
|
||||
expect(context.screenshotSets!.size).toBe(0);
|
||||
});
|
||||
|
||||
it('fetches screenshot sets for each locale', async () => {
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersionLocalization.type}/LOC_1/${AppScreenshotSet.type}`)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: 'SET_1',
|
||||
type: AppScreenshotSet.type,
|
||||
attributes: {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: {} as any,
|
||||
projectDir: '/test/project',
|
||||
versionLocales: [locale],
|
||||
};
|
||||
|
||||
await new ScreenshotsTask().prepareAsync({ context });
|
||||
|
||||
expect(context.screenshotSets).toBeDefined();
|
||||
expect(context.screenshotSets!.size).toBe(1);
|
||||
expect(context.screenshotSets!.get('en-US')).toBeDefined();
|
||||
expect(
|
||||
context.screenshotSets!.get('en-US')!.has(ScreenshotDisplayType.APP_IPHONE_67)
|
||||
).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('skips when screenshot sets are not prepared', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { screenshotSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('skips locales with no screenshot sets', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const screenshotSets = new Map();
|
||||
screenshotSets.set('en-US', new Map());
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('downloads screenshots and sets config', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
const screenshot = new AppScreenshot(requestContext, 'SS_1', {
|
||||
fileName: 'home.png',
|
||||
fileSize: 1024,
|
||||
assetDeliveryState: {
|
||||
state: 'COMPLETE',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Mock getImageAssetUrl
|
||||
jest.spyOn(screenshot, 'getImageAssetUrl').mockReturnValue('https://example.com/home.png');
|
||||
|
||||
const displayTypeMap = new Map<ScreenshotDisplayType, AppScreenshotSet>();
|
||||
const screenshotSet = new AppScreenshotSet(requestContext, 'SET_1', {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [screenshot],
|
||||
} as any);
|
||||
displayTypeMap.set(ScreenshotDisplayType.APP_IPHONE_67, screenshotSet);
|
||||
|
||||
const screenshotSets = new Map();
|
||||
screenshotSets.set('en-US', displayTypeMap);
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
// Mock fetch response
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
|
||||
});
|
||||
|
||||
await new ScreenshotsTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setScreenshots).toBeCalledWith('en-US', {
|
||||
[ScreenshotDisplayType.APP_IPHONE_67]: [
|
||||
'store/apple/screenshot/en-US/APP_IPHONE_67/home.png',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when screenshot sets are not prepared', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: { screenshotSets: undefined, versionLocales: undefined } as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips when no locales are configured', async () => {
|
||||
const config = new AppleConfigReader({});
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
screenshotSets: new Map(),
|
||||
versionLocales: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should not throw, just log and return
|
||||
});
|
||||
|
||||
it('skips locales without screenshots in config', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
// No screenshots
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
screenshotSets: new Map(),
|
||||
versionLocales: [locale],
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Should complete without uploading
|
||||
});
|
||||
|
||||
it('uploads new screenshots for configured locale', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
info: {
|
||||
'en-US': {
|
||||
title: 'My App',
|
||||
screenshots: {
|
||||
APP_IPHONE_67: ['./screenshots/home.png'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const locale = new AppStoreVersionLocalization(requestContext, 'LOC_1', {
|
||||
locale: 'en-US',
|
||||
} as any);
|
||||
|
||||
const newScreenshot = new AppScreenshot(requestContext, 'NEW_SS_1', {
|
||||
fileName: 'home.png',
|
||||
fileSize: 1024,
|
||||
assetDeliveryState: { state: 'COMPLETE', errors: [], warnings: [] },
|
||||
} as any);
|
||||
|
||||
// Mock createAppScreenshotSetAsync on the locale
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Create screenshot set
|
||||
.post(`/v1/${AppScreenshotSet.type}`)
|
||||
.reply(201, {
|
||||
data: {
|
||||
id: 'NEW_SET_1',
|
||||
type: AppScreenshotSet.type,
|
||||
attributes: {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Mock AppScreenshot.uploadAsync to avoid real file access
|
||||
jest.spyOn(AppScreenshot, 'uploadAsync').mockResolvedValue(newScreenshot);
|
||||
|
||||
// Mock AppScreenshotSet.infoAsync for reorder step
|
||||
jest.spyOn(AppScreenshotSet, 'infoAsync').mockResolvedValue(
|
||||
new AppScreenshotSet(requestContext, 'NEW_SET_1', {
|
||||
screenshotDisplayType: ScreenshotDisplayType.APP_IPHONE_67,
|
||||
appScreenshots: [newScreenshot],
|
||||
} as any)
|
||||
);
|
||||
|
||||
// Mock reorderScreenshotsAsync
|
||||
const reorderMock = jest.fn().mockResolvedValue([]);
|
||||
AppScreenshotSet.prototype.reorderScreenshotsAsync = reorderMock;
|
||||
|
||||
const screenshotSets = new Map();
|
||||
screenshotSets.set('en-US', new Map());
|
||||
|
||||
await new ScreenshotsTask().uploadAsync({
|
||||
config,
|
||||
context: {
|
||||
screenshotSets,
|
||||
versionLocales: [locale],
|
||||
projectDir: '/test/project',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
expect(AppScreenshot.uploadAsync).toHaveBeenCalledWith(
|
||||
requestContext,
|
||||
expect.objectContaining({
|
||||
id: 'NEW_SET_1',
|
||||
filePath: '/test/project/screenshots/home.png',
|
||||
waitForProcessing: true,
|
||||
})
|
||||
);
|
||||
// Reorder is skipped because the current order already matches
|
||||
expect(reorderMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AgeRatingDeclaration } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
@@ -6,7 +7,7 @@ import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AgeRatingData = {
|
||||
/** The app age rating declaration for the app info */
|
||||
/** The app age rating declaration for the app version */
|
||||
ageRating: AgeRatingDeclaration;
|
||||
};
|
||||
|
||||
@@ -14,22 +15,7 @@ export class AgeRatingTask extends AppleTask {
|
||||
public name = (): string => 'age rating declarations';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
if (!context.info) {
|
||||
return;
|
||||
}
|
||||
// The ageRatingDeclaration relationship is on appInfos (not appStoreVersions).
|
||||
try {
|
||||
context.ageRating = (await context.info.getAgeRatingDeclarationAsync()) ?? undefined;
|
||||
} catch (error: any) {
|
||||
// Gracefully handle cases where the relationship is not available
|
||||
if (error?.message?.includes('ageRatingDeclaration')) {
|
||||
Log.warn(
|
||||
chalk`{yellow Skipped age rating - not available for this app. This may require updating through App Store Connect directly.}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
context.ageRating = (await context.version!.getAgeRatingDeclarationAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
@@ -39,10 +25,7 @@ export class AgeRatingTask extends AppleTask {
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
if (!context.ageRating) {
|
||||
Log.log(chalk`{dim - Skipped age rating update, not available}`);
|
||||
return;
|
||||
}
|
||||
assert(context.ageRating, `Age rating not initialized, can't update age rating`);
|
||||
|
||||
const ageRating = config.getAgeRating();
|
||||
if (!ageRating) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppStoreReviewDetail } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
@@ -15,10 +16,7 @@ export class AppReviewDetailTask extends AppleTask {
|
||||
public name = (): string => 'app review detail';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
if (!context.version) {
|
||||
return;
|
||||
}
|
||||
context.reviewDetail = (await context.version.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
context.reviewDetail = (await context.version!.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
@@ -34,10 +32,7 @@ export class AppReviewDetailTask extends AppleTask {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.version) {
|
||||
Log.log(chalk`{dim - Skipped store review details, no version available}`);
|
||||
return;
|
||||
}
|
||||
assert(context.version, `App version not initialized, can't upload store review details`);
|
||||
const { versionString } = context.version.attributes;
|
||||
|
||||
if (!context.reviewDetail) {
|
||||
|
||||
@@ -2,8 +2,6 @@ import { AgeRatingTask } from './age-rating';
|
||||
import { AppInfoTask } from './app-info';
|
||||
import { AppReviewDetailTask } from './app-review-detail';
|
||||
import { AppVersionOptions, AppVersionTask } from './app-version';
|
||||
import { PreviewsTask } from './previews';
|
||||
import { ScreenshotsTask } from './screenshots';
|
||||
import { AppleTask } from '../task';
|
||||
|
||||
type AppleTaskOptions = {
|
||||
@@ -19,7 +17,5 @@ export function createAppleTasks({ version }: AppleTaskOptions = {}): AppleTask[
|
||||
new AppInfoTask(),
|
||||
new AgeRatingTask(),
|
||||
new AppReviewDetailTask(),
|
||||
new ScreenshotsTask(),
|
||||
new PreviewsTask(),
|
||||
];
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
import {
|
||||
AppPreview,
|
||||
AppPreviewSet,
|
||||
AppStoreVersionLocalization,
|
||||
PreviewType,
|
||||
} from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import fetch from '../../../fetch';
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
import { ApplePreviewConfig, ApplePreviews } from '../types';
|
||||
|
||||
/** Locale -> PreviewType -> AppPreviewSet */
|
||||
export type PreviewSetsMap = Map<string, Map<PreviewType, AppPreviewSet>>;
|
||||
|
||||
export type PreviewsData = {
|
||||
/** Map of locales to their preview sets */
|
||||
previewSets: PreviewSetsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize preview config to always return an object with path and optional previewFrameTimeCode.
|
||||
*/
|
||||
function normalizePreviewConfig(config: ApplePreviewConfig): {
|
||||
path: string;
|
||||
previewFrameTimeCode?: string;
|
||||
} {
|
||||
if (typeof config === 'string') {
|
||||
return { path: config };
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for managing App Store video previews.
|
||||
* Downloads existing previews and uploads new ones based on store configuration.
|
||||
*/
|
||||
export class PreviewsTask extends AppleTask {
|
||||
public name = (): string => 'video previews';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
// Initialize the preview sets map
|
||||
context.previewSets = new Map();
|
||||
|
||||
if (!context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch preview sets for all locales in parallel
|
||||
await Promise.all(
|
||||
context.versionLocales.map(async locale => {
|
||||
const sets = await locale.getAppPreviewSetsAsync();
|
||||
const previewTypeMap = new Map<PreviewType, AppPreviewSet>();
|
||||
|
||||
for (const set of sets) {
|
||||
previewTypeMap.set(set.attributes.previewType, set);
|
||||
}
|
||||
|
||||
context.previewSets!.set(locale.attributes.locale, previewTypeMap);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
if (!context.previewSets || !context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const locale of context.versionLocales) {
|
||||
const localeCode = locale.attributes.locale;
|
||||
const previewTypeMap = context.previewSets.get(localeCode);
|
||||
|
||||
if (!previewTypeMap || previewTypeMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previews: ApplePreviews = {};
|
||||
|
||||
for (const [previewType, set] of previewTypeMap) {
|
||||
const previewModels = set.attributes.appPreviews;
|
||||
if (!previewModels || previewModels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For now, we only handle the first preview per set (App Store allows up to 3)
|
||||
// We can extend this later to support multiple previews
|
||||
const preview = previewModels[0];
|
||||
const relativePath = await downloadPreviewAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
previewType,
|
||||
preview,
|
||||
0
|
||||
);
|
||||
|
||||
if (relativePath) {
|
||||
// Include preview frame time code if available
|
||||
if (preview.attributes.previewFrameTimeCode) {
|
||||
previews[previewType] = {
|
||||
path: relativePath,
|
||||
previewFrameTimeCode: preview.attributes.previewFrameTimeCode,
|
||||
};
|
||||
} else {
|
||||
previews[previewType] = relativePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(previews).length > 0) {
|
||||
config.setPreviews(localeCode, previews);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
if (!context.previewSets || !context.versionLocales) {
|
||||
Log.log(chalk`{dim - Skipped video previews, no version available}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const locales = config.getLocales();
|
||||
if (locales.length <= 0) {
|
||||
Log.log(chalk`{dim - Skipped video previews, no locales configured}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const localeCode of locales) {
|
||||
const previews = config.getPreviews(localeCode);
|
||||
if (!previews || Object.keys(previews).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
||||
if (!localization) {
|
||||
Log.warn(chalk`{yellow Skipping video previews for ${localeCode} - locale not found}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
||||
if (!previewConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await syncPreviewSetAsync(
|
||||
context.projectDir,
|
||||
localization,
|
||||
previewType as PreviewType,
|
||||
normalizePreviewConfig(previewConfig),
|
||||
context.previewSets.get(localeCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a preview set - upload new preview, delete old one if changed.
|
||||
*/
|
||||
async function syncPreviewSetAsync(
|
||||
projectDir: string,
|
||||
localization: AppStoreVersionLocalization,
|
||||
previewType: PreviewType,
|
||||
previewConfig: { path: string; previewFrameTimeCode?: string },
|
||||
existingSets: Map<PreviewType, AppPreviewSet> | undefined
|
||||
): Promise<void> {
|
||||
const locale = localization.attributes.locale;
|
||||
const absolutePath = path.resolve(projectDir, previewConfig.path);
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
Log.warn(chalk`{yellow Video preview not found: ${absolutePath}}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create the preview set
|
||||
let previewSet = existingSets?.get(previewType);
|
||||
|
||||
if (!previewSet) {
|
||||
previewSet = await logAsync(
|
||||
() =>
|
||||
localization.createAppPreviewSetAsync({
|
||||
previewType,
|
||||
}),
|
||||
{
|
||||
pending: `Creating preview set for ${chalk.bold(previewType)} (${locale})...`,
|
||||
success: `Created preview set for ${chalk.bold(previewType)} (${locale})`,
|
||||
failure: `Failed creating preview set for ${chalk.bold(previewType)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const existingPreviews = previewSet.attributes.appPreviews || [];
|
||||
|
||||
// Check if we need to update (different filename, size, or no existing preview)
|
||||
const existingPreview = existingPreviews.find(p => p.attributes.fileName === fileName);
|
||||
const localSize = fs.statSync(absolutePath).size;
|
||||
|
||||
if (
|
||||
existingPreview &&
|
||||
existingPreview.isComplete() &&
|
||||
existingPreview.attributes.fileSize === localSize
|
||||
) {
|
||||
// Preview with same filename exists, check if we need to update preview frame time code
|
||||
if (
|
||||
previewConfig.previewFrameTimeCode &&
|
||||
existingPreview.attributes.previewFrameTimeCode !== previewConfig.previewFrameTimeCode
|
||||
) {
|
||||
await logAsync(
|
||||
() =>
|
||||
existingPreview.updateAsync({
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Updating preview frame time code for ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Updated preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed updating preview frame time code for ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
Log.log(chalk`{dim Preview ${fileName} already exists, skipping upload}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing previews that don't match
|
||||
for (const preview of existingPreviews) {
|
||||
if (preview.attributes.fileName !== fileName) {
|
||||
await logAsync(() => preview.deleteAsync(), {
|
||||
pending: `Deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})...`,
|
||||
success: `Deleted old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
failure: `Failed deleting old preview ${chalk.bold(preview.attributes.fileName)} (${locale})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Upload new preview
|
||||
await logAsync(
|
||||
() =>
|
||||
AppPreview.uploadAsync(localization.context, {
|
||||
id: previewSet!.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
previewFrameTimeCode: previewConfig.previewFrameTimeCode,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading video preview ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Uploaded video preview ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed uploading video preview ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a video preview to the local filesystem.
|
||||
* Returns the relative path to the downloaded file.
|
||||
*/
|
||||
async function downloadPreviewAsync(
|
||||
projectDir: string,
|
||||
locale: string,
|
||||
previewType: PreviewType,
|
||||
preview: AppPreview,
|
||||
index: number
|
||||
): Promise<string | null> {
|
||||
const videoUrl = preview.getVideoUrl();
|
||||
if (!videoUrl) {
|
||||
Log.warn(chalk`{yellow Could not get download URL for preview ${preview.attributes.fileName}}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create directory structure: store/apple/preview/{locale}/{previewType}/
|
||||
const previewsDir = path.join(projectDir, 'store', 'apple', 'preview', locale, previewType);
|
||||
await fs.promises.mkdir(previewsDir, { recursive: true });
|
||||
|
||||
// Use original filename for matching during sync
|
||||
const fileName = preview.attributes.fileName || `${String(index + 1).padStart(2, '0')}.mp4`;
|
||||
const outputPath = path.join(previewsDir, fileName);
|
||||
const relativePath = path.relative(projectDir, outputPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(videoUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = await response.buffer();
|
||||
await fs.promises.writeFile(outputPath, buffer);
|
||||
|
||||
Log.log(chalk`{dim Downloaded video preview: ${relativePath}}`);
|
||||
return relativePath;
|
||||
} catch (error: any) {
|
||||
Log.warn(chalk`{yellow Failed to download video preview ${fileName}: ${error.message}}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
import {
|
||||
AppScreenshot,
|
||||
AppScreenshotSet,
|
||||
AppStoreVersionLocalization,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import fetch from '../../../fetch';
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
import { AppleScreenshots } from '../types';
|
||||
|
||||
/** Locale -> ScreenshotDisplayType -> AppScreenshotSet */
|
||||
export type ScreenshotSetsMap = Map<string, Map<ScreenshotDisplayType, AppScreenshotSet>>;
|
||||
|
||||
export type ScreenshotsData = {
|
||||
/** Map of locales to their screenshot sets */
|
||||
screenshotSets: ScreenshotSetsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Task for managing App Store screenshots.
|
||||
* Downloads existing screenshots and uploads new ones based on store configuration.
|
||||
*/
|
||||
export class ScreenshotsTask extends AppleTask {
|
||||
public name = (): string => 'screenshots';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
// Initialize the screenshot sets map
|
||||
context.screenshotSets = new Map();
|
||||
|
||||
if (!context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch screenshot sets for all locales in parallel
|
||||
await Promise.all(
|
||||
context.versionLocales.map(async locale => {
|
||||
const sets = await locale.getAppScreenshotSetsAsync();
|
||||
const displayTypeMap = new Map<ScreenshotDisplayType, AppScreenshotSet>();
|
||||
|
||||
for (const set of sets) {
|
||||
displayTypeMap.set(set.attributes.screenshotDisplayType, set);
|
||||
}
|
||||
|
||||
context.screenshotSets!.set(locale.attributes.locale, displayTypeMap);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
if (!context.screenshotSets || !context.versionLocales) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const locale of context.versionLocales) {
|
||||
const localeCode = locale.attributes.locale;
|
||||
const displayTypeMap = context.screenshotSets.get(localeCode);
|
||||
|
||||
if (!displayTypeMap || displayTypeMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenshots: AppleScreenshots = {};
|
||||
|
||||
for (const [displayType, set] of displayTypeMap) {
|
||||
const screenshotModels = set.attributes.appScreenshots;
|
||||
if (!screenshotModels || screenshotModels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download screenshots and save to local filesystem
|
||||
const paths: string[] = [];
|
||||
for (let i = 0; i < screenshotModels.length; i++) {
|
||||
const screenshot = screenshotModels[i];
|
||||
const relativePath = await downloadScreenshotAsync(
|
||||
context.projectDir,
|
||||
localeCode,
|
||||
displayType,
|
||||
screenshot,
|
||||
i
|
||||
);
|
||||
if (relativePath) {
|
||||
paths.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (paths.length > 0) {
|
||||
screenshots[displayType] = paths;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(screenshots).length > 0) {
|
||||
config.setScreenshots(localeCode, screenshots);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
if (!context.screenshotSets || !context.versionLocales) {
|
||||
Log.log(chalk`{dim - Skipped screenshots, no version available}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const locales = config.getLocales();
|
||||
if (locales.length <= 0) {
|
||||
Log.log(chalk`{dim - Skipped screenshots, no locales configured}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const localeCode of locales) {
|
||||
const screenshots = config.getScreenshots(localeCode);
|
||||
if (!screenshots || Object.keys(screenshots).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
||||
if (!localization) {
|
||||
Log.warn(chalk`{yellow Skipping screenshots for ${localeCode} - locale not found}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [displayType, paths] of Object.entries(screenshots)) {
|
||||
if (!paths || paths.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await syncScreenshotSetAsync(
|
||||
context.projectDir,
|
||||
localization,
|
||||
displayType as ScreenshotDisplayType,
|
||||
paths,
|
||||
context.screenshotSets.get(localeCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a screenshot set - upload new screenshots, delete removed ones, reorder if needed.
|
||||
*/
|
||||
async function syncScreenshotSetAsync(
|
||||
projectDir: string,
|
||||
localization: AppStoreVersionLocalization,
|
||||
displayType: ScreenshotDisplayType,
|
||||
paths: string[],
|
||||
existingSets: Map<ScreenshotDisplayType, AppScreenshotSet> | undefined
|
||||
): Promise<void> {
|
||||
const locale = localization.attributes.locale;
|
||||
|
||||
// Get or create the screenshot set
|
||||
let screenshotSet = existingSets?.get(displayType);
|
||||
|
||||
if (!screenshotSet) {
|
||||
screenshotSet = await logAsync(
|
||||
() =>
|
||||
localization.createAppScreenshotSetAsync({
|
||||
screenshotDisplayType: displayType,
|
||||
}),
|
||||
{
|
||||
pending: `Creating screenshot set for ${chalk.bold(displayType)} (${locale})...`,
|
||||
success: `Created screenshot set for ${chalk.bold(displayType)} (${locale})`,
|
||||
failure: `Failed creating screenshot set for ${chalk.bold(displayType)} (${locale})`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const existingScreenshots = screenshotSet.attributes.appScreenshots || [];
|
||||
|
||||
// Build a map of existing screenshots by filename for comparison
|
||||
const existingByFilename = new Map<string, AppScreenshot>();
|
||||
for (const screenshot of existingScreenshots) {
|
||||
existingByFilename.set(screenshot.attributes.fileName, screenshot);
|
||||
}
|
||||
|
||||
// Track which screenshots to keep, upload, and delete
|
||||
const screenshotIdsToKeep: string[] = [];
|
||||
const pathsToUpload: string[] = [];
|
||||
|
||||
for (const relativePath of paths) {
|
||||
const absolutePath = path.resolve(projectDir, relativePath);
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
// Check if screenshot already exists with same name and file size
|
||||
const existing = existingByFilename.get(fileName);
|
||||
const localSize = fs.existsSync(absolutePath) ? fs.statSync(absolutePath).size : null;
|
||||
if (
|
||||
existing &&
|
||||
existing.isComplete() &&
|
||||
(localSize === null || existing.attributes.fileSize === localSize)
|
||||
) {
|
||||
screenshotIdsToKeep.push(existing.id);
|
||||
existingByFilename.delete(fileName);
|
||||
} else {
|
||||
pathsToUpload.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete screenshots that are no longer in config
|
||||
for (const screenshot of existingByFilename.values()) {
|
||||
await logAsync(() => screenshot.deleteAsync(), {
|
||||
pending: `Deleting screenshot ${chalk.bold(screenshot.attributes.fileName)} (${locale})...`,
|
||||
success: `Deleted screenshot ${chalk.bold(screenshot.attributes.fileName)} (${locale})`,
|
||||
failure: `Failed deleting screenshot ${chalk.bold(screenshot.attributes.fileName)} (${locale})`,
|
||||
});
|
||||
}
|
||||
|
||||
// Upload new screenshots
|
||||
for (const absolutePath of pathsToUpload) {
|
||||
const fileName = path.basename(absolutePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
Log.warn(chalk`{yellow Screenshot not found: ${absolutePath}}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newScreenshot = await logAsync(
|
||||
() =>
|
||||
AppScreenshot.uploadAsync(localization.context, {
|
||||
id: screenshotSet!.id,
|
||||
filePath: absolutePath,
|
||||
waitForProcessing: true,
|
||||
}),
|
||||
{
|
||||
pending: `Uploading screenshot ${chalk.bold(fileName)} (${locale})...`,
|
||||
success: `Uploaded screenshot ${chalk.bold(fileName)} (${locale})`,
|
||||
failure: `Failed uploading screenshot ${chalk.bold(fileName)} (${locale})`,
|
||||
}
|
||||
);
|
||||
|
||||
screenshotIdsToKeep.push(newScreenshot.id);
|
||||
}
|
||||
|
||||
// Reorder screenshots to match config order
|
||||
if (screenshotIdsToKeep.length > 0) {
|
||||
const refreshedSet = await AppScreenshotSet.infoAsync(localization.context, {
|
||||
id: screenshotSet.id,
|
||||
});
|
||||
const refreshedScreenshots = refreshedSet.attributes.appScreenshots || [];
|
||||
const screenshotsByFilename = new Map<string, AppScreenshot>();
|
||||
for (const s of refreshedScreenshots) {
|
||||
screenshotsByFilename.set(s.attributes.fileName, s);
|
||||
}
|
||||
|
||||
// Build the desired order based on config paths
|
||||
const orderedIds: string[] = [];
|
||||
for (const relativePath of paths) {
|
||||
const fileName = path.basename(relativePath);
|
||||
const screenshot = screenshotsByFilename.get(fileName);
|
||||
if (screenshot) {
|
||||
orderedIds.push(screenshot.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Only call reorder if the order actually differs from current
|
||||
const currentIds = refreshedScreenshots.map(s => s.id);
|
||||
if (
|
||||
orderedIds.length > 0 &&
|
||||
(orderedIds.length !== currentIds.length || orderedIds.some((id, i) => id !== currentIds[i]))
|
||||
) {
|
||||
await screenshotSet.reorderScreenshotsAsync({ appScreenshots: orderedIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a screenshot to the local filesystem.
|
||||
* Returns the relative path to the downloaded file.
|
||||
*/
|
||||
async function downloadScreenshotAsync(
|
||||
projectDir: string,
|
||||
locale: string,
|
||||
displayType: ScreenshotDisplayType,
|
||||
screenshot: AppScreenshot,
|
||||
index: number
|
||||
): Promise<string | null> {
|
||||
const imageUrl = screenshot.getImageAssetUrl({ type: 'png' });
|
||||
if (!imageUrl) {
|
||||
Log.warn(
|
||||
chalk`{yellow Could not get download URL for screenshot ${screenshot.attributes.fileName}}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create directory structure: store/apple/screenshot/{locale}/{displayType}/
|
||||
const screenshotsDir = path.join(projectDir, 'store', 'apple', 'screenshot', locale, displayType);
|
||||
await fs.promises.mkdir(screenshotsDir, { recursive: true });
|
||||
|
||||
// Use original filename for matching during sync
|
||||
const fileName = screenshot.attributes.fileName || `${String(index + 1).padStart(2, '0')}.png`;
|
||||
const outputPath = path.join(screenshotsDir, fileName);
|
||||
const relativePath = path.relative(projectDir, outputPath);
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const buffer = await response.buffer();
|
||||
await fs.promises.writeFile(outputPath, buffer);
|
||||
|
||||
Log.log(chalk`{dim Downloaded screenshot: ${relativePath}}`);
|
||||
return relativePath;
|
||||
} catch (error: any) {
|
||||
Log.warn(chalk`{yellow Failed to download screenshot ${fileName}: ${error.message}}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,7 @@
|
||||
import type {
|
||||
AgeRatingDeclarationProps,
|
||||
PreviewType,
|
||||
ScreenshotDisplayType,
|
||||
} from '@expo/apple-utils';
|
||||
import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
|
||||
|
||||
export type AppleLocale = string;
|
||||
|
||||
/** Screenshot display type enum values from App Store Connect API */
|
||||
export type AppleScreenshotDisplayType = `${ScreenshotDisplayType}`;
|
||||
|
||||
/** Preview display type enum values from App Store Connect API */
|
||||
export type ApplePreviewType = `${PreviewType}`;
|
||||
|
||||
/**
|
||||
* Screenshots organized by display type.
|
||||
* Key is the display type (e.g., 'APP_IPHONE_67'), value is array of file paths.
|
||||
* @example { "APP_IPHONE_67": ["./screenshots/home.png", "./screenshots/profile.png"] }
|
||||
*/
|
||||
export type AppleScreenshots = Partial<Record<AppleScreenshotDisplayType, string[]>>;
|
||||
|
||||
/**
|
||||
* Video preview configuration - either a simple path string or an object with options.
|
||||
* @example "./previews/demo.mp4"
|
||||
* @example { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" }
|
||||
*/
|
||||
export type ApplePreviewConfig =
|
||||
| string
|
||||
| {
|
||||
/** Video file path (relative to project root) */
|
||||
path: string;
|
||||
/** Optional preview frame time code (e.g., '00:05:00' for 5 seconds) */
|
||||
previewFrameTimeCode?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Video previews organized by display type.
|
||||
* Key is the display type (e.g., 'IPHONE_67'), value is the preview config.
|
||||
* @example { "IPHONE_67": "./previews/demo.mp4" }
|
||||
* @example { "IPHONE_67": { path: "./previews/demo.mp4", previewFrameTimeCode: "00:05:00" } }
|
||||
*/
|
||||
export type ApplePreviews = Partial<Record<ApplePreviewType, ApplePreviewConfig>>;
|
||||
|
||||
export interface AppleMetadata {
|
||||
version?: string;
|
||||
copyright?: string;
|
||||
@@ -48,7 +9,6 @@ export interface AppleMetadata {
|
||||
categories?: AppleCategory;
|
||||
release?: AppleRelease;
|
||||
advisory?: AppleAdvisory;
|
||||
/** @deprecated Use screenshots/previews in AppleInfo instead */
|
||||
preview?: Record<string, string[]>;
|
||||
review?: AppleReview;
|
||||
}
|
||||
@@ -80,10 +40,6 @@ export interface AppleInfo {
|
||||
privacyPolicyText?: string;
|
||||
privacyChoicesUrl?: string;
|
||||
supportUrl?: string;
|
||||
/** Screenshots for this locale, organized by display type */
|
||||
screenshots?: AppleScreenshots;
|
||||
/** Video previews for this locale, organized by display type */
|
||||
previews?: ApplePreviews;
|
||||
}
|
||||
|
||||
export interface AppleReview {
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function downloadMetadataAsync({
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleWriter();
|
||||
const tasks = createAppleTasks();
|
||||
const taskCtx = { app, projectDir };
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function uploadMetadataAsync({
|
||||
version: config.getVersion()?.versionString,
|
||||
});
|
||||
|
||||
const taskCtx = { app, projectDir };
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
|
||||
describe('resolveWorkflowAsync with GitClient', () => {
|
||||
let repoRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
repoRoot = await fs.realpath(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-workflow-test-'))
|
||||
);
|
||||
await spawnAsync('git', ['init'], { cwd: repoRoot });
|
||||
await spawnAsync('git', ['config', 'user.email', '[email protected]'], { cwd: repoRoot });
|
||||
await spawnAsync('git', ['config', 'user.name', 'Test User'], { cwd: repoRoot });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(repoRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('treats tracked gitignored native projects as managed when requireCommit is false', async () => {
|
||||
const vcsClient = new GitClient({
|
||||
requireCommit: false,
|
||||
maybeCwdOverride: repoRoot,
|
||||
});
|
||||
|
||||
await fs.mkdir(path.join(repoRoot, 'ios', 'app.xcodeproj'), { recursive: true });
|
||||
await fs.writeFile(path.join(repoRoot, 'ios', 'app.xcodeproj', 'project.pbxproj'), 'fake');
|
||||
await fs.writeFile(path.join(repoRoot, '.gitignore'), 'ios/\n');
|
||||
|
||||
await spawnAsync('git', ['add', '.gitignore'], { cwd: repoRoot });
|
||||
await spawnAsync('git', ['add', '-f', 'ios/app.xcodeproj/project.pbxproj'], { cwd: repoRoot });
|
||||
await spawnAsync('git', ['commit', '-m', 'test setup'], { cwd: repoRoot });
|
||||
|
||||
await expect(resolveWorkflowAsync(repoRoot, Platform.IOS, vcsClient)).resolves.toBe(
|
||||
Workflow.MANAGED
|
||||
);
|
||||
});
|
||||
|
||||
it('treats tracked gitignored native projects as generic when requireCommit is true', async () => {
|
||||
const vcsClient = new GitClient({
|
||||
requireCommit: true,
|
||||
maybeCwdOverride: repoRoot,
|
||||
});
|
||||
|
||||
await fs.mkdir(path.join(repoRoot, 'ios', 'app.xcodeproj'), { recursive: true });
|
||||
await fs.writeFile(path.join(repoRoot, 'ios', 'app.xcodeproj', 'project.pbxproj'), 'fake');
|
||||
await fs.writeFile(path.join(repoRoot, '.gitignore'), 'ios/\n');
|
||||
|
||||
await spawnAsync('git', ['add', '.gitignore'], { cwd: repoRoot });
|
||||
await spawnAsync('git', ['add', '-f', 'ios/app.xcodeproj/project.pbxproj'], { cwd: repoRoot });
|
||||
await spawnAsync('git', ['commit', '-m', 'test setup'], { cwd: repoRoot });
|
||||
|
||||
await expect(resolveWorkflowAsync(repoRoot, Platform.IOS, vcsClient)).resolves.toBe(
|
||||
Workflow.GENERIC
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -137,21 +137,81 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
})
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('throws an error in non-interactive mode', async () => {
|
||||
it('auto-generates application id in non-interactive mode when suggestion is available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
name: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.jester.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
|
||||
expect(appJson).toMatchObject({
|
||||
expo: { android: { package: 'com.jester.myapp' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses ios.bundleIdentifier as application id in non-interactive mode when available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "blah": {} }',
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp', ios: { bundleIdentifier: 'com.example.myapp' } } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.example.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error in non-interactive mode when no valid suggestion can be generated', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '',
|
||||
exp: {} as any,
|
||||
projectId: '1234',
|
||||
exp: { slug: '' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
})
|
||||
|
||||
@@ -148,15 +148,8 @@ async function configureApplicationIdAsync({
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#package'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
@@ -166,25 +159,50 @@ async function configureApplicationIdAsync({
|
||||
|
||||
assert(paths.staticConfigPath, 'app.json must exist');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 Android application id`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/android-package')
|
||||
)}`
|
||||
);
|
||||
let packageName: string;
|
||||
|
||||
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
const { packageName } = await promptAsync({
|
||||
name: 'packageName',
|
||||
type: 'text',
|
||||
message: `What would you like your Android application id to be?`,
|
||||
initial: suggestedAndroidApplicationId,
|
||||
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
|
||||
});
|
||||
if (nonInteractive) {
|
||||
let suggestedAndroidApplicationId: string | undefined;
|
||||
try {
|
||||
suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
} catch {
|
||||
// If we can't get a suggestion, we'll throw the non-interactive error below
|
||||
}
|
||||
if (!suggestedAndroidApplicationId) {
|
||||
throw new Error(
|
||||
`The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#package'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
packageName = suggestedAndroidApplicationId;
|
||||
Log.log(`Using automatically generated Android application id: ${chalk.bold(packageName)}`);
|
||||
} else {
|
||||
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 Android application id`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/android-package')
|
||||
)}`
|
||||
);
|
||||
|
||||
const result = await promptAsync({
|
||||
name: 'packageName',
|
||||
type: 'text',
|
||||
message: `What would you like your Android application id to be?`,
|
||||
initial: suggestedAndroidApplicationId,
|
||||
validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE),
|
||||
});
|
||||
packageName = result.packageName;
|
||||
}
|
||||
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
|
||||
@@ -115,20 +115,81 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
})
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('throws an error in non-interactive mode', async () => {
|
||||
it('auto-generates bundle identifier in non-interactive mode when suggestion is available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
name: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "blah": {} }',
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.jester.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
|
||||
expect(appJson).toMatchObject({
|
||||
expo: { ios: { bundleIdentifier: 'com.jester.myapp' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses android.package as bundle identifier in non-interactive mode when available', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const result = await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: { slug: 'myapp', android: { package: 'com.example.myapp' } } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('com.example.myapp');
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws an error in non-interactive mode when no valid suggestion can be generated', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: {} as any,
|
||||
exp: { slug: '' } as any,
|
||||
vcsClient,
|
||||
nonInteractive: true,
|
||||
})
|
||||
|
||||
@@ -7,23 +7,6 @@ import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../exe
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../prompts');
|
||||
|
||||
// @expo/require-utils uses require() to load JS configs, which bypasses the
|
||||
// mocked fs. Override loadModuleSync to read from the mocked fs and evaluate.
|
||||
jest.mock('@expo/require-utils', () => {
|
||||
const actual = jest.requireActual('@expo/require-utils');
|
||||
return {
|
||||
...actual,
|
||||
loadModuleSync: (filename: string) => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync(filename, 'utf-8');
|
||||
const mod = { exports: {} as any };
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function('module', 'exports', content)(mod, mod.exports);
|
||||
return mod.exports;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetAllMocks();
|
||||
vol.reset();
|
||||
|
||||
@@ -137,15 +137,8 @@ async function configureBundleIdentifierAsync({
|
||||
exp: ExpoConfig;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
throw new Error(
|
||||
@@ -155,26 +148,51 @@ async function configureBundleIdentifierAsync({
|
||||
|
||||
assert(paths.staticConfigPath, 'app.json must exist');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/bundle-identifier')
|
||||
)}`
|
||||
);
|
||||
let bundleIdentifier: string;
|
||||
|
||||
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
if (nonInteractive) {
|
||||
let suggestedBundleIdentifier: string | undefined;
|
||||
try {
|
||||
suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
} catch {
|
||||
// If we can't get a suggestion, we'll throw the non-interactive error below
|
||||
}
|
||||
if (!suggestedBundleIdentifier) {
|
||||
throw new Error(
|
||||
`The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
bundleIdentifier = suggestedBundleIdentifier;
|
||||
Log.log(`Using automatically generated iOS bundle identifier: ${chalk.bold(bundleIdentifier)}`);
|
||||
} else {
|
||||
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim(
|
||||
learnMore('https://expo.fyi/bundle-identifier')
|
||||
)}`
|
||||
);
|
||||
|
||||
const { bundleIdentifier } = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
type: 'text',
|
||||
message: `What would you like your iOS bundle identifier to be?`,
|
||||
initial: suggestedBundleIdentifier,
|
||||
validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE),
|
||||
});
|
||||
const result = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
type: 'text',
|
||||
message: `What would you like your iOS bundle identifier to be?`,
|
||||
initial: suggestedBundleIdentifier,
|
||||
validate: value =>
|
||||
isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE,
|
||||
});
|
||||
bundleIdentifier = result.bundleIdentifier;
|
||||
}
|
||||
|
||||
const rawStaticConfig = readAppJson(paths.staticConfigPath);
|
||||
rawStaticConfig.expo = {
|
||||
|
||||
@@ -91,14 +91,14 @@ describe('git', () => {
|
||||
await fs.rm(`${repoRoot}/.gitignore`);
|
||||
});
|
||||
|
||||
it.each(testFiles.filter(file => file.gitignore))(
|
||||
it.each(testFiles.filter(file => file.gitignore && !file.commit))(
|
||||
'$filename should be ignored',
|
||||
async file => {
|
||||
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(testFiles.filter(file => !file.gitignore))(
|
||||
it.each(testFiles.filter(file => !file.gitignore || file.commit))(
|
||||
'$filename should not be ignored',
|
||||
async file => {
|
||||
expect(await vcs.isFileIgnoredAsync(file.filename)).toBe(false);
|
||||
|
||||
@@ -376,18 +376,12 @@ export default class GitClient extends Client {
|
||||
return wouldNotBeCopiedToClone && (!isTracked || wouldBeDeletedFromClone);
|
||||
}
|
||||
|
||||
if (isTracked && this.requireCommit) {
|
||||
// With requireCommit=true we rely on a Git checkout, so tracked files are included
|
||||
// even if they match ignore patterns.
|
||||
return false;
|
||||
if (isTracked) {
|
||||
return false; // Tracked files aren't ignored even if they match ignore patterns
|
||||
}
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
'git',
|
||||
['check-ignore', '-q', ...(this.requireCommit ? [] : ['--no-index']), filePath],
|
||||
{ cwd: rootPath }
|
||||
);
|
||||
await spawnAsync('git', ['check-ignore', '-q', filePath], { cwd: rootPath });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "18.4.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/config-plugins": "8.0.8",
|
||||
"@expo/config-types": "52.0.1",
|
||||
"@expo/downloader": "18.0.1",
|
||||
"@expo/eas-build-job": "18.4.0",
|
||||
"@expo/logger": "18.0.1",
|
||||
|
||||
+4
-13
@@ -82,7 +82,6 @@ export async function logProjectDependenciesAsync(
|
||||
});
|
||||
} catch (error: any) {
|
||||
sentry.handleError('Failed to report project dependencies metrics', error, {
|
||||
level: 'warning',
|
||||
tags: {
|
||||
errorCode: 'FAILED_TO_REPORT_PROJECT_DEPENDENCIES_EVENT',
|
||||
},
|
||||
@@ -134,15 +133,10 @@ async function resolveNewArchEnabled(
|
||||
}
|
||||
|
||||
if (
|
||||
(appConfig?.[job.platform] as unknown as Record<string, unknown>)?.newArchEnabled !==
|
||||
undefined ||
|
||||
(appConfig as unknown as Record<string, unknown>).newArchEnabled !== undefined
|
||||
appConfig?.[job.platform]?.newArchEnabled !== undefined ||
|
||||
appConfig.newArchEnabled !== undefined
|
||||
) {
|
||||
return (
|
||||
((appConfig?.[job.platform] as unknown as Record<string, unknown>)
|
||||
?.newArchEnabled as boolean) ??
|
||||
((appConfig as unknown as Record<string, unknown>).newArchEnabled as boolean)
|
||||
);
|
||||
return appConfig?.[job.platform]?.newArchEnabled ?? appConfig.newArchEnabled;
|
||||
}
|
||||
|
||||
if (job.type === Workflow.GENERIC) {
|
||||
@@ -226,10 +220,7 @@ async function getPackageManagerVersion(
|
||||
}
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version'], {
|
||||
cwd,
|
||||
env,
|
||||
});
|
||||
const { stdout } = await spawnAsync(packageManager, ['--version'], { cwd, env });
|
||||
return stdout.toString().trim();
|
||||
} catch {
|
||||
// Some package managers can fail version checks in project dirs with mismatched packageManager fields.
|
||||
|
||||
@@ -844,17 +844,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/code-frame@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier": "npm:^7.28.5"
|
||||
js-tokens: "npm:^4.0.0"
|
||||
picocolors: "npm:^1.1.1"
|
||||
checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.22.13":
|
||||
version: 7.22.13
|
||||
resolution: "@babel/code-frame@npm:7.22.13"
|
||||
@@ -922,13 +911,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/compat-data@npm:^7.28.6":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/compat-data@npm:7.29.0"
|
||||
checksum: 10c0/08f348554989d23aa801bf1405aa34b15e841c0d52d79da7e524285c77a5f9d298e70e11d91cc578d8e2c9542efc586d50c5f5cf8e1915b254a9dcf786913a94
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:7.24.7":
|
||||
version: 7.24.7
|
||||
resolution: "@babel/core@npm:7.24.7"
|
||||
@@ -1044,29 +1026,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.25.2":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/core@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.29.0"
|
||||
"@babel/generator": "npm:^7.29.0"
|
||||
"@babel/helper-compilation-targets": "npm:^7.28.6"
|
||||
"@babel/helper-module-transforms": "npm:^7.28.6"
|
||||
"@babel/helpers": "npm:^7.28.6"
|
||||
"@babel/parser": "npm:^7.29.0"
|
||||
"@babel/template": "npm:^7.28.6"
|
||||
"@babel/traverse": "npm:^7.29.0"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
"@jridgewell/remapping": "npm:^2.3.5"
|
||||
convert-source-map: "npm:^2.0.0"
|
||||
debug: "npm:^4.1.0"
|
||||
gensync: "npm:^1.0.0-beta.2"
|
||||
json5: "npm:^2.2.3"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 10c0/5127d2e8e842ae409e11bcbb5c2dff9874abf5415e8026925af7308e903f4f43397341467a130490d1a39884f461bc2b67f3063bce0be44340db89687fd852aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.16.5":
|
||||
version: 7.16.5
|
||||
resolution: "@babel/generator@npm:7.16.5"
|
||||
@@ -1148,19 +1107,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.29.0":
|
||||
version: 7.29.1
|
||||
resolution: "@babel/generator@npm:7.29.1"
|
||||
dependencies:
|
||||
"@babel/parser": "npm:^7.29.0"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.12"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.28"
|
||||
jsesc: "npm:^3.0.2"
|
||||
checksum: 10c0/349086e6876258ef3fb2823030fee0f6c0eb9c3ebe35fc572e16997f8c030d765f636ddc6299edae63e760ea6658f8ee9a2edfa6d6b24c9a80c917916b973551
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.7.2":
|
||||
version: 7.15.0
|
||||
resolution: "@babel/generator@npm:7.15.0"
|
||||
@@ -1270,19 +1216,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-compilation-targets@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-compilation-targets@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/compat-data": "npm:^7.28.6"
|
||||
"@babel/helper-validator-option": "npm:^7.27.1"
|
||||
browserslist: "npm:^4.24.0"
|
||||
lru-cache: "npm:^5.1.1"
|
||||
semver: "npm:^6.3.1"
|
||||
checksum: 10c0/3fcdf3b1b857a1578e99d20508859dbd3f22f3c87b8a0f3dc540627b4be539bae7f6e61e49d931542fe5b557545347272bbdacd7f58a5c77025a18b745593a50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-create-class-features-plugin@npm:^7.12.1":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/helper-create-class-features-plugin@npm:7.12.1"
|
||||
@@ -1435,16 +1368,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-imports@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-module-imports@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/traverse": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.28.6"
|
||||
checksum: 10c0/b49d8d8f204d9dbfd5ac70c54e533e5269afb3cea966a9d976722b13e9922cc773a653405f53c89acb247d5aebdae4681d631a3ae3df77ec046b58da76eda2ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.12.1":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/helper-module-transforms@npm:7.12.1"
|
||||
@@ -1538,19 +1461,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-module-transforms@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/helper-module-imports": "npm:^7.28.6"
|
||||
"@babel/helper-validator-identifier": "npm:^7.28.5"
|
||||
"@babel/traverse": "npm:^7.28.6"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0
|
||||
checksum: 10c0/6f03e14fc30b287ce0b839474b5f271e72837d0cafe6b172d759184d998fbee3903a035e81e07c2c596449e504f453463d58baa65b6f40a37ded5bec74620b2b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-optimise-call-expression@npm:^7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/helper-optimise-call-expression@npm:7.10.4"
|
||||
@@ -1588,13 +1498,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-plugin-utils@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.28.6"
|
||||
checksum: 10c0/3f5f8acc152fdbb69a84b8624145ff4f9b9f6e776cb989f9f968f8606eb7185c5c3cfcf3ba08534e37e1e0e1c118ac67080610333f56baa4f7376c99b5f1143d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-replace-supers@npm:^7.12.1":
|
||||
version: 7.12.5
|
||||
resolution: "@babel/helper-replace-supers@npm:7.12.5"
|
||||
@@ -1857,16 +1760,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helpers@npm:^7.28.6":
|
||||
version: 7.29.2
|
||||
resolution: "@babel/helpers@npm:7.29.2"
|
||||
dependencies:
|
||||
"@babel/template": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
checksum: 10c0/dab0e65b9318b2502a62c58bc0913572318595eec0482c31f0ad416b72636e6698a1d7c57cd2791d4528eb8c548bca88d338dc4d2a55a108dc1f6702f9bc5512
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/highlight@npm:^7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/highlight@npm:7.10.4"
|
||||
@@ -2029,17 +1922,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0":
|
||||
version: 7.29.2
|
||||
resolution: "@babel/parser@npm:7.29.2"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
bin:
|
||||
parser: ./bin/babel-parser.js
|
||||
checksum: 10c0/e5a4e69e3ac7acdde995f37cf299a68458cfe7009dff66bd0962fd04920bef287201169006af365af479c08ff216bfefbb595e331f87f6ae7283858aebbc3317
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-class-properties@npm:^7.0.0":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/plugin-proposal-class-properties@npm:7.12.1"
|
||||
@@ -2396,18 +2278,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs@npm:^7.24.8":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms": "npm:^7.28.6"
|
||||
"@babel/helper-plugin-utils": "npm:^7.28.6"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/7c45992797c6150644c8552feff4a016ba7bd6d59ff2b039ed969a9c5b20a6804cd9d21db5045fc8cca8ca7f08262497e354e93f8f2be6a1cdf3fbfa8c31a9b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-object-super@npm:^7.0.0":
|
||||
version: 7.12.1
|
||||
resolution: "@babel/plugin-transform-object-super@npm:7.12.1"
|
||||
@@ -2587,17 +2457,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/template@npm:7.28.6"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.28.6"
|
||||
"@babel/parser": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.28.6"
|
||||
checksum: 10c0/66d87225ed0bc77f888181ae2d97845021838c619944877f7c4398c6748bcf611f216dfd6be74d39016af502bca876e6ce6873db3c49e4ac354c56d34d57e9f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.12.1, @babel/traverse@npm:^7.12.5, @babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.5, @babel/traverse@npm:^7.16.7, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.17.10, @babel/traverse@npm:^7.17.3":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/traverse@npm:7.23.2"
|
||||
@@ -2649,21 +2508,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/traverse@npm:7.29.0"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.29.0"
|
||||
"@babel/generator": "npm:^7.29.0"
|
||||
"@babel/helper-globals": "npm:^7.28.0"
|
||||
"@babel/parser": "npm:^7.29.0"
|
||||
"@babel/template": "npm:^7.28.6"
|
||||
"@babel/types": "npm:^7.29.0"
|
||||
debug: "npm:^4.3.1"
|
||||
checksum: 10c0/f63ef6e58d02a9fbf3c0e2e5f1c877da3e0bc57f91a19d2223d53e356a76859cbaf51171c9211c71816d94a0e69efa2732fd27ffc0e1bbc84b636e60932333eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.11.0, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3":
|
||||
version: 7.12.6
|
||||
resolution: "@babel/types@npm:7.12.6"
|
||||
@@ -2779,7 +2623,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.8.3":
|
||||
"@babel/types@npm:^7.8.3":
|
||||
version: 7.29.0
|
||||
resolution: "@babel/types@npm:7.29.0"
|
||||
dependencies:
|
||||
@@ -2847,12 +2691,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/apple-utils@npm:2.1.16":
|
||||
version: 2.1.16
|
||||
resolution: "@expo/apple-utils@npm:2.1.16"
|
||||
"@expo/apple-utils@npm:2.1.15":
|
||||
version: 2.1.15
|
||||
resolution: "@expo/apple-utils@npm:2.1.15"
|
||||
bin:
|
||||
apple-utils: bin.js
|
||||
checksum: 10c0/9d3915f3f25c32e1f1ab323676cabccc2998046be6f30deebc68a0706836bd50bc5145662fa13441e0283f82c56e50f55002d50dcc43aa78e9fa7327d1676a54
|
||||
checksum: 10c0/956abdb77d1d3bcb09d42b70745705305d23b9aa4ed50e3da795e93e50dc3ada126cc16f73a55b49538f0d793ccdc455bad0caf7aaadbadd5ee7a3d0f3c69465
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2860,8 +2704,8 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config": "npm:10.0.6"
|
||||
"@expo/config-plugins": "npm:9.0.12"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/env": "npm:^0.4.0"
|
||||
@@ -2948,24 +2792,48 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-plugins@npm:55.0.7, @expo/config-plugins@npm:~55.0.7":
|
||||
version: 55.0.7
|
||||
resolution: "@expo/config-plugins@npm:55.0.7"
|
||||
"@expo/config-plugins@npm:8.0.8":
|
||||
version: 8.0.8
|
||||
resolution: "@expo/config-plugins@npm:8.0.8"
|
||||
dependencies:
|
||||
"@expo/config-types": "npm:^55.0.5"
|
||||
"@expo/json-file": "npm:~10.0.12"
|
||||
"@expo/plist": "npm:^0.5.2"
|
||||
"@expo/config-types": "npm:^51.0.0-unreleased"
|
||||
"@expo/json-file": "npm:~8.3.0"
|
||||
"@expo/plist": "npm:^0.1.0"
|
||||
"@expo/sdk-runtime-versions": "npm:^1.0.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
debug: "npm:^4.3.5"
|
||||
getenv: "npm:^2.0.0"
|
||||
glob: "npm:^13.0.0"
|
||||
debug: "npm:^4.3.1"
|
||||
find-up: "npm:~5.0.0"
|
||||
getenv: "npm:^1.0.0"
|
||||
glob: "npm:7.1.6"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
semver: "npm:^7.5.4"
|
||||
slash: "npm:^3.0.0"
|
||||
slugify: "npm:^1.6.6"
|
||||
xcode: "npm:^3.0.1"
|
||||
xml2js: "npm:0.6.0"
|
||||
checksum: 10c0/46cea118d9a780ac367862ef136aafa65063a3bc9a9864ece2a18d4b9e66e64d2bb19f0c0972e07f1106a58203f65671208bebcf5e207953f54217210807746c
|
||||
checksum: 10c0/4d403ce9df0cd694cd30f1a914c4e4529c529e9b5f572ddc3cdf88f80d4c47a8ba8336da78ae3f9c67966a7fa1e02d87ea2dff95102a0a337004d118ebc32d63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-plugins@npm:9.0.12":
|
||||
version: 9.0.12
|
||||
resolution: "@expo/config-plugins@npm:9.0.12"
|
||||
dependencies:
|
||||
"@expo/config-types": "npm:^52.0.0"
|
||||
"@expo/json-file": "npm:~9.0.0"
|
||||
"@expo/plist": "npm:^0.2.0"
|
||||
"@expo/sdk-runtime-versions": "npm:^1.0.0"
|
||||
chalk: "npm:^4.1.2"
|
||||
debug: "npm:^4.3.5"
|
||||
getenv: "npm:^1.0.0"
|
||||
glob: "npm:^10.4.2"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
semver: "npm:^7.5.4"
|
||||
slash: "npm:^3.0.0"
|
||||
slugify: "npm:^1.6.6"
|
||||
xcode: "npm:^3.0.1"
|
||||
xml2js: "npm:0.6.0"
|
||||
checksum: 10c0/a27f085caf3d9ce29ca8b532425dd409054d8ef449b5d5338db2a2b1ead8051073e4f155691f6c2dc40b46816229fffbd09ef51c84eb87aa95eefaea9c810d40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2991,7 +2859,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-plugins@npm:~9.0.0, @expo/config-plugins@npm:~9.0.17":
|
||||
"@expo/config-plugins@npm:~9.0.0, @expo/config-plugins@npm:~9.0.10, @expo/config-plugins@npm:~9.0.17":
|
||||
version: 9.0.17
|
||||
resolution: "@expo/config-plugins@npm:9.0.17"
|
||||
dependencies:
|
||||
@@ -3013,10 +2881,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-types@npm:55.0.5, @expo/config-types@npm:^55.0.5":
|
||||
version: 55.0.5
|
||||
resolution: "@expo/config-types@npm:55.0.5"
|
||||
checksum: 10c0/24ce0481cc465ddd3b53cfdde099ef4e899b1f8fff224a0f249b88c93e6c98930e99a55f3929eb53d08138b1b66102ece7b76e16f4e5fadcdf5bbac26c9c3d7e
|
||||
"@expo/config-types@npm:52.0.1":
|
||||
version: 52.0.1
|
||||
resolution: "@expo/config-types@npm:52.0.1"
|
||||
checksum: 10c0/f06287eb20d599fbc422bf58dc97a803d8f9242699ce43188b90bfd92d54da27f83ca1225bf1ec61a5c30e74c57e47e7bfba2ef3cada38983622657658fafe4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config-types@npm:^51.0.0-unreleased":
|
||||
version: 51.0.3
|
||||
resolution: "@expo/config-types@npm:51.0.3"
|
||||
checksum: 10c0/bd87a729da985b0097ab29367c0473a2bced5ff211d416f342729e1b2631a7c00d62878e05cdee49ece7e3b65d3296957917f24380d57e2faef2cf220194fdec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3034,6 +2909,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:10.0.6":
|
||||
version: 10.0.6
|
||||
resolution: "@expo/config@npm:10.0.6"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:~7.10.4"
|
||||
"@expo/config-plugins": "npm:~9.0.10"
|
||||
"@expo/config-types": "npm:^52.0.0"
|
||||
"@expo/json-file": "npm:^9.0.0"
|
||||
deepmerge: "npm:^4.3.1"
|
||||
getenv: "npm:^1.0.0"
|
||||
glob: "npm:^10.4.2"
|
||||
require-from-string: "npm:^2.0.2"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
resolve-workspace-root: "npm:^2.0.0"
|
||||
semver: "npm:^7.6.0"
|
||||
slugify: "npm:^1.3.4"
|
||||
sucrase: "npm:3.35.0"
|
||||
checksum: 10c0/1fdf47ea0e268b475e30e02ebdc1f6739c0b426a4c6c695a7b4ee680109258b4ccfbd3010d3bd031a6efcd7a861670be84d10cd0019d0f5d14dc669a5039b860
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:11.0.10":
|
||||
version: 11.0.10
|
||||
resolution: "@expo/config@npm:11.0.10"
|
||||
@@ -3055,25 +2951,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:55.0.10":
|
||||
version: 55.0.10
|
||||
resolution: "@expo/config@npm:55.0.10"
|
||||
dependencies:
|
||||
"@expo/config-plugins": "npm:~55.0.7"
|
||||
"@expo/config-types": "npm:^55.0.5"
|
||||
"@expo/json-file": "npm:^10.0.12"
|
||||
"@expo/require-utils": "npm:^55.0.3"
|
||||
deepmerge: "npm:^4.3.1"
|
||||
getenv: "npm:^2.0.0"
|
||||
glob: "npm:^13.0.0"
|
||||
resolve-from: "npm:^5.0.0"
|
||||
resolve-workspace-root: "npm:^2.0.0"
|
||||
semver: "npm:^7.6.0"
|
||||
slugify: "npm:^1.3.4"
|
||||
checksum: 10c0/a2e0a100c8d472e56e9ad649a8852eddca76ab64503475c803b4fc372df08e303169243b6a782fe150c54212ba02f47c40c4f0e365c13e77e70f5d777aa8d704
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/config@npm:~10.0.4":
|
||||
version: 10.0.11
|
||||
resolution: "@expo/config@npm:10.0.11"
|
||||
@@ -3197,7 +3074,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:8.3.3":
|
||||
"@expo/json-file@npm:8.3.3, @expo/json-file@npm:~8.3.0":
|
||||
version: 8.3.3
|
||||
resolution: "@expo/json-file@npm:8.3.3"
|
||||
dependencies:
|
||||
@@ -3208,16 +3085,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:^10.0.12, @expo/json-file@npm:~10.0.12":
|
||||
version: 10.0.12
|
||||
resolution: "@expo/json-file@npm:10.0.12"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.20.0"
|
||||
json5: "npm:^2.2.3"
|
||||
checksum: 10c0/52131a6426e96208ff1b295d580fc70eebb8e292b29fde1db016b2f21a0942a7521feec96b3f58efe5b32dcc1642d569b4211d651146fcdb9bf7e5f08b635878
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:^10.0.8":
|
||||
version: 10.0.8
|
||||
resolution: "@expo/json-file@npm:10.0.8"
|
||||
@@ -3259,7 +3126,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/json-file@npm:~9.0.2":
|
||||
"@expo/json-file@npm:~9.0.0, @expo/json-file@npm:~9.0.2":
|
||||
version: 9.0.2
|
||||
resolution: "@expo/json-file@npm:9.0.2"
|
||||
dependencies:
|
||||
@@ -3338,6 +3205,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plist@npm:^0.1.0":
|
||||
version: 0.1.3
|
||||
resolution: "@expo/plist@npm:0.1.3"
|
||||
dependencies:
|
||||
"@xmldom/xmldom": "npm:~0.7.7"
|
||||
base64-js: "npm:^1.2.3"
|
||||
xmlbuilder: "npm:^14.0.0"
|
||||
checksum: 10c0/134315260a7828bc1ce4563e2af67499b498feae46c39c5c2cab9d72082402a42d3b7575f13e269022bcf3c28668948ea960dd4943bd38f52f9c01154317aac5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plist@npm:^0.2.0, @expo/plist@npm:^0.2.2":
|
||||
version: 0.2.2
|
||||
resolution: "@expo/plist@npm:0.2.2"
|
||||
@@ -3360,17 +3238,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plist@npm:^0.5.2":
|
||||
version: 0.5.2
|
||||
resolution: "@expo/plist@npm:0.5.2"
|
||||
dependencies:
|
||||
"@xmldom/xmldom": "npm:^0.8.8"
|
||||
base64-js: "npm:^1.5.1"
|
||||
xmlbuilder: "npm:^15.1.1"
|
||||
checksum: 10c0/19adae2a365ac1a12db93682fb310ff8be03c711f9173bebe5841cbe60cdfb749247bc1a95fa0977b5bac3aa6a078a0fceeafe4ff6c66d1ed67cce496679e310
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/plugin-help@npm:5.1.23":
|
||||
version: 5.1.23
|
||||
resolution: "@expo/plugin-help@npm:5.1.23"
|
||||
@@ -3427,22 +3294,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/require-utils@npm:^55.0.3":
|
||||
version: 55.0.3
|
||||
resolution: "@expo/require-utils@npm:55.0.3"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.20.0"
|
||||
"@babel/core": "npm:^7.25.2"
|
||||
"@babel/plugin-transform-modules-commonjs": "npm:^7.24.8"
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0 || ^5.0.0-0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 10c0/562a2dc71f983fba2215295bbcc376d6911217c3a98b96484331112ff98c7a2e979fb1904ac293008ac557114c6658c1deb9b2f441bd246764b507103d2560cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/results@npm:1.0.0, @expo/results@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@expo/results@npm:1.0.0"
|
||||
@@ -3555,8 +3406,8 @@ __metadata:
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:18.4.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/config-plugins": "npm:8.0.8"
|
||||
"@expo/config-types": "npm:52.0.1"
|
||||
"@expo/downloader": "npm:18.0.1"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/logger": "npm:18.0.1"
|
||||
@@ -5202,7 +5053,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5":
|
||||
"@jridgewell/gen-mapping@npm:^0.3.12":
|
||||
version: 0.3.13
|
||||
resolution: "@jridgewell/gen-mapping@npm:0.3.13"
|
||||
dependencies:
|
||||
@@ -5223,16 +5074,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/remapping@npm:^2.3.5":
|
||||
version: 2.3.5
|
||||
resolution: "@jridgewell/remapping@npm:2.3.5"
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.5"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.24"
|
||||
checksum: 10c0/3de494219ffeb2c5c38711d0d7bb128097edf91893090a2dbc8ee0b55d092bb7347b1fd0f478486c5eab010e855c73927b1666f2107516d472d24a73017d1194
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/resolve-uri@npm:^3.0.3":
|
||||
version: 3.0.7
|
||||
resolution: "@jridgewell/resolve-uri@npm:3.0.7"
|
||||
@@ -10998,10 +10839,10 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli@workspace:packages/eas-cli"
|
||||
dependencies:
|
||||
"@expo/apple-utils": "npm:2.1.16"
|
||||
"@expo/apple-utils": "npm:2.1.15"
|
||||
"@expo/code-signing-certificates": "npm:0.0.5"
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config": "npm:10.0.6"
|
||||
"@expo/config-plugins": "npm:9.0.12"
|
||||
"@expo/eas-build-job": "npm:18.4.0"
|
||||
"@expo/eas-json": "npm:18.4.0"
|
||||
"@expo/env": "npm:^1.0.0"
|
||||
@@ -11960,7 +11801,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-up@npm:^5.0.0":
|
||||
"find-up@npm:^5.0.0, find-up@npm:~5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "find-up@npm:5.0.0"
|
||||
dependencies:
|
||||
@@ -12432,13 +12273,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"getenv@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "getenv@npm:2.0.0"
|
||||
checksum: 10c0/397ff641dd70cd78e414430258651e9a2228d3c5553a8cf15ae7840f75d3f10dfcb83f668f84829e84ea665b0fce2f08a9eddda3c9dcd7faa2d3da1c182c1854
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"git-hooks-list@npm:^3.0.0":
|
||||
version: 3.2.0
|
||||
resolution: "git-hooks-list@npm:3.2.0"
|
||||
@@ -12543,6 +12377,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:7.1.6":
|
||||
version: 7.1.6
|
||||
resolution: "glob@npm:7.1.6"
|
||||
dependencies:
|
||||
fs.realpath: "npm:^1.0.0"
|
||||
inflight: "npm:^1.0.4"
|
||||
inherits: "npm:2"
|
||||
minimatch: "npm:^3.0.4"
|
||||
once: "npm:^1.3.0"
|
||||
path-is-absolute: "npm:^1.0.0"
|
||||
checksum: 10c0/2575cce9306ac534388db751f0aa3e78afedb6af8f3b529ac6b2354f66765545145dba8530abf7bff49fb399a047d3f9b6901c38ee4c9503f592960d9af67763
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2":
|
||||
version: 10.3.10
|
||||
resolution: "glob@npm:10.3.10"
|
||||
|
||||
Reference in new issue
Block a user