@tamagui/static 2.0.0-rc.3 → 2.0.0-rc.31

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 (141) hide show
  1. package/dist/checkDeps.cjs +164 -31
  2. package/dist/exports.cjs +3 -0
  3. package/dist/extractor/bundle.cjs +72 -35
  4. package/dist/extractor/bundleConfig.cjs +219 -35
  5. package/dist/extractor/createExtractor.cjs +170 -28
  6. package/dist/extractor/detectModuleFormat.cjs +49 -0
  7. package/dist/extractor/esbuildTsconfigPaths.cjs +3 -1
  8. package/dist/extractor/extractToClassNames.cjs +7 -5
  9. package/dist/extractor/extractToNative.cjs +7 -8
  10. package/dist/extractor/loadTamagui.cjs +1 -1
  11. package/dist/getPragmaOptions.cjs +7 -3
  12. package/dist/registerRequire.cjs +23 -14
  13. package/package.json +26 -22
  14. package/src/checkDeps.ts +305 -68
  15. package/src/exports.ts +1 -0
  16. package/src/extractor/bundle.ts +140 -37
  17. package/src/extractor/bundleConfig.ts +435 -61
  18. package/src/extractor/createExtractor.ts +261 -41
  19. package/src/extractor/detectModuleFormat.ts +42 -0
  20. package/src/extractor/esbuildTsconfigPaths.ts +6 -1
  21. package/src/extractor/extractToClassNames.ts +15 -9
  22. package/src/extractor/extractToNative.ts +32 -25
  23. package/src/extractor/loadTamagui.ts +2 -2
  24. package/src/getPragmaOptions.ts +6 -1
  25. package/src/registerRequire.ts +40 -8
  26. package/types/checkDeps.d.ts.map +1 -1
  27. package/types/exports.d.ts +1 -0
  28. package/types/exports.d.ts.map +1 -1
  29. package/types/extractor/bundle.d.ts +83 -1
  30. package/types/extractor/bundle.d.ts.map +1 -1
  31. package/types/extractor/bundleConfig.d.ts +15 -2
  32. package/types/extractor/bundleConfig.d.ts.map +1 -1
  33. package/types/extractor/createExtractor.d.ts.map +1 -1
  34. package/types/extractor/detectModuleFormat.d.ts +5 -0
  35. package/types/extractor/detectModuleFormat.d.ts.map +1 -0
  36. package/types/extractor/esbuildTsconfigPaths.d.ts +8 -0
  37. package/types/extractor/esbuildTsconfigPaths.d.ts.map +1 -1
  38. package/types/extractor/extractToClassNames.d.ts.map +1 -1
  39. package/types/extractor/extractToNative.d.ts.map +1 -1
  40. package/types/getPragmaOptions.d.ts.map +1 -1
  41. package/types/registerRequire.d.ts.map +1 -1
  42. package/dist/check-dep-versions.js +0 -389
  43. package/dist/check-dep-versions.js.map +0 -6
  44. package/dist/checkDeps.js +0 -60
  45. package/dist/checkDeps.js.map +0 -6
  46. package/dist/constants.js +0 -34
  47. package/dist/constants.js.map +0 -6
  48. package/dist/exports.js +0 -34
  49. package/dist/exports.js.map +0 -6
  50. package/dist/extractor/accessSafe.js +0 -47
  51. package/dist/extractor/accessSafe.js.map +0 -6
  52. package/dist/extractor/babelParse.js +0 -59
  53. package/dist/extractor/babelParse.js.map +0 -6
  54. package/dist/extractor/buildClassName.js +0 -72
  55. package/dist/extractor/buildClassName.js.map +0 -6
  56. package/dist/extractor/bundle.js +0 -135
  57. package/dist/extractor/bundle.js.map +0 -6
  58. package/dist/extractor/bundleConfig.js +0 -352
  59. package/dist/extractor/bundleConfig.js.map +0 -6
  60. package/dist/extractor/concatClassName.js +0 -69
  61. package/dist/extractor/concatClassName.js.map +0 -6
  62. package/dist/extractor/createEvaluator.js +0 -66
  63. package/dist/extractor/createEvaluator.js.map +0 -6
  64. package/dist/extractor/createExtractor.js +0 -1215
  65. package/dist/extractor/createExtractor.js.map +0 -6
  66. package/dist/extractor/createLogger.js +0 -32
  67. package/dist/extractor/createLogger.js.map +0 -6
  68. package/dist/extractor/ensureImportingConcat.js +0 -50
  69. package/dist/extractor/ensureImportingConcat.js.map +0 -6
  70. package/dist/extractor/errors.js +0 -22
  71. package/dist/extractor/errors.js.map +0 -6
  72. package/dist/extractor/esbuildAliasPlugin.js +0 -36
  73. package/dist/extractor/esbuildAliasPlugin.js.map +0 -6
  74. package/dist/extractor/esbuildTsconfigPaths.js +0 -79
  75. package/dist/extractor/esbuildTsconfigPaths.js.map +0 -6
  76. package/dist/extractor/evaluateAstNode.js +0 -99
  77. package/dist/extractor/evaluateAstNode.js.map +0 -6
  78. package/dist/extractor/extractHelpers.js +0 -108
  79. package/dist/extractor/extractHelpers.js.map +0 -6
  80. package/dist/extractor/extractMediaStyle.js +0 -123
  81. package/dist/extractor/extractMediaStyle.js.map +0 -6
  82. package/dist/extractor/extractToClassNames.js +0 -351
  83. package/dist/extractor/extractToClassNames.js.map +0 -6
  84. package/dist/extractor/extractToNative.js +0 -286
  85. package/dist/extractor/extractToNative.js.map +0 -6
  86. package/dist/extractor/findTopmostFunction.js +0 -32
  87. package/dist/extractor/findTopmostFunction.js.map +0 -6
  88. package/dist/extractor/generatedUid.js +0 -42
  89. package/dist/extractor/generatedUid.js.map +0 -6
  90. package/dist/extractor/getPrefixLogs.js +0 -24
  91. package/dist/extractor/getPrefixLogs.js.map +0 -6
  92. package/dist/extractor/getPropValueFromAttributes.js +0 -65
  93. package/dist/extractor/getPropValueFromAttributes.js.map +0 -6
  94. package/dist/extractor/getSourceModule.js +0 -62
  95. package/dist/extractor/getSourceModule.js.map +0 -6
  96. package/dist/extractor/getStaticBindingsForScope.js +0 -145
  97. package/dist/extractor/getStaticBindingsForScope.js.map +0 -6
  98. package/dist/extractor/getTamaguiConfigPathFromOptionsConfig.js +0 -32
  99. package/dist/extractor/getTamaguiConfigPathFromOptionsConfig.js.map +0 -6
  100. package/dist/extractor/hoistClassNames.js +0 -63
  101. package/dist/extractor/hoistClassNames.js.map +0 -6
  102. package/dist/extractor/literalToAst.js +0 -90
  103. package/dist/extractor/literalToAst.js.map +0 -6
  104. package/dist/extractor/loadFile.js +0 -14
  105. package/dist/extractor/loadFile.js.map +0 -6
  106. package/dist/extractor/loadTamagui.js +0 -306
  107. package/dist/extractor/loadTamagui.js.map +0 -6
  108. package/dist/extractor/logLines.js +0 -30
  109. package/dist/extractor/logLines.js.map +0 -6
  110. package/dist/extractor/normalizeTernaries.js +0 -61
  111. package/dist/extractor/normalizeTernaries.js.map +0 -6
  112. package/dist/extractor/propsToFontFamilyCache.js +0 -33
  113. package/dist/extractor/propsToFontFamilyCache.js.map +0 -6
  114. package/dist/extractor/regenerateConfig.js +0 -123
  115. package/dist/extractor/regenerateConfig.js.map +0 -6
  116. package/dist/extractor/removeUnusedHooks.js +0 -72
  117. package/dist/extractor/removeUnusedHooks.js.map +0 -6
  118. package/dist/extractor/timer.js +0 -38
  119. package/dist/extractor/timer.js.map +0 -6
  120. package/dist/extractor/validHTMLAttributes.js +0 -72
  121. package/dist/extractor/validHTMLAttributes.js.map +0 -6
  122. package/dist/extractor/watchTamaguiConfig.js +0 -57
  123. package/dist/extractor/watchTamaguiConfig.js.map +0 -6
  124. package/dist/getPragmaOptions.js +0 -46
  125. package/dist/getPragmaOptions.js.map +0 -6
  126. package/dist/helpers/memoize.js +0 -33
  127. package/dist/helpers/memoize.js.map +0 -6
  128. package/dist/helpers/requireTamaguiCore.js +0 -30
  129. package/dist/helpers/requireTamaguiCore.js.map +0 -6
  130. package/dist/index.js +0 -30
  131. package/dist/index.js.map +0 -6
  132. package/dist/registerRequire.js +0 -100
  133. package/dist/registerRequire.js.map +0 -6
  134. package/dist/server.js +0 -58
  135. package/dist/server.js.map +0 -6
  136. package/dist/setup.js +0 -1
  137. package/dist/setup.js.map +0 -6
  138. package/dist/types.js +0 -14
  139. package/dist/types.js.map +0 -6
  140. package/dist/worker.js +0 -72
  141. package/dist/worker.js.map +0 -6
package/src/checkDeps.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync } from 'node:fs'
2
- import { join } from 'node:path'
1
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
2
+ import { join, relative } from 'node:path'
3
3
  import { CDVC } from './check-dep-versions'
4
4
 
5
5
  export enum DEPENDENCY_TYPE {
@@ -21,113 +21,350 @@ export type Options = {
21
21
  ignorePathPattern?: readonly string[]
22
22
  }
23
23
 
24
- type PackageJson = {
25
- dependencies?: Record<string, string>
26
- devDependencies?: Record<string, string>
27
- optionalDependencies?: Record<string, string>
28
- }
24
+ // critical packages that must not be duplicated at runtime
25
+ const CRITICAL_PACKAGES = ['@tamagui/web', '@tamagui/core', 'tamagui']
29
26
 
30
27
  /**
31
- * Checks if @tamagui/* packages within a single package.json have mismatched versions.
32
- * Returns a summary of mismatches or empty string if all versions match.
28
+ * Walks node_modules to find duplicate physical copies of critical tamagui packages.
29
+ * Detects nested node_modules that would cause multiple runtime instances.
33
30
  */
34
- function checkTamaguiPackageVersions(root: string): string {
35
- const packageJsonPath = join(root, 'package.json')
36
- if (!existsSync(packageJsonPath)) {
37
- return ''
31
+ function checkDuplicateInstalls(root: string): string {
32
+ const nodeModules = join(root, 'node_modules')
33
+ if (!existsSync(nodeModules)) return ''
34
+
35
+ const duplicates = new Map<string, string[]>()
36
+
37
+ for (const pkg of CRITICAL_PACKAGES) {
38
+ const locations = findAllInstances(nodeModules, pkg)
39
+ if (locations.length > 1) {
40
+ // resolve symlinks to find truly distinct copies
41
+ const realPaths = new Set<string>()
42
+ const distinctLocations: string[] = []
43
+ for (const loc of locations) {
44
+ try {
45
+ const real = realpathSync(loc)
46
+ if (!realPaths.has(real)) {
47
+ realPaths.add(real)
48
+ distinctLocations.push(relative(root, loc))
49
+ }
50
+ } catch {
51
+ distinctLocations.push(relative(root, loc))
52
+ }
53
+ }
54
+ if (distinctLocations.length > 1) {
55
+ duplicates.set(pkg, distinctLocations)
56
+ }
57
+ }
38
58
  }
39
59
 
40
- const packageJson: PackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
60
+ if (duplicates.size === 0) return ''
41
61
 
42
- // Collect all @tamagui/* dependencies and their versions
43
- const tamaguiDeps: { name: string; version: string }[] = []
62
+ const lines: string[] = [
63
+ 'Found duplicate tamagui installations in node_modules:',
64
+ '',
65
+ 'This causes multiple runtime instances, which breaks theme/config detection.',
66
+ '',
67
+ ]
44
68
 
45
- const allDeps = {
46
- ...packageJson.dependencies,
47
- ...packageJson.devDependencies,
48
- ...packageJson.optionalDependencies,
69
+ for (const [pkg, locations] of duplicates) {
70
+ // read versions from each location
71
+ lines.push(` ${pkg}:`)
72
+ for (const loc of locations) {
73
+ const pkgJsonPath = join(root, loc, 'package.json')
74
+ let version = '?'
75
+ try {
76
+ version = JSON.parse(readFileSync(pkgJsonPath, 'utf8')).version
77
+ } catch {}
78
+ lines.push(` ${version} at ${loc}`)
79
+ }
80
+ lines.push('')
49
81
  }
50
82
 
51
- for (const [name, version] of Object.entries(allDeps)) {
52
- if (name === 'tamagui' || name.startsWith('@tamagui/')) {
53
- tamaguiDeps.push({ name, version })
54
- }
83
+ lines.push("Fix: run your package manager's dedupe command:")
84
+ lines.push(' bun install (bun auto-dedupes)')
85
+ lines.push(' npx yarn-deduplicate && yarn install')
86
+ lines.push(' npm dedupe')
87
+ lines.push('')
88
+ lines.push("If that doesn't help, delete node_modules and lockfile, then reinstall.")
89
+
90
+ return lines.join('\n')
91
+ }
92
+
93
+ /**
94
+ * Recursively find all instances of a package in node_modules.
95
+ * Handles both scoped (@tamagui/web) and unscoped (tamagui) packages.
96
+ */
97
+ function findAllInstances(
98
+ nodeModulesDir: string,
99
+ packageName: string,
100
+ found: string[] = [],
101
+ depth = 0
102
+ ): string[] {
103
+ // don't go too deep, typical hoisting issues show up within a few levels
104
+ if (depth > 4 || !existsSync(nodeModulesDir)) return found
105
+
106
+ const pkgDir = join(nodeModulesDir, ...packageName.split('/'))
107
+ if (existsSync(join(pkgDir, 'package.json'))) {
108
+ found.push(pkgDir)
55
109
  }
56
110
 
57
- if (tamaguiDeps.length <= 1) {
111
+ // scan nested node_modules inside direct children
112
+ try {
113
+ const entries = readdirSync(nodeModulesDir)
114
+ for (const entry of entries) {
115
+ if (entry.startsWith('.')) continue
116
+
117
+ if (entry.startsWith('@')) {
118
+ // scoped packages have another level
119
+ const scopeDir = join(nodeModulesDir, entry)
120
+ try {
121
+ const scopeEntries = readdirSync(scopeDir)
122
+ for (const scopeEntry of scopeEntries) {
123
+ const nested = join(scopeDir, scopeEntry, 'node_modules')
124
+ if (existsSync(nested)) {
125
+ findAllInstances(nested, packageName, found, depth + 1)
126
+ }
127
+ }
128
+ } catch {}
129
+ } else {
130
+ const nested = join(nodeModulesDir, entry, 'node_modules')
131
+ if (existsSync(nested)) {
132
+ findAllInstances(nested, packageName, found, depth + 1)
133
+ }
134
+ }
135
+ }
136
+ } catch {}
137
+
138
+ return found
139
+ }
140
+
141
+ /**
142
+ * Checks lockfile for multiple resolved versions of tamagui packages.
143
+ * Supports bun.lock, yarn.lock, and package-lock.json.
144
+ */
145
+ function checkLockfileDuplicates(root: string): string {
146
+ const bunLock = join(root, 'bun.lock')
147
+ const yarnLock = join(root, 'yarn.lock')
148
+ const npmLock = join(root, 'package-lock.json')
149
+
150
+ if (existsSync(bunLock)) return checkBunLockDuplicates(bunLock)
151
+ if (existsSync(yarnLock)) return checkYarnLockDuplicates(yarnLock)
152
+ if (existsSync(npmLock)) return checkNpmLockDuplicates(npmLock)
153
+
154
+ return ''
155
+ }
156
+
157
+ function checkBunLockDuplicates(lockPath: string): string {
158
+ try {
159
+ const content = readFileSync(lockPath, 'utf8')
160
+ const duplicates = new Map<string, Set<string>>()
161
+ const criticalSet = new Set(CRITICAL_PACKAGES)
162
+
163
+ // match patterns like "@tamagui/web@version" or "tamagui@version" in resolved entries
164
+ // bun.lock format: "package@version": ["resolved-url", ...]
165
+ const packagePattern = /["'](@tamagui\/[\w-]+|tamagui)@([^"'\s,]+)["']/g
166
+ let match: RegExpExecArray | null
167
+ while ((match = packagePattern.exec(content)) !== null) {
168
+ const name = match[1]
169
+ const version = match[2]
170
+ if (version.startsWith('workspace:')) continue
171
+ // only flag critical packages — leaf packages can safely differ
172
+ if (!criticalSet.has(name)) continue
173
+ if (!duplicates.has(name)) duplicates.set(name, new Set())
174
+ duplicates.get(name)!.add(version)
175
+ }
176
+
177
+ return formatLockfileDuplicates(duplicates, 'bun.lock')
178
+ } catch {
58
179
  return ''
59
180
  }
181
+ }
60
182
 
61
- // Normalize versions by removing prefixes like ^, ~, >=, etc.
62
- const normalizeVersion = (v: string): string => {
63
- // Handle workspace: protocol
64
- if (v.startsWith('workspace:')) {
65
- return v
183
+ function checkYarnLockDuplicates(lockPath: string): string {
184
+ try {
185
+ const content = readFileSync(lockPath, 'utf8')
186
+ const duplicates = new Map<string, Set<string>>()
187
+ const criticalSet = new Set(CRITICAL_PACKAGES)
188
+
189
+ // yarn.lock format:
190
+ // "@tamagui/web@^1.0.0":
191
+ // version "1.0.1"
192
+ const entryPattern = /^"?(@tamagui\/[\w-]+|tamagui)@[^":\n]+[":]?\s*$/gm
193
+ const versionPattern = /^\s+version\s+"([^"]+)"/gm
194
+
195
+ let entryMatch: RegExpExecArray | null
196
+ while ((entryMatch = entryPattern.exec(content)) !== null) {
197
+ const name = entryMatch[1]
198
+ if (!criticalSet.has(name)) continue
199
+ versionPattern.lastIndex = entryMatch.index
200
+ const verMatch = versionPattern.exec(content)
201
+ if (verMatch) {
202
+ if (!duplicates.has(name)) duplicates.set(name, new Set())
203
+ duplicates.get(name)!.add(verMatch[1])
204
+ }
66
205
  }
67
- return v.replace(/^[\^~>=<]+/, '')
206
+
207
+ return formatLockfileDuplicates(duplicates, 'yarn.lock')
208
+ } catch {
209
+ return ''
68
210
  }
211
+ }
212
+
213
+ function checkNpmLockDuplicates(lockPath: string): string {
214
+ try {
215
+ const lock = JSON.parse(readFileSync(lockPath, 'utf8'))
216
+ const duplicates = new Map<string, Set<string>>()
217
+ const criticalSet = new Set(CRITICAL_PACKAGES)
69
218
 
70
- // Group by normalized version
71
- const versionGroups = new Map<string, string[]>()
72
- for (const dep of tamaguiDeps) {
73
- const normalized = normalizeVersion(dep.version)
74
- if (!versionGroups.has(normalized)) {
75
- versionGroups.set(normalized, [])
219
+ // package-lock.json v2/v3 uses "packages" map with path keys
220
+ const packages = lock.packages || {}
221
+ for (const [path, info] of Object.entries(packages) as [string, any][]) {
222
+ if (!path) continue // skip root
223
+ const name = info.name || path.split('node_modules/').pop()
224
+ if (!name) continue
225
+ if (!criticalSet.has(name)) continue
226
+ const version = info.version
227
+ if (version) {
228
+ if (!duplicates.has(name)) duplicates.set(name, new Set())
229
+ duplicates.get(name)!.add(version)
230
+ }
76
231
  }
77
- versionGroups.get(normalized)!.push(`${dep.name}@${dep.version}`)
78
- }
79
232
 
80
- // If all packages have the same normalized version, no mismatch
81
- if (versionGroups.size <= 1) {
233
+ return formatLockfileDuplicates(duplicates, 'package-lock.json')
234
+ } catch {
82
235
  return ''
83
236
  }
237
+ }
84
238
 
85
- // Build mismatch summary
86
- const lines: string[] = [
87
- 'Found mismatched @tamagui/* package versions in package.json:',
88
- '',
89
- ]
90
-
91
- for (const [version, packages] of versionGroups) {
92
- lines.push(` ${version}:`)
93
- for (const pkg of packages.sort()) {
94
- lines.push(` - ${pkg}`)
239
+ function formatLockfileDuplicates(
240
+ duplicates: Map<string, Set<string>>,
241
+ lockfileName: string
242
+ ): string {
243
+ // filter to only packages with multiple versions
244
+ const multiVersion = new Map<string, string[]>()
245
+ for (const [name, versions] of duplicates) {
246
+ if (versions.size > 1) {
247
+ multiVersion.set(name, [...versions].sort())
95
248
  }
96
249
  }
97
250
 
251
+ if (multiVersion.size === 0) return ''
252
+
253
+ const lines: string[] = [`Found multiple resolved versions in ${lockfileName}:`, '']
254
+
255
+ for (const [name, versions] of multiVersion) {
256
+ lines.push(` ${name}: ${versions.join(', ')}`)
257
+ }
258
+
98
259
  lines.push('')
99
260
  lines.push(
100
- 'All @tamagui/* packages should use the same version to avoid runtime issues.'
261
+ 'Multiple versions cause duplicate runtime instances, breaking config/theme detection.'
101
262
  )
102
- lines.push('Run `npx tamagui upgrade` to sync all packages to the latest version.')
263
+ lines.push('Fix: ensure all tamagui packages use the same version range, then dedupe.')
103
264
 
104
265
  return lines.join('\n')
105
266
  }
106
267
 
268
+ /**
269
+ * Checks that a tamagui config file exists in common locations.
270
+ */
271
+ function checkConfigExists(root: string): string {
272
+ const configNames = [
273
+ 'tamagui.config.ts',
274
+ 'tamagui.config.tsx',
275
+ 'tamagui.config.js',
276
+ 'tamagui.config.mjs',
277
+ 'tamagui.config.cjs',
278
+ ]
279
+
280
+ const searchDirs = [root, join(root, 'src'), join(root, 'app'), join(root, 'config')]
281
+
282
+ for (const dir of searchDirs) {
283
+ for (const name of configNames) {
284
+ if (existsSync(join(dir, name))) {
285
+ return ''
286
+ }
287
+ }
288
+ }
289
+
290
+ // check if tamagui.build.ts references a config path
291
+ const buildConfigNames = [
292
+ 'tamagui.build.ts',
293
+ 'tamagui.build.js',
294
+ 'tamagui.build.mjs',
295
+ 'tamagui.build.cjs',
296
+ ]
297
+ for (const name of buildConfigNames) {
298
+ const buildPath = join(root, name)
299
+ if (existsSync(buildPath)) {
300
+ try {
301
+ const content = readFileSync(buildPath, 'utf8')
302
+ const match = content.match(/config\s*:\s*['"`]([^'"`]+)['"`]/)
303
+ if (match) {
304
+ const configPath = join(root, match[1])
305
+ if (existsSync(configPath)) return ''
306
+ }
307
+ } catch {}
308
+ }
309
+ }
310
+
311
+ // also check if there's a tamagui config referenced in package.json
312
+ const pkgJsonPath = join(root, 'package.json')
313
+ if (existsSync(pkgJsonPath)) {
314
+ try {
315
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
316
+ if (pkg.tamagui?.config) {
317
+ const configPath = join(root, pkg.tamagui.config)
318
+ if (existsSync(configPath)) return ''
319
+ }
320
+ } catch {}
321
+ }
322
+
323
+ // check if this is a monorepo root (has workspaces) - skip config check for root
324
+ if (existsSync(pkgJsonPath)) {
325
+ try {
326
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
327
+ if (pkg.workspaces) return ''
328
+ } catch {}
329
+ }
330
+
331
+ return [
332
+ 'No tamagui.config file found.',
333
+ '',
334
+ 'Tamagui requires a config file (e.g. tamagui.config.ts) that calls createTamagui().',
335
+ 'Without it, components will throw "Can\'t find Tamagui configuration" at runtime.',
336
+ '',
337
+ 'See: https://tamagui.dev/docs/core/configuration',
338
+ ].join('\n')
339
+ }
340
+
107
341
  export async function checkDeps(root: string) {
108
- // Check for @tamagui/* version mismatches within the same package.json
109
- const tamaguiMismatchSummary = checkTamaguiPackageVersions(root)
342
+ const issues: string[] = []
110
343
 
111
- // Check for dependency version mismatches across workspace packages
344
+ // 1. check for dependency version mismatches across workspace packages
112
345
  const workspaceMismatchSummary = new CDVC(root).toMismatchSummary()
346
+ if (workspaceMismatchSummary) issues.push(workspaceMismatchSummary)
347
+
348
+ // 2. check lockfile for duplicate resolved versions of critical packages
349
+ const lockfileSummary = checkLockfileDuplicates(root)
350
+ if (lockfileSummary) issues.push(lockfileSummary)
113
351
 
114
- const hasTamaguiMismatch = Boolean(tamaguiMismatchSummary)
115
- const hasWorkspaceMismatch = Boolean(workspaceMismatchSummary)
352
+ // 3. check for duplicate physical installations in node_modules
353
+ const duplicatesSummary = checkDuplicateInstalls(root)
354
+ if (duplicatesSummary) issues.push(duplicatesSummary)
116
355
 
117
- if (!hasTamaguiMismatch && !hasWorkspaceMismatch) {
356
+ // 4. check that a config file exists
357
+ const configSummary = checkConfigExists(root)
358
+ if (configSummary) issues.push(configSummary)
359
+
360
+ if (issues.length === 0) {
118
361
  console.info(`Tamagui dependencies look good ✅`)
119
362
  process.exit(0)
120
363
  }
121
364
 
122
- if (hasTamaguiMismatch) {
123
- console.error(tamaguiMismatchSummary)
124
- }
125
-
126
- if (hasWorkspaceMismatch) {
127
- if (hasTamaguiMismatch) {
128
- console.error('') // Add spacing between error sections
129
- }
130
- console.error(workspaceMismatchSummary)
365
+ for (let i = 0; i < issues.length; i++) {
366
+ if (i > 0) console.error('')
367
+ console.error(issues[i])
131
368
  }
132
369
 
133
370
  process.exit(1)
package/src/exports.ts CHANGED
@@ -11,4 +11,5 @@ export * from './extractor/loadTamagui'
11
11
  export * from './extractor/watchTamaguiConfig'
12
12
  export * from './extractor/createLogger'
13
13
  export * from './registerRequire'
14
+ export { detectModuleFormat, clearFormatCache } from './extractor/detectModuleFormat'
14
15
  export * from './getPragmaOptions'
@@ -1,7 +1,8 @@
1
- import { basename, dirname, join } from 'node:path'
1
+ import { readFileSync } from 'node:fs'
2
2
  import esbuild from 'esbuild'
3
3
  import * as FS from 'fs-extra'
4
4
  import type { TamaguiPlatform } from '../types'
5
+ import { detectModuleFormat } from './detectModuleFormat'
5
6
  import { esbuildAliasPlugin } from './esbuildAliasPlugin'
6
7
  import { resolveWebOrNativeSpecificEntry } from './loadTamagui'
7
8
  import { TsconfigPathsPlugin } from './esbuildTsconfigPaths'
@@ -63,11 +64,23 @@ function getESBuildConfig(
63
64
  ? entryPoints
64
65
  : entryPoints.map(resolveWebOrNativeSpecificEntry)
65
66
 
67
+ // detect format from entry points if not explicitly provided by caller
68
+ const detectedFormat = options.format || detectEntryFormat(resolvedEntryPoints[0])
69
+
66
70
  const res: esbuild.BuildOptions = {
67
71
  bundle: true,
68
72
  entryPoints: resolvedEntryPoints,
69
- format: 'cjs',
70
- target: 'node20',
73
+ format: detectedFormat,
74
+ // for ESM: prefer "module" field for resolution, add require() shim for bundled CJS deps
75
+ ...(detectedFormat === 'esm'
76
+ ? {
77
+ mainFields: ['module', 'main'],
78
+ banner: {
79
+ js: 'import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);',
80
+ },
81
+ }
82
+ : {}),
83
+ target: 'node24',
71
84
  jsx: 'transform',
72
85
  jsxFactory: 'react',
73
86
  allowOverwrite: true,
@@ -92,38 +105,126 @@ function getESBuildConfig(
92
105
  plugins: [
93
106
  TsconfigPathsPlugin(),
94
107
 
108
+ // handle ESM-only features that can't be used with CJS output
109
+ {
110
+ name: 'handle-esm-features',
111
+ setup(build) {
112
+ // only apply transforms for CJS output - ESM supports these natively
113
+ const isCjs =
114
+ build.initialOptions.format === 'cjs' || !build.initialOptions.format
115
+
116
+ build.onLoad({ filter: /\.(ts|tsx|js|jsx|mjs)$/ }, (args) => {
117
+ // skip if ESM output - import.meta and top-level await work natively
118
+ if (!isCjs) {
119
+ return null
120
+ }
121
+
122
+ // skip most node_modules
123
+ if (args.path.includes('node_modules') && !args.path.includes('@tamagui')) {
124
+ return null
125
+ }
126
+
127
+ let contents = readFileSync(args.path, 'utf8')
128
+ let modified = false
129
+
130
+ // transform import.meta.env -> process.env (Vite-style env vars)
131
+ if (contents.includes('import.meta.env')) {
132
+ contents = contents.replace(/import\.meta\.env/g, 'process.env')
133
+ modified = true
134
+ }
135
+
136
+ // transform import.meta.url -> "" (not needed for static extraction)
137
+ if (contents.includes('import.meta.url')) {
138
+ contents = contents.replace(/import\.meta\.url/g, '""')
139
+ modified = true
140
+ }
141
+
142
+ // transform import.meta.main -> false
143
+ if (contents.includes('import.meta.main')) {
144
+ contents = contents.replace(/import\.meta\.main/g, 'false')
145
+ modified = true
146
+ }
147
+
148
+ // stub files with top-level await - they're typically runtime-only
149
+ if (
150
+ /^\s*(?:const|let|var|export)\s+[^=]*=\s*await\b/m.test(contents) ||
151
+ /^await\s/m.test(contents)
152
+ ) {
153
+ if (process.env.DEBUG?.startsWith('tamagui')) {
154
+ console.info(`[tamagui] stubbing file with top-level await: ${args.path}`)
155
+ }
156
+ return {
157
+ contents: `// stubbed - contains top-level await\nmodule.exports = {}`,
158
+ loader: 'js',
159
+ }
160
+ }
161
+
162
+ if (modified) {
163
+ return {
164
+ contents,
165
+ loader: args.path.endsWith('.tsx')
166
+ ? 'tsx'
167
+ : args.path.endsWith('.ts')
168
+ ? 'ts'
169
+ : args.path.endsWith('.jsx')
170
+ ? 'jsx'
171
+ : 'js',
172
+ }
173
+ }
174
+
175
+ return null
176
+ })
177
+ },
178
+ },
179
+
95
180
  {
96
181
  name: 'external',
97
182
  setup(build) {
98
- build.onResolve({ filter: /@tamagui\/core/ }, (args) => {
183
+ const proxyWormPath = require.resolve('@tamagui/proxy-worm')
184
+
185
+ // only externalize @tamagui/core and @tamagui/web - these are provided at runtime
186
+ // other @tamagui/* packages (like @tamagui/config/v3) must be bundled in to avoid
187
+ // ESM race conditions when multiple threads require() them concurrently
188
+ build.onResolve({ filter: /^@tamagui\/(core|web)$/ }, (args) => {
189
+ if (args.kind === 'entry-point') {
190
+ return null
191
+ }
99
192
  return {
100
- path: platform === 'native' ? '@tamagui/core/native' : '@tamagui/core',
193
+ path: platform === 'native' ? '@tamagui/core/native' : args.path,
101
194
  external: true,
102
195
  }
103
196
  })
104
- build.onResolve({ filter: /react-native\/package.json$/ }, (args) => {
197
+
198
+ build.onResolve({ filter: /react-native\/package.json$/ }, () => {
105
199
  return {
106
200
  path: 'react-native/package.json',
107
201
  external: true,
108
202
  }
109
203
  })
110
- build.onResolve({ filter: /@tamagui\/web/ }, (args) => {
204
+
205
+ build.onResolve({ filter: /^(react-native|react-native\/.*)$/ }, () => {
111
206
  return {
112
- path: platform === 'native' ? '@tamagui/core/native' : '@tamagui/core',
207
+ path: '@tamagui/react-native-web-lite',
113
208
  external: true,
114
209
  }
115
210
  })
116
211
 
117
- build.onResolve({ filter: /^(react-native|react-native\/.*)$/ }, (args) => {
212
+ build.onResolve({ filter: /^react-native-reanimated(?:\/.*)?$/ }, () => {
118
213
  return {
119
- path: '@tamagui/react-native-web-lite',
120
- external: true,
214
+ path: proxyWormPath,
121
215
  }
122
216
  })
123
217
 
124
- build.onResolve({ filter: /react-native-reanimated/ }, (args) => {
218
+ build.onResolve({ filter: /^react-native-worklets(?:\/.*)?$/ }, () => {
125
219
  return {
126
- path: 'react-native-reanimated',
220
+ path: proxyWormPath,
221
+ }
222
+ })
223
+
224
+ // externalize animation libraries - not needed for static extraction
225
+ build.onResolve({ filter: /^(framer-motion|motion)/ }, (args) => {
226
+ return {
227
+ path: args.path,
127
228
  external: true,
128
229
  }
129
230
  })
@@ -139,36 +240,38 @@ function getESBuildConfig(
139
240
  return res
140
241
  }
141
242
 
243
+ function detectEntryFormat(entryPoint: string): esbuild.BuildOptions['format'] {
244
+ // file path - detect from file/package.json
245
+ if (entryPoint.startsWith('/') || entryPoint.startsWith('.')) {
246
+ return detectModuleFormat(entryPoint)
247
+ }
248
+ // bare module specifier - check package.json type field
249
+ try {
250
+ const pkgJsonPath = require.resolve(entryPoint + '/package.json')
251
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'))
252
+ return pkg.type === 'module' ? 'esm' : 'cjs'
253
+ } catch {
254
+ return 'cjs'
255
+ }
256
+ }
257
+
142
258
  export async function esbundleTamaguiConfig(
143
259
  props: Props,
144
260
  platform: TamaguiPlatform,
145
261
  aliases?: Record<string, string>
146
262
  ) {
147
- await asyncLock(props)
148
263
  const config = getESBuildConfig(props, platform, aliases)
149
- return await esbuild.build(config)
150
- }
151
264
 
152
- // until i do fancier things w plugins:
153
- async function asyncLock(props: Props) {
154
- const lockFile = join(dirname(props.outfile), basename(props.outfile, '.lock'))
155
- const lockStat = await FS.stat(lockFile).catch(() => {
156
- /* ok */
265
+ // build to memory first, then write atomically (temp file + rename)
266
+ // to prevent other threads from reading partially-written files
267
+ const tmpFile = props.outfile + '.tmp.' + process.pid
268
+ const result = await esbuild.build({
269
+ ...config,
270
+ outfile: tmpFile,
157
271
  })
158
- const lockedMsAgo = !lockStat
159
- ? Number.POSITIVE_INFINITY
160
- : new Date().getTime() - new Date(lockStat.mtime).getTime()
161
- if (lockedMsAgo < 500) {
162
- if (process.env.DEBUG?.startsWith('tamagui')) {
163
- console.info(`Waiting for existing build`, props.entryPoints)
164
- }
165
- let tries = 5
166
- while (tries--) {
167
- if (await FS.pathExists(props.outfile)) {
168
- return
169
- }
170
- await new Promise((res) => setTimeout(res, 50))
171
- }
172
- }
173
- void FS.writeFile(lockFile, '')
272
+
273
+ // atomic rename prevents other threads from reading partial files
274
+ await FS.rename(tmpFile, props.outfile)
275
+
276
+ return result
174
277
  }