@tamagui/build 2.7.7 → 3.0.0-beta.1093.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 (54) hide show
  1. package/README.md +19 -0
  2. package/esbuild-es5.js +4 -0
  3. package/package.json +30 -7
  4. package/pretty-print-errors.js +0 -31
  5. package/tamagui-build-workspace.js +296 -0
  6. package/tamagui-build.js +480 -212
  7. package/tamagui-tsgo.js +14 -0
  8. package/typescript-native.js +13 -0
  9. package/.turbo/turbo-test$colon$web.log +0 -68
  10. package/.turbo/turbo-test.log +0 -44
  11. package/__tests__/fixtures/js-main-package/.turbo/turbo-build.log +0 -2
  12. package/__tests__/fixtures/js-main-package/LICENSE +0 -1
  13. package/__tests__/fixtures/js-main-package/dist/cjs/index.js +0 -24
  14. package/__tests__/fixtures/js-main-package/dist/cjs/index.js.map +0 -6
  15. package/__tests__/fixtures/js-main-package/dist/cjs/index.native.js +0 -31
  16. package/__tests__/fixtures/js-main-package/dist/cjs/index.native.js.map +0 -1
  17. package/__tests__/fixtures/js-main-package/dist/esm/index.mjs +0 -3
  18. package/__tests__/fixtures/js-main-package/dist/esm/index.mjs.map +0 -1
  19. package/__tests__/fixtures/js-main-package/dist/esm/index.native.js +0 -3
  20. package/__tests__/fixtures/js-main-package/dist/esm/index.native.js.map +0 -1
  21. package/__tests__/fixtures/js-main-package/package.json +0 -14
  22. package/__tests__/fixtures/js-main-package/src/index.ts +0 -1
  23. package/__tests__/fixtures/js-main-package/tsconfig.json +0 -13
  24. package/__tests__/fixtures/js-main-package/types/index.d.ts +0 -2
  25. package/__tests__/fixtures/js-main-package/types/index.d.ts.map +0 -1
  26. package/__tests__/fixtures/simple-package/.turbo/turbo-build.log +0 -2
  27. package/__tests__/fixtures/simple-package/LICENSE +0 -21
  28. package/__tests__/fixtures/simple-package/index.d.ts +0 -2
  29. package/__tests__/fixtures/simple-package/index.d.ts.map +0 -1
  30. package/__tests__/fixtures/simple-package/package.json +0 -25
  31. package/__tests__/fixtures/simple-package/src/index.ts +0 -71
  32. package/__tests__/fixtures/simple-package/src/nativeOnly.ts +0 -3
  33. package/__tests__/fixtures/simple-package/src/nested/index.ts +0 -1
  34. package/__tests__/fixtures/simple-package/tsconfig.json +0 -42
  35. package/__tests__/fixtures/subpath-only-package/.turbo/turbo-build.log +0 -3
  36. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.cjs +0 -25
  37. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.native.cjs +0 -27
  38. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.native.js +0 -29
  39. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.native.js.map +0 -1
  40. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.mjs +0 -4
  41. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.mjs.map +0 -1
  42. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.native.js +0 -4
  43. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.native.js.map +0 -1
  44. package/__tests__/fixtures/subpath-only-package/types/feature.d.ts.map +0 -11
  45. package/__tests__/fixtures/watch-package/.turbo/turbo-build.log +0 -2
  46. package/__tests__/fixtures/watch-package/LICENSE +0 -21
  47. package/__tests__/fixtures/watch-package/index.d.ts +0 -2
  48. package/__tests__/fixtures/watch-package/index.d.ts.map +0 -1
  49. package/__tests__/fixtures/watch-package/package.json +0 -22
  50. package/__tests__/fixtures/watch-package/src/watch.ts +0 -3
  51. package/__tests__/fixtures/watch-package/tsconfig.json +0 -42
  52. package/__tests__/fixtures/watch-package/watch.d.ts +0 -2
  53. package/__tests__/fixtures/watch-package/watch.d.ts.map +0 -1
  54. package/__tests__/integration.test.ts +0 -341
package/tamagui-build.js CHANGED
@@ -23,7 +23,6 @@
23
23
  * tamagui-build --swap-exports -- pnpm publish --no-git-checks
24
24
  */
25
25
 
26
- const { transform } = require('@babel/core')
27
26
  const FSE = require('fs-extra')
28
27
  const esbuild = require('esbuild')
29
28
  const fastGlob = require('fast-glob')
@@ -34,11 +33,11 @@ const debounce = require('lodash.debounce')
34
33
  const { basename, dirname } = require('node:path')
35
34
  const { es5Plugin } = require('./esbuild-es5')
36
35
  const { transformSync: oxcTransformSync } = require('oxc-transform')
37
- const ts = require('typescript')
36
+ const { getTsconfig } = require('get-tsconfig')
38
37
  const path = require('node:path')
39
38
  const childProcess = require('node:child_process')
39
+ const { getTypeScriptNativePath } = require('./typescript-native')
40
40
  const {
41
- printTypescriptDiagnostics,
42
41
  printEsbuildError,
43
42
  printBuildError,
44
43
  printTypescriptCompilationError,
@@ -73,14 +72,18 @@ function dceTamaguiTarget(contents, { format, jsx, platform }) {
73
72
  return contents
74
73
  }
75
74
 
76
- const result = oxcTransformSync(`tamagui-target.${jsx === 'preserve' ? 'jsx' : 'js'}`, contents, {
77
- lang: jsx === 'preserve' ? 'jsx' : 'js',
78
- jsx: jsx === 'preserve' ? 'preserve' : undefined,
79
- sourceType: format === 'cjs' ? 'commonjs' : 'module',
80
- define: {
81
- 'process.env.TAMAGUI_TARGET': JSON.stringify(platform),
82
- },
83
- })
75
+ const result = oxcTransformSync(
76
+ `tamagui-target.${jsx === 'preserve' ? 'jsx' : 'js'}`,
77
+ contents,
78
+ {
79
+ lang: jsx === 'preserve' ? 'jsx' : 'js',
80
+ jsx: jsx === 'preserve' ? 'preserve' : undefined,
81
+ sourceType: format === 'cjs' ? 'commonjs' : 'module',
82
+ define: {
83
+ 'process.env.TAMAGUI_TARGET': JSON.stringify(platform),
84
+ },
85
+ }
86
+ )
84
87
 
85
88
  if (result.errors?.length) {
86
89
  throw new Error(
@@ -270,6 +273,143 @@ async function pruneUnusedImports(contents, filePath) {
270
273
  }
271
274
  }
272
275
 
276
+ function resolveOutputModuleSpecifier(
277
+ specifier,
278
+ filePath,
279
+ outputExtension,
280
+ outputPaths = new Set()
281
+ ) {
282
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
283
+ return specifier
284
+ }
285
+
286
+ const suffixIndex = specifier.search(/[?#]/)
287
+ const suffix = suffixIndex === -1 ? '' : specifier.slice(suffixIndex)
288
+ const pathname = suffixIndex === -1 ? specifier : specifier.slice(0, suffixIndex)
289
+
290
+ const targetPath = path.resolve(path.dirname(filePath), pathname)
291
+ if (
292
+ pathname.endsWith('.js') &&
293
+ (outputPaths.has(targetPath) || FSE.existsSync(targetPath))
294
+ ) {
295
+ const base = pathname.slice(0, -3)
296
+ const platformExtension = outputExtension.match(/^\.(native|web|ios|android)\./)?.[1]
297
+ return platformExtension && base.endsWith(`.${platformExtension}`)
298
+ ? `${base}.js${suffix}`
299
+ : `${base}${outputExtension}${suffix}`
300
+ }
301
+ if (outputPaths.has(`${targetPath}.js`) || FSE.existsSync(`${targetPath}.js`)) {
302
+ const platformExtension = outputExtension.match(/^\.(native|web|ios|android)\./)?.[1]
303
+ const explicitPlatformExtension = pathname.match(/\.(native|web|ios|android)$/)?.[1]
304
+ let extension = outputExtension
305
+ if (explicitPlatformExtension && outputExtension === '.mjs') {
306
+ extension = '.js'
307
+ } else if (platformExtension && pathname.endsWith(`.${platformExtension}`)) {
308
+ extension = outputExtension.replace(`.${platformExtension}`, '')
309
+ }
310
+ return `${pathname}${extension}${suffix}`
311
+ }
312
+
313
+ if (path.extname(pathname)) {
314
+ return specifier
315
+ }
316
+
317
+ if (
318
+ outputPaths.has(path.join(targetPath, 'index.js')) ||
319
+ (FSE.existsSync(targetPath) &&
320
+ FSE.lstatSync(targetPath).isDirectory() &&
321
+ FSE.existsSync(path.join(targetPath, 'index.js')))
322
+ ) {
323
+ return `${pathname.replace(/\/$/, '')}/index${outputExtension}${suffix}`
324
+ }
325
+
326
+ return specifier
327
+ }
328
+
329
+ async function fullySpecifyOutputs(
330
+ outputs,
331
+ { format, outputExtension, availableOutputPaths }
332
+ ) {
333
+ const jsOutputs = outputs.filter((file) => file?.path.endsWith('.js'))
334
+ if (!jsOutputs.length) {
335
+ return new Map()
336
+ }
337
+
338
+ const outputPaths = new Set([
339
+ ...Array.from(availableOutputPaths || [], (file) => path.resolve(file)),
340
+ ...jsOutputs.map((file) => path.resolve(file.path)),
341
+ ])
342
+ const transformed = new Map()
343
+
344
+ await Promise.all(
345
+ jsOutputs.map(async (file) => {
346
+ const bundle = await rolldown({
347
+ input: file.path,
348
+ platform: 'neutral',
349
+ treeshake: false,
350
+ plugins: [
351
+ {
352
+ name: 'tamagui-build-fully-specified',
353
+ resolveId(id, importer) {
354
+ if (!importer) return id
355
+ const resolved = resolveOutputModuleSpecifier(
356
+ id,
357
+ file.path,
358
+ outputExtension,
359
+ outputPaths
360
+ )
361
+ return {
362
+ id: resolved,
363
+ external: true,
364
+ }
365
+ },
366
+ load(id) {
367
+ if (id === file.path) return file.contents
368
+ },
369
+ },
370
+ ],
371
+ })
372
+
373
+ try {
374
+ const result = await bundle.generate({
375
+ format,
376
+ codeSplitting: false,
377
+ sourcemap: !shouldSkipSourceMaps,
378
+ comments: true,
379
+ minify: false,
380
+ polyfillRequire: false,
381
+ })
382
+ const chunk = result.output.find(
383
+ (output) => output.type === 'chunk' && output.facadeModuleId === file.path
384
+ )
385
+ if (!chunk) {
386
+ throw new Error(`rolldown did not emit ${file.path}`)
387
+ }
388
+
389
+ transformed.set(file.path, {
390
+ code: stripRolldownRegionComments(chunk.code).replace(
391
+ /\n?\/\/# sourceMappingURL=.*(?:\n|$)/g,
392
+ ''
393
+ ),
394
+ map: chunk.map || null,
395
+ })
396
+ } finally {
397
+ await bundle.close?.()
398
+ }
399
+ })
400
+ )
401
+
402
+ return transformed
403
+ }
404
+
405
+ function getFullySpecifiedOutput(outputs, filePath) {
406
+ const result = outputs.get(filePath)
407
+ if (!result) {
408
+ throw new Error(`rolldown did not emit ${filePath}`)
409
+ }
410
+ return result
411
+ }
412
+
273
413
  function hasFlag(flag) {
274
414
  return process.argv.includes(flag)
275
415
  }
@@ -291,6 +431,7 @@ const reactCompilerPlugin = {
291
431
  const isTSX = args.path.endsWith('.tsx')
292
432
 
293
433
  try {
434
+ const { transform } = require('@babel/core')
294
435
  const result = transform(source, {
295
436
  filename: args.path,
296
437
  configFile: false,
@@ -342,18 +483,22 @@ const shouldSkipTypes = shouldSwapExports
342
483
  : !!(hasFlag('--skip-types') || process.env.SKIP_TYPES)
343
484
 
344
485
  const shouldSkipNative = hasFlag('--skip-native')
486
+ // a package that hosts both platforms at runtime (the compiler evaluates
487
+ // modules per-platform and saves/restores process.env.TAMAGUI_TARGET) must
488
+ // keep its env reads live: inlining them turns its save/restore into an
489
+ // unconditional env flip
490
+ const shouldSkipTargetDCE = hasFlag('--keep-env-target')
345
491
  const shouldSkipMJS = hasFlag('--skip-mjs')
346
492
  const shouldSkipSourceMaps =
347
493
  hasFlag('--skip-sourcemaps') || getEnvFlag('TAMAGUI_BUILD_SKIP_SOURCEMAPS')
348
494
  // React Compiler is disabled by default - use --react-compiler to enable
349
- const shouldEnableCompiler = !!(
350
- hasFlag('--react-compiler') || process.env.REACT_COMPILER
351
- )
495
+ const shouldEnableCompiler = !!(hasFlag('--react-compiler') || process.env.REACT_COMPILER)
352
496
  const shouldBundleFlag = hasFlag('--bundle')
353
497
  const shouldBundleNodeModules = hasFlag('--bundle-modules')
354
498
  const shouldClean = !!process.argv.includes('clean')
355
499
  const shouldCleanBuildOnly = !!process.argv.includes('clean:build')
356
500
  const shouldWatch = hasFlag('--watch')
501
+ const packageBuildLock = '.tamagui-build-lock'
357
502
 
358
503
  // get command after "--" to run with swapped exports
359
504
  const dashDashIndex = process.argv.indexOf('--')
@@ -368,7 +513,7 @@ const exludeIndex = process.argv.indexOf('--exclude')
368
513
  const baseUrl =
369
514
  baseUrlIndex > -1 && process.argv[baseUrlIndex + 1]
370
515
  ? process.argv[baseUrlIndex + 1]
371
- : '.'
516
+ : null
372
517
  const tsProject =
373
518
  tsProjectIndex > -1 && process.argv[tsProjectIndex + 1]
374
519
  ? process.argv[tsProjectIndex + 1]
@@ -383,17 +528,25 @@ const pkgMain = pkg.main
383
528
  const pkgSource = pkg.source
384
529
  const pkgModule = pkg.module
385
530
  const pkgModuleJSX = pkg['module:jsx']
531
+ const pkgBin = pkg.bin
386
532
  const pkgTypes = Boolean(pkg.types || pkg.typings)
387
533
 
388
534
  // build config from package.json
389
535
  const buildConfig = pkg['tamagui-build'] || {}
536
+ const buildOutputs = new Set(buildConfig.outputs || [])
390
537
  // if bundleOnly is set, only bundle those packages (old behavior)
391
538
  // otherwise bundle ALL packages by default (new behavior)
392
539
  const bundleOnly = buildConfig.bundleOnly || null
393
540
  // if bundleExternal is set, always keep those packages external
394
541
  const bundleExternal = buildConfig.bundleExternal || null
395
542
 
396
- const flatOut = [pkgMain, pkgModule, pkgModuleJSX].filter(Boolean).length === 1
543
+ const shouldBuildCJS = Boolean(pkgMain || buildOutputs.has('cjs'))
544
+ const shouldBuildESM = Boolean(
545
+ pkgModule || buildOutputs.has('esm') || (!pkgMain && !pkgModuleJSX && pkgBin)
546
+ )
547
+ const shouldBuildTypes = Boolean(pkgTypes || buildOutputs.has('types'))
548
+ const flatOut =
549
+ [shouldBuildCJS, shouldBuildESM, Boolean(pkgModuleJSX)].filter(Boolean).length === 1
397
550
 
398
551
  const avoidCJS = pkgMain?.endsWith('.js')
399
552
  const getJsEntryAliasPath = (entry) => {
@@ -407,9 +560,10 @@ const getJsEntryAliasPath = (entry) => {
407
560
  return null
408
561
  }
409
562
  const cjsMainAliasPath = getJsEntryAliasPath(pkgMain)
410
- const esmAliasPaths = [getJsEntryAliasPath(pkgModule), getJsEntryAliasPath(pkgModuleJSX)].filter(
411
- Boolean
412
- )
563
+ const esmAliasPaths = [
564
+ getJsEntryAliasPath(pkgModule),
565
+ getJsEntryAliasPath(pkgModuleJSX),
566
+ ].filter(Boolean)
413
567
 
414
568
  const replaceRNWeb = {
415
569
  esm: (content) =>
@@ -438,7 +592,7 @@ const getProcessLabel = () => {
438
592
  if (shouldSkipTypes) return labels.js
439
593
  if (!skipJS && !jsOnly) return labels.build
440
594
  if (jsOnly) return labels.js
441
- if (!shouldSkipTypes && pkgTypes) return labels.tsc
595
+ if (!shouldSkipTypes && shouldBuildTypes) return labels.tsc
442
596
 
443
597
  return 'Unknown'
444
598
  }
@@ -453,7 +607,7 @@ async function clean() {
453
607
  //
454
608
  FSE.remove('.turbo'),
455
609
  FSE.remove('node_modules'),
456
- FSE.remove('types'),
610
+ shouldSkipTypes ? null : FSE.remove('types'),
457
611
  FSE.remove('dist'),
458
612
  ])
459
613
  } catch {
@@ -480,12 +634,17 @@ if (shouldClean || shouldCleanBuildOnly) {
480
634
  if (shouldWatch) {
481
635
  process.env.IS_WATCHING = true
482
636
  process.env.DISABLE_AUTORUN = true
483
- const rebuild = debounce(() => build({ cleanOutput: false }), 100)
637
+ let buildQueue = Promise.resolve()
638
+ const queueBuild = (options) => {
639
+ buildQueue = buildQueue.then(() => build(options))
640
+ return buildQueue
641
+ }
642
+ const rebuild = debounce(() => void queueBuild({ cleanOutput: false }), 100)
484
643
  const chokidar = require('chokidar')
485
644
 
486
645
  if (!process.env.SKIP_INITIAL_BUILD) {
487
646
  // do one js build but not types
488
- build({
647
+ void queueBuild({
489
648
  skipTypes: true,
490
649
  cleanOutput: true,
491
650
  })
@@ -610,37 +769,112 @@ function swapExportsTypes(pkg, direction) {
610
769
  return swapped
611
770
  }
612
771
 
613
- async function build({ skipTypes, cleanOutput = !shouldWatch } = {}) {
772
+ function processIsRunning(pid) {
773
+ try {
774
+ process.kill(pid, 0)
775
+ return true
776
+ } catch (error) {
777
+ return error?.code !== 'ESRCH'
778
+ }
779
+ }
780
+
781
+ async function withPackageBuildLock(run) {
782
+ while (true) {
783
+ try {
784
+ await FSE.mkdir(packageBuildLock)
785
+ await FSE.writeJSON(path.join(packageBuildLock, 'owner.json'), {
786
+ pid: process.pid,
787
+ })
788
+ break
789
+ } catch (error) {
790
+ if (error?.code !== 'EEXIST') throw error
791
+ const owner = await FSE.readJSON(path.join(packageBuildLock, 'owner.json')).catch(
792
+ () => null
793
+ )
794
+ if (owner?.pid && !processIsRunning(owner.pid)) {
795
+ await FSE.remove(packageBuildLock)
796
+ continue
797
+ }
798
+ const stat = await FSE.stat(packageBuildLock).catch(() => null)
799
+ if (!owner && stat && Date.now() - stat.mtimeMs > 30_000) {
800
+ await FSE.remove(packageBuildLock)
801
+ continue
802
+ }
803
+ await new Promise((resolve) => setTimeout(resolve, 25))
804
+ }
805
+ }
806
+
807
+ try {
808
+ return await run()
809
+ } finally {
810
+ await FSE.remove(packageBuildLock)
811
+ }
812
+ }
813
+
814
+ async function build(options) {
815
+ return withPackageBuildLock(() => buildUnlocked(options))
816
+ }
817
+
818
+ async function buildUnlocked({ skipTypes, cleanOutput = !shouldWatch } = {}) {
614
819
  if (process.env.DEBUG) console.info('🔹', pkg.name)
615
820
  try {
616
821
  const start = Date.now()
617
- const isSkippingTypesForBuild = Boolean(skipTypes || shouldSkipTypes || !pkgTypes)
618
-
619
- if (cleanOutput) {
620
- await Promise.allSettled([
621
- FSE.remove('dist'),
622
- isSkippingTypesForBuild ? null : FSE.remove('types'),
623
- ])
822
+ const distBuild = {
823
+ available: new Set(),
824
+ outputs: new Set(),
825
+ removals: new Set(),
826
+ writes: new Map(),
624
827
  }
625
828
 
829
+ // dist/ and types/ stay available while a build runs. Removing either up
830
+ // front breaks every concurrent consumer when an emit fails. Successful
831
+ // emits record their complete output set and prune stale files below.
832
+
626
833
  const allFiles = (await fastGlob(['src/**/*.(m)?[jt]s(x)?', 'src/**/*.css'])).filter(
627
- (x) => !x.includes('.d.ts') && (exclude ? !x.match(exclude) : true)
834
+ (x) =>
835
+ !x.includes('.d.ts') &&
836
+ !/(?:^|\/)(?:__tests__|tests?)(?:\/|$)/.test(x) &&
837
+ !/\.(?:test-d|test|spec)\.[cm]?[jt]sx?$/.test(x) &&
838
+ (exclude ? !x.match(exclude) : true)
628
839
  )
629
840
 
630
- await Promise.all([
841
+ const results = await Promise.allSettled([
631
842
  //
632
843
  skipTypes ? null : buildTsc(allFiles.filter((x) => /\.tsx?$/.test(x))),
633
- buildJs(allFiles),
844
+ buildJs(allFiles, distBuild),
634
845
  ])
635
-
636
- console.info('built', pkg.name, 'in', Date.now() - start, 'ms')
846
+ const failure = results.find((result) => result.status === 'rejected')
847
+ if (failure) {
848
+ throw failure.reason
849
+ }
637
850
 
638
851
  // Run afterBuild script if defined
639
852
  await runAfterBuild()
853
+
854
+ if (!skipJS) {
855
+ await Promise.all(
856
+ Array.from(distBuild.writes, ([filePath, contents]) =>
857
+ writeIfUnchanged(filePath, contents)
858
+ )
859
+ )
860
+ await Promise.all(
861
+ Array.from(distBuild.removals, (filePath) => FSE.remove(filePath))
862
+ )
863
+ }
864
+
865
+ if (cleanOutput && !skipJS) {
866
+ await pruneStaleOutputs(
867
+ 'dist',
868
+ distBuild.outputs,
869
+ flatOut ? ['**/*'] : ['cjs/**/*', 'esm/**/*', 'jsx/**/*']
870
+ )
871
+ }
872
+
873
+ console.info('built', pkg.name, 'in', Date.now() - start, 'ms')
640
874
  } catch (error) {
641
875
  printBuildError(error, pkg.name, process.cwd())
642
876
  if (!shouldWatch) {
643
- process.exit(1)
877
+ throw error
644
878
  }
645
879
  }
646
880
  }
@@ -668,7 +902,7 @@ async function runAfterBuild() {
668
902
  }
669
903
 
670
904
  async function buildTsc(allFiles) {
671
- if (!pkgTypes || jsOnly || shouldSkipTypes) return
905
+ if (!shouldBuildTypes || jsOnly || shouldSkipTypes) return
672
906
  if (shouldSkipInitialTypes) {
673
907
  shouldSkipInitialTypes = false
674
908
  return
@@ -681,18 +915,6 @@ async function buildTsc(allFiles) {
681
915
  const { config, error } = await loadTsConfig()
682
916
  if (error) throw error
683
917
 
684
- // much slower
685
- // if (config.options.isolatedDeclarations) {
686
- // console.info(
687
- // childProcess
688
- // .execSync(`npx tsgo --project ./tsconfig.json --emitDeclarationOnly`)
689
- // .toString()
690
- // )
691
- // return
692
- // }
693
-
694
- const compilerOptions = createCompilerOptions(config.options, targetDir)
695
-
696
918
  if (config.options.isolatedDeclarations) {
697
919
  const oxc = await import('oxc-transform')
698
920
 
@@ -731,134 +953,164 @@ async function buildTsc(allFiles) {
731
953
 
732
954
  if (allErrors.length > 0) {
733
955
  printOxcErrors(allErrors)
734
- if (!shouldWatch) {
735
- process.exit(1)
736
- }
956
+ throw new Error('isolated declaration emit failed')
737
957
  }
738
958
 
959
+ await pruneStaleDeclarations(targetDir, allFiles)
739
960
  return
740
961
  }
741
962
 
742
- const { program, emitResult, diagnostics } = await compileTypeScript(
743
- config.fileNames,
744
- compilerOptions
745
- )
746
-
747
- // exit on errors
748
- if (diagnostics.some((x) => x.code) && !shouldWatch) {
749
- printTypescriptDiagnostics(diagnostics, ts)
750
- if (shouldWatch) {
751
- return
752
- }
753
- process.exit(1)
754
- }
755
-
756
- reportDiagnostics(diagnostics)
757
-
758
- if (emitResult.emitSkipped) {
759
- throw new Error('TypeScript compilation failed')
760
- }
963
+ await emitDeclarationsWithTsgo(targetDir, allFiles)
964
+ await pruneStaleDeclarations(targetDir, allFiles)
761
965
  } catch (err) {
762
966
  printTypescriptCompilationError(err, pkg.name)
763
- if (!shouldWatch) {
764
- process.exit(1)
765
- }
967
+ throw err
766
968
  } finally {
767
969
  await FSE.remove('tsconfig.tsbuildinfo')
768
970
  }
769
971
  }
770
972
 
771
- async function loadTsConfig() {
772
- if (cachedConfig && shouldWatch) {
773
- return cachedConfig
774
- }
775
-
776
- const configPath = ts.findConfigFile(
777
- './',
778
- ts.sys.fileExists,
779
- tsProject || 'tsconfig.json'
780
- )
781
- if (!configPath) {
782
- return { error: new Error("Could not find a valid 'tsconfig.json'.") }
783
- }
784
-
785
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
786
- if (configFile.error) {
787
- return {
788
- error: new Error(`Error reading tsconfig.json: ${configFile.error.messageText}`),
973
+ // dist/ and types/ are never wiped before a build, so successful emits prune
974
+ // files that no longer belong to the source-derived output set.
975
+ async function pruneStaleOutputs(targetDir, expected, patterns) {
976
+ const targetRoot = path.resolve(targetDir)
977
+ const normalize = (file) =>
978
+ path.relative(process.cwd(), path.resolve(file)).split(path.sep).join('/')
979
+ const dir = normalize(targetDir)
980
+ const normalizedExpected = new Set(Array.from(expected, normalize))
981
+ const existing = await fastGlob(
982
+ patterns.map((pattern) => `${dir}/${pattern}`),
983
+ {
984
+ dot: true,
985
+ followSymbolicLinks: false,
986
+ objectMode: true,
987
+ onlyFiles: false,
789
988
  }
790
- }
791
-
792
- const parsedCommandLine = ts.parseJsonConfigFileContent(
793
- configFile.config,
794
- ts.sys,
795
- path.dirname(configPath)
796
989
  )
797
-
798
- if (parsedCommandLine.errors.length) {
799
- return {
800
- error: new Error(
801
- `Error parsing tsconfig.json: ${ts.formatDiagnostics(parsedCommandLine.errors, formatHost)}`
802
- ),
990
+ for (const entry of existing) {
991
+ if (!entry.dirent.isFile() && !entry.dirent.isSymbolicLink()) continue
992
+ const file = entry.path
993
+ if (!normalizedExpected.has(normalize(file))) {
994
+ const candidate = path.resolve(file)
995
+ if (candidate !== targetRoot && !candidate.startsWith(`${targetRoot}${path.sep}`)) {
996
+ throw new Error(`refusing to prune output outside ${targetDir}: ${file}`)
997
+ }
998
+ await FSE.unlink(candidate)
803
999
  }
804
1000
  }
805
-
806
- cachedConfig = { config: parsedCommandLine }
807
- return cachedConfig
808
1001
  }
809
1002
 
810
- function createCompilerOptions(baseOptions, targetDir) {
811
- const compilerOptions = {
812
- ...baseOptions,
813
- declaration: true,
814
- emitDeclarationOnly: true,
815
- declarationMap: !shouldSkipSourceMaps,
816
- outDir: targetDir,
817
- rootDir: 'src',
818
- incremental: true,
819
- tsBuildInfoFile: 'tsconfig.tsbuildinfo',
1003
+ async function pruneStaleDeclarations(targetDir, allFiles) {
1004
+ if (declarationToRoot) return
1005
+ // fast-glob echoes the pattern's leading "./" back in its results, so both
1006
+ // sides are normalized before comparison — mismatched prefixes here would
1007
+ // prune every declaration in the package.
1008
+ const normalize = (p) => p.replace(/^\.\//, '')
1009
+ const dir = normalize(targetDir)
1010
+ const expected = new Set()
1011
+ for (const file of allFiles) {
1012
+ const base = path.join(dir, ...file.split('/').slice(1)).replace(/\.tsx?$/, '')
1013
+ expected.add(`${base}.d.ts`)
1014
+ expected.add(`${base}.d.ts.map`)
820
1015
  }
1016
+ await pruneStaleOutputs(dir, expected, ['**/*.d.ts', '**/*.d.ts.map'])
1017
+ }
1018
+
1019
+ async function emitDeclarationsWithTsgo(targetDir, allFiles) {
1020
+ const tsgoPath = getTypeScriptNativePath()
1021
+ const declarationProject = `.tamagui-build-tsconfig-${process.pid}.json`
1022
+ const sourceProject = path
1023
+ .relative(process.cwd(), path.resolve(tsProject || 'tsconfig.json'))
1024
+ .replaceAll(path.sep, '/')
1025
+ await FSE.writeJSON(
1026
+ declarationProject,
1027
+ {
1028
+ extends: sourceProject.startsWith('.') ? sourceProject : `./${sourceProject}`,
1029
+ files: allFiles,
1030
+ include: [],
1031
+ exclude: [],
1032
+ },
1033
+ { spaces: 2 }
1034
+ )
1035
+ await FSE.remove('tsconfig.tsbuildinfo')
1036
+ const args = [
1037
+ '--project',
1038
+ declarationProject,
1039
+ '--declaration',
1040
+ '--emitDeclarationOnly',
1041
+ '--declarationMap',
1042
+ String(!shouldSkipSourceMaps),
1043
+ '--outDir',
1044
+ targetDir,
1045
+ '--rootDir',
1046
+ 'src',
1047
+ '--tsBuildInfoFile',
1048
+ 'tsconfig.tsbuildinfo',
1049
+ ]
821
1050
 
822
1051
  if (declarationToRoot) {
823
- compilerOptions.declarationDir = './'
1052
+ args.push('--declarationDir', './')
1053
+ }
1054
+ if (!ignoreBaseUrl && baseUrl) {
1055
+ args.push('--baseUrl', path.resolve(baseUrl))
824
1056
  }
825
1057
 
826
- if (!ignoreBaseUrl) {
827
- compilerOptions.baseUrl = baseUrl
1058
+ try {
1059
+ await new Promise((resolve, reject) => {
1060
+ const child = childProcess.spawn(tsgoPath, args, { stdio: 'inherit' })
1061
+ child.once('error', reject)
1062
+ child.once('exit', (code, signal) => {
1063
+ if (code === 0) {
1064
+ resolve()
1065
+ return
1066
+ }
1067
+ reject(
1068
+ new Error(
1069
+ signal
1070
+ ? `TypeScript native declaration emit stopped by ${signal}`
1071
+ : `TypeScript native declaration emit exited with code ${code}`
1072
+ )
1073
+ )
1074
+ })
1075
+ })
1076
+ } finally {
1077
+ await FSE.remove(declarationProject)
828
1078
  }
829
1079
 
830
- return compilerOptions
1080
+ const declarationRoot = declarationToRoot ? '.' : targetDir
1081
+ await Promise.all(
1082
+ allFiles.map(async (file) => {
1083
+ const relativeFile = path.relative(path.resolve('src'), path.resolve(file))
1084
+ const declarationPath = path
1085
+ .join(declarationRoot, relativeFile)
1086
+ .replace(/\.tsx?$/, '.d.ts')
1087
+ const output = await FSE.readFile(declarationPath, 'utf8')
1088
+ const normalized = output.replace(
1089
+ /(["'])@tamagui\/([^/"']+)\/types\1/g,
1090
+ '$1@tamagui/$2$1'
1091
+ )
1092
+ if (normalized !== output) {
1093
+ await writeIfUnchanged(declarationPath, normalized)
1094
+ }
1095
+ })
1096
+ )
831
1097
  }
832
1098
 
833
- async function compileTypeScript(fileNames, options) {
834
- const program = ts.createProgram(fileNames, options)
835
- const emitResult = program.emit()
836
- const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics)
837
-
838
- return { program, emitResult, diagnostics: allDiagnostics }
839
- }
1099
+ async function loadTsConfig() {
1100
+ if (cachedConfig && shouldWatch) {
1101
+ return cachedConfig
1102
+ }
840
1103
 
841
- const formatHost = {
842
- getCanonicalFileName: (path) => path,
843
- getCurrentDirectory: ts.sys.getCurrentDirectory,
844
- getNewLine: () => ts.sys.newLine,
845
- }
1104
+ const result = getTsconfig(process.cwd(), tsProject || 'tsconfig.json')
1105
+ if (!result) {
1106
+ return { error: new Error("Could not find a valid 'tsconfig.json'.") }
1107
+ }
846
1108
 
847
- function reportDiagnostics(diagnostics) {
848
- diagnostics.forEach((diagnostic) => {
849
- let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
850
- if (diagnostic.file && diagnostic.start !== undefined) {
851
- const { line, character } = ts.getLineAndCharacterOfPosition(
852
- diagnostic.file,
853
- diagnostic.start
854
- )
855
- message = `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
856
- }
857
- console.error(message)
858
- })
1109
+ cachedConfig = { config: { options: result.config.compilerOptions || {} } }
1110
+ return cachedConfig
859
1111
  }
860
1112
 
861
- async function buildJs(allFiles) {
1113
+ async function buildJs(allFiles, distBuild) {
862
1114
  if (skipJS) {
863
1115
  return
864
1116
  }
@@ -966,38 +1218,42 @@ async function buildJs(allFiles) {
966
1218
  }
967
1219
  }
968
1220
 
969
- return await Promise.all([
1221
+ const results = await Promise.allSettled([
970
1222
  // web output to cjs
971
- pkgMain
1223
+ shouldBuildCJS
972
1224
  ? esbuildWriteIfChanged(cjsConfigWeb, {
973
1225
  platform: 'web',
974
1226
  bundle: shouldBundleFlag,
975
1227
  specifyCJS: !avoidCJS,
976
1228
  keepJsOutput: avoidCJS,
977
1229
  preserveJsPaths: [],
1230
+ distBuild,
978
1231
  })
979
1232
  : null,
980
1233
 
981
1234
  // native output to cjs
982
- pkgMain && !shouldSkipNative
1235
+ shouldBuildCJS && !shouldSkipNative
983
1236
  ? esbuildWriteIfChanged(cjsConfig, {
984
1237
  platform: 'native',
1238
+ distBuild,
985
1239
  })
986
1240
  : null,
987
1241
 
988
1242
  // web output to esm
989
- pkgModule
1243
+ shouldBuildESM
990
1244
  ? esbuildWriteIfChanged(esmConfig, {
991
1245
  platform: 'web',
992
1246
  bundle: shouldBundleFlag,
993
1247
  preserveJsPaths: esmAliasPaths,
1248
+ distBuild,
994
1249
  })
995
1250
  : null,
996
1251
 
997
1252
  // native output to esm
998
- pkgModule && !shouldSkipNative
1253
+ shouldBuildESM && !shouldSkipNative
999
1254
  ? esbuildWriteIfChanged(esmConfig, {
1000
1255
  platform: 'native',
1256
+ distBuild,
1001
1257
  })
1002
1258
  : null,
1003
1259
 
@@ -1019,6 +1275,7 @@ async function buildJs(allFiles) {
1019
1275
  {
1020
1276
  platform: 'web',
1021
1277
  preserveJsPaths: esmAliasPaths,
1278
+ distBuild,
1022
1279
  }
1023
1280
  )
1024
1281
  : null,
@@ -1040,16 +1297,20 @@ async function buildJs(allFiles) {
1040
1297
  },
1041
1298
  {
1042
1299
  platform: 'native',
1300
+ distBuild,
1043
1301
  }
1044
1302
  )
1045
1303
  : null,
1046
- ]).then(() => {
1047
- if (process.env.DEBUG) {
1048
- console.info(`built js in ${Date.now() - start}ms`)
1049
- }
1304
+ ])
1050
1305
 
1051
- void esbuild.stop()
1052
- })
1306
+ void esbuild.stop()
1307
+ const failure = results.find((result) => result.status === 'rejected')
1308
+ if (failure) {
1309
+ throw failure.reason
1310
+ }
1311
+ if (process.env.DEBUG) {
1312
+ console.info(`built js in ${Date.now() - start}ms`)
1313
+ }
1053
1314
  }
1054
1315
 
1055
1316
  /**
@@ -1060,12 +1321,18 @@ async function buildJs(allFiles) {
1060
1321
  async function esbuildWriteIfChanged(
1061
1322
  /** @type { import('esbuild').BuildOptions } */
1062
1323
  opts,
1063
- { platform, env, specifyCJS, keepJsOutput, preserveJsPaths } = {
1324
+ { platform, env, specifyCJS, keepJsOutput, preserveJsPaths, distBuild } = {
1064
1325
  platform: '',
1065
1326
  specifyCJS: false,
1066
1327
  keepJsOutput: false,
1067
1328
  preserveJsPaths: [],
1068
1329
  env: '',
1330
+ distBuild: {
1331
+ available: new Set(),
1332
+ outputs: new Set(),
1333
+ removals: new Set(),
1334
+ writes: new Map(),
1335
+ },
1069
1336
  }
1070
1337
  ) {
1071
1338
  if (!platform) {
@@ -1163,9 +1430,7 @@ async function esbuildWriteIfChanged(
1163
1430
  built = await esbuild.build(buildSettings)
1164
1431
  } catch (err) {
1165
1432
  printEsbuildError(err)
1166
- if (!shouldWatch) {
1167
- process.exit(1)
1168
- }
1433
+ throw err
1169
1434
  }
1170
1435
 
1171
1436
  if (!built.outputFiles) {
@@ -1176,6 +1441,7 @@ async function esbuildWriteIfChanged(
1176
1441
  const webFilesMap = {}
1177
1442
 
1178
1443
  for (const p of built.outputFiles) {
1444
+ distBuild.available.add(p.path)
1179
1445
  if (p.path.includes('.native.js')) {
1180
1446
  nativeFilesMap[p.path] = true
1181
1447
  } else if (p.path.includes('.web.js')) {
@@ -1186,7 +1452,15 @@ async function esbuildWriteIfChanged(
1186
1452
  const cleanupNonMjsFiles = []
1187
1453
  const cleanupNonCjsFiles = []
1188
1454
 
1189
- const flush = writeIfUnchanged
1455
+ const flush = (filePath, contents) => {
1456
+ distBuild.outputs.add(filePath)
1457
+ distBuild.removals.delete(filePath)
1458
+ distBuild.writes.set(filePath, contents)
1459
+ }
1460
+ const remove = (filePath) => {
1461
+ distBuild.writes.delete(filePath)
1462
+ distBuild.removals.add(filePath)
1463
+ }
1190
1464
 
1191
1465
  const outputs = (
1192
1466
  await Promise.all(
@@ -1244,23 +1518,24 @@ async function esbuildWriteIfChanged(
1244
1518
  }
1245
1519
 
1246
1520
  if (!isMap && path.endsWith('.js')) {
1247
- const hadTamaguiTarget = contents.includes('process.env.TAMAGUI_TARGET')
1248
- contents = dceTamaguiTarget(contents, {
1249
- format: opts.format,
1250
- jsx: opts.jsx,
1251
- platform,
1252
- })
1521
+ const hadTamaguiTarget =
1522
+ !shouldSkipTargetDCE && contents.includes('process.env.TAMAGUI_TARGET')
1523
+ if (!shouldSkipTargetDCE) {
1524
+ contents = dceTamaguiTarget(contents, {
1525
+ format: opts.format,
1526
+ jsx: opts.jsx,
1527
+ platform,
1528
+ })
1529
+ }
1253
1530
 
1254
1531
  if (isESM && hadTamaguiTarget) {
1255
1532
  contents = await pruneUnusedImports(contents, path)
1256
1533
  }
1257
1534
 
1258
- // esbuild emits bare require() in esm output as a __require() shim that throws
1259
- // ("Dynamic require ... is not supported") under Metro's esm module scope. native
1260
- // always has a real require, so call it directly. this keeps the
1261
- // require('react-native')-behind-a-TAMAGUI_TARGET==='native' DCE guard working on
1262
- // native instead of crashing at runtime (e.g. toast PanResponder).
1263
- if (platform === 'native' && isESM) {
1535
+ // expose esbuild's require shim so rolldown can resolve and rewrite local
1536
+ // specifiers. rolldown restores the shim for web esm, while the native output
1537
+ // below keeps a real require for metro.
1538
+ if (isESM) {
1264
1539
  contents = contents.replaceAll('__require(', 'require(')
1265
1540
  }
1266
1541
  }
@@ -1317,6 +1592,14 @@ async function esbuildWriteIfChanged(
1317
1592
  )
1318
1593
 
1319
1594
  if (specifyCJS) {
1595
+ const transformedOutputs = opts.bundle
1596
+ ? new Map()
1597
+ : await fullySpecifyOutputs(outputs, {
1598
+ format: 'cjs',
1599
+ outputExtension: platform === 'native' ? '.native.cjs' : '.cjs',
1600
+ availableOutputPaths: distBuild.available,
1601
+ })
1602
+
1320
1603
  await Promise.all(
1321
1604
  outputs.map(async (file) => {
1322
1605
  if (!file) return
@@ -1325,19 +1608,7 @@ async function esbuildWriteIfChanged(
1325
1608
 
1326
1609
  const result = opts.bundle
1327
1610
  ? { code: contents }
1328
- : transform(contents, {
1329
- filename: path,
1330
- configFile: false,
1331
- sourceMap: !shouldSkipSourceMaps,
1332
- plugins: [
1333
- [
1334
- require.resolve('@tamagui/babel-plugin-fully-specified/commonjs'),
1335
- {
1336
- esExtensionDefault: platform === 'native' ? '.native.cjs' : '.cjs',
1337
- },
1338
- ],
1339
- ].filter(Boolean),
1340
- })
1611
+ : getFullySpecifiedOutput(transformedOutputs, path)
1341
1612
 
1342
1613
  const shouldPreserveJsAlias =
1343
1614
  preserveJsPathSet.has(path) || preserveJsPathAbsoluteSet.has(path)
@@ -1355,7 +1626,7 @@ async function esbuildWriteIfChanged(
1355
1626
  })
1356
1627
  )
1357
1628
  if (cleanupNonCjsFiles.length) {
1358
- await Promise.all(cleanupNonCjsFiles.map(async (file) => FSE.remove(file)))
1629
+ for (const file of cleanupNonCjsFiles) remove(file)
1359
1630
  }
1360
1631
  return
1361
1632
  }
@@ -1369,6 +1640,14 @@ async function esbuildWriteIfChanged(
1369
1640
  return
1370
1641
  }
1371
1642
 
1643
+ const transformedOutputs = opts.bundle
1644
+ ? new Map()
1645
+ : await fullySpecifyOutputs(outputs, {
1646
+ format: isESM ? 'esm' : 'cjs',
1647
+ outputExtension: platform === 'native' ? '.native.js' : '.mjs',
1648
+ availableOutputPaths: distBuild.available,
1649
+ })
1650
+
1372
1651
  await Promise.all(
1373
1652
  outputs.map(async (file) => {
1374
1653
  if (!file) return
@@ -1384,22 +1663,15 @@ async function esbuildWriteIfChanged(
1384
1663
  // and babel is bad on huge bundled files
1385
1664
  const result = opts.bundle
1386
1665
  ? { code: contents }
1387
- : transform(contents, {
1388
- filename: newOutPath,
1389
- configFile: false,
1390
- sourceMap: !shouldSkipSourceMaps,
1391
- plugins: [
1392
- [
1393
- isESM
1394
- ? require.resolve('@tamagui/babel-plugin-fully-specified')
1395
- : require.resolve('@tamagui/babel-plugin-fully-specified/commonjs'),
1396
- {
1397
- esExtensionDefault: platform === 'native' ? '.native.js' : '.mjs',
1398
- esExtensions: platform === 'native' ? ['.js'] : ['.mjs'],
1399
- },
1400
- ],
1401
- ].filter(Boolean),
1402
- })
1666
+ : getFullySpecifiedOutput(transformedOutputs, path)
1667
+
1668
+ if (platform === 'native' && isESM) {
1669
+ result.code = result.code.replace(/\b__require(?:\$\d+)?\(/g, 'require(')
1670
+ }
1671
+
1672
+ if (platform === 'native' && !isESM && !avoidCJS) {
1673
+ await flush(path.replace(/\.js$/, '.cjs'), result.code)
1674
+ }
1403
1675
 
1404
1676
  const shouldPreserveJsAlias =
1405
1677
  preserveJsPathSet.has(path) || preserveJsPathAbsoluteSet.has(path)
@@ -1420,7 +1692,7 @@ async function esbuildWriteIfChanged(
1420
1692
  if (result.map && !shouldSkipSourceMaps) {
1421
1693
  await flush(newOutPath + '.map', JSON.stringify(result.map))
1422
1694
  } else {
1423
- await FSE.remove(newOutPath + '.map')
1695
+ remove(newOutPath + '.map')
1424
1696
  }
1425
1697
 
1426
1698
  if (shouldPreserveJsAlias) {
@@ -1434,7 +1706,7 @@ async function esbuildWriteIfChanged(
1434
1706
  if (result.map && !shouldSkipSourceMaps) {
1435
1707
  await flush(path + '.map', JSON.stringify(result.map))
1436
1708
  } else {
1437
- await FSE.remove(path + '.map')
1709
+ remove(path + '.map')
1438
1710
  }
1439
1711
  }
1440
1712
  })
@@ -1442,10 +1714,6 @@ async function esbuildWriteIfChanged(
1442
1714
 
1443
1715
  // remove intermediary .js files once the final .mjs/.cjs outputs exist
1444
1716
  if (cleanupNonMjsFiles.length || cleanupNonCjsFiles.length) {
1445
- await Promise.all(
1446
- [...cleanupNonMjsFiles, ...cleanupNonCjsFiles].map(async (file) => {
1447
- await FSE.remove(file)
1448
- })
1449
- )
1717
+ for (const file of [...cleanupNonMjsFiles, ...cleanupNonCjsFiles]) remove(file)
1450
1718
  }
1451
1719
  }