@exodus/stasis-plugins 1.0.0-beta.8 → 1.0.0-rc.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/package.json +6 -3
- package/src/esbuild.js +168 -115
- package/src/metro-babel-transformer.js +129 -0
- package/src/metro-native.js +201 -0
- package/src/metro-transformer.js +68 -55
- package/src/metro.js +258 -282
- package/src/plugins.js +102 -111
- package/src/rollup.js +64 -188
- package/src/webpack.js +61 -181
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { isUtf8 } from 'node:buffer'
|
|
2
|
+
import { basename, dirname, join, posix, relative, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { readJson } from '@exodus/stasis-core/bundle-util'
|
|
5
|
+
import { diskHost } from '@exodus/stasis-core/host'
|
|
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).
|
|
14
|
+
export const AUTO_INCLUDES = ['@react-native-community/cli/setup_env.sh']
|
|
15
|
+
|
|
16
|
+
export function listing(dir, fs) {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readdir(dir)
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
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.
|
|
26
|
+
export const RN_CORE_INCLUDE_FILES = ['sdks/hermes-engine/utils/replace_hermes_version.js']
|
|
27
|
+
|
|
28
|
+
// By name: RN scatters podspecs in subdirs `react-native config` misses.
|
|
29
|
+
const isPodspec = (name) => name.endsWith('.podspec') || name.endsWith('.podspec.json')
|
|
30
|
+
|
|
31
|
+
// The off-platform top-level dir of a native package, which no build on this host compiles.
|
|
32
|
+
export function isExcludedNativeDir(name, { win32 = process.platform === 'win32' } = {}) {
|
|
33
|
+
return !win32 && name === 'windows'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Nested packages (attested separately) and regenerated build output, on top of the shared set.
|
|
37
|
+
const NATIVE_BUILD_OUTPUT_DIRS = new Set(['node_modules', 'build', '.gradle', '.cxx', 'Pods', 'DerivedData'])
|
|
38
|
+
const isSkippedNativeWalkDir = (name) => NATIVE_BUILD_OUTPUT_DIRS.has(name) || isAutoExcludedDir(name) || isNativeArtifact(name)
|
|
39
|
+
|
|
40
|
+
// A compiled tool (Hermes' `hermesc`), never source.
|
|
41
|
+
const isExtensionlessBinary = (name, content) => pathExt(name) === '' && Buffer.isBuffer(content) && !isUtf8(content)
|
|
42
|
+
|
|
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'.
|
|
45
|
+
export function classifyNativeCapture(name, { win32 = process.platform === 'win32', content } = {}) {
|
|
46
|
+
// `.env` files carry secrets: an automated capture must never sweep them in.
|
|
47
|
+
if (isExcludedNativeFile(name, { win32 }) || isDotEnvFile(name)) return { action: 'skip' }
|
|
48
|
+
// Name-matched code is a native build input even where its tag is a Node format the JS graph would own.
|
|
49
|
+
const byName = nameFormat(name)
|
|
50
|
+
if (byName !== undefined) return { action: 'code', format: byName }
|
|
51
|
+
if (CODE_EXTENSIONS.has(pathExt(name))) return { action: 'skip' }
|
|
52
|
+
const format = classifyFormat(name, { content })
|
|
53
|
+
return format === undefined ? { action: 'resource' } : { action: 'code', format }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// classifyNativeCapture refined by the bytes, which only ever demote; a 'resource' gets its format from its bytes.
|
|
57
|
+
export function refineNativeCapture(classified, name, content, resources = new Set()) {
|
|
58
|
+
if (isExtensionlessBinary(name, content)) return { action: 'skip' }
|
|
59
|
+
if (isBinaryPlist(name, content)) return { action: resources.has('plist') ? 'resource' : 'skip' }
|
|
60
|
+
return classified
|
|
61
|
+
}
|
|
62
|
+
|
|
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
|
|
66
|
+
const METRO_PLATFORM_FILE = /^(.+?)(?:\.([^.]+))?\.([^.]+)$/u
|
|
67
|
+
const METRO_ASSET_SCALE = /^(.+?)(?:@([\d.]+)x)?$/u
|
|
68
|
+
export function parseMetroAssetName(name, platform) {
|
|
69
|
+
const match = METRO_PLATFORM_FILE.exec(name)
|
|
70
|
+
if (match === null) return null
|
|
71
|
+
const [, base, suffix, type] = match
|
|
72
|
+
const ofPlatform = suffix !== undefined && suffix === platform
|
|
73
|
+
const [, root, scale] = METRO_ASSET_SCALE.exec(suffix === undefined || ofPlatform ? base : `${base}.${suffix}`)
|
|
74
|
+
const resolution = Number.parseFloat(scale)
|
|
75
|
+
return { asset: `${root}.${type}`, name: root, type, resolution: Number.isNaN(resolution) ? 1 : resolution, platform: ofPlatform ? platform : null }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A file's stat for metroAssetFiles, where a listing names one the disk lacks.
|
|
79
|
+
export const FILE_STAT = { isFile: () => true }
|
|
80
|
+
|
|
81
|
+
// The files `state` (a loaded State) carries, as a directory listing of them (metroAssetFiles' host).
|
|
82
|
+
export function bundleDirHost(state) {
|
|
83
|
+
const dirs = Map.groupBy([...state.sources.keys(), ...state.resources.keys()], (rel) => posix.dirname(rel))
|
|
84
|
+
return {
|
|
85
|
+
readdir: (dir) => (dirs.get(toPosix(state.relative(dir)) || '.') ?? []).map((rel) => ({ name: posix.basename(rel) })),
|
|
86
|
+
stat: () => FILE_STAT,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
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
|
|
93
|
+
export function metroAssetFiles(path, platform = null, host = diskHost) {
|
|
94
|
+
const own = parseMetroAssetName(basename(path), platform)
|
|
95
|
+
if (own === null) return []
|
|
96
|
+
const dir = dirname(path)
|
|
97
|
+
const byPlatform = new Map()
|
|
98
|
+
for (const { name } of host.readdir(dir)) {
|
|
99
|
+
const parsed = parseMetroAssetName(name, platform)
|
|
100
|
+
if (parsed?.asset !== own.asset || host.stat(join(dir, name))?.isFile() !== true) continue
|
|
101
|
+
upsert(byPlatform, parsed.platform, () => []).push(join(dir, name))
|
|
102
|
+
}
|
|
103
|
+
return (platform === null ? undefined : byPlatform.get(platform)) ?? byPlatform.get(null) ?? []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whether the package at `root` ships native code: an ios/ or android/ dir, or a podspec under it.
|
|
107
|
+
export function looksNative(root, fs) {
|
|
108
|
+
return fs.stat(join(root, 'ios')) !== null || fs.stat(join(root, 'android')) !== null || hasPodspec(root, fs)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasPodspec(dir, fs) {
|
|
112
|
+
return listing(dir, fs).some((ent) => !ent.isSymbolicLink() &&
|
|
113
|
+
(ent.isFile() ? isPodspec(ent.name) : ent.isDirectory() && !isSkippedNativeWalkDir(ent.name) && hasPodspec(join(dir, ent.name), fs)))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A spec React Native's codegen parses (its filterJSFile): a JS or TS module named `Native…` or `…NativeComponent`,
|
|
117
|
+
// but a type declaration.
|
|
118
|
+
const isCodegenSpec = (name) => /^(?:Native.+|.+NativeComponent)/u.test(name) && /\.(?:jsx?|tsx?)$/u.test(name) && !name.endsWith('.d.ts')
|
|
119
|
+
|
|
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 }
|
|
123
|
+
export function* nativeInputs(root, fs, { resources = new Set(), skip = () => false } = {}) {
|
|
124
|
+
yield* walk(root, true)
|
|
125
|
+
for (const dir of codegenSources(root, fs)) yield* sources(dir)
|
|
126
|
+
function* sources(dir) {
|
|
127
|
+
for (const ent of listing(dir, fs)) {
|
|
128
|
+
// As codegen skips a dot, node_modules or tests directory.
|
|
129
|
+
if (ent.isSymbolicLink() || ent.name.startsWith('.') || ent.name === 'node_modules' || ent.name.startsWith('__tests')) continue
|
|
130
|
+
const file = join(dir, ent.name)
|
|
131
|
+
if (ent.isDirectory()) yield* sources(file)
|
|
132
|
+
else if (ent.isFile() && isCodegenSpec(ent.name) && !skip(file)) yield { file, source: fs.readFile(file), action: 'code', format: undefined }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function* walk(dir, atRoot) {
|
|
136
|
+
for (const ent of listing(dir, fs)) {
|
|
137
|
+
if (ent.isSymbolicLink()) continue
|
|
138
|
+
const file = join(dir, ent.name)
|
|
139
|
+
if (ent.isDirectory()) {
|
|
140
|
+
if (!isSkippedNativeWalkDir(ent.name) && !(atRoot && isExcludedNativeDir(ent.name))) yield* walk(file, false)
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
if (!ent.isFile() || isNativeArtifact(ent.name) || skip(file)) continue
|
|
144
|
+
const byName = classifyNativeCapture(ent.name)
|
|
145
|
+
if (byName.action === 'skip') continue
|
|
146
|
+
const source = fs.readFile(file)
|
|
147
|
+
const { action, format } = refineNativeCapture(byName, ent.name, source, resources)
|
|
148
|
+
if (action !== 'skip') yield { file, source, action, format }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
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
|
|
156
|
+
function codegenSources(root, fs) {
|
|
157
|
+
const config = readJson(join(root, 'package.json'), fs)?.codegenConfig
|
|
158
|
+
const named = [config?.jsSrcsDir, ...(Array.isArray(config?.libraries) ? config.libraries.map((library) => library?.jsSrcsDir) : [])]
|
|
159
|
+
return named.filter((dir) => typeof dir === 'string' && dir !== '').map((dir) => packageDir(root, dir, fs)).filter((dir) => dir !== null)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The directory `rel` names in the package at `root`, each step a directory entry (not a link, not `..`), else null.
|
|
163
|
+
function packageDir(root, rel, fs) {
|
|
164
|
+
let dir = root
|
|
165
|
+
for (const name of relative(root, join(root, rel)).split(sep)) {
|
|
166
|
+
if (name === '') continue
|
|
167
|
+
if (!listing(dir, fs).some((ent) => ent.name === name && ent.isDirectory())) return null
|
|
168
|
+
dir = join(dir, name)
|
|
169
|
+
}
|
|
170
|
+
return dir
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A package name as npm takes one (`name` or `@scope/name`), which a path can't climb out with.
|
|
174
|
+
const PACKAGE_NAME = /^(?:@[^/\\@.][^/\\]*\/)?[^/\\@.][^/\\]*$/u
|
|
175
|
+
|
|
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
|
|
179
|
+
export function linkedPackages(projectDir, fs) {
|
|
180
|
+
const manifest = readJson(join(projectDir, 'package.json'), fs)
|
|
181
|
+
const listed = (field) => (manifest?.[field] && typeof manifest[field] === 'object' ? Object.keys(manifest[field]) : [])
|
|
182
|
+
const names = new Set([...listed('dependencies'), ...listed('peerDependencies'), ...listed('devDependencies')])
|
|
183
|
+
const dirs = []
|
|
184
|
+
for (const name of names) {
|
|
185
|
+
if (!PACKAGE_NAME.test(name)) continue
|
|
186
|
+
for (let dir = projectDir; ; dir = dirname(dir)) {
|
|
187
|
+
const found = join(dir, 'node_modules', name)
|
|
188
|
+
if (fs.stat(found)?.isDirectory()) {
|
|
189
|
+
dirs.push(found)
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
if (dirname(dir) === dir) break
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return dirs
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Those of linkedPackages a native build links: those that ship native code.
|
|
199
|
+
export function linkedNativePackages(projectDir, fs) {
|
|
200
|
+
return linkedPackages(projectDir, fs).filter((dir) => looksNative(dir, fs))
|
|
201
|
+
}
|
package/src/metro-transformer.js
CHANGED
|
@@ -1,85 +1,98 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
|
-
import { resolve as resolvePath } from 'node:path'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { dirname, isAbsolute, resolve as resolvePath } from 'node:path'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
5
|
|
|
6
6
|
import { State } from '@exodus/stasis-core/state'
|
|
7
|
+
import { servedByBundle } from '@exodus/stasis-core/state-util'
|
|
8
|
+
import { transformContext } from './metro-babel-transformer.js'
|
|
7
9
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// half. In load mode, wrap the user's real transformer and replace Metro's on-disk `data` with the
|
|
13
|
-
// bundle's hash-verified bytes (fail-closed; out-of-scope files pass their disk bytes through).
|
|
14
|
-
// KNOWN LIMITATION: does NOT build with sources absent from disk -- Metro reads + hashes each file
|
|
15
|
-
// 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.
|
|
16
14
|
|
|
17
15
|
const require = createRequire(import.meta.url)
|
|
18
16
|
|
|
19
|
-
// The
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
20
|
+
const bases = new Map()
|
|
21
|
+
function baseOf(config, projectRoot = process.cwd()) {
|
|
22
|
+
const path = config?.stasisBaseTransformerPath ?? (process.env.EXODUS_STASIS_METRO_BASE_TRANSFORMER || 'metro-transform-worker')
|
|
23
|
+
const key = `${projectRoot}\0${path}`
|
|
24
|
+
if (!bases.has(key)) {
|
|
25
|
+
const resolve = (name) => require.resolve(name, { paths: [projectRoot, dirname(fileURLToPath(import.meta.url))] })
|
|
26
|
+
let file = resolve(path)
|
|
27
|
+
if (require(file).transform === transform) file = resolve('metro-transform-worker')
|
|
28
|
+
bases.set(key, { file, transformer: require(file) })
|
|
29
|
+
}
|
|
30
|
+
return bases.get(key)
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
let
|
|
34
|
-
let stateError = null
|
|
33
|
+
const baseConfig = ({ stasisBaseTransformerPath: _, ...config } = {}) => config
|
|
34
|
+
|
|
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.
|
|
37
|
+
let loaded
|
|
35
38
|
function getLoadState() {
|
|
36
|
-
if (
|
|
39
|
+
if (loaded === undefined) {
|
|
37
40
|
try {
|
|
38
|
-
const state = new State(process.cwd())
|
|
39
|
-
|
|
40
|
-
} catch (
|
|
41
|
-
|
|
42
|
-
throw err
|
|
43
|
-
} finally {
|
|
44
|
-
stateInited = true
|
|
41
|
+
const state = new State(process.cwd(), { claim: false })
|
|
42
|
+
loaded = { state: state.config.loadBundle ? state : null }
|
|
43
|
+
} catch (error) {
|
|
44
|
+
loaded = { error }
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
47
|
+
if ('error' in loaded) throw loaded.error
|
|
48
|
+
return loaded.state
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const BABEL_TRANSFORMER = fileURLToPath(new URL('./metro-babel-transformer.js', import.meta.url))
|
|
52
|
+
|
|
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))
|
|
55
|
+
|
|
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`.
|
|
59
|
+
function withAsWritten(result, { asWritten, consumed }) {
|
|
60
|
+
if (!Array.isArray(result?.dependencies)) return result
|
|
61
|
+
const dependencies = result.dependencies.map((dep) => {
|
|
62
|
+
const written = asWritten.get(dep.name)
|
|
63
|
+
if (written === undefined) return { ...dep, stasisInserted: true }
|
|
64
|
+
if (written.size === 1 && written.has(dep.name)) return dep
|
|
65
|
+
return { ...dep, stasisAsWritten: [...written] }
|
|
66
|
+
})
|
|
67
|
+
if (consumed.size === 0 || !Array.isArray(result.output) || result.output.length === 0) return { ...result, dependencies }
|
|
68
|
+
const [first, ...rest] = result.output
|
|
69
|
+
return { ...result, dependencies, output: [{ ...first, data: { ...first.data, stasisConsumed: [...consumed] } }, ...rest] }
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
export function transform(config, projectRoot, filename, data, options) {
|
|
72
|
+
export async function transform(config, projectRoot, filename, data, options) {
|
|
73
|
+
const base = baseOf(config, projectRoot)
|
|
74
|
+
config = baseConfig(config)
|
|
64
75
|
const state = getLoadState()
|
|
65
76
|
if (state) {
|
|
66
|
-
// `filename` is project-relative in Metro's contract
|
|
77
|
+
// `filename` is project-relative in Metro's contract.
|
|
67
78
|
const absolute = resolvePath(projectRoot, filename)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// no disk fallback). Code sources come back as strings; resources as Buffers.
|
|
79
|
+
// getFile re-verifies the hash and throws on a file the bundle doesn't carry: no disk fallback.
|
|
80
|
+
if (servedByBundle(state, absolute)) {
|
|
71
81
|
const { source } = state.getFile(pathToFileURL(absolute).toString())
|
|
72
82
|
data = Buffer.isBuffer(source) ? source : Buffer.from(source)
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
|
-
|
|
85
|
+
if (typeof config.babelTransformerPath !== 'string') return base.transformer.transform(config, projectRoot, filename, data, options)
|
|
86
|
+
const context = { babelTransformerPath: resolveBabelTransformer(base, config.babelTransformerPath), asWritten: new Map(), consumed: new Set() }
|
|
87
|
+
const result = await transformContext.run(context, () =>
|
|
88
|
+
base.transformer.transform({ ...config, babelTransformerPath: BABEL_TRANSFORMER }, projectRoot, filename, data, options))
|
|
89
|
+
return withAsWritten(result, context)
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const baseKey = typeof getBase().getCacheKey === 'function' ? getBase().getCacheKey(config) : ''
|
|
92
|
+
// The base's key, marked by the load State transform() goes by, so a load result never shares a disk build's entry.
|
|
93
|
+
export function getCacheKey(config, opts) {
|
|
94
|
+
const { transformer } = baseOf(config, opts?.projectRoot)
|
|
95
|
+
const baseKey = typeof transformer.getCacheKey === 'function' ? transformer.getCacheKey(baseConfig(config), opts) : ''
|
|
83
96
|
const state = getLoadState()
|
|
84
97
|
const marker = state ? `load:${state.config.bundleFile || 'default'}` : 'off'
|
|
85
98
|
return `${baseKey}$stasis-metro-transformer:${marker}`
|