@@ -259,6 +259,65 @@ const staticContentTypes: Record<string, string> = {
259259 eot : 'application/vnd.ms-fontobject' ,
260260}
261261
262+ const bundledAssetExtensions = new Set ( [ 'ts' , 'tsx' , 'mts' , 'cts' ] )
263+
264+ function isBundledAssetExtension ( ext : string | undefined ) : ext is string {
265+ return Boolean ( ext && bundledAssetExtensions . has ( ext ) )
266+ }
267+
268+ function getAssetExtension ( pathname : string ) : string | undefined {
269+ return pathname . split ( '.' ) . pop ( ) ?. toLowerCase ( )
270+ }
271+
272+ function isSafeAssetPath ( pathname : string ) : boolean {
273+ if ( pathname . includes ( '\0' ) )
274+ return false
275+
276+ return ! pathname
277+ . split ( '/' )
278+ . some ( segment => segment === '..' )
279+ }
280+
281+ function assetRequestPaths ( pathname : string ) : string [ ] {
282+ const paths = [
283+ pathname ,
284+ pathname . replace ( / ^ \/ a s s e t s \/ / , '/resources/assets/' ) ,
285+ pathname . replace ( / ^ \/ r e s o u r c e s \/ a s s e t s \/ / , '/assets/' ) ,
286+ ]
287+
288+ return [ ...new Set ( paths ) ]
289+ }
290+
291+ export async function bundleBrowserAsset ( entrypoint : string ) : Promise < Response > {
292+ const result = await Bun . build ( {
293+ entrypoints : [ entrypoint ] ,
294+ format : 'esm' ,
295+ minify : false ,
296+ packages : 'bundle' ,
297+ sourcemap : 'inline' ,
298+ target : 'browser' ,
299+ } )
300+
301+ if ( ! result . success ) {
302+ const message = result . logs . map ( log => log . message ) . join ( '\n' ) || 'Unable to build TypeScript asset'
303+
304+ return new Response ( message , {
305+ status : 500 ,
306+ headers : {
307+ 'Content-Type' : 'text/plain; charset=utf-8' ,
308+ 'Cache-Control' : 'no-cache' ,
309+ } ,
310+ } )
311+ }
312+
313+ return new Response ( await result . outputs [ 0 ] . text ( ) , {
314+ headers : {
315+ 'Content-Type' : 'application/javascript' ,
316+ 'Cache-Control' : 'no-cache' ,
317+ } ,
318+ } )
319+ }
320+
262321/**
263322 * Start the STX development server
264323 * @param options Server options with patterns and port
@@ -410,7 +469,8 @@ export async function serve(options: ServeOptions): Promise<void> {
410469 // cached copy and re-fetches.
411470 const isCss = f . endsWith ( '.css' )
412471 const isAsset = isCss
413- || f . endsWith ( '.js' ) || f . endsWith ( '.ts' )
472+ || f . endsWith ( '.js' ) || f . endsWith ( '.ts' ) || f . endsWith ( '.tsx' )
473+ || f . endsWith ( '.mts' ) || f . endsWith ( '.cts' )
414474 || f . endsWith ( '.jpg' ) || f . endsWith ( '.jpeg' ) || f . endsWith ( '.png' )
415475 || f . endsWith ( '.gif' ) || f . endsWith ( '.svg' ) || f . endsWith ( '.webp' ) || f . endsWith ( '.avif' )
416476 || f . endsWith ( '.woff' ) || f . endsWith ( '.woff2' ) || f . endsWith ( '.ttf' ) || f . endsWith ( '.otf' )
@@ -1945,37 +2005,28 @@ export async function serve(options: ServeOptions): Promise<void> {
19452005 if ( path . startsWith ( '/assets/' ) || path . startsWith ( '/resources/assets/' ) ) {
19462006 // Ensure assets are copied on first request
19472007 await ensureAssets ( )
1948- // Try multiple possible paths (like Laravel does)
1949- const possiblePaths = [
1950- path , // Original path
1951- path . replace ( / ^ \/ a s s e t s \/ / , '/resources/assets/' ) , // /assets/* -> /resources/assets/*
1952- path . replace ( / ^ \/ r e s o u r c e s \/ a s s e t s \/ / , '/assets/' ) , // /resources/assets/* -> /assets/*
1953- ]
1954-
1955- for ( const assetPath of possiblePaths ) {
2008+ let assetPathname : string
2009+ try {
2010+ assetPathname = decodeURIComponent ( path )
2011+ }
2012+ catch {
2013+ return new Response ( 'Invalid asset path' , { status : 400 } )
2014+ }
2015+
2016+ if ( ! isSafeAssetPath ( assetPathname ) )
2017+ return new Response ( 'Invalid asset path' , { status : 400 } )
2018+
2019+ for ( const assetPath of assetRequestPaths ( assetPathname ) ) {
19562020 try {
1957- const filePath = `.${ assetPath } `
2021+ const filePath = nodePath . resolve ( process . cwd ( ) , `.${ assetPath } ` )
19582022 const file = Bun . file ( filePath )
19592023
19602024 if ( await file . exists ( ) ) {
19612025 // Determine content type based on file extension
1962- const ext = assetPath . split ( '.' ) . pop ( ) ?. toLowerCase ( )
1963-
1964- // Handle TypeScript files - transpile to JavaScript
1965- if ( ext === 'ts' ) {
1966- const transpiler = new Bun . Transpiler ( {
1967- loader : 'ts' ,
1968- } )
1969- const code = await file . text ( )
1970- const transpiled = transpiler . transformSync ( code )
1971-
1972- return new Response ( transpiled , {
1973- headers : {
1974- 'Content-Type' : 'application/javascript' ,
1975- 'Cache-Control' : 'no-cache' , // Dev mode - no caching for TS
1976- } ,
1977- } )
1978- }
2026+ const ext = getAssetExtension ( assetPath )
2027+
2028+ if ( isBundledAssetExtension ( ext ) )
2029+ return bundleBrowserAsset ( filePath )
19792030
19802031 return new Response ( file , {
19812032 headers : {
0 commit comments