@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,346 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+
5
+ import {
6
+ LOWERED_MODULE_PLAN_VERSION,
7
+ contentHash,
8
+ stableStringify,
9
+ tamaguiCacheRoot,
10
+ } from '@tamagui/compiler-core'
11
+ import type { LoweredModulePlan } from '@tamagui/compiler-core'
12
+
13
+ import { metroDiagnostic, type MetroCompilerDiagnostic } from './diagnostics'
14
+
15
+ export const METRO_COMPILER_CACHE_VERSION = 6
16
+
17
+ export interface MetroCompilerCacheEntry {
18
+ schemaVersion: typeof METRO_COMPILER_CACHE_VERSION
19
+ moduleId: string
20
+ /** Hash of the raw on-disk module source the plan was generated from. */
21
+ sourceHash: string
22
+ plan: LoweredModulePlan
23
+ diagnostics: MetroCompilerDiagnostic[]
24
+ }
25
+
26
+ interface MetroCompilerCacheDescriptor {
27
+ blobHash: string
28
+ sourceHash: string
29
+ }
30
+
31
+ interface MetroCompilerCacheManifest {
32
+ schemaVersion: typeof METRO_COMPILER_CACHE_VERSION
33
+ generation: string
34
+ optionsHash: string
35
+ platform: string | null
36
+ entries: Record<string, MetroCompilerCacheDescriptor>
37
+ }
38
+
39
+ export interface MetroCompilerCacheValidation {
40
+ valid: boolean
41
+ diagnostics: MetroCompilerDiagnostic[]
42
+ generation: string | null
43
+ moduleIds: string[]
44
+ /** Raw module source hash by module id, for host freshness checks. */
45
+ sourceHashes: Record<string, string>
46
+ optionsHash: string | null
47
+ }
48
+
49
+ /**
50
+ * The zero build's CSS side effects, persisted beside the plan cache.
51
+ *
52
+ * A published plan generation and its artifact contents are the same fact
53
+ * observed twice: the scan produces both. Persisting only the plans means a
54
+ * warm build reuses them while emitting an artifact missing every rule it never
55
+ * collected, and still derives TAMAGUI_DID_OUTPUT_CSS from it. This is what lets
56
+ * the warm path skip the scan without that divergence.
57
+ */
58
+ export interface MetroZeroCSSSidecar {
59
+ schemaVersion: typeof METRO_COMPILER_CACHE_VERSION
60
+ generation: string
61
+ configCSS: string
62
+ /** Per-module compiler atomic CSS, by resolved module id. */
63
+ zeroModuleCSS: Record<string, string>
64
+ /** Theme-bridge class rules, by bridge id. */
65
+ bridgeCSS: Record<string, string>
66
+ /** The bridge manifest, by island id. */
67
+ bridges: Record<string, unknown[]>
68
+ }
69
+
70
+ export class MetroCompilerCacheError extends Error {
71
+ constructor(readonly diagnostic: MetroCompilerDiagnostic) {
72
+ super(diagnostic.message)
73
+ this.name = 'MetroCompilerCacheError'
74
+ }
75
+ }
76
+
77
+ function compareCodeUnits(left: string, right: string): number {
78
+ return left < right ? -1 : left > right ? 1 : 0
79
+ }
80
+
81
+ function stableEntries<T>(record: Record<string, T>): [string, T][] {
82
+ return Object.entries(record).sort(([left], [right]) => compareCodeUnits(left, right))
83
+ }
84
+
85
+ function cacheCorrupt(message: string, moduleId?: string): MetroCompilerCacheError {
86
+ return new MetroCompilerCacheError(
87
+ metroDiagnostic('metro/cache-corrupt', message, { moduleId })
88
+ )
89
+ }
90
+
91
+ function parseJson<T>(source: string, description: string, moduleId?: string): T {
92
+ try {
93
+ return JSON.parse(source) as T
94
+ } catch (error) {
95
+ throw cacheCorrupt(
96
+ `${description} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
97
+ moduleId
98
+ )
99
+ }
100
+ }
101
+
102
+ export function defaultMetroCompilerCacheRoot(projectRoot: string): string {
103
+ return join(tamaguiCacheRoot(projectRoot), 'metro-compiler')
104
+ }
105
+
106
+ /**
107
+ * Filesystem handoff shared by the Metro main process and isolated transform workers.
108
+ * Immutable blobs are content addressed; a single manifest rename publishes a generation.
109
+ */
110
+ export class MetroCompilerCache {
111
+ readonly #blobsDirectory: string
112
+ readonly #manifestPath: string
113
+
114
+ constructor(readonly root: string) {
115
+ this.#blobsDirectory = join(root, `v${METRO_COMPILER_CACHE_VERSION}`, 'blobs')
116
+ this.#manifestPath = join(root, `v${METRO_COMPILER_CACHE_VERSION}`, 'manifest.json')
117
+ }
118
+
119
+ async publish(
120
+ platform: string | null,
121
+ entries: readonly MetroCompilerCacheEntry[],
122
+ optionsHash: string
123
+ ): Promise<string> {
124
+ await mkdir(this.#blobsDirectory, { recursive: true })
125
+ const descriptors: Record<string, MetroCompilerCacheDescriptor> = {}
126
+
127
+ for (const entry of [...entries].sort((left, right) =>
128
+ compareCodeUnits(left.moduleId, right.moduleId)
129
+ )) {
130
+ if (entry.schemaVersion !== METRO_COMPILER_CACHE_VERSION) {
131
+ throw new Error(`Cannot publish cache schema ${entry.schemaVersion}`)
132
+ }
133
+ const serialized = `${stableStringify(entry)}\n`
134
+ const blobHash = contentHash(serialized)
135
+ const blobPath = join(this.#blobsDirectory, `${blobHash}.json`)
136
+ try {
137
+ await writeFile(blobPath, serialized, { encoding: 'utf8', flag: 'wx' })
138
+ } catch (error) {
139
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
140
+ const existing = await readFile(blobPath, 'utf8')
141
+ if (contentHash(existing) !== blobHash) {
142
+ const temporaryBlobPath = `${blobPath}.${process.pid}-${randomBytes(6).toString('hex')}.tmp`
143
+ await writeFile(temporaryBlobPath, serialized, 'utf8')
144
+ await rename(temporaryBlobPath, blobPath)
145
+ }
146
+ }
147
+ descriptors[entry.moduleId] = {
148
+ blobHash,
149
+ sourceHash: entry.sourceHash,
150
+ }
151
+ }
152
+
153
+ const generation = contentHash(stableStringify(descriptors))
154
+ const manifest: MetroCompilerCacheManifest = {
155
+ schemaVersion: METRO_COMPILER_CACHE_VERSION,
156
+ generation,
157
+ optionsHash,
158
+ platform,
159
+ entries: descriptors,
160
+ }
161
+ const manifestDirectory = join(this.root, `v${METRO_COMPILER_CACHE_VERSION}`)
162
+ const temporaryPath = join(
163
+ manifestDirectory,
164
+ `.manifest-${process.pid}-${randomBytes(6).toString('hex')}.json`
165
+ )
166
+ await writeFile(temporaryPath, `${stableStringify(manifest)}\n`, 'utf8')
167
+ await rename(temporaryPath, this.#manifestPath)
168
+ return generation
169
+ }
170
+
171
+ async read(
172
+ moduleId: string,
173
+ rawSource: string,
174
+ onMiss?: (reason: 'no-entry' | 'source-hash-mismatch', detail?: string) => void
175
+ ): Promise<MetroCompilerCacheEntry | null> {
176
+ const manifest = await this.#readManifest()
177
+ if (!manifest) return null
178
+ const descriptor = manifest.entries[moduleId]
179
+ if (!descriptor) {
180
+ onMiss?.('no-entry')
181
+ return null
182
+ }
183
+ const sourceHash = contentHash(rawSource)
184
+ if (sourceHash !== descriptor.sourceHash) {
185
+ onMiss?.(
186
+ 'source-hash-mismatch',
187
+ `worker ${sourceHash.slice(0, 12)} vs plan ${descriptor.sourceHash.slice(0, 12)}`
188
+ )
189
+ return null
190
+ }
191
+ return await this.#readBlob(moduleId, descriptor)
192
+ }
193
+
194
+ async validate(): Promise<MetroCompilerCacheValidation> {
195
+ const diagnostics: MetroCompilerDiagnostic[] = []
196
+ try {
197
+ const manifest = await this.#readManifest()
198
+ if (!manifest) {
199
+ return {
200
+ valid: false,
201
+ diagnostics,
202
+ generation: null,
203
+ moduleIds: [],
204
+ sourceHashes: {},
205
+ optionsHash: null,
206
+ }
207
+ }
208
+ const sourceHashes: Record<string, string> = {}
209
+ for (const [moduleId, descriptor] of stableEntries(manifest.entries)) {
210
+ await this.#readBlob(moduleId, descriptor)
211
+ sourceHashes[moduleId] = descriptor.sourceHash
212
+ }
213
+ return {
214
+ valid: true,
215
+ diagnostics,
216
+ generation: manifest.generation,
217
+ moduleIds: Object.keys(manifest.entries).sort(compareCodeUnits),
218
+ sourceHashes,
219
+ optionsHash: manifest.optionsHash,
220
+ }
221
+ } catch (error) {
222
+ if (error instanceof MetroCompilerCacheError) {
223
+ diagnostics.push(error.diagnostic)
224
+ return {
225
+ valid: false,
226
+ diagnostics,
227
+ generation: null,
228
+ moduleIds: [],
229
+ sourceHashes: {},
230
+ optionsHash: null,
231
+ }
232
+ }
233
+ throw error
234
+ }
235
+ }
236
+
237
+ async discardManifest(): Promise<void> {
238
+ await rm(this.#manifestPath, { force: true })
239
+ }
240
+
241
+ #zeroSidecarPath(generation: string): string {
242
+ return join(
243
+ this.root,
244
+ `v${METRO_COMPILER_CACHE_VERSION}`,
245
+ `zero-${generation.slice(0, 32)}.json`
246
+ )
247
+ }
248
+
249
+ async publishZeroCSS(sidecar: MetroZeroCSSSidecar): Promise<void> {
250
+ await mkdir(join(this.root, `v${METRO_COMPILER_CACHE_VERSION}`), { recursive: true })
251
+ const file = this.#zeroSidecarPath(sidecar.generation)
252
+ const temporaryPath = `${file}.${process.pid}-${randomBytes(6).toString('hex')}.tmp`
253
+ await writeFile(temporaryPath, `${stableStringify(sidecar)}\n`, 'utf8')
254
+ await rename(temporaryPath, file)
255
+ }
256
+
257
+ /** Null whenever the sidecar is absent or does not describe this generation. */
258
+ async readZeroCSS(generation: string): Promise<MetroZeroCSSSidecar | null> {
259
+ let source: string
260
+ try {
261
+ source = await readFile(this.#zeroSidecarPath(generation), 'utf8')
262
+ } catch (error) {
263
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
264
+ throw error
265
+ }
266
+ const sidecar = parseJson<MetroZeroCSSSidecar>(source, 'Zero CSS sidecar')
267
+ if (
268
+ sidecar.schemaVersion !== METRO_COMPILER_CACHE_VERSION ||
269
+ sidecar.generation !== generation ||
270
+ typeof sidecar.configCSS !== 'string' ||
271
+ !sidecar.zeroModuleCSS ||
272
+ typeof sidecar.zeroModuleCSS !== 'object' ||
273
+ !sidecar.bridgeCSS ||
274
+ typeof sidecar.bridgeCSS !== 'object' ||
275
+ !sidecar.bridges ||
276
+ typeof sidecar.bridges !== 'object'
277
+ ) {
278
+ return null
279
+ }
280
+ return sidecar
281
+ }
282
+
283
+ async #readManifest(): Promise<MetroCompilerCacheManifest | null> {
284
+ let source: string
285
+ try {
286
+ source = await readFile(this.#manifestPath, 'utf8')
287
+ } catch (error) {
288
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
289
+ throw error
290
+ }
291
+ const manifest = parseJson<MetroCompilerCacheManifest>(source, 'Cache manifest')
292
+ if (
293
+ manifest.schemaVersion !== METRO_COMPILER_CACHE_VERSION ||
294
+ typeof manifest.generation !== 'string' ||
295
+ typeof manifest.optionsHash !== 'string' ||
296
+ !manifest.entries ||
297
+ typeof manifest.entries !== 'object'
298
+ ) {
299
+ throw cacheCorrupt('Cache manifest has an unsupported schema')
300
+ }
301
+ return manifest
302
+ }
303
+
304
+ async #readBlob(
305
+ moduleId: string,
306
+ descriptor: MetroCompilerCacheDescriptor
307
+ ): Promise<MetroCompilerCacheEntry> {
308
+ const blobPath = join(this.#blobsDirectory, `${descriptor.blobHash}.json`)
309
+ let source: string
310
+ try {
311
+ source = await readFile(blobPath, 'utf8')
312
+ } catch (error) {
313
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
314
+ throw cacheCorrupt(`Cache blob ${descriptor.blobHash} is missing`, moduleId)
315
+ }
316
+ throw error
317
+ }
318
+ if (contentHash(source) !== descriptor.blobHash) {
319
+ throw cacheCorrupt(`Cache blob ${descriptor.blobHash} failed its hash`, moduleId)
320
+ }
321
+ const entry = parseJson<MetroCompilerCacheEntry>(
322
+ source,
323
+ `Cache blob ${descriptor.blobHash}`,
324
+ moduleId
325
+ )
326
+ if (
327
+ entry.schemaVersion !== METRO_COMPILER_CACHE_VERSION ||
328
+ entry.moduleId !== moduleId ||
329
+ typeof entry.sourceHash !== 'string' ||
330
+ entry.sourceHash !== descriptor.sourceHash ||
331
+ !entry.plan ||
332
+ entry.plan.version !== LOWERED_MODULE_PLAN_VERSION ||
333
+ entry.plan.id !== moduleId ||
334
+ entry.plan.sourceHash !== entry.sourceHash ||
335
+ !Array.isArray(entry.plan.edits) ||
336
+ !Array.isArray(entry.plan.diagnostics) ||
337
+ !Array.isArray(entry.diagnostics)
338
+ ) {
339
+ throw cacheCorrupt(
340
+ `Cache blob ${descriptor.blobHash} has invalid contents`,
341
+ moduleId
342
+ )
343
+ }
344
+ return entry
345
+ }
346
+ }
@@ -0,0 +1,47 @@
1
+ import { isAbsolute, relative } from 'node:path'
2
+
3
+ import type { SourceSpan } from '@tamagui/compiler-core'
4
+
5
+ export type MetroCompilerDiagnosticCode =
6
+ | 'metro/cache-corrupt'
7
+ | 'metro/cache-stale'
8
+ | 'metro/no-linked-components'
9
+ | 'metro/plan-miss'
10
+ | 'metro/resolve-failed'
11
+ | 'metro/transform-failed'
12
+
13
+ export interface MetroCompilerDiagnostic {
14
+ code: MetroCompilerDiagnosticCode
15
+ message: string
16
+ moduleId?: string
17
+ dependency?: string
18
+ span?: SourceSpan
19
+ line?: number
20
+ column?: number
21
+ component?: string
22
+ }
23
+
24
+ export function metroDiagnostic(
25
+ code: MetroCompilerDiagnosticCode,
26
+ message: string,
27
+ details: Omit<MetroCompilerDiagnostic, 'code' | 'message'> = {}
28
+ ): MetroCompilerDiagnostic {
29
+ return { code, message, ...details }
30
+ }
31
+
32
+ export function formatMetroCompilerDiagnostic(
33
+ diagnostic: MetroCompilerDiagnostic,
34
+ projectRoot: string
35
+ ): string {
36
+ const sourceId = diagnostic.span?.id ?? diagnostic.moduleId
37
+ const file = sourceId
38
+ ? isAbsolute(sourceId)
39
+ ? relative(projectRoot, sourceId) || '.'
40
+ : sourceId
41
+ : null
42
+ const location =
43
+ file && diagnostic.line != null && diagnostic.column != null
44
+ ? `${file}:${diagnostic.line}:${diagnostic.column}: `
45
+ : ''
46
+ return `[@tamagui/metro-plugin] ${location}${diagnostic.code}: ${diagnostic.message}`
47
+ }