Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 22 additions & 63 deletions core/plugins/built-in/static/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { join } from "path"
import { statSync, existsSync } from "fs"
import { resolveStaticPath } from "@core/utils/path-resolver"
import type { Plugin, PluginContext } from "@core/plugins"

// Default configuration values
const DEFAULTS = {
enabled: true,
publicDir: "./dist/client",
indexFile: "index.html"
indexFile: "index.html",
cacheMaxAge: 3600 // 1 hour
}

export const staticPlugin: Plugin = {
name: "static",
version: "2.0.0",
description: "Simple and efficient static file serving plugin for FluxStack",
version: "2.1.0",
description: "Optimized static file serving plugin for FluxStack",
author: "FluxStack Team",
priority: 200, // Run after all other plugins
priority: 200,
category: "core",
tags: ["static", "files", "spa"],
tags: ["static", "files", "spa", "optimized"],
dependencies: [],

setup: async (context: PluginContext) => {
Expand All @@ -25,79 +25,38 @@ export const staticPlugin: Plugin = {
return
}

context.logger.info("Static files plugin activated", {
publicDir: DEFAULTS.publicDir
})
context.logger.info("Static files plugin activated (optimized)")

// Static fallback handler (runs last)
const staticFallback = (c: any) => {
const req = c.request
if (!req) return

const url = new URL(req.url)
let pathname = decodeURIComponent(url.pathname)
const pathname = decodeURIComponent(url.pathname)

// Determine base directory using path discovery
const isDev = context.utils.isDevelopment()
let baseDir: string

if (isDev && existsSync(DEFAULTS.publicDir)) {
// Development: use public directory
baseDir = DEFAULTS.publicDir
} else {
// Production: try paths in order of preference
if (existsSync('client')) {
// Found client/ in current directory (running from dist/)
baseDir = 'client'
} else if (existsSync('dist/client')) {
// Found dist/client/ (running from project root)
baseDir = 'dist/client'
} else {
// Fallback to configured path
baseDir = DEFAULTS.publicDir
}
}

// Root or empty path → index.html
if (pathname === '/' || pathname === '') {
pathname = `/${DEFAULTS.indexFile}`
}

const filePath = join(baseDir, pathname)

try {
const info = statSync(filePath)

// File exists → serve it
if (info.isFile()) {
return new Response(Bun.file(filePath))
}
} catch (_) {
// File not found → continue
}
const { filePath, isFile } = resolveStaticPath(pathname, {
publicDir: DEFAULTS.publicDir,
indexFile: DEFAULTS.indexFile
})

// SPA fallback: serve index.html for non-file routes
const indexPath = join(baseDir, DEFAULTS.indexFile)
try {
statSync(indexPath) // Ensure index exists
return new Response(Bun.file(indexPath))
} catch (_) {
// Index not found → let request continue (404)
if (isFile && filePath) {
const response = new Response(Bun.file(filePath))
// Add Cache-Control headers
response.headers.set('Cache-Control', `public, max-age=${DEFAULTS.cacheMaxAge}`)
return response
}
}

// Register as catch-all fallback (runs after all other routes)
context.app.all('*', staticFallback)
},

onServerStart: async (context: PluginContext) => {
if (DEFAULTS.enabled) {
context.logger.info(`Static files plugin ready`, {
publicDir: DEFAULTS.publicDir,
indexFile: DEFAULTS.indexFile
})
context.logger.info(`Static files plugin ready (optimized)`)
}
}
}

export default staticPlugin
export default staticPlugin
129 changes: 16 additions & 113 deletions core/plugins/built-in/vite/index.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,26 @@
import type { FluxStack, PluginContext, RequestContext } from "@core/plugins/types"
import { FLUXSTACK_VERSION } from "@core/utils/version"
import { clientConfig } from '@config'
import { pluginsConfig } from '@config'
import { clientConfig, pluginsConfig } from '@config'
import { isDevelopment } from "@core/utils/helpers"
import { join } from "path"
import { statSync, existsSync, readdirSync } from "fs"
import { resolveStaticPath } from "@core/utils/path-resolver"

type Plugin = FluxStack.Plugin

const PLUGIN_PRIORITY = 800
const INDEX_FILE = "index.html"

/** Cached at module load — NODE_ENV does not change at runtime */
const IS_DEV = isDevelopment()

/** One year in seconds — standard for hashed static assets */
const STATIC_MAX_AGE = 31536000

/** Extensions that carry a content hash in their filename (immutable) */
const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/

/**
* Recursively collect all files under `dir` as relative paths (e.g. "/assets/app.abc123.js").
* Runs once at startup — in production the build output never changes.
*/
function collectFiles(dir: string, prefix = ''): Map<string, string> {
const map = new Map<string, string>()
try {
const entries = readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const rel = prefix + '/' + entry.name
if (entry.isDirectory()) {
for (const [k, v] of collectFiles(join(dir, entry.name), rel)) {
map.set(k, v)
}
} else if (entry.isFile()) {
map.set(rel, join(dir, entry.name))
}
}
} catch {}
return map
}

/** Create static file handler with full in-memory cache */
/** Create static file handler using centralized resolver */
function createStaticFallback() {
// Discover base directory once
const baseDir = existsSync('client') ? 'client'
: existsSync('dist/client') ? 'dist/client'
: clientConfig.build.outDir ?? 'dist/client'

// Pre-scan all files at startup — O(1) lookup per request
const fileMap = collectFiles(baseDir)

// Pre-resolve SPA fallback
const indexAbsolute = join(baseDir, INDEX_FILE)
const indexExists = existsSync(indexAbsolute)
const indexFile = indexExists ? Bun.file(indexAbsolute) : null

// Bun.file() handle cache — avoids re-creating handles on repeated requests
const fileCache = new Map<string, ReturnType<typeof Bun.file>>()

return (c: { request?: Request }) => {
const req = c.request
if (!req) return

// Fast pathname extraction — avoid full URL parse when possible
const rawUrl = req.url
let pathname: string
const qIdx = rawUrl.indexOf('?')
const pathPart = qIdx === -1 ? rawUrl : rawUrl.slice(0, qIdx)

// Handle absolute URLs (http://...) vs relative paths
if (pathPart.charCodeAt(0) === 47) { // '/'
pathname = pathPart
} else {
// Absolute URL — find the path after ://host
const protoEnd = pathPart.indexOf('://')
if (protoEnd !== -1) {
const slashIdx = pathPart.indexOf('/', protoEnd + 3)
pathname = slashIdx === -1 ? '/' : pathPart.slice(slashIdx)
} else {
pathname = pathPart
}
}

// Decode percent-encoding only if needed
if (pathname.includes('%')) {
try { pathname = decodeURIComponent(pathname) } catch {}
}

if (pathname === '/' || pathname === '') {
pathname = `/${INDEX_FILE}`
}

// O(1) lookup in pre-scanned file map
const absolutePath = fileMap.get(pathname)
if (absolutePath) {
let file = fileCache.get(pathname)
if (!file) {
file = Bun.file(absolutePath)
fileCache.set(pathname, file)
}

// Hashed filenames are immutable — cache aggressively
if (HASHED_EXT.test(pathname)) {
return new Response(file, {
headers: {
'Cache-Control': `public, max-age=${STATIC_MAX_AGE}, immutable`
}
})
}

return file
}
const pathname = decodeURIComponent(new URL(req.url).pathname)
const { filePath, isFile } = resolveStaticPath(pathname, {
publicDir: clientConfig.build.outDir ?? 'dist/client'
})

// SPA fallback: serve index.html for unmatched routes
if (indexFile) {
return indexFile
if (isFile && filePath) {
return new Response(Bun.file(filePath))
}
}
}
Expand All @@ -126,7 +30,6 @@ async function proxyToVite(ctx: RequestContext): Promise<void> {
const { host, port } = clientConfig.vite

try {
// Parse URL (handle relative URLs)
let url: URL
try {
url = new URL(ctx.request.url)
Expand Down Expand Up @@ -158,11 +61,11 @@ async function proxyToVite(ctx: RequestContext): Promise<void> {
export const vitePlugin: Plugin = {
name: "vite",
version: FLUXSTACK_VERSION,
description: "Vite integration plugin for FluxStack",
description: "Optimized Vite integration plugin for FluxStack",
author: "FluxStack Team",
priority: PLUGIN_PRIORITY,
category: "development",
tags: ["vite", "development", "hot-reload"],
tags: ["vite", "development", "hot-reload", "optimized"],
dependencies: [],

setup: async (context: PluginContext) => {
Expand All @@ -171,8 +74,8 @@ export const vitePlugin: Plugin = {
return
}

if (!IS_DEV) {
context.logger.debug("Production mode: static file serving enabled")
if (!isDevelopment()) {
context.logger.debug("Production mode: static file serving enabled (optimized)")
context.app.all('*', createStaticFallback())
return
}
Expand All @@ -184,16 +87,16 @@ export const vitePlugin: Plugin = {
onServerStart: async (context: PluginContext) => {
if (!pluginsConfig.viteEnabled) return

if (!IS_DEV) {
context.logger.debug('Static files ready')
if (!isDevelopment()) {
context.logger.debug('Static files ready (optimized)')
return
}

context.logger.debug(`Vite active - ${clientConfig.vite.host}:${clientConfig.vite.port}`)
},

onBeforeRoute: async (ctx: RequestContext) => {
if (!IS_DEV) return
if (!isDevelopment()) return

const shouldSkip = (pluginsConfig.viteExcludePaths ?? []).some(prefix =>
ctx.path === prefix || ctx.path.startsWith(prefix + '/')
Expand Down
53 changes: 53 additions & 0 deletions core/utils/path-resolver/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { join } from "path"
import { statSync, existsSync } from "fs"

export interface StaticPathOptions {
publicDir?: string
indexFile?: string
fallbackToIndex?: boolean
}

/**
* Resolves the physical path for a static file request
*/
export function resolveStaticPath(pathname: string, options: StaticPathOptions = {}) {
const {
publicDir = 'dist/client',
indexFile = 'index.html',
fallbackToIndex = true
} = options

// Determine base directory
let baseDir = publicDir
if (existsSync('client')) {
baseDir = 'client'
} else if (existsSync('dist/client')) {
baseDir = 'dist/client'
}

// Normalize pathname
let targetPath = pathname
if (targetPath === '/' || targetPath === '') {
targetPath = `/${indexFile}`
}

const filePath = join(baseDir, targetPath)

try {
const info = statSync(filePath)
if (info.isFile()) {
return { filePath, isFile: true }
}
} catch (_) {}

if (fallbackToIndex) {
const indexPath = join(baseDir, indexFile)
try {
if (statSync(indexPath).isFile()) {
return { filePath: indexPath, isFile: true, isFallback: true }
}
} catch (_) {}
}

return { filePath: null, isFile: false }
}
Loading