11#!/usr/bin/env bun
2- /**
3- * Bundle script for npm package
4- *
5- * Creates a single-file Node.js bundle using esbuild.
6- * Injects Bun polyfills for Node.js compatibility.
7- * Uploads source maps to Sentry when SENTRY_AUTH_TOKEN is available.
8- *
9- * Usage:
10- * bun run script/bundle.ts
11- *
12- * Output:
13- * dist/bin.cjs - Minified, single-file bundle for npm
14- */
15- import { sentryEsbuildPlugin } from "@sentry/esbuild-plugin" ;
2+ import { unlink } from "node:fs/promises" ;
163import { build , type Plugin } from "esbuild" ;
174import pkg from "../package.json" ;
5+ import { uploadSourcemaps } from "../src/lib/api/sourcemaps.js" ;
6+ import { injectDebugId } from "./debug-id.js" ;
187
198const VERSION = pkg . version ;
209const SENTRY_CLIENT_ID = process . env . SENTRY_CLIENT_ID ?? "" ;
@@ -33,17 +22,15 @@ if (!SENTRY_CLIENT_ID) {
3322const BUN_SQLITE_FILTER = / ^ b u n : s q l i t e $ / ;
3423const ANY_FILTER = / .* / ;
3524
36- // Plugin to replace bun:sqlite with our node:sqlite polyfill
25+ /** Plugin to replace bun:sqlite with our node:sqlite polyfill. */
3726const bunSqlitePlugin : Plugin = {
3827 name : "bun-sqlite-polyfill" ,
3928 setup ( pluginBuild ) {
40- // Intercept imports of "bun:sqlite" and redirect to our polyfill
4129 pluginBuild . onResolve ( { filter : BUN_SQLITE_FILTER } , ( ) => ( {
4230 path : "bun:sqlite" ,
4331 namespace : "bun-sqlite-polyfill" ,
4432 } ) ) ;
4533
46- // Provide the polyfill content
4734 pluginBuild . onLoad (
4835 { filter : ANY_FILTER , namespace : "bun-sqlite-polyfill" } ,
4936 ( ) => ( {
@@ -59,30 +46,135 @@ const bunSqlitePlugin: Plugin = {
5946 } ,
6047} ;
6148
62- // Configure Sentry plugin for source map uploads (production builds only)
63- const plugins : Plugin [ ] = [ bunSqlitePlugin ] ;
49+ type InjectedFile = { jsPath : string ; mapPath : string ; debugId : string } ;
6450
65- if ( process . env . SENTRY_AUTH_TOKEN ) {
66- console . log ( " Sentry auth token found, source maps will be uploaded" ) ;
67- plugins . push (
68- sentryEsbuildPlugin ( {
51+ /** Delete .map files after a successful upload — they shouldn't ship to users. */
52+ async function deleteMapFiles ( injected : InjectedFile [ ] ) : Promise < void > {
53+ for ( const { mapPath } of injected ) {
54+ try {
55+ await unlink ( mapPath ) ;
56+ } catch {
57+ // Ignore — file might already be gone
58+ }
59+ }
60+ }
61+
62+ /** Inject debug IDs into JS outputs and their companion sourcemaps. */
63+ async function injectDebugIdsForOutputs (
64+ jsFiles : string [ ]
65+ ) : Promise < InjectedFile [ ] > {
66+ const injected : InjectedFile [ ] = [ ] ;
67+ for ( const jsPath of jsFiles ) {
68+ const mapPath = `${ jsPath } .map` ;
69+ try {
70+ const { debugId } = await injectDebugId ( jsPath , mapPath ) ;
71+ injected . push ( { jsPath, mapPath, debugId } ) ;
72+ console . log ( ` Debug ID injected: ${ debugId } ` ) ;
73+ } catch ( err ) {
74+ const msg = err instanceof Error ? err . message : String ( err ) ;
75+ console . warn (
76+ ` Warning: Debug ID injection failed for ${ jsPath } : ${ msg } `
77+ ) ;
78+ }
79+ }
80+ return injected ;
81+ }
82+
83+ /**
84+ * Upload injected sourcemaps to Sentry via the chunk-upload protocol.
85+ *
86+ * @returns `true` if upload succeeded, `false` if it failed (non-fatal).
87+ */
88+ async function uploadInjectedSourcemaps (
89+ injected : InjectedFile [ ]
90+ ) : Promise < boolean > {
91+ try {
92+ console . log ( " Uploading sourcemaps to Sentry..." ) ;
93+ await uploadSourcemaps ( {
6994 org : "sentry" ,
7095 project : "cli" ,
71- authToken : process . env . SENTRY_AUTH_TOKEN ,
72- release : {
73- name : VERSION ,
74- } ,
75- sourcemaps : {
76- filesToDeleteAfterUpload : [ "dist/**/*.map" ] ,
77- } ,
78- // Don't fail the build if source map upload fails
79- errorHandler : ( err ) => {
80- console . warn ( " Warning: Source map upload failed:" , err . message ) ;
81- } ,
82- } )
83- ) ;
96+ release : VERSION ,
97+ files : injected . flatMap ( ( { jsPath, mapPath, debugId } ) => {
98+ const jsName = jsPath . split ( "/" ) . pop ( ) ?? "bin.cjs" ;
99+ const mapName = mapPath . split ( "/" ) . pop ( ) ?? "bin.cjs.map" ;
100+ return [
101+ {
102+ path : jsPath ,
103+ debugId,
104+ type : "minified_source" as const ,
105+ url : `~/${ jsName } ` ,
106+ sourcemapFilename : mapName ,
107+ } ,
108+ {
109+ path : mapPath ,
110+ debugId,
111+ type : "source_map" as const ,
112+ url : `~/${ mapName } ` ,
113+ } ,
114+ ] ;
115+ } ) ,
116+ } ) ;
117+ console . log ( " Sourcemaps uploaded to Sentry" ) ;
118+ return true ;
119+ } catch ( err ) {
120+ const msg = err instanceof Error ? err . message : String ( err ) ;
121+ console . warn ( ` Warning: Sourcemap upload failed: ${ msg } ` ) ;
122+ return false ;
123+ }
124+ }
125+
126+ /**
127+ * esbuild plugin that injects debug IDs and uploads sourcemaps to Sentry.
128+ *
129+ * Runs after esbuild finishes bundling (onEnd hook):
130+ * 1. Injects debug IDs into each JS output + its companion .map
131+ * 2. Uploads all artifacts to Sentry via the chunk-upload protocol
132+ * 3. Deletes .map files after upload (they shouldn't ship to users)
133+ *
134+ * Replaces `@sentry/esbuild-plugin` with zero external dependencies.
135+ */
136+ const sentrySourcemapPlugin : Plugin = {
137+ name : "sentry-sourcemap" ,
138+ setup ( pluginBuild ) {
139+ pluginBuild . onEnd ( async ( buildResult ) => {
140+ const outputs = Object . keys ( buildResult . metafile ?. outputs ?? { } ) ;
141+ const jsFiles = outputs . filter (
142+ ( p ) => p . endsWith ( ".cjs" ) || ( p . endsWith ( ".js" ) && ! p . endsWith ( ".map" ) )
143+ ) ;
144+
145+ if ( jsFiles . length === 0 ) {
146+ return ;
147+ }
148+
149+ const injected = await injectDebugIdsForOutputs ( jsFiles ) ;
150+ if ( injected . length === 0 ) {
151+ return ;
152+ }
153+
154+ if ( ! process . env . SENTRY_AUTH_TOKEN ) {
155+ return ;
156+ }
157+
158+ const uploaded = await uploadInjectedSourcemaps ( injected ) ;
159+
160+ // Only delete .map files after a successful upload — preserving
161+ // them on failure allows retrying without a full rebuild.
162+ if ( uploaded ) {
163+ await deleteMapFiles ( injected ) ;
164+ }
165+ } ) ;
166+ } ,
167+ } ;
168+
169+ // Always inject debug IDs (even without auth token); upload is gated inside the plugin
170+ const plugins : Plugin [ ] = [ bunSqlitePlugin , sentrySourcemapPlugin ] ;
171+
172+ if ( process . env . SENTRY_AUTH_TOKEN ) {
173+ console . log ( " Sentry auth token found, source maps will be uploaded" ) ;
84174} else {
85- console . log ( " No SENTRY_AUTH_TOKEN, skipping source map upload" ) ;
175+ console . log (
176+ " No SENTRY_AUTH_TOKEN, debug IDs will be injected but source maps will not be uploaded"
177+ ) ;
86178}
87179
88180const result = await build ( {
0 commit comments