@tamagui/metro-plugin 2.7.7 → 3.0.0-beta.643.1
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 +12 -0
- package/dist/cjs/babel.cjs +77 -0
- package/dist/cjs/compilerCache.cjs +237 -0
- package/dist/cjs/diagnostics.cjs +41 -0
- package/dist/cjs/frontend.cjs +870 -0
- package/dist/cjs/index.cjs +102 -0
- package/dist/cjs/lowering.cjs +109 -0
- package/dist/cjs/metroResolver.cjs +197 -0
- package/dist/cjs/transformOptions.cjs +35 -0
- package/dist/cjs/transformer.cjs +142 -0
- package/dist/cjs/zeroRuntime.cjs +140 -0
- package/dist/cjs/zeroSerializer.cjs +150 -0
- package/dist/esm/babel.mjs +52 -0
- package/dist/esm/babel.mjs.map +1 -0
- package/dist/esm/compilerCache.mjs +212 -0
- package/dist/esm/compilerCache.mjs.map +1 -0
- package/dist/esm/diagnostics.mjs +18 -0
- package/dist/esm/diagnostics.mjs.map +1 -0
- package/dist/esm/frontend.mjs +839 -0
- package/dist/esm/frontend.mjs.map +1 -0
- package/dist/esm/index.mjs +64 -22
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/lowering.mjs +89 -0
- package/dist/esm/lowering.mjs.map +1 -0
- package/dist/esm/metroResolver.mjs +173 -0
- package/dist/esm/metroResolver.mjs.map +1 -0
- package/dist/esm/transformOptions.mjs +14 -0
- package/dist/esm/transformOptions.mjs.map +1 -0
- package/dist/esm/transformer.mjs +119 -0
- package/dist/esm/transformer.mjs.map +1 -0
- package/dist/esm/zeroRuntime.mjs +105 -0
- package/dist/esm/zeroRuntime.mjs.map +1 -0
- package/dist/esm/zeroSerializer.mjs +123 -0
- package/dist/esm/zeroSerializer.mjs.map +1 -0
- package/package.json +33 -5
- package/src/babel.ts +87 -0
- package/src/compilerCache.ts +346 -0
- package/src/diagnostics.ts +47 -0
- package/src/frontend.ts +1178 -0
- package/src/index.ts +117 -14
- package/src/lowering.ts +136 -0
- package/src/metroResolver.ts +209 -0
- package/src/transformOptions.ts +36 -0
- package/src/transformer.ts +210 -0
- package/src/zeroRuntime.ts +212 -0
- package/src/zeroSerializer.ts +175 -0
- package/types/babel.d.ts +28 -0
- package/types/babel.d.ts.map +11 -0
- package/types/compilerCache.d.ts +63 -0
- package/types/compilerCache.d.ts.map +11 -0
- package/types/diagnostics.d.ts +16 -0
- package/types/diagnostics.d.ts.map +11 -0
- package/types/frontend.d.ts +73 -0
- package/types/frontend.d.ts.map +11 -0
- package/types/index.d.ts +49 -32
- package/types/index.d.ts.map +11 -1
- package/types/lowering.d.ts +20 -0
- package/types/lowering.d.ts.map +11 -0
- package/types/metroResolver.d.ts +21 -0
- package/types/metroResolver.d.ts.map +11 -0
- package/types/transformOptions.d.ts +13 -0
- package/types/transformOptions.d.ts.map +11 -0
- package/types/transformer.d.ts +26 -0
- package/types/transformer.d.ts.map +11 -0
- package/types/zeroRuntime.d.ts +75 -0
- package/types/zeroRuntime.d.ts.map +11 -0
- package/types/zeroSerializer.d.ts +6 -0
- package/types/zeroSerializer.d.ts.map +11 -0
- package/dist/cjs/index.js +0 -45
- package/dist/cjs/index.js.map +0 -6
- package/dist/esm/index.js +0 -25
- package/dist/esm/index.js.map +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync, renameSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
compileWithUserBabel,
|
|
7
|
+
userBabelCacheKey,
|
|
8
|
+
type MetroBabelTransformArgs,
|
|
9
|
+
type MetroBabelTransformResult,
|
|
10
|
+
} from './babel'
|
|
11
|
+
import {
|
|
12
|
+
METRO_COMPILER_CACHE_VERSION,
|
|
13
|
+
MetroCompilerCache,
|
|
14
|
+
MetroCompilerCacheError,
|
|
15
|
+
} from './compilerCache'
|
|
16
|
+
import {
|
|
17
|
+
formatMetroCompilerDiagnostic,
|
|
18
|
+
metroDiagnostic,
|
|
19
|
+
type MetroCompilerDiagnostic,
|
|
20
|
+
} from './diagnostics'
|
|
21
|
+
import { isCompilerSourceFile } from './metroResolver'
|
|
22
|
+
import { applyMetroCompilerPlan, type MetroCompilerLoweringResult } from './lowering'
|
|
23
|
+
|
|
24
|
+
export interface MetroCompilerTransformerOptions {
|
|
25
|
+
cacheBaseRoot: string
|
|
26
|
+
originalBabelTransformerPath: string
|
|
27
|
+
projectRoot: string
|
|
28
|
+
/**
|
|
29
|
+
* The integration-owned `TAMAGUI_RUNTIME` literal for this bundle request.
|
|
30
|
+
* Metro never reads an ambient value: the literal is decided by the build and
|
|
31
|
+
* inlined here so every guard is a constant.
|
|
32
|
+
*/
|
|
33
|
+
runtimeLiteral?: 'full' | 'zero'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MetroCompilerTransformMetadata {
|
|
37
|
+
cacheHit: boolean
|
|
38
|
+
diagnostics: MetroCompilerDiagnostic[]
|
|
39
|
+
lowering?: MetroCompilerLoweringResult
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createMetroCompilerTransformer(config: MetroCompilerTransformerOptions): {
|
|
43
|
+
transform(args: MetroBabelTransformArgs): Promise<MetroBabelTransformResult>
|
|
44
|
+
getCacheKey(): string
|
|
45
|
+
} {
|
|
46
|
+
// Metro hands workers project-relative filenames while the compiler cache is
|
|
47
|
+
// keyed by absolute realpaths (the frontend realpaths every module). Resolve
|
|
48
|
+
// to the same form or every plan lookup silently misses and the whole build
|
|
49
|
+
// ships unlowered.
|
|
50
|
+
const moduleIdCache = new Map<string, string>()
|
|
51
|
+
const missWarned = new Set<string>()
|
|
52
|
+
function cacheModuleId(filename: string): string {
|
|
53
|
+
let id = moduleIdCache.get(filename)
|
|
54
|
+
if (!id) {
|
|
55
|
+
const absolute = isAbsolute(filename)
|
|
56
|
+
? filename
|
|
57
|
+
: resolve(config.projectRoot, filename)
|
|
58
|
+
try {
|
|
59
|
+
id = realpathSync(absolute)
|
|
60
|
+
} catch {
|
|
61
|
+
id = absolute
|
|
62
|
+
}
|
|
63
|
+
moduleIdCache.set(filename, id)
|
|
64
|
+
}
|
|
65
|
+
return id
|
|
66
|
+
}
|
|
67
|
+
// Metro also transforms modules the frontend can never plan: bundler-injected
|
|
68
|
+
// polyfills, virtual modules, and node_modules (external by design). A miss
|
|
69
|
+
// is only a lowering defect for a file the frontend's project graph would
|
|
70
|
+
// have crawled.
|
|
71
|
+
function planEligible(moduleId: string): boolean {
|
|
72
|
+
return (
|
|
73
|
+
isCompilerSourceFile(moduleId) &&
|
|
74
|
+
!moduleId.includes(`${join('node_modules')}`) &&
|
|
75
|
+
existsSync(moduleId)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
// Replaces only the exact member expression `process.env.TAMAGUI_RUNTIME`.
|
|
79
|
+
// Metro has no define mechanism, so this is the transform-level equivalent.
|
|
80
|
+
const runtimeLiteral = config.runtimeLiteral ?? 'full'
|
|
81
|
+
const inlineRuntimeLiteral = ({ types }: { types: any }) => ({
|
|
82
|
+
visitor: {
|
|
83
|
+
MemberExpression(nodePath: any) {
|
|
84
|
+
const node = nodePath.node
|
|
85
|
+
if (
|
|
86
|
+
node.computed ||
|
|
87
|
+
!types.isIdentifier(node.property, { name: 'TAMAGUI_RUNTIME' }) ||
|
|
88
|
+
!types.isMemberExpression(node.object) ||
|
|
89
|
+
node.object.computed ||
|
|
90
|
+
!types.isIdentifier(node.object.object, { name: 'process' }) ||
|
|
91
|
+
!types.isIdentifier(node.object.property, { name: 'env' })
|
|
92
|
+
) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
nodePath.replaceWith(types.stringLiteral(runtimeLiteral))
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
async transform(argsIn) {
|
|
102
|
+
const args = {
|
|
103
|
+
...argsIn,
|
|
104
|
+
plugins: [...(argsIn.plugins ?? []), inlineRuntimeLiteral],
|
|
105
|
+
}
|
|
106
|
+
const platform =
|
|
107
|
+
typeof args.options.platform === 'string' ? args.options.platform : 'default'
|
|
108
|
+
const cache = new MetroCompilerCache(join(config.cacheBaseRoot, platform))
|
|
109
|
+
let tamagui: MetroCompilerTransformMetadata = {
|
|
110
|
+
cacheHit: false,
|
|
111
|
+
diagnostics: [],
|
|
112
|
+
}
|
|
113
|
+
const moduleId = cacheModuleId(args.filename)
|
|
114
|
+
try {
|
|
115
|
+
// a manifest exists exactly when the frontend planned this build, so a
|
|
116
|
+
// lookup miss on a plannable file is a lowering defect (unlowered
|
|
117
|
+
// output), never routine — surface it instead of silently shipping
|
|
118
|
+
// runtime-path modules
|
|
119
|
+
const entry = await cache.read(moduleId, args.src, (reason, detail) => {
|
|
120
|
+
if (missWarned.has(moduleId) || !planEligible(moduleId)) return
|
|
121
|
+
missWarned.add(moduleId)
|
|
122
|
+
const diagnostic = metroDiagnostic(
|
|
123
|
+
'metro/plan-miss',
|
|
124
|
+
`Lowering plan lookup missed for ${moduleId} (${reason}${detail ? `: ${detail}` : ''}); module ships unlowered`,
|
|
125
|
+
{ moduleId }
|
|
126
|
+
)
|
|
127
|
+
tamagui.diagnostics.push(diagnostic)
|
|
128
|
+
console.warn(formatMetroCompilerDiagnostic(diagnostic, config.projectRoot))
|
|
129
|
+
})
|
|
130
|
+
if (entry) {
|
|
131
|
+
try {
|
|
132
|
+
const lowered = await applyMetroCompilerPlan(
|
|
133
|
+
{ ...args, filename: moduleId },
|
|
134
|
+
entry.plan,
|
|
135
|
+
config.originalBabelTransformerPath
|
|
136
|
+
)
|
|
137
|
+
return {
|
|
138
|
+
...lowered.compiled.result,
|
|
139
|
+
metadata: {
|
|
140
|
+
...lowered.compiled.result.metadata,
|
|
141
|
+
tamagui: {
|
|
142
|
+
cacheHit: true,
|
|
143
|
+
diagnostics: entry.diagnostics,
|
|
144
|
+
lowering: lowered.lowering,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const diagnostic = metroDiagnostic(
|
|
150
|
+
'metro/cache-corrupt',
|
|
151
|
+
`Cached lowering plan for ${args.filename} could not be applied: ${error instanceof Error ? error.message : String(error)}`,
|
|
152
|
+
{ moduleId }
|
|
153
|
+
)
|
|
154
|
+
tamagui = {
|
|
155
|
+
cacheHit: true,
|
|
156
|
+
diagnostics: [...entry.diagnostics, diagnostic],
|
|
157
|
+
}
|
|
158
|
+
console.warn(formatMetroCompilerDiagnostic(diagnostic, config.projectRoot))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!(error instanceof MetroCompilerCacheError)) throw error
|
|
163
|
+
tamagui.diagnostics.push(error.diagnostic)
|
|
164
|
+
console.warn(formatMetroCompilerDiagnostic(error.diagnostic, config.projectRoot))
|
|
165
|
+
}
|
|
166
|
+
const compiled = await compileWithUserBabel(
|
|
167
|
+
config.originalBabelTransformerPath,
|
|
168
|
+
args
|
|
169
|
+
)
|
|
170
|
+
return {
|
|
171
|
+
...compiled.result,
|
|
172
|
+
metadata: {
|
|
173
|
+
...compiled.result.metadata,
|
|
174
|
+
tamagui,
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
getCacheKey() {
|
|
179
|
+
return createHash('sha256')
|
|
180
|
+
.update(`tamagui-metro-compiler-v${METRO_COMPILER_CACHE_VERSION}`)
|
|
181
|
+
.update('\0')
|
|
182
|
+
.update(runtimeLiteral)
|
|
183
|
+
.update('\0')
|
|
184
|
+
.update(userBabelCacheKey(config.originalBabelTransformerPath))
|
|
185
|
+
.digest('hex')
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function writeMetroCompilerTransformerBridge(
|
|
191
|
+
transformerFactoryPath: string,
|
|
192
|
+
config: MetroCompilerTransformerOptions
|
|
193
|
+
): string {
|
|
194
|
+
const serializedConfig = JSON.stringify(config)
|
|
195
|
+
const bridgeHash = createHash('sha256')
|
|
196
|
+
.update(transformerFactoryPath)
|
|
197
|
+
.update('\0')
|
|
198
|
+
.update(serializedConfig)
|
|
199
|
+
.digest('hex')
|
|
200
|
+
const directory = join(config.cacheBaseRoot, 'bridge')
|
|
201
|
+
const bridgePath = join(directory, `${bridgeHash}.cjs`)
|
|
202
|
+
const temporaryPath = `${bridgePath}.${process.pid}.tmp`
|
|
203
|
+
const source = `'use strict'\nmodule.exports = require(${JSON.stringify(
|
|
204
|
+
transformerFactoryPath
|
|
205
|
+
)}).createMetroCompilerTransformer(${serializedConfig})\n`
|
|
206
|
+
mkdirSync(directory, { recursive: true })
|
|
207
|
+
writeFileSync(temporaryPath, source, 'utf8')
|
|
208
|
+
renameSync(temporaryPath, bridgePath)
|
|
209
|
+
return bridgePath
|
|
210
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import Static from '@tamagui/static'
|
|
6
|
+
import type {
|
|
7
|
+
IslandThemeBridge,
|
|
8
|
+
TamaguiOptions,
|
|
9
|
+
ZeroCSSArtifact,
|
|
10
|
+
ZeroRuntimeResolved,
|
|
11
|
+
ZeroViolationSite,
|
|
12
|
+
} from '@tamagui/static'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Metro's half of the zero-runtime mode.
|
|
16
|
+
*
|
|
17
|
+
* Metro fixes a module's dependencies at resolution time and does no
|
|
18
|
+
* export-level shaking, so nothing after the transform can remove an import.
|
|
19
|
+
* The frontend already lowers every module up front and publishes plans that
|
|
20
|
+
* workers apply before Babel runs, which is the one place early enough: zero
|
|
21
|
+
* reference erasure rides those same plans.
|
|
22
|
+
*
|
|
23
|
+
* An island is a second Metro bundle request rather than a child compilation,
|
|
24
|
+
* because Metro has no sub-compilation concept. The two requests are separate
|
|
25
|
+
* processes, so the CSS coordinator hands island fragments over on disk.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const ZERO_CSS_FILENAME = 'tamagui-zero.css'
|
|
29
|
+
export const ZERO_ISLAND_DIRNAME = 'tamagui-islands'
|
|
30
|
+
|
|
31
|
+
export interface MetroZeroController {
|
|
32
|
+
resolved: ZeroRuntimeResolved
|
|
33
|
+
artifact: ZeroCSSArtifact
|
|
34
|
+
cssHref: string
|
|
35
|
+
root: string
|
|
36
|
+
/** Directory the artifact and island bundle are published from. */
|
|
37
|
+
publicDir: string
|
|
38
|
+
/** Island id when this Metro invocation is building an island, else null. */
|
|
39
|
+
islandBuild: string | null
|
|
40
|
+
bridges: Map<string, IslandThemeBridge[]>
|
|
41
|
+
violations: ZeroViolationSite[]
|
|
42
|
+
/** Modules the zero transform ran on, for the erased-export gate. */
|
|
43
|
+
transformed: Set<string>
|
|
44
|
+
/** Erased exported declarator names, by declaring module. */
|
|
45
|
+
erasedExports: Map<string, string[]>
|
|
46
|
+
loaderIds: Map<string, string>
|
|
47
|
+
islandModuleIds: Map<string, string>
|
|
48
|
+
/** False in `report` mode, where the analysis runs and nothing else changes. */
|
|
49
|
+
isEnforcing: boolean
|
|
50
|
+
/** The evaluated config's CSS, set once the frontend has loaded the project. */
|
|
51
|
+
configCSS: string
|
|
52
|
+
/**
|
|
53
|
+
* True when this build restored the artifact from the plan cache's CSS
|
|
54
|
+
* sidecar instead of rescanning. Recorded in the receipt so a warm rebuild
|
|
55
|
+
* that silently stopped reusing plans, or one that reused them without
|
|
56
|
+
* restoring the artifact, is visible rather than inferred from timing.
|
|
57
|
+
*/
|
|
58
|
+
plansRestoredFromCache: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const normalizePath = (value: string) => value.replace(/\\/g, '/')
|
|
62
|
+
|
|
63
|
+
export const zeroModuleKey = (value: string): string =>
|
|
64
|
+
normalizePath(value).replace(/\.(?:js|jsx|ts|tsx|mjs|cjs)$/, '')
|
|
65
|
+
|
|
66
|
+
/** Where an island build leaves its CSS fragment for the zero build to collect. */
|
|
67
|
+
export function islandFragmentPath(outDir: string, islandId: string): string {
|
|
68
|
+
return path.join(outDir, ZERO_ISLAND_DIRNAME, `${islandId}.css`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function islandBundleHashPath(outDir: string, islandId: string): string {
|
|
72
|
+
return path.join(outDir, ZERO_ISLAND_DIRNAME, `${islandId}.hash`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createMetroZeroController(
|
|
76
|
+
options: TamaguiOptions,
|
|
77
|
+
root: string,
|
|
78
|
+
islandBuild: string | null,
|
|
79
|
+
publicDirName: string
|
|
80
|
+
): MetroZeroController | null {
|
|
81
|
+
const resolved = Static.resolveZeroRuntimeSync(options, root)
|
|
82
|
+
if (resolved.mode === 'off') return null
|
|
83
|
+
Static.assertZeroIntegrationSupport('metro-web', resolved)
|
|
84
|
+
|
|
85
|
+
const cssHref = `/${ZERO_CSS_FILENAME}`
|
|
86
|
+
const artifact = new Static.ZeroCSSArtifact(resolved.cssPath)
|
|
87
|
+
artifact.expectIslands(resolved.islands.map((island) => island.id))
|
|
88
|
+
|
|
89
|
+
const configPath = path.isAbsolute(options.config || '')
|
|
90
|
+
? options.config!
|
|
91
|
+
: path.resolve(root, options.config || 'tamagui.config.ts')
|
|
92
|
+
|
|
93
|
+
for (const island of resolved.islands) {
|
|
94
|
+
Static.writeIslandModules({
|
|
95
|
+
island,
|
|
96
|
+
integration: 'metro-web',
|
|
97
|
+
configPath,
|
|
98
|
+
scriptUrl: `/${ZERO_ISLAND_DIRNAME}/${island.id}.js`,
|
|
99
|
+
cssHref,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
resolved,
|
|
105
|
+
artifact,
|
|
106
|
+
cssHref,
|
|
107
|
+
root,
|
|
108
|
+
publicDir: path.join(root, publicDirName),
|
|
109
|
+
islandBuild,
|
|
110
|
+
bridges: new Map(),
|
|
111
|
+
violations: [],
|
|
112
|
+
transformed: new Set(),
|
|
113
|
+
erasedExports: new Map(),
|
|
114
|
+
isEnforcing: resolved.mode === 'enforce',
|
|
115
|
+
loaderIds: new Map(
|
|
116
|
+
resolved.islands.map((island) => [zeroModuleKey(island.loader), island.id])
|
|
117
|
+
),
|
|
118
|
+
islandModuleIds: new Map(
|
|
119
|
+
resolved.islands.map((island) => [zeroModuleKey(island.module), island.id])
|
|
120
|
+
),
|
|
121
|
+
configCSS: '',
|
|
122
|
+
plansRestoredFromCache: false,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generated shim modules for the island bundle's React handoff.
|
|
128
|
+
*
|
|
129
|
+
* Metro has no externals option, so the island build redirects `react`,
|
|
130
|
+
* `react-dom`, and `react/jsx-runtime` through these, which read the handoff
|
|
131
|
+
* the generated loader publishes. One React instance serves both graphs.
|
|
132
|
+
*/
|
|
133
|
+
export function writeIslandRuntimeShims(outDir: string): Record<string, string> {
|
|
134
|
+
const directory = path.join(outDir, 'runtime-shim')
|
|
135
|
+
mkdirSync(directory, { recursive: true })
|
|
136
|
+
const shims: Record<string, string> = {}
|
|
137
|
+
for (const [specifier, segments] of Object.entries(
|
|
138
|
+
Static.ISLAND_EXTERNAL_GLOBAL_PATHS
|
|
139
|
+
)) {
|
|
140
|
+
const file = path.join(directory, `${specifier.replace(/[^a-zA-Z0-9]+/g, '_')}.js`)
|
|
141
|
+
const source = `// generated by @tamagui/metro-plugin zero-runtime. do not edit.\nmodule.exports = globalThis.${segments.join(
|
|
142
|
+
'.'
|
|
143
|
+
)}\n`
|
|
144
|
+
if (!existsSync(file) || readFileSync(file, 'utf8') !== source) {
|
|
145
|
+
writeFileSync(file, source)
|
|
146
|
+
}
|
|
147
|
+
shims[specifier] = file
|
|
148
|
+
}
|
|
149
|
+
return shims
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface MetroZeroFinalizeInput {
|
|
153
|
+
controller: MetroZeroController
|
|
154
|
+
/** The serialized bundle, hashed into the island's output receipt. */
|
|
155
|
+
bundleCode: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Writes the one CSS artifact for a zero build, or this island's fragment for
|
|
160
|
+
* an island build. `TAMAGUI_DID_OUTPUT_CSS` is derived only when every declared
|
|
161
|
+
* island fragment is present.
|
|
162
|
+
*/
|
|
163
|
+
export function finalizeMetroZero(input: MetroZeroFinalizeInput): {
|
|
164
|
+
cssPath: string
|
|
165
|
+
hash: string
|
|
166
|
+
islandOutputHashes: Record<string, string>
|
|
167
|
+
} {
|
|
168
|
+
const { controller } = input
|
|
169
|
+
const outDir = controller.resolved.outDir
|
|
170
|
+
|
|
171
|
+
if (controller.islandBuild) {
|
|
172
|
+
const fragment = [...controller.artifact.islandCSS(controller.islandBuild)].join('')
|
|
173
|
+
const file = islandFragmentPath(outDir, controller.islandBuild)
|
|
174
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
175
|
+
writeFileSync(file, fragment)
|
|
176
|
+
writeFileSync(
|
|
177
|
+
islandBundleHashPath(outDir, controller.islandBuild),
|
|
178
|
+
createHash('sha256').update(input.bundleCode).digest('hex').slice(0, 16)
|
|
179
|
+
)
|
|
180
|
+
return { cssPath: file, hash: '', islandOutputHashes: {} }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
controller.artifact.setConfigCSS(controller.configCSS)
|
|
184
|
+
const islandOutputHashes: Record<string, string> = {}
|
|
185
|
+
for (const island of controller.resolved.islands) {
|
|
186
|
+
const fragment = islandFragmentPath(outDir, island.id)
|
|
187
|
+
if (!existsSync(fragment)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`[tamagui zero-runtime] island "${island.id}" has not been built. Build every declared island bundle before the zero entry so the one CSS artifact can be finalized.`
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
controller.artifact.setIslandModuleCSS(
|
|
193
|
+
island.id,
|
|
194
|
+
island.module,
|
|
195
|
+
readFileSync(fragment, 'utf8')
|
|
196
|
+
)
|
|
197
|
+
const hashFile = islandBundleHashPath(outDir, island.id)
|
|
198
|
+
islandOutputHashes[island.id] = existsSync(hashFile)
|
|
199
|
+
? readFileSync(hashFile, 'utf8')
|
|
200
|
+
: ''
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const written = controller.artifact.write()
|
|
204
|
+
if (!written.complete) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`[tamagui zero-runtime] cannot derive TAMAGUI_DID_OUTPUT_CSS: the generated CSS artifact is missing ${written.missing.join(
|
|
207
|
+
', '
|
|
208
|
+
)}`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
return { cssPath: written.path, hash: written.hash, islandOutputHashes }
|
|
212
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { gzipSync } from 'node:zlib'
|
|
5
|
+
|
|
6
|
+
import Static from '@tamagui/static'
|
|
7
|
+
import type { ZeroGraphReceipt } from '@tamagui/static'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
finalizeMetroZero,
|
|
11
|
+
writeIslandRuntimeShims,
|
|
12
|
+
ZERO_CSS_FILENAME,
|
|
13
|
+
ZERO_ISLAND_DIRNAME,
|
|
14
|
+
type MetroZeroController,
|
|
15
|
+
} from './zeroRuntime'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Metro's serializer-time gate and the resolver redirects the island bundle
|
|
19
|
+
* needs.
|
|
20
|
+
*
|
|
21
|
+
* The serializer is the first point where Metro's whole module graph exists,
|
|
22
|
+
* so it is where the forbidden-module check runs. It is not where erasure
|
|
23
|
+
* happens: Metro fixes dependencies at resolution, so by serializer time a
|
|
24
|
+
* surviving import is already a graph member. That is the point of checking
|
|
25
|
+
* here rather than fixing here.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const requireFromPlugin = createRequire(
|
|
29
|
+
typeof __filename === 'string' ? __filename : import.meta.url
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// version-pinned, not a feature-detection chain: this is the serializer the
|
|
33
|
+
// repository's Metro ships, and it is used only when the app supplied none
|
|
34
|
+
const baseJSBundle = requireFromPlugin(
|
|
35
|
+
'metro/private/DeltaBundler/Serializers/baseJSBundle'
|
|
36
|
+
).default as (entryPoint: any, preModules: any, graph: any, options: any) => any
|
|
37
|
+
const bundleToString = requireFromPlugin('metro/private/lib/bundleToString').default as (
|
|
38
|
+
bundle: any
|
|
39
|
+
) => { code: string; map: string }
|
|
40
|
+
|
|
41
|
+
type MetroConfigInput = Record<string, any>
|
|
42
|
+
|
|
43
|
+
export function applyMetroZeroRuntime(
|
|
44
|
+
metroConfig: MetroConfigInput,
|
|
45
|
+
zero: MetroZeroController
|
|
46
|
+
): void {
|
|
47
|
+
if (zero.islandBuild) {
|
|
48
|
+
// Metro has no externals option, so React is redirected through generated
|
|
49
|
+
// shim modules that read the handoff the island loader publishes
|
|
50
|
+
const shims = writeIslandRuntimeShims(zero.resolved.outDir)
|
|
51
|
+
const userResolveRequest = metroConfig.resolver?.resolveRequest
|
|
52
|
+
metroConfig.resolver = {
|
|
53
|
+
...metroConfig.resolver,
|
|
54
|
+
resolveRequest(context: any, moduleName: string, platform: string | null) {
|
|
55
|
+
const shim = shims[moduleName]
|
|
56
|
+
if (shim) return { type: 'sourceFile', filePath: shim }
|
|
57
|
+
return userResolveRequest
|
|
58
|
+
? userResolveRequest(context, moduleName, platform)
|
|
59
|
+
: context.resolveRequest(context, moduleName, platform)
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const userSerializer = metroConfig.serializer?.customSerializer
|
|
65
|
+
metroConfig.serializer = {
|
|
66
|
+
...metroConfig.serializer,
|
|
67
|
+
async customSerializer(entryPoint: any, preModules: any, graph: any, opts: any) {
|
|
68
|
+
const receipt = checkGraph(zero, entryPoint, graph)
|
|
69
|
+
const output = userSerializer
|
|
70
|
+
? await userSerializer(entryPoint, preModules, graph, opts)
|
|
71
|
+
: bundleToString(baseJSBundle(entryPoint, preModules, graph, opts)).code
|
|
72
|
+
|
|
73
|
+
const finalized = finalizeMetroZero({
|
|
74
|
+
controller: zero,
|
|
75
|
+
bundleCode: typeof output === 'string' ? output : '',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (!zero.islandBuild) {
|
|
79
|
+
mkdirSync(zero.publicDir, { recursive: true })
|
|
80
|
+
const css = zero.artifact.css()
|
|
81
|
+
const published = path.join(zero.publicDir, ZERO_CSS_FILENAME)
|
|
82
|
+
writeFileSync(published, css)
|
|
83
|
+
// the served copy is what the page loads, so it is the one the claim
|
|
84
|
+
// depends on: read it back rather than trusting the write
|
|
85
|
+
const publishFailure = Static.checkGlobalCSSArtifact({
|
|
86
|
+
cssPath: published,
|
|
87
|
+
expectedCSS: css,
|
|
88
|
+
loadedModuleIds: [published],
|
|
89
|
+
importHint: '',
|
|
90
|
+
})
|
|
91
|
+
if (publishFailure) throw new Error(publishFailure.message)
|
|
92
|
+
receipt.cssArtifact = { path: zero.cssHref, hash: finalized.hash }
|
|
93
|
+
receipt.gzip = {
|
|
94
|
+
[ZERO_CSS_FILENAME]: gzipSync(Buffer.from(css), { level: 9 }).length,
|
|
95
|
+
bundle: gzipSync(Buffer.from(typeof output === 'string' ? output : ''), {
|
|
96
|
+
level: 9,
|
|
97
|
+
}).length,
|
|
98
|
+
}
|
|
99
|
+
const bridgeManifest = Static.canonicalizeBridgeManifest(
|
|
100
|
+
Object.fromEntries(
|
|
101
|
+
[...zero.bridges.entries()].sort(([left], [right]) => (left < right ? -1 : 1))
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
const identityInputs = {
|
|
105
|
+
runtimeLiteral: 'zero' as const,
|
|
106
|
+
target: 'web' as const,
|
|
107
|
+
configGeneration: Static.hashBridgeManifest(zero.configCSS),
|
|
108
|
+
cssHash: finalized.hash,
|
|
109
|
+
compilerVersion: Static.ZERO_COMPILER_VERSION,
|
|
110
|
+
islandEntries: zero.resolved.islands.map((island) => island.module),
|
|
111
|
+
bridgeManifestHash: Static.hashBridgeManifest(bridgeManifest),
|
|
112
|
+
islandOutputHashes: finalized.islandOutputHashes,
|
|
113
|
+
}
|
|
114
|
+
receipt.identity = Static.hashZeroIdentity(identityInputs)
|
|
115
|
+
receipt.plansRestoredFromCache = zero.plansRestoredFromCache
|
|
116
|
+
Static.writeZeroGraphReceipt(zero.resolved.outDir, 'metro-zero', receipt)
|
|
117
|
+
writeFileSync(
|
|
118
|
+
path.join(zero.resolved.outDir, 'metro-zero.bridges.json'),
|
|
119
|
+
`${JSON.stringify(
|
|
120
|
+
{ identity: receipt.identity, identityInputs, bridges: bridgeManifest },
|
|
121
|
+
null,
|
|
122
|
+
2
|
|
123
|
+
)}\n`
|
|
124
|
+
)
|
|
125
|
+
if (receipt.forbidden.length) {
|
|
126
|
+
throw new Error(Static.formatZeroGraphFailure(receipt))
|
|
127
|
+
}
|
|
128
|
+
console.info(
|
|
129
|
+
` ➡ [tamagui zero-runtime] ${receipt.moduleCount} modules, 0 forbidden, css ${receipt.gzip[ZERO_CSS_FILENAME]} gzip, islands: ${
|
|
130
|
+
zero.resolved.islands.map((island) => island.id).join(', ') || 'none'
|
|
131
|
+
}`
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return output
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkGraph(
|
|
141
|
+
zero: MetroZeroController,
|
|
142
|
+
entryPoint: string,
|
|
143
|
+
graph: { dependencies: Map<string, any> }
|
|
144
|
+
): ZeroGraphReceipt {
|
|
145
|
+
const modules: { id: string; importers: string[] }[] = []
|
|
146
|
+
const importerEdges = new Map<string, string[]>()
|
|
147
|
+
for (const [id, module] of graph.dependencies) {
|
|
148
|
+
const importers = [...(module.inverseDependencies ?? [])] as string[]
|
|
149
|
+
importerEdges.set(id, importers)
|
|
150
|
+
modules.push({ id, importers })
|
|
151
|
+
}
|
|
152
|
+
const escape = Static.erasedExportEscape({
|
|
153
|
+
integration: 'metro-web',
|
|
154
|
+
transformed: zero.transformed,
|
|
155
|
+
erasedExports: zero.erasedExports,
|
|
156
|
+
importersOf: importerEdges,
|
|
157
|
+
})
|
|
158
|
+
if (escape) throw new Error(escape)
|
|
159
|
+
const checked = Static.checkZeroGraph({
|
|
160
|
+
entries: [entryPoint],
|
|
161
|
+
modules,
|
|
162
|
+
importerEdges,
|
|
163
|
+
root: zero.resolved.root,
|
|
164
|
+
})
|
|
165
|
+
return {
|
|
166
|
+
integration: 'metro-web',
|
|
167
|
+
graph: zero.islandBuild ? 'island' : 'zero',
|
|
168
|
+
entries: [entryPoint],
|
|
169
|
+
moduleCount: modules.length,
|
|
170
|
+
tamaguiModules: checked.tamaguiModules,
|
|
171
|
+
forbidden: checked.forbidden,
|
|
172
|
+
cssArtifact: null,
|
|
173
|
+
identity: '',
|
|
174
|
+
}
|
|
175
|
+
}
|
package/types/babel.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface MetroBabelTransformArgs {
|
|
2
|
+
filename: string;
|
|
3
|
+
src: string;
|
|
4
|
+
options: Record<string, any>;
|
|
5
|
+
plugins: unknown[];
|
|
6
|
+
}
|
|
7
|
+
export interface MetroBabelTransformResult {
|
|
8
|
+
ast: Record<string, any>;
|
|
9
|
+
metadata?: Record<string, any>;
|
|
10
|
+
functionMap?: unknown;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface CompiledMetroModule {
|
|
14
|
+
code: string;
|
|
15
|
+
map: Record<string, any>;
|
|
16
|
+
result: MetroBabelTransformResult;
|
|
17
|
+
}
|
|
18
|
+
type BabelTransformer = {
|
|
19
|
+
transform(args: MetroBabelTransformArgs): MetroBabelTransformResult | Promise<MetroBabelTransformResult>;
|
|
20
|
+
getCacheKey?(): string;
|
|
21
|
+
};
|
|
22
|
+
export declare function loadMetroBabelTransformer(path: string): BabelTransformer;
|
|
23
|
+
export declare function compileWithUserBabel(transformerPath: string, args: MetroBabelTransformArgs): Promise<CompiledMetroModule>;
|
|
24
|
+
export declare function userBabelCacheKey(transformerPath: string): string;
|
|
25
|
+
export declare function transformerDirectory(transformerPath: string): string;
|
|
26
|
+
export {};
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=babel.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mappings": "AAIA,iBAAiB,wBAAwB;CACvC;CACA;CACA,SAAS;CACT;;AAGF,iBAAiB,0BAA0B;CACzC,KAAK;CACL,WAAW;CACX;;;AAIF,iBAAiB,oBAAoB;CACnC;CACA,KAAK;CACL,QAAQ;;KAGL,mBAAmB;CACtB,UACE,MAAM,0BACL,4BAA4B,QAAQ;CACvC;;AAWF,OAAO,iBAAS,0BAA0B,eAAe;AAIzD,OAAO,iBAAe,qBACpB,yBACA,MAAM,0BACL,QAAQ;AA6BX,OAAO,iBAAS,kBAAkB;AASlC,OAAO,iBAAS,qBAAqB",
|
|
3
|
+
"names": [],
|
|
4
|
+
"sources": [
|
|
5
|
+
"src/babel.ts"
|
|
6
|
+
],
|
|
7
|
+
"version": 3,
|
|
8
|
+
"sourcesContent": [
|
|
9
|
+
"import { createHash } from 'node:crypto'\nimport { createRequire } from 'node:module'\nimport { dirname } from 'node:path'\n\nexport interface MetroBabelTransformArgs {\n filename: string\n src: string\n options: Record<string, any>\n plugins: unknown[]\n}\n\nexport interface MetroBabelTransformResult {\n ast: Record<string, any>\n metadata?: Record<string, any>\n functionMap?: unknown\n [key: string]: unknown\n}\n\nexport interface CompiledMetroModule {\n code: string\n map: Record<string, any>\n result: MetroBabelTransformResult\n}\n\ntype BabelTransformer = {\n transform(\n args: MetroBabelTransformArgs\n ): MetroBabelTransformResult | Promise<MetroBabelTransformResult>\n getCacheKey?(): string\n}\n\nfunction asTransformer(module: any, path: string): BabelTransformer {\n const transformer = module?.default?.transform ? module.default : module\n if (!transformer || typeof transformer.transform !== 'function') {\n throw new Error(`Metro Babel transformer ${path} has no transform function`)\n }\n return transformer\n}\n\nexport function loadMetroBabelTransformer(path: string): BabelTransformer {\n return asTransformer(createRequire(path)(path), path)\n}\n\nexport async function compileWithUserBabel(\n transformerPath: string,\n args: MetroBabelTransformArgs\n): Promise<CompiledMetroModule> {\n const transformer = loadMetroBabelTransformer(transformerPath)\n const result = await transformer.transform(args)\n if (!result?.ast) {\n throw new Error(`Metro Babel transformer ${transformerPath} returned no AST`)\n }\n const requireFromTransformer = createRequire(transformerPath)\n const generatorModule = requireFromTransformer('@babel/generator')\n const generate = generatorModule.default ?? generatorModule\n const generated = generate(\n result.ast,\n {\n comments: true,\n compact: false,\n retainLines: true,\n sourceFileName: args.filename,\n sourceMaps: true,\n },\n args.src\n )\n if (!generated || typeof generated.code !== 'string') {\n throw new Error(`Babel generator for ${transformerPath} returned no code`)\n }\n if (!generated.map) {\n throw new Error(`Babel generator for ${transformerPath} returned no source map`)\n }\n return { code: generated.code, map: generated.map, result }\n}\n\nexport function userBabelCacheKey(transformerPath: string): string {\n const transformer = loadMetroBabelTransformer(transformerPath)\n return createHash('sha256')\n .update(transformerPath)\n .update('\\0')\n .update(transformer.getCacheKey?.() ?? '')\n .digest('hex')\n}\n\nexport function transformerDirectory(transformerPath: string): string {\n return dirname(transformerPath)\n}\n"
|
|
10
|
+
]
|
|
11
|
+
}
|