11import type { DashboardConfig } from './types'
22import path from 'node:path'
3- import { defaultConfig as stxDefaultConfig , generateSignalsRuntime , injectRouterScript , isSpaNavigation , processDirectives , stripDocumentWrapper } from '@stacksjs/stx'
3+ import { defaultConfig as stxDefaultConfig , isSpaNavigation , processDirectives , stripDocumentWrapper } from '@stacksjs/stx'
44import { BroadcastServer } from 'ts-broadcasting'
55import { createApiRoutes , fetchBatchById , fetchBatches , fetchDashboardStats , fetchDependencyGraph , fetchJobById , fetchJobGroups , fetchJobs , fetchMetrics , fetchQueueById , fetchQueues } from './api'
66import { resolveConfig } from './api'
@@ -12,36 +12,15 @@ export { createApiRoutes, fetchBatches, fetchDashboardStats, fetchDependencyGrap
1212const SRC_DIR = import . meta. dir
1313const PAGES_DIR = path . join ( SRC_DIR , 'pages' )
1414const FUNCTIONS_ENTRY = path . join ( SRC_DIR , 'functions' , 'browser.ts' )
15- const SHELL_PATH = path . join ( SRC_DIR , 'app.stx' )
1615
1716let broadcastServer : BroadcastServer | null = null
1817let bundledFunctionsJs : string | null = null
19- let cachedShell : { before : string , after : string , styles : string , scripts : string } | null = null
20- let cachedRouterScript : string | null = null
21-
22- function getRouterScriptTag ( ) : string {
23- if ( cachedRouterScript ) return cachedRouterScript
24- // Extract the router script by injecting into a minimal HTML doc
25- const minimal = '<!DOCTYPE html><html><head></head><body></body></html>'
26- const injected = injectRouterScript ( minimal )
27- const match = injected . match ( / < s c r i p t > [ \s \S ] * ?_ _ s t x R o u t e r [ \s \S ] * ?< \/ s c r i p t > / )
28- cachedRouterScript = match ? match [ 0 ] : ''
29- return cachedRouterScript
30- }
3118
32- const pageTitles : Record < string , string > = {
33- 'index' : 'bun-queue Dashboard' ,
34- 'monitoring' : 'Real-time Monitoring — bun-queue' ,
35- 'metrics' : 'Performance Metrics — bun-queue' ,
36- 'queues' : 'Queues — bun-queue' ,
37- 'queue-details' : 'Queue Details — bun-queue' ,
38- 'jobs' : 'Jobs — bun-queue' ,
39- 'job-details' : 'Job Details — bun-queue' ,
40- 'batches' : 'Batches — bun-queue' ,
41- 'batch-details' : 'Batch Details — bun-queue' ,
42- 'groups' : 'Job Groups — bun-queue' ,
43- 'group-details' : 'Group Details — bun-queue' ,
44- 'dependencies' : 'Job Dependencies — bun-queue' ,
19+ const stxConfig = {
20+ ...stxDefaultConfig ,
21+ componentsDir : path . join ( SRC_DIR , 'components' ) ,
22+ layoutsDir : path . join ( SRC_DIR , 'layouts' ) ,
23+ partialsDir : path . join ( SRC_DIR , 'partials' ) ,
4524}
4625
4726async function buildFunctionsBundle ( ) : Promise < string > {
@@ -64,87 +43,46 @@ async function buildFunctionsBundle(): Promise<string> {
6443 return bundledFunctionsJs
6544}
6645
67- const stxConfig = {
68- ...stxDefaultConfig ,
69- componentsDir : path . join ( SRC_DIR , 'components' ) ,
70- partialsDir : path . join ( SRC_DIR , 'partials' ) ,
71- }
72-
73- async function getShellParts ( ) : Promise < { before : string , after : string , styles : string , scripts : string , signalsRuntime : string } > {
74- if ( cachedShell ) return cachedShell
75-
76- const shellContent = await Bun . file ( SHELL_PATH ) . text ( )
77-
78- // Extract <template> block
79- const templateMatch = shellContent . match ( / < t e m p l a t e \b [ ^ > ] * > ( [ \s \S ] * ?) < \/ t e m p l a t e > / i)
80- let shellTemplate = templateMatch ? templateMatch [ 1 ] . trim ( ) : shellContent
81-
82- // Extract <style> blocks from the full file
83- const styles = ( shellContent . match ( / < s t y l e \b [ ^ > ] * > [ \s \S ] * ?< \/ s t y l e > / gi) || [ ] ) . join ( '\n' )
84-
85- // Extract <script client> blocks, process them through stx for TypeScript transpilation
86- const clientScriptMatches = shellContent . match ( / < s c r i p t \b [ ^ > ] * \b c l i e n t \b [ ^ > ] * > [ \s \S ] * ?< \/ s c r i p t > / gi) || [ ]
87- let scripts = ''
88- if ( clientScriptMatches . length > 0 ) {
89- const scriptHtml = clientScriptMatches . join ( '\n' )
90- const scriptContext = { __filename : SHELL_PATH , __dirname : path . dirname ( SHELL_PATH ) }
91- // Skip runtime here too — we extract it separately
92- scripts = await processDirectives ( scriptHtml , scriptContext , SHELL_PATH , { ...stxConfig , skipSignalsRuntime : true } , new Set ( ) )
93- scripts = stripDocumentWrapper ( scripts )
94- }
95-
96- // Remove scripts and styles from template for processing
97- shellTemplate = shellTemplate . replace ( / < s c r i p t \b [ ^ > ] * > [ \s \S ] * ?< \/ s c r i p t > / gi, '' )
98- shellTemplate = shellTemplate . replace ( / < s t y l e \b [ ^ > ] * > [ \s \S ] * ?< \/ s t y l e > / gi, '' )
99-
100- // Replace <slot /> with placeholder
101- const SLOT = '<!--__STX_SLOT__-->'
102- shellTemplate = shellTemplate . replace ( / < s l o t \s * \/ > / gi, SLOT ) . replace ( / < s l o t \s * > \s * < \/ s l o t > / gi, SLOT )
103-
104- // Process shell template WITHOUT signals runtime — we'll place it in <head> ourselves
105- const context = { __filename : SHELL_PATH , __dirname : path . dirname ( SHELL_PATH ) }
106- let processed = await processDirectives ( shellTemplate , context , SHELL_PATH , { ...stxConfig , skipSignalsRuntime : true } , new Set ( ) )
107- processed = stripDocumentWrapper ( processed )
108-
109- // Get the signals runtime directly
110- const signalsRuntime = `<script data-stx-scoped>${ generateSignalsRuntime ( ) } </script>`
111-
112- const slotIdx = processed . indexOf ( SLOT )
113- if ( slotIdx === - 1 ) {
114- // eslint-disable-next-line no-console
115- console . warn ( '[bq-devtools] Shell has no <slot /> — falling back' )
116- cachedShell = { before : '' , after : '' , styles, scripts, signalsRuntime }
117- return cachedShell
118- }
119-
120- cachedShell = {
121- before : processed . substring ( 0 , slotIdx ) ,
122- after : processed . substring ( slotIdx + SLOT . length ) ,
123- styles,
124- scripts,
125- signalsRuntime,
126- }
127- return cachedShell
128- }
129-
13046async function renderStxPage ( templateName : string , wsUrl : string , req : Request ) : Promise < Response > {
13147 const templatePath = path . join ( PAGES_DIR , `${ templateName } .stx` )
13248 const content = await Bun . file ( templatePath ) . text ( )
13349
134- const context = { __filename : templatePath , __dirname : path . dirname ( templatePath ) }
135- let pageHtml = await processDirectives ( content , context , templatePath , stxConfig , new Set ( ) )
136- pageHtml = stripDocumentWrapper ( pageHtml )
137-
138- // Strip the signals runtime from page fragment — the shell already provides it in <head>
139- pageHtml = pageHtml . replace ( / < s c r i p t d a t a - s t x - s c o p e d > \( f u n c t i o n \( \) \{ ' u s e s t r i c t ' ; v a r c l o a k S t y l e [ \s \S ] * ?< \/ s c r i p t > / , '' )
50+ const context : Record < string , any > = { __filename : templatePath , __dirname : path . dirname ( templatePath ) }
51+ let html = await processDirectives ( content , context , templatePath , stxConfig , new Set ( ) )
14052
141- // Extract the SFC setup function name so we can bind it to the content wrapper
142- const setupMatch = pageHtml . match ( / f u n c t i o n ( _ _ s t x _ s e t u p _ \w + ) / )
143- const pageSetupName = setupMatch ? setupMatch [ 1 ] : null
53+ // Inject WebSocket URL for real-time updates
54+ html = html . replace ( '</head>' , `<script>window.__BQ_WS_URL = "${ wsUrl } ";</script>\n</head>` )
14455
145- // SPA navigation — return fragment only
56+ // SPA navigation — extract <main> content as fragment
14657 if ( isSpaNavigation ( req ) ) {
147- return new Response ( pageHtml , {
58+ let fragment = ''
59+
60+ // Extract <head> styles (page-specific styles from @push('styles'))
61+ const headMatch = html . match ( / < h e a d \b [ ^ > ] * > ( [ \s \S ] * ?) < \/ h e a d > / i)
62+ if ( headMatch ) {
63+ const headContent = headMatch [ 1 ]
64+ const styleRegex = / < s t y l e \b [ ^ > ] * > [ \s \S ] * ?< \/ s t y l e > / gi
65+ let m : RegExpExecArray | null
66+ while ( ( m = styleRegex . exec ( headContent ) ) !== null ) {
67+ fragment += m [ 0 ] + '\n'
68+ }
69+ }
70+
71+ // Extract <main> inner content
72+ const mainOpenMatch = html . match ( / < m a i n \b [ ^ > ] * > / i)
73+ const mainCloseIdx = html . lastIndexOf ( '</main>' )
74+ if ( mainOpenMatch && mainCloseIdx !== - 1 ) {
75+ const mainStart = mainOpenMatch . index ! + mainOpenMatch [ 0 ] . length
76+ fragment += html . slice ( mainStart , mainCloseIdx )
77+ }
78+
79+ // Strip the signals runtime IIFE (shell already has it)
80+ fragment = fragment . replace (
81+ / < s c r i p t d a t a - s t x - s c o p e d > \s * ; ? \( f u n c t i o n \( \) \s * \{ [ \s \S ] * ?< \/ s c r i p t > / g,
82+ '' ,
83+ )
84+
85+ return new Response ( fragment , {
14886 headers : {
14987 'Content-Type' : 'text/html' ,
15088 'Cache-Control' : 'no-store' ,
@@ -153,30 +91,6 @@ async function renderStxPage(templateName: string, wsUrl: string, req: Request):
15391 } )
15492 }
15593
156- // Full page request — compose with shell
157- const shell = await getShellParts ( )
158- const title = pageTitles [ templateName ] || 'bun-queue'
159-
160- const html = `<!DOCTYPE html>
161- <html lang="en">
162- <head>
163- <meta charset="UTF-8">
164- <meta name="viewport" content="width=device-width, initial-scale=1.0">
165- <title>${ title } </title>
166- <script src="/bq-utils.js"><\/script>
167- <script>window.__BQ_WS_URL = "${ wsUrl } ";window.__stxRouterConfig={container:'[data-stx-content]'};<\/script>
168- ${ shell . styles }
169- ${ shell . signalsRuntime }
170- </head>
171- <body class="bg-[#0a0a0f] text-zinc-50 leading-relaxed min-h-screen">
172- ${ shell . before }
173- <div data-stx-content${ pageSetupName ? ` data-stx="${ pageSetupName } "` : '' } >${ pageHtml } </div>
174- ${ shell . after }
175- ${ shell . scripts }
176- ${ getRouterScriptTag ( ) }
177- </body>
178- </html>`
179-
18094 return new Response ( html , { headers : { 'Content-Type' : 'text/html' , 'Cache-Control' : 'no-store' } } )
18195}
18296
@@ -234,9 +148,6 @@ export async function serveDashboard(options: DashboardConfig = {}): Promise<voi
234148 const config = resolveConfig ( options )
235149 const apiRoutes = createApiRoutes ( config )
236150
237- // Pre-process the app shell at startup
238- await getShellParts ( )
239-
240151 // Start WebSocket broadcast server for real-time updates
241152 const broadcastPort = options . broadcastPort || 6001
242153 broadcastServer = new BroadcastServer ( {
0 commit comments