@rn-iso/expo-build-cache 0.14.0
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/README.md +47 -0
- package/index.js +111 -0
- package/package.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @rn-iso/expo-build-cache
|
|
2
|
+
|
|
3
|
+
A local Expo build cache provider. When no native input has changed, the CLI
|
|
4
|
+
installs a cached `.app` / `.apk` instead of compiling — which is the difference
|
|
5
|
+
between a JS-only change costing a simulator boot and costing a full native
|
|
6
|
+
build.
|
|
7
|
+
|
|
8
|
+
Local on purpose: a directory under `$HOME` shared by every worktree on the
|
|
9
|
+
machine. No account, no network, and a second worktree building the same commit
|
|
10
|
+
is a hit rather than a second five-minute build.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -D @rn-iso/expo-build-cache
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Point your Expo config at it. **Which key depends on the SDK, and the wrong one
|
|
17
|
+
is a silent no-op rather than an error:**
|
|
18
|
+
|
|
19
|
+
```jsonc
|
|
20
|
+
// SDK 54+
|
|
21
|
+
{ "expo": { "buildCacheProvider": { "plugin": "@rn-iso/expo-build-cache" } } }
|
|
22
|
+
|
|
23
|
+
// SDK 53 — reads only the experiments key and ignores the top-level one
|
|
24
|
+
{ "expo": { "experiments": { "buildCacheProvider": { "plugin": "@rn-iso/expo-build-cache" } } } }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Add a `.fingerprintignore` for anything that changes without changing the build.
|
|
28
|
+
`ios/Podfile.lock` is the usual culprit: pod checksums can embed absolute paths,
|
|
29
|
+
which makes the fingerprint differ per machine and the cache never hit.
|
|
30
|
+
|
|
31
|
+
Watch for `[build-cache] hit` or `miss` in the output. A miss means you changed
|
|
32
|
+
something native — or that you are the first workspace on this commit.
|
|
33
|
+
|
|
34
|
+
### Housekeeping
|
|
35
|
+
|
|
36
|
+
The cache registers itself with [`rn-iso`](https://www.npmjs.com/package/rn-iso)
|
|
37
|
+
if it is installed, so it can be reported and trimmed:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx rn-iso gc --caches # what it has grown to
|
|
41
|
+
npx rn-iso gc --caches --delete --older-than 30 # drop entries unused for 30 days
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
rn-iso is an optional peer. Without it the cache works exactly the same; it is
|
|
45
|
+
just invisible to housekeeping.
|
|
46
|
+
|
|
47
|
+
`RN_ISO_BUILD_CACHE` overrides the location.
|
package/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// A local Expo build cache provider.
|
|
2
|
+
//
|
|
3
|
+
// Expo fingerprints the project's native inputs and hands the hash to a
|
|
4
|
+
// provider. If a build already exists for that hash, the provider returns it and
|
|
5
|
+
// the CLI installs it instead of compiling. That is the difference between a
|
|
6
|
+
// JS-only change costing a simulator boot and costing a full native build, and
|
|
7
|
+
// most changes are JS-only.
|
|
8
|
+
//
|
|
9
|
+
// Local on purpose: a directory under $HOME shared by every worktree on the
|
|
10
|
+
// machine. No account, no network, and a second worktree building the same
|
|
11
|
+
// commit is a hit rather than a second five-minute build.
|
|
12
|
+
//
|
|
13
|
+
// Wire it up in app.json. Which key depends on the SDK, and the wrong one is a
|
|
14
|
+
// silent no-op rather than an error:
|
|
15
|
+
//
|
|
16
|
+
// SDK 54+ { "expo": { "buildCacheProvider": { "plugin": "@rn-iso/expo-build-cache" } } }
|
|
17
|
+
// SDK 53 { "expo": { "experiments": { "buildCacheProvider": { ... } } } }
|
|
18
|
+
//
|
|
19
|
+
// SDK 53's CLI reads only the experiments key and ignores the top-level one
|
|
20
|
+
// without saying so; SDK 54+ reads the top-level key and falls back to
|
|
21
|
+
// experiments.
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { execFileSync } = require('node:child_process');
|
|
27
|
+
|
|
28
|
+
const CACHE_ROOT =
|
|
29
|
+
process.env.RN_ISO_BUILD_CACHE || path.join(os.homedir(), '.rn-iso-build-cache');
|
|
30
|
+
|
|
31
|
+
function entryDir(platform, fingerprintHash) {
|
|
32
|
+
return path.join(CACHE_ROOT, platform, fingerprintHash);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The cached artifact is the single .app / .apk inside the entry directory.
|
|
36
|
+
function artifactIn(dir) {
|
|
37
|
+
if (!fs.existsSync(dir)) return null;
|
|
38
|
+
const found = fs.readdirSync(dir).find(f => f.endsWith('.app') || f.endsWith('.apk'));
|
|
39
|
+
return found ? path.join(dir, found) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Tell rn-iso this cache exists, so `gc --caches` can report and trim it. Every
|
|
43
|
+
// entry is an independent directory keyed by fingerprint, hence prune: entries.
|
|
44
|
+
//
|
|
45
|
+
// Best effort by design: this package is useful without rn-iso installed, and a
|
|
46
|
+
// missing peer must never break a build. Registration is idempotent, so calling
|
|
47
|
+
// it on every resolve is fine.
|
|
48
|
+
let registered = false;
|
|
49
|
+
function registerOnce() {
|
|
50
|
+
if (registered) return;
|
|
51
|
+
registered = true;
|
|
52
|
+
try {
|
|
53
|
+
// eslint-disable-next-line global-require
|
|
54
|
+
const { register } = require('rn-iso/cache-manifest');
|
|
55
|
+
register({
|
|
56
|
+
dir: CACHE_ROOT,
|
|
57
|
+
name: 'Expo build cache',
|
|
58
|
+
prune: 'entries',
|
|
59
|
+
note: 'built .app/.apk keyed on the native fingerprint',
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
// rn-iso not installed, or too old to export the manifest. Nothing to do.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The hash the CLI passes is stable once .fingerprintignore excludes generated
|
|
67
|
+
// files that embed absolute paths -- ios/Podfile.lock is the usual culprit, as
|
|
68
|
+
// pod checksums can carry machine-specific paths. Use it directly rather than
|
|
69
|
+
// recomputing: fingerprinting walks node_modules and is otherwise the single
|
|
70
|
+
// largest cost of a cache hit.
|
|
71
|
+
async function resolveBuildCache({ platform, fingerprintHash }) {
|
|
72
|
+
registerOnce();
|
|
73
|
+
const key = fingerprintHash;
|
|
74
|
+
const hit = artifactIn(entryDir(platform, key));
|
|
75
|
+
if (hit) {
|
|
76
|
+
console.log(`[build-cache] hit ${platform} ${key.slice(0, 12)}`);
|
|
77
|
+
// Touch on hit so age-based trimming can tell a working entry from a dead
|
|
78
|
+
// one. Without this, the entries earning their keep look identical to the
|
|
79
|
+
// ones nothing has used in months.
|
|
80
|
+
fs.utimesSync(path.dirname(hit), new Date(), new Date());
|
|
81
|
+
return hit;
|
|
82
|
+
}
|
|
83
|
+
console.log(`[build-cache] miss ${platform} ${key.slice(0, 12)}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function uploadBuildCache({ platform, fingerprintHash, buildPath }) {
|
|
88
|
+
registerOnce();
|
|
89
|
+
if (!buildPath || !fs.existsSync(buildPath)) return null;
|
|
90
|
+
|
|
91
|
+
const key = fingerprintHash;
|
|
92
|
+
const dest = entryDir(platform, key);
|
|
93
|
+
if (artifactIn(dest)) return artifactIn(dest);
|
|
94
|
+
|
|
95
|
+
// Stage in a sibling and rename into place. A copy interrupted halfway must
|
|
96
|
+
// never be readable as a complete entry by a worktree building in parallel,
|
|
97
|
+
// and rename is the only step that is atomic.
|
|
98
|
+
const staging = `${dest}.staging-${process.pid}`;
|
|
99
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
100
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
101
|
+
execFileSync('cp', ['-R', buildPath, path.join(staging, path.basename(buildPath))]);
|
|
102
|
+
|
|
103
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
104
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
105
|
+
fs.renameSync(staging, dest);
|
|
106
|
+
|
|
107
|
+
console.log(`[build-cache] stored ${platform} ${key.slice(0, 12)}`);
|
|
108
|
+
return artifactIn(dest);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { resolveBuildCache, uploadBuildCache, CACHE_ROOT };
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rn-iso/expo-build-cache",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "Local Expo build cache provider: installs a cached .app/.apk instead of compiling when no native input changed.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"rn-iso": ">=0.14.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependenciesMeta": {
|
|
15
|
+
"rn-iso": {
|
|
16
|
+
"optional": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|