Last active
May 9, 2025 15:40
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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