@exodus/stasis-plugins 1.0.0-beta.14 → 1.0.0-beta.16
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/package.json +2 -2
- package/src/esbuild.js +70 -160
- package/src/metro-babel-transformer.js +20 -41
- package/src/metro-native.js +44 -80
- package/src/metro-transformer.js +18 -46
- package/src/metro.js +102 -293
- package/src/plugins.js +82 -88
- package/src/rollup.js +58 -169
- package/src/webpack.js +56 -160
package/src/metro-native.js
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
35
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
52
|
-
|
|
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
|
-
//
|
|
57
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
80
|
-
// names
|
|
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
|
-
//
|
|
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` (
|
|
108
|
-
//
|
|
109
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
137
|
-
//
|
|
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
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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
|
-
//
|
|
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
|
|
184
|
-
//
|
|
185
|
-
//
|
|
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
|
|
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,
|
|
211
|
-
//
|
|
212
|
-
//
|
|
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
|
-
//
|
|
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
|
}
|
package/src/metro-transformer.js
CHANGED
|
@@ -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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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
|
|
27
|
-
//
|
|
28
|
-
//
|
|
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
|
-
|
|
45
|
-
function baseConfig({ stasisBaseTransformerPath: _, ...config } = {}) {
|
|
46
|
-
return config
|
|
47
|
-
}
|
|
33
|
+
const baseConfig = ({ stasisBaseTransformerPath: _, ...config } = {}) => config
|
|
48
34
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
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
|
-
//
|
|
71
|
-
|
|
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
|
|
77
|
-
//
|
|
78
|
-
//
|
|
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
|
|
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()
|