@@ -16,19 +16,17 @@ import { serve as bunServe, Glob } from 'bun'
1616import process from 'node:process'
1717import stxPlugin from './index'
1818
19- async function main ( ) {
20- // Parse command line arguments
21- const args = process . argv . slice ( 2 )
22-
23- // Remove 'serve' if it's the first argument (for compatibility)
24- if ( args [ 0 ] === 'serve' ) {
25- args . shift ( )
26- }
27- const portIndex = args . indexOf ( '--port' )
28- const port = portIndex !== - 1 && args [ portIndex + 1 ] ? Number . parseInt ( args [ portIndex + 1 ] ) : 3456
19+ export interface ServeOptions {
20+ patterns : string [ ]
21+ port ?: number
22+ }
2923
30- // Get file patterns (everything that's not a flag)
31- const patterns = args . filter ( arg => ! arg . startsWith ( '--' ) && arg !== args [ portIndex + 1 ] )
24+ /**
25+ * Start the STX development server
26+ * @param options Server options with patterns and port
27+ */
28+ export async function serve ( options : ServeOptions ) : Promise < void > {
29+ const { patterns, port = 3456 } = options
3230
3331 if ( patterns . length === 0 ) {
3432 console . error ( 'Usage: serve <files...> [--port 3000]' )
@@ -39,122 +37,131 @@ async function main() {
3937 console . error ( ' serve pages/ --port 3000' )
4038 console . error ( ' serve index.stx about.md page.html' )
4139 console . error ( '\nAfter installing: bun add bun-plugin-stx' )
42- process . exit ( 1 )
40+ throw new Error ( 'No file patterns provided' )
4341 }
4442
45- console . log ( '🚀 Starting stx development server...\n' )
46-
47- // Discover all .stx, .md, and .html files
48- const sourceFiles : string [ ] = [ ]
49- const supportedExtensions = [ '.stx' , '.md' , '.html' ]
50-
51- for ( const pattern of patterns ) {
52- try {
53- // Check if it's a directory using fs.stat
54- const fs = await import ( 'node:fs/promises' )
55- const stat = await fs . stat ( pattern ) . catch ( ( ) => null )
56-
57- if ( stat ?. isDirectory ( ) ) {
58- // Scan directory for supported files
59- for ( const ext of [ '.stx' , '.md' , '.html' ] ) {
60- const glob = new Glob ( `**/*${ ext } ` )
61- const files = await Array . fromAsync ( glob . scan ( pattern ) )
62- sourceFiles . push ( ...files . map ( f => `${ pattern } /${ f } ` . replace ( / \/ + / g, '/' ) ) )
43+ // Lazy-load: Cache for processed templates
44+ const routes = new Map < string , string > ( )
45+ let sourceFiles : string [ ] | null = null
46+ let assetsInitialized = false
47+
48+ // Lazy file discovery function
49+ async function discoverFiles ( ) {
50+ if ( sourceFiles !== null )
51+ return sourceFiles
52+
53+ const files : string [ ] = [ ]
54+ const supportedExtensions = [ '.stx' , '.md' , '.html' ]
55+
56+ for ( const pattern of patterns ) {
57+ try {
58+ const fs = await import ( 'node:fs/promises' )
59+ const stat = await fs . stat ( pattern ) . catch ( ( ) => null )
60+
61+ if ( stat ?. isDirectory ( ) ) {
62+ for ( const ext of [ '.stx' , '.md' , '.html' ] ) {
63+ const glob = new Glob ( `**/*${ ext } ` )
64+ const discovered = await Array . fromAsync ( glob . scan ( pattern ) )
65+ files . push ( ...discovered . map ( f => `${ pattern } /${ f } ` . replace ( / \/ + / g, '/' ) ) )
66+ }
67+ }
68+ else if ( pattern . includes ( '*' ) ) {
69+ const glob = new Glob ( pattern )
70+ const basePath = pattern . split ( '*' ) [ 0 ] . replace ( / \/ $ / , '' )
71+ const discovered = await Array . fromAsync ( glob . scan ( basePath || '.' ) )
72+ files . push ( ...discovered . map ( f => basePath ? `${ basePath } /${ f } ` : f ) )
73+ }
74+ else if ( supportedExtensions . some ( ext => pattern . endsWith ( ext ) ) ) {
75+ files . push ( pattern )
6376 }
6477 }
65- else if ( pattern . includes ( '*' ) ) {
66- // Handle glob patterns
67- const glob = new Glob ( pattern )
68- const basePath = pattern . split ( '*' ) [ 0 ] . replace ( / \/ $ / , '' )
69- const files = await Array . fromAsync ( glob . scan ( basePath || '.' ) )
70- sourceFiles . push ( ...files . map ( f => basePath ? `${ basePath } /${ f } ` : f ) )
78+ catch ( error ) {
79+ console . error ( `Error processing pattern "${ pattern } ":` , error )
7180 }
72- else if ( supportedExtensions . some ( ext => pattern . endsWith ( ext ) ) ) {
73- // Single file with supported extension
74- sourceFiles . push ( pattern )
81+ }
82+
83+ sourceFiles = files
84+ return files
85+ }
86+
87+ // Lazy asset copy function
88+ async function ensureAssets ( ) {
89+ if ( assetsInitialized )
90+ return
91+
92+ assetsInitialized = true
93+ const fs = await import ( 'node:fs/promises' )
94+ const assetsDir = './resources/assets'
95+ const targetAssetsDir = './.stx/assets'
96+
97+ try {
98+ const assetsExist = await fs . stat ( assetsDir ) . then ( ( ) => true ) . catch ( ( ) => false )
99+ if ( assetsExist ) {
100+ await fs . rm ( targetAssetsDir , { recursive : true , force : true } )
101+ await fs . cp ( assetsDir , targetAssetsDir , { recursive : true } )
75102 }
76103 }
77104 catch ( error ) {
78- console . error ( `Error processing pattern " ${ pattern } ":` , error )
105+ // Silently ignore
79106 }
80107 }
81108
82- if ( sourceFiles . length === 0 ) {
83- console . error ( '❌ No .stx, .md, or .html files found' )
84- process . exit ( 1 )
85- }
109+ // Lazy template processing function
110+ async function processTemplate ( filePath : string ) : Promise < string > {
111+ const path = await import ( 'node:path' )
112+ const content = await Bun . file ( filePath ) . text ( )
86113
87- console . log ( `📄 Found ${ sourceFiles . length } file(s):` )
88- sourceFiles . forEach ( file => console . log ( ` - ${ file } ` ) )
89-
90- // Copy assets directory to build output so they're available during build
91- console . log ( '\n📦 Copying assets...' )
92- const fs = await import ( 'node:fs/promises' )
93- const path = await import ( 'node:path' )
94- const assetsDir = './resources/assets'
95- const targetAssetsDir = './.stx/assets'
96-
97- try {
98- const assetsExist = await fs . stat ( assetsDir ) . then ( ( ) => true ) . catch ( ( ) => false )
99- if ( assetsExist ) {
100- // Remove existing assets dir in build output
101- await fs . rm ( targetAssetsDir , { recursive : true , force : true } )
102- // Copy assets recursively
103- await fs . cp ( assetsDir , targetAssetsDir , { recursive : true } )
104- console . log ( '✓ Assets copied' )
114+ const inlineScriptMatch = content . match ( / < s c r i p t (? ! \s + [ ^ > ] * s r c = ) \b [ ^ > ] * > ( [ \s \S ] * ?) < \/ s c r i p t > / i)
115+ const scriptContent = inlineScriptMatch ? inlineScriptMatch [ 1 ] : ''
116+ const templateContent = inlineScriptMatch
117+ ? content . replace ( / < s c r i p t (? ! \s + [ ^ > ] * s r c = ) \b [ ^ > ] * > [ \s \S ] * ?< \/ s c r i p t > / i, '' )
118+ : content
119+
120+ const context : Record < string , any > = {
121+ __filename : filePath ,
122+ __dirname : path . dirname ( filePath ) ,
105123 }
106- }
107- catch ( error ) {
108- console . log ( '⚠ No assets directory found, skipping copy' )
109- }
110124
111- // Process files directly without using Bun.build to avoid path resolution
112- console . log ( '\n🔨 Processing templates...' )
113- const routes = new Map < string , string > ( )
125+ const { processDirectives, extractVariables, defaultConfig } = await import ( '@stacksjs/stx' )
126+ await extractVariables ( scriptContent , context , filePath )
114127
115- // Import stx processing functions
116- const { processDirectives, extractVariables, defaultConfig } = await import ( '@stacksjs/stx' )
128+ let output = templateContent
129+ const dependencies = new Set < string > ( )
130+ output = await processDirectives ( output , context , filePath , defaultConfig , dependencies )
117131
118- for ( const filePath of sourceFiles ) {
119- try {
120- const content = await Bun . file ( filePath ) . text ( )
121-
122- // Extract inline script (without src attribute) for variable extraction
123- // This should match <script> or <script type="..."> but NOT <script src="...">
124- const inlineScriptMatch = content . match ( / < s c r i p t (? ! \s + [ ^ > ] * s r c = ) \b [ ^ > ] * > ( [ \s \S ] * ?) < \/ s c r i p t > / i)
125- const scriptContent = inlineScriptMatch ? inlineScriptMatch [ 1 ] : ''
126- // Only remove the inline script tag if it exists
127- const templateContent = inlineScriptMatch ? content . replace ( / < s c r i p t (? ! \s + [ ^ > ] * s r c = ) \b [ ^ > ] * > [ \s \S ] * ?< \/ s c r i p t > / i, '' ) : content
128-
129- // Create execution context
130- const context : Record < string , any > = {
131- __filename : filePath ,
132- __dirname : path . dirname ( filePath ) ,
133- }
132+ return output
133+ }
134134
135- // Execute script to extract variables
136- await extractVariables ( scriptContent , context , filePath )
135+ // Function to get or create route
136+ async function getRoute ( path : string ) : Promise < string | null > {
137+ // Check cache first
138+ if ( routes . has ( path ) )
139+ return routes . get ( path ) !
137140
138- // Process template directives
139- let output = templateContent
140- const dependencies = new Set < string > ( )
141- output = await processDirectives ( output , context , filePath , defaultConfig , dependencies )
141+ // Discover files if needed
142+ const files = await discoverFiles ( )
143+ const nodePath = await import ( 'node:path' )
142144
143- // Determine route from filename
144- const filename = path . basename ( filePath , path . extname ( filePath ) )
145+ // Find matching file
146+ for ( const filePath of files ) {
147+ const filename = nodePath . basename ( filePath , nodePath . extname ( filePath ) )
145148 const route = [ 'index' , 'home' ] . includes ( filename ) ? '/' : `/${ filename } `
146149
147- console . log ( ` ✓ ${ filePath } -> ${ route } ` )
148- routes . set ( route , output )
149- }
150- catch ( error ) {
151- console . error ( ` ✗ Error processing ${ filePath } :` , error )
150+ if ( route === path ) {
151+ // Process and cache
152+ const output = await processTemplate ( filePath )
153+ routes . set ( route , output )
154+ return output
155+ }
152156 }
157+
158+ return null
153159 }
154160
155- console . log ( '✅ Processing complete\n' )
161+ // Start server immediately - processing happens on-demand
162+ console . log ( `🌐 Server running at: http://localhost:${ port } ` )
163+ console . log ( `💡 Templates will be processed on first request\n` )
156164
157- // Start server
158165 const _server = bunServe ( {
159166 port,
160167 async fetch ( req ) {
@@ -165,9 +172,10 @@ async function main() {
165172 if ( path === '/index' )
166173 path = '/'
167174
168- // Try to serve the requested page
169- if ( routes . has ( path ) ) {
170- return new Response ( routes . get ( path ) , {
175+ // Try to serve the requested page (lazy load on demand)
176+ const content = await getRoute ( path )
177+ if ( content ) {
178+ return new Response ( content , {
171179 headers : {
172180 'Content-Type' : 'text/html' ,
173181 'Cache-Control' : 'no-cache' ,
@@ -176,8 +184,9 @@ async function main() {
176184 }
177185
178186 // Try without extension
179- if ( routes . has ( `${ path } .html` ) ) {
180- return new Response ( routes . get ( `${ path } .html` ) , {
187+ const contentWithExt = await getRoute ( `${ path } .html` )
188+ if ( contentWithExt ) {
189+ return new Response ( contentWithExt , {
181190 headers : {
182191 'Content-Type' : 'text/html' ,
183192 'Cache-Control' : 'no-cache' ,
@@ -209,6 +218,8 @@ async function main() {
209218 // Smart asset serving - Laravel-style path resolution
210219 // Supports both /assets/* and /resources/assets/* paths
211220 if ( path . startsWith ( '/assets/' ) || path . startsWith ( '/resources/assets/' ) ) {
221+ // Ensure assets are copied on first request
222+ await ensureAssets ( )
212223 // Try multiple possible paths (like Laravel does)
213224 const possiblePaths = [
214225 path , // Original path
@@ -302,10 +313,18 @@ async function main() {
302313 return new Response ( null , { status : 204 } )
303314 }
304315
305- // 404 page
306- const availableRoutes = Array . from ( routes . keys ( ) )
307- . map ( route => `<li><a href="${ route } ">${ route } </a></li>` )
308- . join ( '\n' )
316+ // 404 page - discover files to show available routes
317+ const files = await discoverFiles ( )
318+ const nodePath = await import ( 'node:path' )
319+ const availableRoutes : string [ ] = [ ]
320+
321+ for ( const filePath of files ) {
322+ const filename = nodePath . basename ( filePath , nodePath . extname ( filePath ) )
323+ const route = [ 'index' , 'home' ] . includes ( filename ) ? '/' : `/${ filename } `
324+ availableRoutes . push ( `<li><a href="${ route } ">${ route } </a></li>` )
325+ }
326+
327+ const routesList = availableRoutes . join ( '\n' )
309328
310329 return new Response ( `
311330 <!DOCTYPE html>
@@ -331,7 +350,7 @@ async function main() {
331350 <h1>404 - Page Not Found</h1>
332351 <p>The page "${ path } " doesn't exist.</p>
333352 <h2>Available pages:</h2>
334- <ul>${ availableRoutes } </ul>
353+ <ul>${ routesList } </ul>
335354 </body>
336355 </html>
337356 ` , {
@@ -341,15 +360,29 @@ async function main() {
341360 } ,
342361 } )
343362
344- console . log ( `🌐 Server running at: http://localhost:${ port } \n` )
345- console . log ( '📚 Available routes:' )
346- routes . forEach ( ( _ , route ) => {
347- console . log ( ` http://localhost:${ port } ${ route } ` )
348- } )
349- console . log ( '\n💡 Press Ctrl+C to stop\n' )
350-
351363 // Keep the process running
352364 await Bun . sleep ( Number . POSITIVE_INFINITY )
353365}
354366
355- main ( ) . catch ( console . error )
367+ async function main ( ) {
368+ // Parse command line arguments
369+ const args = process . argv . slice ( 2 )
370+
371+ // Remove 'serve' if it's the first argument (for compatibility)
372+ if ( args [ 0 ] === 'serve' ) {
373+ args . shift ( )
374+ }
375+ const portIndex = args . indexOf ( '--port' )
376+ const port = portIndex !== - 1 && args [ portIndex + 1 ] ? Number . parseInt ( args [ portIndex + 1 ] ) : 3456
377+
378+ // Get file patterns (everything that's not a flag)
379+ const patterns = args . filter ( arg => ! arg . startsWith ( '--' ) && arg !== args [ portIndex + 1 ] )
380+
381+ // Call the exported serve function
382+ await serve ( { patterns, port } )
383+ }
384+
385+ // Only run main() if this file is being executed directly (not imported)
386+ if ( import . meta. main ) {
387+ main ( ) . catch ( console . error )
388+ }
0 commit comments