@forinda/kickjs-vite 2.3.2 → 2.3.3
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/index.d.mts.map +1 -1
- package/dist/index.mjs +7 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/env-watch-plugin.ts","../src/index.ts"],"mappings":";;;;;;;AAuBA;;;;;AAqBA;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/env-watch-plugin.ts","../src/index.ts"],"mappings":";;;;;;;AAuBA;;;;;AAqBA;;;;;;;;ACpBA;;;;;;UDDiB,mBAAA;;AEgFjB;;;;;;;;;;;;EFlEE,KAAA;AAAA;;;;;UAOe,aAAA;;EAEf,KAAA;;EAEA,IAAA;AAAA;;;;;AAzBF;;;;;AAqBA;;;;;;;;ACpBA;;;;iBAAgB,cAAA,CAAA,GAAkB,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBC+ElB,gBAAA,CAAiB,OAAA,GAAS,mBAAA,GAA2B,MAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-vite v2.3.
|
|
2
|
+
* @forinda/kickjs-vite v2.3.3
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import path, { resolve } from "node:path";
|
|
12
12
|
import { normalizePath } from "vite";
|
|
13
13
|
import fs from "node:fs";
|
|
14
|
+
import { reloadEnv } from "@forinda/kickjs";
|
|
14
15
|
//#region src/core-plugin.ts
|
|
15
16
|
/**
|
|
16
17
|
* Creates the core configuration plugin.
|
|
@@ -356,6 +357,11 @@ function envWatchPlugin() {
|
|
|
356
357
|
const basename = path.basename(changedPath);
|
|
357
358
|
if (envFiles.includes(basename)) {
|
|
358
359
|
server.config.logger.info(` .env changed (${basename}), triggering reload...`, { timestamp: true });
|
|
360
|
+
try {
|
|
361
|
+
reloadEnv();
|
|
362
|
+
} catch (err) {
|
|
363
|
+
server.config.logger.warn(` env reload failed: ${err?.message ?? err} — restart may be required`, { timestamp: true });
|
|
364
|
+
}
|
|
359
365
|
const mods = server.moduleGraph.getModulesByFile(path.resolve(root, "src/index.ts"));
|
|
360
366
|
if (mods) for (const mod of mods) server.moduleGraph.invalidateModule(mod);
|
|
361
367
|
server.ws.send({ type: "full-reload" });
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core-plugin.ts","../src/virtual-modules.ts","../src/dev-server.ts","../src/module-discovery.ts","../src/hmr-plugin.ts","../src/env-watch-plugin.ts","../src/index.ts"],"sourcesContent":["/**\n * Core Vite plugin for KickJS — configures the Vite server for backend\n * framework use: custom app type, SSR environment, and Node.js target.\n *\n * This plugin runs first in the array and sets up the environment that\n * the other sub-plugins (dev-server, virtual-modules, hmr) depend on.\n *\n * @module @forinda/kickjs-vite/core-plugin\n */\n\nimport type { Plugin } from 'vite'\nimport type { PluginContext } from './types'\n\n/**\n * Creates the core configuration plugin.\n *\n * Responsibilities:\n * - Sets `appType: 'custom'` so Vite doesn't serve index.html\n * - Configures the SSR environment for Node.js backend code\n * - Prevents Vite from clearing the terminal (KickJS logs route tables on startup)\n * - Externalizes Node.js built-ins and framework packages from the bundle\n *\n * @param ctx - Shared plugin context (entry file, root directory)\n * @returns Vite plugin\n */\nexport function kickjsCorePlugin(ctx: PluginContext): Plugin {\n return {\n name: 'kickjs:core',\n\n /**\n * Vite config hook — runs before config is resolved.\n * Sets the foundational configuration for a backend Node.js framework.\n */\n config(_config, { command }) {\n return {\n // 'custom' tells Vite this is not a SPA or MPA — we handle all requests\n appType: 'custom' as const,\n\n // Don't clear the terminal — KickJS logs route tables on startup\n clearScreen: false,\n\n // SSR environment configuration for backend code\n environments: {\n ssr: {\n // Warm up the entry file for faster first request\n dev: {\n warmup: [ctx.entry],\n },\n },\n },\n\n // Disable dep optimization discovery — KickJS is SSR-only, not a client app\n optimizeDeps: {\n noDiscovery: true,\n },\n\n // SSR-specific settings\n ssr: {\n // Externalize Node.js built-ins and framework packages\n // These should NOT be bundled by Vite — they run natively in Node\n external: [\n '@forinda/kickjs',\n // Legacy packages — still referenced in some dist/ files until V2 migration Phase 4 completes\n '@forinda/kickjs-core',\n '@forinda/kickjs-http',\n 'express',\n 'reflect-metadata',\n ],\n },\n }\n },\n }\n}\n","/**\n * Virtual module plugin for KickJS — generates the `virtual:kickjs/app`\n * module that bridges Vite and the KickJS application.\n *\n * Virtual modules are Vite's mechanism for generating code at build/serve\n * time that doesn't exist on disk. When the dev server calls\n * `ssrLoadModule('virtual:kickjs/app')`, this plugin generates the code\n * that imports and re-exports the user's application entry.\n *\n * In the future, this will also support auto-discovery of `@Module` classes\n * (Step 4 in v3/plan.md) and container manifest generation.\n *\n * @see https://vite.dev/guide/api-plugin.html#virtual-modules-convention\n *\n * @module @forinda/kickjs-vite/virtual-modules\n */\n\nimport { normalizePath, type Plugin } from 'vite'\nimport type { PluginContext } from './types'\n\n/** Prefix for resolved virtual module IDs (Vite convention) */\nconst VIRTUAL_PREFIX = '\\0'\n\n/** Virtual module ID for the KickJS application entry */\nconst VIRTUAL_APP = 'virtual:kickjs/app'\n\n/** Resolved virtual module ID (with \\0 prefix to mark as virtual) */\nconst RESOLVED_APP = `${VIRTUAL_PREFIX}${VIRTUAL_APP}`\n\n/**\n * Creates the virtual modules plugin.\n *\n * Currently provides one virtual module:\n * - `virtual:kickjs/app` — re-exports the user's application entry\n *\n * The dev server plugin imports this on every request via `ssrLoadModule()`.\n * When any source file changes, Vite invalidates the module graph and the\n * next import returns fresh code — this is how server-side HMR works.\n *\n * Future virtual modules (planned for Steps 4-5):\n * - `virtual:kickjs/modules` — auto-discovered `@Module` classes\n * - `virtual:kickjs/manifest` — container metadata for DevTools\n *\n * @param ctx - Shared plugin context (entry file, root directory)\n * @returns Vite plugin\n */\nexport function kickjsVirtualModulesPlugin(ctx: PluginContext): Plugin {\n return {\n name: 'kickjs:virtual-modules',\n\n /**\n * Resolve virtual module IDs.\n * Tells Vite that `virtual:kickjs/app` is a valid module ID\n * and maps it to our internal resolved ID.\n */\n resolveId(id: string) {\n if (id === VIRTUAL_APP) {\n return RESOLVED_APP\n }\n },\n\n /**\n * Load virtual module content.\n * Generates the code for `virtual:kickjs/app` that imports the user's\n * entry file and re-exports the Express app.\n *\n * The generated code:\n * 1. Imports the user's entry file (which calls `bootstrap()`)\n * 2. Re-exports everything from it (including the `app` export)\n *\n * The dev server plugin then does:\n * `const { app } = await ssrLoadModule('virtual:kickjs/app')`\n * and uses `app.handle(req, res, next)` to route requests through Express.\n */\n load(id: string) {\n if (id !== RESOLVED_APP) return\n\n // Generate code that imports and re-exports the user's entry\n // The entry file should: export const app = bootstrap({ modules: [...] })\n const normalizedEntry = normalizePath(ctx.entry)\n return [\n `// Auto-generated by @forinda/kickjs-vite`,\n `// Imports the user's application entry and re-exports the Express app.`,\n `// Vite's module graph tracks this — file changes trigger re-evaluation.`,\n `export * from '${normalizedEntry}'`,\n ].join('\\n')\n },\n }\n}\n\nexport { VIRTUAL_APP, RESOLVED_APP }\n","/**\n * Dev server plugin for KickJS — mounts the Express application on Vite's\n * HTTP server using the `configureServer` hook.\n *\n * ## Architecture\n *\n * In development, Vite owns the HTTP port and creates the `http.Server`.\n * This plugin:\n *\n * 1. **Stores `viteServer.httpServer` on `globalThis`** so KickJS adapters\n * (WsAdapter, Socket.IO, GraphQL subscriptions) can attach to the real\n * server via `server.on('upgrade', ...)`. This is the key to supporting\n * any library that needs the raw `http.Server`.\n *\n * 2. **Registers a post-middleware** that loads the KickJS app through\n * Vite's SSR transform pipeline on every request. When source files\n * change, Vite invalidates the module graph and the next request\n * gets fresh code — no process restart needed.\n *\n * 3. **Stores the Vite server reference** on `globalThis` so the SSR\n * renderer (for future Inertia support) can use `createViteRuntime()`.\n *\n * ## Why NOT middlewareMode\n *\n * The initial KickJS Vite plugin attempt used `middlewareMode: true`, which\n * makes `viteServer.httpServer = null`. This broke WsAdapter and any library\n * needing `server.on('upgrade', ...)`. By letting Vite create the server,\n * `httpServer` is the real `http.Server` — adapters attach to it seamlessly.\n *\n * ## Request Flow\n *\n * ```\n * HTTP Request → Vite static middleware (HMR, assets)\n * → If Vite handles it → done\n * → If not → KickJS post-middleware\n * → ssrLoadModule('virtual:kickjs/app') → fresh Express app\n * → Express handles request (routes, middleware, controllers)\n * ```\n *\n * @see v3/architecture.md Section 2.6 for the httpServer piping design\n * @see bench-mark/react-router-analysis.md for the pattern origin\n *\n * @module @forinda/kickjs-vite/dev-server\n */\n\nimport type { Plugin, ViteDevServer } from 'vite'\nimport type { PluginContext } from './types'\nimport { VIRTUAL_APP } from './virtual-modules'\n\n/**\n * Declare the globalThis properties used for cross-module communication.\n * These persist across Vite's `ssrLoadModule()` re-evaluations.\n *\n * `__kickjs_httpServer` uses the same `HttpServer` type that Vite uses\n * internally (union of http.Server | https.Server | Http2SecureServer).\n * KickJS adapters receive it as `http.Server` via AdapterContext — the\n * cast is safe because Express only works with HTTP/1.1 servers.\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __kickjs_httpServer: any\n // eslint-disable-next-line no-var\n var __kickjs_viteServer: ViteDevServer | null\n}\n\n/**\n * Creates the dev server plugin.\n *\n * This is the critical integration point between Vite and KickJS:\n * - Pipes `viteServer.httpServer` to adapters via `globalThis`\n * - Loads fresh Express app on every request via `ssrLoadModule()`\n * - Fixes stack traces for SSR errors with `ssrFixStacktrace()`\n *\n * @param ctx - Shared plugin context (entry file, root directory)\n * @returns Vite plugin\n */\nexport function kickjsDevServerPlugin(ctx: PluginContext): Plugin {\n return {\n name: 'kickjs:dev-server',\n\n /**\n * Configure the Vite dev server.\n *\n * The `configureServer` hook runs after Vite creates the HTTP server.\n * We use it to:\n * 1. Store the httpServer on globalThis (for WsAdapter, Socket.IO, etc.)\n * 2. Store the Vite server reference (for SSR rendering in Inertia)\n * 3. Return a post-middleware that loads the KickJS app on each request\n *\n * Returning a function from `configureServer` registers it as a\n * **post-middleware** — it runs AFTER Vite's own middleware (static\n * files, HMR client, etc.), so Vite-handled requests never hit Express.\n */\n configureServer(viteServer: ViteDevServer) {\n // ━━━ Store the REAL http.Server on globalThis ━━━\n // This is the key to supporting Socket.IO, WsAdapter, GraphQL WS,\n // and any library that needs `server.on('upgrade', ...)`.\n //\n // Application.start() detects this and skips creating its own server:\n // if (globalThis.__kickjs_httpServer) → reuse Vite's server\n // else → create own http.Server (production mode)\n //\n // Adapters receive the server in afterStart({ server }) — same API\n // in both dev and prod. Zero adapter code changes needed.\n globalThis.__kickjs_httpServer = viteServer.httpServer ?? null\n globalThis.__kickjs_viteServer = viteServer\n\n // Return post-middleware — runs after Vite's static/HMR middleware\n return () => {\n viteServer.middlewares.use(async (req, res, next) => {\n try {\n // Load the KickJS app through Vite's SSR transform pipeline.\n // On first request: evaluates the entry file and all imports.\n // On subsequent requests: returns cached modules (or re-evaluates\n // if source files changed since last request).\n //\n // This is the same pattern React Router and TanStack Start use.\n const mod = await viteServer.ssrLoadModule(VIRTUAL_APP)\n\n // The entry file exports: export const app = bootstrap({ modules })\n // `app` is the configured Express instance (not yet listening).\n const expressApp = mod.app\n\n if (!expressApp?.handle) {\n // Entry file doesn't export an Express app — fall through to Vite's 404\n return next()\n }\n\n // Let Express handle the request.\n // If Express doesn't match any route, it calls next() and\n // Vite's default 404 handler takes over.\n expressApp.handle(req, res, (err?: any) => {\n if (err) {\n // Fix stack traces to point to original source (not compiled)\n if (err instanceof Error) {\n viteServer.ssrFixStacktrace(err)\n }\n return next(err)\n }\n next()\n })\n } catch (err) {\n // SSR load/evaluation error (syntax error, missing import, etc.)\n if (err instanceof Error) {\n viteServer.ssrFixStacktrace(err)\n }\n next(err)\n }\n })\n }\n },\n }\n}\n","/**\n * Module discovery plugin for KickJS — automatically detects `@Module` classes\n * by scanning source files during Vite's `transform()` hook.\n *\n * ## How It Works\n *\n * 1. Vite calls `transform(code, id)` for every file it processes\n * 2. This plugin checks if the file matches `*.module.ts` (or `.js`)\n * 3. If the file contains a class exported with `@Module` or extends `AppModule`,\n * it extracts the class name and records the file path\n * 4. The virtual module `virtual:kickjs/app` is invalidated so the next request\n * picks up the newly discovered module\n *\n * ## Why This Matters\n *\n * Without auto-discovery, users must manually maintain a barrel file\n * (`src/modules/index.ts`) that imports and re-exports all modules.\n * When `kick g module` creates a new module, it has to edit this barrel.\n * With auto-discovery, modules are detected automatically — no barrel needed.\n *\n * ## Interaction with HMR\n *\n * When a module file is added, renamed, or deleted:\n * - `handleHotUpdate()` detects the change\n * - The virtual module is invalidated\n * - The next `ssrLoadModule()` call regenerates the virtual module\n * with the updated module list\n *\n * @module @forinda/kickjs-vite/module-discovery\n */\n\nimport type { Plugin, ViteDevServer } from 'vite'\nimport type { PluginContext } from './types'\nimport { RESOLVED_APP } from './virtual-modules'\n\n/** Regex to detect exported module classes in source files */\nconst MODULE_CLASS_REGEX =\n /export\\s+(?:default\\s+)?class\\s+(\\w+(?:Module))\\s*(?:extends|implements|{)/\n\n/** File extensions to scan for modules */\nconst MODULE_FILE_PATTERN = /\\.module\\.[tj]sx?$/\n\n/**\n * Discovered module entry — maps a file path to its exported class name.\n *\n * @example\n * ```\n * {\n * filePath: '/src/modules/users/user.module.ts',\n * className: 'UserModule'\n * }\n * ```\n */\nexport interface DiscoveredModule {\n /** Absolute file path */\n filePath: string\n /** Exported class name (e.g., 'UserModule') */\n className: string\n}\n\n/**\n * Creates the module auto-discovery plugin.\n *\n * Scans `*.module.ts` files during Vite's transform phase to build a\n * registry of `@Module` classes. The registry is used by the virtual\n * modules plugin to generate auto-imports.\n *\n * @param ctx - Shared plugin context\n * @returns Vite plugin\n */\nexport function kickjsModuleDiscoveryPlugin(ctx: PluginContext): Plugin {\n /**\n * Registry of discovered modules.\n * Keyed by absolute file path to handle renames and deletions.\n * Stored on the plugin context so the virtual modules plugin can read it.\n */\n const discovered = new Map<string, DiscoveredModule>()\n let server: ViteDevServer | null = null\n\n // Expose discovered modules on the context for virtual-modules plugin\n ;(ctx as any).discoveredModules = discovered\n\n return {\n name: 'kickjs:module-discovery',\n\n configureServer(viteServer) {\n server = viteServer\n\n // Watch for new/deleted files in the modules directory\n viteServer.watcher.on('add', (filePath) => {\n if (MODULE_FILE_PATTERN.test(filePath)) {\n // File added — it will be processed in transform() when first imported.\n // Invalidate virtual module so it picks up the new file on next request.\n invalidateVirtualApp(viteServer)\n }\n })\n\n viteServer.watcher.on('unlink', (filePath) => {\n if (discovered.has(filePath)) {\n discovered.delete(filePath)\n invalidateVirtualApp(viteServer)\n }\n })\n },\n\n /**\n * Transform hook — called for every file Vite processes.\n *\n * We don't actually transform the code (return null), but we use this\n * hook to observe which files contain module classes. This is cheaper\n * than AST parsing — a regex match on the source is sufficient because\n * module files follow a consistent naming pattern (`*.module.ts`).\n */\n transform(code: string, id: string) {\n // Only scan files matching the module pattern\n if (!MODULE_FILE_PATTERN.test(id)) return null\n // Skip node_modules\n if (id.includes('node_modules')) return null\n\n const match = code.match(MODULE_CLASS_REGEX)\n if (match) {\n const className = match[1]\n const wasNew = !discovered.has(id)\n discovered.set(id, { filePath: id, className })\n\n if (wasNew && server) {\n // New module discovered — invalidate virtual module\n invalidateVirtualApp(server)\n }\n } else if (discovered.has(id)) {\n // File no longer exports a module class (user removed it)\n discovered.delete(id)\n if (server) {\n invalidateVirtualApp(server)\n }\n }\n\n // Return null — we don't transform the code, just observe it\n return null\n },\n\n /**\n * HMR hook — called when a file changes.\n *\n * If a module file changes, we re-scan it and invalidate the virtual\n * module so the next request picks up the changes.\n */\n handleHotUpdate({ file, server: viteServer }) {\n if (!MODULE_FILE_PATTERN.test(file)) return\n\n // The file will be re-processed in transform() on next import.\n // Invalidate the virtual module so it regenerates with fresh imports.\n invalidateVirtualApp(viteServer)\n },\n }\n}\n\n/**\n * Invalidate the virtual:kickjs/app module in Vite's module graph.\n * This forces the next `ssrLoadModule()` call to re-evaluate the\n * virtual module, picking up any new/removed modules.\n */\nfunction invalidateVirtualApp(server: ViteDevServer): void {\n const mod = server.moduleGraph.getModuleById(RESOLVED_APP)\n if (mod) {\n server.moduleGraph.invalidateModule(mod)\n }\n}\n\n/**\n * Get the current list of discovered modules.\n * Used by the virtual modules plugin to generate auto-imports.\n */\nexport function getDiscoveredModules(ctx: PluginContext): DiscoveredModule[] {\n const map = (ctx as any).discoveredModules as Map<string, DiscoveredModule> | undefined\n return map ? [...map.values()] : []\n}\n","/**\n * HMR plugin for KickJS — selective container invalidation when source\n * files change, with debounced batching for bulk operations.\n *\n * ## How It Works\n *\n * 1. During `transform()`, the plugin scans for decorator patterns\n * (`@Service`, `@Controller`, etc.) and maps file paths to DI token names\n * 2. When a file changes (`handleHotUpdate()`), the plugin:\n * - Looks up which DI tokens are defined in that file\n * - Calls `container.invalidate(token)` for each (which walks the dep graph)\n * - Invalidates the virtual module so `ssrLoadModule()` returns fresh code\n * - Sends a custom HMR event (`kickjs:hmr`) so DevTools/Swagger can react\n * 3. Changes are **debounced** (150ms) so `kick g module` creating 10+ files\n * emits ONE invalidation batch, not 10 separate events\n *\n * ## Interaction with Reactive Container\n *\n * `container.invalidate(token)` (from Step 3) does:\n * - Clears the cached instance for that token\n * - Walks the dependency graph to find dependents\n * - Invalidates all dependents recursively\n * - Emits batched `onChange()` events to all subscribers (Swagger, DevTools, etc.)\n *\n * ## Detection Strategy\n *\n * Uses regex patterns (not AST) for speed. This is the same approach\n * TanStack Start uses (`KindDetectionPatterns` in their compiler).\n * It's fast because:\n * - Only runs on files that actually contain decorator patterns\n * - No Babel/SWC parsing needed\n * - False positives are harmless (extra invalidation is safe)\n *\n * @see bench-mark/tanstack-router-analysis.md — Detection pattern origin\n * @see v3/plan.md Step 5 — Design rationale\n *\n * @module @forinda/kickjs-vite/hmr-plugin\n */\n\nimport type { Plugin, ViteDevServer } from 'vite'\nimport type { PluginContext } from './types'\nimport { RESOLVED_APP } from './virtual-modules'\n\n/**\n * Regex patterns that detect KickJS decorator usage in source files.\n * Matches `@Service()`, `@Controller('/path')`, `@Repository()`, etc.\n * followed by an exported class declaration.\n *\n * Pattern: decorator → optional whitespace → export? class ClassName\n */\nconst DECORATOR_CLASS_REGEX =\n /@(?:Service|Controller|Repository|Injectable|Component)\\s*\\([^)]*\\)\\s*\\n?\\s*(?:export\\s+)?class\\s+(\\w+)/g\n\n/** Debounce time for batching multiple file changes into one invalidation */\nconst DEBOUNCE_MS = 150\n\n/**\n * Creates the HMR selective invalidation plugin.\n *\n * Tracks which DI tokens are defined in which files, then selectively\n * invalidates only the affected tokens (and their dependents) when\n * those files change. Much faster than a full Container.reset().\n *\n * @param ctx - Shared plugin context\n * @returns Vite plugin\n */\nexport function kickjsHmrPlugin(ctx: PluginContext): Plugin {\n /**\n * Maps file paths to the DI token names defined in them.\n * Built up during transform(), read during handleHotUpdate().\n *\n * @example\n * ```\n * '/src/modules/users/user.service.ts' → ['UserService']\n * '/src/modules/users/user.controller.ts' → ['UserController']\n * ```\n */\n const fileTokenMap = new Map<string, string[]>()\n\n /** Pending tokens to invalidate (accumulated during debounce window) */\n let pendingTokens = new Set<string>()\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n return {\n name: 'kickjs:hmr',\n\n /**\n * Transform hook — scans source files for decorator patterns.\n *\n * Doesn't modify the code (returns null), just records which\n * DI tokens are defined in each file. This mapping is used by\n * `handleHotUpdate()` to determine what to invalidate.\n */\n transform(code: string, id: string) {\n // Skip non-TypeScript/JavaScript files and node_modules\n if (!/\\.[tj]sx?$/.test(id)) return null\n if (id.includes('node_modules')) return null\n\n // Quick bail-out: skip files that don't contain any decorator keywords\n if (\n !code.includes('@Service') &&\n !code.includes('@Controller') &&\n !code.includes('@Repository') &&\n !code.includes('@Injectable') &&\n !code.includes('@Component')\n ) {\n // If file was previously tracked but decorators were removed, clean up\n if (fileTokenMap.has(id)) {\n fileTokenMap.delete(id)\n }\n return null\n }\n\n // Extract class names from decorator patterns\n const tokens: string[] = []\n let match: RegExpExecArray | null\n // Reset regex lastIndex for reuse across files\n DECORATOR_CLASS_REGEX.lastIndex = 0\n while ((match = DECORATOR_CLASS_REGEX.exec(code)) !== null) {\n tokens.push(match[1])\n }\n\n if (tokens.length > 0) {\n fileTokenMap.set(id, tokens)\n } else if (fileTokenMap.has(id)) {\n fileTokenMap.delete(id)\n }\n\n return null\n },\n\n /**\n * HMR hook — called when a file changes in dev mode.\n *\n * If the changed file contains KickJS decorated classes:\n * 1. Accumulate the affected tokens in the debounce buffer\n * 2. After 150ms of quiet, flush: invalidate all tokens at once\n * 3. Invalidate the virtual module for fresh `ssrLoadModule()` response\n * 4. Send ONE `kickjs:hmr` event to the client (DevTools, Swagger UI)\n *\n * The 150ms debounce is critical for `kick g module` which creates\n * 10+ files in rapid succession. Without debouncing, each file would\n * trigger a separate invalidation cycle.\n *\n * Returns an empty array to tell Vite we handled the update\n * (prevents Vite's default full-page reload for these files).\n */\n handleHotUpdate({ file, server }) {\n const tokens = fileTokenMap.get(file)\n if (!tokens || tokens.length === 0) return\n\n // Accumulate tokens for batched invalidation\n for (const t of tokens) pendingTokens.add(t)\n\n // Debounce: flush after 150ms of quiet\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n flushInvalidation(server, pendingTokens)\n pendingTokens = new Set()\n }, DEBOUNCE_MS)\n\n // Tell Vite we handled it — don't do default full-page reload\n return []\n },\n }\n}\n\n/**\n * Flush accumulated token invalidations.\n *\n * Called after the debounce window closes. Performs:\n * 1. Container invalidation (clears instances + walks dependency graph)\n * 2. Virtual module invalidation (forces ssrLoadModule to re-evaluate)\n * 3. HMR event broadcast (notifies DevTools, Swagger UI, etc.)\n *\n * @param server - Vite dev server\n * @param tokens - Set of DI token names to invalidate\n */\nfunction flushInvalidation(server: ViteDevServer, tokens: Set<string>): void {\n const batch = [...tokens]\n if (batch.length === 0) return\n\n // 1. Invalidate tokens in the reactive container (if available)\n // container.invalidate() walks the dependency graph and notifies subscribers\n const container = (globalThis as any).__kickjs_container\n if (container && typeof container.invalidate === 'function') {\n for (const token of batch) {\n container.invalidate(token)\n }\n }\n\n // 2. Invalidate the virtual module so next ssrLoadModule() re-evaluates\n const vmod = server.moduleGraph.getModuleById(RESOLVED_APP)\n if (vmod) {\n server.moduleGraph.invalidateModule(vmod)\n }\n\n // 3. Send ONE custom HMR event with the full batch\n // DevTools dashboard, Swagger UI, and other dev tools can listen:\n // import.meta.hot?.on('kickjs:hmr', (data) => { ... })\n server.hot.send({\n type: 'custom',\n event: 'kickjs:hmr',\n data: { tokens: batch, timestamp: Date.now() },\n })\n\n // Log to dev console\n const names = batch.join(', ')\n const label = batch.length === 1 ? '1 token' : `${batch.length} tokens`\n console.log(` ${green('HMR')} invalidated ${label}: ${names}`)\n}\n\n/** ANSI green for terminal output */\nfunction green(text: string): string {\n return `\\x1b[32m${text}\\x1b[0m`\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport type { Plugin } from 'vite'\n\n/**\n * Vite plugin that watches `.env` files and triggers a full reload\n * when they change. This ensures the dev server picks up environment\n * variable changes without a manual restart.\n *\n * Lives in `@forinda/kickjs-vite` so all Vite-only concerns are in one\n * place — the older re-export from `@forinda/kickjs-config` is kept as\n * a deprecated thin shim for back-compat.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { envWatchPlugin } from '@forinda/kickjs-vite'\n *\n * export default defineConfig({\n * plugins: [swc.vite(), envWatchPlugin()],\n * })\n * ```\n */\nexport function envWatchPlugin(): Plugin {\n const envFiles = ['.env', '.env.local', '.env.development', '.env.production', '.env.test']\n\n return {\n name: 'kickjs-env-watch',\n\n configureServer(server) {\n const root = server.config.root\n\n for (const file of envFiles) {\n const filePath = path.resolve(root, file)\n if (fs.existsSync(filePath)) {\n server.watcher.add(filePath)\n }\n }\n\n server.watcher.on('change', (changedPath) => {\n const basename = path.basename(changedPath)\n if (envFiles.includes(basename)) {\n server.config.logger.info(` .env changed (${basename}), triggering reload...`, {\n timestamp: true,\n })\n\n // Invalidate all modules to trigger full HMR rebuild\n const mods = server.moduleGraph.getModulesByFile(path.resolve(root, 'src/index.ts'))\n if (mods) {\n for (const mod of mods) {\n server.moduleGraph.invalidateModule(mod)\n }\n }\n\n server.ws.send({ type: 'full-reload' })\n }\n })\n },\n }\n}\n","/**\n * @forinda/kickjs-vite — Vite plugin for the KickJS framework.\n *\n * Provides first-class Vite integration for KickJS backend applications:\n * - **Dev server**: Mounts Express on Vite's HTTP server (single port)\n * - **HMR**: Fresh server code on every request via `ssrLoadModule()`\n * - **httpServer piping**: Real `http.Server` available to all adapters\n * - **Virtual modules**: Auto-generated application entry\n *\n * ## Quick Start\n *\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite'\n * import { kickjsVitePlugin } from '@forinda/kickjs-vite'\n * import swc from 'unplugin-swc'\n *\n * export default defineConfig({\n * plugins: [\n * swc.vite({ tsconfigFile: 'tsconfig.json' }),\n * kickjsVitePlugin({ entry: 'src/index.ts' }),\n * ],\n * })\n * ```\n *\n * ```ts\n * // src/index.ts\n * import { bootstrap } from '@forinda/kickjs'\n * import { UserModule } from './modules/users/user.module'\n *\n * // Export the Express app — Vite serves it in dev, you start it in prod\n * export const app = bootstrap({\n * modules: [UserModule],\n * middleware: [express.json()],\n * })\n *\n * // Production: start the server directly\n * if (process.env.NODE_ENV === 'production') {\n * app.start()\n * }\n * ```\n *\n * ## Architecture\n *\n * The plugin returns an array of focused sub-plugins (React Router pattern):\n *\n * | Plugin | Responsibility |\n * |--------|---------------|\n * | `kickjs:core` | Vite config: appType, SSR environment, externals |\n * | `kickjs:module-discovery` | Auto-discover `@Module` classes via `transform()` |\n * | `kickjs:hmr` | Selective container invalidation via `handleHotUpdate()` |\n * | `kickjs:virtual-modules` | `virtual:kickjs/app` resolution and generation |\n * | `kickjs:dev-server` | `configureServer()` — mounts Express, pipes httpServer |\n *\n * ## httpServer Piping\n *\n * Vite creates the `http.Server` — this plugin stores it on\n * `globalThis.__kickjs_httpServer`. KickJS adapters (WsAdapter, Socket.IO,\n * GraphQL subscriptions) attach to this server via the standard\n * `afterStart({ server })` hook. Zero adapter code changes needed.\n *\n * @see v3/architecture.md — Full architecture documentation\n * @see v3/plan.md — Implementation plan and rationale\n * @see bench-mark/react-router-analysis.md — Pattern origin\n *\n * @module @forinda/kickjs-vite\n */\n\nimport { resolve } from 'node:path'\nimport type { Plugin } from 'vite'\nimport type { KickJSPluginOptions, PluginContext } from './types'\nimport { kickjsCorePlugin } from './core-plugin'\nimport { kickjsVirtualModulesPlugin } from './virtual-modules'\nimport { kickjsDevServerPlugin } from './dev-server'\nimport { kickjsModuleDiscoveryPlugin } from './module-discovery'\nimport { kickjsHmrPlugin } from './hmr-plugin'\n\n/**\n * Create the KickJS Vite plugin array.\n *\n * Returns an array of focused sub-plugins that together provide full\n * Vite integration for KickJS backend applications. Each sub-plugin\n * has a single responsibility and runs at the appropriate Vite lifecycle stage.\n *\n * @param options - Plugin configuration\n * @param options.entry - Path to app entry file (default: 'src/index.ts')\n * @returns Array of Vite plugins\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite'\n * import { kickjsVitePlugin } from '@forinda/kickjs-vite'\n * import swc from 'unplugin-swc'\n *\n * export default defineConfig({\n * plugins: [\n * swc.vite({ tsconfigFile: 'tsconfig.json' }),\n * kickjsVitePlugin(),\n * ],\n * })\n * ```\n */\nexport function kickjsVitePlugin(options: KickJSPluginOptions = {}): Plugin[] {\n const entry = options.entry ?? 'src/index.ts'\n\n // Create shared context — resolved lazily in config hook since we\n // don't have the root directory until Vite resolves its config.\n const ctx: PluginContext = {\n entry,\n root: process.cwd(),\n }\n\n // Resolve root and entry as early as possible so sub-plugin config() hooks\n // (e.g., warmup) see the correct absolute entry path.\n const rootResolver: Plugin = {\n name: 'kickjs:root-resolver',\n config(config) {\n const root = config.root ?? process.cwd()\n ctx.root = root\n ctx.entry = resolve(root, entry)\n },\n }\n\n return [\n rootResolver,\n kickjsCorePlugin(ctx),\n kickjsModuleDiscoveryPlugin(ctx),\n kickjsHmrPlugin(ctx),\n kickjsVirtualModulesPlugin(ctx),\n kickjsDevServerPlugin(ctx),\n ]\n}\n\n// Re-export types for consumers\nexport type { KickJSPluginOptions, PluginContext } from './types'\n\n// Standalone plugins users can compose alongside `kickjsVitePlugin()`\nexport { envWatchPlugin } from './env-watch-plugin'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core-plugin.ts","../src/virtual-modules.ts","../src/dev-server.ts","../src/module-discovery.ts","../src/hmr-plugin.ts","../src/env-watch-plugin.ts","../src/index.ts"],"sourcesContent":["/**\n * Core Vite plugin for KickJS — configures the Vite server for backend\n * framework use: custom app type, SSR environment, and Node.js target.\n *\n * This plugin runs first in the array and sets up the environment that\n * the other sub-plugins (dev-server, virtual-modules, hmr) depend on.\n *\n * @module @forinda/kickjs-vite/core-plugin\n */\n\nimport type { Plugin } from 'vite'\nimport type { PluginContext } from './types'\n\n/**\n * Creates the core configuration plugin.\n *\n * Responsibilities:\n * - Sets `appType: 'custom'` so Vite doesn't serve index.html\n * - Configures the SSR environment for Node.js backend code\n * - Prevents Vite from clearing the terminal (KickJS logs route tables on startup)\n * - Externalizes Node.js built-ins and framework packages from the bundle\n *\n * @param ctx - Shared plugin context (entry file, root directory)\n * @returns Vite plugin\n */\nexport function kickjsCorePlugin(ctx: PluginContext): Plugin {\n return {\n name: 'kickjs:core',\n\n /**\n * Vite config hook — runs before config is resolved.\n * Sets the foundational configuration for a backend Node.js framework.\n */\n config(_config, { command }) {\n return {\n // 'custom' tells Vite this is not a SPA or MPA — we handle all requests\n appType: 'custom' as const,\n\n // Don't clear the terminal — KickJS logs route tables on startup\n clearScreen: false,\n\n // SSR environment configuration for backend code\n environments: {\n ssr: {\n // Warm up the entry file for faster first request\n dev: {\n warmup: [ctx.entry],\n },\n },\n },\n\n // Disable dep optimization discovery — KickJS is SSR-only, not a client app\n optimizeDeps: {\n noDiscovery: true,\n },\n\n // SSR-specific settings\n ssr: {\n // Externalize Node.js built-ins and framework packages\n // These should NOT be bundled by Vite — they run natively in Node\n external: [\n '@forinda/kickjs',\n // Legacy packages — still referenced in some dist/ files until V2 migration Phase 4 completes\n '@forinda/kickjs-core',\n '@forinda/kickjs-http',\n 'express',\n 'reflect-metadata',\n ],\n },\n }\n },\n }\n}\n","/**\n * Virtual module plugin for KickJS — generates the `virtual:kickjs/app`\n * module that bridges Vite and the KickJS application.\n *\n * Virtual modules are Vite's mechanism for generating code at build/serve\n * time that doesn't exist on disk. When the dev server calls\n * `ssrLoadModule('virtual:kickjs/app')`, this plugin generates the code\n * that imports and re-exports the user's application entry.\n *\n * In the future, this will also support auto-discovery of `@Module` classes\n * (Step 4 in v3/plan.md) and container manifest generation.\n *\n * @see https://vite.dev/guide/api-plugin.html#virtual-modules-convention\n *\n * @module @forinda/kickjs-vite/virtual-modules\n */\n\nimport { normalizePath, type Plugin } from 'vite'\nimport type { PluginContext } from './types'\n\n/** Prefix for resolved virtual module IDs (Vite convention) */\nconst VIRTUAL_PREFIX = '\\0'\n\n/** Virtual module ID for the KickJS application entry */\nconst VIRTUAL_APP = 'virtual:kickjs/app'\n\n/** Resolved virtual module ID (with \\0 prefix to mark as virtual) */\nconst RESOLVED_APP = `${VIRTUAL_PREFIX}${VIRTUAL_APP}`\n\n/**\n * Creates the virtual modules plugin.\n *\n * Currently provides one virtual module:\n * - `virtual:kickjs/app` — re-exports the user's application entry\n *\n * The dev server plugin imports this on every request via `ssrLoadModule()`.\n * When any source file changes, Vite invalidates the module graph and the\n * next import returns fresh code — this is how server-side HMR works.\n *\n * Future virtual modules (planned for Steps 4-5):\n * - `virtual:kickjs/modules` — auto-discovered `@Module` classes\n * - `virtual:kickjs/manifest` — container metadata for DevTools\n *\n * @param ctx - Shared plugin context (entry file, root directory)\n * @returns Vite plugin\n */\nexport function kickjsVirtualModulesPlugin(ctx: PluginContext): Plugin {\n return {\n name: 'kickjs:virtual-modules',\n\n /**\n * Resolve virtual module IDs.\n * Tells Vite that `virtual:kickjs/app` is a valid module ID\n * and maps it to our internal resolved ID.\n */\n resolveId(id: string) {\n if (id === VIRTUAL_APP) {\n return RESOLVED_APP\n }\n },\n\n /**\n * Load virtual module content.\n * Generates the code for `virtual:kickjs/app` that imports the user's\n * entry file and re-exports the Express app.\n *\n * The generated code:\n * 1. Imports the user's entry file (which calls `bootstrap()`)\n * 2. Re-exports everything from it (including the `app` export)\n *\n * The dev server plugin then does:\n * `const { app } = await ssrLoadModule('virtual:kickjs/app')`\n * and uses `app.handle(req, res, next)` to route requests through Express.\n */\n load(id: string) {\n if (id !== RESOLVED_APP) return\n\n // Generate code that imports and re-exports the user's entry\n // The entry file should: export const app = bootstrap({ modules: [...] })\n const normalizedEntry = normalizePath(ctx.entry)\n return [\n `// Auto-generated by @forinda/kickjs-vite`,\n `// Imports the user's application entry and re-exports the Express app.`,\n `// Vite's module graph tracks this — file changes trigger re-evaluation.`,\n `export * from '${normalizedEntry}'`,\n ].join('\\n')\n },\n }\n}\n\nexport { VIRTUAL_APP, RESOLVED_APP }\n","/**\n * Dev server plugin for KickJS — mounts the Express application on Vite's\n * HTTP server using the `configureServer` hook.\n *\n * ## Architecture\n *\n * In development, Vite owns the HTTP port and creates the `http.Server`.\n * This plugin:\n *\n * 1. **Stores `viteServer.httpServer` on `globalThis`** so KickJS adapters\n * (WsAdapter, Socket.IO, GraphQL subscriptions) can attach to the real\n * server via `server.on('upgrade', ...)`. This is the key to supporting\n * any library that needs the raw `http.Server`.\n *\n * 2. **Registers a post-middleware** that loads the KickJS app through\n * Vite's SSR transform pipeline on every request. When source files\n * change, Vite invalidates the module graph and the next request\n * gets fresh code — no process restart needed.\n *\n * 3. **Stores the Vite server reference** on `globalThis` so the SSR\n * renderer (for future Inertia support) can use `createViteRuntime()`.\n *\n * ## Why NOT middlewareMode\n *\n * The initial KickJS Vite plugin attempt used `middlewareMode: true`, which\n * makes `viteServer.httpServer = null`. This broke WsAdapter and any library\n * needing `server.on('upgrade', ...)`. By letting Vite create the server,\n * `httpServer` is the real `http.Server` — adapters attach to it seamlessly.\n *\n * ## Request Flow\n *\n * ```\n * HTTP Request → Vite static middleware (HMR, assets)\n * → If Vite handles it → done\n * → If not → KickJS post-middleware\n * → ssrLoadModule('virtual:kickjs/app') → fresh Express app\n * → Express handles request (routes, middleware, controllers)\n * ```\n *\n * @see v3/architecture.md Section 2.6 for the httpServer piping design\n * @see bench-mark/react-router-analysis.md for the pattern origin\n *\n * @module @forinda/kickjs-vite/dev-server\n */\n\nimport type { Plugin, ViteDevServer } from 'vite'\nimport type { PluginContext } from './types'\nimport { VIRTUAL_APP } from './virtual-modules'\n\n/**\n * Declare the globalThis properties used for cross-module communication.\n * These persist across Vite's `ssrLoadModule()` re-evaluations.\n *\n * `__kickjs_httpServer` uses the same `HttpServer` type that Vite uses\n * internally (union of http.Server | https.Server | Http2SecureServer).\n * KickJS adapters receive it as `http.Server` via AdapterContext — the\n * cast is safe because Express only works with HTTP/1.1 servers.\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __kickjs_httpServer: any\n // eslint-disable-next-line no-var\n var __kickjs_viteServer: ViteDevServer | null\n}\n\n/**\n * Creates the dev server plugin.\n *\n * This is the critical integration point between Vite and KickJS:\n * - Pipes `viteServer.httpServer` to adapters via `globalThis`\n * - Loads fresh Express app on every request via `ssrLoadModule()`\n * - Fixes stack traces for SSR errors with `ssrFixStacktrace()`\n *\n * @param ctx - Shared plugin context (entry file, root directory)\n * @returns Vite plugin\n */\nexport function kickjsDevServerPlugin(ctx: PluginContext): Plugin {\n return {\n name: 'kickjs:dev-server',\n\n /**\n * Configure the Vite dev server.\n *\n * The `configureServer` hook runs after Vite creates the HTTP server.\n * We use it to:\n * 1. Store the httpServer on globalThis (for WsAdapter, Socket.IO, etc.)\n * 2. Store the Vite server reference (for SSR rendering in Inertia)\n * 3. Return a post-middleware that loads the KickJS app on each request\n *\n * Returning a function from `configureServer` registers it as a\n * **post-middleware** — it runs AFTER Vite's own middleware (static\n * files, HMR client, etc.), so Vite-handled requests never hit Express.\n */\n configureServer(viteServer: ViteDevServer) {\n // ━━━ Store the REAL http.Server on globalThis ━━━\n // This is the key to supporting Socket.IO, WsAdapter, GraphQL WS,\n // and any library that needs `server.on('upgrade', ...)`.\n //\n // Application.start() detects this and skips creating its own server:\n // if (globalThis.__kickjs_httpServer) → reuse Vite's server\n // else → create own http.Server (production mode)\n //\n // Adapters receive the server in afterStart({ server }) — same API\n // in both dev and prod. Zero adapter code changes needed.\n globalThis.__kickjs_httpServer = viteServer.httpServer ?? null\n globalThis.__kickjs_viteServer = viteServer\n\n // Return post-middleware — runs after Vite's static/HMR middleware\n return () => {\n viteServer.middlewares.use(async (req, res, next) => {\n try {\n // Load the KickJS app through Vite's SSR transform pipeline.\n // On first request: evaluates the entry file and all imports.\n // On subsequent requests: returns cached modules (or re-evaluates\n // if source files changed since last request).\n //\n // This is the same pattern React Router and TanStack Start use.\n const mod = await viteServer.ssrLoadModule(VIRTUAL_APP)\n\n // The entry file exports: export const app = bootstrap({ modules })\n // `app` is the configured Express instance (not yet listening).\n const expressApp = mod.app\n\n if (!expressApp?.handle) {\n // Entry file doesn't export an Express app — fall through to Vite's 404\n return next()\n }\n\n // Let Express handle the request.\n // If Express doesn't match any route, it calls next() and\n // Vite's default 404 handler takes over.\n expressApp.handle(req, res, (err?: any) => {\n if (err) {\n // Fix stack traces to point to original source (not compiled)\n if (err instanceof Error) {\n viteServer.ssrFixStacktrace(err)\n }\n return next(err)\n }\n next()\n })\n } catch (err) {\n // SSR load/evaluation error (syntax error, missing import, etc.)\n if (err instanceof Error) {\n viteServer.ssrFixStacktrace(err)\n }\n next(err)\n }\n })\n }\n },\n }\n}\n","/**\n * Module discovery plugin for KickJS — automatically detects `@Module` classes\n * by scanning source files during Vite's `transform()` hook.\n *\n * ## How It Works\n *\n * 1. Vite calls `transform(code, id)` for every file it processes\n * 2. This plugin checks if the file matches `*.module.ts` (or `.js`)\n * 3. If the file contains a class exported with `@Module` or extends `AppModule`,\n * it extracts the class name and records the file path\n * 4. The virtual module `virtual:kickjs/app` is invalidated so the next request\n * picks up the newly discovered module\n *\n * ## Why This Matters\n *\n * Without auto-discovery, users must manually maintain a barrel file\n * (`src/modules/index.ts`) that imports and re-exports all modules.\n * When `kick g module` creates a new module, it has to edit this barrel.\n * With auto-discovery, modules are detected automatically — no barrel needed.\n *\n * ## Interaction with HMR\n *\n * When a module file is added, renamed, or deleted:\n * - `handleHotUpdate()` detects the change\n * - The virtual module is invalidated\n * - The next `ssrLoadModule()` call regenerates the virtual module\n * with the updated module list\n *\n * @module @forinda/kickjs-vite/module-discovery\n */\n\nimport type { Plugin, ViteDevServer } from 'vite'\nimport type { PluginContext } from './types'\nimport { RESOLVED_APP } from './virtual-modules'\n\n/** Regex to detect exported module classes in source files */\nconst MODULE_CLASS_REGEX =\n /export\\s+(?:default\\s+)?class\\s+(\\w+(?:Module))\\s*(?:extends|implements|{)/\n\n/** File extensions to scan for modules */\nconst MODULE_FILE_PATTERN = /\\.module\\.[tj]sx?$/\n\n/**\n * Discovered module entry — maps a file path to its exported class name.\n *\n * @example\n * ```\n * {\n * filePath: '/src/modules/users/user.module.ts',\n * className: 'UserModule'\n * }\n * ```\n */\nexport interface DiscoveredModule {\n /** Absolute file path */\n filePath: string\n /** Exported class name (e.g., 'UserModule') */\n className: string\n}\n\n/**\n * Creates the module auto-discovery plugin.\n *\n * Scans `*.module.ts` files during Vite's transform phase to build a\n * registry of `@Module` classes. The registry is used by the virtual\n * modules plugin to generate auto-imports.\n *\n * @param ctx - Shared plugin context\n * @returns Vite plugin\n */\nexport function kickjsModuleDiscoveryPlugin(ctx: PluginContext): Plugin {\n /**\n * Registry of discovered modules.\n * Keyed by absolute file path to handle renames and deletions.\n * Stored on the plugin context so the virtual modules plugin can read it.\n */\n const discovered = new Map<string, DiscoveredModule>()\n let server: ViteDevServer | null = null\n\n // Expose discovered modules on the context for virtual-modules plugin\n ;(ctx as any).discoveredModules = discovered\n\n return {\n name: 'kickjs:module-discovery',\n\n configureServer(viteServer) {\n server = viteServer\n\n // Watch for new/deleted files in the modules directory\n viteServer.watcher.on('add', (filePath) => {\n if (MODULE_FILE_PATTERN.test(filePath)) {\n // File added — it will be processed in transform() when first imported.\n // Invalidate virtual module so it picks up the new file on next request.\n invalidateVirtualApp(viteServer)\n }\n })\n\n viteServer.watcher.on('unlink', (filePath) => {\n if (discovered.has(filePath)) {\n discovered.delete(filePath)\n invalidateVirtualApp(viteServer)\n }\n })\n },\n\n /**\n * Transform hook — called for every file Vite processes.\n *\n * We don't actually transform the code (return null), but we use this\n * hook to observe which files contain module classes. This is cheaper\n * than AST parsing — a regex match on the source is sufficient because\n * module files follow a consistent naming pattern (`*.module.ts`).\n */\n transform(code: string, id: string) {\n // Only scan files matching the module pattern\n if (!MODULE_FILE_PATTERN.test(id)) return null\n // Skip node_modules\n if (id.includes('node_modules')) return null\n\n const match = code.match(MODULE_CLASS_REGEX)\n if (match) {\n const className = match[1]\n const wasNew = !discovered.has(id)\n discovered.set(id, { filePath: id, className })\n\n if (wasNew && server) {\n // New module discovered — invalidate virtual module\n invalidateVirtualApp(server)\n }\n } else if (discovered.has(id)) {\n // File no longer exports a module class (user removed it)\n discovered.delete(id)\n if (server) {\n invalidateVirtualApp(server)\n }\n }\n\n // Return null — we don't transform the code, just observe it\n return null\n },\n\n /**\n * HMR hook — called when a file changes.\n *\n * If a module file changes, we re-scan it and invalidate the virtual\n * module so the next request picks up the changes.\n */\n handleHotUpdate({ file, server: viteServer }) {\n if (!MODULE_FILE_PATTERN.test(file)) return\n\n // The file will be re-processed in transform() on next import.\n // Invalidate the virtual module so it regenerates with fresh imports.\n invalidateVirtualApp(viteServer)\n },\n }\n}\n\n/**\n * Invalidate the virtual:kickjs/app module in Vite's module graph.\n * This forces the next `ssrLoadModule()` call to re-evaluate the\n * virtual module, picking up any new/removed modules.\n */\nfunction invalidateVirtualApp(server: ViteDevServer): void {\n const mod = server.moduleGraph.getModuleById(RESOLVED_APP)\n if (mod) {\n server.moduleGraph.invalidateModule(mod)\n }\n}\n\n/**\n * Get the current list of discovered modules.\n * Used by the virtual modules plugin to generate auto-imports.\n */\nexport function getDiscoveredModules(ctx: PluginContext): DiscoveredModule[] {\n const map = (ctx as any).discoveredModules as Map<string, DiscoveredModule> | undefined\n return map ? [...map.values()] : []\n}\n","/**\n * HMR plugin for KickJS — selective container invalidation when source\n * files change, with debounced batching for bulk operations.\n *\n * ## How It Works\n *\n * 1. During `transform()`, the plugin scans for decorator patterns\n * (`@Service`, `@Controller`, etc.) and maps file paths to DI token names\n * 2. When a file changes (`handleHotUpdate()`), the plugin:\n * - Looks up which DI tokens are defined in that file\n * - Calls `container.invalidate(token)` for each (which walks the dep graph)\n * - Invalidates the virtual module so `ssrLoadModule()` returns fresh code\n * - Sends a custom HMR event (`kickjs:hmr`) so DevTools/Swagger can react\n * 3. Changes are **debounced** (150ms) so `kick g module` creating 10+ files\n * emits ONE invalidation batch, not 10 separate events\n *\n * ## Interaction with Reactive Container\n *\n * `container.invalidate(token)` (from Step 3) does:\n * - Clears the cached instance for that token\n * - Walks the dependency graph to find dependents\n * - Invalidates all dependents recursively\n * - Emits batched `onChange()` events to all subscribers (Swagger, DevTools, etc.)\n *\n * ## Detection Strategy\n *\n * Uses regex patterns (not AST) for speed. This is the same approach\n * TanStack Start uses (`KindDetectionPatterns` in their compiler).\n * It's fast because:\n * - Only runs on files that actually contain decorator patterns\n * - No Babel/SWC parsing needed\n * - False positives are harmless (extra invalidation is safe)\n *\n * @see bench-mark/tanstack-router-analysis.md — Detection pattern origin\n * @see v3/plan.md Step 5 — Design rationale\n *\n * @module @forinda/kickjs-vite/hmr-plugin\n */\n\nimport type { Plugin, ViteDevServer } from 'vite'\nimport type { PluginContext } from './types'\nimport { RESOLVED_APP } from './virtual-modules'\n\n/**\n * Regex patterns that detect KickJS decorator usage in source files.\n * Matches `@Service()`, `@Controller('/path')`, `@Repository()`, etc.\n * followed by an exported class declaration.\n *\n * Pattern: decorator → optional whitespace → export? class ClassName\n */\nconst DECORATOR_CLASS_REGEX =\n /@(?:Service|Controller|Repository|Injectable|Component)\\s*\\([^)]*\\)\\s*\\n?\\s*(?:export\\s+)?class\\s+(\\w+)/g\n\n/** Debounce time for batching multiple file changes into one invalidation */\nconst DEBOUNCE_MS = 150\n\n/**\n * Creates the HMR selective invalidation plugin.\n *\n * Tracks which DI tokens are defined in which files, then selectively\n * invalidates only the affected tokens (and their dependents) when\n * those files change. Much faster than a full Container.reset().\n *\n * @param ctx - Shared plugin context\n * @returns Vite plugin\n */\nexport function kickjsHmrPlugin(ctx: PluginContext): Plugin {\n /**\n * Maps file paths to the DI token names defined in them.\n * Built up during transform(), read during handleHotUpdate().\n *\n * @example\n * ```\n * '/src/modules/users/user.service.ts' → ['UserService']\n * '/src/modules/users/user.controller.ts' → ['UserController']\n * ```\n */\n const fileTokenMap = new Map<string, string[]>()\n\n /** Pending tokens to invalidate (accumulated during debounce window) */\n let pendingTokens = new Set<string>()\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n\n return {\n name: 'kickjs:hmr',\n\n /**\n * Transform hook — scans source files for decorator patterns.\n *\n * Doesn't modify the code (returns null), just records which\n * DI tokens are defined in each file. This mapping is used by\n * `handleHotUpdate()` to determine what to invalidate.\n */\n transform(code: string, id: string) {\n // Skip non-TypeScript/JavaScript files and node_modules\n if (!/\\.[tj]sx?$/.test(id)) return null\n if (id.includes('node_modules')) return null\n\n // Quick bail-out: skip files that don't contain any decorator keywords\n if (\n !code.includes('@Service') &&\n !code.includes('@Controller') &&\n !code.includes('@Repository') &&\n !code.includes('@Injectable') &&\n !code.includes('@Component')\n ) {\n // If file was previously tracked but decorators were removed, clean up\n if (fileTokenMap.has(id)) {\n fileTokenMap.delete(id)\n }\n return null\n }\n\n // Extract class names from decorator patterns\n const tokens: string[] = []\n let match: RegExpExecArray | null\n // Reset regex lastIndex for reuse across files\n DECORATOR_CLASS_REGEX.lastIndex = 0\n while ((match = DECORATOR_CLASS_REGEX.exec(code)) !== null) {\n tokens.push(match[1])\n }\n\n if (tokens.length > 0) {\n fileTokenMap.set(id, tokens)\n } else if (fileTokenMap.has(id)) {\n fileTokenMap.delete(id)\n }\n\n return null\n },\n\n /**\n * HMR hook — called when a file changes in dev mode.\n *\n * If the changed file contains KickJS decorated classes:\n * 1. Accumulate the affected tokens in the debounce buffer\n * 2. After 150ms of quiet, flush: invalidate all tokens at once\n * 3. Invalidate the virtual module for fresh `ssrLoadModule()` response\n * 4. Send ONE `kickjs:hmr` event to the client (DevTools, Swagger UI)\n *\n * The 150ms debounce is critical for `kick g module` which creates\n * 10+ files in rapid succession. Without debouncing, each file would\n * trigger a separate invalidation cycle.\n *\n * Returns an empty array to tell Vite we handled the update\n * (prevents Vite's default full-page reload for these files).\n */\n handleHotUpdate({ file, server }) {\n const tokens = fileTokenMap.get(file)\n if (!tokens || tokens.length === 0) return\n\n // Accumulate tokens for batched invalidation\n for (const t of tokens) pendingTokens.add(t)\n\n // Debounce: flush after 150ms of quiet\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n flushInvalidation(server, pendingTokens)\n pendingTokens = new Set()\n }, DEBOUNCE_MS)\n\n // Tell Vite we handled it — don't do default full-page reload\n return []\n },\n }\n}\n\n/**\n * Flush accumulated token invalidations.\n *\n * Called after the debounce window closes. Performs:\n * 1. Container invalidation (clears instances + walks dependency graph)\n * 2. Virtual module invalidation (forces ssrLoadModule to re-evaluate)\n * 3. HMR event broadcast (notifies DevTools, Swagger UI, etc.)\n *\n * @param server - Vite dev server\n * @param tokens - Set of DI token names to invalidate\n */\nfunction flushInvalidation(server: ViteDevServer, tokens: Set<string>): void {\n const batch = [...tokens]\n if (batch.length === 0) return\n\n // 1. Invalidate tokens in the reactive container (if available)\n // container.invalidate() walks the dependency graph and notifies subscribers\n const container = (globalThis as any).__kickjs_container\n if (container && typeof container.invalidate === 'function') {\n for (const token of batch) {\n container.invalidate(token)\n }\n }\n\n // 2. Invalidate the virtual module so next ssrLoadModule() re-evaluates\n const vmod = server.moduleGraph.getModuleById(RESOLVED_APP)\n if (vmod) {\n server.moduleGraph.invalidateModule(vmod)\n }\n\n // 3. Send ONE custom HMR event with the full batch\n // DevTools dashboard, Swagger UI, and other dev tools can listen:\n // import.meta.hot?.on('kickjs:hmr', (data) => { ... })\n server.hot.send({\n type: 'custom',\n event: 'kickjs:hmr',\n data: { tokens: batch, timestamp: Date.now() },\n })\n\n // Log to dev console\n const names = batch.join(', ')\n const label = batch.length === 1 ? '1 token' : `${batch.length} tokens`\n console.log(` ${green('HMR')} invalidated ${label}: ${names}`)\n}\n\n/** ANSI green for terminal output */\nfunction green(text: string): string {\n return `\\x1b[32m${text}\\x1b[0m`\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { reloadEnv } from '@forinda/kickjs'\nimport type { Plugin } from 'vite'\n\n/**\n * Vite plugin that watches `.env` files and triggers a full reload\n * when they change. This ensures the dev server picks up environment\n * variable changes without a manual restart.\n *\n * Lives in `@forinda/kickjs-vite` so all Vite-only concerns are in one\n * place — the older re-export from `@forinda/kickjs-config` is kept as\n * a deprecated thin shim for back-compat.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { envWatchPlugin } from '@forinda/kickjs-vite'\n *\n * export default defineConfig({\n * plugins: [swc.vite(), envWatchPlugin()],\n * })\n * ```\n */\nexport function envWatchPlugin(): Plugin {\n const envFiles = ['.env', '.env.local', '.env.development', '.env.production', '.env.test']\n\n return {\n name: 'kickjs-env-watch',\n\n configureServer(server) {\n const root = server.config.root\n\n for (const file of envFiles) {\n const filePath = path.resolve(root, file)\n if (fs.existsSync(filePath)) {\n server.watcher.add(filePath)\n }\n }\n\n server.watcher.on('change', (changedPath) => {\n const basename = path.basename(changedPath)\n if (envFiles.includes(basename)) {\n server.config.logger.info(` .env changed (${basename}), triggering reload...`, {\n timestamp: true,\n })\n\n // Re-read the .env file into process.env *before* invalidating\n // modules. Without this, the next SSR evaluation parses the\n // (stale) cached env snapshot — Zod throws on the missing key\n // and the user has to hard-restart the dev server.\n try {\n reloadEnv()\n } catch (err: any) {\n server.config.logger.warn(\n ` env reload failed: ${err?.message ?? err} — restart may be required`,\n { timestamp: true },\n )\n }\n\n // Invalidate all modules to trigger full HMR rebuild\n const mods = server.moduleGraph.getModulesByFile(path.resolve(root, 'src/index.ts'))\n if (mods) {\n for (const mod of mods) {\n server.moduleGraph.invalidateModule(mod)\n }\n }\n\n server.ws.send({ type: 'full-reload' })\n }\n })\n },\n }\n}\n","/**\n * @forinda/kickjs-vite — Vite plugin for the KickJS framework.\n *\n * Provides first-class Vite integration for KickJS backend applications:\n * - **Dev server**: Mounts Express on Vite's HTTP server (single port)\n * - **HMR**: Fresh server code on every request via `ssrLoadModule()`\n * - **httpServer piping**: Real `http.Server` available to all adapters\n * - **Virtual modules**: Auto-generated application entry\n *\n * ## Quick Start\n *\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite'\n * import { kickjsVitePlugin } from '@forinda/kickjs-vite'\n * import swc from 'unplugin-swc'\n *\n * export default defineConfig({\n * plugins: [\n * swc.vite({ tsconfigFile: 'tsconfig.json' }),\n * kickjsVitePlugin({ entry: 'src/index.ts' }),\n * ],\n * })\n * ```\n *\n * ```ts\n * // src/index.ts\n * import { bootstrap } from '@forinda/kickjs'\n * import { UserModule } from './modules/users/user.module'\n *\n * // Export the Express app — Vite serves it in dev, you start it in prod\n * export const app = bootstrap({\n * modules: [UserModule],\n * middleware: [express.json()],\n * })\n *\n * // Production: start the server directly\n * if (process.env.NODE_ENV === 'production') {\n * app.start()\n * }\n * ```\n *\n * ## Architecture\n *\n * The plugin returns an array of focused sub-plugins (React Router pattern):\n *\n * | Plugin | Responsibility |\n * |--------|---------------|\n * | `kickjs:core` | Vite config: appType, SSR environment, externals |\n * | `kickjs:module-discovery` | Auto-discover `@Module` classes via `transform()` |\n * | `kickjs:hmr` | Selective container invalidation via `handleHotUpdate()` |\n * | `kickjs:virtual-modules` | `virtual:kickjs/app` resolution and generation |\n * | `kickjs:dev-server` | `configureServer()` — mounts Express, pipes httpServer |\n *\n * ## httpServer Piping\n *\n * Vite creates the `http.Server` — this plugin stores it on\n * `globalThis.__kickjs_httpServer`. KickJS adapters (WsAdapter, Socket.IO,\n * GraphQL subscriptions) attach to this server via the standard\n * `afterStart({ server })` hook. Zero adapter code changes needed.\n *\n * @see v3/architecture.md — Full architecture documentation\n * @see v3/plan.md — Implementation plan and rationale\n * @see bench-mark/react-router-analysis.md — Pattern origin\n *\n * @module @forinda/kickjs-vite\n */\n\nimport { resolve } from 'node:path'\nimport type { Plugin } from 'vite'\nimport type { KickJSPluginOptions, PluginContext } from './types'\nimport { kickjsCorePlugin } from './core-plugin'\nimport { kickjsVirtualModulesPlugin } from './virtual-modules'\nimport { kickjsDevServerPlugin } from './dev-server'\nimport { kickjsModuleDiscoveryPlugin } from './module-discovery'\nimport { kickjsHmrPlugin } from './hmr-plugin'\n\n/**\n * Create the KickJS Vite plugin array.\n *\n * Returns an array of focused sub-plugins that together provide full\n * Vite integration for KickJS backend applications. Each sub-plugin\n * has a single responsibility and runs at the appropriate Vite lifecycle stage.\n *\n * @param options - Plugin configuration\n * @param options.entry - Path to app entry file (default: 'src/index.ts')\n * @returns Array of Vite plugins\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite'\n * import { kickjsVitePlugin } from '@forinda/kickjs-vite'\n * import swc from 'unplugin-swc'\n *\n * export default defineConfig({\n * plugins: [\n * swc.vite({ tsconfigFile: 'tsconfig.json' }),\n * kickjsVitePlugin(),\n * ],\n * })\n * ```\n */\nexport function kickjsVitePlugin(options: KickJSPluginOptions = {}): Plugin[] {\n const entry = options.entry ?? 'src/index.ts'\n\n // Create shared context — resolved lazily in config hook since we\n // don't have the root directory until Vite resolves its config.\n const ctx: PluginContext = {\n entry,\n root: process.cwd(),\n }\n\n // Resolve root and entry as early as possible so sub-plugin config() hooks\n // (e.g., warmup) see the correct absolute entry path.\n const rootResolver: Plugin = {\n name: 'kickjs:root-resolver',\n config(config) {\n const root = config.root ?? process.cwd()\n ctx.root = root\n ctx.entry = resolve(root, entry)\n },\n }\n\n return [\n rootResolver,\n kickjsCorePlugin(ctx),\n kickjsModuleDiscoveryPlugin(ctx),\n kickjsHmrPlugin(ctx),\n kickjsVirtualModulesPlugin(ctx),\n kickjsDevServerPlugin(ctx),\n ]\n}\n\n// Re-export types for consumers\nexport type { KickJSPluginOptions, PluginContext } from './types'\n\n// Standalone plugins users can compose alongside `kickjsVitePlugin()`\nexport { envWatchPlugin } from './env-watch-plugin'\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-vite",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
4
4
|
"description": "Vite plugin for KickJS — dev server integration, HMR, virtual modules, httpServer piping",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@forinda/kickjs": "2.3.
|
|
71
|
+
"@forinda/kickjs": "2.3.3"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"vite": ">=6.0.0",
|