@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.
Files changed (72) hide show
  1. package/README.md +12 -0
  2. package/dist/cjs/babel.cjs +77 -0
  3. package/dist/cjs/compilerCache.cjs +237 -0
  4. package/dist/cjs/diagnostics.cjs +41 -0
  5. package/dist/cjs/frontend.cjs +870 -0
  6. package/dist/cjs/index.cjs +102 -0
  7. package/dist/cjs/lowering.cjs +109 -0
  8. package/dist/cjs/metroResolver.cjs +197 -0
  9. package/dist/cjs/transformOptions.cjs +35 -0
  10. package/dist/cjs/transformer.cjs +142 -0
  11. package/dist/cjs/zeroRuntime.cjs +140 -0
  12. package/dist/cjs/zeroSerializer.cjs +150 -0
  13. package/dist/esm/babel.mjs +52 -0
  14. package/dist/esm/babel.mjs.map +1 -0
  15. package/dist/esm/compilerCache.mjs +212 -0
  16. package/dist/esm/compilerCache.mjs.map +1 -0
  17. package/dist/esm/diagnostics.mjs +18 -0
  18. package/dist/esm/diagnostics.mjs.map +1 -0
  19. package/dist/esm/frontend.mjs +839 -0
  20. package/dist/esm/frontend.mjs.map +1 -0
  21. package/dist/esm/index.mjs +64 -22
  22. package/dist/esm/index.mjs.map +1 -1
  23. package/dist/esm/lowering.mjs +89 -0
  24. package/dist/esm/lowering.mjs.map +1 -0
  25. package/dist/esm/metroResolver.mjs +173 -0
  26. package/dist/esm/metroResolver.mjs.map +1 -0
  27. package/dist/esm/transformOptions.mjs +14 -0
  28. package/dist/esm/transformOptions.mjs.map +1 -0
  29. package/dist/esm/transformer.mjs +119 -0
  30. package/dist/esm/transformer.mjs.map +1 -0
  31. package/dist/esm/zeroRuntime.mjs +105 -0
  32. package/dist/esm/zeroRuntime.mjs.map +1 -0
  33. package/dist/esm/zeroSerializer.mjs +123 -0
  34. package/dist/esm/zeroSerializer.mjs.map +1 -0
  35. package/package.json +33 -5
  36. package/src/babel.ts +87 -0
  37. package/src/compilerCache.ts +346 -0
  38. package/src/diagnostics.ts +47 -0
  39. package/src/frontend.ts +1178 -0
  40. package/src/index.ts +117 -14
  41. package/src/lowering.ts +136 -0
  42. package/src/metroResolver.ts +209 -0
  43. package/src/transformOptions.ts +36 -0
  44. package/src/transformer.ts +210 -0
  45. package/src/zeroRuntime.ts +212 -0
  46. package/src/zeroSerializer.ts +175 -0
  47. package/types/babel.d.ts +28 -0
  48. package/types/babel.d.ts.map +11 -0
  49. package/types/compilerCache.d.ts +63 -0
  50. package/types/compilerCache.d.ts.map +11 -0
  51. package/types/diagnostics.d.ts +16 -0
  52. package/types/diagnostics.d.ts.map +11 -0
  53. package/types/frontend.d.ts +73 -0
  54. package/types/frontend.d.ts.map +11 -0
  55. package/types/index.d.ts +49 -32
  56. package/types/index.d.ts.map +11 -1
  57. package/types/lowering.d.ts +20 -0
  58. package/types/lowering.d.ts.map +11 -0
  59. package/types/metroResolver.d.ts +21 -0
  60. package/types/metroResolver.d.ts.map +11 -0
  61. package/types/transformOptions.d.ts +13 -0
  62. package/types/transformOptions.d.ts.map +11 -0
  63. package/types/transformer.d.ts +26 -0
  64. package/types/transformer.d.ts.map +11 -0
  65. package/types/zeroRuntime.d.ts +75 -0
  66. package/types/zeroRuntime.d.ts.map +11 -0
  67. package/types/zeroSerializer.d.ts +6 -0
  68. package/types/zeroSerializer.d.ts.map +11 -0
  69. package/dist/cjs/index.js +0 -45
  70. package/dist/cjs/index.js.map +0 -6
  71. package/dist/esm/index.js +0 -25
  72. 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
+ }
@@ -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
+ }