@tamagui/build 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 (53) hide show
  1. package/README.md +19 -0
  2. package/package.json +27 -5
  3. package/pretty-print-errors.js +0 -31
  4. package/tamagui-build-workspace.js +296 -0
  5. package/tamagui-build.js +466 -206
  6. package/tamagui-tsgo.js +14 -0
  7. package/typescript-native.js +13 -0
  8. package/.turbo/turbo-test$colon$web.log +0 -68
  9. package/.turbo/turbo-test.log +0 -44
  10. package/__tests__/fixtures/js-main-package/.turbo/turbo-build.log +0 -2
  11. package/__tests__/fixtures/js-main-package/LICENSE +0 -1
  12. package/__tests__/fixtures/js-main-package/dist/cjs/index.js +0 -24
  13. package/__tests__/fixtures/js-main-package/dist/cjs/index.js.map +0 -6
  14. package/__tests__/fixtures/js-main-package/dist/cjs/index.native.js +0 -31
  15. package/__tests__/fixtures/js-main-package/dist/cjs/index.native.js.map +0 -1
  16. package/__tests__/fixtures/js-main-package/dist/esm/index.mjs +0 -3
  17. package/__tests__/fixtures/js-main-package/dist/esm/index.mjs.map +0 -1
  18. package/__tests__/fixtures/js-main-package/dist/esm/index.native.js +0 -3
  19. package/__tests__/fixtures/js-main-package/dist/esm/index.native.js.map +0 -1
  20. package/__tests__/fixtures/js-main-package/package.json +0 -14
  21. package/__tests__/fixtures/js-main-package/src/index.ts +0 -1
  22. package/__tests__/fixtures/js-main-package/tsconfig.json +0 -13
  23. package/__tests__/fixtures/js-main-package/types/index.d.ts +0 -2
  24. package/__tests__/fixtures/js-main-package/types/index.d.ts.map +0 -1
  25. package/__tests__/fixtures/simple-package/.turbo/turbo-build.log +0 -2
  26. package/__tests__/fixtures/simple-package/LICENSE +0 -21
  27. package/__tests__/fixtures/simple-package/index.d.ts +0 -2
  28. package/__tests__/fixtures/simple-package/index.d.ts.map +0 -1
  29. package/__tests__/fixtures/simple-package/package.json +0 -25
  30. package/__tests__/fixtures/simple-package/src/index.ts +0 -71
  31. package/__tests__/fixtures/simple-package/src/nativeOnly.ts +0 -3
  32. package/__tests__/fixtures/simple-package/src/nested/index.ts +0 -1
  33. package/__tests__/fixtures/simple-package/tsconfig.json +0 -42
  34. package/__tests__/fixtures/subpath-only-package/.turbo/turbo-build.log +0 -3
  35. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.cjs +0 -25
  36. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.native.cjs +0 -27
  37. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.native.js +0 -29
  38. package/__tests__/fixtures/subpath-only-package/dist/cjs/feature.native.js.map +0 -1
  39. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.mjs +0 -4
  40. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.mjs.map +0 -1
  41. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.native.js +0 -4
  42. package/__tests__/fixtures/subpath-only-package/dist/esm/feature.native.js.map +0 -1
  43. package/__tests__/fixtures/subpath-only-package/types/feature.d.ts.map +0 -11
  44. package/__tests__/fixtures/watch-package/.turbo/turbo-build.log +0 -2
  45. package/__tests__/fixtures/watch-package/LICENSE +0 -21
  46. package/__tests__/fixtures/watch-package/index.d.ts +0 -2
  47. package/__tests__/fixtures/watch-package/index.d.ts.map +0 -1
  48. package/__tests__/fixtures/watch-package/package.json +0 -22
  49. package/__tests__/fixtures/watch-package/src/watch.ts +0 -3
  50. package/__tests__/fixtures/watch-package/tsconfig.json +0 -42
  51. package/__tests__/fixtures/watch-package/watch.d.ts +0 -2
  52. package/__tests__/fixtures/watch-package/watch.d.ts.map +0 -1
  53. 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,
@@ -346,14 +487,13 @@ const shouldSkipMJS = hasFlag('--skip-mjs')
346
487
  const shouldSkipSourceMaps =
347
488
  hasFlag('--skip-sourcemaps') || getEnvFlag('TAMAGUI_BUILD_SKIP_SOURCEMAPS')
348
489
  // React Compiler is disabled by default - use --react-compiler to enable
349
- const shouldEnableCompiler = !!(
350
- hasFlag('--react-compiler') || process.env.REACT_COMPILER
351
- )
490
+ const shouldEnableCompiler = !!(hasFlag('--react-compiler') || process.env.REACT_COMPILER)
352
491
  const shouldBundleFlag = hasFlag('--bundle')
353
492
  const shouldBundleNodeModules = hasFlag('--bundle-modules')
354
493
  const shouldClean = !!process.argv.includes('clean')
355
494
  const shouldCleanBuildOnly = !!process.argv.includes('clean:build')
356
495
  const shouldWatch = hasFlag('--watch')
496
+ const packageBuildLock = '.tamagui-build-lock'
357
497
 
358
498
  // get command after "--" to run with swapped exports
359
499
  const dashDashIndex = process.argv.indexOf('--')
@@ -368,7 +508,7 @@ const exludeIndex = process.argv.indexOf('--exclude')
368
508
  const baseUrl =
369
509
  baseUrlIndex > -1 && process.argv[baseUrlIndex + 1]
370
510
  ? process.argv[baseUrlIndex + 1]
371
- : '.'
511
+ : null
372
512
  const tsProject =
373
513
  tsProjectIndex > -1 && process.argv[tsProjectIndex + 1]
374
514
  ? process.argv[tsProjectIndex + 1]
@@ -383,17 +523,25 @@ const pkgMain = pkg.main
383
523
  const pkgSource = pkg.source
384
524
  const pkgModule = pkg.module
385
525
  const pkgModuleJSX = pkg['module:jsx']
526
+ const pkgBin = pkg.bin
386
527
  const pkgTypes = Boolean(pkg.types || pkg.typings)
387
528
 
388
529
  // build config from package.json
389
530
  const buildConfig = pkg['tamagui-build'] || {}
531
+ const buildOutputs = new Set(buildConfig.outputs || [])
390
532
  // if bundleOnly is set, only bundle those packages (old behavior)
391
533
  // otherwise bundle ALL packages by default (new behavior)
392
534
  const bundleOnly = buildConfig.bundleOnly || null
393
535
  // if bundleExternal is set, always keep those packages external
394
536
  const bundleExternal = buildConfig.bundleExternal || null
395
537
 
396
- const flatOut = [pkgMain, pkgModule, pkgModuleJSX].filter(Boolean).length === 1
538
+ const shouldBuildCJS = Boolean(pkgMain || buildOutputs.has('cjs'))
539
+ const shouldBuildESM = Boolean(
540
+ pkgModule || buildOutputs.has('esm') || (!pkgMain && !pkgModuleJSX && pkgBin)
541
+ )
542
+ const shouldBuildTypes = Boolean(pkgTypes || buildOutputs.has('types'))
543
+ const flatOut =
544
+ [shouldBuildCJS, shouldBuildESM, Boolean(pkgModuleJSX)].filter(Boolean).length === 1
397
545
 
398
546
  const avoidCJS = pkgMain?.endsWith('.js')
399
547
  const getJsEntryAliasPath = (entry) => {
@@ -407,9 +555,10 @@ const getJsEntryAliasPath = (entry) => {
407
555
  return null
408
556
  }
409
557
  const cjsMainAliasPath = getJsEntryAliasPath(pkgMain)
410
- const esmAliasPaths = [getJsEntryAliasPath(pkgModule), getJsEntryAliasPath(pkgModuleJSX)].filter(
411
- Boolean
412
- )
558
+ const esmAliasPaths = [
559
+ getJsEntryAliasPath(pkgModule),
560
+ getJsEntryAliasPath(pkgModuleJSX),
561
+ ].filter(Boolean)
413
562
 
414
563
  const replaceRNWeb = {
415
564
  esm: (content) =>
@@ -438,7 +587,7 @@ const getProcessLabel = () => {
438
587
  if (shouldSkipTypes) return labels.js
439
588
  if (!skipJS && !jsOnly) return labels.build
440
589
  if (jsOnly) return labels.js
441
- if (!shouldSkipTypes && pkgTypes) return labels.tsc
590
+ if (!shouldSkipTypes && shouldBuildTypes) return labels.tsc
442
591
 
443
592
  return 'Unknown'
444
593
  }
@@ -453,7 +602,7 @@ async function clean() {
453
602
  //
454
603
  FSE.remove('.turbo'),
455
604
  FSE.remove('node_modules'),
456
- FSE.remove('types'),
605
+ shouldSkipTypes ? null : FSE.remove('types'),
457
606
  FSE.remove('dist'),
458
607
  ])
459
608
  } catch {
@@ -480,12 +629,17 @@ if (shouldClean || shouldCleanBuildOnly) {
480
629
  if (shouldWatch) {
481
630
  process.env.IS_WATCHING = true
482
631
  process.env.DISABLE_AUTORUN = true
483
- const rebuild = debounce(() => build({ cleanOutput: false }), 100)
632
+ let buildQueue = Promise.resolve()
633
+ const queueBuild = (options) => {
634
+ buildQueue = buildQueue.then(() => build(options))
635
+ return buildQueue
636
+ }
637
+ const rebuild = debounce(() => void queueBuild({ cleanOutput: false }), 100)
484
638
  const chokidar = require('chokidar')
485
639
 
486
640
  if (!process.env.SKIP_INITIAL_BUILD) {
487
641
  // do one js build but not types
488
- build({
642
+ void queueBuild({
489
643
  skipTypes: true,
490
644
  cleanOutput: true,
491
645
  })
@@ -610,37 +764,112 @@ function swapExportsTypes(pkg, direction) {
610
764
  return swapped
611
765
  }
612
766
 
613
- async function build({ skipTypes, cleanOutput = !shouldWatch } = {}) {
767
+ function processIsRunning(pid) {
768
+ try {
769
+ process.kill(pid, 0)
770
+ return true
771
+ } catch (error) {
772
+ return error?.code !== 'ESRCH'
773
+ }
774
+ }
775
+
776
+ async function withPackageBuildLock(run) {
777
+ while (true) {
778
+ try {
779
+ await FSE.mkdir(packageBuildLock)
780
+ await FSE.writeJSON(path.join(packageBuildLock, 'owner.json'), {
781
+ pid: process.pid,
782
+ })
783
+ break
784
+ } catch (error) {
785
+ if (error?.code !== 'EEXIST') throw error
786
+ const owner = await FSE.readJSON(path.join(packageBuildLock, 'owner.json')).catch(
787
+ () => null
788
+ )
789
+ if (owner?.pid && !processIsRunning(owner.pid)) {
790
+ await FSE.remove(packageBuildLock)
791
+ continue
792
+ }
793
+ const stat = await FSE.stat(packageBuildLock).catch(() => null)
794
+ if (!owner && stat && Date.now() - stat.mtimeMs > 30_000) {
795
+ await FSE.remove(packageBuildLock)
796
+ continue
797
+ }
798
+ await new Promise((resolve) => setTimeout(resolve, 25))
799
+ }
800
+ }
801
+
802
+ try {
803
+ return await run()
804
+ } finally {
805
+ await FSE.remove(packageBuildLock)
806
+ }
807
+ }
808
+
809
+ async function build(options) {
810
+ return withPackageBuildLock(() => buildUnlocked(options))
811
+ }
812
+
813
+ async function buildUnlocked({ skipTypes, cleanOutput = !shouldWatch } = {}) {
614
814
  if (process.env.DEBUG) console.info('🔹', pkg.name)
615
815
  try {
616
816
  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
- ])
817
+ const distBuild = {
818
+ available: new Set(),
819
+ outputs: new Set(),
820
+ removals: new Set(),
821
+ writes: new Map(),
624
822
  }
625
823
 
824
+ // dist/ and types/ stay available while a build runs. Removing either up
825
+ // front breaks every concurrent consumer when an emit fails. Successful
826
+ // emits record their complete output set and prune stale files below.
827
+
626
828
  const allFiles = (await fastGlob(['src/**/*.(m)?[jt]s(x)?', 'src/**/*.css'])).filter(
627
- (x) => !x.includes('.d.ts') && (exclude ? !x.match(exclude) : true)
829
+ (x) =>
830
+ !x.includes('.d.ts') &&
831
+ !/(?:^|\/)(?:__tests__|tests?)(?:\/|$)/.test(x) &&
832
+ !/\.(?:test-d|test|spec)\.[cm]?[jt]sx?$/.test(x) &&
833
+ (exclude ? !x.match(exclude) : true)
628
834
  )
629
835
 
630
- await Promise.all([
836
+ const results = await Promise.allSettled([
631
837
  //
632
838
  skipTypes ? null : buildTsc(allFiles.filter((x) => /\.tsx?$/.test(x))),
633
- buildJs(allFiles),
839
+ buildJs(allFiles, distBuild),
634
840
  ])
635
-
636
- console.info('built', pkg.name, 'in', Date.now() - start, 'ms')
841
+ const failure = results.find((result) => result.status === 'rejected')
842
+ if (failure) {
843
+ throw failure.reason
844
+ }
637
845
 
638
846
  // Run afterBuild script if defined
639
847
  await runAfterBuild()
848
+
849
+ if (!skipJS) {
850
+ await Promise.all(
851
+ Array.from(distBuild.writes, ([filePath, contents]) =>
852
+ writeIfUnchanged(filePath, contents)
853
+ )
854
+ )
855
+ await Promise.all(
856
+ Array.from(distBuild.removals, (filePath) => FSE.remove(filePath))
857
+ )
858
+ }
859
+
860
+ if (cleanOutput && !skipJS) {
861
+ await pruneStaleOutputs(
862
+ 'dist',
863
+ distBuild.outputs,
864
+ flatOut ? ['**/*'] : ['cjs/**/*', 'esm/**/*', 'jsx/**/*']
865
+ )
866
+ }
867
+
868
+ console.info('built', pkg.name, 'in', Date.now() - start, 'ms')
640
869
  } catch (error) {
641
870
  printBuildError(error, pkg.name, process.cwd())
642
871
  if (!shouldWatch) {
643
- process.exit(1)
872
+ throw error
644
873
  }
645
874
  }
646
875
  }
@@ -668,7 +897,7 @@ async function runAfterBuild() {
668
897
  }
669
898
 
670
899
  async function buildTsc(allFiles) {
671
- if (!pkgTypes || jsOnly || shouldSkipTypes) return
900
+ if (!shouldBuildTypes || jsOnly || shouldSkipTypes) return
672
901
  if (shouldSkipInitialTypes) {
673
902
  shouldSkipInitialTypes = false
674
903
  return
@@ -681,18 +910,6 @@ async function buildTsc(allFiles) {
681
910
  const { config, error } = await loadTsConfig()
682
911
  if (error) throw error
683
912
 
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
913
  if (config.options.isolatedDeclarations) {
697
914
  const oxc = await import('oxc-transform')
698
915
 
@@ -731,134 +948,164 @@ async function buildTsc(allFiles) {
731
948
 
732
949
  if (allErrors.length > 0) {
733
950
  printOxcErrors(allErrors)
734
- if (!shouldWatch) {
735
- process.exit(1)
736
- }
951
+ throw new Error('isolated declaration emit failed')
737
952
  }
738
953
 
954
+ await pruneStaleDeclarations(targetDir, allFiles)
739
955
  return
740
956
  }
741
957
 
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
- }
958
+ await emitDeclarationsWithTsgo(targetDir, allFiles)
959
+ await pruneStaleDeclarations(targetDir, allFiles)
761
960
  } catch (err) {
762
961
  printTypescriptCompilationError(err, pkg.name)
763
- if (!shouldWatch) {
764
- process.exit(1)
765
- }
962
+ throw err
766
963
  } finally {
767
964
  await FSE.remove('tsconfig.tsbuildinfo')
768
965
  }
769
966
  }
770
967
 
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}`),
968
+ // dist/ and types/ are never wiped before a build, so successful emits prune
969
+ // files that no longer belong to the source-derived output set.
970
+ async function pruneStaleOutputs(targetDir, expected, patterns) {
971
+ const targetRoot = path.resolve(targetDir)
972
+ const normalize = (file) =>
973
+ path.relative(process.cwd(), path.resolve(file)).split(path.sep).join('/')
974
+ const dir = normalize(targetDir)
975
+ const normalizedExpected = new Set(Array.from(expected, normalize))
976
+ const existing = await fastGlob(
977
+ patterns.map((pattern) => `${dir}/${pattern}`),
978
+ {
979
+ dot: true,
980
+ followSymbolicLinks: false,
981
+ objectMode: true,
982
+ onlyFiles: false,
789
983
  }
790
- }
791
-
792
- const parsedCommandLine = ts.parseJsonConfigFileContent(
793
- configFile.config,
794
- ts.sys,
795
- path.dirname(configPath)
796
984
  )
797
-
798
- if (parsedCommandLine.errors.length) {
799
- return {
800
- error: new Error(
801
- `Error parsing tsconfig.json: ${ts.formatDiagnostics(parsedCommandLine.errors, formatHost)}`
802
- ),
985
+ for (const entry of existing) {
986
+ if (!entry.dirent.isFile() && !entry.dirent.isSymbolicLink()) continue
987
+ const file = entry.path
988
+ if (!normalizedExpected.has(normalize(file))) {
989
+ const candidate = path.resolve(file)
990
+ if (candidate !== targetRoot && !candidate.startsWith(`${targetRoot}${path.sep}`)) {
991
+ throw new Error(`refusing to prune output outside ${targetDir}: ${file}`)
992
+ }
993
+ await FSE.unlink(candidate)
803
994
  }
804
995
  }
805
-
806
- cachedConfig = { config: parsedCommandLine }
807
- return cachedConfig
808
996
  }
809
997
 
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',
998
+ async function pruneStaleDeclarations(targetDir, allFiles) {
999
+ if (declarationToRoot) return
1000
+ // fast-glob echoes the pattern's leading "./" back in its results, so both
1001
+ // sides are normalized before comparison — mismatched prefixes here would
1002
+ // prune every declaration in the package.
1003
+ const normalize = (p) => p.replace(/^\.\//, '')
1004
+ const dir = normalize(targetDir)
1005
+ const expected = new Set()
1006
+ for (const file of allFiles) {
1007
+ const base = path.join(dir, ...file.split('/').slice(1)).replace(/\.tsx?$/, '')
1008
+ expected.add(`${base}.d.ts`)
1009
+ expected.add(`${base}.d.ts.map`)
820
1010
  }
1011
+ await pruneStaleOutputs(dir, expected, ['**/*.d.ts', '**/*.d.ts.map'])
1012
+ }
1013
+
1014
+ async function emitDeclarationsWithTsgo(targetDir, allFiles) {
1015
+ const tsgoPath = getTypeScriptNativePath()
1016
+ const declarationProject = `.tamagui-build-tsconfig-${process.pid}.json`
1017
+ const sourceProject = path
1018
+ .relative(process.cwd(), path.resolve(tsProject || 'tsconfig.json'))
1019
+ .replaceAll(path.sep, '/')
1020
+ await FSE.writeJSON(
1021
+ declarationProject,
1022
+ {
1023
+ extends: sourceProject.startsWith('.') ? sourceProject : `./${sourceProject}`,
1024
+ files: allFiles,
1025
+ include: [],
1026
+ exclude: [],
1027
+ },
1028
+ { spaces: 2 }
1029
+ )
1030
+ await FSE.remove('tsconfig.tsbuildinfo')
1031
+ const args = [
1032
+ '--project',
1033
+ declarationProject,
1034
+ '--declaration',
1035
+ '--emitDeclarationOnly',
1036
+ '--declarationMap',
1037
+ String(!shouldSkipSourceMaps),
1038
+ '--outDir',
1039
+ targetDir,
1040
+ '--rootDir',
1041
+ 'src',
1042
+ '--tsBuildInfoFile',
1043
+ 'tsconfig.tsbuildinfo',
1044
+ ]
821
1045
 
822
1046
  if (declarationToRoot) {
823
- compilerOptions.declarationDir = './'
1047
+ args.push('--declarationDir', './')
1048
+ }
1049
+ if (!ignoreBaseUrl && baseUrl) {
1050
+ args.push('--baseUrl', path.resolve(baseUrl))
824
1051
  }
825
1052
 
826
- if (!ignoreBaseUrl) {
827
- compilerOptions.baseUrl = baseUrl
1053
+ try {
1054
+ await new Promise((resolve, reject) => {
1055
+ const child = childProcess.spawn(tsgoPath, args, { stdio: 'inherit' })
1056
+ child.once('error', reject)
1057
+ child.once('exit', (code, signal) => {
1058
+ if (code === 0) {
1059
+ resolve()
1060
+ return
1061
+ }
1062
+ reject(
1063
+ new Error(
1064
+ signal
1065
+ ? `TypeScript native declaration emit stopped by ${signal}`
1066
+ : `TypeScript native declaration emit exited with code ${code}`
1067
+ )
1068
+ )
1069
+ })
1070
+ })
1071
+ } finally {
1072
+ await FSE.remove(declarationProject)
828
1073
  }
829
1074
 
830
- return compilerOptions
1075
+ const declarationRoot = declarationToRoot ? '.' : targetDir
1076
+ await Promise.all(
1077
+ allFiles.map(async (file) => {
1078
+ const relativeFile = path.relative(path.resolve('src'), path.resolve(file))
1079
+ const declarationPath = path
1080
+ .join(declarationRoot, relativeFile)
1081
+ .replace(/\.tsx?$/, '.d.ts')
1082
+ const output = await FSE.readFile(declarationPath, 'utf8')
1083
+ const normalized = output.replace(
1084
+ /(["'])@tamagui\/([^/"']+)\/types\1/g,
1085
+ '$1@tamagui/$2$1'
1086
+ )
1087
+ if (normalized !== output) {
1088
+ await writeIfUnchanged(declarationPath, normalized)
1089
+ }
1090
+ })
1091
+ )
831
1092
  }
832
1093
 
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
- }
1094
+ async function loadTsConfig() {
1095
+ if (cachedConfig && shouldWatch) {
1096
+ return cachedConfig
1097
+ }
840
1098
 
841
- const formatHost = {
842
- getCanonicalFileName: (path) => path,
843
- getCurrentDirectory: ts.sys.getCurrentDirectory,
844
- getNewLine: () => ts.sys.newLine,
845
- }
1099
+ const result = getTsconfig(process.cwd(), tsProject || 'tsconfig.json')
1100
+ if (!result) {
1101
+ return { error: new Error("Could not find a valid 'tsconfig.json'.") }
1102
+ }
846
1103
 
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
- })
1104
+ cachedConfig = { config: { options: result.config.compilerOptions || {} } }
1105
+ return cachedConfig
859
1106
  }
860
1107
 
861
- async function buildJs(allFiles) {
1108
+ async function buildJs(allFiles, distBuild) {
862
1109
  if (skipJS) {
863
1110
  return
864
1111
  }
@@ -966,38 +1213,42 @@ async function buildJs(allFiles) {
966
1213
  }
967
1214
  }
968
1215
 
969
- return await Promise.all([
1216
+ const results = await Promise.allSettled([
970
1217
  // web output to cjs
971
- pkgMain
1218
+ shouldBuildCJS
972
1219
  ? esbuildWriteIfChanged(cjsConfigWeb, {
973
1220
  platform: 'web',
974
1221
  bundle: shouldBundleFlag,
975
1222
  specifyCJS: !avoidCJS,
976
1223
  keepJsOutput: avoidCJS,
977
1224
  preserveJsPaths: [],
1225
+ distBuild,
978
1226
  })
979
1227
  : null,
980
1228
 
981
1229
  // native output to cjs
982
- pkgMain && !shouldSkipNative
1230
+ shouldBuildCJS && !shouldSkipNative
983
1231
  ? esbuildWriteIfChanged(cjsConfig, {
984
1232
  platform: 'native',
1233
+ distBuild,
985
1234
  })
986
1235
  : null,
987
1236
 
988
1237
  // web output to esm
989
- pkgModule
1238
+ shouldBuildESM
990
1239
  ? esbuildWriteIfChanged(esmConfig, {
991
1240
  platform: 'web',
992
1241
  bundle: shouldBundleFlag,
993
1242
  preserveJsPaths: esmAliasPaths,
1243
+ distBuild,
994
1244
  })
995
1245
  : null,
996
1246
 
997
1247
  // native output to esm
998
- pkgModule && !shouldSkipNative
1248
+ shouldBuildESM && !shouldSkipNative
999
1249
  ? esbuildWriteIfChanged(esmConfig, {
1000
1250
  platform: 'native',
1251
+ distBuild,
1001
1252
  })
1002
1253
  : null,
1003
1254
 
@@ -1019,6 +1270,7 @@ async function buildJs(allFiles) {
1019
1270
  {
1020
1271
  platform: 'web',
1021
1272
  preserveJsPaths: esmAliasPaths,
1273
+ distBuild,
1022
1274
  }
1023
1275
  )
1024
1276
  : null,
@@ -1040,16 +1292,20 @@ async function buildJs(allFiles) {
1040
1292
  },
1041
1293
  {
1042
1294
  platform: 'native',
1295
+ distBuild,
1043
1296
  }
1044
1297
  )
1045
1298
  : null,
1046
- ]).then(() => {
1047
- if (process.env.DEBUG) {
1048
- console.info(`built js in ${Date.now() - start}ms`)
1049
- }
1299
+ ])
1050
1300
 
1051
- void esbuild.stop()
1052
- })
1301
+ void esbuild.stop()
1302
+ const failure = results.find((result) => result.status === 'rejected')
1303
+ if (failure) {
1304
+ throw failure.reason
1305
+ }
1306
+ if (process.env.DEBUG) {
1307
+ console.info(`built js in ${Date.now() - start}ms`)
1308
+ }
1053
1309
  }
1054
1310
 
1055
1311
  /**
@@ -1060,12 +1316,18 @@ async function buildJs(allFiles) {
1060
1316
  async function esbuildWriteIfChanged(
1061
1317
  /** @type { import('esbuild').BuildOptions } */
1062
1318
  opts,
1063
- { platform, env, specifyCJS, keepJsOutput, preserveJsPaths } = {
1319
+ { platform, env, specifyCJS, keepJsOutput, preserveJsPaths, distBuild } = {
1064
1320
  platform: '',
1065
1321
  specifyCJS: false,
1066
1322
  keepJsOutput: false,
1067
1323
  preserveJsPaths: [],
1068
1324
  env: '',
1325
+ distBuild: {
1326
+ available: new Set(),
1327
+ outputs: new Set(),
1328
+ removals: new Set(),
1329
+ writes: new Map(),
1330
+ },
1069
1331
  }
1070
1332
  ) {
1071
1333
  if (!platform) {
@@ -1163,9 +1425,7 @@ async function esbuildWriteIfChanged(
1163
1425
  built = await esbuild.build(buildSettings)
1164
1426
  } catch (err) {
1165
1427
  printEsbuildError(err)
1166
- if (!shouldWatch) {
1167
- process.exit(1)
1168
- }
1428
+ throw err
1169
1429
  }
1170
1430
 
1171
1431
  if (!built.outputFiles) {
@@ -1176,6 +1436,7 @@ async function esbuildWriteIfChanged(
1176
1436
  const webFilesMap = {}
1177
1437
 
1178
1438
  for (const p of built.outputFiles) {
1439
+ distBuild.available.add(p.path)
1179
1440
  if (p.path.includes('.native.js')) {
1180
1441
  nativeFilesMap[p.path] = true
1181
1442
  } else if (p.path.includes('.web.js')) {
@@ -1186,7 +1447,15 @@ async function esbuildWriteIfChanged(
1186
1447
  const cleanupNonMjsFiles = []
1187
1448
  const cleanupNonCjsFiles = []
1188
1449
 
1189
- const flush = writeIfUnchanged
1450
+ const flush = (filePath, contents) => {
1451
+ distBuild.outputs.add(filePath)
1452
+ distBuild.removals.delete(filePath)
1453
+ distBuild.writes.set(filePath, contents)
1454
+ }
1455
+ const remove = (filePath) => {
1456
+ distBuild.writes.delete(filePath)
1457
+ distBuild.removals.add(filePath)
1458
+ }
1190
1459
 
1191
1460
  const outputs = (
1192
1461
  await Promise.all(
@@ -1255,12 +1524,10 @@ async function esbuildWriteIfChanged(
1255
1524
  contents = await pruneUnusedImports(contents, path)
1256
1525
  }
1257
1526
 
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) {
1527
+ // expose esbuild's require shim so rolldown can resolve and rewrite local
1528
+ // specifiers. rolldown restores the shim for web esm, while the native output
1529
+ // below keeps a real require for metro.
1530
+ if (isESM) {
1264
1531
  contents = contents.replaceAll('__require(', 'require(')
1265
1532
  }
1266
1533
  }
@@ -1317,6 +1584,14 @@ async function esbuildWriteIfChanged(
1317
1584
  )
1318
1585
 
1319
1586
  if (specifyCJS) {
1587
+ const transformedOutputs = opts.bundle
1588
+ ? new Map()
1589
+ : await fullySpecifyOutputs(outputs, {
1590
+ format: 'cjs',
1591
+ outputExtension: platform === 'native' ? '.native.cjs' : '.cjs',
1592
+ availableOutputPaths: distBuild.available,
1593
+ })
1594
+
1320
1595
  await Promise.all(
1321
1596
  outputs.map(async (file) => {
1322
1597
  if (!file) return
@@ -1325,19 +1600,7 @@ async function esbuildWriteIfChanged(
1325
1600
 
1326
1601
  const result = opts.bundle
1327
1602
  ? { 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
- })
1603
+ : getFullySpecifiedOutput(transformedOutputs, path)
1341
1604
 
1342
1605
  const shouldPreserveJsAlias =
1343
1606
  preserveJsPathSet.has(path) || preserveJsPathAbsoluteSet.has(path)
@@ -1355,7 +1618,7 @@ async function esbuildWriteIfChanged(
1355
1618
  })
1356
1619
  )
1357
1620
  if (cleanupNonCjsFiles.length) {
1358
- await Promise.all(cleanupNonCjsFiles.map(async (file) => FSE.remove(file)))
1621
+ for (const file of cleanupNonCjsFiles) remove(file)
1359
1622
  }
1360
1623
  return
1361
1624
  }
@@ -1369,6 +1632,14 @@ async function esbuildWriteIfChanged(
1369
1632
  return
1370
1633
  }
1371
1634
 
1635
+ const transformedOutputs = opts.bundle
1636
+ ? new Map()
1637
+ : await fullySpecifyOutputs(outputs, {
1638
+ format: isESM ? 'esm' : 'cjs',
1639
+ outputExtension: platform === 'native' ? '.native.js' : '.mjs',
1640
+ availableOutputPaths: distBuild.available,
1641
+ })
1642
+
1372
1643
  await Promise.all(
1373
1644
  outputs.map(async (file) => {
1374
1645
  if (!file) return
@@ -1384,22 +1655,15 @@ async function esbuildWriteIfChanged(
1384
1655
  // and babel is bad on huge bundled files
1385
1656
  const result = opts.bundle
1386
1657
  ? { 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
- })
1658
+ : getFullySpecifiedOutput(transformedOutputs, path)
1659
+
1660
+ if (platform === 'native' && isESM) {
1661
+ result.code = result.code.replace(/\b__require(?:\$\d+)?\(/g, 'require(')
1662
+ }
1663
+
1664
+ if (platform === 'native' && !isESM && !avoidCJS) {
1665
+ await flush(path.replace(/\.js$/, '.cjs'), result.code)
1666
+ }
1403
1667
 
1404
1668
  const shouldPreserveJsAlias =
1405
1669
  preserveJsPathSet.has(path) || preserveJsPathAbsoluteSet.has(path)
@@ -1420,7 +1684,7 @@ async function esbuildWriteIfChanged(
1420
1684
  if (result.map && !shouldSkipSourceMaps) {
1421
1685
  await flush(newOutPath + '.map', JSON.stringify(result.map))
1422
1686
  } else {
1423
- await FSE.remove(newOutPath + '.map')
1687
+ remove(newOutPath + '.map')
1424
1688
  }
1425
1689
 
1426
1690
  if (shouldPreserveJsAlias) {
@@ -1434,7 +1698,7 @@ async function esbuildWriteIfChanged(
1434
1698
  if (result.map && !shouldSkipSourceMaps) {
1435
1699
  await flush(path + '.map', JSON.stringify(result.map))
1436
1700
  } else {
1437
- await FSE.remove(path + '.map')
1701
+ remove(path + '.map')
1438
1702
  }
1439
1703
  }
1440
1704
  })
@@ -1442,10 +1706,6 @@ async function esbuildWriteIfChanged(
1442
1706
 
1443
1707
  // remove intermediary .js files once the final .mjs/.cjs outputs exist
1444
1708
  if (cleanupNonMjsFiles.length || cleanupNonCjsFiles.length) {
1445
- await Promise.all(
1446
- [...cleanupNonMjsFiles, ...cleanupNonCjsFiles].map(async (file) => {
1447
- await FSE.remove(file)
1448
- })
1449
- )
1709
+ for (const file of [...cleanupNonMjsFiles, ...cleanupNonCjsFiles]) remove(file)
1450
1710
  }
1451
1711
  }