miaoda-expo-devkit 0.1.1-beta.95 → 0.1.1-beta.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/lint.js CHANGED
@@ -3221,8 +3221,8 @@ var require_utils = __commonJS({
3221
3221
  }
3222
3222
  return ind;
3223
3223
  }
3224
- function removeDotSegments(path) {
3225
- let input = path;
3224
+ function removeDotSegments(path3) {
3225
+ let input = path3;
3226
3226
  const output = [];
3227
3227
  let nextSlash = -1;
3228
3228
  let len = 0;
@@ -3421,8 +3421,8 @@ var require_schemes = __commonJS({
3421
3421
  wsComponent.secure = void 0;
3422
3422
  }
3423
3423
  if (wsComponent.resourceName) {
3424
- const [path, query] = wsComponent.resourceName.split("?");
3425
- wsComponent.path = path && path !== "/" ? path : void 0;
3424
+ const [path3, query] = wsComponent.resourceName.split("?");
3425
+ wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
3426
3426
  wsComponent.query = query;
3427
3427
  wsComponent.resourceName = void 0;
3428
3428
  }
@@ -6748,12 +6748,12 @@ var require_dist = __commonJS({
6748
6748
  throw new Error(`Unknown format "${name}"`);
6749
6749
  return f;
6750
6750
  };
6751
- function addFormats(ajv, list, fs, exportName) {
6751
+ function addFormats(ajv, list, fs2, exportName) {
6752
6752
  var _a;
6753
6753
  var _b;
6754
6754
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = codegen_1._`require("ajv-formats/dist/formats").${exportName}`;
6755
6755
  for (const f of list)
6756
- ajv.addFormat(f, fs[f]);
6756
+ ajv.addFormat(f, fs2[f]);
6757
6757
  }
6758
6758
  module2.exports = exports2 = formatsPlugin;
6759
6759
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -7141,8 +7141,8 @@ var require_node = __commonJS({
7141
7141
  }
7142
7142
  break;
7143
7143
  case "FILE":
7144
- var fs = require("fs");
7145
- stream2 = new fs.SyncWriteStream(fd2, { autoClose: false });
7144
+ var fs2 = require("fs");
7145
+ stream2 = new fs2.SyncWriteStream(fd2, { autoClose: false });
7146
7146
  stream2._type = "fs";
7147
7147
  break;
7148
7148
  case "PIPE":
@@ -9089,29 +9089,27 @@ var require_has_flag = __commonJS({
9089
9089
  }
9090
9090
  });
9091
9091
 
9092
- // ../../node_modules/.pnpm/supports-color@8.1.1/node_modules/supports-color/index.js
9092
+ // ../../node_modules/.pnpm/supports-color@7.2.0/node_modules/supports-color/index.js
9093
9093
  var require_supports_color = __commonJS({
9094
- "../../node_modules/.pnpm/supports-color@8.1.1/node_modules/supports-color/index.js"(exports2, module2) {
9094
+ "../../node_modules/.pnpm/supports-color@7.2.0/node_modules/supports-color/index.js"(exports2, module2) {
9095
9095
  "use strict";
9096
9096
  var os = require("os");
9097
9097
  var tty = require("tty");
9098
9098
  var hasFlag = require_has_flag();
9099
9099
  var { env } = process;
9100
- var flagForceColor;
9100
+ var forceColor;
9101
9101
  if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
9102
- flagForceColor = 0;
9102
+ forceColor = 0;
9103
9103
  } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
9104
- flagForceColor = 1;
9104
+ forceColor = 1;
9105
9105
  }
9106
- function envForceColor() {
9107
- if ("FORCE_COLOR" in env) {
9108
- if (env.FORCE_COLOR === "true") {
9109
- return 1;
9110
- }
9111
- if (env.FORCE_COLOR === "false") {
9112
- return 0;
9113
- }
9114
- return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
9106
+ if ("FORCE_COLOR" in env) {
9107
+ if (env.FORCE_COLOR === "true") {
9108
+ forceColor = 1;
9109
+ } else if (env.FORCE_COLOR === "false") {
9110
+ forceColor = 0;
9111
+ } else {
9112
+ forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3);
9115
9113
  }
9116
9114
  }
9117
9115
  function translateLevel(level) {
@@ -9125,22 +9123,15 @@ var require_supports_color = __commonJS({
9125
9123
  has16m: level >= 3
9126
9124
  };
9127
9125
  }
9128
- function supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
9129
- const noFlagForceColor = envForceColor();
9130
- if (noFlagForceColor !== void 0) {
9131
- flagForceColor = noFlagForceColor;
9132
- }
9133
- const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
9126
+ function supportsColor(haveStream, streamIsTTY) {
9134
9127
  if (forceColor === 0) {
9135
9128
  return 0;
9136
9129
  }
9137
- if (sniffFlags) {
9138
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
9139
- return 3;
9140
- }
9141
- if (hasFlag("color=256")) {
9142
- return 2;
9143
- }
9130
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
9131
+ return 3;
9132
+ }
9133
+ if (hasFlag("color=256")) {
9134
+ return 2;
9144
9135
  }
9145
9136
  if (haveStream && !streamIsTTY && forceColor === void 0) {
9146
9137
  return 0;
@@ -9157,7 +9148,7 @@ var require_supports_color = __commonJS({
9157
9148
  return 1;
9158
9149
  }
9159
9150
  if ("CI" in env) {
9160
- if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
9151
+ if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
9161
9152
  return 1;
9162
9153
  }
9163
9154
  return min;
@@ -9169,7 +9160,7 @@ var require_supports_color = __commonJS({
9169
9160
  return 3;
9170
9161
  }
9171
9162
  if ("TERM_PROGRAM" in env) {
9172
- const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
9163
+ const version = parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
9173
9164
  switch (env.TERM_PROGRAM) {
9174
9165
  case "iTerm.app":
9175
9166
  return version >= 3 ? 3 : 2;
@@ -9188,17 +9179,14 @@ var require_supports_color = __commonJS({
9188
9179
  }
9189
9180
  return min;
9190
9181
  }
9191
- function getSupportLevel(stream, options = {}) {
9192
- const level = supportsColor(stream, {
9193
- streamIsTTY: stream && stream.isTTY,
9194
- ...options
9195
- });
9182
+ function getSupportLevel(stream) {
9183
+ const level = supportsColor(stream, stream && stream.isTTY);
9196
9184
  return translateLevel(level);
9197
9185
  }
9198
9186
  module2.exports = {
9199
9187
  supportsColor: getSupportLevel,
9200
- stdout: getSupportLevel({ isTTY: tty.isatty(1) }),
9201
- stderr: getSupportLevel({ isTTY: tty.isatty(2) })
9188
+ stdout: translateLevel(supportsColor(true, tty.isatty(1))),
9189
+ stderr: translateLevel(supportsColor(true, tty.isatty(2)))
9202
9190
  };
9203
9191
  }
9204
9192
  });
@@ -9471,9 +9459,9 @@ var require_auth = __commonJS({
9471
9459
  nc = nc + "";
9472
9460
  return padding.substr(0, 8 - nc.length) + nc;
9473
9461
  };
9474
- digest.generate = function(header, user, pass, method, path) {
9462
+ digest.generate = function(header, user, pass, method, path3) {
9475
9463
  var nc = 1, cnonce = null, challenge = digest.parse_header(header);
9476
- var ha1 = md5(user + ":" + challenge.realm + ":" + pass), ha2 = md5(method.toUpperCase() + ":" + path), resp = [ha1, challenge.nonce];
9464
+ var ha1 = md5(user + ":" + challenge.realm + ":" + pass), ha2 = md5(method.toUpperCase() + ":" + path3), resp = [ha1, challenge.nonce];
9477
9465
  if (typeof challenge.qop === "string") {
9478
9466
  cnonce = md5(Math.random().toString(36)).substr(0, 8);
9479
9467
  nc = digest.update_nc(nc);
@@ -9483,7 +9471,7 @@ var require_auth = __commonJS({
9483
9471
  resp = resp.concat(ha2);
9484
9472
  }
9485
9473
  var params = {
9486
- uri: path,
9474
+ uri: path3,
9487
9475
  realm: challenge.realm,
9488
9476
  nonce: challenge.nonce,
9489
9477
  username: user,
@@ -14815,7 +14803,7 @@ var require_package = __commonJS({
14815
14803
  var require_needle = __commonJS({
14816
14804
  "../../node_modules/.pnpm/needle@2.9.1/node_modules/needle/lib/needle.js"(exports2, module2) {
14817
14805
  "use strict";
14818
- var fs = require("fs");
14806
+ var fs2 = require("fs");
14819
14807
  var http = require("http");
14820
14808
  var https = require("https");
14821
14809
  var url = require("url");
@@ -14924,7 +14912,7 @@ var require_needle = __commonJS({
14924
14912
  return cb(given_length);
14925
14913
  if (stream2.end !== void 0 && stream2.end !== Infinity && stream2.start !== void 0)
14926
14914
  return cb(stream2.end + 1 - (stream2.start || 0));
14927
- fs.stat(stream2.path, function(err, stat) {
14915
+ fs2.stat(stream2.path, function(err, stat) {
14928
14916
  cb(stat ? stat.size - (stream2.start || 0) : null);
14929
14917
  });
14930
14918
  }
@@ -15231,7 +15219,7 @@ var require_needle = __commonJS({
15231
15219
  if (err && err.message == "write after end") request.destroy();
15232
15220
  });
15233
15221
  if (config.output && resp.statusCode == 200) {
15234
- var file = fs.createWriteStream(config.output);
15222
+ var file = fs2.createWriteStream(config.output);
15235
15223
  file.on("error", had_error);
15236
15224
  out.on("end", function() {
15237
15225
  if (file.writable) file.end();
@@ -16891,11 +16879,11 @@ var require_Util = __commonJS({
16891
16879
  return get(schema, (0, exports2.fieldPathToSchemaPath)(fieldPath));
16892
16880
  };
16893
16881
  exports2.fieldPathToSchema = fieldPathToSchema;
16894
- function pathToSegments(path) {
16895
- return Array.isArray(path) ? path : path.split(".");
16882
+ function pathToSegments(path3) {
16883
+ return Array.isArray(path3) ? path3 : path3.split(".");
16896
16884
  }
16897
- function get(object, path) {
16898
- const segments = pathToSegments(path);
16885
+ function get(object, path3) {
16886
+ const segments = pathToSegments(path3);
16899
16887
  const length = segments.length;
16900
16888
  let index = 0;
16901
16889
  while (object != null && index < length) {
@@ -17185,18 +17173,205 @@ var require_build = __commonJS({
17185
17173
 
17186
17174
  // src/cli/lint.ts
17187
17175
  var import_node_child_process = require("child_process");
17188
- var import_node_fs = require("fs");
17176
+ var import_node_fs2 = require("fs");
17189
17177
  var import_node_module = require("module");
17190
- var import_node_path = require("path");
17178
+ var import_node_path3 = require("path");
17191
17179
  var import_schemer = __toESM(require_build());
17192
17180
 
17193
17181
  // src/schemas/app-config-schema-v55.json
17194
17182
  var app_config_schema_v55_default = { 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: "A URL-friendly name for your project that is unique across your account.", 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. It is not meant to be set directly. 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. It is not meant to be set directly. 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 } }, 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: "Property indicating compatibility between a build's native code and an OTA update.", tsType: "string | { policy: 'nativeVersion' | 'sdkVersion' | 'appVersion' | 'fingerprint'; }", oneOf: [{ type: "string", pattern: "^[a-zA-Z\\d][a-zA-Z\\d._+()-]{0,254}$", meta: { notHuman: 'String beginning with an alphanumeric character followed by any combination of alphanumeric character, "_", "+", ".","(", ")", or "-". Valid examples: "1.0.3a+", "1.0.0", "0".', regexHuman: 'String beginning with an alphanumeric character followed by any combination of alphanumeric character, "_", "+", ".","(", ")", or "-". Valid examples: "1.0.3a+", "1.0.0", "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", "appVersion", "fingerprint"] } }, required: ["policy"], additionalProperties: false }] }, version: { description: "Your app version. In addition to this field, you'll also use `ios.buildNumber` and `android.versionCode` \u2014 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"]`. If `react-dom` is installed, `web` is also included by default.', 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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 Go 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/<PROJECT-NAME>/Assets.xcassets/AppIcon.appiconset` (we recommend using Xcode), and `android/app/src/main/res/mipmap-<RESOLUTION>`. 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." } }, androidStatusBar: { description: "Configuration for the status bar on Android. For more details 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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: "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). Explicitly setting this property to `true` will add `android:windowTranslucentStatus` to `styles.xml` and may cause unexpected keyboard behavior on Android when using the `softwareKeyboardLayoutMode` set to `resize`. In this case you will have to use `KeyboardAvoidingView` to manage the keyboard layout.", type: "boolean" } }, additionalProperties: false }, androidNavigationBar: { description: "@deprecated Use the `expo-navigation-bar` config plugin instead. Configuration for the bottom navigation bar on Android.", type: "object", meta: { deprecated: true }, 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", meta: { regexHuman: "6 character long hex color string, for example, `'#000000'`" } }, enforceContrast: { description: "Determines whether the operating system should keep the navigation bar translucent to provide contrast between the navigation buttons and app content. Defaults to true.", type: "boolean", fallback: true } }, 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: "URL scheme(s) to link into your app. For example, if we set this to `'demo'`, then demo:// URLs would open your app when tapped. This is a build-time configuration, it has no effect in Expo Go.", oneOf: [{ type: "string", pattern: "^[a-z][a-z0-9+.-]*$" }, { type: "array", items: { 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`" } }, extra: { description: "Any extra fields you want to pass to your experience. Values are accessible via `Constants.expoConfig.extra` ([Learn more](https://docs.expo.dev/versions/latest/sdk/constants/#constantsmanifest))", type: "object", properties: {}, additionalProperties: true }, updates: { description: "Configuration for the expo-updates library", type: "object", properties: { enabled: { description: "Whether the updates system will run. Defaults to true. If set to false, builds will only use code and assets bundled at time of build.", type: "boolean" }, checkAutomatically: { description: "By default, expo-updates 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. Set this to `NEVER` to disable automatic checking. Valid values: `ON_LOAD` (default value), `ON_ERROR_RECOVERY`, `WIFI_ONLY`, `NEVER`", enum: ["ON_ERROR_RECOVERY", "ON_LOAD", "WIFI_ONLY", "NEVER"], type: "string" }, useEmbeddedUpdate: { description: "Whether to load the embedded update. Defaults to true. If set to false, an update will be fetched at launch. When set to false, ensure that `checkAutomatically` is set to `ON_LOAD` and `fallbackToCacheTimeout` is large enough for the initial remote update to download. This should not be used in production.", type: "boolean" }, fallbackToCacheTimeout: { description: "How long (in ms) to wait for the app to check for and fetch a new update upon launch before falling back to the most recent update already present on the device. Defaults to 0. Must be between 0 and 300000 (5 minutes). If the startup update check takes longer than this value, any update downloaded during the check will be applied upon the next app launch.", type: "number", minimum: 0, maximum: 3e5 }, 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 verifying codesigned updates. When provided, all updates downloaded by expo-updates must be signed.", type: "string" }, codeSigningMetadata: { description: "Metadata for `codeSigningCertificate`", type: "object", properties: { alg: { description: "Algorithm used to generate manifest code signing signature. Valid values: `rsa-v1_5-sha256`", 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 }, requestHeaders: { description: "Extra HTTP headers to include in HTTP requests made by `expo-updates` when fetching manifests or assets. These may override preset headers.", type: "object", additionalProperties: true }, assetPatternsToBeBundled: { description: "Array of glob patterns specifying which files should be included in updates. Glob patterns are relative to the project root. A value of `['**']` will match all asset files within the project root. When not supplied all asset files will be included. Example: Given a value of `['app/images/**/*.png', 'app/fonts/**/*.woff']` all `.png` files in all subdirectories of `app/images` and all `.woff` files in all subdirectories of `app/fonts` will be included in updates.", type: "array", items: { type: "string" } }, disableAntiBrickingMeasures: { description: "Whether to disable the built-in expo-updates anti-bricking measures. Defaults to false. If set to true, this will allow overriding certain configuration options from the JS API, which is liable to leave an app in a bricked state if not done carefully. This should not be used in production.", type: "boolean" }, useNativeDebug: { description: "Enable debugging of native code with updates enabled. Defaults to false. If set to true, the EX_UPDATES_NATIVE_DEBUG environment variable will be set in Podfile.properties.json and gradle.properties. This causes Xcode and Android Studio debug builds to be built with expo-updates enabled, and JS debugging (with dev client or packager) disabled. This should not be used in production.", type: "boolean" }, enableBsdiffPatchSupport: { description: "Whether to enable support for downloading and applying bundle diffs using bsdiff. Defaults to false.", type: "boolean" } }, additionalProperties: false }, locales: { description: "Provide per-locale values for System Dialog prompts such as Permissions Boxes, and create Localizable.strings file to localize (for example) push notifications. Platform-specific locale strings should be nested under `ios` and `android` keys.", 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"] } }, assetBundlePatterns: { description: "@deprecated Follow [the guide to select and exclude assets](https://docs.expo.dev/eas-update/asset-selection/) for EAS Update instead. 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/)", meta: { deprecated: true }, 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: { description: "@deprecated Use the `expo-splash-screen` config plugin instead.", meta: { deprecated: true }, type: "object", properties: { backgroundColor: { description: "Color to fill the loading screen background", type: "string", pattern: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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" } } } }, buildCacheProvider: { description: "Enable downloading cached builds from remote.", anyOf: [{ type: "string", enum: ["eas"] }, { type: "object", additionalProperties: false, properties: { plugin: { type: "string" }, options: { type: "object" } }, required: ["plugin"] }] }, ios: { description: "Configuration that is specific to the iOS platform.", type: "object", meta: { standaloneOnly: true }, properties: { appleTeamId: { description: "The Apple development team ID to use for all native targets. You can find your team ID in [the Apple Developer Portal](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/).", type: "string" }, 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/documentation/bundleresources/information_property_list/cfbundleversion). (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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", meta: { regexHuman: "6 character long hex color string, for example, `'#000000'`" } }, scheme: { description: "URL scheme(s) to link into your iOS app. Schemes added to this field will be merged with the schemes in the `scheme` key at the top level of the config.", oneOf: [{ type: "string", pattern: "^[a-z][a-z0-9+.-]*$" }, { type: "array", items: { 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`" } }, icon: { description: "Local path or remote URL to an image to use for your app's icon on iOS. Alternatively, an object specifying different icons for various system appearances (e.g., dark, tinted) can be provided. You can also provide a path to a .icon directory. 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\nExpo will generate the other required sizes. This icon will appear on the home screen and within the Expo Go app.", oneOf: [{ type: "string", pattern: "\\.icon$", meta: { asset: true, contentTypePattern: "directory", contentTypeHuman: "a .icon directory" } }, { type: "string", not: { pattern: "\\.icon$" }, meta: { asset: true, contentTypePattern: "^image/png$", contentTypeHuman: ".png image", square: true } }, { description: "Configuration that is specific to the iOS platform icons.", type: "object", additionalProperties: false, properties: { light: { description: "The light icon. It will appear when neither dark nor tinted icons are used, or if they are not provided.", type: "string", meta: { asset: true, contentTypePattern: "^image/png$", contentTypeHuman: ".png image", square: true } }, dark: { description: "The dark icon. It will appear for the app when the user's system appearance is dark. See Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/app-icons#iOS-iPadOS) for more information.", type: "string", meta: { asset: true, contentTypePattern: "^image/png$", contentTypeHuman: ".png image", square: true } }, tinted: { description: "The tinted icon. It will appear for the app when the user's system appearance is tinted. See Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/app-icons#iOS-iPadOS) for more information.", type: "string", meta: { asset: true, contentTypePattern: "^image/png$", contentTypeHuman: ".png image", square: 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 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: "@deprecated This field was used by the `expo-ads-admob` package, which has been deprecated and removed. [Google Mobile Ads App ID](https://support.google.com/admob/answer/6232340) Google AdMob App ID.", type: "string", meta: { deprecated: true } }, googleMobileAdsAutoInit: { description: "@deprecated This field was used by the `expo-ads-admob` package, which has been deprecated and removed. 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, meta: { deprecated: true } } }, 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 }, privacyManifests: { description: "Dictionary of privacy manifest definitions to add to your app's native PrivacyInfo.xcprivacy file. [Learn more](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files)", type: "object", properties: { NSPrivacyAccessedAPITypes: { type: "array", description: "A list of required reasons of why your app uses restricted API categories. [Learn more](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api)", items: { type: "object", additionalProperties: false, required: ["NSPrivacyAccessedAPIType", "NSPrivacyAccessedAPITypeReasons"], properties: { NSPrivacyAccessedAPIType: { type: "string", description: "A string that identifies the category of required reason APIs your app uses" }, NSPrivacyAccessedAPITypeReasons: { type: "array", uniqueItems: true, description: "A list of reasons for a specific category.", items: { type: "string" } } } } }, NSPrivacyTrackingDomains: { type: "array", uniqueItems: true, description: "A list of domains that your app uses for tracking.", items: { type: "string" } }, NSPrivacyTracking: { type: "boolean", description: "A Boolean that indicates whether your app or third-party SDK uses data for tracking." }, NSPrivacyCollectedDataTypes: { description: "A list of collected data types that your app uses.", type: "array", items: { type: "object", additionalProperties: false, required: ["NSPrivacyCollectedDataType", "NSPrivacyCollectedDataTypeLinked", "NSPrivacyCollectedDataTypeTracking", "NSPrivacyCollectedDataTypePurposes"], properties: { NSPrivacyCollectedDataType: { type: "string" }, NSPrivacyCollectedDataTypeLinked: { type: "boolean" }, NSPrivacyCollectedDataTypeTracking: { type: "boolean" }, NSPrivacyCollectedDataTypePurposes: { type: "array", uniqueItems: true, items: { type: "string" } } } } } }, additionalProperties: false }, 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:<fully qualified domain>[: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 }, usesBroadcastPushNotifications: { description: " A boolean indicating if the app uses Push Notifications Broadcast option for Push Notifications capability. If true, EAS CLI will use the value during capability syncing. If EAS CLI is not used, this configuration will not have any effect unless another tool is used to operate on it, so enable the capability manually on the Apple Developer Portal in that case.", 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: "@deprecated Use the `expo-splash-screen` config plugin instead. Configuration for loading and splash screen for standalone iOS apps.", meta: { deprecated: true }, type: "object", properties: { backgroundColor: { description: "Color to fill the loading screen background", type: "string", pattern: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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" } }, dark: { description: "Configuration for loading and splash screen for standalone iOS apps in dark mode.", type: "object", properties: { backgroundColor: { description: "Color to fill the loading screen background", type: "string", pattern: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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" } } } } } }, runtimeVersion: { description: "Property indicating compatibility between an iOS build's native code and an OTA update for the iOS platform. If provided, this will override the value of the top level `runtimeVersion` key on iOS.", tsType: "string | { policy: 'nativeVersion' | 'sdkVersion' | 'appVersion' | 'fingerprint'; }", oneOf: [{ type: "string", pattern: "^[a-zA-Z\\d][a-zA-Z\\d._+()-]{0,254}$", meta: { notHuman: 'String beginning with an alphanumeric character followed by any combination of alphanumeric character, "_", "+", ".","(", ")", or "-". Valid examples: "1.0.3a+", "1.0.0", "0".', regexHuman: 'String beginning with an alphanumeric character followed by any combination of alphanumeric character, "_", "+", ".","(", ")", or "-". Valid examples: "1.0.3a+", "1.0.0", "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", "appVersion", "fingerprint"] } }, required: ["policy"], additionalProperties: false }] }, version: { description: "Your iOS app version. Takes precedence over the root `version` field. In addition to this field, you'll also use `ios.buildNumber` \u2014 read more about how to version your app [here](https://docs.expo.dev/distribution/app-stores/#versioning-your-app). This corresponds to `CFBundleShortVersionString`. 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`" } } }, additionalProperties: false }, 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: "^(?!.*\\bnative\\b)[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: 21e8, 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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"] }, scheme: { description: "URL scheme(s) to link into your Android app. Schemes added to this field will be merged with the schemes in the `scheme` key at the top level of the config.", oneOf: [{ type: "string", pattern: "^[a-z][a-z0-9+.-]*$" }, { type: "array", items: { 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`" } }, 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 Go 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 } }, monochromeImage: { description: "Local path or remote URL to an image representing the Android 13+ monochromatic icon. Should follow the [specified guidelines](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive). This icon will appear on the home screen when the user enables 'Themed icons' in system settings on a device running Android 13+.", 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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: "A list of permissions to add to the app `AndroidManifest.xml` during prebuild. For example: `['android.permission.SCHEDULE_EXACT_ALARM']`", type: "array", meta: { bareWorkflow: 'To change the permissions your app requests, edit `AndroidManifest.xml` directly. To prevent your app from requesting specific permissions (which may automatically be added through an installed native package), add those permissions to `AndroidManifest.xml` along with a `tools:node="remove"` tag.' }, items: { type: "string" } }, blockedPermissions: { description: 'List of permissions to block in the final `AndroidManifest.xml`. This is useful for removing permissions that are added by native package `AndroidManifest.xml` files which are merged into the final manifest. Internally this feature uses the `tools:node="remove"` XML attribute to remove permissions. Not available in Expo Go.', type: "array", items: { type: "string" } }, googleServicesFile: { description: "[Firebase Configuration File](https://support.google.com/firebase/answer/7015592) Location of the `google-services.json` 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: "@deprecated This field was used by the `expo-ads-admob` package, which has been deprecated and removed. [Google Mobile Ads App ID](https://support.google.com/admob/answer/6232340) Google AdMob App ID.", type: "string", meta: { deprecated: true } }, googleMobileAdsAutoInit: { description: "@deprecated This field was used by the `expo-ads-admob` package, which has been deprecated and removed. 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, meta: { deprecated: true } } }, additionalProperties: false }, splash: { description: "@deprecated Use the `expo-splash-screen` config plugin instead. Configuration for loading and splash screen for managed and standalone Android apps.", meta: { deprecated: true }, type: "object", properties: { backgroundColor: { description: "Color to fill the loading screen background", type: "string", pattern: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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" } }, dark: { description: "Configuration for loading and splash screen for managed and standalone Android apps in dark mode.", type: "object", properties: { backgroundColor: { description: "Color to fill the loading screen background", type: "string", pattern: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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: [{ 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 }, { type: ["array"], items: { 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 } }] }, 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" }, runtimeVersion: { description: "Property indicating compatibility between a Android build's native code and an OTA update for the Android platform. If provided, this will override the value of top level `runtimeVersion` key on Android.", tsType: "string | { policy: 'nativeVersion' | 'sdkVersion' | 'appVersion' | 'fingerprint'; }", oneOf: [{ type: "string", pattern: "^[a-zA-Z\\d][a-zA-Z\\d._+()-]{0,254}$", meta: { notHuman: 'String beginning with an alphanumeric character followed by any combination of alphanumeric character, "_", "+", ".","(", ")", or "-". Valid examples: "1.0.3a+", "1.0.0", "0".', regexHuman: 'String beginning with an alphanumeric character followed by any combination of alphanumeric character, "_", "+", ".","(", ")", or "-". Valid examples: "1.0.3a+", "1.0.0", "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", "appVersion", "fingerprint"] } }, required: ["policy"], additionalProperties: false }] }, version: { description: "Your android app version. Takes precedence over the root `version` field. In addition to this field, you'll also use `android.versionCode` \u2014 read more about how to version your app [here](https://docs.expo.dev/distribution/app-stores/#versioning-your-app). 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 `versionName` string in `android/app/build.gradle`" } }, predictiveBackGestureEnabled: { description: "Enable your app to use the [predictive back gesture](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) on Android 13 (API level 33) and later. Default to false.", type: "boolean", fallback: false, meta: { bareWorkflow: "To change the setting, update the `android:enableOnBackInvokedCallback` value in `AndroidManifest.xml`." } } }, additionalProperties: false }, web: { description: "Configuration that is specific to the web platform.", type: "object", additionalProperties: true, properties: { output: { description: "Sets the export method for the web app for both `expo start` and `expo export`. `static` statically renders HTML files for every route in the `app/` directory, which is available only in Expo Router apps. `single` outputs a Single Page Application (SPA), with a single `index.html` in the output folder, and has no statically indexable HTML. `server` outputs static HTML, and API Routes for hosting with a custom Node.js server. Defaults to `single`.", enum: ["single", "static", "server"], type: "string" }, 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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\u2019 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 \u201Cbackground color\u201D for the website. This value repeats what is already available in the site\u2019s 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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: "^(#|&#x23;)[0-9a-fA-F]{6,8}$", 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" } } } } }, bundler: { description: "Sets the bundler to use for the web platform. Only supported in the local CLI `npx expo`. Defaults to `webpack` if the `@expo/webpack-config` package is installed, if not, it defaults to `metro`.", enum: ["webpack", "metro"], fallback: "metro" } } }, experiments: { description: "Enable experimental features that may be unstable, unsupported, or removed without deprecation notices.", type: "object", additionalProperties: false, properties: { autolinkingModuleResolution: { description: "Apply Expo Autolinking's search results to Metro's module resolution. This forces your project's dependencies on `react`, `react-dom`, and `react-native`, and the autolinked versions of any Expo and React Native modules to be resolved when bundling your app. This prevents version misalignment and is useful for monorepos and to prevent conflicts.", type: "boolean", fallback: false }, baseUrl: { description: "Export a website relative to a subpath of a domain. The path will be prepended as-is to links to all bundled resources. Prefix the path with a `/` (recommended) to load all resources relative to the server root. If the path **does not** start with a `/` then resources will be loaded relative to the code that requests them, this could lead to unexpected behavior. Example '/subpath'. Defaults to '' (empty string).", type: "string", fallback: "" }, buildCacheProvider: { description: "@deprecated This field is not longer marked as experimental and will be removed in a future release, use the `buildCacheProvider` field instead.", meta: { deprecated: true }, anyOf: [{ type: "string", enum: ["eas"] }, { type: "object", additionalProperties: false, properties: { plugin: { type: "string" }, options: { type: "object" } }, required: ["plugin"] }] }, supportsTVOnly: { description: "If true, indicates that this project does not support tablets or handsets, and only supports Apple TV and Android TV", type: "boolean", fallback: false }, functionalCSS: { description: "Enable React-based CSS support for native platforms. Only supports a subset of CSS properties, class names selectors, and has no cascading.", type: "boolean", fallback: false }, tsconfigPaths: { description: "Enable tsconfig/jsconfig `compilerOptions.paths` and `compilerOptions.baseUrl` support for import aliases in Metro.", type: "boolean", fallback: true }, typedRoutes: { description: "Enable support for statically typed links in Expo Router. This feature requires TypeScript be set up in your Expo Router v2 project.", type: "boolean", fallback: false }, 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 }, reactCanary: { description: "Experimentally use a vendored canary build of React for testing upcoming features.", type: "boolean", fallback: false }, reactCompiler: { description: "Experimentally enable React Compiler.", type: "boolean", fallback: false }, reactServerComponentRoutes: { description: "Experimentally enable React Server Components by default in Expo Router and concurrent routing for transitions.", type: "boolean", fallback: false }, reactServerFunctions: { description: "Experimentally enable React Server Functions support in Expo CLI and Expo Router.", 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 } } };
17195
17183
 
17184
+ // src/checks/no-duplicate-drawable-resource.ts
17185
+ var import_fast_glob = __toESM(require("fast-glob"));
17186
+ var import_node_path = __toESM(require("path"));
17187
+ var DRAWABLE_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp"];
17188
+ var SCAN_ROOTS = ["assets", "src/assets"];
17189
+ function toDrawableResourceKey(relativePathWithoutExt) {
17190
+ return `assets/${relativePathWithoutExt}`.toLowerCase().replace(/\//g, "_").replace(/[^a-z0-9_]/g, "").replace(/^assets_/, "");
17191
+ }
17192
+ function containsNonAsciiChar(relativePathWithoutExt) {
17193
+ return /[^\x00-\x7f]/.test(relativePathWithoutExt);
17194
+ }
17195
+ function isDegenerateResourceKey(resourceKey) {
17196
+ return /^_*$/.test(resourceKey);
17197
+ }
17198
+ function toPosixRelativePath(from, to) {
17199
+ return import_node_path.default.relative(from, to).split(import_node_path.default.sep).join("/");
17200
+ }
17201
+ function stripExtension(relativePath) {
17202
+ const ext = import_node_path.default.extname(relativePath);
17203
+ return relativePath.slice(0, relativePath.length - ext.length);
17204
+ }
17205
+ function scanRoot(projectRoot, scanRoot3) {
17206
+ const absoluteRoot = import_node_path.default.join(projectRoot, scanRoot3);
17207
+ const extensionGlob = `**/*.{${DRAWABLE_IMAGE_EXTENSIONS.join(",")}}`;
17208
+ const matches = import_fast_glob.default.sync(extensionGlob, {
17209
+ cwd: absoluteRoot,
17210
+ absolute: true,
17211
+ caseSensitiveMatch: false,
17212
+ onlyFiles: true,
17213
+ ignore: ["**/node_modules/**"]
17214
+ });
17215
+ return matches.map((absolutePath) => {
17216
+ const relativePath = toPosixRelativePath(absoluteRoot, absolutePath);
17217
+ const relativePathWithoutExt = stripExtension(relativePath);
17218
+ return {
17219
+ absolutePath,
17220
+ resourceKey: toDrawableResourceKey(relativePathWithoutExt),
17221
+ hasNonAsciiChar: containsNonAsciiChar(relativePathWithoutExt)
17222
+ };
17223
+ });
17224
+ }
17225
+ function checkDuplicateDrawableResources(projectRoot) {
17226
+ const scanned = SCAN_ROOTS.flatMap((root) => scanRoot(projectRoot, root));
17227
+ const seenAbsolutePaths = /* @__PURE__ */ new Set();
17228
+ const uniqueScanned = scanned.filter((image) => {
17229
+ if (seenAbsolutePaths.has(image.absolutePath)) return false;
17230
+ seenAbsolutePaths.add(image.absolutePath);
17231
+ return true;
17232
+ });
17233
+ const groupsByKey = /* @__PURE__ */ new Map();
17234
+ for (const image of uniqueScanned) {
17235
+ const group = groupsByKey.get(image.resourceKey);
17236
+ if (group) {
17237
+ group.push(image);
17238
+ } else {
17239
+ groupsByKey.set(image.resourceKey, [image]);
17240
+ }
17241
+ }
17242
+ const conflicts = [...groupsByKey.entries()].filter(([, files]) => files.length > 1);
17243
+ if (conflicts.length === 0) return true;
17244
+ const nonAsciiHint = " \u63D0\u793A\uFF1AAndroid \u6253\u5305\u65F6\u4F1A\u5220\u9664\u6587\u4EF6\u540D/\u76EE\u5F55\u540D\u4E2D\u7684\u4E2D\u6587\u7B49\u975E ASCII \u5B57\u7B26\uFF08\u4E0D\u8F6C\u5199\uFF09\uFF0C\u8FD9\u662F\u5BFC\u81F4\u8D44\u6E90\u540D\u76F8\u540C\u7684\u6839\u672C\u539F\u56E0\uFF0C\u8BF7\u5C06\u76F8\u5173\u6587\u4EF6\u6539\u4E3A\u82F1\u6587\u6216\u62FC\u97F3\u547D\u540D\u3002";
17245
+ console.error("\n[drawable-resources]");
17246
+ for (const [resourceKey, files] of conflicts) {
17247
+ if (isDegenerateResourceKey(resourceKey)) {
17248
+ console.error(
17249
+ " \u4EE5\u4E0B\u6587\u4EF6\u7684\u8DEF\u5F84\u5728\u53BB\u9664\u4E2D\u6587\u7B49\u975E ASCII \u5B57\u7B26\u540E\u90FD\u5F52\u7EA6\u4E3A\u540C\u4E00\u4E2A\u7A7A/\u5360\u4F4D\u8D44\u6E90\u540D\uFF0C\u65E0\u6CD5\u751F\u6210\u4EFB\u4F55\u6709\u6548\u7684 Android \u8D44\u6E90\u540D\uFF0C\u4F1A\u5BFC\u81F4\u6253\u5305\u5931\u8D25\uFF0C\u8BF7\u6539\u4E3A\u82F1\u6587\u6216\u62FC\u97F3\u547D\u540D\uFF1A"
17250
+ );
17251
+ } else {
17252
+ console.error(` Resource name '${resourceKey}' would collide from multiple files:`);
17253
+ }
17254
+ for (const file of files) {
17255
+ console.error(` - ${import_node_path.default.relative(projectRoot, file.absolutePath)}`);
17256
+ }
17257
+ if (!isDegenerateResourceKey(resourceKey) && files.some((file) => file.hasNonAsciiChar)) {
17258
+ console.error(nonAsciiHint);
17259
+ }
17260
+ }
17261
+ return false;
17262
+ }
17263
+
17264
+ // src/checks/no-mismatched-image-extension.ts
17265
+ var import_fast_glob2 = __toESM(require("fast-glob"));
17266
+ var import_node_path2 = __toESM(require("path"));
17267
+
17268
+ // src/rules/image-format.ts
17269
+ var import_node_fs = __toESM(require("fs"));
17270
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
17271
+ var JPEG_SIGNATURE = Buffer.from([255, 216, 255]);
17272
+ var GIF87_SIGNATURE = Buffer.from("GIF87a", "ascii");
17273
+ var GIF89_SIGNATURE = Buffer.from("GIF89a", "ascii");
17274
+ var RIFF_SIGNATURE = Buffer.from("RIFF", "ascii");
17275
+ var WEBP_SIGNATURE = Buffer.from("WEBP", "ascii");
17276
+ function detectImageFormat(filePath) {
17277
+ let fd;
17278
+ try {
17279
+ fd = import_node_fs.default.openSync(filePath, "r");
17280
+ } catch {
17281
+ return null;
17282
+ }
17283
+ try {
17284
+ const buf = Buffer.alloc(12);
17285
+ const bytesRead = import_node_fs.default.readSync(fd, buf, 0, buf.length, 0);
17286
+ if (bytesRead >= 8 && buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
17287
+ return "png";
17288
+ }
17289
+ if (bytesRead >= 3 && buf.subarray(0, 3).equals(JPEG_SIGNATURE)) {
17290
+ return "jpeg";
17291
+ }
17292
+ if (bytesRead >= 6 && (buf.subarray(0, 6).equals(GIF87_SIGNATURE) || buf.subarray(0, 6).equals(GIF89_SIGNATURE))) {
17293
+ return "gif";
17294
+ }
17295
+ if (bytesRead >= 12 && buf.subarray(0, 4).equals(RIFF_SIGNATURE) && buf.subarray(8, 12).equals(WEBP_SIGNATURE)) {
17296
+ return "webp";
17297
+ }
17298
+ return null;
17299
+ } catch {
17300
+ return null;
17301
+ } finally {
17302
+ import_node_fs.default.closeSync(fd);
17303
+ }
17304
+ }
17305
+
17306
+ // src/checks/no-mismatched-image-extension.ts
17307
+ var SCAN_ROOTS2 = ["assets", "src/assets"];
17308
+ var SCAN_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp"];
17309
+ var ALLOWED_EXTENSIONS_BY_FORMAT = {
17310
+ png: ["png"],
17311
+ jpeg: ["jpg", "jpeg"],
17312
+ gif: ["gif"],
17313
+ webp: ["webp"]
17314
+ };
17315
+ function scanRoot2(projectRoot, scanRoot3) {
17316
+ const absoluteRoot = import_node_path2.default.join(projectRoot, scanRoot3);
17317
+ const extensionGlob = `**/*.{${SCAN_EXTENSIONS.join(",")}}`;
17318
+ const matches = import_fast_glob2.default.sync(extensionGlob, {
17319
+ cwd: absoluteRoot,
17320
+ absolute: true,
17321
+ caseSensitiveMatch: false,
17322
+ onlyFiles: true,
17323
+ ignore: ["**/node_modules/**"]
17324
+ });
17325
+ const mismatched = [];
17326
+ for (const absolutePath of matches) {
17327
+ const declaredExtension = import_node_path2.default.extname(absolutePath).slice(1).toLowerCase();
17328
+ const actualFormat = detectImageFormat(absolutePath);
17329
+ if (actualFormat === null) continue;
17330
+ if (!ALLOWED_EXTENSIONS_BY_FORMAT[actualFormat].includes(declaredExtension)) {
17331
+ mismatched.push({ absolutePath, declaredExtension, actualFormat });
17332
+ }
17333
+ }
17334
+ return mismatched;
17335
+ }
17336
+ function checkMismatchedImageExtensions(projectRoot) {
17337
+ const scanned = SCAN_ROOTS2.flatMap((root) => scanRoot2(projectRoot, root));
17338
+ const seenAbsolutePaths = /* @__PURE__ */ new Set();
17339
+ const uniqueMismatched = scanned.filter((image) => {
17340
+ if (seenAbsolutePaths.has(image.absolutePath)) return false;
17341
+ seenAbsolutePaths.add(image.absolutePath);
17342
+ return true;
17343
+ });
17344
+ if (uniqueMismatched.length === 0) return true;
17345
+ const groupsByMismatchKind = /* @__PURE__ */ new Map();
17346
+ for (const image of uniqueMismatched) {
17347
+ const key = `${image.declaredExtension} ${image.actualFormat}`;
17348
+ const group = groupsByMismatchKind.get(key);
17349
+ if (group) {
17350
+ group.push(image);
17351
+ } else {
17352
+ groupsByMismatchKind.set(key, [image]);
17353
+ }
17354
+ }
17355
+ console.error("\n[mismatched-image-extension]");
17356
+ for (const files of groupsByMismatchKind.values()) {
17357
+ const { declaredExtension, actualFormat } = files[0];
17358
+ console.error(
17359
+ ` \u6587\u4EF6\u540E\u7F00\u4E3A ${declaredExtension.toUpperCase()}\uFF0C\u4F46\u771F\u5B9E\u683C\u5F0F\u662F ${actualFormat.toUpperCase()}\uFF0C\u8BF7\u5C06\u6587\u4EF6\u5185\u5BB9\u8F6C\u7801\u4E3A\u771F\u6B63\u7684 ${declaredExtension.toUpperCase()}\uFF08\u4E0D\u8981\u4EC5\u91CD\u547D\u540D\u540E\u7F00\uFF09\uFF1A`
17360
+ );
17361
+ for (const { absolutePath } of files) {
17362
+ console.error(` - ${import_node_path2.default.relative(projectRoot, absolutePath)}`);
17363
+ }
17364
+ }
17365
+ console.error(
17366
+ " \u8BE5\u95EE\u9898\u53EA\u5728 Android release \u6253\u5305\u65F6\u88AB AAPT2 \u68C0\u6D4B\u5230\u5E76\u62A5\u9519\uFF08`Android resource compilation failed ... file failed to compile`\uFF09\uFF0Cdev/Expo Go \u4E0D\u4F1A\u89E6\u53D1\u3002"
17367
+ );
17368
+ return false;
17369
+ }
17370
+
17196
17371
  // src/cli/lint.ts
17197
- var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
17198
- var oxlintConfig = (0, import_node_path.join)(devkitRoot, "oxlint-config.json");
17199
- var biomeConfig = (0, import_node_path.join)(devkitRoot, "biome-config.json");
17372
+ var devkitRoot = (0, import_node_path3.resolve)(__dirname, "../..");
17373
+ var oxlintConfig = (0, import_node_path3.join)(devkitRoot, "oxlint-config.json");
17374
+ var biomeConfig = (0, import_node_path3.join)(devkitRoot, "biome-config.json");
17200
17375
  var targets = process.argv.slice(2);
17201
17376
  var lintTargets = targets.length > 0 ? targets : ["."];
17202
17377
  var oxlintIgnorePatterns = [
@@ -17217,13 +17392,13 @@ function run(name, cmd, args) {
17217
17392
  function generateTypedRoutes() {
17218
17393
  const projectRoot = process.cwd();
17219
17394
  try {
17220
- const appJson = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(projectRoot, "app.json"), "utf8"));
17395
+ const appJson = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(projectRoot, "app.json"), "utf8"));
17221
17396
  const routerRoot = appJson?.expo?.router?.root ?? "src";
17222
- process.env.EXPO_ROUTER_APP_ROOT = (0, import_node_path.join)(projectRoot, routerRoot, "app");
17223
- const expoRouterDir = (0, import_node_path.dirname)(
17397
+ process.env.EXPO_ROUTER_APP_ROOT = (0, import_node_path3.join)(projectRoot, routerRoot, "app");
17398
+ const expoRouterDir = (0, import_node_path3.dirname)(
17224
17399
  require.resolve("expo-router/package.json", { paths: [projectRoot] })
17225
17400
  );
17226
- const req = (0, import_node_module.createRequire)((0, import_node_path.join)(expoRouterDir, "index.js"));
17401
+ const req = (0, import_node_module.createRequire)((0, import_node_path3.join)(expoRouterDir, "index.js"));
17227
17402
  const { getTypedRoutesDeclarationFile } = req("@expo/router-server/build/typed-routes/generate");
17228
17403
  const ctx = req("expo-router/internal/testing").requireContext(
17229
17404
  process.env.EXPO_ROUTER_APP_ROOT,
@@ -17231,18 +17406,18 @@ function generateTypedRoutes() {
17231
17406
  req("expo-router/_ctx-shared").EXPO_ROUTER_CTX_IGNORE
17232
17407
  );
17233
17408
  const content = getTypedRoutesDeclarationFile(ctx);
17234
- const typesDir = (0, import_node_path.join)(projectRoot, ".expo/types");
17235
- (0, import_node_fs.mkdirSync)(typesDir, { recursive: true });
17236
- (0, import_node_fs.writeFileSync)((0, import_node_path.join)(typesDir, "router.d.ts"), content);
17409
+ const typesDir = (0, import_node_path3.join)(projectRoot, ".expo/types");
17410
+ (0, import_node_fs2.mkdirSync)(typesDir, { recursive: true });
17411
+ (0, import_node_fs2.writeFileSync)((0, import_node_path3.join)(typesDir, "router.d.ts"), content);
17237
17412
  } catch {
17238
17413
  }
17239
17414
  }
17240
17415
  function validateExpoConfig() {
17241
17416
  const projectRoot = process.cwd();
17242
- if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(projectRoot, "app.json"))) return true;
17417
+ if (!(0, import_node_fs2.existsSync)((0, import_node_path3.join)(projectRoot, "app.json"))) return true;
17243
17418
  try {
17244
- const expoDir = (0, import_node_path.dirname)(require.resolve("expo/package.json", { paths: [projectRoot] }));
17245
- const req = (0, import_node_module.createRequire)((0, import_node_path.join)(expoDir, "index.js"));
17419
+ const expoDir = (0, import_node_path3.dirname)(require.resolve("expo/package.json", { paths: [projectRoot] }));
17420
+ const req = (0, import_node_module.createRequire)((0, import_node_path3.join)(expoDir, "index.js"));
17246
17421
  const { getConfig } = req("@expo/config");
17247
17422
  getConfig(projectRoot, { skipSDKVersionRequirement: true });
17248
17423
  return true;
@@ -17251,7 +17426,7 @@ function validateExpoConfig() {
17251
17426
  console.error("\n[expo-config]");
17252
17427
  console.error(message);
17253
17428
  try {
17254
- const appJson = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(projectRoot, "app.json"), "utf8"));
17429
+ const appJson = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(projectRoot, "app.json"), "utf8"));
17255
17430
  const plugins = appJson?.expo?.plugins;
17256
17431
  if (plugins) {
17257
17432
  console.error("\napp.json plugins:");
@@ -17264,11 +17439,11 @@ function validateExpoConfig() {
17264
17439
  }
17265
17440
  async function validateAppJsonSchema() {
17266
17441
  const projectRoot = process.cwd();
17267
- const appJsonPath = (0, import_node_path.join)(projectRoot, "app.json");
17268
- if (!(0, import_node_fs.existsSync)(appJsonPath)) return true;
17442
+ const appJsonPath = (0, import_node_path3.join)(projectRoot, "app.json");
17443
+ if (!(0, import_node_fs2.existsSync)(appJsonPath)) return true;
17269
17444
  let appJson;
17270
17445
  try {
17271
- appJson = JSON.parse((0, import_node_fs.readFileSync)(appJsonPath, "utf8"));
17446
+ appJson = JSON.parse((0, import_node_fs2.readFileSync)(appJsonPath, "utf8"));
17272
17447
  } catch {
17273
17448
  console.error(`[app.json] Failed to parse ${appJsonPath}`);
17274
17449
  return false;
@@ -17310,6 +17485,10 @@ async function main() {
17310
17485
  results.push({ name: "expo-config", ok: expoConfigOk });
17311
17486
  const appJsonOk = await validateAppJsonSchema();
17312
17487
  results.push({ name: "app.json", ok: appJsonOk });
17488
+ const drawableOk = checkDuplicateDrawableResources(process.cwd());
17489
+ results.push({ name: "drawable-resources", ok: drawableOk });
17490
+ const mismatchedExtOk = checkMismatchedImageExtensions(process.cwd());
17491
+ results.push({ name: "mismatched-image-extension", ok: mismatchedExtOk });
17313
17492
  const failed = results.filter((r) => !r.ok);
17314
17493
  if (failed.length > 0) {
17315
17494
  const names = failed.map((r) => r.name).join(", ");
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/rules/no-invalid-expo-config-value.ts
31
+ var no_invalid_expo_config_value_exports = {};
32
+ __export(no_invalid_expo_config_value_exports, {
33
+ default: () => no_invalid_expo_config_value_default
34
+ });
35
+ module.exports = __toCommonJS(no_invalid_expo_config_value_exports);
36
+ var import_node_fs = __toESM(require("fs"));
37
+ var import_node_path2 = __toESM(require("path"));
38
+
39
+ // src/rules/utils.ts
40
+ var import_node_path = __toESM(require("path"));
41
+ var import_package_up = require("package-up");
42
+ function findProjectRoot(startPath) {
43
+ const pkgPath = (0, import_package_up.packageUpSync)({ cwd: import_node_path.default.dirname(startPath) });
44
+ return pkgPath ? import_node_path.default.dirname(pkgPath) : null;
45
+ }
46
+
47
+ // src/rules/no-invalid-expo-config-value.ts
48
+ var USER_INTERFACE_STYLE = /* @__PURE__ */ new Set(["light", "dark", "automatic"]);
49
+ var ORIENTATION = /* @__PURE__ */ new Set(["default", "portrait", "landscape"]);
50
+ var SPLASH_RESIZE_MODE = /* @__PURE__ */ new Set(["contain", "cover"]);
51
+ var INITIAL_ORIENTATION = /* @__PURE__ */ new Set([
52
+ "DEFAULT",
53
+ "ALL",
54
+ "PORTRAIT",
55
+ "PORTRAIT_UP",
56
+ "PORTRAIT_DOWN",
57
+ "LANDSCAPE",
58
+ "LANDSCAPE_LEFT",
59
+ "LANDSCAPE_RIGHT"
60
+ ]);
61
+ function collectEnumIssues(appJson) {
62
+ const issues = [];
63
+ const expo = appJson.expo ?? {};
64
+ const push = (field, value, allowed, allowedLabel) => {
65
+ if (typeof value === "string" && value.length > 0 && !allowed.has(value)) {
66
+ issues.push({ field, value, allowed: allowedLabel });
67
+ }
68
+ };
69
+ push("expo.orientation", expo.orientation, ORIENTATION, "default, portrait, landscape");
70
+ push("expo.userInterfaceStyle", expo.userInterfaceStyle, USER_INTERFACE_STYLE, "light, dark, automatic");
71
+ push(
72
+ "expo.android.userInterfaceStyle",
73
+ expo.android?.userInterfaceStyle,
74
+ USER_INTERFACE_STYLE,
75
+ "light, dark, automatic"
76
+ );
77
+ const splashes = [
78
+ ["expo.splash", expo.splash],
79
+ ["expo.android.splash", expo.android?.splash],
80
+ ["expo.ios.splash", expo.ios?.splash]
81
+ ];
82
+ for (const [prefix, splash] of splashes) {
83
+ if (!splash) continue;
84
+ push(`${prefix}.resizeMode`, splash.resizeMode, SPLASH_RESIZE_MODE, "contain, cover");
85
+ }
86
+ for (const entry of expo.plugins ?? []) {
87
+ if (!Array.isArray(entry) || entry[0] !== "expo-screen-orientation") continue;
88
+ const opts = entry[1];
89
+ if (opts != null && typeof opts === "object") {
90
+ const initial = opts.initialOrientation;
91
+ push(
92
+ "plugins[expo-screen-orientation].initialOrientation",
93
+ initial,
94
+ INITIAL_ORIENTATION,
95
+ "DEFAULT, ALL, PORTRAIT, PORTRAIT_UP, PORTRAIT_DOWN, LANDSCAPE, LANDSCAPE_LEFT, LANDSCAPE_RIGHT"
96
+ );
97
+ }
98
+ }
99
+ return issues;
100
+ }
101
+ var noInvalidExpoConfigValueRule = {
102
+ meta: {
103
+ type: "problem",
104
+ docs: {
105
+ description: "Validate enum/string fields in app.json that `expo prebuild` consumes (userInterfaceStyle, orientation, splash.resizeMode, screen-orientation.initialOrientation)."
106
+ },
107
+ schema: [],
108
+ messages: {
109
+ invalidEnum: '{{field}} has invalid value "{{value}}". `expo prebuild` expects one of: {{allowed}}. Using an unrecognized value will either abort prebuild or be silently ignored.'
110
+ }
111
+ },
112
+ create(context) {
113
+ const projectRoot = findProjectRoot(context.filename);
114
+ if (!projectRoot) return {};
115
+ return {
116
+ Program(node) {
117
+ let appJson;
118
+ try {
119
+ appJson = JSON.parse(
120
+ import_node_fs.default.readFileSync(import_node_path2.default.join(projectRoot, "app.json"), "utf-8")
121
+ );
122
+ } catch {
123
+ return;
124
+ }
125
+ for (const issue of collectEnumIssues(appJson)) {
126
+ context.report({
127
+ node,
128
+ messageId: "invalidEnum",
129
+ data: { field: issue.field, value: issue.value, allowed: issue.allowed }
130
+ });
131
+ }
132
+ }
133
+ };
134
+ }
135
+ };
136
+ var plugin = {
137
+ meta: { name: "expo-config-value" },
138
+ rules: { "no-invalid-expo-config-value": noInvalidExpoConfigValueRule }
139
+ };
140
+ var no_invalid_expo_config_value_default = plugin;
141
+ module.exports = module.exports.default;
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/rules/no-invalid-notification-config.ts
31
+ var no_invalid_notification_config_exports = {};
32
+ __export(no_invalid_notification_config_exports, {
33
+ default: () => no_invalid_notification_config_default
34
+ });
35
+ module.exports = __toCommonJS(no_invalid_notification_config_exports);
36
+ var import_node_fs2 = __toESM(require("fs"));
37
+ var import_node_path2 = __toESM(require("path"));
38
+
39
+ // src/rules/utils.ts
40
+ var import_node_path = __toESM(require("path"));
41
+ var import_package_up = require("package-up");
42
+ function findProjectRoot(startPath) {
43
+ const pkgPath = (0, import_package_up.packageUpSync)({ cwd: import_node_path.default.dirname(startPath) });
44
+ return pkgPath ? import_node_path.default.dirname(pkgPath) : null;
45
+ }
46
+
47
+ // src/rules/image-format.ts
48
+ var import_node_fs = __toESM(require("fs"));
49
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
50
+ var JPEG_SIGNATURE = Buffer.from([255, 216, 255]);
51
+ var GIF87_SIGNATURE = Buffer.from("GIF87a", "ascii");
52
+ var GIF89_SIGNATURE = Buffer.from("GIF89a", "ascii");
53
+ var RIFF_SIGNATURE = Buffer.from("RIFF", "ascii");
54
+ var WEBP_SIGNATURE = Buffer.from("WEBP", "ascii");
55
+ function detectImageFormat(filePath) {
56
+ let fd;
57
+ try {
58
+ fd = import_node_fs.default.openSync(filePath, "r");
59
+ } catch {
60
+ return null;
61
+ }
62
+ try {
63
+ const buf = Buffer.alloc(12);
64
+ const bytesRead = import_node_fs.default.readSync(fd, buf, 0, buf.length, 0);
65
+ if (bytesRead >= 8 && buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
66
+ return "png";
67
+ }
68
+ if (bytesRead >= 3 && buf.subarray(0, 3).equals(JPEG_SIGNATURE)) {
69
+ return "jpeg";
70
+ }
71
+ if (bytesRead >= 6 && (buf.subarray(0, 6).equals(GIF87_SIGNATURE) || buf.subarray(0, 6).equals(GIF89_SIGNATURE))) {
72
+ return "gif";
73
+ }
74
+ if (bytesRead >= 12 && buf.subarray(0, 4).equals(RIFF_SIGNATURE) && buf.subarray(8, 12).equals(WEBP_SIGNATURE)) {
75
+ return "webp";
76
+ }
77
+ return null;
78
+ } catch {
79
+ return null;
80
+ } finally {
81
+ import_node_fs.default.closeSync(fd);
82
+ }
83
+ }
84
+
85
+ // src/rules/no-invalid-notification-config.ts
86
+ var SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
87
+ "png",
88
+ "jpeg",
89
+ "webp",
90
+ "gif"
91
+ ]);
92
+ var EXT_TO_FORMAT = {
93
+ png: "png",
94
+ jpg: "jpeg",
95
+ jpeg: "jpeg",
96
+ jpe: "jpeg",
97
+ webp: "webp",
98
+ gif: "gif"
99
+ };
100
+ function isRemoteUrl(value) {
101
+ return value.startsWith("http://") || value.startsWith("https://");
102
+ }
103
+ var HEX_COLOR = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?$/;
104
+ function isHexColor(value) {
105
+ return HEX_COLOR.test(value);
106
+ }
107
+ function looksLikeColorName(value) {
108
+ if (!/^[a-z][a-z0-9_]*$/i.test(value) || value.includes(" ")) return false;
109
+ return !/^[0-9a-f]+$/i.test(value);
110
+ }
111
+ function getNotificationPluginOptions(plugins) {
112
+ for (const entry of plugins) {
113
+ if (entry === "expo-notifications") return {};
114
+ if (Array.isArray(entry) && entry[0] === "expo-notifications") {
115
+ const opts = entry[1];
116
+ if (opts != null && typeof opts === "object") return opts;
117
+ return {};
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ function checkIcon(projectRoot, icon) {
123
+ if (typeof icon !== "string") return null;
124
+ if (isRemoteUrl(icon)) return null;
125
+ const abs = import_node_path2.default.resolve(projectRoot, icon);
126
+ if (!import_node_fs2.default.existsSync(abs)) return { kind: "iconMissing" };
127
+ const ext = import_node_path2.default.extname(icon).replace(/^\./, "").toLowerCase();
128
+ const declared = EXT_TO_FORMAT[ext];
129
+ if (!declared) {
130
+ return { kind: "iconUnsupported", format: ext || "unknown" };
131
+ }
132
+ const actual = detectImageFormat(abs);
133
+ if (actual === null || !SUPPORTED_FORMATS.has(actual)) {
134
+ return { kind: "iconUnsupported", format: actual ?? "unknown" };
135
+ }
136
+ if (declared !== actual) {
137
+ return { kind: "iconMasquerade", declared, actual };
138
+ }
139
+ return null;
140
+ }
141
+ function collectIssues(projectRoot, options) {
142
+ const issues = [];
143
+ if (options.sounds !== void 0 && options.sounds !== null) {
144
+ if (!Array.isArray(options.sounds) && typeof options.sounds !== "string") {
145
+ issues.push({ kind: "soundsNotArray", actualType: typeof options.sounds });
146
+ }
147
+ }
148
+ if (typeof options.color === "string" && options.color.length > 0) {
149
+ if (!isHexColor(options.color) && !looksLikeColorName(options.color)) {
150
+ issues.push({ kind: "invalidColor", value: options.color });
151
+ }
152
+ }
153
+ if (options.icon !== void 0 && options.icon !== null) {
154
+ const iconIssue = checkIcon(projectRoot, options.icon);
155
+ if (iconIssue) issues.push(iconIssue);
156
+ }
157
+ return issues;
158
+ }
159
+ var noInvalidNotificationConfigRule = {
160
+ meta: {
161
+ type: "problem",
162
+ docs: {
163
+ description: "Validate expo-notifications plugin config: sounds must be an array, color must be a valid hex/name, icon must be a supported image format and not masquerading."
164
+ },
165
+ schema: [],
166
+ messages: {
167
+ soundsNotArray: 'expo-notifications `sounds` must be an array of file paths, found {{actualType}}. `expo prebuild` throws: "Must provide an array of sound files in your app config".',
168
+ invalidColor: 'expo-notifications `color` "{{value}}" is not a valid color. Use a hex color like "#E63946" (supports #RGB / #RRGGBB / #AARRGGBB) or a lowercase color name.',
169
+ iconMissing: "expo-notifications `icon` references a file that does not exist.",
170
+ iconUnsupported: 'expo-notifications `icon` has unsupported image format "{{format}}". `expo prebuild` (via @expo/image-utils) only supports png, jpeg, webp, gif.',
171
+ iconMasquerade: "expo-notifications `icon` is declared as .{{declared}} but the file content is actually {{actual}}. This format masquerade fails prebuild. Convert the file to match its extension."
172
+ }
173
+ },
174
+ create(context) {
175
+ const projectRoot = findProjectRoot(context.filename);
176
+ if (!projectRoot) return {};
177
+ return {
178
+ Program(node) {
179
+ let appJson;
180
+ try {
181
+ appJson = JSON.parse(
182
+ import_node_fs2.default.readFileSync(import_node_path2.default.join(projectRoot, "app.json"), "utf-8")
183
+ );
184
+ } catch {
185
+ return;
186
+ }
187
+ const plugins = appJson.expo?.plugins ?? [];
188
+ const options = getNotificationPluginOptions(plugins);
189
+ if (!options) return;
190
+ for (const issue of collectIssues(projectRoot, options)) {
191
+ if (issue.kind === "soundsNotArray") {
192
+ context.report({
193
+ node,
194
+ messageId: "soundsNotArray",
195
+ data: { actualType: issue.actualType }
196
+ });
197
+ } else if (issue.kind === "invalidColor") {
198
+ context.report({
199
+ node,
200
+ messageId: "invalidColor",
201
+ data: { value: issue.value }
202
+ });
203
+ } else if (issue.kind === "iconMissing") {
204
+ context.report({ node, messageId: "iconMissing" });
205
+ } else if (issue.kind === "iconUnsupported") {
206
+ context.report({
207
+ node,
208
+ messageId: "iconUnsupported",
209
+ data: { format: issue.format }
210
+ });
211
+ } else {
212
+ context.report({
213
+ node,
214
+ messageId: "iconMasquerade",
215
+ data: { declared: issue.declared, actual: issue.actual }
216
+ });
217
+ }
218
+ }
219
+ }
220
+ };
221
+ }
222
+ };
223
+ var plugin = {
224
+ meta: { name: "expo-notifications-config" },
225
+ rules: { "no-invalid-notification-config": noInvalidNotificationConfigRule }
226
+ };
227
+ var no_invalid_notification_config_default = plugin;
228
+ module.exports = module.exports.default;
@@ -35,6 +35,7 @@ __export(no_missing_notification_asset_exports, {
35
35
  module.exports = __toCommonJS(no_missing_notification_asset_exports);
36
36
  var import_node_fs = __toESM(require("fs"));
37
37
  var import_node_path2 = __toESM(require("path"));
38
+ var import_config_plugins = require("@expo/config-plugins");
38
39
 
39
40
  // src/rules/utils.ts
40
41
  var import_node_path = __toESM(require("path"));
@@ -78,12 +79,13 @@ var noMissingNotificationAssetRule = {
78
79
  meta: {
79
80
  type: "problem",
80
81
  docs: {
81
- description: "Ensure local asset files referenced by expo-notifications plugin (icon, sounds) exist on disk."
82
+ description: "Ensure local asset files referenced by expo-notifications plugin (icon, sounds) exist on disk and have Android-valid names."
82
83
  },
83
84
  schema: [],
84
85
  messages: {
85
86
  missingIcon: 'expo-notifications plugin references icon "{{path}}" but the file does not exist.',
86
- missingSound: 'expo-notifications plugin references sound "{{path}}" but the file does not exist.'
87
+ missingSound: 'expo-notifications plugin references sound "{{path}}" but the file does not exist.',
88
+ invalidSoundName: 'expo-notifications sound "{{path}}" has invalid Android resource name "{{name}}". Android resource names must start with a letter and contain only lowercase a-z, 0-9, or underscore (no hyphens, dots, uppercase). This will fail `expo prebuild`. Rename the file (e.g. use underscores) and update the config.'
87
89
  }
88
90
  },
89
91
  create(context) {
@@ -112,6 +114,15 @@ var noMissingNotificationAssetRule = {
112
114
  const soundPath = import_node_path2.default.resolve(projectRoot, sound);
113
115
  if (!import_node_fs.default.existsSync(soundPath)) {
114
116
  context.report({ node, messageId: "missingSound", data: { path: sound } });
117
+ continue;
118
+ }
119
+ const nameWithoutExt = import_node_path2.default.parse(import_node_path2.default.basename(sound)).name;
120
+ if (!(0, import_config_plugins.isValidAndroidAssetName)(nameWithoutExt)) {
121
+ context.report({
122
+ node,
123
+ messageId: "invalidSoundName",
124
+ data: { path: sound, name: nameWithoutExt }
125
+ });
115
126
  }
116
127
  }
117
128
  }
@@ -16,6 +16,8 @@
16
16
  { "name": "image-import", "specifier": "miaoda-expo-devkit/rules/no-missing-image-import" },
17
17
  { "name": "expo-splash-config", "specifier": "miaoda-expo-devkit/rules/no-splash-screen-missing-image" },
18
18
  { "name": "expo-notifications-assets", "specifier": "miaoda-expo-devkit/rules/no-missing-notification-asset" },
19
+ { "name": "expo-config-value", "specifier": "miaoda-expo-devkit/rules/no-invalid-expo-config-value" },
20
+ { "name": "expo-notifications-config", "specifier": "miaoda-expo-devkit/rules/no-invalid-notification-config" },
19
21
  { "name": "gifted-charts-linear-gradient", "specifier": "miaoda-expo-devkit/rules/no-gifted-charts-missing-linear-gradient" }
20
22
  ],
21
23
 
@@ -38,6 +40,8 @@
38
40
  "image-import/no-missing-image-import": "error",
39
41
  "expo-splash-config/no-splash-screen-missing-image": "error",
40
42
  "expo-notifications-assets/no-missing-notification-asset": "error",
43
+ "expo-config-value/no-invalid-expo-config-value": "error",
44
+ "expo-notifications-config/no-invalid-notification-config": "error",
41
45
  "gifted-charts-linear-gradient/no-gifted-charts-missing-linear-gradient": "error",
42
46
 
43
47
  "expo/no-dynamic-env-var": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-expo-devkit",
3
- "version": "0.1.1-beta.95",
3
+ "version": "0.1.1-beta.97",
4
4
  "description": "Expo 应用开发工具集:Sentry DSN 替换 stub、错误/网络捕获、Metro 符号化",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -64,6 +64,8 @@
64
64
  "./rules/no-missing-image-import": "./dist/rules/no-missing-image-import.js",
65
65
  "./rules/no-splash-screen-missing-image": "./dist/rules/no-splash-screen-missing-image.js",
66
66
  "./rules/no-missing-notification-asset": "./dist/rules/no-missing-notification-asset.js",
67
+ "./rules/no-invalid-notification-config": "./dist/rules/no-invalid-notification-config.js",
68
+ "./rules/no-invalid-expo-config-value": "./dist/rules/no-invalid-expo-config-value.js",
67
69
  "./rules/no-gifted-charts-missing-linear-gradient": "./dist/rules/no-gifted-charts-missing-linear-gradient.js",
68
70
  "./biome": "./biome-config.json",
69
71
  "./oxlint": "./oxlint-config.json",
@@ -81,6 +83,7 @@
81
83
  "dependencies": {
82
84
  "@zumer/snapdom": "^2.12.9",
83
85
  "connect": "^3.7.0",
86
+ "fast-glob": "3.3.3",
84
87
  "lodash.throttle": "^4.1.1",
85
88
  "metro-minify-esbuild": "^0.2.0",
86
89
  "oxc-resolver": "^11.19.1",
@@ -88,6 +91,7 @@
88
91
  "stacktrace-parser": "^0.1.11"
89
92
  },
90
93
  "peerDependencies": {
94
+ "@expo/config-plugins": ">=55.0.0",
91
95
  "@sentry/core": "^10.38.0",
92
96
  "@sentry/react-native": ">=8.0.0",
93
97
  "babel-preset-expo": ">=55.0.0",
@@ -105,6 +109,9 @@
105
109
  "typescript": ">=5.0.0"
106
110
  },
107
111
  "peerDependenciesMeta": {
112
+ "@expo/config-plugins": {
113
+ "optional": false
114
+ },
108
115
  "@sentry/core": {
109
116
  "optional": true
110
117
  },