@withl5e/l5e 0.3.1 → 0.3.3-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entry-server.js +118 -116
- package/dist/entry-server.js.map +1 -1
- package/dist/{generateMetadata-Cn2uJjg_.js → generateMetadata-Cn8BIxq6.js} +4 -4
- package/dist/{generateMetadata-Cn2uJjg_.js.map → generateMetadata-Cn8BIxq6.js.map} +1 -1
- package/dist/index.js +40 -37
- package/dist/island/runtime.js +40 -23
- package/dist/island/runtime.js.map +1 -1
- package/dist/island.js +34 -28
- package/dist/island.js.map +1 -1
- package/dist/{jsx-runtime-Bokflh8Q.js → jsx-runtime-DqAp8xlt.js} +86 -60
- package/dist/jsx-runtime-DqAp8xlt.js.map +1 -0
- package/dist/jsx-runtime.js +20 -17
- package/dist/{render-DTXfJu0d.js → render-CRag-5V7.js} +2 -2
- package/dist/{render-DTXfJu0d.js.map → render-CRag-5V7.js.map} +1 -1
- package/dist/seo.js +3 -3
- package/dist/server.js +246 -241
- package/dist/server.js.map +1 -1
- package/dist/vite-plugin.js +17 -15
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +1 -1
- package/src/core/entry-server.ts +24 -2
- package/src/core/jsx-runtime.ts +68 -6
- package/src/core/server.ts +43 -4
- package/src/core/vite-plugin.ts +24 -2
- package/src/island/ClientIsland.ts +24 -1
- package/src/island/runtime.ts +27 -1
- package/dist/jsx-runtime-Bokflh8Q.js.map +0 -1
package/dist/vite-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.js","sources":["../src/core/vite-plugin.ts"],"sourcesContent":["import { transform } from 'esbuild';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'fs';\nimport { dirname, join, relative, resolve } from 'path';\nimport type { Plugin, UserConfig } from 'vite';\n\nconst VIRTUAL_L5E_VIEWS = 'virtual:l5e-views';\nconst VIRTUAL_L5E_ROUTE = 'virtual:l5e-route';\nconst VIRTUAL_L5E_SSR_ENTRY = 'virtual:l5e-ssr-entry';\nconst VIRTUAL_L5E_GLOBAL_LOADER = 'virtual:l5e-global-loader';\nconst VIRTUAL_L5E_ISLAND_STRATEGIES = 'virtual:l5e-island-strategies';\nconst VIRTUAL_L5E_ISLANDS = 'virtual:l5e-islands';\nconst VIRTUAL_L5E_ACTIONS = 'virtual:l5e-actions';\nconst VIRTUAL_L5E_MIDDLEWARE = 'virtual:l5e-middleware';\n\n/**\n * Vite's built-in env vars on `import.meta.env`. These are statically replaced\n * by Vite itself (DEV/PROD/SSR are booleans, MODE/BASE_URL strings), so the SSR\n * `import.meta.env.* -> process.env.*` rewrite must skip them — otherwise it\n * clobbers Vite defaults (e.g. `import.meta.env.DEV` would become the undefined\n * `process.env.DEV`).\n */\nconst VITE_RESERVED_ENV = new Set(['MODE', 'BASE_URL', 'PROD', 'DEV', 'SSR', 'LEGACY']);\n\n/**\n * Recursively scan directory for .tsx and .ts files\n */\nfunction scanTsFiles(dir: string, fileList: string[] = []): string[] {\n const files = readdirSync(dir);\n\n for (const file of files) {\n const filePath = join(dir, file);\n const stat = statSync(filePath);\n\n if (stat.isDirectory()) {\n // Skip node_modules and dist directories\n if (file === 'node_modules' || file === 'dist' || file === '.git') {\n continue;\n }\n scanTsFiles(filePath, fileList);\n } else if (file.endsWith('.tsx') || file.endsWith('.ts')) {\n fileList.push(filePath);\n }\n }\n\n return fileList;\n}\n\n/**\n * Short hash function for island keys\n */\nfunction shortHash(str: string): string {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).slice(0, 4);\n}\n\n/**\n * Derive component name from path: \"./react/Counter\" → \"Counter\"\n */\nfunction deriveComponentName(fromPath: string): string {\n const segments = fromPath.split('/');\n let filename = segments[segments.length - 1];\n // Remove extension if present\n filename = filename.replace(/\\.(tsx?|jsx?)$/, '');\n return filename;\n}\n\n/**\n * Create island registry key: \"/src/views/test-island/react/Counter\" → \"Counter_a3f2\"\n */\nfunction makeIslandKey(resolvedPath: string): string {\n const name = deriveComponentName(resolvedPath);\n return `${name}_${shortHash(resolvedPath)}`;\n}\n\n/**\n * Resolve file path with extension (.tsx, .ts, .jsx, .js)\n * Returns the relative path with extension (e.g., \"src/views/.../Counter.tsx\")\n * or null if file not found\n */\nfunction resolveWithExtension(resolvedPath: string, rootDir: string): string | null {\n // resolvedPath: \"/src/views/test-island/react/Counter\" (no extension, leading /)\n const relPath = resolvedPath.replace(/^\\//, ''); // \"src/views/.../Counter\"\n const absBase = resolve(rootDir, relPath);\n\n // Check with extensions\n for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {\n if (existsSync(absBase + ext)) {\n return relPath + ext; // \"src/views/.../Counter.tsx\"\n }\n }\n // Check without extension (file might already have one)\n if (existsSync(absBase)) {\n return relPath;\n }\n return null;\n}\n\n/**\n * Extract island entries from file using regex on JSX source\n */\nfunction extractIslandEntries(\n filePath: string,\n rootDir: string,\n): Array<{ component: string; resolvedPath: string; src: string; key: string }> {\n const content = readFileSync(filePath, 'utf-8');\n const entries: Array<{ component: string; resolvedPath: string; src: string; key: string }> = [];\n\n // Regex scan JSX source (NOT post-transform code).\n // Just find <ClientIsland ... from=\"Y\" ...> — component name derived from path.\n const regex = /<ClientIsland\\s[^>]*?from\\s*=\\s*\"([^\"]+)\"/g;\n\n const seen = new Set<string>();\n let match;\n\n while ((match = regex.exec(content)) !== null) {\n const fromPath = match[1];\n\n // Resolve relative path → absolute\n let resolvedPath: string;\n if (fromPath.startsWith('~/')) {\n // Alias ~/ → /src/ (project convention)\n resolvedPath = '/src/' + fromPath.substring(2);\n } else if (fromPath.startsWith('/src/')) {\n resolvedPath = fromPath;\n } else if (fromPath.startsWith('./') || fromPath.startsWith('../')) {\n const abs = resolve(filePath, '..', fromPath);\n resolvedPath = '/' + relative(rootDir, abs).replace(/\\\\/g, '/');\n } else {\n resolvedPath = fromPath;\n }\n\n // Resolve file extension for manifest compatibility\n const src = resolveWithExtension(resolvedPath, rootDir);\n if (!src) {\n console.warn(\n `[l5e] Island component not found: ${resolvedPath} (from ${fromPath} in ${filePath})`,\n );\n continue;\n }\n\n const key = makeIslandKey(resolvedPath);\n if (!seen.has(key)) {\n seen.add(key);\n entries.push({\n component: deriveComponentName(resolvedPath),\n resolvedPath,\n src, // \"src/views/.../Counter.tsx\" — matches manifest key format\n key,\n });\n }\n }\n return entries;\n}\n\n/**\n * Extract paths from useCss and useClientJs calls using regex\n */\nfunction extractPathsFromFile(filePath: string, rootDir: string): { css: string[]; js: string[] } {\n const content = readFileSync(filePath, 'utf-8');\n const css: string[] = [];\n const js: string[] = [];\n\n // Regex patterns to match useCss('path') or useCss(\"path\")\n // Handles single and double quotes, and escaped quotes\n const useCssPattern = /useCss\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,?\\s*\\)/g;\n const useClientJsPattern = /useClientJs\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,?\\s*\\)/g;\n\n let match;\n\n // Extract CSS paths\n while ((match = useCssPattern.exec(content)) !== null) {\n const path = match[1];\n // Convert relative path to absolute if needed\n if (path.startsWith('/src/')) {\n css.push(path);\n } else if (path.startsWith('./') || path.startsWith('../')) {\n // Resolve relative path\n const absolutePath = resolve(filePath, '..', path);\n const relativePath = '/' + relative(rootDir, absolutePath).replace(/\\\\/g, '/');\n css.push(relativePath);\n } else {\n css.push(path);\n }\n }\n\n // Extract JS paths\n while ((match = useClientJsPattern.exec(content)) !== null) {\n const path = match[1];\n if (path.startsWith('/src/')) {\n js.push(path);\n } else if (path.startsWith('./') || path.startsWith('../')) {\n const absolutePath = resolve(filePath, '..', path);\n const relativePath = '/' + relative(rootDir, absolutePath).replace(/\\\\/g, '/');\n js.push(relativePath);\n } else {\n js.push(path);\n }\n }\n\n return { css, js };\n}\n\n/**\n * Auto-discover rollup input entries from useCss and useClientJs calls\n */\nfunction discoverRollupInput(rootDir: string): {\n input: Record<string, string>;\n islandRegistry: Map<string, string>;\n pathToKey: Map<string, string>;\n keyToSrc: Map<string, string>;\n actionRegistry: Map<string, { modulePath: string; actionName: string }>;\n} {\n const input: Record<string, string> = {};\n const srcDir = join(rootDir, 'src');\n\n // Island registries\n const islandRegistry = new Map<string, string>(); // key → resolvedPath\n const pathToKey = new Map<string, string>(); // resolvedPath → key\n const keyToSrc = new Map<string, string>(); // key → src (manifest-compatible path with extension)\n\n // Action registry: actionKey → { modulePath, actionName }\n const actionRegistry = new Map<string, { modulePath: string; actionName: string }>();\n\n try {\n // Check if src directory exists\n if (!statSync(srcDir).isDirectory()) {\n return { input, islandRegistry, pathToKey, keyToSrc, actionRegistry };\n }\n\n // Check if src/client.global.ts exists and add it as an entry\n const globalTsPath = join(rootDir, 'src', 'client.global.ts');\n if (existsSync(globalTsPath)) {\n input['global'] = globalTsPath;\n console.log('[l5e] Detected src/client.global.ts and added as entry');\n }\n\n // Scan all .tsx and .ts files\n const tsFiles = scanTsFiles(srcDir);\n\n // Extract all paths\n const allCssPaths = new Set<string>();\n const allJsPaths = new Set<string>();\n\n for (const file of tsFiles) {\n const { css, js } = extractPathsFromFile(file, rootDir);\n css.forEach((path) => allCssPaths.add(path));\n js.forEach((path) => allJsPaths.add(path));\n\n // Extract island entries\n const islandEntries = extractIslandEntries(file, rootDir);\n for (const entry of islandEntries) {\n islandRegistry.set(entry.key, entry.resolvedPath);\n pathToKey.set(entry.resolvedPath, entry.key);\n keyToSrc.set(entry.key, entry.src);\n }\n\n // Extract action entries from actions.ts/tsx files\n const normalizedFile = file.replace(/\\\\/g, '/');\n if (/\\/actions\\.(ts|tsx)$/.test(normalizedFile)) {\n const content = readFileSync(file, 'utf-8');\n const actionExportRegex = /export\\s+const\\s+(\\w+)\\s*=\\s*defineAction\\s*\\(/g;\n let actionMatch;\n\n // Compute modulePath: relative path from src/ to parent dir\n const relFromSrc = relative(srcDir, dirname(file)).replace(/\\\\/g, '/');\n const modulePath = relFromSrc || '.';\n\n while ((actionMatch = actionExportRegex.exec(content)) !== null) {\n const actionName = actionMatch[1];\n const actionKey = `${actionName}_${shortHash(modulePath)}`;\n actionRegistry.set(actionKey, { modulePath, actionName });\n }\n }\n }\n\n // Convert paths to rollup input entries\n // CSS files\n for (const cssPath of allCssPaths) {\n // Remove leading /src/ and convert to relative path\n const relativePath = cssPath.startsWith('/src/')\n ? cssPath.substring(1) // Remove leading /\n : cssPath.startsWith('/')\n ? cssPath.substring(1)\n : cssPath;\n\n const absolutePath = resolve(rootDir, relativePath);\n\n // Only add if file exists\n if (!existsSync(absolutePath)) {\n console.warn(`[l5e] CSS file not found: ${absolutePath} (from ${cssPath})`);\n continue;\n }\n\n // Generate entry name from path (e.g., /src/views/home/home.css -> views-home-home)\n const entryName = relativePath\n .replace(/^src\\//, '')\n .replace(/\\.css$/, '')\n .replace(/\\//g, '-')\n .replace(/\\\\/g, '-');\n\n input[entryName] = absolutePath;\n }\n\n // JS/TS files\n for (const jsPath of allJsPaths) {\n const relativePath = jsPath.startsWith('/src/')\n ? jsPath.substring(1)\n : jsPath.startsWith('/')\n ? jsPath.substring(1)\n : jsPath;\n\n const absolutePath = resolve(rootDir, relativePath);\n\n // Only add if file exists\n if (!existsSync(absolutePath)) {\n console.warn(`[l5e] JS/TS file not found: ${absolutePath} (from ${jsPath})`);\n continue;\n }\n\n // Generate entry name from path\n const entryName = relativePath\n .replace(/^src\\//, '')\n .replace(/\\.(ts|tsx|js|jsx)$/, '')\n .replace(/\\//g, '-')\n .replace(/\\\\/g, '-');\n\n input[entryName] = absolutePath;\n }\n\n // Add island component files to rollup input so they appear in manifest.\n // server.ts looks up manifest at runtime to resolve per-page island URLs.\n for (const [key, src] of keyToSrc) {\n const absolutePath = resolve(rootDir, src);\n input[`island-${key}`] = absolutePath;\n }\n\n if (islandRegistry.size > 0) {\n console.log(`[l5e] Detected ${islandRegistry.size} island(s)`);\n }\n if (actionRegistry.size > 0) {\n console.log(`[l5e] Detected ${actionRegistry.size} action(s)`);\n }\n } catch (error) {\n // Silently fail if directory doesn't exist or other errors\n console.warn('[l5e] Failed to discover rollup input:', error);\n }\n\n return { input, islandRegistry, pathToKey, keyToSrc, actionRegistry };\n}\n\n/**\n * Inject __key prop into ClientIsland calls\n * Use anchor-based approach to avoid fragile regex with nested objects\n */\nfunction injectIslandKeys(\n code: string,\n fileId: string,\n rootDir: string,\n pathToKey: Map<string, string>,\n keyToSrc: Map<string, string>,\n): string {\n // After esbuild, code has form:\n // jsxFactory(ClientIsland, { from: \"./react/Counter\", props: { n: 5 } })\n // ^anchor ^find from here\n\n const result: string[] = [];\n let lastIndex = 0;\n\n // Find each position \"ClientIsland,\" (anchor)\n const anchorRegex = /ClientIsland\\s*,\\s*\\{/g;\n let anchorMatch;\n\n while ((anchorMatch = anchorRegex.exec(code)) !== null) {\n const searchStart = anchorMatch.index + anchorMatch[0].length;\n\n // Scan for from: \"...\" in window ~500 chars after anchor\n const window = code.substring(searchStart, searchStart + 500);\n const fromMatch = /from:\\s*\"([^\"]+)\"/.exec(window);\n\n if (!fromMatch) continue;\n\n const fromPath = fromMatch[1];\n\n // Resolve path (handle ~/ alias)\n let resolved: string;\n if (fromPath.startsWith('~/')) {\n resolved = '/src/' + fromPath.substring(2);\n } else if (fromPath.startsWith('/src/')) {\n resolved = fromPath;\n } else if (fromPath.startsWith('./') || fromPath.startsWith('../')) {\n const abs = resolve(fileId, '..', fromPath);\n resolved = '/' + relative(rootDir, abs).replace(/\\\\/g, '/');\n } else {\n resolved = fromPath;\n }\n\n const key = pathToKey.get(resolved);\n if (!key) continue;\n\n const src = keyToSrc.get(key);\n if (!src) continue;\n\n // Inject __key and __src right after from: \"...\"\n const insertPos = searchStart + fromMatch.index + fromMatch[0].length;\n result.push(code.substring(lastIndex, insertPos));\n result.push(`, __key: \"${key}\", __src: \"${src}\"`);\n lastIndex = insertPos;\n }\n\n result.push(code.substring(lastIndex));\n return result.join('');\n}\n\nexport function coreVite(): Plugin {\n let rootDir: string = process.cwd();\n let islandRegistry = new Map<string, string>();\n let pathToKey = new Map<string, string>();\n let keyToSrc = new Map<string, string>();\n let actionRegistry = new Map<string, { modulePath: string; actionName: string }>();\n\n return {\n name: 'l5e-jsx-classic',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n // Store root directory for later use\n rootDir = resolvedConfig.root || process.cwd();\n },\n\n handleHotUpdate({ file, server }) {\n const relPath = relative(rootDir, file);\n\n // Re-scan action registry when an actions file changes\n if (/actions\\.(ts|tsx)$/.test(file)) {\n const discovered = discoverRollupInput(rootDir);\n actionRegistry = discovered.actionRegistry;\n // Invalidate the virtual:l5e-actions module so server picks up new registry\n const mod = server.moduleGraph.getModuleById('\\0' + VIRTUAL_L5E_ACTIONS);\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n }\n console.log(\n `[l5e] Action file changed: ${relPath} — re-scanned ${actionRegistry.size} action(s)`,\n );\n }\n\n // Allow Vite's default HMR for CSS and client-side JS files\n if (file.endsWith('.css')) {\n console.log(`[l5e] CSS changed: ${relPath} - using Vite HMR`);\n return; // Let Vite handle CSS HMR\n }\n\n if (file.includes('client.ts')) {\n console.log(`[l5e] Client JS changed: ${relPath} - using Vite HMR`);\n return; // Let Vite handle client JS HMR\n }\n\n // Trigger full page reload for SSR-related file changes (components, loaders, routes)\n // This ensures that server-side rendered components update properly\n if (file.includes('/src/') || file.includes('\\\\src\\\\')) {\n console.log(`[l5e] SSR file changed: ${relPath} - triggering full reload`);\n server.ws.send({\n type: 'full-reload',\n path: '*',\n });\n return []; // Prevent Vite's default HMR behavior\n }\n },\n\n buildEnd() {\n // Cleanup temporary files after build\n const tempDir = join(rootDir, '.l5e-temp');\n if (existsSync(tempDir)) {\n try {\n rmSync(tempDir, { recursive: true, force: true });\n console.log('[l5e] Cleaned up temporary files');\n } catch (error) {\n console.warn('[l5e] Failed to cleanup temporary files:', error);\n }\n }\n },\n\n writeBundle(_options, _bundle) {\n // Emit action registry JSON for production server\n if (actionRegistry.size > 0) {\n const outDir = join(rootDir, 'dist', 'server');\n if (!existsSync(outDir)) {\n mkdirSync(outDir, { recursive: true });\n }\n const registryObj = Object.fromEntries(actionRegistry);\n writeFileSync(join(outDir, 'action-registry.json'), JSON.stringify(registryObj), 'utf-8');\n console.log(`[l5e] Wrote action-registry.json (${actionRegistry.size} actions)`);\n }\n },\n\n config(userConfig) {\n // Auto-discover rollup input from useCss and useClientJs\n const projectRoot = userConfig.root || process.cwd();\n const discovered = discoverRollupInput(projectRoot);\n\n // Store island registries for use in other hooks\n islandRegistry = discovered.islandRegistry;\n pathToKey = discovered.pathToKey;\n keyToSrc = discovered.keyToSrc;\n actionRegistry = discovered.actionRegistry;\n\n // Merge with existing rollupOptions.input if any\n const existingInput = userConfig.build?.rollupOptions?.input || {};\n const mergedInput =\n typeof existingInput === 'object' && !Array.isArray(existingInput)\n ? { ...discovered.input, ...existingInput }\n : discovered.input;\n\n return {\n build: {\n ...userConfig.build,\n rollupOptions: {\n ...userConfig.build?.rollupOptions,\n input: Object.keys(mergedInput).length > 0 ? mergedInput : undefined,\n // Preserve exports for island component entries — without this,\n // Rollup tree-shakes their exports since nothing in the bundle imports them\n // (they're loaded at runtime via dynamic import from the island runtime).\n preserveEntrySignatures: 'exports-only',\n },\n },\n } satisfies UserConfig;\n },\n\n resolveId(id) {\n if (id === VIRTUAL_L5E_VIEWS) {\n return '\\0' + VIRTUAL_L5E_VIEWS;\n }\n if (id === VIRTUAL_L5E_ROUTE) {\n return '\\0' + VIRTUAL_L5E_ROUTE;\n }\n if (id === VIRTUAL_L5E_SSR_ENTRY) {\n return '\\0' + VIRTUAL_L5E_SSR_ENTRY;\n }\n if (id === VIRTUAL_L5E_GLOBAL_LOADER) {\n return '\\0' + VIRTUAL_L5E_GLOBAL_LOADER;\n }\n if (id === VIRTUAL_L5E_ISLAND_STRATEGIES) {\n return '\\0' + VIRTUAL_L5E_ISLAND_STRATEGIES;\n }\n if (id === VIRTUAL_L5E_ISLANDS) {\n return '\\0' + VIRTUAL_L5E_ISLANDS;\n }\n if (id === VIRTUAL_L5E_ACTIONS) {\n return '\\0' + VIRTUAL_L5E_ACTIONS;\n }\n if (id === VIRTUAL_L5E_MIDDLEWARE) {\n return '\\0' + VIRTUAL_L5E_MIDDLEWARE;\n }\n return null;\n },\n\n async transform(code, id, options) {\n // Auto-inject island runtime into client.global.ts so it's always loaded globally\n if (id.replace(/\\\\/g, '/').endsWith('src/client.global.ts') && !options?.ssr) {\n return {\n code: `import '@withl5e/l5e/island/runtime';\\n${code}`,\n map: null,\n };\n }\n\n // Client-side action transform: replace defineAction exports with fetch stubs\n // Supports actions anywhere under src/ (e.g., src/views/*, src/features/*, etc.)\n if (!options?.ssr) {\n const normalizedId = id.replace(/\\\\/g, '/');\n const actionMatch = normalizedId.match(/\\/src\\/(.+)\\/actions\\.(ts|tsx)$/);\n if (actionMatch) {\n const modulePath = actionMatch[1];\n\n // Parse exported action names\n const exportRegex = /export\\s+const\\s+(\\w+)\\s*=\\s*defineAction\\s*\\(/g;\n const actions: Array<{ name: string; method: string }> = [];\n let m;\n while ((m = exportRegex.exec(code)) !== null) {\n const name = m[1];\n // Find method for this action — scan from the defineAction( position\n const chunk = code.substring(m.index, m.index + 500);\n const methodMatch = chunk.match(/method:\\s*['\"](\\w+)['\"]/);\n const method = methodMatch ? methodMatch[1].toUpperCase() : 'GET';\n actions.push({ name, method });\n }\n\n if (actions.length > 0) {\n const stubs = actions.map(({ name, method }) => {\n const actionKey = `${name}_${shortHash(modulePath)}`;\n if (method === 'GET') {\n return `export async function ${name}(params) {\n const res = await fetch('/_l5e/action/${actionKey}?' + new URLSearchParams(params));\n if (!res.ok) throw Object.assign(new Error('HTTP ' + res.status), { status: res.status });\n return res;\n}`;\n }\n return `export async function ${name}(body) {\n const res = await fetch('/_l5e/action/${actionKey}', {\n method: '${method}',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw Object.assign(new Error('HTTP ' + res.status), { status: res.status });\n return res;\n}`;\n });\n\n return {\n code: stubs.join('\\n\\n'),\n map: null,\n };\n }\n }\n }\n\n // Skip node_modules\n if (id.includes('node_modules')) {\n return null;\n }\n\n // Skip files in /react/ directories\n if (id.includes('/react/') || id.includes('\\\\react\\\\')) {\n return null;\n }\n\n // Check if this is a JSX/TSX file that needs L5E JSX transformation\n const isJsxFile = /\\.(tsx|jsx)$/.test(id);\n\n if (isJsxFile) {\n try {\n const injected = `import { Fragment as __Fragment, jsxFactory } from \"@withl5e/l5e/jsx-runtime\"\\n${code}`;\n\n // Transform JSX using esbuild with L5E's JSX runtime (async)\n const result = await transform(injected, {\n loader: id.endsWith('.tsx') ? 'tsx' : 'jsx',\n jsx: 'transform',\n jsxFactory: 'jsxFactory',\n jsxFragment: '__Fragment',\n sourcemap: true,\n sourcefile: id,\n target: 'es2020',\n });\n\n let transformedCode = result.code;\n\n // Inject __key and __src for ALL ClientIsland calls\n if (transformedCode.includes('ClientIsland')) {\n transformedCode = injectIslandKeys(transformedCode, id, rootDir, pathToKey, keyToSrc);\n }\n\n return {\n code: transformedCode,\n map: result.map || null,\n };\n } catch (error) {\n // Log error but let Vite handle it\n console.error(`[l5e] Failed to transform JSX in ${id}:`, error);\n throw error;\n }\n }\n\n // Transform import.meta.env thà nh process.env ở server side\n // Server side có thể access tất cả env variables\n // Client side chỉ access được VITE_ env variables\n // Transform nà y chạy ở runtime khi module được load trong SSR context\n if (options?.ssr) {\n let transformedCode = code;\n let hasChanges = false;\n\n // Thay thế import.meta.env.VARIABLE_NAME thà nh process.env.VARIABLE_NAME\n // Match: import.meta.env.VITE_EVENT_CATEGORY_SLUG\n transformedCode = transformedCode.replace(\n /import\\.meta\\.env\\.([a-zA-Z_][a-zA-Z0-9_]*)/g,\n (match, varName) => {\n // Leave Vite's built-in env vars for Vite to handle.\n if (VITE_RESERVED_ENV.has(varName)) return match;\n hasChanges = true;\n return `process.env.${varName}`;\n },\n );\n\n // Thay thế import.meta.env['VARIABLE_NAME'] hoặc import.meta.env[\"VARIABLE_NAME\"]\n // thà nh process.env['VARIABLE_NAME'] hoặc process.env[\"VARIABLE_NAME\"]\n // Match: import.meta.env['VITE_EVENT_CATEGORY_SLUG'] hoặc import.meta.env[\"VITE_EVENT_CATEGORY_SLUG\"]\n transformedCode = transformedCode.replace(\n /import\\.meta\\.env\\[(['\"`])([^'\"`]+)\\1\\]/g,\n (match, quote, varName) => {\n // Leave Vite's built-in env vars for Vite to handle.\n if (VITE_RESERVED_ENV.has(varName)) return match;\n hasChanges = true;\n return `process.env[${quote}${varName}${quote}]`;\n },\n );\n\n if (hasChanges) {\n return {\n code: transformedCode,\n map: null, // Không cần source map cho env transform\n };\n }\n }\n return null;\n },\n\n load(id) {\n // Virtual module: l5e-views\n if (id === '\\0' + VIRTUAL_L5E_VIEWS) {\n return `\nexport const viewLoaders = import.meta.glob('/src/views/*/loader.{ts,tsx}');\nexport const viewComponents = import.meta.glob('/src/views/*/index.tsx');\n`;\n }\n\n // Virtual module: l5e-route\n if (id === '\\0' + VIRTUAL_L5E_ROUTE) {\n // Always use TypeScript\n return `export { default } from '/src/route.ts';`;\n }\n\n // Virtual module: l5e-ssr-entry\n if (id === '\\0' + VIRTUAL_L5E_SSR_ENTRY) {\n return `export { render } from '@withl5e/l5e/entry-server';\nexport { viewActions } from 'virtual:l5e-actions';`;\n }\n\n // Virtual module: l5e-global-loader\n if (id === '\\0' + VIRTUAL_L5E_GLOBAL_LOADER) {\n return `export const globalLoader = import.meta.glob('/src/global-loader.{ts,tsx}', { eager: false });`;\n }\n\n // Virtual module: l5e-actions\n if (id === '\\0' + VIRTUAL_L5E_ACTIONS) {\n const registryObj = Object.fromEntries(actionRegistry);\n return `export const viewActions = import.meta.glob('/src/**/actions.{ts,tsx}');\nexport const actionRegistry = ${JSON.stringify(registryObj)};`;\n }\n\n // Virtual module: l5e-middleware\n if (id === '\\0' + VIRTUAL_L5E_MIDDLEWARE) {\n return `\nconst middlewareModules = import.meta.glob([\n '/src/middleware.{ts,tsx,js,jsx}',\n '/src/middleware/index.{ts,tsx,js,jsx}',\n]);\n\nconst middlewarePaths = [\n '/src/middleware.ts',\n '/src/middleware.tsx',\n '/src/middleware.js',\n '/src/middleware.jsx',\n '/src/middleware/index.ts',\n '/src/middleware/index.tsx',\n '/src/middleware/index.js',\n '/src/middleware/index.jsx',\n];\n\nexport async function loadMiddleware() {\n const middlewarePath = middlewarePaths.find((path) => middlewareModules[path]);\n if (!middlewarePath) return undefined;\n\n const mod = await middlewareModules[middlewarePath]();\n return mod.onRequest;\n}\n`;\n }\n\n // Virtual module: l5e-islands\n // Lazy glob of all React island components so the SSR entry can import a\n // component by its __src path and renderToString() it. Non-eager → only\n // islands actually present on a page (with ssr) get imported at runtime.\n if (id === '\\0' + VIRTUAL_L5E_ISLANDS) {\n return `export const islandModules = import.meta.glob('/src/**/react/*.{tsx,jsx}');`;\n }\n\n // Virtual module: l5e-island-strategies\n if (id === '\\0' + VIRTUAL_L5E_ISLAND_STRATEGIES) {\n // Check if src/island-strategies.ts exists\n const strategiesFile = join(rootDir, 'src', 'island-strategies.ts');\n if (existsSync(strategiesFile)) {\n // Re-export user's file → Vite will build this file\n return `import '/src/island-strategies.ts';`;\n }\n // No file → empty module, no error\n return `/* no custom island strategies */`;\n }\n\n return null;\n },\n };\n}\n\nexport default coreVite;\n"],"names":["VIRTUAL_L5E_VIEWS","VIRTUAL_L5E_ROUTE","VIRTUAL_L5E_SSR_ENTRY","VIRTUAL_L5E_GLOBAL_LOADER","VIRTUAL_L5E_ISLAND_STRATEGIES","VIRTUAL_L5E_ISLANDS","VIRTUAL_L5E_ACTIONS","VIRTUAL_L5E_MIDDLEWARE","VITE_RESERVED_ENV","scanTsFiles","dir","fileList","files","readdirSync","file","filePath","join","statSync","shortHash","str","hash","i","deriveComponentName","fromPath","segments","filename","makeIslandKey","resolvedPath","resolveWithExtension","rootDir","relPath","absBase","resolve","ext","existsSync","extractIslandEntries","content","readFileSync","entries","regex","seen","match","abs","relative","src","key","extractPathsFromFile","css","js","useCssPattern","useClientJsPattern","path","absolutePath","relativePath","discoverRollupInput","input","srcDir","islandRegistry","pathToKey","keyToSrc","actionRegistry","globalTsPath","tsFiles","allCssPaths","allJsPaths","islandEntries","entry","normalizedFile","actionExportRegex","actionMatch","modulePath","dirname","actionName","actionKey","cssPath","entryName","jsPath","error","injectIslandKeys","code","fileId","result","lastIndex","anchorRegex","anchorMatch","searchStart","window","fromMatch","resolved","insertPos","coreVite","resolvedConfig","server","mod","tempDir","rmSync","_options","_bundle","outDir","mkdirSync","registryObj","writeFileSync","userConfig","projectRoot","discovered","existingInput","mergedInput","id","options","exportRegex","actions","m","name","methodMatch","method","injected","transform","transformedCode","hasChanges","varName","quote","strategiesFile"],"mappings":";;;AAaA,MAAMA,IAAoB,qBACpBC,IAAoB,qBACpBC,IAAwB,yBACxBC,IAA4B,6BAC5BC,IAAgC,iCAChCC,IAAsB,uBACtBC,IAAsB,uBACtBC,IAAyB,0BASzBC,IAAoB,oBAAI,IAAI,CAAC,QAAQ,YAAY,QAAQ,OAAO,OAAO,QAAQ,CAAC;AAKtF,SAASC,EAAYC,GAAaC,IAAqB,IAAc;AACnE,QAAMC,IAAQC,EAAYH,CAAG;AAE7B,aAAWI,KAAQF,GAAO;AACxB,UAAMG,IAAWC,EAAKN,GAAKI,CAAI;AAG/B,QAFaG,EAASF,CAAQ,EAErB,eAAe;AAEtB,UAAID,MAAS,kBAAkBA,MAAS,UAAUA,MAAS;AACzD;AAEF,MAAAL,EAAYM,GAAUJ,CAAQ;AAAA,IAChC,MAAA,EAAWG,EAAK,SAAS,MAAM,KAAKA,EAAK,SAAS,KAAK,MACrDH,EAAS,KAAKI,CAAQ;AAAA,EAE1B;AAEA,SAAOJ;AACT;AAKA,SAASO,EAAUC,GAAqB;AACtC,MAAIC,IAAO;AACX,WAASC,IAAI,GAAGA,IAAIF,EAAI,QAAQE;AAC9B,IAAAD,KAAQA,KAAQ,KAAKA,IAAOD,EAAI,WAAWE,CAAC,GAC5CD,IAAOA,IAAOA;AAEhB,SAAO,KAAK,IAAIA,CAAI,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC;AAC/C;AAKA,SAASE,EAAoBC,GAA0B;AACrD,QAAMC,IAAWD,EAAS,MAAM,GAAG;AACnC,MAAIE,IAAWD,EAASA,EAAS,SAAS,CAAC;AAE3C,SAAAC,IAAWA,EAAS,QAAQ,kBAAkB,EAAE,GACzCA;AACT;AAKA,SAASC,EAAcC,GAA8B;AAEnD,SAAO,GADML,EAAoBK,CAAY,CAC/B,IAAIT,EAAUS,CAAY,CAAC;AAC3C;AAOA,SAASC,EAAqBD,GAAsBE,GAAgC;AAElF,QAAMC,IAAUH,EAAa,QAAQ,OAAO,EAAE,GACxCI,IAAUC,EAAQH,GAASC,CAAO;AAGxC,aAAWG,KAAO,CAAC,QAAQ,OAAO,QAAQ,KAAK;AAC7C,QAAIC,EAAWH,IAAUE,CAAG;AAC1B,aAAOH,IAAUG;AAIrB,SAAIC,EAAWH,CAAO,IACbD,IAEF;AACT;AAKA,SAASK,EACPpB,GACAc,GAC8E;AAC9E,QAAMO,IAAUC,EAAatB,GAAU,OAAO,GACxCuB,IAAwF,CAAA,GAIxFC,IAAQ,8CAERC,wBAAW,IAAA;AACjB,MAAIC;AAEJ,UAAQA,IAAQF,EAAM,KAAKH,CAAO,OAAO,QAAM;AAC7C,UAAMb,IAAWkB,EAAM,CAAC;AAGxB,QAAId;AACJ,QAAIJ,EAAS,WAAW,IAAI;AAE1B,MAAAI,IAAe,UAAUJ,EAAS,UAAU,CAAC;AAAA,aACpCA,EAAS,WAAW,OAAO;AACpC,MAAAI,IAAeJ;AAAA,aACNA,EAAS,WAAW,IAAI,KAAKA,EAAS,WAAW,KAAK,GAAG;AAClE,YAAMmB,IAAMV,EAAQjB,GAAU,MAAMQ,CAAQ;AAC5C,MAAAI,IAAe,MAAMgB,EAASd,GAASa,CAAG,EAAE,QAAQ,OAAO,GAAG;AAAA,IAChE;AACE,MAAAf,IAAeJ;AAIjB,UAAMqB,IAAMhB,EAAqBD,GAAcE,CAAO;AACtD,QAAI,CAACe,GAAK;AACR,cAAQ;AAAA,QACN,qCAAqCjB,CAAY,UAAUJ,CAAQ,OAAOR,CAAQ;AAAA,MAAA;AAEpF;AAAA,IACF;AAEA,UAAM8B,IAAMnB,EAAcC,CAAY;AACtC,IAAKa,EAAK,IAAIK,CAAG,MACfL,EAAK,IAAIK,CAAG,GACZP,EAAQ,KAAK;AAAA,MACX,WAAWhB,EAAoBK,CAAY;AAAA,MAC3C,cAAAA;AAAA,MACA,KAAAiB;AAAA;AAAA,MACA,KAAAC;AAAA,IAAA,CACD;AAAA,EAEL;AACA,SAAOP;AACT;AAKA,SAASQ,EAAqB/B,GAAkBc,GAAkD;AAChG,QAAMO,IAAUC,EAAatB,GAAU,OAAO,GACxCgC,IAAgB,CAAA,GAChBC,IAAe,CAAA,GAIfC,IAAgB,6CAChBC,IAAqB;AAE3B,MAAIT;AAGJ,UAAQA,IAAQQ,EAAc,KAAKb,CAAO,OAAO,QAAM;AACrD,UAAMe,IAAOV,EAAM,CAAC;AAEpB,QAAIU,EAAK,WAAW,OAAO;AACzB,MAAAJ,EAAI,KAAKI,CAAI;AAAA,aACJA,EAAK,WAAW,IAAI,KAAKA,EAAK,WAAW,KAAK,GAAG;AAE1D,YAAMC,IAAepB,EAAQjB,GAAU,MAAMoC,CAAI,GAC3CE,IAAe,MAAMV,EAASd,GAASuB,CAAY,EAAE,QAAQ,OAAO,GAAG;AAC7E,MAAAL,EAAI,KAAKM,CAAY;AAAA,IACvB;AACE,MAAAN,EAAI,KAAKI,CAAI;AAAA,EAEjB;AAGA,UAAQV,IAAQS,EAAmB,KAAKd,CAAO,OAAO,QAAM;AAC1D,UAAMe,IAAOV,EAAM,CAAC;AACpB,QAAIU,EAAK,WAAW,OAAO;AACzB,MAAAH,EAAG,KAAKG,CAAI;AAAA,aACHA,EAAK,WAAW,IAAI,KAAKA,EAAK,WAAW,KAAK,GAAG;AAC1D,YAAMC,IAAepB,EAAQjB,GAAU,MAAMoC,CAAI,GAC3CE,IAAe,MAAMV,EAASd,GAASuB,CAAY,EAAE,QAAQ,OAAO,GAAG;AAC7E,MAAAJ,EAAG,KAAKK,CAAY;AAAA,IACtB;AACE,MAAAL,EAAG,KAAKG,CAAI;AAAA,EAEhB;AAEA,SAAO,EAAE,KAAAJ,GAAK,IAAAC,EAAA;AAChB;AAKA,SAASM,EAAoBzB,GAM3B;AACA,QAAM0B,IAAgC,CAAA,GAChCC,IAASxC,EAAKa,GAAS,KAAK,GAG5B4B,wBAAqB,IAAA,GACrBC,wBAAgB,IAAA,GAChBC,wBAAe,IAAA,GAGfC,wBAAqB,IAAA;AAE3B,MAAI;AAEF,QAAI,CAAC3C,EAASuC,CAAM,EAAE;AACpB,aAAO,EAAE,OAAAD,GAAO,gBAAAE,GAAgB,WAAAC,GAAW,UAAAC,GAAU,gBAAAC,EAAA;AAIvD,UAAMC,IAAe7C,EAAKa,GAAS,OAAO,kBAAkB;AAC5D,IAAIK,EAAW2B,CAAY,MACzBN,EAAM,SAAYM,GAClB,QAAQ,IAAI,wDAAwD;AAItE,UAAMC,IAAUrD,EAAY+C,CAAM,GAG5BO,wBAAkB,IAAA,GAClBC,wBAAiB,IAAA;AAEvB,eAAWlD,KAAQgD,GAAS;AAC1B,YAAM,EAAE,KAAAf,GAAK,IAAAC,EAAA,IAAOF,EAAqBhC,GAAMe,CAAO;AACtD,MAAAkB,EAAI,QAAQ,CAACI,MAASY,EAAY,IAAIZ,CAAI,CAAC,GAC3CH,EAAG,QAAQ,CAACG,MAASa,EAAW,IAAIb,CAAI,CAAC;AAGzC,YAAMc,IAAgB9B,EAAqBrB,GAAMe,CAAO;AACxD,iBAAWqC,KAASD;AAClB,QAAAR,EAAe,IAAIS,EAAM,KAAKA,EAAM,YAAY,GAChDR,EAAU,IAAIQ,EAAM,cAAcA,EAAM,GAAG,GAC3CP,EAAS,IAAIO,EAAM,KAAKA,EAAM,GAAG;AAInC,YAAMC,IAAiBrD,EAAK,QAAQ,OAAO,GAAG;AAC9C,UAAI,uBAAuB,KAAKqD,CAAc,GAAG;AAC/C,cAAM/B,IAAUC,EAAavB,GAAM,OAAO,GACpCsD,IAAoB;AAC1B,YAAIC;AAIJ,cAAMC,IADa3B,EAASa,GAAQe,EAAQzD,CAAI,CAAC,EAAE,QAAQ,OAAO,GAAG,KACpC;AAEjC,gBAAQuD,IAAcD,EAAkB,KAAKhC,CAAO,OAAO,QAAM;AAC/D,gBAAMoC,IAAaH,EAAY,CAAC,GAC1BI,IAAY,GAAGD,CAAU,IAAItD,EAAUoD,CAAU,CAAC;AACxD,UAAAV,EAAe,IAAIa,GAAW,EAAE,YAAAH,GAAY,YAAAE,GAAY;AAAA,QAC1D;AAAA,MACF;AAAA,IACF;AAIA,eAAWE,KAAWX,GAAa;AAEjC,YAAMV,IAAeqB,EAAQ,WAAW,OAAO,KAE3CA,EAAQ,WAAW,GAAG,IADtBA,EAAQ,UAAU,CAAC,IAGjBA,GAEAtB,IAAepB,EAAQH,GAASwB,CAAY;AAGlD,UAAI,CAACnB,EAAWkB,CAAY,GAAG;AAC7B,gBAAQ,KAAK,6BAA6BA,CAAY,UAAUsB,CAAO,GAAG;AAC1E;AAAA,MACF;AAGA,YAAMC,IAAYtB,EACf,QAAQ,UAAU,EAAE,EACpB,QAAQ,UAAU,EAAE,EACpB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG;AAErB,MAAAE,EAAMoB,CAAS,IAAIvB;AAAA,IACrB;AAGA,eAAWwB,KAAUZ,GAAY;AAC/B,YAAMX,IAAeuB,EAAO,WAAW,OAAO,KAE1CA,EAAO,WAAW,GAAG,IADrBA,EAAO,UAAU,CAAC,IAGhBA,GAEAxB,IAAepB,EAAQH,GAASwB,CAAY;AAGlD,UAAI,CAACnB,EAAWkB,CAAY,GAAG;AAC7B,gBAAQ,KAAK,+BAA+BA,CAAY,UAAUwB,CAAM,GAAG;AAC3E;AAAA,MACF;AAGA,YAAMD,IAAYtB,EACf,QAAQ,UAAU,EAAE,EACpB,QAAQ,sBAAsB,EAAE,EAChC,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG;AAErB,MAAAE,EAAMoB,CAAS,IAAIvB;AAAA,IACrB;AAIA,eAAW,CAACP,GAAKD,CAAG,KAAKe,GAAU;AACjC,YAAMP,IAAepB,EAAQH,GAASe,CAAG;AACzC,MAAAW,EAAM,UAAUV,CAAG,EAAE,IAAIO;AAAA,IAC3B;AAEA,IAAIK,EAAe,OAAO,KACxB,QAAQ,IAAI,kBAAkBA,EAAe,IAAI,YAAY,GAE3DG,EAAe,OAAO,KACxB,QAAQ,IAAI,kBAAkBA,EAAe,IAAI,YAAY;AAAA,EAEjE,SAASiB,GAAO;AAEd,YAAQ,KAAK,0CAA0CA,CAAK;AAAA,EAC9D;AAEA,SAAO,EAAE,OAAAtB,GAAO,gBAAAE,GAAgB,WAAAC,GAAW,UAAAC,GAAU,gBAAAC,EAAA;AACvD;AAMA,SAASkB,EACPC,GACAC,GACAnD,GACA6B,GACAC,GACQ;AAKR,QAAMsB,IAAmB,CAAA;AACzB,MAAIC,IAAY;AAGhB,QAAMC,IAAc;AACpB,MAAIC;AAEJ,UAAQA,IAAcD,EAAY,KAAKJ,CAAI,OAAO,QAAM;AACtD,UAAMM,IAAcD,EAAY,QAAQA,EAAY,CAAC,EAAE,QAGjDE,IAASP,EAAK,UAAUM,GAAaA,IAAc,GAAG,GACtDE,IAAY,oBAAoB,KAAKD,CAAM;AAEjD,QAAI,CAACC,EAAW;AAEhB,UAAMhE,IAAWgE,EAAU,CAAC;AAG5B,QAAIC;AACJ,QAAIjE,EAAS,WAAW,IAAI;AAC1B,MAAAiE,IAAW,UAAUjE,EAAS,UAAU,CAAC;AAAA,aAChCA,EAAS,WAAW,OAAO;AACpC,MAAAiE,IAAWjE;AAAA,aACFA,EAAS,WAAW,IAAI,KAAKA,EAAS,WAAW,KAAK,GAAG;AAClE,YAAMmB,IAAMV,EAAQgD,GAAQ,MAAMzD,CAAQ;AAC1C,MAAAiE,IAAW,MAAM7C,EAASd,GAASa,CAAG,EAAE,QAAQ,OAAO,GAAG;AAAA,IAC5D;AACE,MAAA8C,IAAWjE;AAGb,UAAMsB,IAAMa,EAAU,IAAI8B,CAAQ;AAClC,QAAI,CAAC3C,EAAK;AAEV,UAAMD,IAAMe,EAAS,IAAId,CAAG;AAC5B,QAAI,CAACD,EAAK;AAGV,UAAM6C,IAAYJ,IAAcE,EAAU,QAAQA,EAAU,CAAC,EAAE;AAC/D,IAAAN,EAAO,KAAKF,EAAK,UAAUG,GAAWO,CAAS,CAAC,GAChDR,EAAO,KAAK,aAAapC,CAAG,cAAcD,CAAG,GAAG,GAChDsC,IAAYO;AAAA,EACd;AAEA,SAAAR,EAAO,KAAKF,EAAK,UAAUG,CAAS,CAAC,GAC9BD,EAAO,KAAK,EAAE;AACvB;AAEO,SAASS,KAAmB;AACjC,MAAI7D,IAAkB,QAAQ,IAAA,GAE1B6B,wBAAgB,IAAA,GAChBC,wBAAe,IAAA,GACfC,wBAAqB,IAAA;AAEzB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,eAAe+B,GAAgB;AAE7B,MAAA9D,IAAU8D,EAAe,QAAQ,QAAQ,IAAA;AAAA,IAC3C;AAAA,IAEA,gBAAgB,EAAE,MAAA7E,GAAM,QAAA8E,KAAU;AAChC,YAAM9D,IAAUa,EAASd,GAASf,CAAI;AAGtC,UAAI,qBAAqB,KAAKA,CAAI,GAAG;AAEnC,QAAA8C,IADmBN,EAAoBzB,CAAO,EAClB;AAE5B,cAAMgE,IAAMD,EAAO,YAAY,cAAc,OAAOtF,CAAmB;AACvE,QAAIuF,KACFD,EAAO,YAAY,iBAAiBC,CAAG,GAEzC,QAAQ;AAAA,UACN,8BAA8B/D,CAAO,mBAAmB8B,EAAe,IAAI;AAAA,QAAA;AAAA,MAE/E;AAGA,UAAI9C,EAAK,SAAS,MAAM,GAAG;AACzB,gBAAQ,IAAI,sBAAsBgB,CAAO,mBAAmB;AAC5D;AAAA,MACF;AAEA,UAAIhB,EAAK,SAAS,WAAW,GAAG;AAC9B,gBAAQ,IAAI,4BAA4BgB,CAAO,mBAAmB;AAClE;AAAA,MACF;AAIA,UAAIhB,EAAK,SAAS,OAAO,KAAKA,EAAK,SAAS,SAAS;AACnD,uBAAQ,IAAI,2BAA2BgB,CAAO,2BAA2B,GACzE8D,EAAO,GAAG,KAAK;AAAA,UACb,MAAM;AAAA,UACN,MAAM;AAAA,QAAA,CACP,GACM,CAAA;AAAA,IAEX;AAAA,IAEA,WAAW;AAET,YAAME,IAAU9E,EAAKa,GAAS,WAAW;AACzC,UAAIK,EAAW4D,CAAO;AACpB,YAAI;AACF,UAAAC,EAAOD,GAAS,EAAE,WAAW,IAAM,OAAO,IAAM,GAChD,QAAQ,IAAI,kCAAkC;AAAA,QAChD,SAASjB,GAAO;AACd,kBAAQ,KAAK,4CAA4CA,CAAK;AAAA,QAChE;AAAA,IAEJ;AAAA,IAEA,YAAYmB,GAAUC,GAAS;AAE7B,UAAIrC,EAAe,OAAO,GAAG;AAC3B,cAAMsC,IAASlF,EAAKa,GAAS,QAAQ,QAAQ;AAC7C,QAAKK,EAAWgE,CAAM,KACpBC,EAAUD,GAAQ,EAAE,WAAW,GAAA,CAAM;AAEvC,cAAME,IAAc,OAAO,YAAYxC,CAAc;AACrD,QAAAyC,EAAcrF,EAAKkF,GAAQ,sBAAsB,GAAG,KAAK,UAAUE,CAAW,GAAG,OAAO,GACxF,QAAQ,IAAI,qCAAqCxC,EAAe,IAAI,WAAW;AAAA,MACjF;AAAA,IACF;AAAA,IAEA,OAAO0C,GAAY;AAEjB,YAAMC,IAAcD,EAAW,QAAQ,QAAQ,IAAA,GACzCE,IAAalD,EAAoBiD,CAAW;AAGjC,MAAAC,EAAW,gBAC5B9C,IAAY8C,EAAW,WACvB7C,IAAW6C,EAAW,UACtB5C,IAAiB4C,EAAW;AAG5B,YAAMC,IAAgBH,EAAW,OAAO,eAAe,SAAS,CAAA,GAC1DI,IACJ,OAAOD,KAAkB,YAAY,CAAC,MAAM,QAAQA,CAAa,IAC7D,EAAE,GAAGD,EAAW,OAAO,GAAGC,EAAA,IAC1BD,EAAW;AAEjB,aAAO;AAAA,QACL,OAAO;AAAA,UACL,GAAGF,EAAW;AAAA,UACd,eAAe;AAAA,YACb,GAAGA,EAAW,OAAO;AAAA,YACrB,OAAO,OAAO,KAAKI,CAAW,EAAE,SAAS,IAAIA,IAAc;AAAA;AAAA;AAAA;AAAA,YAI3D,yBAAyB;AAAA,UAAA;AAAA,QAC3B;AAAA,MACF;AAAA,IAEJ;AAAA,IAEA,UAAUC,GAAI;AACZ,aAAIA,MAAO3G,IACF,OAAOA,IAEZ2G,MAAO1G,IACF,OAAOA,IAEZ0G,MAAOzG,IACF,OAAOA,IAEZyG,MAAOxG,IACF,OAAOA,IAEZwG,MAAOvG,IACF,OAAOA,IAEZuG,MAAOtG,IACF,OAAOA,IAEZsG,MAAOrG,IACF,OAAOA,IAEZqG,MAAOpG,IACF,OAAOA,IAET;AAAA,IACT;AAAA,IAEA,MAAM,UAAUwE,GAAM4B,GAAIC,GAAS;AAEjC,UAAID,EAAG,QAAQ,OAAO,GAAG,EAAE,SAAS,sBAAsB,KAAK,CAACC,GAAS;AACvE,eAAO;AAAA,UACL,MAAM;AAAA,EAA0C7B,CAAI;AAAA,UACpD,KAAK;AAAA,QAAA;AAMT,UAAI,CAAC6B,GAAS,KAAK;AAEjB,cAAMvC,IADesC,EAAG,QAAQ,OAAO,GAAG,EACT,MAAM,iCAAiC;AACxE,YAAItC,GAAa;AACf,gBAAMC,IAAaD,EAAY,CAAC,GAG1BwC,IAAc,mDACdC,IAAmD,CAAA;AACzD,cAAIC;AACJ,kBAAQA,IAAIF,EAAY,KAAK9B,CAAI,OAAO,QAAM;AAC5C,kBAAMiC,IAAOD,EAAE,CAAC,GAGVE,IADQlC,EAAK,UAAUgC,EAAE,OAAOA,EAAE,QAAQ,GAAG,EACzB,MAAM,yBAAyB,GACnDG,IAASD,IAAcA,EAAY,CAAC,EAAE,gBAAgB;AAC5D,YAAAH,EAAQ,KAAK,EAAE,MAAAE,GAAM,QAAAE,EAAA,CAAQ;AAAA,UAC/B;AAEA,cAAIJ,EAAQ,SAAS;AAqBnB,mBAAO;AAAA,cACL,MArBYA,EAAQ,IAAI,CAAC,EAAE,MAAAE,GAAM,QAAAE,QAAa;AAC9C,sBAAMzC,IAAY,GAAGuC,CAAI,IAAI9F,EAAUoD,CAAU,CAAC;AAClD,uBAAI4C,MAAW,QACN,yBAAyBF,CAAI;AAAA,0CACVvC,CAAS;AAAA;AAAA;AAAA,KAK9B,yBAAyBuC,CAAI;AAAA,0CACRvC,CAAS;AAAA,eACpCyC,CAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAOT,CAAC,EAGa,KAAK;AAAA;AAAA,CAAM;AAAA,cACvB,KAAK;AAAA,YAAA;AAAA,QAGX;AAAA,MACF;AAQA,UALIP,EAAG,SAAS,cAAc,KAK1BA,EAAG,SAAS,SAAS,KAAKA,EAAG,SAAS,WAAW;AACnD,eAAO;AAMT,UAFkB,eAAe,KAAKA,CAAE;AAGtC,YAAI;AACF,gBAAMQ,IAAW;AAAA,EAAkFpC,CAAI,IAGjGE,IAAS,MAAMmC,EAAUD,GAAU;AAAA,YACvC,QAAQR,EAAG,SAAS,MAAM,IAAI,QAAQ;AAAA,YACtC,KAAK;AAAA,YACL,YAAY;AAAA,YACZ,aAAa;AAAA,YACb,WAAW;AAAA,YACX,YAAYA;AAAA,YACZ,QAAQ;AAAA,UAAA,CACT;AAED,cAAIU,IAAkBpC,EAAO;AAG7B,iBAAIoC,EAAgB,SAAS,cAAc,MACzCA,IAAkBvC,EAAiBuC,GAAiBV,GAAI9E,GAAS6B,GAAWC,CAAQ,IAG/E;AAAA,YACL,MAAM0D;AAAA,YACN,KAAKpC,EAAO,OAAO;AAAA,UAAA;AAAA,QAEvB,SAASJ,GAAO;AAEd,wBAAQ,MAAM,oCAAoC8B,CAAE,KAAK9B,CAAK,GACxDA;AAAA,QACR;AAOF,UAAI+B,GAAS,KAAK;AAChB,YAAIS,IAAkBtC,GAClBuC,IAAa;AA2BjB,YAvBAD,IAAkBA,EAAgB;AAAA,UAChC;AAAA,UACA,CAAC5E,GAAO8E,MAEF/G,EAAkB,IAAI+G,CAAO,IAAU9E,KAC3C6E,IAAa,IACN,eAAeC,CAAO;AAAA,QAC/B,GAMFF,IAAkBA,EAAgB;AAAA,UAChC;AAAA,UACA,CAAC5E,GAAO+E,GAAOD,MAET/G,EAAkB,IAAI+G,CAAO,IAAU9E,KAC3C6E,IAAa,IACN,eAAeE,CAAK,GAAGD,CAAO,GAAGC,CAAK;AAAA,QAC/C,GAGEF;AACF,iBAAO;AAAA,YACL,MAAMD;AAAA,YACN,KAAK;AAAA;AAAA,UAAA;AAAA,MAGX;AACA,aAAO;AAAA,IACT;AAAA,IAEA,KAAKV,GAAI;AAEP,UAAIA,MAAO,OAAO3G;AAChB,eAAO;AAAA;AAAA;AAAA;AAOT,UAAI2G,MAAO,OAAO1G;AAEhB,eAAO;AAIT,UAAI0G,MAAO,OAAOzG;AAChB,eAAO;AAAA;AAKT,UAAIyG,MAAO,OAAOxG;AAChB,eAAO;AAIT,UAAIwG,MAAO,OAAOrG,GAAqB;AACrC,cAAM8F,IAAc,OAAO,YAAYxC,CAAc;AACrD,eAAO;AAAA,gCACiB,KAAK,UAAUwC,CAAW,CAAC;AAAA,MACrD;AAGA,UAAIO,MAAO,OAAOpG;AAChB,eAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+BT,UAAIoG,MAAO,OAAOtG;AAChB,eAAO;AAIT,UAAIsG,MAAO,OAAOvG,GAA+B;AAE/C,cAAMqH,IAAiBzG,EAAKa,GAAS,OAAO,sBAAsB;AAClE,eAAIK,EAAWuF,CAAc,IAEpB,wCAGF;AAAA,MACT;AAEA,aAAO;AAAA,IACT;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"vite-plugin.js","sources":["../src/core/vite-plugin.ts"],"sourcesContent":["import { transform } from 'esbuild';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n rmSync,\n statSync,\n writeFileSync,\n} from 'fs';\nimport { dirname, join, relative, resolve } from 'path';\nimport type { Plugin, UserConfig } from 'vite';\n\nconst VIRTUAL_L5E_VIEWS = 'virtual:l5e-views';\nconst VIRTUAL_L5E_ROUTE = 'virtual:l5e-route';\nconst VIRTUAL_L5E_SSR_ENTRY = 'virtual:l5e-ssr-entry';\nconst VIRTUAL_L5E_GLOBAL_LOADER = 'virtual:l5e-global-loader';\nconst VIRTUAL_L5E_ISLAND_STRATEGIES = 'virtual:l5e-island-strategies';\nconst VIRTUAL_L5E_ISLANDS = 'virtual:l5e-islands';\nconst VIRTUAL_L5E_ACTIONS = 'virtual:l5e-actions';\nconst VIRTUAL_L5E_MIDDLEWARE = 'virtual:l5e-middleware';\n\n/**\n * Vite's built-in env vars on `import.meta.env`. These are statically replaced\n * by Vite itself (DEV/PROD/SSR are booleans, MODE/BASE_URL strings), so the SSR\n * `import.meta.env.* -> process.env.*` rewrite must skip them — otherwise it\n * clobbers Vite defaults (e.g. `import.meta.env.DEV` would become the undefined\n * `process.env.DEV`).\n */\nconst VITE_RESERVED_ENV = new Set(['MODE', 'BASE_URL', 'PROD', 'DEV', 'SSR', 'LEGACY']);\n\n/**\n * Recursively scan directory for .tsx and .ts files\n */\nfunction scanTsFiles(dir: string, fileList: string[] = []): string[] {\n const files = readdirSync(dir);\n\n for (const file of files) {\n const filePath = join(dir, file);\n const stat = statSync(filePath);\n\n if (stat.isDirectory()) {\n // Skip node_modules and dist directories\n if (file === 'node_modules' || file === 'dist' || file === '.git') {\n continue;\n }\n scanTsFiles(filePath, fileList);\n } else if (file.endsWith('.tsx') || file.endsWith('.ts')) {\n fileList.push(filePath);\n }\n }\n\n return fileList;\n}\n\n/**\n * Short hash function for island keys\n */\nfunction shortHash(str: string): string {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash << 5) - hash + str.charCodeAt(i);\n hash = hash & hash;\n }\n return Math.abs(hash).toString(16).slice(0, 4);\n}\n\n/**\n * Derive component name from path: \"./react/Counter\" → \"Counter\"\n */\nfunction deriveComponentName(fromPath: string): string {\n const segments = fromPath.split('/');\n let filename = segments[segments.length - 1];\n // Remove extension if present\n filename = filename.replace(/\\.(tsx?|jsx?)$/, '');\n return filename;\n}\n\n/**\n * Create island registry key: \"/src/views/test-island/react/Counter\" → \"Counter_a3f2\"\n */\nfunction makeIslandKey(resolvedPath: string): string {\n const name = deriveComponentName(resolvedPath);\n return `${name}_${shortHash(resolvedPath)}`;\n}\n\n/**\n * Resolve file path with extension (.tsx, .ts, .jsx, .js)\n * Returns the relative path with extension (e.g., \"src/views/.../Counter.tsx\")\n * or null if file not found\n */\nfunction resolveWithExtension(resolvedPath: string, rootDir: string): string | null {\n // resolvedPath: \"/src/views/test-island/react/Counter\" (no extension, leading /)\n const relPath = resolvedPath.replace(/^\\//, ''); // \"src/views/.../Counter\"\n const absBase = resolve(rootDir, relPath);\n\n // Check with extensions\n for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {\n if (existsSync(absBase + ext)) {\n return relPath + ext; // \"src/views/.../Counter.tsx\"\n }\n }\n // Check without extension (file might already have one)\n if (existsSync(absBase)) {\n return relPath;\n }\n return null;\n}\n\n/**\n * Extract island entries from file using regex on JSX source\n */\nfunction extractIslandEntries(\n filePath: string,\n rootDir: string,\n): Array<{ component: string; resolvedPath: string; src: string; key: string }> {\n const content = readFileSync(filePath, 'utf-8');\n const entries: Array<{ component: string; resolvedPath: string; src: string; key: string }> = [];\n\n // Regex scan JSX source (NOT post-transform code).\n // Just find <ClientIsland ... from=\"Y\" ...> — component name derived from path.\n const regex = /<ClientIsland\\s[^>]*?from\\s*=\\s*\"([^\"]+)\"/g;\n\n const seen = new Set<string>();\n let match;\n\n while ((match = regex.exec(content)) !== null) {\n const fromPath = match[1];\n\n // Resolve relative path → absolute\n let resolvedPath: string;\n if (fromPath.startsWith('~/')) {\n // Alias ~/ → /src/ (project convention)\n resolvedPath = '/src/' + fromPath.substring(2);\n } else if (fromPath.startsWith('/src/')) {\n resolvedPath = fromPath;\n } else if (fromPath.startsWith('./') || fromPath.startsWith('../')) {\n const abs = resolve(filePath, '..', fromPath);\n resolvedPath = '/' + relative(rootDir, abs).replace(/\\\\/g, '/');\n } else {\n resolvedPath = fromPath;\n }\n\n // Resolve file extension for manifest compatibility\n const src = resolveWithExtension(resolvedPath, rootDir);\n if (!src) {\n console.warn(\n `[l5e] Island component not found: ${resolvedPath} (from ${fromPath} in ${filePath})`,\n );\n continue;\n }\n\n const key = makeIslandKey(resolvedPath);\n if (!seen.has(key)) {\n seen.add(key);\n entries.push({\n component: deriveComponentName(resolvedPath),\n resolvedPath,\n src, // \"src/views/.../Counter.tsx\" — matches manifest key format\n key,\n });\n }\n }\n return entries;\n}\n\n/**\n * Extract paths from useCss and useClientJs calls using regex\n */\nfunction extractPathsFromFile(filePath: string, rootDir: string): { css: string[]; js: string[] } {\n const content = readFileSync(filePath, 'utf-8');\n const css: string[] = [];\n const js: string[] = [];\n\n // Regex patterns to match useCss('path') or useCss(\"path\")\n // Handles single and double quotes, and escaped quotes\n const useCssPattern = /useCss\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,?\\s*\\)/g;\n const useClientJsPattern = /useClientJs\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,?\\s*\\)/g;\n\n let match;\n\n // Extract CSS paths\n while ((match = useCssPattern.exec(content)) !== null) {\n const path = match[1];\n // Convert relative path to absolute if needed\n if (path.startsWith('/src/')) {\n css.push(path);\n } else if (path.startsWith('./') || path.startsWith('../')) {\n // Resolve relative path\n const absolutePath = resolve(filePath, '..', path);\n const relativePath = '/' + relative(rootDir, absolutePath).replace(/\\\\/g, '/');\n css.push(relativePath);\n } else {\n css.push(path);\n }\n }\n\n // Extract JS paths\n while ((match = useClientJsPattern.exec(content)) !== null) {\n const path = match[1];\n if (path.startsWith('/src/')) {\n js.push(path);\n } else if (path.startsWith('./') || path.startsWith('../')) {\n const absolutePath = resolve(filePath, '..', path);\n const relativePath = '/' + relative(rootDir, absolutePath).replace(/\\\\/g, '/');\n js.push(relativePath);\n } else {\n js.push(path);\n }\n }\n\n return { css, js };\n}\n\n/**\n * Auto-discover rollup input entries from useCss and useClientJs calls\n */\nfunction discoverRollupInput(rootDir: string): {\n input: Record<string, string>;\n islandRegistry: Map<string, string>;\n pathToKey: Map<string, string>;\n keyToSrc: Map<string, string>;\n actionRegistry: Map<string, { modulePath: string; actionName: string }>;\n} {\n const input: Record<string, string> = {};\n const srcDir = join(rootDir, 'src');\n\n // Island registries\n const islandRegistry = new Map<string, string>(); // key → resolvedPath\n const pathToKey = new Map<string, string>(); // resolvedPath → key\n const keyToSrc = new Map<string, string>(); // key → src (manifest-compatible path with extension)\n\n // Action registry: actionKey → { modulePath, actionName }\n const actionRegistry = new Map<string, { modulePath: string; actionName: string }>();\n\n try {\n // Check if src directory exists\n if (!statSync(srcDir).isDirectory()) {\n return { input, islandRegistry, pathToKey, keyToSrc, actionRegistry };\n }\n\n // Check if src/client.global.ts exists and add it as an entry\n const globalTsPath = join(rootDir, 'src', 'client.global.ts');\n if (existsSync(globalTsPath)) {\n input['global'] = globalTsPath;\n console.log('[l5e] Detected src/client.global.ts and added as entry');\n }\n\n // Scan all .tsx and .ts files\n const tsFiles = scanTsFiles(srcDir);\n\n // Extract all paths\n const allCssPaths = new Set<string>();\n const allJsPaths = new Set<string>();\n\n for (const file of tsFiles) {\n const { css, js } = extractPathsFromFile(file, rootDir);\n css.forEach((path) => allCssPaths.add(path));\n js.forEach((path) => allJsPaths.add(path));\n\n // Extract island entries\n const islandEntries = extractIslandEntries(file, rootDir);\n for (const entry of islandEntries) {\n islandRegistry.set(entry.key, entry.resolvedPath);\n pathToKey.set(entry.resolvedPath, entry.key);\n keyToSrc.set(entry.key, entry.src);\n }\n\n // Extract action entries from actions.ts/tsx files\n const normalizedFile = file.replace(/\\\\/g, '/');\n if (/\\/actions\\.(ts|tsx)$/.test(normalizedFile)) {\n const content = readFileSync(file, 'utf-8');\n const actionExportRegex = /export\\s+const\\s+(\\w+)\\s*=\\s*defineAction\\s*\\(/g;\n let actionMatch;\n\n // Compute modulePath: relative path from src/ to parent dir\n const relFromSrc = relative(srcDir, dirname(file)).replace(/\\\\/g, '/');\n const modulePath = relFromSrc || '.';\n\n while ((actionMatch = actionExportRegex.exec(content)) !== null) {\n const actionName = actionMatch[1];\n const actionKey = `${actionName}_${shortHash(modulePath)}`;\n actionRegistry.set(actionKey, { modulePath, actionName });\n }\n }\n }\n\n // Convert paths to rollup input entries\n // CSS files\n for (const cssPath of allCssPaths) {\n // Remove leading /src/ and convert to relative path\n const relativePath = cssPath.startsWith('/src/')\n ? cssPath.substring(1) // Remove leading /\n : cssPath.startsWith('/')\n ? cssPath.substring(1)\n : cssPath;\n\n const absolutePath = resolve(rootDir, relativePath);\n\n // Only add if file exists\n if (!existsSync(absolutePath)) {\n console.warn(`[l5e] CSS file not found: ${absolutePath} (from ${cssPath})`);\n continue;\n }\n\n // Generate entry name from path (e.g., /src/views/home/home.css -> views-home-home)\n const entryName = relativePath\n .replace(/^src\\//, '')\n .replace(/\\.css$/, '')\n .replace(/\\//g, '-')\n .replace(/\\\\/g, '-');\n\n input[entryName] = absolutePath;\n }\n\n // JS/TS files\n for (const jsPath of allJsPaths) {\n const relativePath = jsPath.startsWith('/src/')\n ? jsPath.substring(1)\n : jsPath.startsWith('/')\n ? jsPath.substring(1)\n : jsPath;\n\n const absolutePath = resolve(rootDir, relativePath);\n\n // Only add if file exists\n if (!existsSync(absolutePath)) {\n console.warn(`[l5e] JS/TS file not found: ${absolutePath} (from ${jsPath})`);\n continue;\n }\n\n // Generate entry name from path\n const entryName = relativePath\n .replace(/^src\\//, '')\n .replace(/\\.(ts|tsx|js|jsx)$/, '')\n .replace(/\\//g, '-')\n .replace(/\\\\/g, '-');\n\n input[entryName] = absolutePath;\n }\n\n // Add island component files to rollup input so they appear in manifest.\n // server.ts looks up manifest at runtime to resolve per-page island URLs.\n for (const [key, src] of keyToSrc) {\n const absolutePath = resolve(rootDir, src);\n input[`island-${key}`] = absolutePath;\n }\n\n if (islandRegistry.size > 0) {\n console.log(`[l5e] Detected ${islandRegistry.size} island(s)`);\n }\n if (actionRegistry.size > 0) {\n console.log(`[l5e] Detected ${actionRegistry.size} action(s)`);\n }\n } catch (error) {\n // Silently fail if directory doesn't exist or other errors\n console.warn('[l5e] Failed to discover rollup input:', error);\n }\n\n return { input, islandRegistry, pathToKey, keyToSrc, actionRegistry };\n}\n\n/**\n * Inject __key prop into ClientIsland calls\n * Use anchor-based approach to avoid fragile regex with nested objects\n */\nfunction injectIslandKeys(\n code: string,\n fileId: string,\n rootDir: string,\n pathToKey: Map<string, string>,\n keyToSrc: Map<string, string>,\n): string {\n // After esbuild, code has form:\n // jsxFactory(ClientIsland, { from: \"./react/Counter\", props: { n: 5 } })\n // ^anchor ^find from here\n\n const result: string[] = [];\n let lastIndex = 0;\n\n // Find each position \"ClientIsland,\" (anchor)\n const anchorRegex = /ClientIsland\\s*,\\s*\\{/g;\n let anchorMatch;\n\n while ((anchorMatch = anchorRegex.exec(code)) !== null) {\n const searchStart = anchorMatch.index + anchorMatch[0].length;\n\n // Scan for from: \"...\" in window ~500 chars after anchor\n const window = code.substring(searchStart, searchStart + 500);\n const fromMatch = /from:\\s*\"([^\"]+)\"/.exec(window);\n\n if (!fromMatch) continue;\n\n const fromPath = fromMatch[1];\n\n // Resolve path (handle ~/ alias)\n let resolved: string;\n if (fromPath.startsWith('~/')) {\n resolved = '/src/' + fromPath.substring(2);\n } else if (fromPath.startsWith('/src/')) {\n resolved = fromPath;\n } else if (fromPath.startsWith('./') || fromPath.startsWith('../')) {\n const abs = resolve(fileId, '..', fromPath);\n resolved = '/' + relative(rootDir, abs).replace(/\\\\/g, '/');\n } else {\n resolved = fromPath;\n }\n\n const key = pathToKey.get(resolved);\n if (!key) continue;\n\n const src = keyToSrc.get(key);\n if (!src) continue;\n\n // Inject __key and __src right after from: \"...\"\n const insertPos = searchStart + fromMatch.index + fromMatch[0].length;\n result.push(code.substring(lastIndex, insertPos));\n result.push(`, __key: \"${key}\", __src: \"${src}\"`);\n lastIndex = insertPos;\n }\n\n result.push(code.substring(lastIndex));\n return result.join('');\n}\n\nexport function coreVite(): Plugin {\n let rootDir: string = process.cwd();\n let islandRegistry = new Map<string, string>();\n let pathToKey = new Map<string, string>();\n let keyToSrc = new Map<string, string>();\n let actionRegistry = new Map<string, { modulePath: string; actionName: string }>();\n\n return {\n name: 'l5e-jsx-classic',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n // Store root directory for later use\n rootDir = resolvedConfig.root || process.cwd();\n },\n\n handleHotUpdate({ file, server }) {\n const relPath = relative(rootDir, file);\n\n // Re-scan action registry when an actions file changes\n if (/actions\\.(ts|tsx)$/.test(file)) {\n const discovered = discoverRollupInput(rootDir);\n actionRegistry = discovered.actionRegistry;\n // Invalidate the virtual:l5e-actions module so server picks up new registry\n const mod = server.moduleGraph.getModuleById('\\0' + VIRTUAL_L5E_ACTIONS);\n if (mod) {\n server.moduleGraph.invalidateModule(mod);\n }\n console.log(\n `[l5e] Action file changed: ${relPath} — re-scanned ${actionRegistry.size} action(s)`,\n );\n }\n\n // Allow Vite's default HMR for CSS and client-side JS files\n if (file.endsWith('.css')) {\n console.log(`[l5e] CSS changed: ${relPath} - using Vite HMR`);\n return; // Let Vite handle CSS HMR\n }\n\n if (file.includes('client.ts')) {\n console.log(`[l5e] Client JS changed: ${relPath} - using Vite HMR`);\n return; // Let Vite handle client JS HMR\n }\n\n // Trigger full page reload for SSR-related file changes (components, loaders, routes)\n // This ensures that server-side rendered components update properly\n if (file.includes('/src/') || file.includes('\\\\src\\\\')) {\n console.log(`[l5e] SSR file changed: ${relPath} - triggering full reload`);\n server.ws.send({\n type: 'full-reload',\n path: '*',\n });\n return []; // Prevent Vite's default HMR behavior\n }\n },\n\n buildEnd() {\n // Cleanup temporary files after build\n const tempDir = join(rootDir, '.l5e-temp');\n if (existsSync(tempDir)) {\n try {\n rmSync(tempDir, { recursive: true, force: true });\n console.log('[l5e] Cleaned up temporary files');\n } catch (error) {\n console.warn('[l5e] Failed to cleanup temporary files:', error);\n }\n }\n },\n\n writeBundle(_options, _bundle) {\n // Emit action registry JSON for production server\n if (actionRegistry.size > 0) {\n const outDir = join(rootDir, 'dist', 'server');\n if (!existsSync(outDir)) {\n mkdirSync(outDir, { recursive: true });\n }\n const registryObj = Object.fromEntries(actionRegistry);\n writeFileSync(join(outDir, 'action-registry.json'), JSON.stringify(registryObj), 'utf-8');\n console.log(`[l5e] Wrote action-registry.json (${actionRegistry.size} actions)`);\n }\n },\n\n config(userConfig) {\n // Auto-discover rollup input from useCss and useClientJs\n const projectRoot = userConfig.root || process.cwd();\n const discovered = discoverRollupInput(projectRoot);\n\n // Store island registries for use in other hooks\n islandRegistry = discovered.islandRegistry;\n pathToKey = discovered.pathToKey;\n keyToSrc = discovered.keyToSrc;\n actionRegistry = discovered.actionRegistry;\n\n // Merge with existing rollupOptions.input if any\n const existingInput = userConfig.build?.rollupOptions?.input || {};\n const mergedInput =\n typeof existingInput === 'object' && !Array.isArray(existingInput)\n ? { ...discovered.input, ...existingInput }\n : discovered.input;\n\n return {\n build: {\n ...userConfig.build,\n rollupOptions: {\n ...userConfig.build?.rollupOptions,\n input: Object.keys(mergedInput).length > 0 ? mergedInput : undefined,\n // Preserve exports for island component entries — without this,\n // Rollup tree-shakes their exports since nothing in the bundle imports them\n // (they're loaded at runtime via dynamic import from the island runtime).\n preserveEntrySignatures: 'exports-only',\n },\n },\n } satisfies UserConfig;\n },\n\n resolveId(id) {\n if (id === VIRTUAL_L5E_VIEWS) {\n return '\\0' + VIRTUAL_L5E_VIEWS;\n }\n if (id === VIRTUAL_L5E_ROUTE) {\n return '\\0' + VIRTUAL_L5E_ROUTE;\n }\n if (id === VIRTUAL_L5E_SSR_ENTRY) {\n return '\\0' + VIRTUAL_L5E_SSR_ENTRY;\n }\n if (id === VIRTUAL_L5E_GLOBAL_LOADER) {\n return '\\0' + VIRTUAL_L5E_GLOBAL_LOADER;\n }\n if (id === VIRTUAL_L5E_ISLAND_STRATEGIES) {\n return '\\0' + VIRTUAL_L5E_ISLAND_STRATEGIES;\n }\n if (id === VIRTUAL_L5E_ISLANDS) {\n return '\\0' + VIRTUAL_L5E_ISLANDS;\n }\n if (id === VIRTUAL_L5E_ACTIONS) {\n return '\\0' + VIRTUAL_L5E_ACTIONS;\n }\n if (id === VIRTUAL_L5E_MIDDLEWARE) {\n return '\\0' + VIRTUAL_L5E_MIDDLEWARE;\n }\n return null;\n },\n\n async transform(code, id, options) {\n // Auto-inject island runtime into client.global.ts so it's always loaded globally\n if (id.replace(/\\\\/g, '/').endsWith('src/client.global.ts') && !options?.ssr) {\n return {\n code: `import '@withl5e/l5e/island/runtime';\\n${code}`,\n map: null,\n };\n }\n\n // Client-side action transform: replace defineAction exports with fetch stubs\n // Supports actions anywhere under src/ (e.g., src/views/*, src/features/*, etc.)\n if (!options?.ssr) {\n const normalizedId = id.replace(/\\\\/g, '/');\n const actionMatch = normalizedId.match(/\\/src\\/(.+)\\/actions\\.(ts|tsx)$/);\n if (actionMatch) {\n const modulePath = actionMatch[1];\n\n // Parse exported action names\n const exportRegex = /export\\s+const\\s+(\\w+)\\s*=\\s*defineAction\\s*\\(/g;\n const actions: Array<{ name: string; method: string }> = [];\n let m;\n while ((m = exportRegex.exec(code)) !== null) {\n const name = m[1];\n // Find method for this action — scan from the defineAction( position\n const chunk = code.substring(m.index, m.index + 500);\n const methodMatch = chunk.match(/method:\\s*['\"](\\w+)['\"]/);\n const method = methodMatch ? methodMatch[1].toUpperCase() : 'GET';\n actions.push({ name, method });\n }\n\n if (actions.length > 0) {\n const stubs = actions.map(({ name, method }) => {\n const actionKey = `${name}_${shortHash(modulePath)}`;\n if (method === 'GET') {\n return `export async function ${name}(params) {\n const res = await fetch('/_l5e/action/${actionKey}?' + new URLSearchParams(params));\n if (!res.ok) throw Object.assign(new Error('HTTP ' + res.status), { status: res.status });\n return res;\n}`;\n }\n return `export async function ${name}(body) {\n const res = await fetch('/_l5e/action/${actionKey}', {\n method: '${method}',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw Object.assign(new Error('HTTP ' + res.status), { status: res.status });\n return res;\n}`;\n });\n\n return {\n code: stubs.join('\\n\\n'),\n map: null,\n };\n }\n }\n }\n\n // Skip node_modules\n if (id.includes('node_modules')) {\n return null;\n }\n\n // Skip files in /react/ directories\n if (id.includes('/react/') || id.includes('\\\\react\\\\')) {\n return null;\n }\n\n // Check if this is a JSX/TSX file that needs L5E JSX transformation\n const isJsxFile = /\\.(tsx|jsx)$/.test(id);\n\n if (isJsxFile) {\n try {\n const injected = `import { Fragment as __Fragment, jsxFactory } from \"@withl5e/l5e/jsx-runtime\"\\n${code}`;\n\n // Transform JSX using esbuild with L5E's JSX runtime (async)\n const result = await transform(injected, {\n loader: id.endsWith('.tsx') ? 'tsx' : 'jsx',\n jsx: 'transform',\n jsxFactory: 'jsxFactory',\n jsxFragment: '__Fragment',\n sourcemap: true,\n sourcefile: id,\n target: 'es2020',\n });\n\n let transformedCode = result.code;\n\n // Inject __key and __src for ALL ClientIsland calls\n if (transformedCode.includes('ClientIsland')) {\n transformedCode = injectIslandKeys(transformedCode, id, rootDir, pathToKey, keyToSrc);\n }\n\n return {\n code: transformedCode,\n map: result.map || null,\n };\n } catch (error) {\n // Log error but let Vite handle it\n console.error(`[l5e] Failed to transform JSX in ${id}:`, error);\n throw error;\n }\n }\n\n // Transform import.meta.env thà nh process.env ở server side\n // Server side có thể access tất cả env variables\n // Client side chỉ access được VITE_ env variables\n // Transform nà y chạy ở runtime khi module được load trong SSR context\n if (options?.ssr) {\n let transformedCode = code;\n let hasChanges = false;\n\n // Thay thế import.meta.env.VARIABLE_NAME thà nh process.env.VARIABLE_NAME\n // Match: import.meta.env.VITE_EVENT_CATEGORY_SLUG\n transformedCode = transformedCode.replace(\n /import\\.meta\\.env\\.([a-zA-Z_][a-zA-Z0-9_]*)/g,\n (match, varName) => {\n // Leave Vite's built-in env vars for Vite to handle.\n if (VITE_RESERVED_ENV.has(varName)) return match;\n hasChanges = true;\n return `process.env.${varName}`;\n },\n );\n\n // Thay thế import.meta.env['VARIABLE_NAME'] hoặc import.meta.env[\"VARIABLE_NAME\"]\n // thà nh process.env['VARIABLE_NAME'] hoặc process.env[\"VARIABLE_NAME\"]\n // Match: import.meta.env['VITE_EVENT_CATEGORY_SLUG'] hoặc import.meta.env[\"VITE_EVENT_CATEGORY_SLUG\"]\n transformedCode = transformedCode.replace(\n /import\\.meta\\.env\\[(['\"`])([^'\"`]+)\\1\\]/g,\n (match, quote, varName) => {\n // Leave Vite's built-in env vars for Vite to handle.\n if (VITE_RESERVED_ENV.has(varName)) return match;\n hasChanges = true;\n return `process.env[${quote}${varName}${quote}]`;\n },\n );\n\n if (hasChanges) {\n return {\n code: transformedCode,\n map: null, // Không cần source map cho env transform\n };\n }\n }\n return null;\n },\n\n load(id) {\n // Virtual module: l5e-views\n if (id === '\\0' + VIRTUAL_L5E_VIEWS) {\n return `\nexport const viewLoaders = import.meta.glob('/src/views/*/loader.{ts,tsx}');\nexport const viewComponents = import.meta.glob('/src/views/*/index.tsx');\n`;\n }\n\n // Virtual module: l5e-route\n if (id === '\\0' + VIRTUAL_L5E_ROUTE) {\n // Load /src/route.ts LAZILY (dynamic import inside the handler) rather\n // than via a static `export { default } from '/src/route.ts'`.\n //\n // A static re-export drags the user's route module — and everything it\n // imports — into entry-server's *static* SSR graph. Route files commonly\n // import the `@withl5e/l5e` barrel (for RedirectException, types, ...),\n // and that barrel re-exports `render` from entry-server. That closes a\n // static import cycle:\n // entry-server → virtual:l5e-route → /src/route.ts → @withl5e/l5e → entry-server\n // On the first dev request Vite can begin evaluating entry-server while\n // it is still suspended awaiting this very import, so `render()` runs\n // before `const __vite_ssr_import_N__ = await import('virtual:l5e-route')`\n // has been assigned — throwing \"Cannot access '__vite_ssr_import_N__'\n // before initialization\". A page reload \"fixes\" it only because the graph\n // is fully warm the second time.\n //\n // Importing lazily removes /src/route.ts from the static graph: entry-server\n // finishes initializing first, and the route module (with its barrel import\n // back to a now-complete entry-server) is pulled only on the first render()\n // call. No manual caching — Vite/ESM caches the evaluated module and later\n // imports still observe HMR invalidations of route.ts.\n return `export default function routeHandler(requestInfo) {\n return import('/src/route.ts').then((mod) => mod.default(requestInfo));\n}`;\n }\n\n // Virtual module: l5e-ssr-entry\n if (id === '\\0' + VIRTUAL_L5E_SSR_ENTRY) {\n return `export { render } from '@withl5e/l5e/entry-server';\nexport { viewActions } from 'virtual:l5e-actions';`;\n }\n\n // Virtual module: l5e-global-loader\n if (id === '\\0' + VIRTUAL_L5E_GLOBAL_LOADER) {\n return `export const globalLoader = import.meta.glob('/src/global-loader.{ts,tsx}', { eager: false });`;\n }\n\n // Virtual module: l5e-actions\n if (id === '\\0' + VIRTUAL_L5E_ACTIONS) {\n const registryObj = Object.fromEntries(actionRegistry);\n return `export const viewActions = import.meta.glob('/src/**/actions.{ts,tsx}');\nexport const actionRegistry = ${JSON.stringify(registryObj)};`;\n }\n\n // Virtual module: l5e-middleware\n if (id === '\\0' + VIRTUAL_L5E_MIDDLEWARE) {\n return `\nconst middlewareModules = import.meta.glob([\n '/src/middleware.{ts,tsx,js,jsx}',\n '/src/middleware/index.{ts,tsx,js,jsx}',\n]);\n\nconst middlewarePaths = [\n '/src/middleware.ts',\n '/src/middleware.tsx',\n '/src/middleware.js',\n '/src/middleware.jsx',\n '/src/middleware/index.ts',\n '/src/middleware/index.tsx',\n '/src/middleware/index.js',\n '/src/middleware/index.jsx',\n];\n\nexport async function loadMiddleware() {\n const middlewarePath = middlewarePaths.find((path) => middlewareModules[path]);\n if (!middlewarePath) return undefined;\n\n const mod = await middlewareModules[middlewarePath]();\n return mod.onRequest;\n}\n`;\n }\n\n // Virtual module: l5e-islands\n // Lazy glob of all React island components so the SSR entry can import a\n // component by its __src path and renderToString() it. Non-eager → only\n // islands actually present on a page (with ssr) get imported at runtime.\n if (id === '\\0' + VIRTUAL_L5E_ISLANDS) {\n return `export const islandModules = import.meta.glob('/src/**/react/*.{tsx,jsx}');`;\n }\n\n // Virtual module: l5e-island-strategies\n if (id === '\\0' + VIRTUAL_L5E_ISLAND_STRATEGIES) {\n // Check if src/island-strategies.ts exists\n const strategiesFile = join(rootDir, 'src', 'island-strategies.ts');\n if (existsSync(strategiesFile)) {\n // Re-export user's file → Vite will build this file\n return `import '/src/island-strategies.ts';`;\n }\n // No file → empty module, no error\n return `/* no custom island strategies */`;\n }\n\n return null;\n },\n };\n}\n\nexport default coreVite;\n"],"names":["VIRTUAL_L5E_VIEWS","VIRTUAL_L5E_ROUTE","VIRTUAL_L5E_SSR_ENTRY","VIRTUAL_L5E_GLOBAL_LOADER","VIRTUAL_L5E_ISLAND_STRATEGIES","VIRTUAL_L5E_ISLANDS","VIRTUAL_L5E_ACTIONS","VIRTUAL_L5E_MIDDLEWARE","VITE_RESERVED_ENV","scanTsFiles","dir","fileList","files","readdirSync","file","filePath","join","statSync","shortHash","str","hash","i","deriveComponentName","fromPath","segments","filename","makeIslandKey","resolvedPath","resolveWithExtension","rootDir","relPath","absBase","resolve","ext","existsSync","extractIslandEntries","content","readFileSync","entries","regex","seen","match","abs","relative","src","key","extractPathsFromFile","css","js","useCssPattern","useClientJsPattern","path","absolutePath","relativePath","discoverRollupInput","input","srcDir","islandRegistry","pathToKey","keyToSrc","actionRegistry","globalTsPath","tsFiles","allCssPaths","allJsPaths","islandEntries","entry","normalizedFile","actionExportRegex","actionMatch","modulePath","dirname","actionName","actionKey","cssPath","entryName","jsPath","error","injectIslandKeys","code","fileId","result","lastIndex","anchorRegex","anchorMatch","searchStart","window","fromMatch","resolved","insertPos","coreVite","resolvedConfig","server","mod","tempDir","rmSync","_options","_bundle","outDir","mkdirSync","registryObj","writeFileSync","userConfig","projectRoot","discovered","existingInput","mergedInput","id","options","exportRegex","actions","m","name","methodMatch","method","injected","transform","transformedCode","hasChanges","varName","quote","strategiesFile"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/core/entry-server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getCssEntries,
|
|
18
18
|
getHeadContent,
|
|
19
19
|
getIslandEntries,
|
|
20
|
+
getIslandProps,
|
|
20
21
|
getSchemas,
|
|
21
22
|
getSsrIslands,
|
|
22
23
|
jsxFactory as h,
|
|
@@ -111,6 +112,12 @@ export interface RenderResult {
|
|
|
111
112
|
scripts?: string[];
|
|
112
113
|
styles?: string[];
|
|
113
114
|
islands?: Array<{ key: string; src: string; name: string }>;
|
|
115
|
+
/**
|
|
116
|
+
* Externalized island props (index === element's `data-island-idx`). Present
|
|
117
|
+
* only in externalized mode; createPageResponse serializes it into the
|
|
118
|
+
* `<script id="_l5e_data_">` at the end of the document.
|
|
119
|
+
*/
|
|
120
|
+
islandData?: unknown[];
|
|
114
121
|
head?: string;
|
|
115
122
|
lang?: string;
|
|
116
123
|
statusCode?: number;
|
|
@@ -221,7 +228,20 @@ async function renderErrorView(err: HttpException, lang?: string): Promise<Rende
|
|
|
221
228
|
}
|
|
222
229
|
}
|
|
223
230
|
|
|
224
|
-
export
|
|
231
|
+
export interface RenderOptions {
|
|
232
|
+
/**
|
|
233
|
+
* Externalize island props into the trailing `_l5e_data_` script instead of
|
|
234
|
+
* inlining `data-island-props` on each element. Default true.
|
|
235
|
+
*/
|
|
236
|
+
externalizeIslandProps?: boolean;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function render(
|
|
240
|
+
url: string,
|
|
241
|
+
requestInfo: RequestInfo = {},
|
|
242
|
+
options: RenderOptions = {},
|
|
243
|
+
): Promise<RenderResult> {
|
|
244
|
+
const externalizeIslandProps = options.externalizeIslandProps ?? true;
|
|
225
245
|
return runInRenderContext(async () => {
|
|
226
246
|
try {
|
|
227
247
|
// Step 1: Call route handler to get view name
|
|
@@ -409,6 +429,7 @@ export async function render(url: string, requestInfo: RequestInfo = {}): Promis
|
|
|
409
429
|
const clientEntries = getClientJsEntries();
|
|
410
430
|
const cssEntries = getCssEntries();
|
|
411
431
|
const islandEntries = getIslandEntries();
|
|
432
|
+
const islandData = getIslandProps();
|
|
412
433
|
const cacheTags = getCacheTags();
|
|
413
434
|
const headContent = getHeadContent();
|
|
414
435
|
|
|
@@ -423,6 +444,7 @@ export async function render(url: string, requestInfo: RequestInfo = {}): Promis
|
|
|
423
444
|
scripts: clientEntries.map((entry) => entry.path),
|
|
424
445
|
styles: cssEntries.map((entry) => entry.path),
|
|
425
446
|
islands: islandEntries.length > 0 ? islandEntries : undefined,
|
|
447
|
+
islandData: islandData.length > 0 ? islandData : undefined,
|
|
426
448
|
head: headHtml,
|
|
427
449
|
lang,
|
|
428
450
|
maxAge,
|
|
@@ -463,5 +485,5 @@ export async function render(url: string, requestInfo: RequestInfo = {}): Promis
|
|
|
463
485
|
});
|
|
464
486
|
return await renderErrorView(serviceError);
|
|
465
487
|
}
|
|
466
|
-
}, requestInfo);
|
|
488
|
+
}, requestInfo, undefined, { externalizeIslandProps });
|
|
467
489
|
}
|
package/src/core/jsx-runtime.ts
CHANGED
|
@@ -82,6 +82,15 @@ interface RenderContext {
|
|
|
82
82
|
cssRegistry: Array<{ path: string; from: string }>;
|
|
83
83
|
islandRegistry: IslandEntry[];
|
|
84
84
|
ssrIslands: SsrIslandEntry[];
|
|
85
|
+
/**
|
|
86
|
+
* When true, ClientIsland writes its props into `islandProps` (referenced by a
|
|
87
|
+
* `data-island-idx`) instead of inlining a `data-island-props` attribute, so a
|
|
88
|
+
* single `<script id="_l5e_data_">` at the end of the document carries all
|
|
89
|
+
* island props — keeping the SSR HTML lean for crawlers reading top-down.
|
|
90
|
+
*/
|
|
91
|
+
externalizeIslandProps: boolean;
|
|
92
|
+
/** Ordered island props; index === the element's `data-island-idx`. */
|
|
93
|
+
islandProps: unknown[];
|
|
85
94
|
cacheTags: Set<string>;
|
|
86
95
|
headRegistry: HeadEntry[]; // Thay vì JSXChild[]
|
|
87
96
|
metadataStack: Metadata[]; // Stack để track metadata hierarchy
|
|
@@ -92,13 +101,23 @@ interface RenderContext {
|
|
|
92
101
|
|
|
93
102
|
const renderStore = new AsyncLocalStorage<RenderContext>();
|
|
94
103
|
|
|
104
|
+
export interface RenderContextOptions {
|
|
105
|
+
/** Default true — see RenderContext.externalizeIslandProps. */
|
|
106
|
+
externalizeIslandProps?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
// Create context for each request
|
|
96
|
-
function createRequestContext(
|
|
110
|
+
function createRequestContext(
|
|
111
|
+
requestInfo: RequestInfo,
|
|
112
|
+
options?: RenderContextOptions,
|
|
113
|
+
): RenderContext {
|
|
97
114
|
return {
|
|
98
115
|
clientJsRegistry: [],
|
|
99
116
|
cssRegistry: [],
|
|
100
117
|
islandRegistry: [],
|
|
101
118
|
ssrIslands: [],
|
|
119
|
+
externalizeIslandProps: options?.externalizeIslandProps ?? false,
|
|
120
|
+
islandProps: [],
|
|
102
121
|
cacheTags: new Set(),
|
|
103
122
|
headRegistry: [],
|
|
104
123
|
metadataStack: [],
|
|
@@ -107,11 +126,26 @@ function createRequestContext(requestInfo: RequestInfo): RenderContext {
|
|
|
107
126
|
};
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Chuẩn hoá path asset về dạng web path có leading `/`.
|
|
131
|
+
* Dev sinh thẻ thẳng từ path này, còn prod strip leading `/` để tra manifest —
|
|
132
|
+
* không normalize thì `'src/a.css'` và `'/src/a.css'` là hai entry khác nhau ở
|
|
133
|
+
* dev nhưng lại trỏ cùng một manifest key ở prod.
|
|
134
|
+
*/
|
|
135
|
+
function normalizeAssetPath(path: string): string {
|
|
136
|
+
const normalized = path.trim().replace(/\\/g, '/');
|
|
137
|
+
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
export function useClientJs(path: string): string {
|
|
111
|
-
if (typeof path === 'string' && path.length > 0) {
|
|
141
|
+
if (typeof path === 'string' && path.trim().length > 0) {
|
|
112
142
|
const renderContext = renderStore.getStore();
|
|
113
143
|
if (renderContext) {
|
|
114
|
-
|
|
144
|
+
const normalized = normalizeAssetPath(path);
|
|
145
|
+
// Dedupe theo path, giữ thứ tự lần gọi đầu tiên
|
|
146
|
+
if (!renderContext.clientJsRegistry.some((entry) => entry.path === normalized)) {
|
|
147
|
+
renderContext.clientJsRegistry.push({ path: normalized, from: 'Unknown' });
|
|
148
|
+
}
|
|
115
149
|
}
|
|
116
150
|
}
|
|
117
151
|
return '';
|
|
@@ -133,6 +167,29 @@ export function getIslandEntries(): IslandEntry[] {
|
|
|
133
167
|
return context.islandRegistry.slice();
|
|
134
168
|
}
|
|
135
169
|
|
|
170
|
+
/** True when island props should be externalized into the `_l5e_data_` script. */
|
|
171
|
+
export function isIslandPropsExternalized(): boolean {
|
|
172
|
+
return renderStore.getStore()?.externalizeIslandProps ?? false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Append island props to the per-request store and return its index (used as the
|
|
177
|
+
* element's `data-island-idx`). Returns -1 outside a render context so the caller
|
|
178
|
+
* can fall back to inlining the props.
|
|
179
|
+
*/
|
|
180
|
+
export function pushIslandProps(props: unknown): number {
|
|
181
|
+
const context = renderStore.getStore();
|
|
182
|
+
if (!context) return -1;
|
|
183
|
+
return context.islandProps.push(props) - 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Ordered island props for the trailing `_l5e_data_` script (index === data-island-idx). */
|
|
187
|
+
export function getIslandProps(): unknown[] {
|
|
188
|
+
const context = renderStore.getStore();
|
|
189
|
+
if (!context) return [];
|
|
190
|
+
return context.islandProps.slice();
|
|
191
|
+
}
|
|
192
|
+
|
|
136
193
|
/**
|
|
137
194
|
* Register a pending SSR island render. Returns a unique placeholder token that
|
|
138
195
|
* the caller embeds (as raw HTML) in the island's body. entry-server replaces
|
|
@@ -153,10 +210,14 @@ export function getSsrIslands(): SsrIslandEntry[] {
|
|
|
153
210
|
}
|
|
154
211
|
|
|
155
212
|
export function useCss(path: string): string {
|
|
156
|
-
if (typeof path === 'string' && path.length > 0) {
|
|
213
|
+
if (typeof path === 'string' && path.trim().length > 0) {
|
|
157
214
|
const renderContext = renderStore.getStore();
|
|
158
215
|
if (renderContext) {
|
|
159
|
-
|
|
216
|
+
const normalized = normalizeAssetPath(path);
|
|
217
|
+
// Dedupe theo path — thứ tự lần gọi đầu tiên quyết định thứ tự cascade
|
|
218
|
+
if (!renderContext.cssRegistry.some((entry) => entry.path === normalized)) {
|
|
219
|
+
renderContext.cssRegistry.push({ path: normalized, from: 'Unknown' });
|
|
220
|
+
}
|
|
160
221
|
}
|
|
161
222
|
}
|
|
162
223
|
return '';
|
|
@@ -167,8 +228,9 @@ export function runInRenderContext<T>(
|
|
|
167
228
|
renderFn: () => T | Promise<T>,
|
|
168
229
|
requestInfo: RequestInfo,
|
|
169
230
|
viewName?: string,
|
|
231
|
+
options?: RenderContextOptions,
|
|
170
232
|
): Promise<T> {
|
|
171
|
-
const context = createRequestContext(requestInfo);
|
|
233
|
+
const context = createRequestContext(requestInfo, options);
|
|
172
234
|
if (viewName) {
|
|
173
235
|
context.viewName = viewName;
|
|
174
236
|
}
|
package/src/core/server.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface ServerOptions {
|
|
|
19
19
|
publicDir?: string;
|
|
20
20
|
setupApp?: (app: any) => void | Promise<void>;
|
|
21
21
|
app?: any; // Express
|
|
22
|
+
/**
|
|
23
|
+
* Externalize React island props into a single `<script id="_l5e_data_">` at
|
|
24
|
+
* the end of the document (referenced by `data-island-idx`) instead of inlining
|
|
25
|
+
* a large `data-island-props` attribute on each element — keeps the SSR HTML
|
|
26
|
+
* lean so crawlers read content first. Default true; set false for the legacy
|
|
27
|
+
* inline behavior.
|
|
28
|
+
*/
|
|
29
|
+
externalizeIslandProps?: boolean;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export interface ServerContext {
|
|
@@ -46,7 +54,11 @@ export function applyHtmlLang(template: string, lang: string): string {
|
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
type EntryServerModule = {
|
|
49
|
-
render: (
|
|
57
|
+
render: (
|
|
58
|
+
url: string,
|
|
59
|
+
requestInfo?: RequestInfo,
|
|
60
|
+
options?: { externalizeIslandProps?: boolean },
|
|
61
|
+
) => Promise<RenderResult>;
|
|
50
62
|
loadMiddleware?: () => Promise<MiddlewareHandler | undefined>;
|
|
51
63
|
};
|
|
52
64
|
|
|
@@ -235,6 +247,18 @@ async function createPageResponse({
|
|
|
235
247
|
let globalScripts: string[] = [];
|
|
236
248
|
let islandRegistryScript = '';
|
|
237
249
|
|
|
250
|
+
// Externalized island props → a single JSON script at the end of the document.
|
|
251
|
+
// serialize-javascript with isJSON escapes `<`, `>`, `&` (and U+2028/2029) so the
|
|
252
|
+
// props can't break out of the <script> block or inject markup (XSS). The runtime
|
|
253
|
+
// reads it via `document.getElementById('_l5e_data_')` + JSON.parse.
|
|
254
|
+
let islandDataScript = '';
|
|
255
|
+
if (rendered.islandData && rendered.islandData.length > 0) {
|
|
256
|
+
islandDataScript = `<script type="application/json" id="_l5e_data_">${serialize(
|
|
257
|
+
rendered.islandData,
|
|
258
|
+
{ isJSON: true },
|
|
259
|
+
)}</script>`;
|
|
260
|
+
}
|
|
261
|
+
|
|
238
262
|
if (isProduction && manifest) {
|
|
239
263
|
scriptSrcList = scriptSrcList.filter((src) => !src.includes('.global.'));
|
|
240
264
|
cssSrcList = cssSrcList.filter((src) => !src.includes('.global.'));
|
|
@@ -330,7 +354,10 @@ async function createPageResponse({
|
|
|
330
354
|
|
|
331
355
|
let cssHtml = '';
|
|
332
356
|
if (!isProduction) {
|
|
333
|
-
|
|
357
|
+
// Registry đã dedupe, nhưng vẫn lọc lại ở đây để dev không bao giờ ra thẻ trùng
|
|
358
|
+
cssHtml = [...new Set(cssSrcList)]
|
|
359
|
+
.map((src) => `<link rel="stylesheet" href="${src}">`)
|
|
360
|
+
.join('');
|
|
334
361
|
}
|
|
335
362
|
|
|
336
363
|
let allScripts = [...globalScripts, ...scriptSrcList];
|
|
@@ -341,6 +368,10 @@ async function createPageResponse({
|
|
|
341
368
|
allScripts = ['/src/client.global.ts', ...allScripts];
|
|
342
369
|
}
|
|
343
370
|
|
|
371
|
+
// useClientJs('/src/client.global.ts') do user tự gọi sẽ trùng với entry
|
|
372
|
+
// được prepend ở trên — registry không bắt được ca này nên dedupe lại
|
|
373
|
+
allScripts = [...new Set(allScripts)];
|
|
374
|
+
|
|
344
375
|
if (islandEntries.length > 0) {
|
|
345
376
|
const islandMap: Record<string, string> = {};
|
|
346
377
|
for (const island of islandEntries) {
|
|
@@ -351,6 +382,7 @@ async function createPageResponse({
|
|
|
351
382
|
}
|
|
352
383
|
|
|
353
384
|
const scriptsHtml =
|
|
385
|
+
islandDataScript +
|
|
354
386
|
islandRegistryScript +
|
|
355
387
|
allScripts.map((src) => `<script type="module" src="${src}"></script>`).join('');
|
|
356
388
|
|
|
@@ -394,6 +426,7 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
|
|
|
394
426
|
const root = options.root || process.cwd();
|
|
395
427
|
const base = options.base || '/';
|
|
396
428
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
429
|
+
const externalizeIslandProps = options.externalizeIslandProps ?? true;
|
|
397
430
|
|
|
398
431
|
// Cached production assets
|
|
399
432
|
const templateHtml = isProduction
|
|
@@ -646,7 +679,11 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
|
|
|
646
679
|
const url = req.originalUrl.replace(base, '');
|
|
647
680
|
|
|
648
681
|
let template: string;
|
|
649
|
-
let render: (
|
|
682
|
+
let render: (
|
|
683
|
+
url: string,
|
|
684
|
+
requestInfo?: any,
|
|
685
|
+
options?: { externalizeIslandProps?: boolean },
|
|
686
|
+
) => Promise<any>;
|
|
650
687
|
let loadMiddleware: EntryServerModule['loadMiddleware'];
|
|
651
688
|
let manifest: Record<string, any> | undefined;
|
|
652
689
|
|
|
@@ -700,7 +737,9 @@ export async function createServer(options: ServerOptions = {}): Promise<ServerC
|
|
|
700
737
|
const renderResponse = async (webRequest: globalThis.Request) => {
|
|
701
738
|
const nextRequestInfo = createRequestInfo(req, webRequest, base, locals);
|
|
702
739
|
const nextUrl = getRenderUrl(nextRequestInfo.url!, base);
|
|
703
|
-
const nextRendered = await render(nextUrl, nextRequestInfo
|
|
740
|
+
const nextRendered = await render(nextUrl, nextRequestInfo, {
|
|
741
|
+
externalizeIslandProps,
|
|
742
|
+
});
|
|
704
743
|
return createPageResponse({
|
|
705
744
|
rendered: nextRendered,
|
|
706
745
|
template,
|
package/src/core/vite-plugin.ts
CHANGED
|
@@ -725,8 +725,30 @@ export const viewComponents = import.meta.glob('/src/views/*/index.tsx');
|
|
|
725
725
|
|
|
726
726
|
// Virtual module: l5e-route
|
|
727
727
|
if (id === '\0' + VIRTUAL_L5E_ROUTE) {
|
|
728
|
-
//
|
|
729
|
-
|
|
728
|
+
// Load /src/route.ts LAZILY (dynamic import inside the handler) rather
|
|
729
|
+
// than via a static `export { default } from '/src/route.ts'`.
|
|
730
|
+
//
|
|
731
|
+
// A static re-export drags the user's route module — and everything it
|
|
732
|
+
// imports — into entry-server's *static* SSR graph. Route files commonly
|
|
733
|
+
// import the `@withl5e/l5e` barrel (for RedirectException, types, ...),
|
|
734
|
+
// and that barrel re-exports `render` from entry-server. That closes a
|
|
735
|
+
// static import cycle:
|
|
736
|
+
// entry-server → virtual:l5e-route → /src/route.ts → @withl5e/l5e → entry-server
|
|
737
|
+
// On the first dev request Vite can begin evaluating entry-server while
|
|
738
|
+
// it is still suspended awaiting this very import, so `render()` runs
|
|
739
|
+
// before `const __vite_ssr_import_N__ = await import('virtual:l5e-route')`
|
|
740
|
+
// has been assigned — throwing "Cannot access '__vite_ssr_import_N__'
|
|
741
|
+
// before initialization". A page reload "fixes" it only because the graph
|
|
742
|
+
// is fully warm the second time.
|
|
743
|
+
//
|
|
744
|
+
// Importing lazily removes /src/route.ts from the static graph: entry-server
|
|
745
|
+
// finishes initializing first, and the route module (with its barrel import
|
|
746
|
+
// back to a now-complete entry-server) is pulled only on the first render()
|
|
747
|
+
// call. No manual caching — Vite/ESM caches the evaluated module and later
|
|
748
|
+
// imports still observe HMR invalidations of route.ts.
|
|
749
|
+
return `export default function routeHandler(requestInfo) {
|
|
750
|
+
return import('/src/route.ts').then((mod) => mod.default(requestInfo));
|
|
751
|
+
}`;
|
|
730
752
|
}
|
|
731
753
|
|
|
732
754
|
// Virtual module: l5e-ssr-entry
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
jsxFactory,
|
|
3
|
+
isIslandPropsExternalized,
|
|
4
|
+
pushIslandProps,
|
|
3
5
|
registerIsland,
|
|
4
6
|
registerSsrIsland,
|
|
5
7
|
type JSXChild,
|
|
@@ -67,10 +69,31 @@ export function ClientIsland(attrs: ClientIslandProps): JSXNode {
|
|
|
67
69
|
const dataAttrs: Record<string, string> = {
|
|
68
70
|
'data-island': __key || 'unresolved',
|
|
69
71
|
'data-island-name': componentName,
|
|
70
|
-
'data-island-props': JSON.stringify(props),
|
|
71
72
|
'data-island-mount': mount,
|
|
72
73
|
};
|
|
73
74
|
|
|
75
|
+
// Props delivery. `mount="none"` never mounts/hydrates on the client, so its
|
|
76
|
+
// props are dead weight → omit them entirely in the externalized mode.
|
|
77
|
+
//
|
|
78
|
+
// Externalized (default): props go into the per-request store and the element
|
|
79
|
+
// just carries a `data-island-idx` pointer; a single `_l5e_data_` script at the
|
|
80
|
+
// end of the document holds every island's props, keeping the SSR body lean.
|
|
81
|
+
// Falls back to inlining when there's no render context (idx === -1).
|
|
82
|
+
//
|
|
83
|
+
// Legacy inline mode is kept byte-for-byte (props inlined even for mount="none").
|
|
84
|
+
if (isIslandPropsExternalized()) {
|
|
85
|
+
if (mount !== 'none') {
|
|
86
|
+
const idx = pushIslandProps(props);
|
|
87
|
+
if (idx >= 0) {
|
|
88
|
+
dataAttrs['data-island-idx'] = String(idx);
|
|
89
|
+
} else {
|
|
90
|
+
dataAttrs['data-island-props'] = JSON.stringify(props);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
dataAttrs['data-island-props'] = JSON.stringify(props);
|
|
95
|
+
}
|
|
96
|
+
|
|
74
97
|
if (mountOpts) {
|
|
75
98
|
dataAttrs['data-island-opts'] = mountOpts;
|
|
76
99
|
}
|