From 627e412b54086dc1b11cf07d80ae2e6d5a138a0c Mon Sep 17 00:00:00 2001 From: MarcosBrendonDePaula <46361998+MarcosBrendonDePaula@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:39:35 -0500 Subject: [PATCH] perf: optimize static file serving and centralize path resolution This commit introduces a centralized path resolver for static assets and adds Cache-Control support to the static plugin, improving performance and maintainability. --- core/plugins/built-in/static/index.ts | 85 +++++------------ core/plugins/built-in/vite/index.ts | 129 ++++---------------------- core/utils/path-resolver/index.ts | 53 +++++++++++ 3 files changed, 91 insertions(+), 176 deletions(-) create mode 100644 core/utils/path-resolver/index.ts diff --git a/core/plugins/built-in/static/index.ts b/core/plugins/built-in/static/index.ts index 0f0083d5..0b508bf2 100644 --- a/core/plugins/built-in/static/index.ts +++ b/core/plugins/built-in/static/index.ts @@ -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) => { @@ -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 \ No newline at end of file +export default staticPlugin diff --git a/core/plugins/built-in/vite/index.ts b/core/plugins/built-in/vite/index.ts index 6948ca6a..7e34534d 100644 --- a/core/plugins/built-in/vite/index.ts +++ b/core/plugins/built-in/vite/index.ts @@ -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 { - const map = new Map() - 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>() - 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)) } } } @@ -126,7 +30,6 @@ async function proxyToVite(ctx: RequestContext): Promise { const { host, port } = clientConfig.vite try { - // Parse URL (handle relative URLs) let url: URL try { url = new URL(ctx.request.url) @@ -158,11 +61,11 @@ async function proxyToVite(ctx: RequestContext): Promise { 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) => { @@ -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 } @@ -184,8 +87,8 @@ 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 } @@ -193,7 +96,7 @@ export const vitePlugin: Plugin = { }, onBeforeRoute: async (ctx: RequestContext) => { - if (!IS_DEV) return + if (!isDevelopment()) return const shouldSkip = (pluginsConfig.viteExcludePaths ?? []).some(prefix => ctx.path === prefix || ctx.path.startsWith(prefix + '/') diff --git a/core/utils/path-resolver/index.ts b/core/utils/path-resolver/index.ts new file mode 100644 index 00000000..7bb125cf --- /dev/null +++ b/core/utils/path-resolver/index.ts @@ -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 } +}