Skip to content

Commit 4b681f0

Browse files
committed
chore: wip
1 parent 07141e8 commit 4b681f0

File tree

17 files changed

+3438
-160
lines changed

17 files changed

+3438
-160
lines changed

packages/benchmarks/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
},
1818
"dependencies": {
1919
"@11ty/eleventy": "^3.1.2",
20-
"@stacksjs/markdown": "workspace:*",
2120
"@stacksjs/sanitizer": "workspace:*",
21+
"ts-md": "link:ts-md",
2222
"dompurify": "^3.2.7",
2323
"ejs": "^3.1.10",
2424
"gray-matter": "^4.0.3",

packages/bun-plugin/src/serve.ts

Lines changed: 154 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,17 @@ import { serve as bunServe, Glob } from 'bun'
1616
import process from 'node:process'
1717
import 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(/<script(?!\s+[^>]*src=)\b[^>]*>([\s\S]*?)<\/script>/i)
115+
const scriptContent = inlineScriptMatch ? inlineScriptMatch[1] : ''
116+
const templateContent = inlineScriptMatch
117+
? content.replace(/<script(?!\s+[^>]*src=)\b[^>]*>[\s\S]*?<\/script>/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(/<script(?!\s+[^>]*src=)\b[^>]*>([\s\S]*?)<\/script>/i)
125-
const scriptContent = inlineScriptMatch ? inlineScriptMatch[1] : ''
126-
// Only remove the inline script tag if it exists
127-
const templateContent = inlineScriptMatch ? content.replace(/<script(?!\s+[^>]*src=)\b[^>]*>[\s\S]*?<\/script>/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

Comments
 (0)