@tamagui/build 2.1.0 → 2.2.0-1781044625560

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.
@@ -9,39 +9,41 @@ $ node ../../../tamagui-build.js --skip-mjs
9
9
  $ node ../../../tamagui-build.js --skip-sourcemaps
10
10
  $ node ../../../tamagui-build.js --ignore-base-url
11
11
  stdout | __tests__/integration.test.ts > tamagui-build integration test > should rebuild the package on file change when --watch is used
12
- Watch process output: built tamagui-build-test-watch-package in 84 ms
12
+ Watch process output: built tamagui-build-test-watch-package in 79 ms
13
13
 
14
14
  Initial build complete, modifying file...
15
15
 
16
16
  stdout | __tests__/integration.test.ts > tamagui-build integration test > should rebuild the package on file change when --watch is used
17
- Watch process output: built tamagui-build-test-watch-package in 372 ms
17
+ Watch process output: built tamagui-build-test-watch-package in 356 ms
18
18
 
19
19
  Rebuild after file modification complete
20
20
 
21
21
  $ node ../../../tamagui-build.js
22
22
  $ node ../../../tamagui-build.js
23
23
  $ node ../../../tamagui-build.js
24
+ $ node ../../../tamagui-build.js
24
25
  $ MINIFY=true node ../../../tamagui-build.js
25
26
  $ node ../../../tamagui-build.js
26
27
  $ node ../../../tamagui-build.js
27
28
  $ node ../../../tamagui-build.js
28
29
  $ node ../../../tamagui-build.js
29
- ✓ __tests__/integration.test.ts (12 tests) 9959ms
30
- ✓ should build the package correctly 646ms
31
- ✓ should bundle the package correctly 572ms
32
- ✓ should skip mjs files when --skip-mjs is used 643ms
33
- ✓ should skip sourcemaps when --skip-sourcemaps is used 647ms
34
- ✓ should ignore base URL when --ignore-base-url is used 654ms
35
- ✓ should rebuild the package on file change when --watch is used 777ms
36
- ✓ should generate correct platform-specific output 662ms
37
- ✓ should keep side-effectful native statements outside dev-only guards 653ms
38
- ✓ should minify the output when MINIFY=true is set 1346ms
39
- ✓ should clean stale outputs before building 1392ms
40
- ✓ should keep only the required js aliases after postprocessing 690ms
41
- ✓ should keep explicit cjs .js mains as final output 1234ms
30
+ ✓ __tests__/integration.test.ts (13 tests) 10843ms
31
+ ✓ should build the package correctly 662ms
32
+ ✓ should bundle the package correctly 598ms
33
+ ✓ should skip mjs files when --skip-mjs is used 654ms
34
+ ✓ should skip sourcemaps when --skip-sourcemaps is used 670ms
35
+ ✓ should ignore base URL when --ignore-base-url is used 680ms
36
+ ✓ should rebuild the package on file change when --watch is used 781ms
37
+ ✓ should generate correct platform-specific output 678ms
38
+ ✓ should prune imports left unused after platform DCE 724ms
39
+ ✓ should keep side-effectful native statements outside dev-only guards 730ms
40
+ ✓ should minify the output when MINIFY=true is set 1462ms
41
+ ✓ should clean stale outputs before building 1336ms
42
+ ✓ should keep only the required js aliases after postprocessing 679ms
43
+ ✓ should keep explicit cjs .js mains as final output 1154ms
42
44
 
43
45
  Test Files 1 passed (1)
44
- Tests 12 passed (12)
45
- Start at 12:38:37
46
- Duration 10.08s (transform 41ms, setup 0ms, collect 47ms, tests 9.96s, environment 0ms, prepare 2ms)
46
+ Tests 13 passed (13)
47
+ Start at 17:38:08
48
+ Duration 10.96s (transform 41ms, setup 0ms, collect 48ms, tests 10.84s, environment 0ms, prepare 2ms)
47
49
 
@@ -1,2 +1,2 @@
1
1
  $ node ../../../tamagui-build.js
2
- built tamagui-build-test-js-main-package in 1895 ms
2
+ built tamagui-build-test-js-main-package in 1531 ms
@@ -1,2 +1,2 @@
1
1
  $ node ../../../tamagui-build.js
2
- built tamagui-build-test-simple-tpackage in 1026 ms
2
+ built tamagui-build-test-simple-tpackage in 941 ms
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "tamagui-build-test-simple-tpackage",
3
3
  "version": "2.0.0-rc.0-1769885482630",
4
+ "sideEffects": false,
4
5
  "main": "dist/cjs",
5
6
  "module": "dist/esm",
6
7
  "module:jsx": "dist/jsx",
@@ -1,3 +1,5 @@
1
+ import { getNativeOnlyMarker } from './nativeOnly'
2
+
1
3
  export { nestedHello } from './nested'
2
4
 
3
5
  export const greet = (name: string): string => {
@@ -29,6 +31,26 @@ export function getPlatformMarker() {
29
31
  return process.env.TAMAGUI_TARGET === 'native' ? 'native-only-marker' : 'web-only-marker'
30
32
  }
31
33
 
34
+ export function getNativeImportMarker() {
35
+ if (process.env.TAMAGUI_TARGET === 'native') {
36
+ return getNativeOnlyMarker()
37
+ }
38
+
39
+ return 'web-import-marker'
40
+ }
41
+
42
+ export function getNodeEnvMarker() {
43
+ if (process.env.NODE_ENV === 'test') {
44
+ return 'test-env-marker'
45
+ }
46
+
47
+ if (process.env.NODE_ENV === 'development') {
48
+ return 'development-env-marker'
49
+ }
50
+
51
+ return 'runtime-env-marker'
52
+ }
53
+
32
54
  export function applyNativeLogicalMarker(items: string[]) {
33
55
  process.env.TAMAGUI_TARGET === 'native' && items.push('native-logical-marker')
34
56
  process.env.TAMAGUI_TARGET !== 'native' && items.push('web-logical-marker')
@@ -0,0 +1,3 @@
1
+ export function getNativeOnlyMarker() {
2
+ return 'native-import-marker'
3
+ }
@@ -1,2 +1,2 @@
1
1
  $ node ../../../tamagui-build.js
2
- built tamagui-build-test-watch-package in 1049 ms
2
+ built tamagui-build-test-watch-package in 548 ms
@@ -30,7 +30,7 @@ describe('tamagui-build integration test', () => {
30
30
  beforeAll(() => {
31
31
  // Clean up dist directory before starting
32
32
  execSync('rm -rf dist && rm -rf types', { cwd: simplePackagePath })
33
- execSync('rm -rf dist && rm -rf types', { cwd: jsMainPackagePath })
33
+ execSync('rm -rf dist', { cwd: jsMainPackagePath })
34
34
  })
35
35
 
36
36
  it('should build the package correctly', () => {
@@ -117,10 +117,11 @@ describe('tamagui-build integration test', () => {
117
117
  await new Promise<void>((resolve, reject) => {
118
118
  const timeout = setTimeout(() => {
119
119
  reject(new Error('Timeout waiting for build to complete'))
120
- }, 15000) // 30 second timeout
120
+ }, 15000)
121
121
 
122
122
  let initialBuildComplete = false
123
123
  let fileModified = false
124
+ const replacementGreeting = originalContent.includes('Hi,') ? 'Hey' : 'Hi'
124
125
 
125
126
  watchProcess.stdout.on('data', (data) => {
126
127
  console.log('Watch process output:', data.toString())
@@ -130,7 +131,7 @@ describe('tamagui-build integration test', () => {
130
131
  console.log('Initial build complete, modifying file...')
131
132
  // Modify the source file
132
133
  const newContent = `export const greet = (name: string): string => {
133
- return \`Hi, \${name}!\`;
134
+ return \`${replacementGreeting}, \${name}!\`;
134
135
  };`
135
136
  writeFileSync(watchSrcFilePath, newContent)
136
137
  fileModified = true
@@ -138,7 +139,7 @@ describe('tamagui-build integration test', () => {
138
139
  console.log('Rebuild after file modification complete')
139
140
  // Check the updated content of the output file
140
141
  const output = readFileSync(watchDistCjsFilePath, 'utf-8')
141
- expect(output).toContain('Hi,')
142
+ expect(output).toContain(`${replacementGreeting},`)
142
143
 
143
144
  // Change content back to original
144
145
  writeFileSync(watchSrcFilePath, originalContent)
@@ -150,6 +151,7 @@ describe('tamagui-build integration test', () => {
150
151
  })
151
152
  })
152
153
  } finally {
154
+ writeFileSync(watchSrcFilePath, originalContent)
153
155
  watchProcess.kill()
154
156
  }
155
157
  }, 15000)
@@ -183,6 +185,27 @@ describe('tamagui-build integration test', () => {
183
185
  expect(nativeOutput).toContain('greet:')
184
186
  })
185
187
 
188
+ it('should prune imports left unused after platform DCE', async () => {
189
+ execSync('bun run build', { cwd: simplePackagePath })
190
+
191
+ const webOutput = await readFile(join(distPath, 'esm', 'index.mjs'), 'utf-8')
192
+ const nativeOutput = await readFile(join(distPath, 'esm', 'index.native.js'), 'utf-8')
193
+ const nativeOnlyOutput = await readFile(
194
+ join(distPath, 'esm', 'nativeOnly.native.js'),
195
+ 'utf-8'
196
+ )
197
+
198
+ expect(webOutput).toContain('web-import-marker')
199
+ expect(webOutput).not.toContain('native-import-marker')
200
+ expect(webOutput).not.toContain('getNativeOnlyMarker')
201
+ expect(webOutput).not.toContain('./nativeOnly')
202
+ expect(webOutput).toContain('process.env.NODE_ENV === "test"')
203
+ expect(webOutput).toContain('process.env.NODE_ENV === "development"')
204
+
205
+ expect(nativeOutput).toContain('getNativeOnlyMarker')
206
+ expect(nativeOnlyOutput).toContain('native-import-marker')
207
+ })
208
+
186
209
  it('should keep side-effectful native statements outside dev-only guards', async () => {
187
210
  execSync('bun run build', { cwd: simplePackagePath })
188
211
 
@@ -292,6 +315,6 @@ describe('tamagui-build integration test', () => {
292
315
  afterAll(() => {
293
316
  // Clean up dist directory after tests
294
317
  execSync('rm -rf dist && rm -rf types', { cwd: simplePackagePath })
295
- execSync('rm -rf dist && rm -rf types', { cwd: jsMainPackagePath })
318
+ execSync('rm -rf dist', { cwd: jsMainPackagePath })
296
319
  })
297
320
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/build",
3
- "version": "2.1.0",
3
+ "version": "2.2.0-1781044625560",
4
4
  "gitHead": "a49cc7ea6b93ba384e77a4880ae48ac4a5635c14",
5
5
  "bin": {
6
6
  "tamagui-build": "tamagui-build.js",
@@ -17,7 +17,7 @@
17
17
  "@babel/core": "^7.25.2",
18
18
  "@babel/preset-typescript": "^7.26.0",
19
19
  "@swc/core": "^1.14.0",
20
- "@tamagui/babel-plugin-fully-specified": "2.1.0",
20
+ "@tamagui/babel-plugin-fully-specified": "2.2.0-1781044625560",
21
21
  "@types/fs-extra": "^9.0.13",
22
22
  "baseline-browser-mapping": "^2.9.15",
23
23
  "chokidar": "^3.5.2",
@@ -28,6 +28,8 @@
28
28
  "fs-extra": "^11.2.0",
29
29
  "lodash.debounce": "^4.0.8",
30
30
  "oxc-transform": "^0.112.0",
31
+ "picomatch": "4.0.3",
32
+ "rolldown": "1.0.1",
31
33
  "typescript": "~5.9.2"
32
34
  },
33
35
  "peerDependencies": {
package/tamagui-build.js CHANGED
@@ -27,6 +27,8 @@ const { transform } = require('@babel/core')
27
27
  const FSE = require('fs-extra')
28
28
  const esbuild = require('esbuild')
29
29
  const fastGlob = require('fast-glob')
30
+ const picomatch = require('picomatch')
31
+ const { rolldown } = require('rolldown')
30
32
  const createExternalPlugin = require('./externalNodePlugin')
31
33
  const debounce = require('lodash.debounce')
32
34
  const { basename, dirname } = require('node:path')
@@ -91,6 +93,183 @@ function dceTamaguiTarget(contents, { format, jsx, platform }) {
91
93
  return result.code || contents
92
94
  }
93
95
 
96
+ const sideEffectsMatchersCache = new Map()
97
+ const packageSideEffectsCache = new Map()
98
+ const sideEffectsExtensionCandidates = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx']
99
+
100
+ function normalizeSideEffectsSpecifier(specifier) {
101
+ return specifier
102
+ .replace(/[?#].*$/, '')
103
+ .replace(/^\.\/+/, '')
104
+ .replace(/^\/+/, '')
105
+ }
106
+
107
+ function getSideEffectsSpecifierCandidates(specifier) {
108
+ const normalized = normalizeSideEffectsSpecifier(specifier)
109
+ if (!normalized) return []
110
+
111
+ const candidates = [normalized]
112
+ const lastSegment = normalized.split('/').pop() || ''
113
+ const hasKnownExtension = /\.(?:[cm]?js|jsx|tsx?|css|json)$/.test(lastSegment)
114
+
115
+ if (!hasKnownExtension) {
116
+ for (const extension of sideEffectsExtensionCandidates) {
117
+ candidates.push(`${normalized}${extension}`)
118
+ }
119
+ for (const extension of sideEffectsExtensionCandidates) {
120
+ candidates.push(`${normalized}/index${extension}`)
121
+ }
122
+ }
123
+
124
+ return candidates
125
+ }
126
+
127
+ function sideEffectsMatch(sideEffects, specifier) {
128
+ if (sideEffects === true || sideEffects === undefined) return true
129
+ if (sideEffects === false) return false
130
+ if (!Array.isArray(sideEffects)) return true
131
+
132
+ const candidates = getSideEffectsSpecifierCandidates(specifier)
133
+ if (!candidates.length) return false
134
+
135
+ const cacheKey = JSON.stringify(sideEffects)
136
+ let matcher = sideEffectsMatchersCache.get(cacheKey)
137
+ if (!matcher) {
138
+ matcher = picomatch(sideEffects, { dot: true })
139
+ sideEffectsMatchersCache.set(cacheKey, matcher)
140
+ }
141
+ return candidates.some((candidate) => matcher(candidate))
142
+ }
143
+
144
+ function getPackageNameFromSpecifier(specifier) {
145
+ if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) return null
146
+ if (specifier.startsWith('node:')) return null
147
+ const parts = specifier.split('/')
148
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
149
+ }
150
+
151
+ function findPackageJsonFromResolvedFile(filePath, packageName) {
152
+ let current = dirname(filePath)
153
+ while (current && current !== dirname(current)) {
154
+ const packageJsonPath = path.join(current, 'package.json')
155
+ if (FSE.existsSync(packageJsonPath)) {
156
+ try {
157
+ const json = FSE.readJSONSync(packageJsonPath)
158
+ if (!packageName || json.name === packageName) {
159
+ return { path: packageJsonPath, json }
160
+ }
161
+ } catch {
162
+ return null
163
+ }
164
+ }
165
+ current = dirname(current)
166
+ }
167
+ return null
168
+ }
169
+
170
+ function getPackageSideEffects(packageName) {
171
+ if (packageSideEffectsCache.has(packageName)) {
172
+ return packageSideEffectsCache.get(packageName)
173
+ }
174
+
175
+ let found = null
176
+
177
+ try {
178
+ const packageJsonPath = require.resolve(`${packageName}/package.json`, {
179
+ paths: [process.cwd()],
180
+ })
181
+ found = {
182
+ path: packageJsonPath,
183
+ json: FSE.readJSONSync(packageJsonPath),
184
+ }
185
+ } catch {
186
+ try {
187
+ const resolved = require.resolve(packageName, {
188
+ paths: [process.cwd()],
189
+ })
190
+ found = findPackageJsonFromResolvedFile(resolved, packageName)
191
+ } catch {
192
+ found = null
193
+ }
194
+ }
195
+
196
+ const value = found
197
+ ? {
198
+ sideEffects: found.json.sideEffects,
199
+ }
200
+ : null
201
+
202
+ packageSideEffectsCache.set(packageName, value)
203
+ return value
204
+ }
205
+
206
+ function moduleHasSideEffects(id) {
207
+ if (id.startsWith('.') || id.startsWith('/')) {
208
+ return sideEffectsMatch(pkg.sideEffects, id)
209
+ }
210
+
211
+ const packageName = getPackageNameFromSpecifier(id)
212
+ if (!packageName) return true
213
+
214
+ const packageInfo = getPackageSideEffects(packageName)
215
+ if (!packageInfo) return true
216
+
217
+ const subpath = normalizeSideEffectsSpecifier(id.slice(packageName.length))
218
+ return sideEffectsMatch(packageInfo.sideEffects, subpath)
219
+ }
220
+
221
+ function stripRolldownRegionComments(contents) {
222
+ return contents
223
+ .replace(/^\/\/#region[^\n]*(?:\n|$)/gm, '')
224
+ .replace(/^\/\/#endregion[^\n]*(?:\n|$)/gm, '')
225
+ }
226
+
227
+ async function pruneUnusedImports(contents, filePath) {
228
+ if (!contents.includes('import')) {
229
+ return contents
230
+ }
231
+
232
+ const entryId = `${path.resolve(filePath)}?tamagui-build-prune`
233
+
234
+ const bundle = await rolldown({
235
+ input: entryId,
236
+ external: (id) => id !== entryId,
237
+ platform: 'neutral',
238
+ plugins: [
239
+ {
240
+ name: 'tamagui-build-prune-entry',
241
+ resolveId(id) {
242
+ if (id === entryId) return id
243
+ },
244
+ load(id) {
245
+ if (id === entryId) {
246
+ return {
247
+ code: contents,
248
+ moduleSideEffects: true,
249
+ }
250
+ }
251
+ },
252
+ },
253
+ ],
254
+ treeshake: {
255
+ moduleSideEffects: (id) => moduleHasSideEffects(id),
256
+ },
257
+ })
258
+
259
+ try {
260
+ const result = await bundle.generate({
261
+ format: 'esm',
262
+ sourcemap: false,
263
+ comments: false,
264
+ minify: false,
265
+ })
266
+ const chunk = result.output.find((item) => item.type === 'chunk')
267
+ return chunk?.code ? stripRolldownRegionComments(chunk.code) : contents
268
+ } finally {
269
+ await bundle.close?.()
270
+ }
271
+ }
272
+
94
273
  function hasFlag(flag) {
95
274
  return process.argv.includes(flag)
96
275
  }
@@ -1065,21 +1244,22 @@ async function esbuildWriteIfChanged(
1065
1244
  }
1066
1245
 
1067
1246
  if (!isMap && path.endsWith('.js')) {
1247
+ const hadTamaguiTarget = contents.includes('process.env.TAMAGUI_TARGET')
1068
1248
  contents = dceTamaguiTarget(contents, {
1069
1249
  format: opts.format,
1070
1250
  jsx: opts.jsx,
1071
1251
  platform,
1072
1252
  })
1253
+
1254
+ if (isESM && hadTamaguiTarget) {
1255
+ contents = await pruneUnusedImports(contents, path)
1256
+ }
1073
1257
  }
1074
1258
 
1075
1259
  if (isESM && pkg.sideEffects !== true && pkg.sideEffects !== undefined) {
1076
1260
  const sideEffects = pkg.sideEffects
1077
1261
  // sideEffects: false means nothing has side effects, strip all bare imports
1078
1262
  // sideEffects: ["*.css", ...] means only matching files have side effects
1079
- const picomatch = require('picomatch')
1080
- const matchers = sideEffects === false
1081
- ? null // strip everything
1082
- : (Array.isArray(sideEffects) ? sideEffects : []).map((p) => picomatch(p))
1083
1263
 
1084
1264
  const result = []
1085
1265
  const lines = contents.split('\n')
@@ -1090,14 +1270,13 @@ async function esbuildWriteIfChanged(
1090
1270
  result.push(line)
1091
1271
  continue
1092
1272
  }
1093
- const specifier = match[1].replace(/^\.\//, '')
1094
1273
  // sideEffects: false → strip all bare imports
1095
- if (matchers === null) {
1274
+ if (sideEffects === false) {
1096
1275
  result.push('')
1097
1276
  continue
1098
1277
  }
1099
1278
  // sideEffects: [...] → keep if specifier matches any pattern
1100
- const hasSideEffect = matchers.some((m) => m(specifier))
1279
+ const hasSideEffect = sideEffectsMatch(sideEffects, match[1])
1101
1280
  result.push(hasSideEffect ? line : '')
1102
1281
  }
1103
1282