diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 61f4c4ba2106e..6e5e01d2a8a9a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,7 @@ /packages/@expo/metro-runtime @EvanBacon @bycedric @marklawlor /packages/@expo/package-manager @EvanBacon @bycedric /packages/@expo/plist @EvanBacon @bycedric +/packages/@expo/schemer @bycedric /packages/babel-preset-expo @brentvatne @ide @EvanBacon /packages/create-expo @EvanBacon @bycedric /packages/expo-* @alanjhughes diff --git a/packages/@expo/schemer/.eslintignore b/packages/@expo/schemer/.eslintignore new file mode 100644 index 0000000000000..567609b1234a9 --- /dev/null +++ b/packages/@expo/schemer/.eslintignore @@ -0,0 +1 @@ +build/ diff --git a/packages/@expo/schemer/.eslintrc.js b/packages/@expo/schemer/.eslintrc.js new file mode 100644 index 0000000000000..2720197860feb --- /dev/null +++ b/packages/@expo/schemer/.eslintrc.js @@ -0,0 +1,2 @@ +// @generated by expo-module-scripts +module.exports = require('expo-module-scripts/eslintrc.base.js'); diff --git a/packages/@expo/schemer/CHANGELOG.md b/packages/@expo/schemer/CHANGELOG.md new file mode 100644 index 0000000000000..c373391a8124f --- /dev/null +++ b/packages/@expo/schemer/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## Unpublished + +### šŸ›  Breaking changes + +### šŸŽ‰ New features + +### šŸ› Bug fixes + +### šŸ’” Others + +- Move package from `expo/expo-cli` to `expo/expo`. ([#25769](https://github.com/expo/expo/pull/25769) by [@byCedric](https://github.com/byCedric)) diff --git a/packages/@expo/schemer/LICENSE b/packages/@expo/schemer/LICENSE new file mode 100644 index 0000000000000..339cf3e66d689 --- /dev/null +++ b/packages/@expo/schemer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017-present, 650 Industries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@expo/schemer/README.md b/packages/@expo/schemer/README.md new file mode 100644 index 0000000000000..7631972403afe --- /dev/null +++ b/packages/@expo/schemer/README.md @@ -0,0 +1,103 @@ + +

+šŸ‘‹ Welcome to
@expo/schemer +

+ +

A Schema validation library for Expo.

+ + + +Details can be found here: +* https://paper.dropbox.com/doc/Expo-Schema-Validation-Library-mQU07rRejSnEe4Vf5dkcS + +## Usage + +### Usage with XDL + +```javascript +import { getConfig } from '@expo/config'; +import Schemer from '@expo/schemer'; + +const { exp } = getConfig(projectRoot); +const schema = await getSchemaAsync(exp.sdkVersion); +const validator = new Schemer(require('schema.json')); + +validator.validateName('Wilson Zhao'); +validator.validateAssets(exp); +``` + +### Schema-only validation + +```javascript +const validator = new Schemer(require('schema.json')); +try { + await validator.validateSchemaAsync(require('data.json')); +} catch (e) { + console.error(e); +} +``` + +### Validating a property + +```javascript +const validator = new Schemer(require('schema.json')); +await validator.validateName('Wilson Zhao'); +``` + +## Description + +Schemer takes in a custom JSON Schema and uses it to validate various data. + +Under the hood, it uses Ajv (https://github.com/epoberezkin/ajv) as the Javascript engine for basic schema validation. +However, each subschema also contains a custom meta tag, which can be parsed for further "manual" validation. As of now, Schemer supports manual validation for assets: + +```javascript +{ + meta: + { + asset, + contentType, //mime type + dimensions: {width, height}, + square, + + // For custom error messages and docs + regexHuman, + autogenerated, + notHuman + } +} +``` + +All errors can be accessed in `this.errors`, which has a getter function that combines Ajv JSON Schema errors with custom meta/asset validation errors into a unified array of `ValidationErrors`. +If they exist, the errors are thrown at the end of each public-facing function. + +All public-facing functions are async functions because asset validation has to be async (accessing the file-system or making a web request). + +## API + +#### new Schemer(Object JSON Schema, Object options) -> Object + +#### .validateSchemaAsync(Object data) -> Promise + +Returns a promise that resolve to `true` if the data is conforms to the schema. Otherwise, it rejects and throws an array of `ValidationError`s. + +#### .validateAssetsAsync(Object data) -> Promise + +Returns a promise that resolve to `true` if the data is conforms to the additional validation steps found in each meta tag. For example, it will download an asset and read the header of the file to see if it is a certain content type. +Otherwise, it rejects and throws an array of `ValidationError`s. + +#### .validateAll(Object data) -> Promise + +Runs both `.validateSchemaAsync` and `.validateAssetsAsync`. +Returns a promise that resolve to `true` if the data passes both functions. Otherwise, it rejects and throws an array of `ValidationError`s. + +#### .validateProperty(String fieldPath, Object data) -> Promise + +Extracts the subSchema for the given field path and validates the data against it. Also checks for the meta tag. +Returns a promise that resolve to `true` if the data conforms to the subschema. Otherwise, it rejects and throws an array of `ValidationError`s. + +#### .errors + +Contains an array of ValidationErrors + +#### new ValidationError({errorCode, fieldPath, message, data, meta}) -> Object diff --git a/packages/@expo/schemer/build/Error.d.ts b/packages/@expo/schemer/build/Error.d.ts new file mode 100644 index 0000000000000..8a8944b6ea93e --- /dev/null +++ b/packages/@expo/schemer/build/Error.d.ts @@ -0,0 +1,32 @@ +export declare class SchemerError extends Error { + readonly name = "SchemerError"; + errors: ValidationError[]; + constructor(errors: ValidationError[]); +} +export declare class ValidationError extends Error { + readonly name = "ValidationError"; + errorCode: string; + fieldPath: string; + data: any; + meta: any; + constructor({ errorCode, fieldPath, message, data, meta, }: { + errorCode: ErrorCode; + fieldPath: string; + message: string; + data: any; + meta: any; + }); +} +export type ErrorCode = keyof typeof ErrorCodes; +export declare const ErrorCodes: { + SCHEMA_VALIDATION_ERROR: string; + SCHEMA_ADDITIONAL_PROPERTY: string; + SCHEMA_MISSING_REQUIRED_PROPERTY: string; + SCHEMA_INVALID_PATTERN: string; + SCHEMA_INVALID_NOT: string; + INVALID_ASSET_URI: string; + INVALID_DIMENSIONS: string; + INVALID_CONTENT_TYPE: string; + NOT_SQUARE: string; + FILE_EXTENSION_MISMATCH: string; +}; diff --git a/packages/@expo/schemer/build/Error.js b/packages/@expo/schemer/build/Error.js new file mode 100644 index 0000000000000..8d8181dfe1492 --- /dev/null +++ b/packages/@expo/schemer/build/Error.js @@ -0,0 +1,41 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ErrorCodes = exports.ValidationError = exports.SchemerError = void 0; +class SchemerError extends Error { + name = 'SchemerError'; + errors; + constructor(errors) { + super(''); + this.message = errors.map((error) => error.message).join('\n'); + this.errors = errors; + } +} +exports.SchemerError = SchemerError; +class ValidationError extends Error { + name = 'ValidationError'; + errorCode; + fieldPath; + data; + meta; + constructor({ errorCode, fieldPath, message, data, meta, }) { + super(message); + this.errorCode = errorCode; + this.fieldPath = fieldPath; + this.data = data; + this.meta = meta; + } +} +exports.ValidationError = ValidationError; +exports.ErrorCodes = { + SCHEMA_VALIDATION_ERROR: 'SCHEMA_VALIDATION_ERROR', + SCHEMA_ADDITIONAL_PROPERTY: 'SCHEMA_ADDITIONAL_PROPERTY', + SCHEMA_MISSING_REQUIRED_PROPERTY: 'SCHEMA_MISSING_REQUIRED_PROPERTY', + SCHEMA_INVALID_PATTERN: 'SCHEMA_INVALID_PATTERN', + SCHEMA_INVALID_NOT: 'SCHEMA_INVALID_NOT', + INVALID_ASSET_URI: 'INVALID_ASSET_URI', + INVALID_DIMENSIONS: 'INVALID_DIMENSIONS', + INVALID_CONTENT_TYPE: 'INVALID_CONTENT_TYPE', + NOT_SQUARE: 'NOT_SQUARE', + FILE_EXTENSION_MISMATCH: 'FILE_EXTENSION_MISMATCH', +}; +//# sourceMappingURL=Error.js.map \ No newline at end of file diff --git a/packages/@expo/schemer/build/Error.js.map b/packages/@expo/schemer/build/Error.js.map new file mode 100644 index 0000000000000..6e82476773d84 --- /dev/null +++ b/packages/@expo/schemer/build/Error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Error.js","sourceRoot":"","sources":["../src/Error.ts"],"names":[],"mappings":";;;AAAA,MAAa,YAAa,SAAQ,KAAK;IAC5B,IAAI,GAAG,cAAc,CAAC;IAC/B,MAAM,CAAoB;IAE1B,YAAY,MAAyB;QACnC,KAAK,CAAC,EAAE,CAAC,CAAC;QACV,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AATD,oCASC;AAED,MAAa,eAAgB,SAAQ,KAAK;IAC/B,IAAI,GAAG,iBAAiB,CAAC;IAClC,SAAS,CAAS;IAClB,SAAS,CAAS;IAClB,IAAI,CAAM;IACV,IAAI,CAAM;IACV,YAAY,EACV,SAAS,EACT,SAAS,EACT,OAAO,EACP,IAAI,EACJ,IAAI,GAOL;QACC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAzBD,0CAyBC;AAIY,QAAA,UAAU,GAAG;IACxB,uBAAuB,EAAE,yBAAyB;IAClD,0BAA0B,EAAE,4BAA4B;IACxD,gCAAgC,EAAE,kCAAkC;IACpE,sBAAsB,EAAE,wBAAwB;IAChD,kBAAkB,EAAE,oBAAoB;IAExC,iBAAiB,EAAE,mBAAmB;IACtC,kBAAkB,EAAE,oBAAoB;IACxC,oBAAoB,EAAE,sBAAsB;IAC5C,UAAU,EAAE,YAAY;IACxB,uBAAuB,EAAE,yBAAyB;CACnD,CAAC"} \ No newline at end of file diff --git a/packages/@expo/schemer/build/Util.d.ts b/packages/@expo/schemer/build/Util.d.ts new file mode 100644 index 0000000000000..af7d0f3ae7cef --- /dev/null +++ b/packages/@expo/schemer/build/Util.d.ts @@ -0,0 +1,5 @@ +export declare const fieldPathToSchemaPath: (fieldPath: string) => string; +export declare const schemaPointerToFieldPath: (jsonPointer: string) => string; +export declare const fieldPathToSchema: (schema: object, fieldPath: string) => any; +export declare function pathToSegments(path: string | string[]): string[]; +export declare function get(object: any, path: string | string[]): any; diff --git a/packages/@expo/schemer/build/Util.js b/packages/@expo/schemer/build/Util.js new file mode 100644 index 0000000000000..25f4a6b9fd251 --- /dev/null +++ b/packages/@expo/schemer/build/Util.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.get = exports.pathToSegments = exports.fieldPathToSchema = exports.schemaPointerToFieldPath = exports.fieldPathToSchemaPath = void 0; +const fieldPathToSchemaPath = (fieldPath) => { + return pathToSegments(fieldPath) + .map((segment) => `properties.${segment}`) + .join('.'); +}; +exports.fieldPathToSchemaPath = fieldPathToSchemaPath; +// Assumption: used only for jsonPointer returned from traverse +const schemaPointerToFieldPath = (jsonPointer) => { + return jsonPointer + .split('/') + .slice(2) + .filter((error) => error !== 'properties') + .join('.'); +}; +exports.schemaPointerToFieldPath = schemaPointerToFieldPath; +const fieldPathToSchema = (schema, fieldPath) => { + return get(schema, (0, exports.fieldPathToSchemaPath)(fieldPath)); +}; +exports.fieldPathToSchema = fieldPathToSchema; +function pathToSegments(path) { + return Array.isArray(path) ? path : path.split('.'); +} +exports.pathToSegments = pathToSegments; +function get(object, path) { + const segments = pathToSegments(path); + const length = segments.length; + let index = 0; + while (object != null && index < length) { + object = object[segments[index++]]; + } + return index && index === length ? object : undefined; +} +exports.get = get; +//# sourceMappingURL=Util.js.map \ No newline at end of file diff --git a/packages/@expo/schemer/build/Util.js.map b/packages/@expo/schemer/build/Util.js.map new file mode 100644 index 0000000000000..0e9ff38da7b80 --- /dev/null +++ b/packages/@expo/schemer/build/Util.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Util.js","sourceRoot":"","sources":["../src/Util.ts"],"names":[],"mappings":";;;AAAO,MAAM,qBAAqB,GAAG,CAAC,SAAiB,EAAE,EAAE;IACzD,OAAO,cAAc,CAAC,SAAS,CAAC;SAC7B,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,OAAO,EAAE,CAAC;SACzC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC,CAAC;AAJW,QAAA,qBAAqB,yBAIhC;AACF,+DAA+D;AACxD,MAAM,wBAAwB,GAAG,CAAC,WAAmB,EAAE,EAAE;IAC9D,OAAO,WAAW;SACf,KAAK,CAAC,GAAG,CAAC;SACV,KAAK,CAAC,CAAC,CAAC;SACR,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,YAAY,CAAC;SACzC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC,CAAC;AANW,QAAA,wBAAwB,4BAMnC;AAEK,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAE,SAAiB,EAAE,EAAE;IACrE,OAAO,GAAG,CAAC,MAAM,EAAE,IAAA,6BAAqB,EAAC,SAAS,CAAC,CAAC,CAAC;AACvD,CAAC,CAAC;AAFW,QAAA,iBAAiB,qBAE5B;AAEF,SAAgB,cAAc,CAAC,IAAuB;IACpD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACtD,CAAC;AAFD,wCAEC;AAED,SAAgB,GAAG,CAAC,MAAW,EAAE,IAAuB;IACtD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,MAAM,IAAI,IAAI,IAAI,KAAK,GAAG,MAAM,EAAE;QACvC,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;KACpC;IAED,OAAO,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC;AAVD,kBAUC"} \ No newline at end of file diff --git a/packages/@expo/schemer/build/index.d.ts b/packages/@expo/schemer/build/index.d.ts new file mode 100644 index 0000000000000..c7ee5061a48fc --- /dev/null +++ b/packages/@expo/schemer/build/index.d.ts @@ -0,0 +1,44 @@ +import Ajv, { ErrorObject, Options } from 'ajv'; +import { ValidationError } from './Error'; +type Meta = { + asset?: boolean; + dimensions?: { + width: number; + height: number; + }; + square?: boolean; + contentTypePattern?: string; + contentTypeHuman?: string; +}; +type SchemerOptions = Options & { + rootDir?: string; +}; +type AssetField = { + fieldPath: string; + data: string; + meta: Meta; +}; +export { SchemerError, ValidationError, ErrorCodes, ErrorCode } from './Error'; +export default class Schemer { + options: SchemerOptions; + ajv: Ajv; + schema: object; + rootDir: string; + manualValidationErrors: ValidationError[]; + constructor(schema: object, options?: SchemerOptions); + _formatAjvErrorMessage({ keyword, instancePath, params, parentSchema, data, message, }: ErrorObject): ValidationError; + getErrors(): ValidationError[]; + _throwOnErrors(): void; + validateAll(data: any): Promise; + validateAssetsAsync(data: any): Promise; + validateSchemaAsync(data: any): Promise; + _validateSchemaAsync(data: any): void; + _validateAssetsAsync(data: any): Promise; + _validateImageAsync({ fieldPath, data, meta }: AssetField): Promise; + _validateAssetAsync({ fieldPath, data, meta }: AssetField): Promise; + validateProperty(fieldPath: string, data: any): Promise; + validateName(name: string): Promise; + validateSlug(slug: string): Promise; + validateSdkVersion(version: string): Promise; + validateIcon(iconPath: string): Promise; +} diff --git a/packages/@expo/schemer/build/index.js b/packages/@expo/schemer/build/index.js new file mode 100644 index 0000000000000..7d352b96d0554 --- /dev/null +++ b/packages/@expo/schemer/build/index.js @@ -0,0 +1,243 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ErrorCodes = exports.ValidationError = exports.SchemerError = void 0; +const ajv_1 = __importDefault(require("ajv")); +const ajv_formats_1 = __importDefault(require("ajv-formats")); +const fs_1 = __importDefault(require("fs")); +const json_schema_traverse_1 = __importDefault(require("json-schema-traverse")); +const path_1 = __importDefault(require("path")); +const probe_image_size_1 = __importDefault(require("probe-image-size")); +const Error_1 = require("./Error"); +const Util_1 = require("./Util"); +function lowerFirst(str) { + return str.charAt(0).toLowerCase() + str.slice(1); +} +var Error_2 = require("./Error"); +Object.defineProperty(exports, "SchemerError", { enumerable: true, get: function () { return Error_2.SchemerError; } }); +Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return Error_2.ValidationError; } }); +Object.defineProperty(exports, "ErrorCodes", { enumerable: true, get: function () { return Error_2.ErrorCodes; } }); +class Schemer { + options; + ajv; + schema; + rootDir; + manualValidationErrors; + // Schema is a JSON Schema object + constructor(schema, options = {}) { + this.options = { + allErrors: true, + verbose: true, + meta: true, + strict: false, + unicodeRegExp: false, + ...options, + }; + this.ajv = new ajv_1.default(this.options); + (0, ajv_formats_1.default)(this.ajv, { mode: 'full' }); + this.schema = schema; + this.rootDir = this.options.rootDir || __dirname; + this.manualValidationErrors = []; + } + _formatAjvErrorMessage({ keyword, instancePath, params, parentSchema, data, message, }) { + const meta = parentSchema && parentSchema.meta; + // This removes the "." in front of a fieldPath + instancePath = instancePath.slice(1); + switch (keyword) { + case 'additionalProperties': { + return new Error_1.ValidationError({ + errorCode: 'SCHEMA_ADDITIONAL_PROPERTY', + fieldPath: instancePath, + message: `should NOT have additional property '${params.additionalProperty}'`, + data, + meta, + }); + } + case 'required': + return new Error_1.ValidationError({ + errorCode: 'SCHEMA_MISSING_REQUIRED_PROPERTY', + fieldPath: instancePath, + message: `is missing required property '${params.missingProperty}'`, + data, + meta, + }); + case 'pattern': { + //@TODO Parse the message in a less hacky way. Perhaps for regex validation errors, embed the error message under the meta tag? + const regexHuman = meta?.regexHuman; + const regexErrorMessage = regexHuman + ? `'${instancePath}' should be a ${regexHuman[0].toLowerCase() + regexHuman.slice(1)}` + : `'${instancePath}' ${message}`; + return new Error_1.ValidationError({ + errorCode: 'SCHEMA_INVALID_PATTERN', + fieldPath: instancePath, + message: regexErrorMessage, + data, + meta, + }); + } + case 'not': { + const notHuman = meta?.notHuman; + const notHumanErrorMessage = notHuman + ? `'${instancePath}' should be ${notHuman[0].toLowerCase() + notHuman.slice(1)}` + : `'${instancePath}' ${message}`; + return new Error_1.ValidationError({ + errorCode: 'SCHEMA_INVALID_NOT', + fieldPath: instancePath, + message: notHumanErrorMessage, + data, + meta, + }); + } + default: + return new Error_1.ValidationError({ + errorCode: 'SCHEMA_VALIDATION_ERROR', + fieldPath: instancePath, + message: message || 'Validation error', + data, + meta, + }); + } + } + getErrors() { + // Convert AJV JSONSchema errors to our ValidationErrors + let valErrors = []; + if (this.ajv.errors) { + valErrors = this.ajv.errors.map((error) => this._formatAjvErrorMessage(error)); + } + return [...valErrors, ...this.manualValidationErrors]; + } + _throwOnErrors() { + // Clean error state after each validation + const errors = this.getErrors(); + if (errors.length > 0) { + this.manualValidationErrors = []; + this.ajv.errors = []; + throw new Error_1.SchemerError(errors); + } + } + async validateAll(data) { + await this._validateSchemaAsync(data); + await this._validateAssetsAsync(data); + this._throwOnErrors(); + } + async validateAssetsAsync(data) { + await this._validateAssetsAsync(data); + this._throwOnErrors(); + } + async validateSchemaAsync(data) { + await this._validateSchemaAsync(data); + this._throwOnErrors(); + } + _validateSchemaAsync(data) { + this.ajv.validate(this.schema, data); + } + async _validateAssetsAsync(data) { + const assets = []; + (0, json_schema_traverse_1.default)(this.schema, { allKeys: true }, (subSchema, jsonPointer, a, b, c, d, property) => { + if (property && subSchema.meta && subSchema.meta.asset) { + const fieldPath = (0, Util_1.schemaPointerToFieldPath)(jsonPointer); + assets.push({ + fieldPath, + data: (0, Util_1.get)(data, lowerFirst(fieldPath)) || (0, Util_1.get)(data, fieldPath), + meta: subSchema.meta, + }); + } + }); + await Promise.all(assets.map(this._validateAssetAsync.bind(this))); + } + async _validateImageAsync({ fieldPath, data, meta }) { + if (meta && meta.asset && data) { + const { dimensions, square, contentTypePattern } = meta; + // filePath could be an URL + const filePath = path_1.default.resolve(this.rootDir, data); + try { + // This cases on whether filePath is a remote URL or located on the machine + const isLocalFile = fs_1.default.existsSync(filePath); + const probeResult = isLocalFile + ? await (0, probe_image_size_1.default)(require('fs').createReadStream(filePath)) + : await (0, probe_image_size_1.default)(data); + if (!probeResult) { + return; + } + const { width, height, type, mime } = probeResult; + const fileExtension = filePath.split('.').pop(); + if (isLocalFile && mime !== `image/${fileExtension}`) { + this.manualValidationErrors.push(new Error_1.ValidationError({ + errorCode: 'FILE_EXTENSION_MISMATCH', + fieldPath, + message: `the file extension should match the content, but the file extension is .${fileExtension} while the file content at '${data}' is of type ${type}`, + data, + meta, + })); + } + if (contentTypePattern && !mime.match(new RegExp(contentTypePattern))) { + this.manualValidationErrors.push(new Error_1.ValidationError({ + errorCode: 'INVALID_CONTENT_TYPE', + fieldPath, + message: `field '${fieldPath}' should point to ${meta.contentTypeHuman} but the file at '${data}' has type ${type}`, + data, + meta, + })); + } + if (dimensions && (dimensions.height !== height || dimensions.width !== width)) { + this.manualValidationErrors.push(new Error_1.ValidationError({ + errorCode: 'INVALID_DIMENSIONS', + fieldPath, + message: `'${fieldPath}' should have dimensions ${dimensions.width}x${dimensions.height}, but the file at '${data}' has dimensions ${width}x${height}`, + data, + meta, + })); + } + if (square && width !== height) { + this.manualValidationErrors.push(new Error_1.ValidationError({ + errorCode: 'NOT_SQUARE', + fieldPath, + message: `image should be square, but the file at '${data}' has dimensions ${width}x${height}`, + data, + meta, + })); + } + } + catch { + this.manualValidationErrors.push(new Error_1.ValidationError({ + errorCode: 'INVALID_ASSET_URI', + fieldPath, + message: `cannot access file at '${data}'`, + data, + meta, + })); + } + } + } + async _validateAssetAsync({ fieldPath, data, meta }) { + if (meta && meta.asset && data) { + if (meta.contentTypePattern && meta.contentTypePattern.startsWith('^image')) { + await this._validateImageAsync({ fieldPath, data, meta }); + } + } + } + async validateProperty(fieldPath, data) { + const subSchema = (0, Util_1.fieldPathToSchema)(this.schema, fieldPath); + this.ajv.validate(subSchema, data); + if (subSchema.meta && subSchema.meta.asset) { + await this._validateAssetAsync({ fieldPath, data, meta: subSchema.meta }); + } + this._throwOnErrors(); + } + validateName(name) { + return this.validateProperty('name', name); + } + validateSlug(slug) { + return this.validateProperty('slug', slug); + } + validateSdkVersion(version) { + return this.validateProperty('sdkVersion', version); + } + validateIcon(iconPath) { + return this.validateProperty('icon', iconPath); + } +} +exports.default = Schemer; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/@expo/schemer/build/index.js.map b/packages/@expo/schemer/build/index.js.map new file mode 100644 index 0000000000000..bed0d4e534f0e --- /dev/null +++ b/packages/@expo/schemer/build/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,8CAAgD;AAChD,8DAAqC;AACrC,4CAAoB;AACpB,gFAA4C;AAC5C,gDAAwB;AACxB,wEAA0C;AAE1C,mCAAwD;AACxD,iCAA0E;AAE1E,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC;AAmBD,iCAA+E;AAAtE,qGAAA,YAAY,OAAA;AAAE,wGAAA,eAAe,OAAA;AAAE,mGAAA,UAAU,OAAA;AAClD,MAAqB,OAAO;IAC1B,OAAO,CAAiB;IACxB,GAAG,CAAM;IACT,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,sBAAsB,CAAoB;IAC1C,iCAAiC;IACjC,YAAY,MAAc,EAAE,UAA0B,EAAE;QACtD,IAAI,CAAC,OAAO,GAAG;YACb,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,KAAK;YACb,aAAa,EAAE,KAAK;YACpB,GAAG,OAAO;SACX,CAAC;QAEF,IAAI,CAAC,GAAG,GAAG,IAAI,aAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,IAAA,qBAAU,EAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC;QACjD,IAAI,CAAC,sBAAsB,GAAG,EAAE,CAAC;IACnC,CAAC;IAED,sBAAsB,CAAC,EACrB,OAAO,EACP,YAAY,EACZ,MAAM,EACN,YAAY,EACZ,IAAI,EACJ,OAAO,GACK;QACZ,MAAM,IAAI,GAAG,YAAY,IAAK,YAAoB,CAAC,IAAI,CAAC;QACxD,+CAA+C;QAC/C,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACrC,QAAQ,OAAO,EAAE;YACf,KAAK,sBAAsB,CAAC,CAAC;gBAC3B,OAAO,IAAI,uBAAe,CAAC;oBACzB,SAAS,EAAE,4BAA4B;oBACvC,SAAS,EAAE,YAAY;oBACvB,OAAO,EAAE,wCAAyC,MAAc,CAAC,kBAAkB,GAAG;oBACtF,IAAI;oBACJ,IAAI;iBACL,CAAC,CAAC;aACJ;YACD,KAAK,UAAU;gBACb,OAAO,IAAI,uBAAe,CAAC;oBACzB,SAAS,EAAE,kCAAkC;oBAC7C,SAAS,EAAE,YAAY;oBACvB,OAAO,EAAE,iCAAkC,MAAc,CAAC,eAAe,GAAG;oBAC5E,IAAI;oBACJ,IAAI;iBACL,CAAC,CAAC;YACL,KAAK,SAAS,CAAC,CAAC;gBACd,+HAA+H;gBAC/H,MAAM,UAAU,GAAG,IAAI,EAAE,UAAU,CAAC;gBACpC,MAAM,iBAAiB,GAAG,UAAU;oBAClC,CAAC,CAAC,IAAI,YAAY,iBAAiB,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;oBACtF,CAAC,CAAC,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;gBACnC,OAAO,IAAI,uBAAe,CAAC;oBACzB,SAAS,EAAE,wBAAwB;oBACnC,SAAS,EAAE,YAAY;oBACvB,OAAO,EAAE,iBAAiB;oBAC1B,IAAI;oBACJ,IAAI;iBACL,CAAC,CAAC;aACJ;YACD,KAAK,KAAK,CAAC,CAAC;gBACV,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,CAAC;gBAChC,MAAM,oBAAoB,GAAG,QAAQ;oBACnC,CAAC,CAAC,IAAI,YAAY,eAAe,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;oBAChF,CAAC,CAAC,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;gBACnC,OAAO,IAAI,uBAAe,CAAC;oBACzB,SAAS,EAAE,oBAAoB;oBAC/B,SAAS,EAAE,YAAY;oBACvB,OAAO,EAAE,oBAAoB;oBAC7B,IAAI;oBACJ,IAAI;iBACL,CAAC,CAAC;aACJ;YACD;gBACE,OAAO,IAAI,uBAAe,CAAC;oBACzB,SAAS,EAAE,yBAAyB;oBACpC,SAAS,EAAE,YAAY;oBACvB,OAAO,EAAE,OAAO,IAAI,kBAAkB;oBACtC,IAAI;oBACJ,IAAI;iBACL,CAAC,CAAC;SACN;IACH,CAAC;IAED,SAAS;QACP,wDAAwD;QACxD,IAAI,SAAS,GAAsB,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE;YACnB,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;SAChF;QACD,OAAO,CAAC,GAAG,SAAS,EAAE,GAAG,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACxD,CAAC;IAED,cAAc;QACZ,0CAA0C;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;YACrB,IAAI,CAAC,sBAAsB,GAAG,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC;YACrB,MAAM,IAAI,oBAAY,CAAC,MAAM,CAAC,CAAC;SAChC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAS;QACzB,MAAM,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,IAAS;QACjC,MAAM,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,IAAS;QACjC,MAAM,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,oBAAoB,CAAC,IAAS;QAC5B,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,IAAS;QAClC,MAAM,MAAM,GAAiB,EAAE,CAAC;QAChC,IAAA,8BAAQ,EAAC,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE;YACxF,IAAI,QAAQ,IAAI,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE;gBACtD,MAAM,SAAS,GAAG,IAAA,+BAAwB,EAAC,WAAW,CAAC,CAAC;gBACxD,MAAM,CAAC,IAAI,CAAC;oBACV,SAAS;oBACT,IAAI,EAAE,IAAA,UAAG,EAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,IAAI,IAAA,UAAG,EAAC,IAAI,EAAE,SAAS,CAAC;oBAC9D,IAAI,EAAE,SAAS,CAAC,IAAI;iBACrB,CAAC,CAAC;aACJ;QACH,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAc;QAC7D,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,EAAE;YAC9B,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,kBAAkB,EAAE,GAAS,IAAI,CAAC;YAC9D,2BAA2B;YAC3B,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAClD,IAAI;gBACF,4EAA4E;gBAC5E,MAAM,WAAW,GAAG,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;gBAC5C,MAAM,WAAW,GAAG,WAAW;oBAC7B,CAAC,CAAC,MAAM,IAAA,0BAAU,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;oBAC5D,CAAC,CAAC,MAAM,IAAA,0BAAU,EAAC,IAAI,CAAC,CAAC;gBAE3B,IAAI,CAAC,WAAW,EAAE;oBAChB,OAAO;iBACR;gBAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC;gBAElD,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;gBAEhD,IAAI,WAAW,IAAI,IAAI,KAAK,SAAS,aAAa,EAAE,EAAE;oBACpD,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAC9B,IAAI,uBAAe,CAAC;wBAClB,SAAS,EAAE,yBAAyB;wBACpC,SAAS;wBACT,OAAO,EAAE,2EAA2E,aAAa,+BAA+B,IAAI,gBAAgB,IAAI,EAAE;wBAC1J,IAAI;wBACJ,IAAI;qBACL,CAAC,CACH,CAAC;iBACH;gBAED,IAAI,kBAAkB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE;oBACrE,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAC9B,IAAI,uBAAe,CAAC;wBAClB,SAAS,EAAE,sBAAsB;wBACjC,SAAS;wBACT,OAAO,EAAE,UAAU,SAAS,qBAAqB,IAAI,CAAC,gBAAgB,qBAAqB,IAAI,cAAc,IAAI,EAAE;wBACnH,IAAI;wBACJ,IAAI;qBACL,CAAC,CACH,CAAC;iBACH;gBAED,IAAI,UAAU,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,MAAM,IAAI,UAAU,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE;oBAC9E,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAC9B,IAAI,uBAAe,CAAC;wBAClB,SAAS,EAAE,oBAAoB;wBAC/B,SAAS;wBACT,OAAO,EAAE,IAAI,SAAS,4BAA4B,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,MAAM,sBAAsB,IAAI,oBAAoB,KAAK,IAAI,MAAM,EAAE;wBACtJ,IAAI;wBACJ,IAAI;qBACL,CAAC,CACH,CAAC;iBACH;gBAED,IAAI,MAAM,IAAI,KAAK,KAAK,MAAM,EAAE;oBAC9B,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAC9B,IAAI,uBAAe,CAAC;wBAClB,SAAS,EAAE,YAAY;wBACvB,SAAS;wBACT,OAAO,EAAE,4CAA4C,IAAI,oBAAoB,KAAK,IAAI,MAAM,EAAE;wBAC9F,IAAI;wBACJ,IAAI;qBACL,CAAC,CACH,CAAC;iBACH;aACF;YAAC,MAAM;gBACN,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAC9B,IAAI,uBAAe,CAAC;oBAClB,SAAS,EAAE,mBAAmB;oBAC9B,SAAS;oBACT,OAAO,EAAE,0BAA0B,IAAI,GAAG;oBAC1C,IAAI;oBACJ,IAAI;iBACL,CAAC,CACH,CAAC;aACH;SACF;IACH,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAc;QAC7D,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,EAAE;YAC9B,IAAI,IAAI,CAAC,kBAAkB,IAAI,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;gBAC3E,MAAM,IAAI,CAAC,mBAAmB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;aAC3D;SACF;IACH,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,SAAiB,EAAE,IAAS;QACjD,MAAM,SAAS,GAAG,IAAA,wBAAiB,EAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC5D,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAEnC,IAAI,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE;YAC1C,MAAM,IAAI,CAAC,mBAAmB,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;SAC3E;QACD,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,YAAY,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,YAAY,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,kBAAkB,CAAC,OAAe;QAChC,OAAO,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IAED,YAAY,CAAC,QAAgB;QAC3B,OAAO,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAC;CACF;AAnQD,0BAmQC"} \ No newline at end of file diff --git a/packages/@expo/schemer/e2e/__tests__/network-test.ts b/packages/@expo/schemer/e2e/__tests__/network-test.ts new file mode 100644 index 0000000000000..7c66920b16799 --- /dev/null +++ b/packages/@expo/schemer/e2e/__tests__/network-test.ts @@ -0,0 +1,46 @@ +import Schemer from '../..'; + +it('validates corrent icon hosted remotely', async () => { + const schema = require('../fixtures/schema.json').schema; + const validator = new Schemer(schema, { rootDir: './__tests__' }); + + await expect( + validator.validateIcon( + 'https://upload.wikimedia.org/wikipedia/commons/0/0f/Icon_Pinguin_2_512x512.png' + ) + ).resolves.toBeUndefined(); +}); + +it('validates correct asset dimensions hosted remotely', async () => { + const validator = new Schemer({ + properties: { + icon: { + meta: { + asset: true, + contentTypePattern: '^image/png$', + dimensions: { width: 100, height: 100 }, + }, + }, + }, + }); + + await expect(validator.validateIcon('https://httpbin.org/image/png')).resolves.toBeUndefined(); +}); + +it('validates incorrect asset dimensions hosted remotely', async () => { + const validator = new Schemer({ + properties: { + icon: { + meta: { + asset: true, + contentTypePattern: '^image/png$', + dimensions: { width: 101, height: 100 }, + }, + }, + }, + }); + + await expect(validator.validateIcon('https://httpbin.org/image/png')).rejects.toThrowError( + `'icon' should have dimensions 101x100, but the file at 'https://httpbin.org/image/png' has dimensions 100x100` + ); +}); diff --git a/packages/@expo/schemer/e2e/fixtures/schema.json b/packages/@expo/schemer/e2e/fixtures/schema.json new file mode 100644 index 0000000000000..6a300303f80d1 --- /dev/null +++ b/packages/@expo/schemer/e2e/fixtures/schema.json @@ -0,0 +1,1371 @@ +{ + "schema": { + "definitions": { + "Android": { + "description": "Configuration that is specific to the Android platform.", + "type": "object", + "meta": { + "standaloneOnly": true + }, + "properties": { + "publishManifestPath": { + "description": "The manifest for the Android version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "publishBundlePath": { + "description": "The bundle for the Android version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "package": { + "description": "The package name for your Android standalone app. You make it up, but it needs to be unique on the Play Store. See [this StackOverflow question](http://stackoverflow.com/questions/6273892/android-package-name-convention).", + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9\\_]*(\\.[a-zA-Z][a-zA-Z0-9\\_]*)+$", + "meta": { + "regexHuman": "Reverse DNS notation unique name for your app. Valid Android Application ID. For example, `com.example.app`, where `com.example` is our domain and `app` is our app. The name may only contain lowercase and uppercase letters (a-z, A-Z), numbers (0-9) and underscores (_), separated by periods (.). Each component of the name should start with a lowercase letter.", + "bareWorkflow": "This is set in `android/app/build.gradle` as `applicationId` as well as in your `AndroidManifest.xml` file (multiple places)." + } + }, + "versionCode": { + "description": "Version number required by Google Play. Increment by one for each release. Must be a positive integer. [Learn more](https://developer.android.com/studio/publish/versioning.html)", + "type": "integer", + "minimum": 0, + "maximum": 2100000000, + "meta": { + "bareWorkflow": "This is set in `android/app/build.gradle` as `versionCode`" + } + }, + "backgroundColor": { + "description": "The background color for your Android app, behind any of your React views. Overrides the top-level `backgroundColor` key if it is present.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`", + "bareWorkflow": "This is set in `android/app/src/main/AndroidManifest.xml` under `android:windowBackground`" + } + }, + "userInterfaceStyle": { + "description": "Configuration to force the app to always use the light or dark user-interface appearance, such as \"dark mode\", or make it automatically adapt to the system preferences. If not provided, defaults to `light`. Requires `expo-system-ui` be installed in your project to work on Android.", + "type": "string", + "fallback": "light", + "enum": ["light", "dark", "automatic"] + }, + "useNextNotificationsApi": { + "deprecated": true, + "description": "@deprecated A Boolean value that indicates whether the app should use the new notifications API.", + "type": "boolean", + "fallback": false + }, + "icon": { + "description": "Local path or remote URL to an image to use for your app's icon on Android. If specified, this overrides the top-level `icon` key. We recommend that you use a 1024x1024 png file (transparency is recommended for the Google Play Store). This icon will appear on the home screen and within the Expo app.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "adaptiveIcon": { + "description": "Settings for an Adaptive Launcher Icon on Android. [Learn more](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)", + "type": "object", + "properties": { + "foregroundImage": { + "description": "Local path or remote URL to an image to use for your app's icon on Android. If specified, this overrides the top-level `icon` and the `android.icon` keys. Should follow the [specified guidelines](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive). This icon will appear on the home screen.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "backgroundImage": { + "description": "Local path or remote URL to a background image for your app's Adaptive Icon on Android. If specified, this overrides the `backgroundColor` key. Must have the same dimensions as foregroundImage`, and has no effect if `foregroundImage` is not specified. Should follow the [specified guidelines](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive).", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "backgroundColor": { + "description": "Color to use as the background for your app's Adaptive Icon on Android. Defaults to white, `#FFFFFF`. Has no effect if `foregroundImage` is not specified.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + } + }, + "additionalProperties": false + }, + "playStoreUrl": { + "description": "URL to your app on the Google Play Store, if you have deployed it there. This is used to link to your store page from your Expo project page if your app is public.", + "pattern": "^https://play\\.google\\.com/", + "example": "https://play.google.com/store/apps/details?id=host.exp.exponent", + "type": ["string"] + }, + "permissions": { + "description": "List of permissions used by the standalone app. \n\n To use ONLY the following minimum necessary permissions and none of the extras supported by Expo in a default managed app, set `permissions` to `[]`. The minimum necessary permissions do not require a Privacy Policy when uploading to Google Play Store and are: \nā€¢ receive data from Internet \nā€¢ view network connections \nā€¢ full network access \nā€¢ change your audio settings \nā€¢ prevent device from sleeping \n\n To use ALL permissions supported by Expo by default, do not specify the `permissions` key. \n\n To use the minimum necessary permissions ALONG with certain additional permissions, specify those extras in `permissions`, e.g.\n\n `[ \"CAMERA\", \"ACCESS_FINE_LOCATION\" ]`.\n\n You can specify the following permissions depending on what you need:\n\n- `ACCESS_COARSE_LOCATION`\n- `ACCESS_FINE_LOCATION`\n- `ACCESS_BACKGROUND_LOCATION`\n- `CAMERA`\n- `RECORD_AUDIO`\n- `READ_CONTACTS`\n- `WRITE_CONTACTS`\n- `READ_CALENDAR`\n- `WRITE_CALENDAR`\n- `READ_EXTERNAL_STORAGE`\n- `WRITE_EXTERNAL_STORAGE`\n- `USE_FINGERPRINT`\n- `USE_BIOMETRIC`\n- `WRITE_SETTINGS`\n- `VIBRATE`\n- `READ_PHONE_STATE`\n- `FOREGROUND_SERVICE`\n- `WAKE_LOCK`\n- `com.anddoes.launcher.permission.UPDATE_COUNT`\n- `com.android.launcher.permission.INSTALL_SHORTCUT`\n- `com.google.android.c2dm.permission.RECEIVE`\n- `com.google.android.gms.permission.ACTIVITY_RECOGNITION`\n- `com.google.android.providers.gsf.permission.READ_GSERVICES`\n- `com.htc.launcher.permission.READ_SETTINGS`\n- `com.htc.launcher.permission.UPDATE_SHORTCUT`\n- `com.majeur.launcher.permission.UPDATE_BADGE`\n- `com.sec.android.provider.badge.permission.READ`\n- `com.sec.android.provider.badge.permission.WRITE`\n- `com.sonyericsson.home.permission.BROADCAST_BADGE`\n", + "type": "array", + "meta": { + "bareWorkflow": "To change the permissions your app requests, you'll need to edit `AndroidManifest.xml` manually. To prevent your app from requesting one of the permissions listed below, you'll need to explicitly add it to `AndroidManifest.xml` along with a `tools:node=\"remove\"` tag." + }, + "items": { + "type": "string" + } + }, + "googleServicesFile": { + "description": "[Firebase Configuration File](https://support.google.com/firebase/answer/7015592) Location of the `GoogleService-Info.plist` file for configuring Firebase. Including this key automatically enables FCM in your standalone app.", + "type": "string", + "meta": { + "bareWorkflow": "Add or edit the file directly at `android/app/google-services.json`" + } + }, + "config": { + "type": "object", + "description": "Note: This property key is not included in the production manifest and will evaluate to `undefined`. It is used internally only in the build process, because it contains API keys that some may want to keep private.", + "properties": { + "branch": { + "description": "[Branch](https://branch.io/) key to hook up Branch linking services.", + "type": "object", + "properties": { + "apiKey": { + "description": "Your Branch API key", + "type": "string" + } + }, + "additionalProperties": false + }, + "googleMaps": { + "description": "[Google Maps Android SDK](https://developers.google.com/maps/documentation/android-api/signup) configuration for your standalone app.", + "type": "object", + "properties": { + "apiKey": { + "description": "Your Google Maps Android SDK API key", + "type": "string" + } + }, + "additionalProperties": false + }, + "googleMobileAdsAppId": { + "description": "[Google Mobile Ads App ID](https://support.google.com/admob/answer/6232340) Google AdMob App ID. ", + "type": "string" + }, + "googleMobileAdsAutoInit": { + "description": "A boolean indicating whether to initialize Google App Measurement and begin sending user-level event data to Google immediately when the app starts. The default in Expo (Client and in standalone apps) is `false`. [Sets the opposite of the given value to the following key in `Info.plist`](https://developers.google.com/admob/ios/eu-consent#delay_app_measurement_optional)", + "type": "boolean", + "fallback": false + }, + "googleSignIn": { + "deprecated": true, + "meta": { + "deprecated": true + }, + "description": "@deprecated Use `googleServicesFile` instead. [Google Sign-In Android SDK](https://developers.google.com/identity/sign-in/android/start-integrating) keys for your standalone app.", + "type": "object", + "properties": { + "apiKey": { + "description": "The Android API key. Can be found in the credentials section of the developer console or in `google-services.json`.", + "type": "string" + }, + "certificateHash": { + "description": "The SHA-1 hash of the signing certificate used to build the APK without any separator (`:`). Can be found in `google-services.json`. https://developers.google.com/android/guides/client-auth", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "splash": { + "description": "Configuration for loading and splash screen for managed and standalone Android apps.", + "type": "object", + "properties": { + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover`, `contain` or `native`, defaults to `contain`.", + "enum": ["cover", "contain", "native"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "mdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Natural sized image (baseline)`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "hdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 1.5x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "xhdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 2x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "xxhdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 3x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "xxxhdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 4x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + } + }, + "intentFilters": { + "description": "Configuration for setting an array of custom intent filters in Android manifest. [Learn more](https://developer.android.com/guide/components/intents-filters)", + "example": [ + { + "autoVerify": true, + "action": "VIEW", + "data": { + "scheme": "https", + "host": "*.example.com" + }, + "category": ["BROWSABLE", "DEFAULT"] + } + ], + "exampleString": "\n [{ \n \"autoVerify\": true, \n \"action\": \"VIEW\", \n \"data\": { \n \"scheme\": \"https\", \n \"host\": \"*.example.com\" \n }, \n \"category\": [\"BROWSABLE\", \"DEFAULT\"] \n }]", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "autoVerify": { + "description": "You may also use an intent filter to set your app as the default handler for links (without showing the user a dialog with options). To do so use `true` and then configure your server to serve a JSON file verifying that you own the domain. [Learn more](https://developer.android.com/training/app-links)", + "type": "boolean" + }, + "action": { "type": "string" }, + "data": { + "anyOf": [ + { "$ref": "#/definitions/AndroidIntentFiltersData" }, + { + "type": ["array"], + "items": { + "$ref": "#/definitions/AndroidIntentFiltersData" + } + } + ] + }, + "category": { + "anyOf": [ + { + "type": ["string"] + }, + { + "type": "array", + "items": { "type": "string" } + } + ] + } + }, + "additionalProperties": false, + "required": ["action"] + }, + "meta": { + "bareWorkflow": "This is set in `AndroidManifest.xml` directly. [Learn more.](https://developer.android.com/guide/components/intents-filters)" + } + }, + "allowBackup": { + "description": "Allows your user's app data to be automatically backed up to their Google Drive. If this is set to false, no backup or restore of the application will ever be performed (this is useful if your app deals with sensitive information). Defaults to the Android default, which is `true`.", + "fallback": true, + "type": "boolean" + }, + "softwareKeyboardLayoutMode": { + "description": "Determines how the software keyboard will impact the layout of your application. This maps to the `android:windowSoftInputMode` property. Defaults to `resize`. Valid values: `resize`, `pan`.", + "enum": ["resize", "pan"], + "type": "string", + "fallback": "resize" + }, + "jsEngine": { + "description": "Specifies the JavaScript engine for Android apps. Supported only on EAS Build and in Expo Go. Defaults to `jsc`. Valid values: `hermes`, `jsc`.", + "type": "string", + "fallback": "jsc", + "enum": ["hermes", "jsc"], + "meta": { + "bareWorkflow": "To change the JavaScript engine, update the `expo.jsEngine` value in `android/gradle.properties`" + } + }, + "runtimeVersion": { + "description": "**Note: Don't use this property unless you are sure what you're doing** \n\nThe runtime version associated with this manifest for the Android platform. If provided, this will override the top level runtimeVersion key.\nSet this to `{\"policy\": \"nativeVersion\"}` to generate it automatically.", + "$ref": "#/definitions/RuntimeVersion" + } + }, + "additionalProperties": false + }, + "AndroidIntentFiltersData": { + "type": "object", + "properties": { + "scheme": { + "description": "Scheme of the URL, e.g. `https`", + "type": "string" + }, + "host": { "description": "Hostname, e.g. `myapp.io`", "type": "string" }, + "port": { "description": "Port, e.g. `3000`", "type": "string" }, + "path": { + "description": "Exact path for URLs that should be matched by the filter, e.g. `/records`", + "type": "string" + }, + "pathPattern": { + "description": "Pattern for paths that should be matched by the filter, e.g. `.*`. Must begin with `/`", + "type": "string" + }, + "pathPrefix": { + "description": "Prefix for paths that should be matched by the filter, e.g. `/records/` will match `/records/123`", + "type": "string" + }, + "mimeType": { + "description": "MIME type for URLs that should be matched by the filter", + "type": "string" + } + }, + "additionalProperties": false + }, + "IOS": { + "description": "Configuration that is specific to the iOS platform.", + "type": "object", + "meta": { + "standaloneOnly": true + }, + "properties": { + "publishManifestPath": { + "description": "The manifest for the iOS version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "publishBundlePath": { + "description": "The bundle for the iOS version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "bundleIdentifier": { + "description": "The bundle identifier for your iOS standalone app. You make it up, but it needs to be unique on the App Store. See [this StackOverflow question](http://stackoverflow.com/questions/11347470/what-does-bundle-identifier-mean-in-the-ios-project).", + "type": "string", + "pattern": "^[a-zA-Z0-9.-]+$", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `CFBundleIdentifier`", + "regexHuman": "iOS bundle identifier notation unique name for your app. For example, `host.exp.expo`, where `exp.host` is our domain and `expo` is our app name." + } + }, + "buildNumber": { + "description": "Build number for your iOS standalone app. Corresponds to `CFBundleVersion` and must match Apple's [specified format](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102364). (Note: Transporter will pull the value for `Version Number` from `expo.version` and NOT from `expo.ios.buildNumber`.)", + "type": "string", + "pattern": "^[A-Za-z0-9\\.]+$", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `CFBundleVersion`" + } + }, + "backgroundColor": { + "description": "The background color for your iOS app, behind any of your React views. Overrides the top-level `backgroundColor` key if it is present. Requires `expo-system-ui` be installed in your project to work on iOS.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "icon": { + "description": "Local path or remote URL to an image to use for your app's icon on iOS. If specified, this overrides the top-level `icon` key. Use a 1024x1024 icon which follows Apple's interface guidelines for icons, including color profile and transparency. \n\n Expo will generate the other required sizes. This icon will appear on the home screen and within the Expo app.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "merchantId": { + "description": "@deprecated Merchant ID for use with Apple Pay in your standalone app.", + "type": "string", + "meta": { + "deprecated": true + } + }, + "appStoreUrl": { + "description": "URL to your app on the Apple App Store, if you have deployed it there. This is used to link to your store page from your Expo project page if your app is public.", + "pattern": "^https://(itunes|apps)\\.apple\\.com/.*?\\d+", + "example": "https://apps.apple.com/us/app/expo-client/id982107779", + "type": ["string"] + }, + "bitcode": { + "description": "Enable iOS Bitcode optimizations in the native build. Accepts the name of an iOS build configuration to enable for a single configuration and disable for all others, e.g. Debug, Release. Not available in the classic 'expo build:ios' or Expo Go. Defaults to `undefined` which uses the template's predefined settings.", + "anyOf": [ + { + "type": ["boolean"] + }, + { + "type": ["string"] + } + ] + }, + "config": { + "type": "object", + "description": "Note: This property key is not included in the production manifest and will evaluate to `undefined`. It is used internally only in the build process, because it contains API keys that some may want to keep private.", + "properties": { + "branch": { + "description": "[Branch](https://branch.io/) key to hook up Branch linking services.", + "type": "object", + "properties": { + "apiKey": { + "description": "Your Branch API key", + "type": "string" + } + }, + "additionalProperties": false + }, + "usesNonExemptEncryption": { + "description": "Sets `ITSAppUsesNonExemptEncryption` in the standalone ipa's Info.plist to the given boolean value.", + "type": "boolean" + }, + "googleMapsApiKey": { + "description": "[Google Maps iOS SDK](https://developers.google.com/maps/documentation/ios-sdk/start) key for your standalone app.", + "type": "string" + }, + "googleMobileAdsAppId": { + "description": "[Google Mobile Ads App ID](https://support.google.com/admob/answer/6232340) Google AdMob App ID. ", + "type": "string" + }, + "googleMobileAdsAutoInit": { + "description": "A boolean indicating whether to initialize Google App Measurement and begin sending user-level event data to Google immediately when the app starts. The default in Expo (Go and in standalone apps) is `false`. [Sets the opposite of the given value to the following key in `Info.plist`.](https://developers.google.com/admob/ios/eu-consent#delay_app_measurement_optional)", + "type": "boolean", + "fallback": false + }, + "googleSignIn": { + "description": "@deprecated Use `ios.googleServicesFile` instead.", + "type": "object", + "meta": { + "deprecated": true + }, + "properties": { + "reservedClientId": { + "description": "@deprecated Use `ios.googleServicesFile` instead.", + "type": "string", + "meta": { + "deprecated": true + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "googleServicesFile": { + "description": "[Firebase Configuration File](https://support.google.com/firebase/answer/7015592) Location of the `GoogleService-Info.plist` file for configuring Firebase.", + "type": "string" + }, + "supportsTablet": { + "description": "Whether your standalone iOS app supports tablet screen sizes. Defaults to `false`.", + "type": "boolean", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `UISupportedInterfaceOrientations~ipad`" + } + }, + "isTabletOnly": { + "description": "If true, indicates that your standalone iOS app does not support handsets, and only supports tablets.", + "type": "boolean", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `UISupportedInterfaceOrientations`" + } + }, + "requireFullScreen": { + "description": "If true, indicates that your standalone iOS app does not support Slide Over and Split View on iPad. Defaults to `false`", + "type": "boolean", + "meta": { + "bareWorkflow": "Use Xcode to set `UIRequiresFullScreen`" + } + }, + "userInterfaceStyle": { + "description": "Configuration to force the app to always use the light or dark user-interface appearance, such as \"dark mode\", or make it automatically adapt to the system preferences. If not provided, defaults to `light`.", + "type": "string", + "fallback": "light", + "enum": ["light", "dark", "automatic"] + }, + "infoPlist": { + "description": "Dictionary of arbitrary configuration to add to your standalone app's native Info.plist. Applied prior to all other Expo-specific configuration. No other validation is performed, so use this at your own risk of rejection from the App Store.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "entitlements": { + "description": "Dictionary of arbitrary configuration to add to your standalone app's native *.entitlements (plist). Applied prior to all other Expo-specific configuration. No other validation is performed, so use this at your own risk of rejection from the App Store.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "associatedDomains": { + "description": "An array that contains Associated Domains for the standalone app. [Learn more](https://developer.apple.com/documentation/safariservices/supporting_associated_domains).", + "type": "array", + "uniqueItems": true, + "items": { "type": "string" }, + "meta": { + "regexHuman": "Entries must follow the format `applinks:[:port number]`. [Learn more](https://developer.apple.com/documentation/safariservices/supporting_associated_domains).", + "bareWorkflow": "Build with EAS, or use Xcode to enable this capability manually. [Learn more](https://developer.apple.com/documentation/safariservices/supporting_associated_domains)." + } + }, + "usesIcloudStorage": { + "description": "A boolean indicating if the app uses iCloud Storage for `DocumentPicker`. See `DocumentPicker` docs for details.", + "type": "boolean", + "meta": { + "bareWorkflow": "Use Xcode, or ios.entitlements to configure this." + } + }, + "usesAppleSignIn": { + "description": "A boolean indicating if the app uses Apple Sign-In. See `AppleAuthentication` docs for details.", + "type": "boolean", + "fallback": false + }, + "accessesContactNotes": { + "description": "A Boolean value that indicates whether the app may access the notes stored in contacts. You must [receive permission from Apple](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes) before you can submit your app for review with this capability.", + "type": "boolean", + "fallback": false + }, + "splash": { + "description": "Configuration for loading and splash screen for standalone iOS apps.", + "type": "object", + "properties": { + "xib": { + "description": "@deprecated Apple has deprecated `.xib` splash screens in favor of `.storyboard` files. Local path to a XIB file as the loading screen. It overrides other loading screen options. Note: This will only be used in the standalone app (i.e., after you build the app). It will not be used in the Expo Go.", + "type": "string", + "meta": { + "deprecated": true, + "asset": true, + "contentTypePattern": "^text/xml$", + "contentTypeHuman": ".xib interface builder document" + } + }, + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover` or `contain`, defaults to `contain`.", + "enum": ["cover", "contain"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "tabletImage": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + } + }, + "jsEngine": { + "description": "Specifies the JavaScript engine for iOS apps. Supported only on EAS Build. Defaults to `jsc`. Valid values: `hermes`, `jsc`.", + "type": "string", + "fallback": "jsc", + "enum": ["hermes", "jsc"], + "meta": { + "bareWorkflow": "To change the JavaScript engine, update the `expo.jsEngine` value in `ios/Podfile.properties.json`" + } + }, + "runtimeVersion": { + "description": "**Note: Don't use this property unless you are sure what you're doing** \n\nThe runtime version associated with this manifest for the iOS platform. If provided, this will override the top level runtimeVersion key.\nSet this to `{\"policy\": \"nativeVersion\"}` to generate it automatically.", + "$ref": "#/definitions/RuntimeVersion" + } + }, + "additionalProperties": false + }, + "Splash": { + "description": "Configuration for loading and splash screen for standalone apps.", + "type": "object", + "properties": { + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`", + "bareWorkflow": "For Android, edit the `colorPrimary` item in `android/app/src/main/res/values/colors.xml`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover` or `contain`, defaults to `contain`.", + "enum": ["cover", "contain"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + }, + "meta": { + "bareWorkflow": "To change your app's icon, edit or replace the files in `ios//Assets.xcassets/AppIcon.appiconset` (we recommend using Xcode), and `android/app/src/main/res/mipmap-` (Android Studio can [generate the appropriate image files for you](https://developer.android.com/studio/write/image-asset-studio)). Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each required size." + } + }, + "PublishHook": { + "type": "object", + "additionalProperties": true, + "properties": { + "file": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": true, + "properties": {} + } + } + }, + "Web": { + "description": "Configuration that is specific to the web platform.", + "type": "object", + "additionalProperties": true, + "properties": { + "favicon": { + "description": "Relative path of an image to use for your app's favicon.", + "type": "string" + }, + "name": { + "description": "Defines the title of the document, defaults to the outer level name", + "type": "string", + "meta": { + "pwa": "name" + } + }, + "shortName": { + "description": "A short version of the app's name, 12 characters or fewer. Used in app launcher and new tab pages. Maps to `short_name` in the PWA manifest.json. Defaults to the `name` property.", + "type": "string", + "meta": { + "pwa": "short_name", + "regexHuman": "Maximum 12 characters long" + } + }, + "lang": { + "description": "Specifies the primary language for the values in the name and short_name members. This value is a string containing a single language tag.", + "type": "string", + "fallback": "en", + "meta": { + "pwa": "lang" + } + }, + "scope": { + "description": "Defines the navigation scope of this website's context. This restricts what web pages can be viewed while the manifest is applied. If the user navigates outside the scope, it returns to a normal web page inside a browser tab/window. If the scope is a relative URL, the base URL will be the URL of the manifest.", + "type": "string", + "meta": { + "pwa": "scope" + } + }, + "themeColor": { + "description": "Defines the color of the Android tool bar, and may be reflected in the app's preview in task switchers.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "pwa": "theme_color", + "html": "theme-color", + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "description": { + "description": "Provides a general description of what the pinned website does.", + "type": "string", + "meta": { + "html": "description", + "pwa": "description" + } + }, + "dir": { + "description": "Specifies the primary text direction for the name, short_name, and description members. Together with the lang member, it helps the correct display of right-to-left languages.", + "enum": ["auto", "ltr", "rtl"], + "type": "string", + "meta": { + "pwa": "dir" + } + }, + "display": { + "description": "Defines the developersā€™ preferred display mode for the website.", + "enum": ["fullscreen", "standalone", "minimal-ui", "browser"], + "type": "string", + "meta": { + "pwa": "display" + } + }, + "startUrl": { + "description": "The URL that loads when a user launches the application (e.g., when added to home screen), typically the index. Note: This has to be a relative URL, relative to the manifest URL.", + "type": "string", + "meta": { + "pwa": "start_url" + } + }, + "orientation": { + "description": "Defines the default orientation for all the website's top level browsing contexts.", + "enum": [ + "any", + "natural", + "landscape", + "landscape-primary", + "landscape-secondary", + "portrait", + "portrait-primary", + "portrait-secondary" + ], + "type": "string", + "meta": { + "pwa": "orientation" + } + }, + "backgroundColor": { + "description": "Defines the expected ā€œbackground colorā€ for the website. This value repeats what is already available in the siteā€™s CSS, but can be used by browsers to draw the background color of a shortcut when the manifest is available before the stylesheet has loaded. This creates a smooth transition between launching the web application and loading the site's content.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "pwa": "background_color", + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "barStyle": { + "description": "If content is set to default, the status bar appears normal. If set to black, the status bar has a black background. If set to black-translucent, the status bar is black and translucent. If set to default or black, the web content is displayed below the status bar. If set to black-translucent, the web content is displayed on the entire screen, partially obscured by the status bar.", + "enum": ["default", "black", "black-translucent"], + "type": "string", + "fallback": "black-translucent", + "meta": { + "html": "apple-mobile-web-app-status-bar-style", + "pwa": "name" + } + }, + "preferRelatedApplications": { + "description": "Hints for the user agent to indicate to the user that the specified native applications (defined in expo.ios and expo.android) are recommended over the website.", + "type": "boolean", + "fallback": true, + "meta": { + "pwa": "prefer_related_applications" + } + }, + "dangerous": { + "description": "Experimental features. These will break without deprecation notice.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "splash": { + "description": "Configuration for PWA splash screens.", + "type": "object", + "properties": { + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover` or `contain`, defaults to `contain`.", + "enum": ["cover", "contain"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + }, + "meta": { + "bareWorkflow": "Use [expo-splash-screen](https://github.com/expo/expo/tree/main/packages/expo-splash-screen#expo-splash-screen)" + } + }, + "config": { + "description": "Firebase web configuration. Used by the expo-firebase packages on both web and native. [Learn more](https://firebase.google.com/docs/reference/js/firebase.html#initializeapp)", + "type": "object", + "properties": { + "firebase": { + "type": "object", + "properties": { + "apiKey": { "type": "string" }, + "authDomain": { "type": "string" }, + "databaseURL": { "type": "string" }, + "projectId": { "type": "string" }, + "storageBucket": { "type": "string" }, + "messagingSenderId": { "type": "string" }, + "appId": { "type": "string" }, + "measurementId": { "type": "string" } + } + } + } + } + } + }, + "RuntimeVersion": { + "allOf": [ + { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-zA-Z\\d][a-zA-Z\\d._+()-]{0,254}$", + "meta": { + "regexHuman": "String beginning with an alphanumeric character followed by any combination of alphanumeric character, \"_\", \"+\", \".\",\"(\", \")\", or \"-\". Examples: \"1.0.3a+\", \"1.0.0\", \"._+()-0a1\", \"0\"." + } + }, + { + "type": "string", + "pattern": "^exposdk:((\\d+\\.\\d+\\.\\d+)|(UNVERSIONED))$", + "meta": { + "regexHuman": "An 'exposdk:' prefix followed by the SDK version of your project. Example: \"exposdk:44.0.0\"." + } + }, + { + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": ["nativeVersion", "sdkVersion"] + } + }, + "required": ["policy"], + "additionalProperties": false + } + ] + }, + { + "not": { + "pattern": "^\\d+\\.\\d*0$" + }, + "meta": { + "notHuman": "Not a decimal ending in a 0." + } + } + ] + } + }, + "type": "object", + "properties": { + "name": { + "description": "The name of your app as it appears both within Expo Go and on your home screen as a standalone app.", + "type": "string", + "meta": { + "bareWorkflow": "To change the name of your app, edit the 'Display Name' field in Xcode and the `app_name` string in `android/app/src/main/res/values/strings.xml`" + } + }, + "description": { + "description": "A short description of what your app is and why it is great.", + "type": "string" + }, + "slug": { + "description": "The friendly URL name for publishing. For example, `myAppName` will refer to the `expo.dev/@project-owner/myAppName` project.", + "type": "string", + "pattern": "^[a-zA-Z0-9_\\-]+$" + }, + "owner": { + "description": "The name of the Expo account that owns the project. This is useful for teams collaborating on a project. If not provided, the owner defaults to the username of the current user.", + "type": "string", + "minLength": 1 + }, + "currentFullName": { + "description": "The auto generated Expo account name and slug used for display purposes. Formatted like `@username/slug`. When unauthenticated, the username is `@anonymous`. For published projects, this value may change when a project is transferred between accounts or renamed.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "originalFullName": { + "description": "The auto generated Expo account name and slug used for services like Notifications and AuthSession proxy. Formatted like `@username/slug`. When unauthenticated, the username is `@anonymous`. For published projects, this value will not change when a project is transferred between accounts or renamed.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "privacy": { + "description": "Defaults to `unlisted`. `unlisted` hides the project from search results. `hidden` restricts access to the project page to only the owner and other users that have been granted access. Valid values: `public`, `unlisted`, `hidden`.", + "enum": ["public", "unlisted", "hidden"], + "type": "string", + "fallback": "unlisted" + }, + "sdkVersion": { + "description": "The Expo sdkVersion to run the project on. This should line up with the version specified in your package.json.", + "type": "string", + "pattern": "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$" + }, + "runtimeVersion": { + "description": "**Note: Don't use this property unless you are sure what you're doing** \n\nThe runtime version associated with this manifest.\nSet this to `{\"policy\": \"nativeVersion\"}` to generate it automatically.", + "$ref": "#/definitions/RuntimeVersion" + }, + "version": { + "description": "Your app version. In addition to this field, you'll also use `ios.buildNumber` and `android.versionCode` ā€” read more about how to version your app [here](https://docs.expo.dev/distribution/app-stores/#versioning-your-app). On iOS this corresponds to `CFBundleShortVersionString`, and on Android, this corresponds to `versionName`. The required format can be found [here](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring).", + "type": "string", + "meta": { + "bareWorkflow": "To change your app version, edit the 'Version' field in Xcode and the `versionName` string in `android/app/build.gradle`" + } + }, + "platforms": { + "description": "Platforms that your project explicitly supports. If not specified, it defaults to `[\"ios\", \"android\"]`.", + "example": ["ios", "android", "web"], + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "enum": ["android", "ios", "web"] } + }, + "githubUrl": { + "description": "If you would like to share the source code of your app on Github, enter the URL for the repository here and it will be linked to from your Expo project page.", + "pattern": "^https://github\\.com/", + "example": "https://github.com/expo/expo", + "type": ["string"] + }, + "orientation": { + "description": "Locks your app to a specific orientation with portrait or landscape. Defaults to no lock. Valid values: `default`, `portrait`, `landscape`", + "enum": ["default", "portrait", "landscape"], + "type": "string" + }, + "userInterfaceStyle": { + "description": "Configuration to force the app to always use the light or dark user-interface appearance, such as \"dark mode\", or make it automatically adapt to the system preferences. If not provided, defaults to `light`. Requires `expo-system-ui` be installed in your project to work on Android.", + "type": "string", + "fallback": "light", + "enum": ["light", "dark", "automatic"] + }, + "backgroundColor": { + "description": "The background color for your app, behind any of your React views. This is also known as the root view background color. Requires `expo-system-ui` be installed in your project to work on iOS.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`. Default is white: `'#ffffff'`" + } + }, + "primaryColor": { + "description": "On Android, this will determine the color of your app in the multitasker. Currently this is not used on iOS, but it may be used for other purposes in the future.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "icon": { + "description": "Local path or remote URL to an image to use for your app's icon. We recommend that you use a 1024x1024 png file. This icon will appear on the home screen and within the Expo app.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true, + "bareWorkflow": "To change your app's icon, edit or replace the files in `ios//Assets.xcassets/AppIcon.appiconset` (we recommend using Xcode), and `android/app/src/main/res/mipmap-`. Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each existing size." + } + }, + "notification": { + "description": "Configuration for remote (push) notifications.", + "type": "object", + "properties": { + "icon": { + "description": "(Android only) Local path or remote URL to an image to use as the icon for push notifications. 96x96 png grayscale with transparency. We recommend following [Google's design guidelines](https://material.io/design/iconography/product-icons.html#design-principles). If not provided, defaults to your app icon.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "color": { + "description": "(Android only) Tint color for the push notification image when it appears in the notification tray. Defaults to `#ffffff`", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "iosDisplayInForeground": { + "description": "Whether or not to display notifications when the app is in the foreground on iOS. `_displayInForeground` option in the individual push notification message overrides this option. [Learn more.](https://docs.expo.dev/push-notifications/receiving-notifications/#foreground-notification-behavior) Defaults to `false`.", + "type": "boolean" + }, + "androidMode": { + "description": "Show each push notification individually (`default`) or collapse into one (`collapse`).", + "enum": ["default", "collapse"], + "type": "string" + }, + "androidCollapsedTitle": { + "description": "If `androidMode` is set to `collapse`, this title is used for the collapsed notification message. For example, `'#{unread_notifications} new interactions'`.", + "type": "string" + } + }, + "additionalProperties": false + }, + "appKey": { + "description": "@deprecated By default, Expo looks for the application registered with the AppRegistry as `main`. If you would like to change this, you can specify the name in this property.", + "meta": { + "deprecated": true + }, + "type": "string" + }, + "androidStatusBar": { + "description": "Configuration for the status bar on Android. For more details please navigate to [Configuring StatusBar](https://docs.expo.dev/guides/configuring-statusbar/).", + "type": "object", + "properties": { + "barStyle": { + "description": "Configures the status bar icons to have a light or dark color. Valid values: `light-content`, `dark-content`. Defaults to `dark-content`", + "type": "string", + "enum": ["light-content", "dark-content"] + }, + "backgroundColor": { + "description": "Specifies the background color of the status bar. Defaults to `#00000000` (transparent) for `dark-content` bar style and `#00000088` (semi-transparent black) for `light-content` bar style", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string `'#RRGGBB'`, for example, `'#000000'` for black. Or 8 character long hex color string `'#RRGGBBAA'`, for example, `'#00000088'` for semi-transparent black." + } + }, + "hidden": { + "description": "Instructs the system whether the status bar should be visible or not. Defaults to `false`", + "type": "boolean" + }, + "translucent": { + "description": "Sets `android:windowTranslucentStatus` in `styles.xml`. When false, the system status bar pushes the content of your app down (similar to `position: relative`). When true, the status bar floats above the content in your app (similar to `position: absolute`). Defaults to `true` to match the iOS status bar behavior (which can only float above content).", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "androidNavigationBar": { + "description": "Configuration for the bottom navigation bar on Android. Can be used to configure the `expo-navigation-bar` module in EAS Build.", + "type": "object", + "properties": { + "visible": { + "description": "Determines how and when the navigation bar is shown. [Learn more](https://developer.android.com/training/system-ui/immersive). Requires `expo-navigation-bar` be installed in your project. Valid values: `leanback`, `immersive`, `sticky-immersive` \n\n `leanback` results in the navigation bar being hidden until the first touch gesture is registered. \n\n `immersive` results in the navigation bar being hidden until the user swipes up from the edge where the navigation bar is hidden. \n\n `sticky-immersive` is identical to `'immersive'` except that the navigation bar will be semi-transparent and will be hidden again after a short period of time.", + "type": "string", + "enum": ["leanback", "immersive", "sticky-immersive"] + }, + "barStyle": { + "description": "Configure the navigation bar icons to have a light or dark color. Supported on Android Oreo and newer. Valid values: `'light-content'`, `'dark-content'`", + "type": "string", + "enum": ["light-content", "dark-content"] + }, + "backgroundColor": { + "description": "Specifies the background color of the navigation bar.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + } + }, + "additionalProperties": false + }, + "developmentClient": { + "description": "Settings that apply specifically to running this app in a development client", + "type": "object", + "properties": { + "silentLaunch": { + "description": "If true, the app will launch in a development client with no additional dialogs or progress indicators, just like in a standalone app.", + "type": "boolean", + "fallback": false + } + }, + "additionalProperties": false + }, + "scheme": { + "description": "**Standalone Apps Only**. URL scheme to link into your app. For example, if we set this to `'demo'`, then demo:// URLs would open your app when tapped.", + "type": "string", + "pattern": "^[a-z][a-z0-9+.-]*$", + "meta": { + "regexHuman": "String beginning with a **lowercase** letter followed by any combination of **lowercase** letters, digits, \"+\", \".\" or \"-\"", + "standaloneOnly": true, + "bareWorkflow": "To change your app's scheme, replace all occurrences of the old scheme in `Info.plist` and `AndroidManifest.xml`" + } + }, + "entryPoint": { + "description": "The relative path to your main JavaScript file.", + "type": "string" + }, + "extra": { + "description": "Any extra fields you want to pass to your experience. Values are accessible via `Expo.Constants.manifest.extra` ([Learn more](https://docs.expo.dev/versions/latest/sdk/constants/#constantsmanifest))", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "packagerOpts": { + "description": "@deprecated Use a `metro.config.js` file instead. [Learn more](https://docs.expo.dev/guides/customizing-metro/)", + "meta": { + "deprecated": true, + "autogenerated": true + }, + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "updates": { + "description": "Configuration for how and when the app should request OTA JavaScript updates", + "type": "object", + "properties": { + "enabled": { + "description": "If set to false, your standalone app will never download any code, and will only use code bundled locally on the device. In that case, all updates to your app must be submitted through app store review. Defaults to true. (Note: This will not work out of the box with ExpoKit projects)", + "type": "boolean" + }, + "checkAutomatically": { + "description": "By default, Expo will check for updates every time the app is loaded. Set this to `ON_ERROR_RECOVERY` to disable automatic checking unless recovering from an error. Must be one of `ON_LOAD` or `ON_ERROR_RECOVERY`", + "enum": ["ON_ERROR_RECOVERY", "ON_LOAD"], + "type": "string" + }, + "fallbackToCacheTimeout": { + "description": "How long (in ms) to allow for fetching OTA updates before falling back to a cached version of the app. Defaults to 0. Must be between 0 and 300000 (5 minutes).", + "type": "number", + "minimum": 0, + "maximum": 300000 + }, + "url": { + "description": "URL from which expo-updates will fetch update manifests", + "type": "string" + }, + "codeSigningCertificate": { + "description": "Local path of a PEM-formatted X.509 certificate used for requiring and verifying signed Expo updates", + "type": "string" + }, + "codeSigningMetadata": { + "description": "Metadata for `codeSigningCertificate`", + "type": "object", + "properties": { + "alg": { + "description": "Algorithm used to generate manifest code signing signature.", + "enum": ["rsa-v1_5-sha256"], + "type": "string" + }, + "keyid": { + "description": "Identifier for the key in the certificate. Used to instruct signing mechanisms when signing or verifying signatures.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "locales": { + "description": "Provide overrides by locale for System Dialog prompts like Permissions Boxes", + "type": "object", + "properties": {}, + "meta": { + "bareWorkflow": "To add or change language and localization information in your iOS app, you need to use Xcode." + }, + "additionalProperties": { + "type": ["string", "object"] + } + }, + "facebookAppId": { + "description": "Used for all Facebook libraries. Set up your Facebook App ID at https://developers.facebook.com.", + "type": "string", + "pattern": "^[0-9]+$", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookAutoInitEnabled": { + "description": "Whether the Facebook SDK should be initialized automatically. The default in Expo (Client and in standalone apps) is `false`.", + "type": "boolean" + }, + "facebookAutoLogAppEventsEnabled": { + "description": "Whether the Facebook SDK log app events automatically. If you don't set this property, Facebook's default will be used. (Applicable only to standalone apps.) Note: The Facebook SDK must be initialized for app events to work. You may autoinitialize Facebook SDK by setting `facebookAutoInitEnabled` to `true`", + "type": "boolean", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookAdvertiserIDCollectionEnabled": { + "description": "Whether the Facebook SDK should collect advertiser ID properties, like the Apple IDFA and Android Advertising ID, automatically. If you don't set this property, Facebook's default policy will be used. (Applicable only to standalone apps.)", + "type": "boolean", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookDisplayName": { + "description": "Used for native Facebook login.", + "type": "string", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookScheme": { + "description": "Used for Facebook native login. Starts with 'fb' and followed by a string of digits, like 'fb1234567890'. You can find your scheme [here](https://developers.facebook.com/docs/facebook-login/ios)in the 'Configuring Your info.plist' section (only applicable to standalone apps and custom Expo Go apps).", + "type": "string", + "pattern": "^fb[0-9]+[A-Za-z]*$", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "isDetached": { + "description": "Is app detached", + "type": "boolean", + "meta": { + "autogenerated": true + } + }, + "detach": { + "description": "Extra fields needed by detached apps", + "type": "object", + "properties": {}, + "meta": { + "autogenerated": true + }, + "additionalProperties": true + }, + "assetBundlePatterns": { + "description": "An array of file glob strings which point to assets that will be bundled within your standalone app binary. Read more in the [Offline Support guide](https://docs.expo.dev/guides/offline-support/)", + "type": "array", + "items": { "type": "string" } + }, + "plugins": { + "description": "Config plugins for adding extra functionality to your project. [Learn more](https://docs.expo.dev/guides/config-plugins/).", + "meta": { + "bareWorkflow": "Plugins that add modifications can only be used with [prebuilding](https://expo.fyi/prebuilding) and managed EAS Build" + }, + "type": "array", + "items": { + "anyOf": [ + { + "type": ["string"] + }, + { + "type": "array", + "items": [ + { + "type": ["string"] + }, + {} + ], + "additionalItems": false + } + ] + } + }, + "splash": { + "$ref": "#/definitions/Splash" + }, + "jsEngine": { + "description": "Specifies the JavaScript engine for apps. Supported only on EAS Build. Defaults to `jsc`. Valid values: `hermes`, `jsc`.", + "type": "string", + "fallback": "jsc", + "enum": ["hermes", "jsc"], + "meta": { + "bareWorkflow": "To change the JavaScript engine, update the `expo.jsEngine` value in `ios/Podfile.properties.json` or `android/gradle.properties`" + } + }, + "ios": { + "$ref": "#/definitions/IOS" + }, + "android": { + "$ref": "#/definitions/Android" + }, + "web": { + "$ref": "#/definitions/Web" + }, + "hooks": { + "description": "Configuration for scripts to run to hook into the publish process", + "type": "object", + "additionalProperties": false, + "properties": { + "postPublish": { + "type": "array", + "items": { + "$ref": "#/definitions/PublishHook" + } + }, + "postExport": { + "type": "array", + "items": { + "$ref": "#/definitions/PublishHook" + } + } + } + }, + "experiments": { + "description": "Enable experimental features that may be unstable, unsupported, or removed without deprecation notices.", + "type": "object", + "additionalProperties": false, + "properties": { + "turboModules": { + "description": "Enables Turbo Modules, which are a type of native modules that use a different way of communicating between JS and platform code. When installing a Turbo Module you will need to enable this experimental option (the library still needs to be a part of Expo SDK already, like react-native-reanimated v2). Turbo Modules do not support remote debugging and enabling this option will disable remote debugging.", + "type": "boolean", + "fallback": false + } + } + }, + "_internal": { + "description": "Internal properties for developer tools", + "type": "object", + "properties": { + "pluginHistory": { + "description": "List of plugins already run on the config", + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "additionalProperties": true, + "meta": { + "autogenerated": true + } + } + }, + "additionalProperties": false, + "required": ["name", "slug"] + } +} diff --git a/packages/@expo/schemer/e2e/jest.config.js b/packages/@expo/schemer/e2e/jest.config.js new file mode 100644 index 0000000000000..c2a9ae5b2156a --- /dev/null +++ b/packages/@expo/schemer/e2e/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('jest').Config} */ +module.exports = { + ...require('expo-module-scripts/jest-preset-cli'), + preset: 'ts-jest', + displayName: require('../package').name, + rootDir: __dirname, + roots: ['.'], +}; diff --git a/packages/@expo/schemer/jest.config.js b/packages/@expo/schemer/jest.config.js new file mode 100644 index 0000000000000..ab2d91b72b590 --- /dev/null +++ b/packages/@expo/schemer/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('jest').Config} */ +module.exports = { + ...require('expo-module-scripts/jest-preset-cli'), + preset: 'ts-jest', + displayName: require('./package').name, + rootDir: __dirname, + roots: ['src'], +}; diff --git a/packages/@expo/schemer/package.json b/packages/@expo/schemer/package.json new file mode 100644 index 0000000000000..7c57acd6d3a06 --- /dev/null +++ b/packages/@expo/schemer/package.json @@ -0,0 +1,44 @@ +{ + "name": "@expo/schemer", + "version": "1.4.5", + "description": "Centralized scheme validation library for Expo", + "license": "MIT", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "files": [ + "build" + ], + "scripts": { + "build": "expo-module tsc", + "prepare": "yarn run clean && yarn run build", + "clean": "expo-module clean", + "lint": "expo-module lint", + "test": "expo-module test", + "test:e2e": "expo-module test --config e2e/jest.config.js", + "typecheck": "expo-module typecheck", + "watch": "yarn run --watch --preserveWatchOutput", + "prepublishOnly": "expo-module prepublishOnly" + }, + "homepage": "https://github.com/expo/expo/tree/main/packages/@expo/schemer#readme", + "repository": { + "type": "git", + "url": "https://github.com/expo/expo.git", + "directory": "packages/@expo/schemer" + }, + "bugs": { + "url": "https://github.com/expo/expo/issues" + }, + "dependencies": { + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", + "json-schema-traverse": "^1.0.0", + "probe-image-size": "^7.1.0" + }, + "devDependencies": { + "@types/probe-image-size": "^7.2.4", + "expo-module-scripts": "^3.3.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@expo/schemer/src/Error.ts b/packages/@expo/schemer/src/Error.ts new file mode 100644 index 0000000000000..685488b35b036 --- /dev/null +++ b/packages/@expo/schemer/src/Error.ts @@ -0,0 +1,53 @@ +export class SchemerError extends Error { + readonly name = 'SchemerError'; + errors: ValidationError[]; + + constructor(errors: ValidationError[]) { + super(''); + this.message = errors.map((error) => error.message).join('\n'); + this.errors = errors; + } +} + +export class ValidationError extends Error { + readonly name = 'ValidationError'; + errorCode: string; + fieldPath: string; + data: any; + meta: any; + constructor({ + errorCode, + fieldPath, + message, + data, + meta, + }: { + errorCode: ErrorCode; + fieldPath: string; + message: string; + data: any; + meta: any; + }) { + super(message); + this.errorCode = errorCode; + this.fieldPath = fieldPath; + this.data = data; + this.meta = meta; + } +} + +export type ErrorCode = keyof typeof ErrorCodes; + +export const ErrorCodes = { + SCHEMA_VALIDATION_ERROR: 'SCHEMA_VALIDATION_ERROR', + SCHEMA_ADDITIONAL_PROPERTY: 'SCHEMA_ADDITIONAL_PROPERTY', + SCHEMA_MISSING_REQUIRED_PROPERTY: 'SCHEMA_MISSING_REQUIRED_PROPERTY', + SCHEMA_INVALID_PATTERN: 'SCHEMA_INVALID_PATTERN', + SCHEMA_INVALID_NOT: 'SCHEMA_INVALID_NOT', + + INVALID_ASSET_URI: 'INVALID_ASSET_URI', + INVALID_DIMENSIONS: 'INVALID_DIMENSIONS', + INVALID_CONTENT_TYPE: 'INVALID_CONTENT_TYPE', + NOT_SQUARE: 'NOT_SQUARE', + FILE_EXTENSION_MISMATCH: 'FILE_EXTENSION_MISMATCH', +}; diff --git a/packages/@expo/schemer/src/Util.ts b/packages/@expo/schemer/src/Util.ts new file mode 100644 index 0000000000000..81301ce8fb15a --- /dev/null +++ b/packages/@expo/schemer/src/Util.ts @@ -0,0 +1,33 @@ +export const fieldPathToSchemaPath = (fieldPath: string) => { + return pathToSegments(fieldPath) + .map((segment) => `properties.${segment}`) + .join('.'); +}; +// Assumption: used only for jsonPointer returned from traverse +export const schemaPointerToFieldPath = (jsonPointer: string) => { + return jsonPointer + .split('/') + .slice(2) + .filter((error) => error !== 'properties') + .join('.'); +}; + +export const fieldPathToSchema = (schema: object, fieldPath: string) => { + return get(schema, fieldPathToSchemaPath(fieldPath)); +}; + +export function pathToSegments(path: string | string[]) { + return Array.isArray(path) ? path : path.split('.'); +} + +export function get(object: any, path: string | string[]) { + const segments = pathToSegments(path); + const length = segments.length; + let index = 0; + + while (object != null && index < length) { + object = object[segments[index++]]; + } + + return index && index === length ? object : undefined; +} diff --git a/packages/@expo/schemer/src/__tests__/Schemer-test.ts b/packages/@expo/schemer/src/__tests__/Schemer-test.ts new file mode 100644 index 0000000000000..0bd3451c5fb5c --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/Schemer-test.ts @@ -0,0 +1,224 @@ +import good from './fixtures/app.json'; +import bad from './fixtures/bad.json'; +import badWithNot from './fixtures/badwithnot.json'; +import invalidAppIcon from './fixtures/invalidAppIcon.json'; +import schema from './fixtures/schema.json'; +import Schemer from '..'; +import { ErrorCodes, SchemerError } from '../Error'; + +const validator = new Schemer(schema.schema, { rootDir: __dirname }); + +describe('Sanity Tests', () => { + it('returns instance of Schemer', () => { + expect(validator instanceof Schemer).toBe(true); + }); + + it('returns instance with public functions', () => { + expect(validator).toMatchObject({ + validateAll: expect.any(Function), + validateProperty: expect.any(Function), + }); + }); +}); + +describe('Image Validation', () => { + it('errors for webp images', async () => { + const error = await expectSchemerToThrowAsync(() => + validator.validateAssetsAsync({ + android: { + adaptiveIcon: { foregroundImage: './fixtures/webp.webp' }, + }, + }) + ); + + expect(error.errors).toEqual([ + expect.objectContaining({ errorCode: ErrorCodes.INVALID_CONTENT_TYPE }), + expect.objectContaining({ errorCode: ErrorCodes.NOT_SQUARE }), + ]); + expectSchemerErrorToMatchSnapshot(error); + }); + + it('errors when file extension and content do not match up', async () => { + const error = await expectSchemerToThrowAsync(() => + validator.validateAssetsAsync({ + icon: './fixtures/secretlyPng.jpg', + }) + ); + + expect(error.errors).toEqual([ + expect.objectContaining({ errorCode: ErrorCodes.FILE_EXTENSION_MISMATCH }), + ]); + expectSchemerErrorToMatchSnapshot(error); + }); +}); + +describe('Holistic Unit Test', () => { + it('good example app.json all', async () => { + expect(await validator.validateAll(good)).toBeUndefined(); + }); + + it('good example app.json schema', async () => { + expect(await validator.validateSchemaAsync(good)).toBeUndefined(); + }); + + it('bad example app.json schema', async () => { + expectSchemerErrorToMatchSnapshot( + await expectSchemerToThrowAsync(() => validator.validateSchemaAsync(bad)) + ); + }); + + it('bad example app.json schema with field with not', async () => { + expectSchemerErrorToMatchSnapshot( + await expectSchemerToThrowAsync(() => validator.validateSchemaAsync(badWithNot)) + ); + }); + + it('bad example app.json - invalid path for app icon', async () => { + expectSchemerErrorToMatchSnapshot( + await expectSchemerToThrowAsync(() => validator.validateAll(invalidAppIcon)) + ); + }); +}); + +describe('Manual Validation Individual Unit Tests', () => { + it('Local Icon', async () => { + expect(await validator.validateIcon('./fixtures/check.png')).toBeUndefined(); + }); + + it('Local Square Icon correct', async () => { + const customValidator = new Schemer( + { properties: { icon: { meta: { asset: true, square: true } } } }, + { rootDir: __dirname } + ); + expect(await customValidator.validateIcon('./fixtures/check.png')).toBeUndefined(); + }); + + it('Local icon dimensions wrong', async () => { + const customValidator = new Schemer( + { + properties: { + icon: { + meta: { + asset: true, + dimensions: { width: 400, height: 401 }, + contentTypePattern: '^image/png$', + }, + }, + }, + }, + { rootDir: __dirname } + ); + const error = await expectSchemerToThrowAsync(() => + customValidator.validateIcon('./fixtures/check.png') + ); + + expect(error.errors).toHaveLength(1); + expectSchemerErrorToMatchSnapshot(error); + }); +}); + +describe('Individual Unit Tests', () => { + it('Error when missing Required Property', async () => { + const customValidator = new Schemer( + { + properties: { name: {} }, + required: ['name'], + }, + { rootDir: __dirname } + ); + const error = await expectSchemerToThrowAsync(() => + customValidator.validateAll({ noName: '' }) + ); + + expect(error.errors).toHaveLength(1); + expect(error.errors).toEqual([ + expect.objectContaining({ errorCode: ErrorCodes.SCHEMA_MISSING_REQUIRED_PROPERTY }), + ]); + expectSchemerErrorToMatchSnapshot(error); + }); + + it('Error when data has an additional property', async () => { + const customValidator = new Schemer({ additionalProperties: false }, { rootDir: __dirname }); + + const error = await expectSchemerToThrowAsync(() => + customValidator.validateAll({ extraProperty: 'extra' }) + ); + + expect(error.errors).toHaveLength(1); + expect(error.errors).toEqual([ + expect.objectContaining({ errorCode: ErrorCodes.SCHEMA_ADDITIONAL_PROPERTY }), + ]); + expectSchemerErrorToMatchSnapshot(error); + }); + + it.each` + name | expectedError + ${'wilson'} | ${undefined} + ${[1, 2, 3, 4]} | ${'must be string'} + ${23.232332} | ${'must be string'} + ${/regex.*/} | ${'must be string'} + `('validates name: $name', async ({ name, expectedError }) => { + if (!expectedError) { + expect(await validator.validateName(name)).toBeUndefined(); + } else { + const error = await expectSchemerToThrowAsync(() => validator.validateName(name)); + expect(error.message).toBe(expectedError); + } + }); + + it.each` + slug | expectedError + ${'wilson'} | ${undefined} + ${12312123123} | ${'must be string'} + ${[1, 23]} | ${'must be string'} + ${'wilson123'} | ${undefined} + ${'wilson-123'} | ${undefined} + ${'wilson/test'} | ${'\'\' must match pattern "^[a-zA-Z0-9_\\-]+$"'} + ${'wilson-test%'} | ${'\'\' must match pattern "^[a-zA-Z0-9_\\-]+$"'} + ${'wilson-test-zhao--javascript-is-super-funky'} | ${undefined} + `('validates slug: $slug', async ({ slug, expectedError }) => { + if (!expectedError) { + expect(await validator.validateSlug(slug)).toBeUndefined(); + } else { + const error = await expectSchemerToThrowAsync(() => validator.validateSlug(slug)); + expect(error.message).toBe(expectedError); + } + }); + + it.each` + sdkVersion | expectedError + ${'1.0.0'} | ${undefined} + ${'2.0.0.0.1'} | ${undefined} + ${'UNVERSIONED'} | ${undefined} + ${'12.2a.3'} | ${'\'\' must match pattern "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$"'} + ${'9,9,9'} | ${'\'\' must match pattern "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$"'} + ${'1.2'} | ${'\'\' must match pattern "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$"'} + `('validates SDK version: $sdkVersion', async ({ sdkVersion, expectedError }) => { + if (!expectedError) { + expect(await validator.validateSdkVersion(sdkVersion)).toBeUndefined(); + } else { + const error = await expectSchemerToThrowAsync(() => validator.validateSdkVersion(sdkVersion)); + expect(error.message).toBe(expectedError); + } + }); +}); + +async function expectSchemerToThrowAsync(action: () => any): Promise { + try { + await action(); + } catch (error: any) { + expect(error).toBeInstanceOf(SchemerError); + return error; + } + + throw new Error('Expression did not throw the expected error'); +} + +function expectSchemerErrorToMatchSnapshot(error: SchemerError) { + expect( + error.errors.map((validationError) => { + const { stack, message, ...rest } = validationError; + return { ...rest, message }; + }) + ).toMatchSnapshot(); +} diff --git a/packages/@expo/schemer/src/__tests__/Util-test.ts b/packages/@expo/schemer/src/__tests__/Util-test.ts new file mode 100644 index 0000000000000..29ad87cdbf283 --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/Util-test.ts @@ -0,0 +1,25 @@ +import { fieldPathToSchemaPath, schemaPointerToFieldPath } from '../Util'; + +describe('Helper function unit tests', () => { + it('fieldPathToSchemaPath short path', () => { + expect(fieldPathToSchemaPath('name')).toBe('properties.name'); + }); + + it('fieldPathToSchemaPath long property string', () => { + expect(fieldPathToSchemaPath('app.android.icon')).toBe( + 'properties.app.properties.android.properties.icon' + ); + }); + + it('fieldPathToSchemaPath mixed characters', () => { + expect(fieldPathToSchemaPath('a23-df34.fef4383')).toBe( + 'properties.a23-df34.properties.fef4383' + ); + }); + + it('schemaPointerToFieldPath', () => { + expect(schemaPointerToFieldPath('/properties/loading/properties/backgroundImage')).toBe( + 'loading.backgroundImage' + ); + }); +}); diff --git a/packages/@expo/schemer/src/__tests__/__snapshots__/Schemer-test.ts.snap b/packages/@expo/schemer/src/__tests__/__snapshots__/Schemer-test.ts.snap new file mode 100644 index 0000000000000..08f045fb747fa --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/__snapshots__/Schemer-test.ts.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Holistic Unit Test bad example app.json - invalid path for app icon 1`] = ` +[ + { + "data": "./unknown/path.png", + "errorCode": "INVALID_ASSET_URI", + "fieldPath": "icon", + "message": "cannot access file at './unknown/path.png'", + "meta": { + "asset": true, + "bareWorkflow": "To change your app's icon, edit or replace the files in \`ios//Assets.xcassets/AppIcon.appiconset\` (we recommend using Xcode), and \`android/app/src/main/res/mipmap-\`. Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each existing size.", + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, +] +`; + +exports[`Holistic Unit Test bad example app.json schema 1`] = ` +[ + { + "data": { + "asdfasdfandroid": { + "package": "com.yourcompany.yourappname", + }, + "icon": "DoesNotExist.png", + "orientaasdfasdftion": "portrait", + "sdkVersion": "17.0.0abad", + "slug": "1*@)#($*@)(#$*)", + }, + "errorCode": "SCHEMA_MISSING_REQUIRED_PROPERTY", + "fieldPath": "", + "message": "is missing required property 'name'", + "meta": undefined, + "name": "ValidationError", + }, + { + "data": { + "asdfasdfandroid": { + "package": "com.yourcompany.yourappname", + }, + "icon": "DoesNotExist.png", + "orientaasdfasdftion": "portrait", + "sdkVersion": "17.0.0abad", + "slug": "1*@)#($*@)(#$*)", + }, + "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", + "fieldPath": "", + "message": "should NOT have additional property 'orientaasdfasdftion'", + "meta": undefined, + "name": "ValidationError", + }, + { + "data": { + "asdfasdfandroid": { + "package": "com.yourcompany.yourappname", + }, + "icon": "DoesNotExist.png", + "orientaasdfasdftion": "portrait", + "sdkVersion": "17.0.0abad", + "slug": "1*@)#($*@)(#$*)", + }, + "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", + "fieldPath": "", + "message": "should NOT have additional property 'asdfasdfandroid'", + "meta": undefined, + "name": "ValidationError", + }, + { + "data": "1*@)#($*@)(#$*)", + "errorCode": "SCHEMA_INVALID_PATTERN", + "fieldPath": "slug", + "message": "'slug' must match pattern \"^[a-zA-Z0-9_\\-]+$\"", + "meta": undefined, + "name": "ValidationError", + }, +] +`; + +exports[`Holistic Unit Test bad example app.json schema with field with not 1`] = ` +[ + { + "data": "1.0", + "errorCode": "SCHEMA_INVALID_NOT", + "fieldPath": "runtimeVersion", + "message": "'runtimeVersion' should be not a decimal ending in a 0.", + "meta": { + "notHuman": "Not a decimal ending in a 0.", + }, + "name": "ValidationError", + }, +] +`; + +exports[`Image Validation errors for webp images 1`] = ` +[ + { + "data": "./fixtures/webp.webp", + "errorCode": "INVALID_CONTENT_TYPE", + "fieldPath": "Android.adaptiveIcon.foregroundImage", + "message": "field 'Android.adaptiveIcon.foregroundImage' should point to .png image but the file at './fixtures/webp.webp' has type webp", + "meta": { + "asset": true, + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, + { + "data": "./fixtures/webp.webp", + "errorCode": "NOT_SQUARE", + "fieldPath": "Android.adaptiveIcon.foregroundImage", + "message": "image should be square, but the file at './fixtures/webp.webp' has dimensions 320x214", + "meta": { + "asset": true, + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, +] +`; + +exports[`Image Validation errors when file extension and content do not match up 1`] = ` +[ + { + "data": "./fixtures/secretlyPng.jpg", + "errorCode": "FILE_EXTENSION_MISMATCH", + "fieldPath": "icon", + "message": "the file extension should match the content, but the file extension is .jpg while the file content at './fixtures/secretlyPng.jpg' is of type png", + "meta": { + "asset": true, + "bareWorkflow": "To change your app's icon, edit or replace the files in \`ios//Assets.xcassets/AppIcon.appiconset\` (we recommend using Xcode), and \`android/app/src/main/res/mipmap-\`. Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each existing size.", + "contentTypeHuman": ".png image", + "contentTypePattern": "^image/png$", + "square": true, + }, + "name": "ValidationError", + }, +] +`; + +exports[`Individual Unit Tests Error when data has an additional property 1`] = ` +[ + { + "data": { + "extraProperty": "extra", + }, + "errorCode": "SCHEMA_ADDITIONAL_PROPERTY", + "fieldPath": "", + "message": "should NOT have additional property 'extraProperty'", + "meta": undefined, + "name": "ValidationError", + }, +] +`; + +exports[`Individual Unit Tests Error when missing Required Property 1`] = ` +[ + { + "data": { + "noName": "", + }, + "errorCode": "SCHEMA_MISSING_REQUIRED_PROPERTY", + "fieldPath": "", + "message": "is missing required property 'name'", + "meta": undefined, + "name": "ValidationError", + }, +] +`; + +exports[`Manual Validation Individual Unit Tests Local icon dimensions wrong 1`] = ` +[ + { + "data": "./fixtures/check.png", + "errorCode": "INVALID_DIMENSIONS", + "fieldPath": "icon", + "message": "'icon' should have dimensions 400x401, but the file at './fixtures/check.png' has dimensions 512x512", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "dimensions": { + "height": 401, + "width": 400, + }, + }, + "name": "ValidationError", + }, +] +`; diff --git a/packages/@expo/schemer/src/__tests__/fixtures/app.json b/packages/@expo/schemer/src/__tests__/fixtures/app.json new file mode 100644 index 0000000000000..d8c570263513f --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/fixtures/app.json @@ -0,0 +1,10 @@ +{ + "name": "test app", + "slug": "asdfasdf", + "sdkVersion": "17.0.0", + "orientation": "portrait", + "android": { + "package": "com.yourcompany.yourappname" + }, + "icon": "https://upload.wikimedia.org/wikipedia/commons/0/0f/Icon_Pinguin_2_512x512.png" +} diff --git a/packages/@expo/schemer/src/__tests__/fixtures/bad.json b/packages/@expo/schemer/src/__tests__/fixtures/bad.json new file mode 100644 index 0000000000000..c4c733e844ebd --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/fixtures/bad.json @@ -0,0 +1,9 @@ +{ + "sdkVersion": "17.0.0abad", + "orientaasdfasdftion": "portrait", + "asdfasdfandroid": { + "package": "com.yourcompany.yourappname" + }, + "slug": "1*@)#($*@)(#$*)", + "icon": "DoesNotExist.png" +} diff --git a/packages/@expo/schemer/src/__tests__/fixtures/badwithnot.json b/packages/@expo/schemer/src/__tests__/fixtures/badwithnot.json new file mode 100644 index 0000000000000..d8f546c20d735 --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/fixtures/badwithnot.json @@ -0,0 +1,11 @@ +{ + "name": "test app", + "slug": "asdfasdf", + "sdkVersion": "17.0.0", + "orientation": "portrait", + "android": { + "package": "com.yourcompany.yourappname" + }, + "runtimeVersion": "1.0", + "icon": "https://upload.wikimedia.org/wikipedia/commons/0/0f/Icon_Pinguin_2_512x512.png" +} diff --git a/packages/@expo/schemer/src/__tests__/fixtures/check.png b/packages/@expo/schemer/src/__tests__/fixtures/check.png new file mode 100644 index 0000000000000..b54421b580521 Binary files /dev/null and b/packages/@expo/schemer/src/__tests__/fixtures/check.png differ diff --git a/packages/@expo/schemer/src/__tests__/fixtures/invalidAppIcon.json b/packages/@expo/schemer/src/__tests__/fixtures/invalidAppIcon.json new file mode 100644 index 0000000000000..806737806285b --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/fixtures/invalidAppIcon.json @@ -0,0 +1,10 @@ +{ + "name": "test app", + "slug": "asdfasdf", + "sdkVersion": "17.0.0", + "orientation": "portrait", + "android": { + "package": "com.yourcompany.yourappname" + }, + "icon": "./unknown/path.png" +} diff --git a/packages/@expo/schemer/src/__tests__/fixtures/schema.json b/packages/@expo/schemer/src/__tests__/fixtures/schema.json new file mode 100644 index 0000000000000..6a300303f80d1 --- /dev/null +++ b/packages/@expo/schemer/src/__tests__/fixtures/schema.json @@ -0,0 +1,1371 @@ +{ + "schema": { + "definitions": { + "Android": { + "description": "Configuration that is specific to the Android platform.", + "type": "object", + "meta": { + "standaloneOnly": true + }, + "properties": { + "publishManifestPath": { + "description": "The manifest for the Android version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "publishBundlePath": { + "description": "The bundle for the Android version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "package": { + "description": "The package name for your Android standalone app. You make it up, but it needs to be unique on the Play Store. See [this StackOverflow question](http://stackoverflow.com/questions/6273892/android-package-name-convention).", + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9\\_]*(\\.[a-zA-Z][a-zA-Z0-9\\_]*)+$", + "meta": { + "regexHuman": "Reverse DNS notation unique name for your app. Valid Android Application ID. For example, `com.example.app`, where `com.example` is our domain and `app` is our app. The name may only contain lowercase and uppercase letters (a-z, A-Z), numbers (0-9) and underscores (_), separated by periods (.). Each component of the name should start with a lowercase letter.", + "bareWorkflow": "This is set in `android/app/build.gradle` as `applicationId` as well as in your `AndroidManifest.xml` file (multiple places)." + } + }, + "versionCode": { + "description": "Version number required by Google Play. Increment by one for each release. Must be a positive integer. [Learn more](https://developer.android.com/studio/publish/versioning.html)", + "type": "integer", + "minimum": 0, + "maximum": 2100000000, + "meta": { + "bareWorkflow": "This is set in `android/app/build.gradle` as `versionCode`" + } + }, + "backgroundColor": { + "description": "The background color for your Android app, behind any of your React views. Overrides the top-level `backgroundColor` key if it is present.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`", + "bareWorkflow": "This is set in `android/app/src/main/AndroidManifest.xml` under `android:windowBackground`" + } + }, + "userInterfaceStyle": { + "description": "Configuration to force the app to always use the light or dark user-interface appearance, such as \"dark mode\", or make it automatically adapt to the system preferences. If not provided, defaults to `light`. Requires `expo-system-ui` be installed in your project to work on Android.", + "type": "string", + "fallback": "light", + "enum": ["light", "dark", "automatic"] + }, + "useNextNotificationsApi": { + "deprecated": true, + "description": "@deprecated A Boolean value that indicates whether the app should use the new notifications API.", + "type": "boolean", + "fallback": false + }, + "icon": { + "description": "Local path or remote URL to an image to use for your app's icon on Android. If specified, this overrides the top-level `icon` key. We recommend that you use a 1024x1024 png file (transparency is recommended for the Google Play Store). This icon will appear on the home screen and within the Expo app.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "adaptiveIcon": { + "description": "Settings for an Adaptive Launcher Icon on Android. [Learn more](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)", + "type": "object", + "properties": { + "foregroundImage": { + "description": "Local path or remote URL to an image to use for your app's icon on Android. If specified, this overrides the top-level `icon` and the `android.icon` keys. Should follow the [specified guidelines](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive). This icon will appear on the home screen.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "backgroundImage": { + "description": "Local path or remote URL to a background image for your app's Adaptive Icon on Android. If specified, this overrides the `backgroundColor` key. Must have the same dimensions as foregroundImage`, and has no effect if `foregroundImage` is not specified. Should follow the [specified guidelines](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive).", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "backgroundColor": { + "description": "Color to use as the background for your app's Adaptive Icon on Android. Defaults to white, `#FFFFFF`. Has no effect if `foregroundImage` is not specified.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + } + }, + "additionalProperties": false + }, + "playStoreUrl": { + "description": "URL to your app on the Google Play Store, if you have deployed it there. This is used to link to your store page from your Expo project page if your app is public.", + "pattern": "^https://play\\.google\\.com/", + "example": "https://play.google.com/store/apps/details?id=host.exp.exponent", + "type": ["string"] + }, + "permissions": { + "description": "List of permissions used by the standalone app. \n\n To use ONLY the following minimum necessary permissions and none of the extras supported by Expo in a default managed app, set `permissions` to `[]`. The minimum necessary permissions do not require a Privacy Policy when uploading to Google Play Store and are: \nā€¢ receive data from Internet \nā€¢ view network connections \nā€¢ full network access \nā€¢ change your audio settings \nā€¢ prevent device from sleeping \n\n To use ALL permissions supported by Expo by default, do not specify the `permissions` key. \n\n To use the minimum necessary permissions ALONG with certain additional permissions, specify those extras in `permissions`, e.g.\n\n `[ \"CAMERA\", \"ACCESS_FINE_LOCATION\" ]`.\n\n You can specify the following permissions depending on what you need:\n\n- `ACCESS_COARSE_LOCATION`\n- `ACCESS_FINE_LOCATION`\n- `ACCESS_BACKGROUND_LOCATION`\n- `CAMERA`\n- `RECORD_AUDIO`\n- `READ_CONTACTS`\n- `WRITE_CONTACTS`\n- `READ_CALENDAR`\n- `WRITE_CALENDAR`\n- `READ_EXTERNAL_STORAGE`\n- `WRITE_EXTERNAL_STORAGE`\n- `USE_FINGERPRINT`\n- `USE_BIOMETRIC`\n- `WRITE_SETTINGS`\n- `VIBRATE`\n- `READ_PHONE_STATE`\n- `FOREGROUND_SERVICE`\n- `WAKE_LOCK`\n- `com.anddoes.launcher.permission.UPDATE_COUNT`\n- `com.android.launcher.permission.INSTALL_SHORTCUT`\n- `com.google.android.c2dm.permission.RECEIVE`\n- `com.google.android.gms.permission.ACTIVITY_RECOGNITION`\n- `com.google.android.providers.gsf.permission.READ_GSERVICES`\n- `com.htc.launcher.permission.READ_SETTINGS`\n- `com.htc.launcher.permission.UPDATE_SHORTCUT`\n- `com.majeur.launcher.permission.UPDATE_BADGE`\n- `com.sec.android.provider.badge.permission.READ`\n- `com.sec.android.provider.badge.permission.WRITE`\n- `com.sonyericsson.home.permission.BROADCAST_BADGE`\n", + "type": "array", + "meta": { + "bareWorkflow": "To change the permissions your app requests, you'll need to edit `AndroidManifest.xml` manually. To prevent your app from requesting one of the permissions listed below, you'll need to explicitly add it to `AndroidManifest.xml` along with a `tools:node=\"remove\"` tag." + }, + "items": { + "type": "string" + } + }, + "googleServicesFile": { + "description": "[Firebase Configuration File](https://support.google.com/firebase/answer/7015592) Location of the `GoogleService-Info.plist` file for configuring Firebase. Including this key automatically enables FCM in your standalone app.", + "type": "string", + "meta": { + "bareWorkflow": "Add or edit the file directly at `android/app/google-services.json`" + } + }, + "config": { + "type": "object", + "description": "Note: This property key is not included in the production manifest and will evaluate to `undefined`. It is used internally only in the build process, because it contains API keys that some may want to keep private.", + "properties": { + "branch": { + "description": "[Branch](https://branch.io/) key to hook up Branch linking services.", + "type": "object", + "properties": { + "apiKey": { + "description": "Your Branch API key", + "type": "string" + } + }, + "additionalProperties": false + }, + "googleMaps": { + "description": "[Google Maps Android SDK](https://developers.google.com/maps/documentation/android-api/signup) configuration for your standalone app.", + "type": "object", + "properties": { + "apiKey": { + "description": "Your Google Maps Android SDK API key", + "type": "string" + } + }, + "additionalProperties": false + }, + "googleMobileAdsAppId": { + "description": "[Google Mobile Ads App ID](https://support.google.com/admob/answer/6232340) Google AdMob App ID. ", + "type": "string" + }, + "googleMobileAdsAutoInit": { + "description": "A boolean indicating whether to initialize Google App Measurement and begin sending user-level event data to Google immediately when the app starts. The default in Expo (Client and in standalone apps) is `false`. [Sets the opposite of the given value to the following key in `Info.plist`](https://developers.google.com/admob/ios/eu-consent#delay_app_measurement_optional)", + "type": "boolean", + "fallback": false + }, + "googleSignIn": { + "deprecated": true, + "meta": { + "deprecated": true + }, + "description": "@deprecated Use `googleServicesFile` instead. [Google Sign-In Android SDK](https://developers.google.com/identity/sign-in/android/start-integrating) keys for your standalone app.", + "type": "object", + "properties": { + "apiKey": { + "description": "The Android API key. Can be found in the credentials section of the developer console or in `google-services.json`.", + "type": "string" + }, + "certificateHash": { + "description": "The SHA-1 hash of the signing certificate used to build the APK without any separator (`:`). Can be found in `google-services.json`. https://developers.google.com/android/guides/client-auth", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "splash": { + "description": "Configuration for loading and splash screen for managed and standalone Android apps.", + "type": "object", + "properties": { + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover`, `contain` or `native`, defaults to `contain`.", + "enum": ["cover", "contain", "native"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "mdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Natural sized image (baseline)`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "hdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 1.5x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "xhdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 2x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "xxhdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 3x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "xxxhdpi": { + "description": "Local path or remote URL to an image to fill the background of the loading screen in \"native\" mode. Image size and aspect ratio are up to you. [Learn more]( https://developer.android.com/training/multiscreen/screendensities) \n\n `Scale 4x`", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + } + }, + "intentFilters": { + "description": "Configuration for setting an array of custom intent filters in Android manifest. [Learn more](https://developer.android.com/guide/components/intents-filters)", + "example": [ + { + "autoVerify": true, + "action": "VIEW", + "data": { + "scheme": "https", + "host": "*.example.com" + }, + "category": ["BROWSABLE", "DEFAULT"] + } + ], + "exampleString": "\n [{ \n \"autoVerify\": true, \n \"action\": \"VIEW\", \n \"data\": { \n \"scheme\": \"https\", \n \"host\": \"*.example.com\" \n }, \n \"category\": [\"BROWSABLE\", \"DEFAULT\"] \n }]", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "autoVerify": { + "description": "You may also use an intent filter to set your app as the default handler for links (without showing the user a dialog with options). To do so use `true` and then configure your server to serve a JSON file verifying that you own the domain. [Learn more](https://developer.android.com/training/app-links)", + "type": "boolean" + }, + "action": { "type": "string" }, + "data": { + "anyOf": [ + { "$ref": "#/definitions/AndroidIntentFiltersData" }, + { + "type": ["array"], + "items": { + "$ref": "#/definitions/AndroidIntentFiltersData" + } + } + ] + }, + "category": { + "anyOf": [ + { + "type": ["string"] + }, + { + "type": "array", + "items": { "type": "string" } + } + ] + } + }, + "additionalProperties": false, + "required": ["action"] + }, + "meta": { + "bareWorkflow": "This is set in `AndroidManifest.xml` directly. [Learn more.](https://developer.android.com/guide/components/intents-filters)" + } + }, + "allowBackup": { + "description": "Allows your user's app data to be automatically backed up to their Google Drive. If this is set to false, no backup or restore of the application will ever be performed (this is useful if your app deals with sensitive information). Defaults to the Android default, which is `true`.", + "fallback": true, + "type": "boolean" + }, + "softwareKeyboardLayoutMode": { + "description": "Determines how the software keyboard will impact the layout of your application. This maps to the `android:windowSoftInputMode` property. Defaults to `resize`. Valid values: `resize`, `pan`.", + "enum": ["resize", "pan"], + "type": "string", + "fallback": "resize" + }, + "jsEngine": { + "description": "Specifies the JavaScript engine for Android apps. Supported only on EAS Build and in Expo Go. Defaults to `jsc`. Valid values: `hermes`, `jsc`.", + "type": "string", + "fallback": "jsc", + "enum": ["hermes", "jsc"], + "meta": { + "bareWorkflow": "To change the JavaScript engine, update the `expo.jsEngine` value in `android/gradle.properties`" + } + }, + "runtimeVersion": { + "description": "**Note: Don't use this property unless you are sure what you're doing** \n\nThe runtime version associated with this manifest for the Android platform. If provided, this will override the top level runtimeVersion key.\nSet this to `{\"policy\": \"nativeVersion\"}` to generate it automatically.", + "$ref": "#/definitions/RuntimeVersion" + } + }, + "additionalProperties": false + }, + "AndroidIntentFiltersData": { + "type": "object", + "properties": { + "scheme": { + "description": "Scheme of the URL, e.g. `https`", + "type": "string" + }, + "host": { "description": "Hostname, e.g. `myapp.io`", "type": "string" }, + "port": { "description": "Port, e.g. `3000`", "type": "string" }, + "path": { + "description": "Exact path for URLs that should be matched by the filter, e.g. `/records`", + "type": "string" + }, + "pathPattern": { + "description": "Pattern for paths that should be matched by the filter, e.g. `.*`. Must begin with `/`", + "type": "string" + }, + "pathPrefix": { + "description": "Prefix for paths that should be matched by the filter, e.g. `/records/` will match `/records/123`", + "type": "string" + }, + "mimeType": { + "description": "MIME type for URLs that should be matched by the filter", + "type": "string" + } + }, + "additionalProperties": false + }, + "IOS": { + "description": "Configuration that is specific to the iOS platform.", + "type": "object", + "meta": { + "standaloneOnly": true + }, + "properties": { + "publishManifestPath": { + "description": "The manifest for the iOS version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "publishBundlePath": { + "description": "The bundle for the iOS version of your app will be written to this path during publish.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "bundleIdentifier": { + "description": "The bundle identifier for your iOS standalone app. You make it up, but it needs to be unique on the App Store. See [this StackOverflow question](http://stackoverflow.com/questions/11347470/what-does-bundle-identifier-mean-in-the-ios-project).", + "type": "string", + "pattern": "^[a-zA-Z0-9.-]+$", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `CFBundleIdentifier`", + "regexHuman": "iOS bundle identifier notation unique name for your app. For example, `host.exp.expo`, where `exp.host` is our domain and `expo` is our app name." + } + }, + "buildNumber": { + "description": "Build number for your iOS standalone app. Corresponds to `CFBundleVersion` and must match Apple's [specified format](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102364). (Note: Transporter will pull the value for `Version Number` from `expo.version` and NOT from `expo.ios.buildNumber`.)", + "type": "string", + "pattern": "^[A-Za-z0-9\\.]+$", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `CFBundleVersion`" + } + }, + "backgroundColor": { + "description": "The background color for your iOS app, behind any of your React views. Overrides the top-level `backgroundColor` key if it is present. Requires `expo-system-ui` be installed in your project to work on iOS.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "icon": { + "description": "Local path or remote URL to an image to use for your app's icon on iOS. If specified, this overrides the top-level `icon` key. Use a 1024x1024 icon which follows Apple's interface guidelines for icons, including color profile and transparency. \n\n Expo will generate the other required sizes. This icon will appear on the home screen and within the Expo app.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "merchantId": { + "description": "@deprecated Merchant ID for use with Apple Pay in your standalone app.", + "type": "string", + "meta": { + "deprecated": true + } + }, + "appStoreUrl": { + "description": "URL to your app on the Apple App Store, if you have deployed it there. This is used to link to your store page from your Expo project page if your app is public.", + "pattern": "^https://(itunes|apps)\\.apple\\.com/.*?\\d+", + "example": "https://apps.apple.com/us/app/expo-client/id982107779", + "type": ["string"] + }, + "bitcode": { + "description": "Enable iOS Bitcode optimizations in the native build. Accepts the name of an iOS build configuration to enable for a single configuration and disable for all others, e.g. Debug, Release. Not available in the classic 'expo build:ios' or Expo Go. Defaults to `undefined` which uses the template's predefined settings.", + "anyOf": [ + { + "type": ["boolean"] + }, + { + "type": ["string"] + } + ] + }, + "config": { + "type": "object", + "description": "Note: This property key is not included in the production manifest and will evaluate to `undefined`. It is used internally only in the build process, because it contains API keys that some may want to keep private.", + "properties": { + "branch": { + "description": "[Branch](https://branch.io/) key to hook up Branch linking services.", + "type": "object", + "properties": { + "apiKey": { + "description": "Your Branch API key", + "type": "string" + } + }, + "additionalProperties": false + }, + "usesNonExemptEncryption": { + "description": "Sets `ITSAppUsesNonExemptEncryption` in the standalone ipa's Info.plist to the given boolean value.", + "type": "boolean" + }, + "googleMapsApiKey": { + "description": "[Google Maps iOS SDK](https://developers.google.com/maps/documentation/ios-sdk/start) key for your standalone app.", + "type": "string" + }, + "googleMobileAdsAppId": { + "description": "[Google Mobile Ads App ID](https://support.google.com/admob/answer/6232340) Google AdMob App ID. ", + "type": "string" + }, + "googleMobileAdsAutoInit": { + "description": "A boolean indicating whether to initialize Google App Measurement and begin sending user-level event data to Google immediately when the app starts. The default in Expo (Go and in standalone apps) is `false`. [Sets the opposite of the given value to the following key in `Info.plist`.](https://developers.google.com/admob/ios/eu-consent#delay_app_measurement_optional)", + "type": "boolean", + "fallback": false + }, + "googleSignIn": { + "description": "@deprecated Use `ios.googleServicesFile` instead.", + "type": "object", + "meta": { + "deprecated": true + }, + "properties": { + "reservedClientId": { + "description": "@deprecated Use `ios.googleServicesFile` instead.", + "type": "string", + "meta": { + "deprecated": true + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "googleServicesFile": { + "description": "[Firebase Configuration File](https://support.google.com/firebase/answer/7015592) Location of the `GoogleService-Info.plist` file for configuring Firebase.", + "type": "string" + }, + "supportsTablet": { + "description": "Whether your standalone iOS app supports tablet screen sizes. Defaults to `false`.", + "type": "boolean", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `UISupportedInterfaceOrientations~ipad`" + } + }, + "isTabletOnly": { + "description": "If true, indicates that your standalone iOS app does not support handsets, and only supports tablets.", + "type": "boolean", + "meta": { + "bareWorkflow": "Set this value in `info.plist` under `UISupportedInterfaceOrientations`" + } + }, + "requireFullScreen": { + "description": "If true, indicates that your standalone iOS app does not support Slide Over and Split View on iPad. Defaults to `false`", + "type": "boolean", + "meta": { + "bareWorkflow": "Use Xcode to set `UIRequiresFullScreen`" + } + }, + "userInterfaceStyle": { + "description": "Configuration to force the app to always use the light or dark user-interface appearance, such as \"dark mode\", or make it automatically adapt to the system preferences. If not provided, defaults to `light`.", + "type": "string", + "fallback": "light", + "enum": ["light", "dark", "automatic"] + }, + "infoPlist": { + "description": "Dictionary of arbitrary configuration to add to your standalone app's native Info.plist. Applied prior to all other Expo-specific configuration. No other validation is performed, so use this at your own risk of rejection from the App Store.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "entitlements": { + "description": "Dictionary of arbitrary configuration to add to your standalone app's native *.entitlements (plist). Applied prior to all other Expo-specific configuration. No other validation is performed, so use this at your own risk of rejection from the App Store.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "associatedDomains": { + "description": "An array that contains Associated Domains for the standalone app. [Learn more](https://developer.apple.com/documentation/safariservices/supporting_associated_domains).", + "type": "array", + "uniqueItems": true, + "items": { "type": "string" }, + "meta": { + "regexHuman": "Entries must follow the format `applinks:[:port number]`. [Learn more](https://developer.apple.com/documentation/safariservices/supporting_associated_domains).", + "bareWorkflow": "Build with EAS, or use Xcode to enable this capability manually. [Learn more](https://developer.apple.com/documentation/safariservices/supporting_associated_domains)." + } + }, + "usesIcloudStorage": { + "description": "A boolean indicating if the app uses iCloud Storage for `DocumentPicker`. See `DocumentPicker` docs for details.", + "type": "boolean", + "meta": { + "bareWorkflow": "Use Xcode, or ios.entitlements to configure this." + } + }, + "usesAppleSignIn": { + "description": "A boolean indicating if the app uses Apple Sign-In. See `AppleAuthentication` docs for details.", + "type": "boolean", + "fallback": false + }, + "accessesContactNotes": { + "description": "A Boolean value that indicates whether the app may access the notes stored in contacts. You must [receive permission from Apple](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes) before you can submit your app for review with this capability.", + "type": "boolean", + "fallback": false + }, + "splash": { + "description": "Configuration for loading and splash screen for standalone iOS apps.", + "type": "object", + "properties": { + "xib": { + "description": "@deprecated Apple has deprecated `.xib` splash screens in favor of `.storyboard` files. Local path to a XIB file as the loading screen. It overrides other loading screen options. Note: This will only be used in the standalone app (i.e., after you build the app). It will not be used in the Expo Go.", + "type": "string", + "meta": { + "deprecated": true, + "asset": true, + "contentTypePattern": "^text/xml$", + "contentTypeHuman": ".xib interface builder document" + } + }, + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover` or `contain`, defaults to `contain`.", + "enum": ["cover", "contain"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + }, + "tabletImage": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + } + }, + "jsEngine": { + "description": "Specifies the JavaScript engine for iOS apps. Supported only on EAS Build. Defaults to `jsc`. Valid values: `hermes`, `jsc`.", + "type": "string", + "fallback": "jsc", + "enum": ["hermes", "jsc"], + "meta": { + "bareWorkflow": "To change the JavaScript engine, update the `expo.jsEngine` value in `ios/Podfile.properties.json`" + } + }, + "runtimeVersion": { + "description": "**Note: Don't use this property unless you are sure what you're doing** \n\nThe runtime version associated with this manifest for the iOS platform. If provided, this will override the top level runtimeVersion key.\nSet this to `{\"policy\": \"nativeVersion\"}` to generate it automatically.", + "$ref": "#/definitions/RuntimeVersion" + } + }, + "additionalProperties": false + }, + "Splash": { + "description": "Configuration for loading and splash screen for standalone apps.", + "type": "object", + "properties": { + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`", + "bareWorkflow": "For Android, edit the `colorPrimary` item in `android/app/src/main/res/values/colors.xml`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover` or `contain`, defaults to `contain`.", + "enum": ["cover", "contain"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + }, + "meta": { + "bareWorkflow": "To change your app's icon, edit or replace the files in `ios//Assets.xcassets/AppIcon.appiconset` (we recommend using Xcode), and `android/app/src/main/res/mipmap-` (Android Studio can [generate the appropriate image files for you](https://developer.android.com/studio/write/image-asset-studio)). Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each required size." + } + }, + "PublishHook": { + "type": "object", + "additionalProperties": true, + "properties": { + "file": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": true, + "properties": {} + } + } + }, + "Web": { + "description": "Configuration that is specific to the web platform.", + "type": "object", + "additionalProperties": true, + "properties": { + "favicon": { + "description": "Relative path of an image to use for your app's favicon.", + "type": "string" + }, + "name": { + "description": "Defines the title of the document, defaults to the outer level name", + "type": "string", + "meta": { + "pwa": "name" + } + }, + "shortName": { + "description": "A short version of the app's name, 12 characters or fewer. Used in app launcher and new tab pages. Maps to `short_name` in the PWA manifest.json. Defaults to the `name` property.", + "type": "string", + "meta": { + "pwa": "short_name", + "regexHuman": "Maximum 12 characters long" + } + }, + "lang": { + "description": "Specifies the primary language for the values in the name and short_name members. This value is a string containing a single language tag.", + "type": "string", + "fallback": "en", + "meta": { + "pwa": "lang" + } + }, + "scope": { + "description": "Defines the navigation scope of this website's context. This restricts what web pages can be viewed while the manifest is applied. If the user navigates outside the scope, it returns to a normal web page inside a browser tab/window. If the scope is a relative URL, the base URL will be the URL of the manifest.", + "type": "string", + "meta": { + "pwa": "scope" + } + }, + "themeColor": { + "description": "Defines the color of the Android tool bar, and may be reflected in the app's preview in task switchers.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "pwa": "theme_color", + "html": "theme-color", + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "description": { + "description": "Provides a general description of what the pinned website does.", + "type": "string", + "meta": { + "html": "description", + "pwa": "description" + } + }, + "dir": { + "description": "Specifies the primary text direction for the name, short_name, and description members. Together with the lang member, it helps the correct display of right-to-left languages.", + "enum": ["auto", "ltr", "rtl"], + "type": "string", + "meta": { + "pwa": "dir" + } + }, + "display": { + "description": "Defines the developersā€™ preferred display mode for the website.", + "enum": ["fullscreen", "standalone", "minimal-ui", "browser"], + "type": "string", + "meta": { + "pwa": "display" + } + }, + "startUrl": { + "description": "The URL that loads when a user launches the application (e.g., when added to home screen), typically the index. Note: This has to be a relative URL, relative to the manifest URL.", + "type": "string", + "meta": { + "pwa": "start_url" + } + }, + "orientation": { + "description": "Defines the default orientation for all the website's top level browsing contexts.", + "enum": [ + "any", + "natural", + "landscape", + "landscape-primary", + "landscape-secondary", + "portrait", + "portrait-primary", + "portrait-secondary" + ], + "type": "string", + "meta": { + "pwa": "orientation" + } + }, + "backgroundColor": { + "description": "Defines the expected ā€œbackground colorā€ for the website. This value repeats what is already available in the siteā€™s CSS, but can be used by browsers to draw the background color of a shortcut when the manifest is available before the stylesheet has loaded. This creates a smooth transition between launching the web application and loading the site's content.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "pwa": "background_color", + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "barStyle": { + "description": "If content is set to default, the status bar appears normal. If set to black, the status bar has a black background. If set to black-translucent, the status bar is black and translucent. If set to default or black, the web content is displayed below the status bar. If set to black-translucent, the web content is displayed on the entire screen, partially obscured by the status bar.", + "enum": ["default", "black", "black-translucent"], + "type": "string", + "fallback": "black-translucent", + "meta": { + "html": "apple-mobile-web-app-status-bar-style", + "pwa": "name" + } + }, + "preferRelatedApplications": { + "description": "Hints for the user agent to indicate to the user that the specified native applications (defined in expo.ios and expo.android) are recommended over the website.", + "type": "boolean", + "fallback": true, + "meta": { + "pwa": "prefer_related_applications" + } + }, + "dangerous": { + "description": "Experimental features. These will break without deprecation notice.", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "splash": { + "description": "Configuration for PWA splash screens.", + "type": "object", + "properties": { + "backgroundColor": { + "description": "Color to fill the loading screen background", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "resizeMode": { + "description": "Determines how the `image` will be displayed in the splash loading screen. Must be one of `cover` or `contain`, defaults to `contain`.", + "enum": ["cover", "contain"], + "type": "string" + }, + "image": { + "description": "Local path or remote URL to an image to fill the background of the loading screen. Image size and aspect ratio are up to you. Must be a .png.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image" + } + } + }, + "meta": { + "bareWorkflow": "Use [expo-splash-screen](https://github.com/expo/expo/tree/main/packages/expo-splash-screen#expo-splash-screen)" + } + }, + "config": { + "description": "Firebase web configuration. Used by the expo-firebase packages on both web and native. [Learn more](https://firebase.google.com/docs/reference/js/firebase.html#initializeapp)", + "type": "object", + "properties": { + "firebase": { + "type": "object", + "properties": { + "apiKey": { "type": "string" }, + "authDomain": { "type": "string" }, + "databaseURL": { "type": "string" }, + "projectId": { "type": "string" }, + "storageBucket": { "type": "string" }, + "messagingSenderId": { "type": "string" }, + "appId": { "type": "string" }, + "measurementId": { "type": "string" } + } + } + } + } + } + }, + "RuntimeVersion": { + "allOf": [ + { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-zA-Z\\d][a-zA-Z\\d._+()-]{0,254}$", + "meta": { + "regexHuman": "String beginning with an alphanumeric character followed by any combination of alphanumeric character, \"_\", \"+\", \".\",\"(\", \")\", or \"-\". Examples: \"1.0.3a+\", \"1.0.0\", \"._+()-0a1\", \"0\"." + } + }, + { + "type": "string", + "pattern": "^exposdk:((\\d+\\.\\d+\\.\\d+)|(UNVERSIONED))$", + "meta": { + "regexHuman": "An 'exposdk:' prefix followed by the SDK version of your project. Example: \"exposdk:44.0.0\"." + } + }, + { + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": ["nativeVersion", "sdkVersion"] + } + }, + "required": ["policy"], + "additionalProperties": false + } + ] + }, + { + "not": { + "pattern": "^\\d+\\.\\d*0$" + }, + "meta": { + "notHuman": "Not a decimal ending in a 0." + } + } + ] + } + }, + "type": "object", + "properties": { + "name": { + "description": "The name of your app as it appears both within Expo Go and on your home screen as a standalone app.", + "type": "string", + "meta": { + "bareWorkflow": "To change the name of your app, edit the 'Display Name' field in Xcode and the `app_name` string in `android/app/src/main/res/values/strings.xml`" + } + }, + "description": { + "description": "A short description of what your app is and why it is great.", + "type": "string" + }, + "slug": { + "description": "The friendly URL name for publishing. For example, `myAppName` will refer to the `expo.dev/@project-owner/myAppName` project.", + "type": "string", + "pattern": "^[a-zA-Z0-9_\\-]+$" + }, + "owner": { + "description": "The name of the Expo account that owns the project. This is useful for teams collaborating on a project. If not provided, the owner defaults to the username of the current user.", + "type": "string", + "minLength": 1 + }, + "currentFullName": { + "description": "The auto generated Expo account name and slug used for display purposes. Formatted like `@username/slug`. When unauthenticated, the username is `@anonymous`. For published projects, this value may change when a project is transferred between accounts or renamed.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "originalFullName": { + "description": "The auto generated Expo account name and slug used for services like Notifications and AuthSession proxy. Formatted like `@username/slug`. When unauthenticated, the username is `@anonymous`. For published projects, this value will not change when a project is transferred between accounts or renamed.", + "type": "string", + "meta": { + "autogenerated": true + } + }, + "privacy": { + "description": "Defaults to `unlisted`. `unlisted` hides the project from search results. `hidden` restricts access to the project page to only the owner and other users that have been granted access. Valid values: `public`, `unlisted`, `hidden`.", + "enum": ["public", "unlisted", "hidden"], + "type": "string", + "fallback": "unlisted" + }, + "sdkVersion": { + "description": "The Expo sdkVersion to run the project on. This should line up with the version specified in your package.json.", + "type": "string", + "pattern": "^(\\d+\\.\\d+\\.\\d+)|(UNVERSIONED)$" + }, + "runtimeVersion": { + "description": "**Note: Don't use this property unless you are sure what you're doing** \n\nThe runtime version associated with this manifest.\nSet this to `{\"policy\": \"nativeVersion\"}` to generate it automatically.", + "$ref": "#/definitions/RuntimeVersion" + }, + "version": { + "description": "Your app version. In addition to this field, you'll also use `ios.buildNumber` and `android.versionCode` ā€” read more about how to version your app [here](https://docs.expo.dev/distribution/app-stores/#versioning-your-app). On iOS this corresponds to `CFBundleShortVersionString`, and on Android, this corresponds to `versionName`. The required format can be found [here](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring).", + "type": "string", + "meta": { + "bareWorkflow": "To change your app version, edit the 'Version' field in Xcode and the `versionName` string in `android/app/build.gradle`" + } + }, + "platforms": { + "description": "Platforms that your project explicitly supports. If not specified, it defaults to `[\"ios\", \"android\"]`.", + "example": ["ios", "android", "web"], + "type": "array", + "uniqueItems": true, + "items": { "type": "string", "enum": ["android", "ios", "web"] } + }, + "githubUrl": { + "description": "If you would like to share the source code of your app on Github, enter the URL for the repository here and it will be linked to from your Expo project page.", + "pattern": "^https://github\\.com/", + "example": "https://github.com/expo/expo", + "type": ["string"] + }, + "orientation": { + "description": "Locks your app to a specific orientation with portrait or landscape. Defaults to no lock. Valid values: `default`, `portrait`, `landscape`", + "enum": ["default", "portrait", "landscape"], + "type": "string" + }, + "userInterfaceStyle": { + "description": "Configuration to force the app to always use the light or dark user-interface appearance, such as \"dark mode\", or make it automatically adapt to the system preferences. If not provided, defaults to `light`. Requires `expo-system-ui` be installed in your project to work on Android.", + "type": "string", + "fallback": "light", + "enum": ["light", "dark", "automatic"] + }, + "backgroundColor": { + "description": "The background color for your app, behind any of your React views. This is also known as the root view background color. Requires `expo-system-ui` be installed in your project to work on iOS.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`. Default is white: `'#ffffff'`" + } + }, + "primaryColor": { + "description": "On Android, this will determine the color of your app in the multitasker. Currently this is not used on iOS, but it may be used for other purposes in the future.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "icon": { + "description": "Local path or remote URL to an image to use for your app's icon. We recommend that you use a 1024x1024 png file. This icon will appear on the home screen and within the Expo app.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true, + "bareWorkflow": "To change your app's icon, edit or replace the files in `ios//Assets.xcassets/AppIcon.appiconset` (we recommend using Xcode), and `android/app/src/main/res/mipmap-`. Be sure to follow the guidelines for each platform ([iOS](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/), [Android 7.1 and below](https://material.io/design/iconography/#icon-treatments), and [Android 8+](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)) and to provide your new icon in each existing size." + } + }, + "notification": { + "description": "Configuration for remote (push) notifications.", + "type": "object", + "properties": { + "icon": { + "description": "(Android only) Local path or remote URL to an image to use as the icon for push notifications. 96x96 png grayscale with transparency. We recommend following [Google's design guidelines](https://material.io/design/iconography/product-icons.html#design-principles). If not provided, defaults to your app icon.", + "type": "string", + "meta": { + "asset": true, + "contentTypePattern": "^image/png$", + "contentTypeHuman": ".png image", + "square": true + } + }, + "color": { + "description": "(Android only) Tint color for the push notification image when it appears in the notification tray. Defaults to `#ffffff`", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + }, + "iosDisplayInForeground": { + "description": "Whether or not to display notifications when the app is in the foreground on iOS. `_displayInForeground` option in the individual push notification message overrides this option. [Learn more.](https://docs.expo.dev/push-notifications/receiving-notifications/#foreground-notification-behavior) Defaults to `false`.", + "type": "boolean" + }, + "androidMode": { + "description": "Show each push notification individually (`default`) or collapse into one (`collapse`).", + "enum": ["default", "collapse"], + "type": "string" + }, + "androidCollapsedTitle": { + "description": "If `androidMode` is set to `collapse`, this title is used for the collapsed notification message. For example, `'#{unread_notifications} new interactions'`.", + "type": "string" + } + }, + "additionalProperties": false + }, + "appKey": { + "description": "@deprecated By default, Expo looks for the application registered with the AppRegistry as `main`. If you would like to change this, you can specify the name in this property.", + "meta": { + "deprecated": true + }, + "type": "string" + }, + "androidStatusBar": { + "description": "Configuration for the status bar on Android. For more details please navigate to [Configuring StatusBar](https://docs.expo.dev/guides/configuring-statusbar/).", + "type": "object", + "properties": { + "barStyle": { + "description": "Configures the status bar icons to have a light or dark color. Valid values: `light-content`, `dark-content`. Defaults to `dark-content`", + "type": "string", + "enum": ["light-content", "dark-content"] + }, + "backgroundColor": { + "description": "Specifies the background color of the status bar. Defaults to `#00000000` (transparent) for `dark-content` bar style and `#00000088` (semi-transparent black) for `light-content` bar style", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string `'#RRGGBB'`, for example, `'#000000'` for black. Or 8 character long hex color string `'#RRGGBBAA'`, for example, `'#00000088'` for semi-transparent black." + } + }, + "hidden": { + "description": "Instructs the system whether the status bar should be visible or not. Defaults to `false`", + "type": "boolean" + }, + "translucent": { + "description": "Sets `android:windowTranslucentStatus` in `styles.xml`. When false, the system status bar pushes the content of your app down (similar to `position: relative`). When true, the status bar floats above the content in your app (similar to `position: absolute`). Defaults to `true` to match the iOS status bar behavior (which can only float above content).", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "androidNavigationBar": { + "description": "Configuration for the bottom navigation bar on Android. Can be used to configure the `expo-navigation-bar` module in EAS Build.", + "type": "object", + "properties": { + "visible": { + "description": "Determines how and when the navigation bar is shown. [Learn more](https://developer.android.com/training/system-ui/immersive). Requires `expo-navigation-bar` be installed in your project. Valid values: `leanback`, `immersive`, `sticky-immersive` \n\n `leanback` results in the navigation bar being hidden until the first touch gesture is registered. \n\n `immersive` results in the navigation bar being hidden until the user swipes up from the edge where the navigation bar is hidden. \n\n `sticky-immersive` is identical to `'immersive'` except that the navigation bar will be semi-transparent and will be hidden again after a short period of time.", + "type": "string", + "enum": ["leanback", "immersive", "sticky-immersive"] + }, + "barStyle": { + "description": "Configure the navigation bar icons to have a light or dark color. Supported on Android Oreo and newer. Valid values: `'light-content'`, `'dark-content'`", + "type": "string", + "enum": ["light-content", "dark-content"] + }, + "backgroundColor": { + "description": "Specifies the background color of the navigation bar.", + "type": "string", + "pattern": "^#|(#)\\d{6}$", + "meta": { + "regexHuman": "6 character long hex color string, for example, `'#000000'`" + } + } + }, + "additionalProperties": false + }, + "developmentClient": { + "description": "Settings that apply specifically to running this app in a development client", + "type": "object", + "properties": { + "silentLaunch": { + "description": "If true, the app will launch in a development client with no additional dialogs or progress indicators, just like in a standalone app.", + "type": "boolean", + "fallback": false + } + }, + "additionalProperties": false + }, + "scheme": { + "description": "**Standalone Apps Only**. URL scheme to link into your app. For example, if we set this to `'demo'`, then demo:// URLs would open your app when tapped.", + "type": "string", + "pattern": "^[a-z][a-z0-9+.-]*$", + "meta": { + "regexHuman": "String beginning with a **lowercase** letter followed by any combination of **lowercase** letters, digits, \"+\", \".\" or \"-\"", + "standaloneOnly": true, + "bareWorkflow": "To change your app's scheme, replace all occurrences of the old scheme in `Info.plist` and `AndroidManifest.xml`" + } + }, + "entryPoint": { + "description": "The relative path to your main JavaScript file.", + "type": "string" + }, + "extra": { + "description": "Any extra fields you want to pass to your experience. Values are accessible via `Expo.Constants.manifest.extra` ([Learn more](https://docs.expo.dev/versions/latest/sdk/constants/#constantsmanifest))", + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "packagerOpts": { + "description": "@deprecated Use a `metro.config.js` file instead. [Learn more](https://docs.expo.dev/guides/customizing-metro/)", + "meta": { + "deprecated": true, + "autogenerated": true + }, + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "updates": { + "description": "Configuration for how and when the app should request OTA JavaScript updates", + "type": "object", + "properties": { + "enabled": { + "description": "If set to false, your standalone app will never download any code, and will only use code bundled locally on the device. In that case, all updates to your app must be submitted through app store review. Defaults to true. (Note: This will not work out of the box with ExpoKit projects)", + "type": "boolean" + }, + "checkAutomatically": { + "description": "By default, Expo will check for updates every time the app is loaded. Set this to `ON_ERROR_RECOVERY` to disable automatic checking unless recovering from an error. Must be one of `ON_LOAD` or `ON_ERROR_RECOVERY`", + "enum": ["ON_ERROR_RECOVERY", "ON_LOAD"], + "type": "string" + }, + "fallbackToCacheTimeout": { + "description": "How long (in ms) to allow for fetching OTA updates before falling back to a cached version of the app. Defaults to 0. Must be between 0 and 300000 (5 minutes).", + "type": "number", + "minimum": 0, + "maximum": 300000 + }, + "url": { + "description": "URL from which expo-updates will fetch update manifests", + "type": "string" + }, + "codeSigningCertificate": { + "description": "Local path of a PEM-formatted X.509 certificate used for requiring and verifying signed Expo updates", + "type": "string" + }, + "codeSigningMetadata": { + "description": "Metadata for `codeSigningCertificate`", + "type": "object", + "properties": { + "alg": { + "description": "Algorithm used to generate manifest code signing signature.", + "enum": ["rsa-v1_5-sha256"], + "type": "string" + }, + "keyid": { + "description": "Identifier for the key in the certificate. Used to instruct signing mechanisms when signing or verifying signatures.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "locales": { + "description": "Provide overrides by locale for System Dialog prompts like Permissions Boxes", + "type": "object", + "properties": {}, + "meta": { + "bareWorkflow": "To add or change language and localization information in your iOS app, you need to use Xcode." + }, + "additionalProperties": { + "type": ["string", "object"] + } + }, + "facebookAppId": { + "description": "Used for all Facebook libraries. Set up your Facebook App ID at https://developers.facebook.com.", + "type": "string", + "pattern": "^[0-9]+$", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookAutoInitEnabled": { + "description": "Whether the Facebook SDK should be initialized automatically. The default in Expo (Client and in standalone apps) is `false`.", + "type": "boolean" + }, + "facebookAutoLogAppEventsEnabled": { + "description": "Whether the Facebook SDK log app events automatically. If you don't set this property, Facebook's default will be used. (Applicable only to standalone apps.) Note: The Facebook SDK must be initialized for app events to work. You may autoinitialize Facebook SDK by setting `facebookAutoInitEnabled` to `true`", + "type": "boolean", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookAdvertiserIDCollectionEnabled": { + "description": "Whether the Facebook SDK should collect advertiser ID properties, like the Apple IDFA and Android Advertising ID, automatically. If you don't set this property, Facebook's default policy will be used. (Applicable only to standalone apps.)", + "type": "boolean", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookDisplayName": { + "description": "Used for native Facebook login.", + "type": "string", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "facebookScheme": { + "description": "Used for Facebook native login. Starts with 'fb' and followed by a string of digits, like 'fb1234567890'. You can find your scheme [here](https://developers.facebook.com/docs/facebook-login/ios)in the 'Configuring Your info.plist' section (only applicable to standalone apps and custom Expo Go apps).", + "type": "string", + "pattern": "^fb[0-9]+[A-Za-z]*$", + "meta": { + "bareWorkflow": "For details, check the [Facebook iOS SDK documentation](https://developers.facebook.com/docs/facebook-login/ios/#4--configure-your-project) and [Android SDK documentation](https://developers.facebook.com/docs/facebook-login/android#manifest)" + } + }, + "isDetached": { + "description": "Is app detached", + "type": "boolean", + "meta": { + "autogenerated": true + } + }, + "detach": { + "description": "Extra fields needed by detached apps", + "type": "object", + "properties": {}, + "meta": { + "autogenerated": true + }, + "additionalProperties": true + }, + "assetBundlePatterns": { + "description": "An array of file glob strings which point to assets that will be bundled within your standalone app binary. Read more in the [Offline Support guide](https://docs.expo.dev/guides/offline-support/)", + "type": "array", + "items": { "type": "string" } + }, + "plugins": { + "description": "Config plugins for adding extra functionality to your project. [Learn more](https://docs.expo.dev/guides/config-plugins/).", + "meta": { + "bareWorkflow": "Plugins that add modifications can only be used with [prebuilding](https://expo.fyi/prebuilding) and managed EAS Build" + }, + "type": "array", + "items": { + "anyOf": [ + { + "type": ["string"] + }, + { + "type": "array", + "items": [ + { + "type": ["string"] + }, + {} + ], + "additionalItems": false + } + ] + } + }, + "splash": { + "$ref": "#/definitions/Splash" + }, + "jsEngine": { + "description": "Specifies the JavaScript engine for apps. Supported only on EAS Build. Defaults to `jsc`. Valid values: `hermes`, `jsc`.", + "type": "string", + "fallback": "jsc", + "enum": ["hermes", "jsc"], + "meta": { + "bareWorkflow": "To change the JavaScript engine, update the `expo.jsEngine` value in `ios/Podfile.properties.json` or `android/gradle.properties`" + } + }, + "ios": { + "$ref": "#/definitions/IOS" + }, + "android": { + "$ref": "#/definitions/Android" + }, + "web": { + "$ref": "#/definitions/Web" + }, + "hooks": { + "description": "Configuration for scripts to run to hook into the publish process", + "type": "object", + "additionalProperties": false, + "properties": { + "postPublish": { + "type": "array", + "items": { + "$ref": "#/definitions/PublishHook" + } + }, + "postExport": { + "type": "array", + "items": { + "$ref": "#/definitions/PublishHook" + } + } + } + }, + "experiments": { + "description": "Enable experimental features that may be unstable, unsupported, or removed without deprecation notices.", + "type": "object", + "additionalProperties": false, + "properties": { + "turboModules": { + "description": "Enables Turbo Modules, which are a type of native modules that use a different way of communicating between JS and platform code. When installing a Turbo Module you will need to enable this experimental option (the library still needs to be a part of Expo SDK already, like react-native-reanimated v2). Turbo Modules do not support remote debugging and enabling this option will disable remote debugging.", + "type": "boolean", + "fallback": false + } + } + }, + "_internal": { + "description": "Internal properties for developer tools", + "type": "object", + "properties": { + "pluginHistory": { + "description": "List of plugins already run on the config", + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "additionalProperties": true, + "meta": { + "autogenerated": true + } + } + }, + "additionalProperties": false, + "required": ["name", "slug"] + } +} diff --git a/packages/@expo/schemer/src/__tests__/fixtures/secretlyPng.jpg b/packages/@expo/schemer/src/__tests__/fixtures/secretlyPng.jpg new file mode 100644 index 0000000000000..9c34ad30b1c1d Binary files /dev/null and b/packages/@expo/schemer/src/__tests__/fixtures/secretlyPng.jpg differ diff --git a/packages/@expo/schemer/src/__tests__/fixtures/webp.webp b/packages/@expo/schemer/src/__tests__/fixtures/webp.webp new file mode 100644 index 0000000000000..0da983e2ce533 Binary files /dev/null and b/packages/@expo/schemer/src/__tests__/fixtures/webp.webp differ diff --git a/packages/@expo/schemer/src/index.ts b/packages/@expo/schemer/src/index.ts new file mode 100644 index 0000000000000..66dc0066e40da --- /dev/null +++ b/packages/@expo/schemer/src/index.ts @@ -0,0 +1,292 @@ +import Ajv, { ErrorObject, Options } from 'ajv'; +import addFormats from 'ajv-formats'; +import fs from 'fs'; +import traverse from 'json-schema-traverse'; +import path from 'path'; +import imageProbe from 'probe-image-size'; + +import { SchemerError, ValidationError } from './Error'; +import { get, fieldPathToSchema, schemaPointerToFieldPath } from './Util'; + +function lowerFirst(str: string): string { + return str.charAt(0).toLowerCase() + str.slice(1); +} + +type Meta = { + asset?: boolean; + dimensions?: { + width: number; + height: number; + }; + square?: boolean; + contentTypePattern?: string; + contentTypeHuman?: string; +}; + +type SchemerOptions = Options & { + rootDir?: string; +}; + +type AssetField = { fieldPath: string; data: string; meta: Meta }; + +export { SchemerError, ValidationError, ErrorCodes, ErrorCode } from './Error'; +export default class Schemer { + options: SchemerOptions; + ajv: Ajv; + schema: object; + rootDir: string; + manualValidationErrors: ValidationError[]; + // Schema is a JSON Schema object + constructor(schema: object, options: SchemerOptions = {}) { + this.options = { + allErrors: true, + verbose: true, + meta: true, + strict: false, + unicodeRegExp: false, + ...options, + }; + + this.ajv = new Ajv(this.options); + addFormats(this.ajv, { mode: 'full' }); + this.schema = schema; + this.rootDir = this.options.rootDir || __dirname; + this.manualValidationErrors = []; + } + + _formatAjvErrorMessage({ + keyword, + instancePath, + params, + parentSchema, + data, + message, + }: ErrorObject) { + const meta = parentSchema && (parentSchema as any).meta; + // This removes the "." in front of a fieldPath + instancePath = instancePath.slice(1); + switch (keyword) { + case 'additionalProperties': { + return new ValidationError({ + errorCode: 'SCHEMA_ADDITIONAL_PROPERTY', + fieldPath: instancePath, + message: `should NOT have additional property '${(params as any).additionalProperty}'`, + data, + meta, + }); + } + case 'required': + return new ValidationError({ + errorCode: 'SCHEMA_MISSING_REQUIRED_PROPERTY', + fieldPath: instancePath, + message: `is missing required property '${(params as any).missingProperty}'`, + data, + meta, + }); + case 'pattern': { + //@TODO Parse the message in a less hacky way. Perhaps for regex validation errors, embed the error message under the meta tag? + const regexHuman = meta?.regexHuman; + const regexErrorMessage = regexHuman + ? `'${instancePath}' should be a ${regexHuman[0].toLowerCase() + regexHuman.slice(1)}` + : `'${instancePath}' ${message}`; + return new ValidationError({ + errorCode: 'SCHEMA_INVALID_PATTERN', + fieldPath: instancePath, + message: regexErrorMessage, + data, + meta, + }); + } + case 'not': { + const notHuman = meta?.notHuman; + const notHumanErrorMessage = notHuman + ? `'${instancePath}' should be ${notHuman[0].toLowerCase() + notHuman.slice(1)}` + : `'${instancePath}' ${message}`; + return new ValidationError({ + errorCode: 'SCHEMA_INVALID_NOT', + fieldPath: instancePath, + message: notHumanErrorMessage, + data, + meta, + }); + } + default: + return new ValidationError({ + errorCode: 'SCHEMA_VALIDATION_ERROR', + fieldPath: instancePath, + message: message || 'Validation error', + data, + meta, + }); + } + } + + getErrors(): ValidationError[] { + // Convert AJV JSONSchema errors to our ValidationErrors + let valErrors: ValidationError[] = []; + if (this.ajv.errors) { + valErrors = this.ajv.errors.map((error) => this._formatAjvErrorMessage(error)); + } + return [...valErrors, ...this.manualValidationErrors]; + } + + _throwOnErrors() { + // Clean error state after each validation + const errors = this.getErrors(); + if (errors.length > 0) { + this.manualValidationErrors = []; + this.ajv.errors = []; + throw new SchemerError(errors); + } + } + + async validateAll(data: any) { + await this._validateSchemaAsync(data); + await this._validateAssetsAsync(data); + this._throwOnErrors(); + } + + async validateAssetsAsync(data: any) { + await this._validateAssetsAsync(data); + this._throwOnErrors(); + } + + async validateSchemaAsync(data: any) { + await this._validateSchemaAsync(data); + this._throwOnErrors(); + } + + _validateSchemaAsync(data: any) { + this.ajv.validate(this.schema, data); + } + + async _validateAssetsAsync(data: any) { + const assets: AssetField[] = []; + traverse(this.schema, { allKeys: true }, (subSchema, jsonPointer, a, b, c, d, property) => { + if (property && subSchema.meta && subSchema.meta.asset) { + const fieldPath = schemaPointerToFieldPath(jsonPointer); + assets.push({ + fieldPath, + data: get(data, lowerFirst(fieldPath)) || get(data, fieldPath), + meta: subSchema.meta, + }); + } + }); + await Promise.all(assets.map(this._validateAssetAsync.bind(this))); + } + + async _validateImageAsync({ fieldPath, data, meta }: AssetField) { + if (meta && meta.asset && data) { + const { dimensions, square, contentTypePattern }: Meta = meta; + // filePath could be an URL + const filePath = path.resolve(this.rootDir, data); + try { + // This cases on whether filePath is a remote URL or located on the machine + const isLocalFile = fs.existsSync(filePath); + const probeResult = isLocalFile + ? await imageProbe(require('fs').createReadStream(filePath)) + : await imageProbe(data); + + if (!probeResult) { + return; + } + + const { width, height, type, mime } = probeResult; + + const fileExtension = filePath.split('.').pop(); + + if (isLocalFile && mime !== `image/${fileExtension}`) { + this.manualValidationErrors.push( + new ValidationError({ + errorCode: 'FILE_EXTENSION_MISMATCH', + fieldPath, + message: `the file extension should match the content, but the file extension is .${fileExtension} while the file content at '${data}' is of type ${type}`, + data, + meta, + }) + ); + } + + if (contentTypePattern && !mime.match(new RegExp(contentTypePattern))) { + this.manualValidationErrors.push( + new ValidationError({ + errorCode: 'INVALID_CONTENT_TYPE', + fieldPath, + message: `field '${fieldPath}' should point to ${meta.contentTypeHuman} but the file at '${data}' has type ${type}`, + data, + meta, + }) + ); + } + + if (dimensions && (dimensions.height !== height || dimensions.width !== width)) { + this.manualValidationErrors.push( + new ValidationError({ + errorCode: 'INVALID_DIMENSIONS', + fieldPath, + message: `'${fieldPath}' should have dimensions ${dimensions.width}x${dimensions.height}, but the file at '${data}' has dimensions ${width}x${height}`, + data, + meta, + }) + ); + } + + if (square && width !== height) { + this.manualValidationErrors.push( + new ValidationError({ + errorCode: 'NOT_SQUARE', + fieldPath, + message: `image should be square, but the file at '${data}' has dimensions ${width}x${height}`, + data, + meta, + }) + ); + } + } catch { + this.manualValidationErrors.push( + new ValidationError({ + errorCode: 'INVALID_ASSET_URI', + fieldPath, + message: `cannot access file at '${data}'`, + data, + meta, + }) + ); + } + } + } + + async _validateAssetAsync({ fieldPath, data, meta }: AssetField) { + if (meta && meta.asset && data) { + if (meta.contentTypePattern && meta.contentTypePattern.startsWith('^image')) { + await this._validateImageAsync({ fieldPath, data, meta }); + } + } + } + + async validateProperty(fieldPath: string, data: any) { + const subSchema = fieldPathToSchema(this.schema, fieldPath); + this.ajv.validate(subSchema, data); + + if (subSchema.meta && subSchema.meta.asset) { + await this._validateAssetAsync({ fieldPath, data, meta: subSchema.meta }); + } + this._throwOnErrors(); + } + + validateName(name: string) { + return this.validateProperty('name', name); + } + + validateSlug(slug: string) { + return this.validateProperty('slug', slug); + } + + validateSdkVersion(version: string) { + return this.validateProperty('sdkVersion', version); + } + + validateIcon(iconPath: string) { + return this.validateProperty('icon', iconPath); + } +} diff --git a/packages/@expo/schemer/tsconfig.json b/packages/@expo/schemer/tsconfig.json new file mode 100644 index 0000000000000..a1c4e7aec25ec --- /dev/null +++ b/packages/@expo/schemer/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.node", + "include": ["./src"], + "exclude": ["**/__mocks__/*", "**/__tests__/*"], + "compilerOptions": { + "outDir": "build", + "rootDir": "src", + "sourceMap": true + }, +} diff --git a/yarn.lock b/yarn.lock index 65d3e0745dc12..d1ecec10daca7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1480,18 +1480,6 @@ remove-trailing-slash "^0.1.0" uuid "^8.3.2" -"@expo/schemer@1.4.5": - version "1.4.5" - resolved "https://registry.yarnpkg.com/@expo/schemer/-/schemer-1.4.5.tgz#ea466b7793be60af02e429325842d0db150a26e6" - integrity sha512-i96A2GaZWLE7K1McRt8Vf7vsMKzzM/1t+xUXOTdBEiGH2ffiJjU69ufbTI0OwjLFCUCzPI2LzXwAHVnSP+Rkog== - dependencies: - ajv "^8.1.0" - ajv-formats "^2.0.2" - json-schema-traverse "^1.0.0" - lodash "^4.17.21" - probe-image-size "^7.1.0" - read-chunk "^3.2.0" - "@expo/sdk-runtime-versions@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz#d7ebd21b19f1c6b0395e50d78da4416941c57f7c" @@ -4472,6 +4460,13 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/needle@*": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/needle/-/needle-3.2.3.tgz#9d850da6f0f3fb713c2ea184fecd357a025bacbf" + integrity sha512-aUtoZUGROl654rDZlZYPRYaysAOBaVgjnbmYKq3n32afuqFvEts31YGixTebSOCJt7B7qKnHzCzcjbMig5LcQg== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.5.12", "@types/node-fetch@^2.5.7", "@types/node-fetch@^2.5.8", "@types/node-fetch@^2.6.5": version "2.6.9" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.9.tgz#15f529d247f1ede1824f7e7acdaa192d5f28071e" @@ -4539,6 +4534,14 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== +"@types/probe-image-size@^7.2.4": + version "7.2.4" + resolved "https://registry.yarnpkg.com/@types/probe-image-size/-/probe-image-size-7.2.4.tgz#337af8343d60dd8d94a4497435e7ef8a8a8b889b" + integrity sha512-HVqYj3L+D+S/6qpQRv5qMxrD/5pglzZuhP7ZIqgVSZ+Ck4z1TCFkNIRG8WesFueQTqWFTSgkkAl6f8lwxFPQSw== + dependencies: + "@types/needle" "*" + "@types/node" "*" + "@types/progress@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@types/progress/-/progress-2.0.5.tgz#6e0febf3a82cc0ffdc1cebb4e56d6949fd108775" @@ -15213,7 +15216,7 @@ p-timeout@^2.0.1: dependencies: p-finally "^1.0.0" -p-try@^2.0.0, p-try@^2.1.0: +p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== @@ -16919,14 +16922,6 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -read-chunk@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca" - integrity sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ== - dependencies: - pify "^4.0.1" - with-open-file "^0.1.6" - read-pkg-up@^7.0.0, read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -20736,15 +20731,6 @@ window-size@0.1.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= -with-open-file@^0.1.6: - version "0.1.7" - resolved "https://registry.yarnpkg.com/with-open-file/-/with-open-file-0.1.7.tgz#e2de8d974e8a8ae6e58886be4fe8e7465b58a729" - integrity sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA== - dependencies: - p-finally "^1.0.0" - p-try "^2.1.0" - pify "^4.0.1" - wonka@^4.0.14: version "4.0.15" resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89"