@@ -19,12 +19,17 @@ import Semaphore from "./pages/node/semaphore/Semaphore.ts";
1919
2020const th = ( msg : string ) => new Error ( `es.ts error: ${ msg } ` ) ;
2121
22- interface Config extends Pick < esbuild . BuildOptions , "target" | "charset" | "minify" | "bundle" | "format" > {
22+ interface InternalConfig {
23+ target : string ;
2324 loader : esbuild . Loader ;
25+ format : "esm" | "cjs" | "iife" ;
26+ charset : "utf8" | "ascii" ;
27+ minify : boolean ;
28+ bundle : boolean ;
2429 extension : string ;
2530}
2631
27- const CONFIG : Config = {
32+ const CONFIG : InternalConfig = {
2833 target : "esnext" ,
2934 loader : "ts" ,
3035 format : "esm" ,
@@ -92,8 +97,7 @@ async function stripTypes(filePath: string): Promise<string | undefined> {
9297 const startMarker = "/** @es.ts" ;
9398 const endMarker = `@es.ts *${ SLASH } ` ;
9499
95- let buildMode : "bundle" | "transform" = CONFIG . bundle ? "bundle" : "transform" ;
96- let localOptions : any = { ...CONFIG } ;
100+ let config : any = { } ;
97101
98102 const startIndex = source . indexOf ( startMarker ) ;
99103 const endIndex = source . indexOf ( endMarker ) ;
@@ -102,40 +106,53 @@ async function stripTypes(filePath: string): Promise<string | undefined> {
102106 const configStr = source . substring ( startIndex + startMarker . length , endIndex ) . trim ( ) ;
103107 try {
104108 // Using Function to safely parse the object literal (allows unquoted keys)
105- const config = new Function ( `return (${ configStr } )` ) ( ) ;
106- if ( config . mode ) {
107- if ( config . mode !== "bundle" && config . mode !== "transform" ) {
108- throw th ( `Invalid mode "${ config . mode } " in ${ filePath } . Only "bundle" or "transform" are allowed.` ) ;
109- }
110- buildMode = config . mode ;
111- }
112- if ( config . extension ) {
113- localOptions . extension = config . extension ;
114- }
115- if ( config . options ) {
116- localOptions = { ...localOptions , ...config . options } ;
117- }
109+ config = new Function ( `return (${ configStr } )` ) ( ) ;
118110 } catch ( e : any ) {
119- if ( e . message . includes ( 'Invalid mode "' ) ) {
120- throw e ;
121- }
122111 console . error ( `Error parsing @es.ts config in ${ filePath } : ${ e . message } ` ) ;
123112 }
124113 }
125114
126- const outPath : string = join ( dirname ( filePath ) , basename ( filePath ) . replace ( / \. t s $ / , localOptions . extension ) ) ;
115+ const buildMode = config . mode || ( CONFIG . bundle ? "bundle" : "transform" ) ;
116+ const extension = config . extension || CONFIG . extension ;
117+ const loader = config . loader || CONFIG . loader ;
127118
128- const options : esbuild . BuildOptions = {
129- entryPoints : [ filePath ] ,
119+ // 1. Prepare base options from defaults
120+ let baseOptions : esbuild . BuildOptions = {
121+ target : CONFIG . target as any ,
122+ charset : CONFIG . charset ,
123+ minify : CONFIG . minify ,
130124 bundle : buildMode === "bundle" ,
131- write : false ,
132- target : localOptions . target as any ,
133- charset : localOptions . charset ,
134- minify : localOptions . minify ,
135- legalComments : "inline" ,
125+ format : CONFIG . format ,
136126 platform : "node" ,
137- format : localOptions . format ,
127+ legalComments : "inline" ,
128+ } ;
129+
130+ // 2. If 'setup' is provided, it replaces the base options
131+ if ( config . setup ) {
132+ baseOptions = { ...config . setup } ;
133+ }
134+
135+ // 3. Merge top-level config fields (excluding tool-specific ones)
136+ const { mode, extension : _ext , setup, options, ...rest } = config ;
137+ const mergedOptions : any = {
138+ ...baseOptions ,
139+ ...rest ,
140+ } ;
141+
142+ // Allow unsetting defaults by passing undefined
143+ Object . keys ( mergedOptions ) . forEach ( ( key ) => {
144+ if ( mergedOptions [ key ] === undefined ) {
145+ delete mergedOptions [ key ] ;
146+ }
147+ } ) ;
148+
149+ // 4. Force essential options and inject plugin
150+ const finalOptions : esbuild . BuildOptions = {
151+ ...mergedOptions ,
152+ entryPoints : [ filePath ] ,
153+ write : false ,
138154 plugins : [
155+ ...( mergedOptions . plugins || [ ] ) ,
139156 {
140157 name : "protect-comments" ,
141158 setup ( build ) {
@@ -144,35 +161,49 @@ async function stripTypes(filePath: string): Promise<string | undefined> {
144161 const contents = content
145162 . replace ( / \/ \* \* / g, "/*!" ) // JSDoc -> Legal block
146163 . replace ( / \/ \/ / g, "//! " ) ; // Single line -> Legal line
147- return { contents, loader : localOptions . loader } ;
164+ return { contents, loader } ;
148165 } ) ;
149166 } ,
150167 } ,
151168 ] ,
152169 } ;
153170
171+ // 5. Handle output path defaulting if not specified
172+ if ( ! finalOptions . outfile && ! finalOptions . outdir ) {
173+ finalOptions . outfile = join ( dirname ( filePath ) , basename ( filePath ) . replace ( / \. t s $ / , extension ) ) ;
174+ }
175+
154176 if ( env . DEBUG ) {
155- console . log ( JSON . stringify ( options , null , 2 ) ) ;
177+ console . log ( `Final esbuild options for ${ filePath } :` ) ;
178+ console . log ( JSON . stringify ( finalOptions , null , 2 ) ) ;
156179 }
157180
158- const result = await esbuild . build ( options ) ;
181+ const result = await esbuild . build ( finalOptions ) ;
182+
183+ let firstOutPath : string | undefined ;
159184
160- if ( result . outputFiles && result . outputFiles [ 0 ] ) {
161- let outputText : string = result . outputFiles [ 0 ] . text ;
185+ if ( result . outputFiles ) {
186+ for ( const file of result . outputFiles ) {
187+ let outputText : string = file . text ;
162188
163- // Restore protected comments
164- outputText = outputText
165- . replace ( / \/ \* \! / g, "/**" )
166- . replace ( / \/ \/ ! / g, "// " )
167- . replace ( / ( @ e s \. t s \* \/ \s * ) / g, `@es.ts *${ SLASH } ` ) ;
189+ // Restore protected comments
190+ outputText = outputText
191+ . replace ( / \/ \* \! / g, "/**" )
192+ . replace ( / \/ \/ ! / g, "// " )
193+ . replace ( / ( @ e s \. t s \* \/ \s * ) / g, `@es.ts *${ SLASH } ` ) ;
168194
169- writeFileSync ( outPath , outputText ) ;
195+ writeFileSync ( file . path , outputText ) ;
196+
197+ if ( ! firstOutPath ) {
198+ firstOutPath = file . path ;
199+ }
200+ }
170201 }
171202
172- if ( ! PRODUCE_GITIGNORE ) {
173- console . log ( `${ buildMode === "bundle" ? "Bundled" : "Transpiled" } (esbuild): ${ filePath } -> ${ outPath } ` ) ;
203+ if ( ! PRODUCE_GITIGNORE && firstOutPath ) {
204+ console . log ( `${ buildMode === "bundle" ? "Bundled" : "Transpiled" } (esbuild): ${ filePath } -> ${ firstOutPath } ` ) ;
174205 }
175- return outPath ;
206+ return firstOutPath ;
176207 } catch ( err : unknown ) {
177208 hasError = true ;
178209 const message = err instanceof Error ? err . message : String ( err ) ;
@@ -267,12 +298,19 @@ Description:
267298 Add this block to a .ts file to override default behavior:
268299/** @es.ts
269300{
270- mode: "bundle|transform",
271- extension: ".js|.mjs",
272- options: {
273- target: "esnext", loader: "ts",
274- charset: "utf8", minify: false
275- }
301+ // Tool-specific keys:
302+ mode: "bundle|transform", // optional, shorthand for bundle: true/false
303+ extension: ".js|.mjs", // optional, default output extension
304+ setup: { ... }, // optional, if present replaces all default esbuild options
305+
306+ // Any other keys are treated as esbuild BuildOptions and merged with defaults:
307+ target: "esnext",
308+ minify: false,
309+ platform: "node",
310+ // ...
311+
312+ // Use 'undefined' to unset a default and let esbuild decide:
313+ minify: undefined,
276314}
277315@es.ts *${ SLASH }
278316
@@ -281,7 +319,7 @@ Description:
281319 to esbuild.transform (input and options) are dumped to the
282320 console for each processed file.
283321
284- Built-in Config:
322+ Built-in Config (generated internally - so it is true setup which will be really used) :
285323${ JSON . stringify ( CONFIG , null , 2 ) }
286324` ) ;
287325}
@@ -311,8 +349,9 @@ for await (const line of rl) {
311349 // Start the task and keep track of it in the activeTasks Set
312350 const task : Promise < void > = ( async ( ) => {
313351 try {
314- const outPath = await stripTypes ( file ) ;
352+ let outPath = await stripTypes ( file ) ;
315353 if ( PRODUCE_GITIGNORE && outPath ) {
354+ outPath = relative ( gitRoot , outPath ) ;
316355 gitignorePaths . push ( outPath ) ;
317356 }
318357 } finally {
0 commit comments