@@ -77,7 +77,10 @@ export type SentryBuildPluginManager = {
77
77
/**
78
78
* Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded
79
79
*/
80
- uploadSourcemaps ( buildArtifactPaths : string [ ] ) : Promise < void > ;
80
+ uploadSourcemaps (
81
+ buildArtifactPaths : string [ ] ,
82
+ opts ?: { prepareArtifacts ?: boolean }
83
+ ) : Promise < void > ;
81
84
82
85
/**
83
86
* Will delete artifacts based on the passed `sourcemaps.filesToDeleteAfterUpload` option.
@@ -546,9 +549,16 @@ export function createSentryBuildPluginManager(
546
549
} ,
547
550
548
551
/**
549
- * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded
552
+ * Uploads sourcemaps using the "Debug ID" method.
553
+ *
554
+ * By default, this prepares bundles in a temporary folder before uploading. You can opt into an
555
+ * in-place, direct upload path by setting `prepareArtifacts` to `false`. If `prepareArtifacts` is set to
556
+ * `false`, no preparation (e.g. adding `//# debugId=...` and writing adjusted source maps) is performed and no temp folder is used.
557
+ *
558
+ * @param buildArtifactPaths - The paths of the build artifacts to upload
559
+ * @param opts - Optional flags to control temp folder usage and preparation
550
560
*/
551
- async uploadSourcemaps ( buildArtifactPaths : string [ ] ) {
561
+ async uploadSourcemaps ( buildArtifactPaths : string [ ] , opts ?: { prepareArtifacts ?: boolean } ) {
552
562
if ( ! canUploadSourceMaps ( options , logger , isDevMode ) ) {
553
563
return ;
554
564
}
@@ -557,26 +567,16 @@ export function createSentryBuildPluginManager(
557
567
// This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
558
568
{ name : "debug-id-sourcemap-upload" , scope : sentryScope , forceTransaction : true } ,
559
569
async ( ) => {
570
+ // If we're not using a temp folder, we must not prepare artifacts in-place (to avoid mutating user files)
571
+ const shouldPrepare = opts ?. prepareArtifacts ?? true ;
572
+
560
573
let folderToCleanUp : string | undefined ;
561
574
562
575
// It is possible that this writeBundle hook (which calls this function) is called multiple times in one build (for example when reusing the plugin, or when using build tooling like `@vitejs/plugin-legacy`)
563
576
// Therefore we need to actually register the execution of this hook as dependency on the sourcemap files.
564
577
const freeUploadDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts ( ) ;
565
578
566
579
try {
567
- const tmpUploadFolder = await startSpan (
568
- { name : "mkdtemp" , scope : sentryScope } ,
569
- async ( ) => {
570
- return (
571
- process . env ?. [ "SENTRY_TEST_OVERRIDE_TEMP_DIR" ] ||
572
- ( await fs . promises . mkdtemp (
573
- path . join ( os . tmpdir ( ) , "sentry-bundler-plugin-upload-" )
574
- ) )
575
- ) ;
576
- }
577
- ) ;
578
-
579
- folderToCleanUp = tmpUploadFolder ;
580
580
const assets = options . sourcemaps ?. assets ;
581
581
582
582
let globAssets : string | string [ ] ;
@@ -594,14 +594,17 @@ export function createSentryBuildPluginManager(
594
594
async ( ) =>
595
595
await glob ( globAssets , {
596
596
absolute : true ,
597
- nodir : true ,
597
+ // If we do not use a temp folder, we allow directories and files; CLI will traverse as needed when given paths.
598
+ nodir : shouldPrepare ,
598
599
ignore : options . sourcemaps ?. ignore ,
599
600
} )
600
601
) ;
601
602
602
- const debugIdChunkFilePaths = globResult . filter ( ( debugIdChunkFilePath ) => {
603
- return ! ! stripQueryAndHashFromPath ( debugIdChunkFilePath ) . match ( / \. ( j s | m j s | c j s ) $ / ) ;
604
- } ) ;
603
+ const debugIdChunkFilePaths = shouldPrepare
604
+ ? globResult . filter ( ( debugIdChunkFilePath ) => {
605
+ return ! ! stripQueryAndHashFromPath ( debugIdChunkFilePath ) . match ( / \. ( j s | m j s | c j s ) $ / ) ;
606
+ } )
607
+ : globResult ;
605
608
606
609
// The order of the files output by glob() is not deterministic
607
610
// Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
@@ -616,75 +619,101 @@ export function createSentryBuildPluginManager(
616
619
"Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
617
620
) ;
618
621
} else {
619
- const numUploadedFiles = await startSpan (
620
- { name : "prepare-bundles" , scope : sentryScope } ,
621
- async ( prepBundlesSpan ) => {
622
- // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
623
- // instead we do it with a maximum of 16 concurrent workers
624
- const preparationTasks = debugIdChunkFilePaths . map (
625
- ( chunkFilePath , chunkIndex ) => async ( ) => {
626
- await prepareBundleForDebugIdUpload (
627
- chunkFilePath ,
628
- tmpUploadFolder ,
629
- chunkIndex ,
630
- logger ,
631
- options . sourcemaps ?. rewriteSources ?? defaultRewriteSourcesHook ,
632
- options . sourcemaps ?. resolveSourceMap
633
- ) ;
634
- }
635
- ) ;
636
- const workers : Promise < void > [ ] = [ ] ;
637
- const worker = async ( ) : Promise < void > => {
638
- while ( preparationTasks . length > 0 ) {
639
- const task = preparationTasks . shift ( ) ;
640
- if ( task ) {
641
- await task ( ) ;
622
+ if ( ! shouldPrepare ) {
623
+ // Direct CLI upload from existing artifact paths (no preparation or temp copies)
624
+ await startSpan ( { name : "upload" , scope : sentryScope } , async ( ) => {
625
+ const cliInstance = createCliInstance ( options ) ;
626
+ await cliInstance . releases . uploadSourceMaps ( options . release . name ?? "undefined" , {
627
+ include : [
628
+ {
629
+ paths : debugIdChunkFilePaths ,
630
+ rewrite : false ,
631
+ dist : options . release . dist ,
632
+ } ,
633
+ ] ,
634
+ live : "rejectOnError" ,
635
+ } ) ;
636
+ } ) ;
637
+
638
+ logger . info ( "Successfully uploaded source maps to Sentry" ) ;
639
+ } else {
640
+ const tmpUploadFolder = await startSpan (
641
+ { name : "mkdtemp" , scope : sentryScope } ,
642
+ async ( ) => {
643
+ return (
644
+ process . env ?. [ "SENTRY_TEST_OVERRIDE_TEMP_DIR" ] ||
645
+ ( await fs . promises . mkdtemp (
646
+ path . join ( os . tmpdir ( ) , "sentry-bundler-plugin-upload-" )
647
+ ) )
648
+ ) ;
649
+ }
650
+ ) ;
651
+ folderToCleanUp = tmpUploadFolder ;
652
+
653
+ // Prepare into temp folder, then upload
654
+ await startSpan (
655
+ { name : "prepare-bundles" , scope : sentryScope } ,
656
+ async ( prepBundlesSpan ) => {
657
+ // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
658
+ // instead we do it with a maximum of 16 concurrent workers
659
+ const preparationTasks = debugIdChunkFilePaths . map (
660
+ ( chunkFilePath , chunkIndex ) => async ( ) => {
661
+ await prepareBundleForDebugIdUpload (
662
+ chunkFilePath ,
663
+ tmpUploadFolder ,
664
+ chunkIndex ,
665
+ logger ,
666
+ options . sourcemaps ?. rewriteSources ?? defaultRewriteSourcesHook ,
667
+ options . sourcemaps ?. resolveSourceMap
668
+ ) ;
669
+ }
670
+ ) ;
671
+ const workers : Promise < void > [ ] = [ ] ;
672
+ const worker = async ( ) : Promise < void > => {
673
+ while ( preparationTasks . length > 0 ) {
674
+ const task = preparationTasks . shift ( ) ;
675
+ if ( task ) {
676
+ await task ( ) ;
677
+ }
642
678
}
679
+ } ;
680
+ for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
681
+ workers . push ( worker ( ) ) ;
643
682
}
644
- } ;
645
- for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
646
- workers . push ( worker ( ) ) ;
647
- }
648
683
649
- await Promise . all ( workers ) ;
684
+ await Promise . all ( workers ) ;
650
685
651
- const files = await fs . promises . readdir ( tmpUploadFolder ) ;
652
- const stats = files . map ( ( file ) =>
653
- fs . promises . stat ( path . join ( tmpUploadFolder , file ) )
654
- ) ;
655
- const uploadSize = ( await Promise . all ( stats ) ) . reduce (
656
- ( accumulator , { size } ) => accumulator + size ,
657
- 0
658
- ) ;
659
-
660
- setMeasurement ( "files" , files . length , "none" , prepBundlesSpan ) ;
661
- setMeasurement ( "upload_size" , uploadSize , "byte" , prepBundlesSpan ) ;
662
-
663
- await startSpan ( { name : "upload" , scope : sentryScope } , async ( ) => {
664
- const cliInstance = createCliInstance ( options ) ;
665
-
666
- await cliInstance . releases . uploadSourceMaps (
667
- options . release . name ?? "undefined" , // unfortunately this needs a value for now but it will not matter since debug IDs overpower releases anyhow
668
- {
669
- include : [
670
- {
671
- paths : [ tmpUploadFolder ] ,
672
- rewrite : false ,
673
- dist : options . release . dist ,
674
- } ,
675
- ] ,
676
- // We want this promise to throw if the sourcemaps fail to upload so that we know about it.
677
- // see: https://github.com/getsentry/sentry-cli/pull/2605
678
- live : "rejectOnError" ,
679
- }
686
+ const files = await fs . promises . readdir ( tmpUploadFolder ) ;
687
+ const stats = files . map ( ( file ) =>
688
+ fs . promises . stat ( path . join ( tmpUploadFolder , file ) )
689
+ ) ;
690
+ const uploadSize = ( await Promise . all ( stats ) ) . reduce (
691
+ ( accumulator , { size } ) => accumulator + size ,
692
+ 0
680
693
) ;
681
- } ) ;
682
694
683
- return files . length ;
684
- }
685
- ) ;
695
+ setMeasurement ( "files" , files . length , "none" , prepBundlesSpan ) ;
696
+ setMeasurement ( "upload_size" , uploadSize , "byte" , prepBundlesSpan ) ;
697
+
698
+ await startSpan ( { name : "upload" , scope : sentryScope } , async ( ) => {
699
+ const cliInstance = createCliInstance ( options ) ;
700
+ await cliInstance . releases . uploadSourceMaps (
701
+ options . release . name ?? "undefined" ,
702
+ {
703
+ include : [
704
+ {
705
+ paths : [ tmpUploadFolder ] ,
706
+ rewrite : false ,
707
+ dist : options . release . dist ,
708
+ } ,
709
+ ] ,
710
+ live : "rejectOnError" ,
711
+ }
712
+ ) ;
713
+ } ) ;
714
+ }
715
+ ) ;
686
716
687
- if ( numUploadedFiles > 0 ) {
688
717
logger . info ( "Successfully uploaded source maps to Sentry" ) ;
689
718
}
690
719
}
0 commit comments