@exodus/stasis-plugins 1.0.0-beta.15 → 1.0.0-beta.17

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.
@@ -3,22 +3,16 @@ import { basename, dirname, join, posix, relative, sep } from 'node:path'
3
3
 
4
4
  import { readJson } from '@exodus/stasis-core/bundle-util'
5
5
  import { diskHost } from '@exodus/stasis-core/host'
6
- import { CODE_EXTENSIONS, classifyFormat, isAutoExcludedDir, isBinaryPlist, isDotEnvFile, isExcludedNativeFile, isNativeArtifact, nameFormat, pathExt, toPosix } from '@exodus/stasis-core/util'
7
-
8
- // The native build inputs of a React Native app's packages, shared by the Metro plugin's capture and
9
- // `stasis bundle --metro` so the two carry the same files. Both read through `fs`:
10
- // readdir(dir) -> Dirent[] (throws where `dir` isn't one), readFile(file) -> Buffer,
11
- // stat(path) -> Stats, or null where nothing is there.
12
-
13
- // Files a React Native build requires that neither Metro's module graph nor a run's loader carries,
14
- // each as a require() of it from the project root resolves it (one that doesn't, or lies outside the
15
- // root, is passed over), tagged by the shared classifier: setup_env.sh, which the React Native CLI runs
16
- // (`execFileSync`) as every command of it starts, `react-native config` the Metro plugin runs included.
17
- // The run's toolchain, none of it the app's: the Metro plugin records each under `run`, `stasis bundle
18
- // --metro` as its own (`bundle`).
6
+ import { CODE_EXTENSIONS, classifyFormat, isAutoExcludedDir, isBinaryPlist, isDotEnvFile, isExcludedNativeFile, isNativeArtifact, nameFormat, pathExt, toPosix, upsert } from '@exodus/stasis-core/util'
7
+
8
+ // The native build inputs of a React Native app's packages, shared by the Metro plugin's capture and `stasis bundle
9
+ // --metro` so the two carry the same files. `fs` is a host: readdir(dir) -> Dirent[] (throws where `dir` isn't one),
10
+ // readFile(file) -> Buffer, stat(path) -> Stats, or null where nothing is there.
11
+
12
+ // Files a React Native build requires that neither Metro's graph nor a run's loader carries, resolved from the project
13
+ // root: setup_env.sh, which the React Native CLI runs as every command starts (`react-native config` included).
19
14
  export const AUTO_INCLUDES = ['@react-native-community/cli/setup_env.sh']
20
15
 
21
- // The entries of the directory `dir`, none where it's missing or no directory.
22
16
  export function listing(dir, fs) {
23
17
  try {
24
18
  return fs.readdir(dir)
@@ -27,39 +21,31 @@ export function listing(dir, fs) {
27
21
  }
28
22
  }
29
23
 
30
- // RN core build scripts the native walk would defer to Metro as `.js`, but Metro never sees them (RN's
31
- // podspecs invoke them at pod-install). Project-relative, so react-native's `exports` can't hide them.
24
+ // RN core build scripts the native walk would defer to Metro as `.js`, but Metro never sees them (RN's podspecs
25
+ // invoke them at pod-install). Project-relative, so react-native's `exports` can't hide them.
32
26
  export const RN_CORE_INCLUDE_FILES = ['sdks/hermes-engine/utils/replace_hermes_version.js']
33
27
 
34
- // Discovered by name: RN scatters podspecs in subdirs `react-native config` misses.
35
- function isPodspec(name) {
36
- return name.endsWith('.podspec') || name.endsWith('.podspec.json')
37
- }
28
+ // By name: RN scatters podspecs in subdirs `react-native config` misses.
29
+ const isPodspec = (name) => name.endsWith('.podspec') || name.endsWith('.podspec.json')
38
30
 
39
31
  // The off-platform top-level dir of a native package, which no build on this host compiles.
40
32
  export function isExcludedNativeDir(name, { win32 = process.platform === 'win32' } = {}) {
41
33
  return !win32 && name === 'windows'
42
34
  }
43
35
 
44
- // Dirs the NATIVE walks (Metro plugin + the static --metro bundler) skip, on top of the shared set:
45
- // nested packages (attested separately) and regenerated build output -- none is a build input.
36
+ // Nested packages (attested separately) and regenerated build output, on top of the shared set.
46
37
  const NATIVE_BUILD_OUTPUT_DIRS = new Set(['node_modules', 'build', '.gradle', '.cxx', 'Pods', 'DerivedData'])
47
- function isSkippedNativeWalkDir(name) {
48
- return NATIVE_BUILD_OUTPUT_DIRS.has(name) || isAutoExcludedDir(name) || isNativeArtifact(name)
49
- }
38
+ const isSkippedNativeWalkDir = (name) => NATIVE_BUILD_OUTPUT_DIRS.has(name) || isAutoExcludedDir(name) || isNativeArtifact(name)
50
39
 
51
- // An extensionless non-UTF-8 file is a compiled tool (Hermes' `hermesc`), never source -- skipped.
52
- function isExtensionlessBinary(name, content) {
53
- return pathExt(name) === '' && Buffer.isBuffer(content) && !isUtf8(content)
54
- }
40
+ // A compiled tool (Hermes' `hermesc`), never source.
41
+ const isExtensionlessBinary = (name, content) => pathExt(name) === '' && Buffer.isBuffer(content) && !isUtf8(content)
55
42
 
56
- // The Metro native capture's policy view. Returns { action, format }: 'code' (a native build input),
57
- // 'skip' (excluded noise, or a JS-family file matched BY EXTENSION -- Metro owns those), else 'resource'.
43
+ // A native capture's view of a file by name: { action: 'code', format } for a native build input, 'skip' for noise
44
+ // or a JS-family file by extension (Metro owns those), else 'resource'.
58
45
  export function classifyNativeCapture(name, { win32 = process.platform === 'win32', content } = {}) {
59
46
  // `.env` files carry secrets: an automated capture must never sweep them in.
60
47
  if (isExcludedNativeFile(name, { win32 }) || isDotEnvFile(name)) return { action: 'skip' }
61
- // Name-matched code is always a native build input, even when its tag is a Node format the JS
62
- // graph would otherwise own.
48
+ // Name-matched code is a native build input even where its tag is a Node format the JS graph would own.
63
49
  const byName = nameFormat(name)
64
50
  if (byName !== undefined) return { action: 'code', format: byName }
65
51
  if (CODE_EXTENSIONS.has(pathExt(name))) return { action: 'skip' }
@@ -67,18 +53,16 @@ export function classifyNativeCapture(name, { win32 = process.platform === 'win3
67
53
  return format === undefined ? { action: 'resource' } : { action: 'code', format }
68
54
  }
69
55
 
70
- // The byte-level half of the native classification, refining what classifyNativeCapture derived from the
71
- // NAME. Both rules only ever demote. A 'resource' carries no format: storage derives base64 from bytes.
56
+ // classifyNativeCapture refined by the bytes, which only ever demote; a 'resource' gets its format from its bytes.
72
57
  export function refineNativeCapture(classified, name, content, resources = new Set()) {
73
58
  if (isExtensionlessBinary(name, content)) return { action: 'skip' }
74
59
  if (isBinaryPlist(name, content)) return { action: resources.has('plist') ? 'resource' : 'skip' }
75
60
  return classified
76
61
  }
77
62
 
78
- // A file name as Metro's AssetPaths reads it (parsePlatformFilePath, then parseBaseName): its asset,
79
- // `${name}.${type}` less any scale (`a@2x.png` is `a.png`'s), its scale (`resolution`, 1 where it
80
- // names none), and its platform where it names `platform` (`a.ios.png`; another's, `a.android.png`,
81
- // is an asset of its own). -> { asset, name, type, resolution, platform }, or null for a name with no type
63
+ // A file name as Metro's AssetPaths reads it (parsePlatformFilePath, then parseBaseName): its asset, `${name}.${type}`
64
+ // less any scale (`a@2x.png` is `a.png`'s), its scale (`resolution`, 1 where it names none), and its platform where it
65
+ // names `platform` (`a.ios.png`; `a.android.png` is an asset of its own). -> null for a name with no type
82
66
  const METRO_PLATFORM_FILE = /^(.+?)(?:\.([^.]+))?\.([^.]+)$/u
83
67
  const METRO_ASSET_SCALE = /^(.+?)(?:@([\d.]+)x)?$/u
84
68
  export function parseMetroAssetName(name, platform) {
@@ -91,11 +75,10 @@ export function parseMetroAssetName(name, platform) {
91
75
  return { asset: `${root}.${type}`, name: root, type, resolution: Number.isNaN(resolution) ? 1 : resolution, platform: ofPlatform ? platform : null }
92
76
  }
93
77
 
94
- // What a file's stat answers to metroAssetFiles, for a file a listing names but the disk lacks.
78
+ // A file's stat for metroAssetFiles, where a listing names one the disk lacks.
95
79
  export const FILE_STAT = { isFile: () => true }
96
80
 
97
- // The files `state` (a loaded State) carries, as a directory listing of them (metroAssetFiles' host):
98
- // an asset's files as captured.
81
+ // The files `state` (a loaded State) carries, as a directory listing of them (metroAssetFiles' host).
99
82
  export function bundleDirHost(state) {
100
83
  const dirs = Map.groupBy([...state.sources.keys(), ...state.resources.keys()], (rel) => posix.dirname(rel))
101
84
  return {
@@ -104,10 +87,9 @@ export function bundleDirHost(state) {
104
87
  }
105
88
  }
106
89
 
107
- // Metro's record of the asset module `path` (its Assets' getAbsoluteAssetRecord): the files it reads
108
- // to build the module (its `scales` and `hash`) and copies out with it. In `path`'s directory, each of
109
- // the asset's name and type at any scale (`a.png`, `a@2x.png`, `a@3x.png`): those for `platform`
110
- // (`a.ios.png`, `a@3x.ios.png`) where it has any, else those for none. -> absolute paths
90
+ // Metro's record of the asset module `path` (getAbsoluteAssetRecord): the files it reads to build it and copies out
91
+ // with it, each of the asset's name and type at any scale in its directory, those for `platform` where it has any,
92
+ // else those for none. -> absolute paths
111
93
  export function metroAssetFiles(path, platform = null, host = diskHost) {
112
94
  const own = parseMetroAssetName(basename(path), platform)
113
95
  if (own === null) return []
@@ -116,45 +98,34 @@ export function metroAssetFiles(path, platform = null, host = diskHost) {
116
98
  for (const { name } of host.readdir(dir)) {
117
99
  const parsed = parseMetroAssetName(name, platform)
118
100
  if (parsed?.asset !== own.asset || host.stat(join(dir, name))?.isFile() !== true) continue
119
- if (!byPlatform.has(parsed.platform)) byPlatform.set(parsed.platform, [])
120
- byPlatform.get(parsed.platform).push(join(dir, name))
101
+ upsert(byPlatform, parsed.platform, () => []).push(join(dir, name))
121
102
  }
122
103
  return (platform === null ? undefined : byPlatform.get(platform)) ?? byPlatform.get(null) ?? []
123
104
  }
124
105
 
125
- // True if the package at `root` ships native code: an ios/ or android/ dir, or a podspec under it.
106
+ // Whether the package at `root` ships native code: an ios/ or android/ dir, or a podspec under it.
126
107
  export function looksNative(root, fs) {
127
108
  return fs.stat(join(root, 'ios')) !== null || fs.stat(join(root, 'android')) !== null || hasPodspec(root, fs)
128
109
  }
129
110
 
130
- // True if any *.podspec exists under `dir`, pruning the same subtrees the native walk skips.
131
111
  function hasPodspec(dir, fs) {
132
112
  return listing(dir, fs).some((ent) => !ent.isSymbolicLink() &&
133
113
  (ent.isFile() ? isPodspec(ent.name) : ent.isDirectory() && !isSkippedNativeWalkDir(ent.name) && hasPodspec(join(dir, ent.name), fs)))
134
114
  }
135
115
 
136
- // The specs React Native's codegen parses in a package's codegen sources (codegenSources), as its own
137
- // filter takes them (filterJSFile): a JS or TS module named `Native…` or `…NativeComponent` (a platform's
138
- // `.ios`/`.android` one too), but a type declaration.
116
+ // A spec React Native's codegen parses (its filterJSFile): a JS or TS module named `Native…` or `…NativeComponent`,
117
+ // but a type declaration.
139
118
  const isCodegenSpec = (name) => /^(?:Native.+|.+NativeComponent)/u.test(name) && /\.(?:jsx?|tsx?)$/u.test(name) && !name.endsWith('.d.ts')
140
119
 
141
- // Each native build input in the package at `root`, walked whole, a native build reading sources
142
- // from anywhere in it (a podspec compiling `cpp/` beside `ios/`): every file but build output and
143
- // prebuilt binaries (isSkippedNativeWalkDir, isNativeArtifact), an off-platform top-level dir
144
- // (isExcludedNativeDir), what the shared classifier skips (the JS and TS Metro's graph owns, docs,
145
- // secrets) and symlinks (a cycle or escape hazard). And the specs of its codegen sources
146
- // (codegenSources, isCodegenSpec), as code with no format, as Metro's code has none: none of the
147
- // other JS there (a react-native.config.js, the package's build, where `jsSrcsDir` is its root), which
148
- // codegen never reads. A file `skip(file)` holds is passed over unread. `resources` is the allowlist
149
- // the byte-level rules consult.
150
- // -> yields { file, source, action, format }, `action` 'code' (under the source-language `format`,
151
- // none for JS and TS) or 'resource'
120
+ // Each native build input of the package at `root`, walked whole (a podspec may compile `cpp/` beside `ios/`) but
121
+ // build output, prebuilt binaries, an off-platform top-level dir, symlinks and what the shared classifier skips; and
122
+ // its codegen specs, as code with no format. -> yields { file, source, action: 'code' | 'resource', format }
152
123
  export function* nativeInputs(root, fs, { resources = new Set(), skip = () => false } = {}) {
153
124
  yield* walk(root, true)
154
125
  for (const dir of codegenSources(root, fs)) yield* sources(dir)
155
126
  function* sources(dir) {
156
127
  for (const ent of listing(dir, fs)) {
157
- // None in a dot, node_modules or tests directory, as codegen skips one.
128
+ // As codegen skips a dot, node_modules or tests directory.
158
129
  if (ent.isSymbolicLink() || ent.name.startsWith('.') || ent.name === 'node_modules' || ent.name.startsWith('__tests')) continue
159
130
  const file = join(dir, ent.name)
160
131
  if (ent.isDirectory()) yield* sources(file)
@@ -173,27 +144,22 @@ export function* nativeInputs(root, fs, { resources = new Set(), skip = () => fa
173
144
  const byName = classifyNativeCapture(ent.name)
174
145
  if (byName.action === 'skip') continue
175
146
  const source = fs.readFile(file)
176
- // Byte-level rules (prebuilt binaries, binary plists) -- see refineNativeCapture.
177
147
  const { action, format } = refineNativeCapture(byName, ent.name, source, resources)
178
148
  if (action !== 'skip') yield { file, source, action, format }
179
149
  }
180
150
  }
181
151
  }
182
152
 
183
- // The directories of the package at `root` its package.json's `codegenConfig` names as its JS sources
184
- // (`jsSrcsDir`, or each of its `libraries`', as React Native's own names `src`), each reached within it
185
- // through no symlink, as the walk follows none: React Native's codegen parses the specs there to
186
- // generate the native code a build compiles (a missing one fails it), and `react-native config` reads
187
- // them for the components they declare (`codegenNativeComponent`, in a `…NativeComponent` spec).
188
- // -> the absolute dirs
153
+ // The dirs the package's `codegenConfig` names as its JS sources (`jsSrcsDir`, or each of its `libraries`'), each
154
+ // reached through no symlink, as the walk follows none: codegen parses the specs there for the native code a build
155
+ // compiles, and `react-native config` reads them for the components they declare. -> the absolute dirs
189
156
  function codegenSources(root, fs) {
190
157
  const config = readJson(join(root, 'package.json'), fs)?.codegenConfig
191
158
  const named = [config?.jsSrcsDir, ...(Array.isArray(config?.libraries) ? config.libraries.map((library) => library?.jsSrcsDir) : [])]
192
159
  return named.filter((dir) => typeof dir === 'string' && dir !== '').map((dir) => packageDir(root, dir, fs)).filter((dir) => dir !== null)
193
160
  }
194
161
 
195
- // The directory `rel` names in the package at `root`, each step of it one (a link's entry is none, and
196
- // none is `..`). -> its absolute path, or null
162
+ // The directory `rel` names in the package at `root`, each step a directory entry (not a link, not `..`), else null.
197
163
  function packageDir(root, rel, fs) {
198
164
  let dir = root
199
165
  for (const name of relative(root, join(root, rel)).split(sep)) {
@@ -207,10 +173,9 @@ function packageDir(root, rel, fs) {
207
173
  // A package name as npm takes one (`name` or `@scope/name`), which a path can't climb out with.
208
174
  const PACKAGE_NAME = /^(?:@[^/\\@.][^/\\]*\/)?[^/\\@.][^/\\]*$/u
209
175
 
210
- // The app's packages `react-native config` reads, read as data rather than by running the CLI: each
211
- // name the app's package.json lists under dependencies, peerDependencies or devDependencies (the
212
- // CLI's list), in the nearest node_modules up from `projectDir` that holds it (as the CLI looks). What
213
- // a react-native.config.js changes, being code, isn't seen. -> the package dirs, as found
176
+ // The app's packages `react-native config` reads, as data rather than by running the CLI: each name its package.json
177
+ // lists under dependencies, peerDependencies or devDependencies, in the nearest node_modules up from `projectDir` that
178
+ // holds it. What a react-native.config.js changes, being code, isn't seen. -> the package dirs
214
179
  export function linkedPackages(projectDir, fs) {
215
180
  const manifest = readJson(join(projectDir, 'package.json'), fs)
216
181
  const listed = (field) => (manifest?.[field] && typeof manifest[field] === 'object' ? Object.keys(manifest[field]) : [])
@@ -230,8 +195,7 @@ export function linkedPackages(projectDir, fs) {
230
195
  return dirs
231
196
  }
232
197
 
233
- // Of those, the ones `react-native config` links, as a native build would: those that ship native
234
- // code (looksNative). -> the package dirs, as found
198
+ // Those of linkedPackages a native build links: those that ship native code.
235
199
  export function linkedNativePackages(projectDir, fs) {
236
200
  return linkedPackages(projectDir, fs).filter((dir) => looksNative(dir, fs))
237
201
  }
@@ -7,27 +7,16 @@ import { State } from '@exodus/stasis-core/state'
7
7
  import { servedByBundle } from '@exodus/stasis-core/state-util'
8
8
  import { transformContext } from './metro-babel-transformer.js'
9
9
 
10
- // Companion to the StasisMetro serializer, which withStasis() wires as Metro's `transformerPath`
11
- // whenever stasis attests the build (wire it yourself only alongside a hand-wired serializer). It
12
- // wraps the project's own transformer, and does two things in each worker:
13
- // - keeps each import as written for the serializer, through the Babel transformer it swaps in
14
- // (./metro-babel-transformer.js): a dependency Babel rewrote (e.g. through
15
- // babel-plugin-module-resolver) carries the specifiers written in the source as `stasisAsWritten`,
16
- // and the output, the ones of the imports a Babel plugin consumed as `stasisConsumed`;
17
- // - is the LOAD half of stasis for Metro (under load the serializer only asserts edges). Load only
18
- // READS an immutable bundle and every worker reads the same bytes (no IPC), so a per-worker
19
- // transformer is the natural seam: it replaces Metro's on-disk `data` with the bundle's
20
- // hash-verified bytes (fail-closed; out-of-scope files pass their disk bytes through).
21
- // KNOWN LIMITATION: does NOT build with sources absent from disk -- Metro reads + hashes each
22
- // file before this runs, so the guarantee is "build attested bytes, fail closed on disk drift."
10
+ // The worker half of StasisMetro, wired by withStasis() as `transformerPath` around the project's transformer. It
11
+ // keeps each import as written for the serializer (./metro-babel-transformer.js), and is the LOAD half: a per-worker
12
+ // seam, as load only reads an immutable bundle, replacing Metro's `data` with the bundle's hash-verified bytes. Metro
13
+ // reads and hashes each file before this runs, so sources must stay on disk: attested bytes, fail closed on drift.
23
14
 
24
15
  const require = createRequire(import.meta.url)
25
16
 
26
- // The project's own transformer, which this one runs: the `transformerPath` withStasis() replaced,
27
- // handed over in Metro's transformer config, else EXODUS_STASIS_METRO_BASE_TRANSFORMER, else
28
- // Metro's default. Metro requires a name from its own dir, which only the project's node_modules
29
- // is sure to reach, so a name resolves from the project root, then from here. A base that is this
30
- // transformer itself (wired by hand as well) is passed over for Metro's default.
17
+ // The project's transformer: the `transformerPath` withStasis() replaced, else EXODUS_STASIS_METRO_BASE_TRANSFORMER,
18
+ // else Metro's default, a name resolved from the project root first, as Metro requires it from its own dir. This
19
+ // transformer itself (wired by hand as well) means Metro's default.
31
20
  const bases = new Map()
32
21
  function baseOf(config, projectRoot = process.cwd()) {
33
22
  const path = config?.stasisBaseTransformerPath ?? (process.env.EXODUS_STASIS_METRO_BASE_TRANSFORMER || 'metro-transform-worker')
@@ -41,15 +30,10 @@ function baseOf(config, projectRoot = process.cwd()) {
41
30
  return bases.get(key)
42
31
  }
43
32
 
44
- // The config the project's transformer sees: its own, without what withStasis() handed over.
45
- function baseConfig({ stasisBaseTransformerPath: _, ...config } = {}) {
46
- return config
47
- }
33
+ const baseConfig = ({ stasisBaseTransformerPath: _, ...config } = {}) => config
48
34
 
49
- // Per-worker load State, built once from the usual env + stasis.config.json. Non-load mode -> null
50
- // -> transparent pass-through, so wiring it permanently is safe. A construction failure is cached
51
- // and rethrown on every call -- never throw-once-then-silently-pass-through. It never writes, so it
52
- // claims no write path: a capturing worker's own preload State already holds them.
35
+ // The worker's load State, null outside load (a pass-through, so wiring this permanently is safe). A failure is
36
+ // cached and rethrown on every call, never passed through after the first. It never writes, so claims no write path.
53
37
  let loaded
54
38
  function getLoadState() {
55
39
  if (loaded === undefined) {
@@ -64,23 +48,14 @@ function getLoadState() {
64
48
  return loaded.state
65
49
  }
66
50
 
67
- // The Babel transformer swapped in for the project's, by path as metro-transform-worker requires it.
68
51
  const BABEL_TRANSFORMER = fileURLToPath(new URL('./metro-babel-transformer.js', import.meta.url))
69
52
 
70
- // The project's Babel transformer, absolute. Metro resolves a configured one; its default
71
- // ('metro-babel-transformer') stays a name, which the base transformer requires from its own dir.
72
- function resolveBabelTransformer(base, path) {
73
- return isAbsolute(path) ? path : createRequire(base.file).resolve(path)
74
- }
53
+ // Metro resolves a configured Babel transformer; its default stays a name, which the base requires from its own dir.
54
+ const resolveBabelTransformer = (base, path) => (isAbsolute(path) ? path : createRequire(base.file).resolve(path))
75
55
 
76
- // Each dependency as the source wrote it. One named by a specifier Babel rewrote carries the ones
77
- // written in the source: one Metro dependency can stand for several, written apart and rewritten
78
- // alike (`#local/abc` and `./src/abc`). One no specifier in the source names carries
79
- // `stasisInserted`: Babel added it (a helper from @babel/runtime, the JSX runtime), or Metro did (its
80
- // asyncRequire for an `import()`, an asset's registry). A dependency written as Metro names it is
81
- // left as is. The imports a Babel plugin consumed, read and inlined in their stead
82
- // (`require('./messages.po')`, @exodus/i18n's plugin-inline-po), no dependency of Metro's, ride on
83
- // the output Metro keeps for the module, its first entry's data, as `stasisConsumed`.
56
+ // Each dependency as the source wrote it: `stasisAsWritten` where Babel rewrote its specifier (several written apart
57
+ // may be rewritten alike), `stasisInserted` where no specifier in the source names it (a Babel helper, Metro's
58
+ // asyncRequire). The imports a Babel plugin consumed ride on the module's first output's data, as `stasisConsumed`.
84
59
  function withAsWritten(result, { asWritten, consumed }) {
85
60
  if (!Array.isArray(result?.dependencies)) return result
86
61
  const dependencies = result.dependencies.map((dep) => {
@@ -99,11 +74,10 @@ export async function transform(config, projectRoot, filename, data, options) {
99
74
  config = baseConfig(config)
100
75
  const state = getLoadState()
101
76
  if (state) {
102
- // `filename` is project-relative in Metro's contract; resolve against projectRoot.
77
+ // `filename` is project-relative in Metro's contract.
103
78
  const absolute = resolvePath(projectRoot, filename)
79
+ // getFile re-verifies the hash and throws on a file the bundle doesn't carry: no disk fallback.
104
80
  if (servedByBundle(state, absolute)) {
105
- // getFile re-verifies the hash and throws on a file the bundle doesn't carry (fail-closed,
106
- // no disk fallback). Code sources come back as strings; resources as Buffers.
107
81
  const { source } = state.getFile(pathToFileURL(absolute).toString())
108
82
  data = Buffer.isBuffer(source) ? source : Buffer.from(source)
109
83
  }
@@ -115,10 +89,8 @@ export async function transform(config, projectRoot, filename, data, options) {
115
89
  return withAsWritten(result, context)
116
90
  }
117
91
 
92
+ // The base's key, marked by the load State transform() goes by, so a load result never shares a disk build's entry.
118
93
  export function getCacheKey(config, opts) {
119
- // Fold in the base transformer's cache key, then add a stasis-load discriminator so load results
120
- // (which can differ from a disk build even when disk == bundle) don't cross-pollinate the cache.
121
- // Derive the marker from the resolved load State, which transform() goes by.
122
94
  const { transformer } = baseOf(config, opts?.projectRoot)
123
95
  const baseKey = typeof transformer.getCacheKey === 'function' ? transformer.getCacheKey(baseConfig(config), opts) : ''
124
96
  const state = getLoadState()