New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@expo/eas-json

Package Overview
Dependencies
Maintainers
24
Versions
157
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@expo/eas-json - npm Package Compare versions

Comparing version 0.22.1 to 0.22.2

build/DeprecatedConfig.types.d.ts

36

build/EasJsonReader.d.ts

@@ -1,34 +0,20 @@

import { Workflow } from '@expo/eas-build-job';
import { EasConfig } from './Config.types';
interface EasJson {
builds: {
android?: {
[key: string]: BuildProfilePreValidation;
};
ios?: {
[key: string]: BuildProfilePreValidation;
};
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson, RawBuildProfile } from './EasJson.types';
interface EasJsonPreValidation {
build: {
[profile: string]: object;
};
}
interface BuildProfilePreValidation {
workflow?: Workflow;
extends?: string;
}
export declare class EasJsonReader {
private projectDir;
private platform;
static formatEasJsonPath(projectDir: string): string;
constructor(projectDir: string, platform: 'android' | 'ios' | 'all');
/**
* Return build profile names for a particular platform.
* If platform is 'all', return common build profiles for all platforms
*/
constructor(projectDir: string);
getBuildProfileNamesAsync(): Promise<string[]>;
readAsync(buildProfileName: string): Promise<EasConfig>;
validateAsync(): Promise<void>;
readRawAsync(): Promise<EasJson>;
private validateBuildProfile;
readBuildProfileAsync<T extends Platform>(buildProfileName: string, platform: T): Promise<BuildProfile<T>>;
readAndValidateAsync(): Promise<EasJson>;
readRawAsync(): Promise<EasJsonPreValidation>;
private resolveBuildProfile;
private ensureProfileExists;
}
export declare function deepMerge(base: Record<string, any>, update: Record<string, any>): Record<string, any>;
export declare function profileMerge(base: RawBuildProfile, update: RawBuildProfile): RawBuildProfile;
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deepMerge = exports.EasJsonReader = void 0;
exports.profileMerge = exports.EasJsonReader = void 0;
const tslib_1 = require("tslib");

@@ -8,10 +8,11 @@ const eas_build_job_1 = require("@expo/eas-build-job");

const path_1 = tslib_1.__importDefault(require("path"));
const EasJson_types_1 = require("./EasJson.types");
const EasJsonSchema_1 = require("./EasJsonSchema");
function intersect(setA, setB) {
return new Set([...setA].filter(i => setB.has(i)));
}
const defaults = {
distribution: 'store',
credentialsSource: EasJson_types_1.CredentialsSource.REMOTE,
};
class EasJsonReader {
constructor(projectDir, platform) {
constructor(projectDir) {
this.projectDir = projectDir;
this.platform = platform;
}

@@ -21,63 +22,28 @@ static formatEasJsonPath(projectDir) {

}
/**
* Return build profile names for a particular platform.
* If platform is 'all', return common build profiles for all platforms
*/
async getBuildProfileNamesAsync() {
var _a, _b, _c, _d, _e, _f, _g, _h;
var _a;
const easJson = await this.readRawAsync();
if (this.platform === 'android') {
return Object.keys((_b = (_a = easJson === null || easJson === void 0 ? void 0 : easJson.builds) === null || _a === void 0 ? void 0 : _a.android) !== null && _b !== void 0 ? _b : {});
return Object.keys((_a = easJson === null || easJson === void 0 ? void 0 : easJson.build) !== null && _a !== void 0 ? _a : {});
}
async readBuildProfileAsync(buildProfileName, platform) {
const easJson = await this.readAndValidateAsync();
this.ensureProfileExists(easJson, buildProfileName);
const _a = this.resolveBuildProfile(easJson, buildProfileName), { android: resolvedAndroidSpecificValues, ios: resolvedIosSpecificValues } = _a, resolvedProfile = tslib_1.__rest(_a, ["android", "ios"]);
if (platform === eas_build_job_1.Platform.ANDROID) {
const profileWithoutDefaults = profileMerge(resolvedProfile, resolvedAndroidSpecificValues !== null && resolvedAndroidSpecificValues !== void 0 ? resolvedAndroidSpecificValues : {});
return profileMerge(defaults, profileWithoutDefaults);
}
else if (this.platform === 'ios') {
return Object.keys((_d = (_c = easJson === null || easJson === void 0 ? void 0 : easJson.builds) === null || _c === void 0 ? void 0 : _c.ios) !== null && _d !== void 0 ? _d : {});
else if (platform === eas_build_job_1.Platform.IOS) {
const profileWithoutDefaults = profileMerge(resolvedProfile, resolvedIosSpecificValues !== null && resolvedIosSpecificValues !== void 0 ? resolvedIosSpecificValues : {});
return profileMerge(defaults, profileWithoutDefaults);
}
else {
const intersectingProfileNames = intersect(new Set(Object.keys((_f = (_e = easJson === null || easJson === void 0 ? void 0 : easJson.builds) === null || _e === void 0 ? void 0 : _e.ios) !== null && _f !== void 0 ? _f : {})), new Set(Object.keys((_h = (_g = easJson === null || easJson === void 0 ? void 0 : easJson.builds) === null || _g === void 0 ? void 0 : _g.android) !== null && _h !== void 0 ? _h : {})));
return Array.from(intersectingProfileNames);
throw new Error(`Unknown platform ${platform}`);
}
}
async readAsync(buildProfileName) {
var _a, _b;
async readAndValidateAsync() {
const easJson = await this.readRawAsync();
let androidConfig;
if (['android', 'all'].includes(this.platform)) {
androidConfig = this.validateBuildProfile(eas_build_job_1.Platform.ANDROID, buildProfileName, ((_a = easJson.builds) === null || _a === void 0 ? void 0 : _a.android) || {});
}
let iosConfig;
if (['ios', 'all'].includes(this.platform)) {
iosConfig = this.validateBuildProfile(eas_build_job_1.Platform.IOS, buildProfileName, ((_b = easJson.builds) === null || _b === void 0 ? void 0 : _b.ios) || {});
}
return {
builds: Object.assign(Object.assign({}, (androidConfig ? { android: androidConfig } : {})), (iosConfig ? { ios: iosConfig } : {})),
};
}
async validateAsync() {
var _a, _b, _c, _d;
const easJson = await this.readRawAsync();
const androidProfiles = (_b = (_a = easJson.builds) === null || _a === void 0 ? void 0 : _a.android) !== null && _b !== void 0 ? _b : {};
for (const name of Object.keys(androidProfiles)) {
try {
this.validateBuildProfile(eas_build_job_1.Platform.ANDROID, name, androidProfiles);
}
catch (err) {
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
throw err;
}
}
const iosProfiles = (_d = (_c = easJson.builds) === null || _c === void 0 ? void 0 : _c.ios) !== null && _d !== void 0 ? _d : {};
for (const name of Object.keys(iosProfiles)) {
try {
this.validateBuildProfile(eas_build_job_1.Platform.IOS, name, iosProfiles);
}
catch (err) {
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
throw err;
}
}
}
async readRawAsync() {
const rawFile = await fs_extra_1.default.readFile(EasJsonReader.formatEasJsonPath(this.projectDir), 'utf8');
const json = JSON.parse(rawFile);
const { value, error } = EasJsonSchema_1.EasJsonSchema.validate(json, {
const { value, error } = EasJsonSchema_1.EasJsonSchema.validate(easJson, {
allowUnknown: false,
convert: true,
abortEarly: false,

@@ -90,26 +56,24 @@ });

}
validateBuildProfile(platform, buildProfileName, buildProfiles) {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
const schema = EasJsonSchema_1.schemaBuildProfileMap[platform];
const { value, error } = schema.validate(buildProfile, {
stripUnknown: true,
convert: true,
async readRawAsync() {
const rawFile = await fs_extra_1.default.readFile(EasJsonReader.formatEasJsonPath(this.projectDir), 'utf8');
const json = JSON.parse(rawFile);
const { value, error } = EasJsonSchema_1.MinimalEasJsonSchema.validate(json, {
abortEarly: false,
});
if (error) {
throw new Error(`Object "${platform}.${buildProfileName}" in eas.json is not valid [${error.toString()}]`);
throw new Error(`eas.json is not valid [${error.toString()}]`);
}
return value;
}
resolveBuildProfile(platform, buildProfileName, buildProfiles, depth = 0) {
resolveBuildProfile(easJson, profileName, depth = 0) {
if (depth >= 2) {
throw new Error('Too long chain of build profile extensions, make sure "extends" keys do not make a cycle');
}
const buildProfile = buildProfiles[buildProfileName];
const buildProfile = easJson.build[profileName];
if (!buildProfile) {
throw new Error(`There is no profile named ${buildProfileName} for platform ${platform}`);
throw new Error(`There is no profile named ${profileName}`);
}
const { extends: baseProfileName } = buildProfile, buildProfileRest = tslib_1.__rest(buildProfile, ["extends"]);
if (baseProfileName) {
return deepMerge(this.resolveBuildProfile(platform, baseProfileName, buildProfiles, depth + 1), buildProfileRest);
return profileMerge(this.resolveBuildProfile(easJson, baseProfileName, depth + 1), buildProfileRest);
}

@@ -120,30 +84,22 @@ else {

}
ensureProfileExists(easJson, profileName) {
if (!easJson.build || !easJson.build[profileName]) {
throw new Error(`There is no profile named ${profileName} in eas.json.`);
}
}
}
exports.EasJsonReader = EasJsonReader;
function isObject(value) {
return typeof value === 'object' && value !== null;
}
function deepMerge(base, update) {
const result = {};
Object.keys(base).forEach(key => {
const oldValue = base[key];
const newValue = update[key];
if (isObject(newValue) && isObject(oldValue)) {
result[key] = deepMerge(oldValue, newValue);
}
else if (newValue !== undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
}
else {
result[key] = isObject(oldValue) ? deepMerge({}, oldValue) : oldValue;
}
});
Object.keys(update).forEach(key => {
const newValue = update[key];
if (result[key] === undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
}
});
function profileMerge(base, update) {
const result = Object.assign(Object.assign({}, base), update);
if (base.env && update.env) {
result.env = Object.assign(Object.assign({}, base.env), update.env);
}
if (base.android && update.android) {
result.android = profileMerge(base.android, update.android);
}
if (base.ios && update.ios) {
result.ios = profileMerge(base.ios, update.ios);
}
return result;
}
exports.deepMerge = deepMerge;
exports.profileMerge = profileMerge;
/// <reference types="hapi__joi" />
import Joi from '@hapi/joi';
export declare const schemaBuildProfileMap: Record<string, Joi.Schema>;
export declare const MinimalEasJsonSchema: Joi.ObjectSchema<any>;
export declare const EasJsonSchema: Joi.ObjectSchema<any>;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EasJsonSchema = exports.schemaBuildProfileMap = void 0;
exports.EasJsonSchema = exports.MinimalEasJsonSchema = void 0;
const tslib_1 = require("tslib");
const eas_build_job_1 = require("@expo/eas-build-job");
const joi_1 = tslib_1.__importDefault(require("@hapi/joi"));
const semverSchemaCheck = (value, helpers) => {
const semverSchemaCheck = (value) => {
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {

@@ -15,71 +15,49 @@ return value;

};
const AndroidBuilderEnvironmentSchema = joi_1.default.object({
image: joi_1.default.string()
.valid(...eas_build_job_1.Android.builderBaseImages)
.default('default'),
const CacheSchema = joi_1.default.object({
disabled: joi_1.default.boolean(),
key: joi_1.default.string().max(128),
cacheDefaultPaths: joi_1.default.boolean(),
customPaths: joi_1.default.array().items(joi_1.default.string()),
});
const CommonBuildProfileSchema = joi_1.default.object({
credentialsSource: joi_1.default.string().valid('local', 'remote'),
distribution: joi_1.default.string().valid('store', 'internal'),
cache: CacheSchema,
releaseChannel: joi_1.default.string(),
channel: joi_1.default.string(),
developmentClient: joi_1.default.boolean(),
node: joi_1.default.string().empty(null).custom(semverSchemaCheck),
yarn: joi_1.default.string().empty(null).custom(semverSchemaCheck),
ndk: joi_1.default.string().empty(null).custom(semverSchemaCheck),
expoCli: joi_1.default.string().empty(null).custom(semverSchemaCheck),
env: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.string().empty(null)).default({}),
env: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.string().empty(null)),
});
const IosBuilderEnvironmentSchema = joi_1.default.object({
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(joi_1.default.object({
withoutCredentials: joi_1.default.boolean(),
image: joi_1.default.string().valid(...eas_build_job_1.Android.builderBaseImages),
ndk: joi_1.default.string().empty(null).custom(semverSchemaCheck),
artifactPath: joi_1.default.string(),
gradleCommand: joi_1.default.string(),
buildType: joi_1.default.string().valid('apk', 'app-bundle'),
}));
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(joi_1.default.object({
enterpriseProvisioning: joi_1.default.string().valid('adhoc', 'universal'),
autoIncrement: joi_1.default.alternatives().try(joi_1.default.boolean(), joi_1.default.string().valid('version', 'buildNumber')),
image: joi_1.default.string().valid(...eas_build_job_1.Ios.builderBaseImages),
node: joi_1.default.string().empty(null).custom(semverSchemaCheck),
yarn: joi_1.default.string().empty(null).custom(semverSchemaCheck),
bundler: joi_1.default.string().empty(null).custom(semverSchemaCheck),
fastlane: joi_1.default.string().empty(null).custom(semverSchemaCheck),
cocoapods: joi_1.default.string().empty(null).custom(semverSchemaCheck),
expoCli: joi_1.default.string().empty(null).custom(semverSchemaCheck),
env: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.string().empty(null)).default({}),
});
const CacheSchema = joi_1.default.object({
disabled: joi_1.default.boolean().default(false),
key: joi_1.default.string().max(128),
cacheDefaultPaths: joi_1.default.boolean().default(true),
customPaths: joi_1.default.array().items(joi_1.default.string()).default([]),
});
const AndroidSchema = joi_1.default.object({
workflow: joi_1.default.string(),
credentialsSource: joi_1.default.string().valid('local', 'remote').default('remote'),
releaseChannel: joi_1.default.string(),
channel: joi_1.default.string(),
distribution: joi_1.default.string().valid('store', 'internal').default('store'),
cache: CacheSchema.default(),
withoutCredentials: joi_1.default.boolean().default(false),
artifactPath: joi_1.default.string(),
gradleCommand: joi_1.default.string(),
buildType: joi_1.default.alternatives().conditional('distribution', {
is: 'internal',
then: joi_1.default.string().valid('apk', 'development-client'),
otherwise: joi_1.default.string().valid('apk', 'app-bundle', 'development-client'),
}),
}).concat(AndroidBuilderEnvironmentSchema);
const IosSchema = joi_1.default.object({
workflow: joi_1.default.string(),
credentialsSource: joi_1.default.string().valid('local', 'remote').default('remote'),
releaseChannel: joi_1.default.string(),
channel: joi_1.default.string(),
distribution: joi_1.default.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: joi_1.default.string().valid('adhoc', 'universal'),
autoIncrement: joi_1.default.alternatives()
.try(joi_1.default.boolean(), joi_1.default.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
artifactPath: joi_1.default.string(),
scheme: joi_1.default.string(),
schemeBuildConfiguration: joi_1.default.string(),
buildType: joi_1.default.string().valid('release', 'development-client'),
}).concat(IosBuilderEnvironmentSchema);
exports.schemaBuildProfileMap = {
android: AndroidSchema,
ios: IosSchema,
};
buildConfiguration: joi_1.default.string(),
}));
const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(joi_1.default.object({
extends: joi_1.default.string(),
android: AndroidBuildProfileSchema,
ios: IosBuildProfileSchema,
}));
exports.MinimalEasJsonSchema = joi_1.default.object({
build: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.object()),
});
exports.EasJsonSchema = joi_1.default.object({
builds: joi_1.default.object({
android: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.object({}).unknown(true) // profile is validated further only if build is for that platform
),
ios: joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.object({}).unknown(true) // profile is validated further only if build is for that platform
),
}),
build: joi_1.default.object().pattern(joi_1.default.string(), EasJsonBuildProfileSchema),
});

@@ -1,2 +0,3 @@

export { CredentialsSource, AndroidDistributionType, AndroidBuildProfile, BuildProfile, IosDistributionType, IosBuildProfile, IosEnterpriseProvisioning, DistributionType, EasConfig, VersionAutoIncrement, } from './Config.types';
export { AndroidBuildProfile, BuildProfile, CredentialsSource, DistributionType, EasJson, IosBuildProfile, IosEnterpriseProvisioning, VersionAutoIncrement, } from './EasJson.types';
export { EasJsonReader } from './EasJsonReader';
export { hasMismatchedExtendsAsync, isUsingDeprecatedFormatAsync, migrateAsync } from './migrate';
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EasJsonReader = exports.CredentialsSource = void 0;
var Config_types_1 = require("./Config.types");
Object.defineProperty(exports, "CredentialsSource", { enumerable: true, get: function () { return Config_types_1.CredentialsSource; } });
exports.migrateAsync = exports.isUsingDeprecatedFormatAsync = exports.hasMismatchedExtendsAsync = exports.EasJsonReader = exports.CredentialsSource = void 0;
var EasJson_types_1 = require("./EasJson.types");
Object.defineProperty(exports, "CredentialsSource", { enumerable: true, get: function () { return EasJson_types_1.CredentialsSource; } });
var EasJsonReader_1 = require("./EasJsonReader");
Object.defineProperty(exports, "EasJsonReader", { enumerable: true, get: function () { return EasJsonReader_1.EasJsonReader; } });
var migrate_1 = require("./migrate");
Object.defineProperty(exports, "hasMismatchedExtendsAsync", { enumerable: true, get: function () { return migrate_1.hasMismatchedExtendsAsync; } });
Object.defineProperty(exports, "isUsingDeprecatedFormatAsync", { enumerable: true, get: function () { return migrate_1.isUsingDeprecatedFormatAsync; } });
Object.defineProperty(exports, "migrateAsync", { enumerable: true, get: function () { return migrate_1.migrateAsync; } });
{
"name": "@expo/eas-json",
"description": "A library for interacting with the eas.json",
"version": "0.22.1",
"version": "0.22.2",
"author": "Expo <support@expo.io>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.44",
"@expo/eas-build-job": "0.2.45",
"@hapi/joi": "17.1.1",

@@ -42,3 +42,3 @@ "fs-extra": "9.0.1",

},
"gitHead": "47784ca6d36a9e5cd04d2154afeb827ef58d70c1"
"gitHead": "20b091fbfa45ef52a81b0ec2b885e6c98b71d7a7"
}

@@ -0,1 +1,2 @@

import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';

@@ -13,32 +14,33 @@ import { vol } from 'memfs';

test('minimal valid android eas.json', async () => {
test('minimal valid eas.json for both platforms', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {},
},
build: {
release: {},
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('release');
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync('release', Platform.IOS);
const androidProfile = await reader.readBuildProfileAsync('release', Platform.ANDROID);
expect({
builds: {
android: {
distribution: 'store',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
}).toEqual(easJson);
distribution: 'store',
credentialsSource: 'remote',
}).toEqual(androidProfile);
expect({
distribution: 'store',
credentialsSource: 'remote',
}).toEqual(iosProfile);
});
test('minimal valid ios eas.json', async () => {
test('valid eas.json for development client builds', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: {},
build: {
release: {},
debug: {
developmentClient: true,
android: {
withoutCredentials: true,
},
},

@@ -48,60 +50,46 @@ },

const reader = new EasJsonReader('/project', 'ios');
const easJson = await reader.readAsync('release');
const reader = new EasJsonReader('/project');
const iosProfile = await reader.readBuildProfileAsync('debug', Platform.IOS);
const androidProfile = await reader.readBuildProfileAsync('debug', Platform.ANDROID);
expect({
builds: {
ios: {
credentialsSource: 'remote',
distribution: 'store',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
},
},
}).toEqual(easJson);
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
withoutCredentials: true,
}).toEqual(androidProfile);
expect({
credentialsSource: 'remote',
distribution: 'store',
developmentClient: true,
}).toEqual(iosProfile);
});
test('minimal valid eas.json for both platforms', async () => {
test('valid profile for internal distribution on Android', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {},
build: {
internal: {
distribution: 'internal',
},
ios: {
release: {},
},
},
});
const reader = new EasJsonReader('/project', 'all');
const easJson = await reader.readAsync('release');
const reader = new EasJsonReader('/project');
const profile = await reader.readBuildProfileAsync('internal', Platform.ANDROID);
expect({
builds: {
android: {
distribution: 'store',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
ios: {
distribution: 'store',
credentialsSource: 'remote',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
},
},
}).toEqual(easJson);
distribution: 'internal',
credentialsSource: 'remote',
}).toEqual(profile);
});
test('valid eas.json with both platform, but reading only android', async () => {
test('valid profile extending other profile', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: {},
build: {
base: {
node: '12.0.0',
},
android: {
release: {},
extension: {
extends: 'base',
distribution: 'internal',
node: '13.0.0',
},

@@ -111,31 +99,37 @@ },

const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('release');
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
const extendedProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
expect({
builds: {
android: {
distribution: 'store',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
},
}).toEqual(easJson);
distribution: 'store',
credentialsSource: 'remote',
node: '12.0.0',
}).toEqual(baseProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
node: '13.0.0',
}).toEqual(extendedProfile);
});
test('valid eas.json for development client builds', async () => {
test('valid profile extending other profile with platform specific envs', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
ios: {
release: {},
debug: { buildType: 'development-client' },
build: {
base: {
env: {
BASE_ENV: '1',
PROFILE: 'base',
},
},
android: {
release: {},
debug: {
withoutCredentials: false,
buildType: 'development-client',
extension: {
extends: 'base',
distribution: 'internal',
env: {
PROFILE: 'extension',
},
android: {
env: {
PROFILE: 'extension:android',
},
},
},

@@ -145,129 +139,108 @@ },

const reader = new EasJsonReader('/project', 'all');
const easJson = await reader.readAsync('debug');
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
const extendedAndroidProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
const extendedIosProfile = await reader.readBuildProfileAsync('extension', Platform.IOS);
expect({
builds: {
android: {
credentialsSource: 'remote',
distribution: 'store',
env: {},
image: 'default',
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
ios: {
credentialsSource: 'remote',
distribution: 'store',
autoIncrement: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
buildType: 'development-client',
},
distribution: 'store',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'base',
},
}).toEqual(easJson);
});
test('valid generic profile for internal distribution on Android', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
internal: {
distribution: 'internal',
},
},
}).toEqual(baseProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'extension:android',
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('internal');
}).toEqual(extendedAndroidProfile);
expect({
builds: {
android: {
distribution: 'internal',
credentialsSource: 'remote',
env: {},
withoutCredentials: false,
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
distribution: 'internal',
credentialsSource: 'remote',
env: {
BASE_ENV: '1',
PROFILE: 'extension',
},
}).toEqual(easJson);
}).toEqual(extendedIosProfile);
});
test('valid managed profile for internal distribution on Android', async () => {
test('valid profile extending other profile with platform specific caching', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
internal: {
distribution: 'internal',
build: {
base: {
cache: {
disabled: true,
},
},
extension: {
extends: 'base',
distribution: 'internal',
cache: {
key: 'extend-key',
},
android: {
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
},
},
},
});
const reader = new EasJsonReader('/project', 'android');
const easJson = await reader.readAsync('internal');
const reader = new EasJsonReader('/project');
const baseProfile = await reader.readBuildProfileAsync('base', Platform.ANDROID);
const extendedAndroidProfile = await reader.readBuildProfileAsync('extension', Platform.ANDROID);
const extendedIosProfile = await reader.readBuildProfileAsync('extension', Platform.IOS);
expect({
builds: {
android: {
distribution: 'internal',
credentialsSource: 'remote',
withoutCredentials: false,
env: {},
cache: { disabled: false, cacheDefaultPaths: true, customPaths: [] },
image: 'default',
},
distribution: 'store',
credentialsSource: 'remote',
cache: {
disabled: true,
},
}).toEqual(easJson);
});
}).toEqual(baseProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
}).toEqual(extendedAndroidProfile);
expect({
distribution: 'internal',
credentialsSource: 'remote',
test('invalid managed profile for internal distribution on Android', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
internal: {
buildType: 'aab',
distribution: 'internal',
},
},
cache: {
key: 'extend-key',
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('internal');
await expect(promise).rejects.toThrowError(
'Object "android.internal" in eas.json is not valid [ValidationError: "buildType" must be one of [apk, development-client]]'
);
}).toEqual(extendedIosProfile);
});
test('invalid eas.json with missing preset', async () => {
test('valid eas.json with missing profile', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: {},
},
build: {
release: {},
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('debug');
await expect(promise).rejects.toThrowError(
'There is no profile named debug for platform android'
);
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('debug', Platform.ANDROID);
await expect(promise).rejects.toThrowError('There is no profile named debug in eas.json.');
});
test('invalid eas.json when using buildType for wrong platform', async () => {
test('invalid eas.json when using wrong buildType', async () => {
await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { buildType: 'archive' },
},
build: {
release: { android: { buildType: 'archive' } },
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
await expect(promise).rejects.toThrowError(
'Object "android.release" in eas.json is not valid [ValidationError: "buildType" must be one of [apk, app-bundle, development-client]]'
'eas.json is not valid [ValidationError: "build.release.android.buildType" must be one of [apk, app-bundle]]'
);

@@ -279,7 +252,5 @@ });

const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
await expect(promise).rejects.toThrowError(
'There is no profile named release for platform android'
);
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
await expect(promise).rejects.toThrowError('There is no profile named release in eas.json.');
});

@@ -289,13 +260,11 @@

await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { node: '12.0.0-alpha' },
},
build: {
release: { node: '12.0.0-alpha' },
},
});
const reader = new EasJsonReader('/project', 'android');
const promise = reader.readAsync('release');
const reader = new EasJsonReader('/project');
const promise = reader.readBuildProfileAsync('release', Platform.ANDROID);
await expect(promise).rejects.toThrowError(
'Object "android.release" in eas.json is not valid [ValidationError: "node" failed custom validation because 12.0.0-alpha is not a valid version]'
'eas.json is not valid [ValidationError: "build.release.node" failed custom validation because 12.0.0-alpha is not a valid version]'
);

@@ -306,25 +275,11 @@ });

await fs.writeJson('/project/eas.json', {
builds: {
android: {
release: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
ios: {
test: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
build: {
release: { node: '12.0.0-alpha' },
blah: { node: '12.0.0-alpha' },
},
});
const androidReader = new EasJsonReader('/project', 'android');
const androidProfileNames = await androidReader.getBuildProfileNamesAsync();
expect(androidProfileNames.sort()).toEqual(['release', 'blah'].sort());
const iosReader = new EasJsonReader('/project', 'ios');
const iosProfileNames = await iosReader.getBuildProfileNamesAsync();
expect(iosProfileNames.sort()).toEqual(['test', 'blah'].sort());
const allReader = new EasJsonReader('/project', 'all');
const allProfileNames = await allReader.getBuildProfileNamesAsync();
expect(allProfileNames.sort()).toEqual(['blah'].sort());
const reader = new EasJsonReader('/project');
const allProfileNames = await reader.getBuildProfileNamesAsync();
expect(allProfileNames.sort()).toEqual(['blah', 'release'].sort());
});

@@ -1,24 +0,17 @@

import { Platform, Workflow } from '@expo/eas-build-job';
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { AndroidBuildProfile, BuildProfile, EasConfig, IosBuildProfile } from './Config.types';
import { EasJsonSchema, schemaBuildProfileMap } from './EasJsonSchema';
import { BuildProfile, CredentialsSource, EasJson, RawBuildProfile } from './EasJson.types';
import { EasJsonSchema, MinimalEasJsonSchema } from './EasJsonSchema';
interface EasJson {
builds: {
android?: { [key: string]: BuildProfilePreValidation };
ios?: { [key: string]: BuildProfilePreValidation };
};
interface EasJsonPreValidation {
build: { [profile: string]: object };
}
interface BuildProfilePreValidation {
workflow?: Workflow;
extends?: string;
}
const defaults = {
distribution: 'store',
credentialsSource: CredentialsSource.REMOTE,
} as const;
function intersect<T>(setA: Set<T>, setB: Set<T>): Set<T> {
return new Set([...setA].filter(i => setB.has(i)));
}
export class EasJsonReader {

@@ -29,78 +22,53 @@ public static formatEasJsonPath(projectDir: string) {

constructor(private projectDir: string, private platform: 'android' | 'ios' | 'all') {}
constructor(private projectDir: string) {}
/**
* Return build profile names for a particular platform.
* If platform is 'all', return common build profiles for all platforms
*/
public async getBuildProfileNamesAsync(): Promise<string[]> {
const easJson = await this.readRawAsync();
if (this.platform === 'android') {
return Object.keys(easJson?.builds?.android ?? {});
} else if (this.platform === 'ios') {
return Object.keys(easJson?.builds?.ios ?? {});
} else {
const intersectingProfileNames = intersect(
new Set(Object.keys(easJson?.builds?.ios ?? {})),
new Set(Object.keys(easJson?.builds?.android ?? {}))
);
return Array.from(intersectingProfileNames);
}
return Object.keys(easJson?.build ?? {});
}
public async readAsync(buildProfileName: string): Promise<EasConfig> {
const easJson = await this.readRawAsync();
let androidConfig;
if (['android', 'all'].includes(this.platform)) {
androidConfig = this.validateBuildProfile<AndroidBuildProfile>(
Platform.ANDROID,
buildProfileName,
easJson.builds?.android || {}
public async readBuildProfileAsync<T extends Platform>(
buildProfileName: string,
platform: T
): Promise<BuildProfile<T>> {
const easJson = await this.readAndValidateAsync();
this.ensureProfileExists(easJson, buildProfileName);
const {
android: resolvedAndroidSpecificValues,
ios: resolvedIosSpecificValues,
...resolvedProfile
} = this.resolveBuildProfile(easJson, buildProfileName);
if (platform === Platform.ANDROID) {
const profileWithoutDefaults = profileMerge(
resolvedProfile,
resolvedAndroidSpecificValues ?? {}
);
return profileMerge(defaults, profileWithoutDefaults) as BuildProfile<T>;
} else if (platform === Platform.IOS) {
const profileWithoutDefaults = profileMerge(resolvedProfile, resolvedIosSpecificValues ?? {});
return profileMerge(defaults, profileWithoutDefaults) as BuildProfile<T>;
} else {
throw new Error(`Unknown platform ${platform}`);
}
let iosConfig;
if (['ios', 'all'].includes(this.platform)) {
iosConfig = this.validateBuildProfile<IosBuildProfile>(
Platform.IOS,
buildProfileName,
easJson.builds?.ios || {}
);
}
return {
builds: {
...(androidConfig ? { android: androidConfig } : {}),
...(iosConfig ? { ios: iosConfig } : {}),
},
};
}
public async validateAsync(): Promise<void> {
public async readAndValidateAsync(): Promise<EasJson> {
const easJson = await this.readRawAsync();
const { value, error } = EasJsonSchema.validate(easJson, {
allowUnknown: false,
convert: true,
abortEarly: false,
});
const androidProfiles = easJson.builds?.android ?? {};
for (const name of Object.keys(androidProfiles)) {
try {
this.validateBuildProfile(Platform.ANDROID, name, androidProfiles);
} catch (err) {
err.msg = `Failed to validate Android build profile "${name}"\n${err.msg}`;
throw err;
}
if (error) {
throw new Error(`eas.json is not valid [${error.toString()}]`);
}
const iosProfiles = easJson.builds?.ios ?? {};
for (const name of Object.keys(iosProfiles)) {
try {
this.validateBuildProfile(Platform.IOS, name, iosProfiles);
} catch (err) {
err.msg = `Failed to validate iOS build profile "${name}"\n${err.msg}`;
throw err;
}
}
return value as EasJson;
}
public async readRawAsync(): Promise<EasJson> {
public async readRawAsync(): Promise<EasJsonPreValidation> {
const rawFile = await fs.readFile(EasJsonReader.formatEasJsonPath(this.projectDir), 'utf8');
const json = JSON.parse(rawFile);
const { value, error } = EasJsonSchema.validate(json, {
const { value, error } = MinimalEasJsonSchema.validate(json, {
abortEarly: false,

@@ -115,29 +83,7 @@ });

private validateBuildProfile<T extends BuildProfile>(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>
): T {
const buildProfile = this.resolveBuildProfile(platform, buildProfileName, buildProfiles);
const schema = schemaBuildProfileMap[platform];
const { value, error } = schema.validate(buildProfile, {
stripUnknown: true,
convert: true,
abortEarly: false,
});
if (error) {
throw new Error(
`Object "${platform}.${buildProfileName}" in eas.json is not valid [${error.toString()}]`
);
}
return value;
}
private resolveBuildProfile(
platform: Platform,
buildProfileName: string,
buildProfiles: Record<string, BuildProfilePreValidation>,
easJson: EasJson,
profileName: string,
depth: number = 0
): Record<string, any> {
): RawBuildProfile {
if (depth >= 2) {

@@ -148,10 +94,10 @@ throw new Error(

}
const buildProfile = buildProfiles[buildProfileName];
const buildProfile = easJson.build[profileName];
if (!buildProfile) {
throw new Error(`There is no profile named ${buildProfileName} for platform ${platform}`);
throw new Error(`There is no profile named ${profileName}`);
}
const { extends: baseProfileName, ...buildProfileRest } = buildProfile;
if (baseProfileName) {
return deepMerge(
this.resolveBuildProfile(platform, baseProfileName, buildProfiles, depth + 1),
return profileMerge(
this.resolveBuildProfile(easJson, baseProfileName, depth + 1),
buildProfileRest

@@ -163,31 +109,28 @@ );

}
}
function isObject(value: any): boolean {
return typeof value === 'object' && value !== null;
private ensureProfileExists(easJson: EasJson, profileName: string) {
if (!easJson.build || !easJson.build[profileName]) {
throw new Error(`There is no profile named ${profileName} in eas.json.`);
}
}
}
export function deepMerge(
base: Record<string, any>,
update: Record<string, any>
): Record<string, any> {
const result: Record<string, any> = {};
Object.keys(base).forEach(key => {
const oldValue = base[key];
const newValue = update[key];
if (isObject(newValue) && isObject(oldValue)) {
result[key] = deepMerge(oldValue, newValue);
} else if (newValue !== undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
} else {
result[key] = isObject(oldValue) ? deepMerge({}, oldValue) : oldValue;
}
});
Object.keys(update).forEach(key => {
const newValue = update[key];
if (result[key] === undefined) {
result[key] = isObject(newValue) ? deepMerge({}, newValue) : newValue;
}
});
export function profileMerge(base: RawBuildProfile, update: RawBuildProfile): RawBuildProfile {
const result = {
...base,
...update,
};
if (base.env && update.env) {
result.env = {
...base.env,
...update.env,
};
}
if (base.android && update.android) {
result.android = profileMerge(base.android, update.android);
}
if (base.ios && update.ios) {
result.ios = profileMerge(base.ios, update.ios);
}
return result;
}
import { Android, Ios } from '@expo/eas-build-job';
import Joi, { CustomHelpers } from '@hapi/joi';
import Joi from '@hapi/joi';
const semverSchemaCheck = (value: any, helpers: CustomHelpers) => {
const semverSchemaCheck = (value: any) => {
if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(value)) {

@@ -12,85 +12,70 @@ return value;

const AndroidBuilderEnvironmentSchema = Joi.object({
image: Joi.string()
.valid(...Android.builderBaseImages)
.default('default'),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
const CacheSchema = Joi.object({
disabled: Joi.boolean(),
key: Joi.string().max(128),
cacheDefaultPaths: Joi.boolean(),
customPaths: Joi.array().items(Joi.string()),
});
const IosBuilderEnvironmentSchema = Joi.object({
image: Joi.string().valid(...Ios.builderBaseImages),
const CommonBuildProfileSchema = Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
cache: CacheSchema,
releaseChannel: Joi.string(),
channel: Joi.string(),
developmentClient: Joi.boolean(),
node: Joi.string().empty(null).custom(semverSchemaCheck),
yarn: Joi.string().empty(null).custom(semverSchemaCheck),
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
expoCli: Joi.string().empty(null).custom(semverSchemaCheck),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)).default({}),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
});
const CacheSchema = Joi.object({
disabled: Joi.boolean().default(false),
key: Joi.string().max(128),
cacheDefaultPaths: Joi.boolean().default(true),
customPaths: Joi.array().items(Joi.string()).default([]),
});
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
withoutCredentials: Joi.boolean(),
const AndroidSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema.default(),
withoutCredentials: Joi.boolean().default(false),
image: Joi.string().valid(...Android.builderBaseImages),
ndk: Joi.string().empty(null).custom(semverSchemaCheck),
artifactPath: Joi.string(),
gradleCommand: Joi.string(),
artifactPath: Joi.string(),
gradleCommand: Joi.string(),
buildType: Joi.alternatives().conditional('distribution', {
is: 'internal',
then: Joi.string().valid('apk', 'development-client'),
otherwise: Joi.string().valid('apk', 'app-bundle', 'development-client'),
}),
}).concat(AndroidBuilderEnvironmentSchema);
buildType: Joi.string().valid('apk', 'app-bundle'),
})
);
const IosSchema = Joi.object({
workflow: Joi.string(),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
releaseChannel: Joi.string(),
channel: Joi.string(),
distribution: Joi.string().valid('store', 'internal', 'simulator').default('store'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives()
.try(Joi.boolean(), Joi.string().valid('version', 'buildNumber'))
.default(false),
cache: CacheSchema.default(),
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('version', 'buildNumber')
),
artifactPath: Joi.string(),
scheme: Joi.string(),
schemeBuildConfiguration: Joi.string(),
image: Joi.string().valid(...Ios.builderBaseImages),
bundler: Joi.string().empty(null).custom(semverSchemaCheck),
fastlane: Joi.string().empty(null).custom(semverSchemaCheck),
cocoapods: Joi.string().empty(null).custom(semverSchemaCheck),
buildType: Joi.string().valid('release', 'development-client'),
}).concat(IosBuilderEnvironmentSchema);
artifactPath: Joi.string(),
scheme: Joi.string(),
buildConfiguration: Joi.string(),
})
);
export const schemaBuildProfileMap: Record<string, Joi.Schema> = {
android: AndroidSchema,
ios: IosSchema,
};
const EasJsonBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
extends: Joi.string(),
android: AndroidBuildProfileSchema,
ios: IosBuildProfileSchema,
})
);
export const MinimalEasJsonSchema = Joi.object({
build: Joi.object().pattern(Joi.string(), Joi.object()),
});
export const EasJsonSchema = Joi.object({
builds: Joi.object({
android: Joi.object().pattern(
Joi.string(),
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
ios: Joi.object().pattern(
Joi.string(),
Joi.object({}).unknown(true) // profile is validated further only if build is for that platform
),
}),
build: Joi.object().pattern(Joi.string(), EasJsonBuildProfileSchema),
});
export {
CredentialsSource,
AndroidDistributionType,
AndroidBuildProfile,
BuildProfile,
IosDistributionType,
CredentialsSource,
DistributionType,
EasJson,
IosBuildProfile,
IosEnterpriseProvisioning,
DistributionType,
EasConfig,
VersionAutoIncrement,
} from './Config.types';
} from './EasJson.types';
export { EasJsonReader } from './EasJsonReader';
export { hasMismatchedExtendsAsync, isUsingDeprecatedFormatAsync, migrateAsync } from './migrate';
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc