Skip to content

Instantly share code, notes, and snippets.

@javascripter
Last active May 9, 2025 15:40
Show Gist options
  • Save javascripter/51b8142dade048ce1b1f62a86f673363 to your computer and use it in GitHub Desktop.
Save javascripter/51b8142dade048ce1b1f62a86f673363 to your computer and use it in GitHub Desktop.
(Experimental) local build binary cache using https://docs.expo.dev/guides/cache-builds-remotely/
/**
* A file-system based build cache provider for `expo run:[android|ios]`.
* Available in Expo SDK 53+.
* https://docs.expo.dev/guides/cache-builds-remotely/
*
* This plugin speeds up local rebuilds by caching `.apk` or `.app` outputs on disk,
* based on a stable fingerprint hash provided by Expo CLI.
*
* Usage:
* 1. Save this file as `provider.plugin.js` in your project root.
* 2. Add this to your `app.json` or `app.config.js`:
*
* ```javascript
* // app.config.js
* module.exports = {
* experiments: {
* buildCacheProvider: {
* plugin: './provider.plugin.js',
* options: {
* // Cache path relative to the project root.
* cachePath: 'node_modules/.cache/expo-build-cache'
* }
* }
* }
* }
* ```
*
*
* @typedef {import('@expo/config').RunOptions} RunOptions
* @typedef {import('@expo/config').ResolveBuildCacheProps} ResolveBuildCacheProps
* @typedef {import('@expo/config').UploadBuildCacheProps} UploadBuildCacheProps
* @typedef {import('@expo/config').BuildCacheProviderPlugin<BuildCacheProviderOptions>} BuildCacheProviderPlugin
*/
/**
* @typedef BuildCacheProviderOptions
* @property {string} cachePath Absolute or relative directory where cache files will be stored.
*/
const { getPackageJson } = require('@expo/config')
const fs = require('node:fs')
const path = require('node:path')
/**
* Does this project depend on expo-dev-client?
* @param {string} projectRoot
* @returns {boolean}
*/
function hasDirectDevClientDependency(projectRoot) {
const { dependencies = {}, devDependencies = {} } =
getPackageJson(projectRoot)
return Boolean(
dependencies['expo-dev-client'] || devDependencies['expo-dev-client'],
)
}
/**
* Determine whether the current build is a dev-client build.
* @param {{ runOptions: RunOptions, projectRoot: string }} params
* @returns {boolean}
*/
function isDevClientBuild({ runOptions, projectRoot }) {
if (!hasDirectDevClientDependency(projectRoot)) return false
if ('variant' in runOptions && runOptions.variant !== undefined) {
return runOptions.variant === 'debug'
}
if ('configuration' in runOptions && runOptions.configuration !== undefined) {
return runOptions.configuration === 'Debug'
}
return true // default to “yes” when in doubt
}
/**
* Compute the cache file path for this build.
* @param {{ projectRoot: string, platform: 'ios' | 'android', runOptions: RunOptions, fingerprintHash: string }} params
* @param {BuildCacheProviderOptions} options
* @returns {string}
*/
function getCacheFilePath(
{ projectRoot, platform, runOptions, fingerprintHash },
options,
) {
const isDevClient = isDevClientBuild({ projectRoot, runOptions })
const fileName = [
'fingerprint',
fingerprintHash,
isDevClient ? 'dev-client' : undefined,
platform === 'ios' ? 'app' : 'apk',
]
.filter(Boolean)
.join('.')
return path.resolve(projectRoot, options.cachePath, fileName)
}
/**
* Locate a previously stored cache artefact.
* @param {ResolveBuildCacheProps} props
* @param {BuildCacheProviderOptions} options
* @returns {Promise<string|null>}
*/
async function resolveBuildCache(props, options) {
const cacheFilePath = getCacheFilePath(props, options)
try {
await fs.promises.access(cacheFilePath)
return cacheFilePath
} catch (error) {
if (/** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT') {
return null
}
throw error
}
}
/**
* Store the artefact for future builds.
* @param {UploadBuildCacheProps} props
* @param {BuildCacheProviderOptions} options
* @returns {Promise<string|null>}
*/
async function uploadBuildCache(props, options) {
const cacheFilePath = getCacheFilePath(props, options)
try {
await fs.promises.mkdir(
path.resolve(props.projectRoot, options.cachePath),
{
recursive: true,
},
)
await fs.promises.cp(props.buildPath, cacheFilePath, { recursive: true })
return cacheFilePath // non-null signals success
} catch (error) {
console.error(error)
return null // tells Expo the upload failed
}
}
/** @type {BuildCacheProviderPlugin} */
const providerPlugin = {
resolveBuildCache,
uploadBuildCache,
}
module.exports = providerPlugin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment