@@ -20,8 +20,10 @@ import { setActiveRenderer } from './render/active.ts'
2020import { serveCompatibility } from './server/compatibility.ts'
2121import { serveLint } from './server/linter.ts'
2222import { sendEmail } from './server/email.ts'
23+ import { serveStaticFile } from './server/static.ts'
2324import { normalizeComponentSources } from './utils/componentSources.ts'
2425import { createWatchedFileMatcher , deriveWatchRoots } from './utils/watchPaths.ts'
26+ import { staticBase } from './utils/staticPaths.ts'
2527import type { MaizzleConfig } from './types/index.ts'
2628
2729const __dirname = dirname ( fileURLToPath ( import . meta. url ) )
@@ -262,6 +264,34 @@ function maizzleDevPlugin(
262264
263265 let isWatchedFile = applyWatchPaths ( config )
264266
267+ /**
268+ * Serve the `static.source` directories at `/<static.destination>`,
269+ * mirroring what `build` copies to the output dir, so templates that
270+ * reference `/images/logo.png` resolve in the preview iframe. Each
271+ * positive pattern's glob-free prefix is mounted (looked up on disk
272+ * per request, so files added after startup are served) and added
273+ * to the watcher so the preview refreshes on changes. Runs again
274+ * after a config reload; the middleware below reads the current
275+ * mounts on every request.
276+ */
277+ const applyStaticMounts = ( cfg : MaizzleConfig ) => {
278+ const sources = cfg . static ?. source ?? [ 'public/**/*.*' ]
279+ const destination = ( cfg . static ?. destination ?? 'public' ) . replace ( / ^ \/ + | \/ + $ / g, '' )
280+ const prefix = destination ? `/${ destination } ` : ''
281+ const bases = [ ...new Set ( sources . filter ( s => ! s . startsWith ( '!' ) ) . map ( staticBase ) ) ]
282+
283+ for ( const base of bases ) {
284+ server . watcher . add ( base )
285+ }
286+
287+ return {
288+ mounts : bases . map ( base => ( { prefix, base } ) ) ,
289+ isStaticFile : createWatchedFileMatcher ( sources , process . cwd ( ) ) ,
290+ }
291+ }
292+
293+ let { mounts : staticMounts , isStaticFile } = applyStaticMounts ( config )
294+
265295 /**
266296 * Serialize watcher work onto one chain. The change handler closes and
267297 * recreates the renderer across awaits; without serialization a second
@@ -281,6 +311,8 @@ function maizzleDevPlugin(
281311 await renderer . invalidateAll ( )
282312 bumpGeneration ( )
283313 server . ws . send ( { type : 'custom' , event : 'maizzle:templates-changed' } )
314+ } else if ( isStaticFile ( file ) ) {
315+ server . ws . send ( { type : 'custom' , event : 'maizzle:template-updated' , data : { file } } )
284316 }
285317 } ) )
286318
@@ -289,6 +321,8 @@ function maizzleDevPlugin(
289321 await renderer . invalidateAll ( )
290322 bumpGeneration ( )
291323 server . ws . send ( { type : 'custom' , event : 'maizzle:templates-changed' } )
324+ } else if ( isStaticFile ( file ) ) {
325+ server . ws . send ( { type : 'custom' , event : 'maizzle:template-updated' , data : { file } } )
292326 }
293327 } ) )
294328
@@ -313,6 +347,7 @@ function maizzleDevPlugin(
313347 // content, components.source, root, or server.watch values keep
314348 // emitting events without a server restart.
315349 isWatchedFile = applyWatchPaths ( config )
350+ ; ( { mounts : staticMounts , isStaticFile } = applyStaticMounts ( config ) )
316351
317352 /**
318353 * Push UI-relevant config bits so the dev UI reacts to live edits
@@ -333,6 +368,7 @@ function maizzleDevPlugin(
333368 if (
334369 isTemplateFile ( file )
335370 || isWatchedFile ( file )
371+ || isStaticFile ( file )
336372 ) {
337373 server . ws . send ( { type : 'custom' , event : 'maizzle:template-updated' , data : { file } } )
338374 }
@@ -385,6 +421,19 @@ function maizzleDevPlugin(
385421 next ( )
386422 } )
387423
424+ // Static assets from `static.source`, mounted at `/<static.destination>`
425+ server . middlewares . use ( ( req : any , res : any , next : any ) => {
426+ const url : string = req . url || '/'
427+
428+ for ( const { prefix, base } of staticMounts ) {
429+ if ( url === prefix || url . startsWith ( `${ prefix } /` ) || url . startsWith ( `${ prefix } ?` ) ) {
430+ if ( serveStaticFile ( base , url . slice ( prefix . length ) || '/' , res ) ) return
431+ }
432+ }
433+
434+ next ( )
435+ } )
436+
388437 // Dev UI fallback (after Vite's middleware)
389438 return ( ) => {
390439 server . middlewares . use ( async ( req : any , res : any , next : any ) => {
0 commit comments