npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-gradle-properties

v0.1.2

Published

Expo config plugin to set arbitrary keys in android/gradle.properties — the generic escape hatch expo-build-properties doesn't have.

Readme

expo-gradle-properties

npm version CI license

An Expo config plugin that writes arbitrary keys into the generated android/gradle.properties.

It is the generic escape hatch that expo-build-properties doesn't have.

// app.config.ts
plugins: [
  ['expo-gradle-properties', {
    'org.gradle.jvmargs': '-Xmx4g -XX:MaxMetaspaceSize=2g',
    'kotlin.daemon.jvmargs': '-Xmx2g',
    'org.gradle.parallel': true,
  }],
]

Contents — Why · Install · Usage · What you can set · Android ABIs · Behaviour · Override warning · Validation · vs expo-build-properties

Why

In a prebuild (CNG) Expo app, android/ is a build artifact. Editing android/gradle.properties by hand works exactly until the next expo prebuild throws it away, so the only durable way to set those values is a config plugin.

expo-build-properties is the obvious place for this, and it can't do it. As of v57.0.13 its entire Android surface is:

minSdkVersion  compileSdkVersion  targetSdkVersion  buildToolsVersion  cmakeVersion
kotlinVersion  enableMinifyInReleaseBuilds  enableShrinkResourcesInReleaseBuilds
enablePngCrunchInReleaseBuilds  extraProguardRules  packagingOptions  networkInspector
extraMavenRepos  usesCleartextTraffic  useLegacyPackaging  manifestQueries
useDayNightTheme  enableBundleCompression  buildArchs  exclusiveMavenMirror

Exactly one of those reaches gradle.properties — buildArchs, written out as reactNativeArchitectures. There is no way to express org.gradle.jvmargs, kotlin.daemon.jvmargs, org.gradle.caching, or any flag your own Gradle code reads.

And the failure is a quiet one. Passing an unknown key to expo-build-properties is not an error and not a warning — the value is accepted, dropped, and the build carries on with the template defaults. You find out fourteen minutes into a release build, via an OutOfMemoryError: Metaspace that points at nothing.

So this plugin has one job, and it is loud about doing it.

Install

npx expo install expo-gradle-properties

Usage

Shorthand

Pass a flat map. This is the common case.

plugins: [
  ['expo-gradle-properties', {
    'org.gradle.jvmargs': '-Xmx4g -XX:MaxMetaspaceSize=2g -Dfile.encoding=UTF-8',
    'kotlin.daemon.jvmargs': '-Xmx2g',
    'org.gradle.parallel': true,
    'org.gradle.caching': true,
    reactNativeArchitectures: 'arm64-v8a',
  }],
]

Full form

Pass properties plus options.

plugins: [
  ['expo-gradle-properties', {
    properties: {
      'org.gradle.jvmargs': '-Xmx4g',
      'org.gradle.configureondemand': null,  // remove the key entirely
    },
    comment: 'set by our build config',
    warnOnUserOverride: true,
  }],
]

The full form is used when the config has a properties key holding an object. (A Gradle property genuinely named properties therefore needs the full form: { properties: { properties: '…' } }.)

Listing it more than once

The plugin is not run-once, so you can list it several times to group unrelated settings. Every entry applies.

Set any given key in one entry only. If two entries set the same key, the winner is decided by Expo's mod ordering rather than by the plugin — and that ordering is the reverse of what you would expect, so the earlier entry wins. Don't build on that; keep each key in one place.

plugins: [
  // memory, with a reason attached
  ['expo-gradle-properties', { 'org.gradle.jvmargs': '-Xmx4g' }],

  // ...and CI-only caching, kept separate
  ['expo-gradle-properties', {
    properties: { 'org.gradle.caching': process.env.CI ? true : null },
    comment: 'CI overrides',
  }],
]

Options

| Option | Default | Meaning | | --- | --- | --- | | properties | — | The map of properties to write. Required in the full form. | | comment | 'Managed by expo-gradle-properties' | Header written above appended keys. false writes none. | | warnOnUserOverride | true | Warn when $GRADLE_USER_HOME/gradle.properties will beat these values. See below. |

Values

| You write | The file gets | | --- | --- | | '-Xmx4g' | -Xmx4g | | 4096 | 4096 | | true / false | true / false | | null / undefined | the key is removed |

null lets you drop a key the Expo template sets, and makes conditionals read cleanly:

'org.gradle.caching': process.env.CI ? true : null,

What you can set

Any key at all. The plugin has no list of supported properties — it writes whatever you give it, which is the whole point. These are simply the ones people reach for most often.

| Category | Properties | | --- | --- | | Build speed | org.gradle.caching · org.gradle.parallel · org.gradle.configureondemand · org.gradle.workers.max · org.gradle.daemon | | Memory | org.gradle.jvmargs · kotlin.daemon.jvmargs | | Architectures | reactNativeArchitectures — see below | | React Native / Expo | newArchEnabled · hermesEnabled · expo.useLegacyPackaging · expo.gif.enabled · expo.webp.enabled · expo.webp.animated | | AndroidX | android.useAndroidX · android.enableJetifier · android.nonTransitiveRClass | | Your own | any flag your build.gradle reads via project.findProperty('myFlag') |

Three things worth knowing before you reach for these.

The Expo template already sets several of them — newArchEnabled, hermesEnabled, reactNativeArchitectures and the expo.* image flags all ship in the generated file. Setting one here overwrites it in place, keeping the template's explanatory comment attached. Setting it to null deletes the line entirely and lets Gradle fall back to its own default.

One of them has a first-party equivalent. reactNativeArchitectures is also reachable as buildArchs in expo-build-properties. Either works — just don't set it in both places, or you'll spend an afternoon working out which one won.

Don't put secrets here. Signing passwords and API keys are a traditional gradle.properties habit, but anything you pass to this plugin lives in your app.config.ts, which is committed to git. Use EAS secrets or environment variables and read them in build.gradle instead.

Setting the Android ABIs

reactNativeArchitectures is the property people most often come here for, so it's worth spelling out. It takes a comma-separated string, not an array:

plugins: [
  ['expo-gradle-properties', {
    reactNativeArchitectures: 'arm64-v8a',
  }],
]

Android has four ABIs (CPU architectures):

| ABI | What it is | Do you need it? | | --- | --- | --- | | arm64-v8a | 64-bit ARM | Yes. Every modern phone, and required by Google Play. | | armeabi-v7a | 32-bit ARM | Only for older devices. Still supported — not deprecated. | | x86_64 | 64-bit Intel | Only for emulators (Apple Silicon and Intel). | | x86 | 32-bit Intel | Effectively legacy. Physical x86 Android devices are long gone. |

The Expo template builds all four. That default is safe but expensive: every native dependency you have — Reanimated, Gesture Handler, Nitro, MapLibre, SVG, Screens — gets its C++ compiled four separate times. On a large app that dominates build time.

Narrowing to one architecture is the single biggest build-time win available:

reactNativeArchitectures: 'arm64-v8a'   // every modern phone

The tradeoff is real: that build will not install on 32-bit ARM devices or on x86 emulators. It's ideal for local development and CI on Apple Silicon, and for testing on modern hardware.

To go back to the full set:

reactNativeArchitectures: 'armeabi-v7a,arm64-v8a,x86,x86_64'

A note on app size

Narrowing ABIs does not shrink what your users download, if you ship an App Bundle (.aab) — which Google Play requires. Play splits the bundle per device and delivers only the matching ABI, so a user on an arm64 phone downloads arm64 code either way.

What narrowing does shrink is a universal APK — a dev-client build, an internal-distribution APK, or anything installed via adb. And what it always saves is build time.

What Google Play requires

Since August 2019, Play has required a 64-bit version of any app containing native code — you cannot publish 32-bit only. armeabi-v7a is not deprecated and 32-bit devices still exist, so the usual production answer is to ship both ARM ABIs and drop the x86 pair:

reactNativeArchitectures: 'armeabi-v7a,arm64-v8a'

From Android 14, devices with ARMv9 cores cannot run 32-bit code at all, so the long-term direction is arm64-only — but that's not the situation today.

Behaviour

Existing keys are replaced in place. The entry keeps its original position, so a prebuild diff is one changed line rather than a deletion plus an append at the bottom — and the template's explanatory comment stays attached to the property it explains.

Duplicates are collapsed. If a key somehow appears twice, the first entry is replaced and the rest are deleted. This is a correctness requirement, not tidiness: gradle.properties is last-wins, so a plugin that blindly appends leaves a file that reads as though it worked while Gradle uses the other line.

New keys are appended at the end, under a single comment header, so anyone reading the generated file can see what put them there.

Removals are idempotent. Removing a key that isn't there is a no-op, and running prebuild twice produces a byte-identical file.

The machine-level override warning

This is the part worth having.

Gradle resolves gradle.properties with the user-level file winning over the project-level one. Highest priority first:

  1. command line -P
  2. $GRADLE_USER_HOME/gradle.properties — usually ~/.gradle/gradle.properties
  3. <project>/gradle.properties ← everything this plugin writes
  4. $GRADLE_HOME/gradle.properties

That inverts the usual "closest file wins" intuition, and it bites hard. A developer who once set org.gradle.jvmargs in ~/.gradle/gradle.properties gets builds that silently disagree with CI on the same commit. On a self-hosted runner, the runner's home file beats your repo for every build that machine will ever run.

So during prebuild the plugin reads that file and tells you:

[expo-gradle-properties] 1 property is overridden by your machine-level Gradle config.
  ~/.gradle/gradle.properties takes precedence over the project's gradle.properties, so the build will NOT use the value this plugin sets.
    org.gradle.jvmargs
      this plugin: -Xmx4g -XX:MaxMetaspaceSize=2g
      ~/.gradle/gradle.properties: -Xmx1g
  Remove the key from ~/.gradle/gradle.properties, or pass warnOnUserOverride: false to silence this.

It warns and never throws — a machine-level override can be deliberate, and a config plugin has no business failing your build over your own dotfile. Set warnOnUserOverride: false to turn it off.

Validation

Expo writes gradle.properties as key=value with no escaping at all, so a malformed key produces a file Gradle quietly reads as something else. Everything below throws while your app config is being read, naming the key and the reason:

  • keys containing whitespace, =, : or \ — all of these separate or escape entries
  • keys starting with # or ! — the line would be a comment
  • values containing a line break
  • values ending in an odd number of backslashes — a trailing \ is a line continuation and would swallow the next property
  • non-finite numbers and objects
  • arrays — with a message showing the joined string to write instead, since expo-build-properties takes its ABI list as an array and the habit carries over
  • unknown options in the full form — because silently dropping an option is the bug this package exists to fix

Should you use this or expo-build-properties?

Use expo-build-properties for anything it covers. It is first-party, and it edits build.gradle, Proguard rules and Maven repos, which this plugin deliberately does not touch.

| | expo-build-properties | expo-gradle-properties | | --- | --- | --- | | SDK versions, Kotlin, minify, Proguard, Maven repos | ✅ | ❌ (out of scope) | | reactNativeArchitectures | ✅ as buildArchs | ✅ | | org.gradle.jvmargs, kotlin.daemon.jvmargs, org.gradle.caching, custom flags | ❌ | ✅ | | Unknown option | silently ignored | throws | | iOS | ✅ | ❌ (no counterpart file) |

They compose — run both.

Requirements

  • Expo SDK 50 or newer
  • Node 18+
  • A prebuild (CNG) workflow. If you check android/ into git and never run expo prebuild, edit the file directly instead.

Development

bun install
bun run test        # 99 tests
bun run typecheck
bun run build

The interesting logic is pure and lives in src/apply.ts and src/normalize.ts; neither imports anything from Expo, so it can be tested without a prebuild. src/integration.test.ts round-trips through Expo's own parsePropertiesFile / propertiesListToString to prove the output is a file Expo will still read.

See DESIGN.md for why it is built this way.

Contributing

Bug reports, failing test cases and doc fixes are all welcome — you don't need to bring a full solution. CONTRIBUTING.md covers the layout, where your change probably goes, and how to test against a real prebuild.

By taking part you agree to the Code of Conduct.

License

MIT © dangerdeveloper