@forinda/kickjs-vite 3.2.0 → 4.1.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/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @forinda/kickjs-vite
2
2
 
3
- Vite plugin for KickJS — single-port dev server, HMR, virtual modules, and
4
- `http.Server` piping for adapters (WebSocket, GraphQL subscriptions, etc.).
3
+ Vite plugin for KickJS — single-port dev server, HMR, virtual modules, and `http.Server` piping for adapters (WebSocket, GraphQL subscriptions, etc.).
5
4
 
6
5
  ## Install
7
6
 
@@ -9,102 +8,40 @@ Vite plugin for KickJS — single-port dev server, HMR, virtual modules, and
9
8
  pnpm add -D @forinda/kickjs-vite vite unplugin-swc
10
9
  ```
11
10
 
12
- `vite` (>=6) and `express` (^5) are peer dependencies.
13
-
14
- ## Quick Start
11
+ ## Quick Example
15
12
 
16
13
  ```ts
17
14
  // vite.config.ts
18
15
  import { defineConfig } from 'vite'
19
- import { kickjsVitePlugin } from '@forinda/kickjs-vite'
20
16
  import swc from 'unplugin-swc'
17
+ import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
21
18
 
22
19
  export default defineConfig({
20
+ oxc: false,
23
21
  plugins: [
24
- swc.vite({ tsconfigFile: 'tsconfig.json' }),
22
+ swc.vite(),
25
23
  kickjsVitePlugin({ entry: 'src/index.ts' }),
24
+ envWatchPlugin(),
26
25
  ],
26
+ ssr: { external: ['pino', 'pino-pretty'] },
27
+ build: { target: 'node20', ssr: true, outDir: 'dist' },
27
28
  })
28
29
  ```
29
30
 
30
31
  ```ts
31
32
  // src/index.ts
33
+ import 'reflect-metadata'
32
34
  import { bootstrap } from '@forinda/kickjs'
33
- import express from 'express'
34
- import { UserModule } from './modules/users/user.module'
35
-
36
- // Export the app — Vite serves it in dev, you start it in prod
37
- export const app = bootstrap({
38
- modules: [UserModule],
39
- middleware: [express.json()],
40
- })
41
-
42
- ```
43
-
44
- Then run `vite` (or `kick dev`) and the Express app is mounted on Vite's HTTP
45
- server. Edits trigger selective HMR — only the affected modules re-register in
46
- the DI container, so DB pools, Redis clients, and WebSocket connections stay
47
- alive across reloads.
35
+ import { modules } from './modules'
48
36
 
49
- ## What you get
50
-
51
- - **Single-port dev server** — Express is mounted on Vite's HTTP server, so
52
- one URL serves both your API and any Vite-managed assets.
53
- - **Selective HMR** — only changed modules re-register in the KickJS
54
- container; DB pools, Redis clients, and WebSocket connections stay alive
55
- across reloads.
56
- - **Adapter-friendly** — KickJS adapters (`WsAdapter`, GraphQL subscriptions,
57
- etc.) attach to the dev `http.Server` through the standard
58
- `afterStart({ server })` hook with zero extra wiring.
59
- - **Auto-generated entry** — discovered modules are wired into the app
60
- automatically; no manual entry boilerplate.
61
-
62
- ## Options
63
-
64
- ```ts
65
- interface KickJSPluginOptions {
66
- /** Path to app entry file, relative to Vite root. Default: 'src/index.ts' */
67
- entry?: string
68
- }
37
+ export const app = await bootstrap({ modules })
69
38
  ```
70
39
 
71
- ## `envWatchPlugin()` — `.env` file hot-reload
72
-
73
- Watches `.env`, `.env.local`, `.env.development`, `.env.production`, and
74
- `.env.test`, and triggers a full Vite reload whenever any of them change.
75
- Compose it alongside `kickjsVitePlugin()`:
76
-
77
- ```ts
78
- // vite.config.ts
79
- import { defineConfig } from 'vite'
80
- import { kickjsVitePlugin, envWatchPlugin } from '@forinda/kickjs-vite'
81
- import swc from 'unplugin-swc'
82
-
83
- export default defineConfig({
84
- plugins: [
85
- swc.vite({ tsconfigFile: 'tsconfig.json' }),
86
- kickjsVitePlugin({ entry: 'src/index.ts' }),
87
- envWatchPlugin(),
88
- ],
89
- })
90
- ```
91
-
92
- > **Moved.** This used to live in `@forinda/kickjs-config`. The old export
93
- > still exists as a back-compat shim and prints a deprecation warning;
94
- > migrate to `@forinda/kickjs-vite` before v3.
95
-
96
- `envWatchPlugin()` pairs naturally with the merged `ConfigService`
97
- (`@forinda/kickjs`) — once a `.env` change triggers a reload, the next
98
- `loadEnv()` call re-validates `process.env` against your Zod schema.
99
-
100
- ## With the CLI
101
-
102
- If you use `kick dev` from `@forinda/kickjs-cli`, this plugin is wired up
103
- automatically — no manual `vite.config.ts` is required for typical projects.
40
+ The plugin reads the exported `app` from the entry file, mounts Express on Vite's HTTP server (single port for both Vite assets + KickJS API), and gives HMR-aware reloads. `envWatchPlugin()` triggers a full reload when `.env` changes so config tweaks land without a manual restart.
104
41
 
105
42
  ## Documentation
106
43
 
107
- [Full documentation](https://github.com/forinda/kick-js)
44
+ [forinda.github.io/kick-js/guide/vite-plugin](https://forinda.github.io/kick-js/guide/vite-plugin)
108
45
 
109
46
  ## License
110
47
 
package/dist/index.d.mts CHANGED
@@ -39,6 +39,57 @@ interface KickJSPluginOptions {
39
39
  * ```
40
40
  */
41
41
  entry?: string;
42
+ /** HMR logger / behaviour overrides (see {@link HmrOptions}). */
43
+ hmr?: HmrOptions;
44
+ }
45
+ /**
46
+ * Payload handed to {@link HmrOptions.onInvalidation}.
47
+ *
48
+ * `tokens` is the deduplicated batch flushed for one debounce window —
49
+ * a mix of class names (`'UserController'`), v4 factory entries
50
+ * (`'MyAdapter (defineAdapter)'`), and bare basenames (`'hello.ts'`)
51
+ * for plain source files without kickjs patterns. `timestamp` matches
52
+ * the value broadcast on the `kickjs:hmr` HMR client event so dev tools
53
+ * can correlate.
54
+ */
55
+ interface HmrInvalidationContext {
56
+ /** Token names / file basenames flushed in this debounce window. */
57
+ tokens: readonly string[];
58
+ /** Epoch ms when the batch was flushed. */
59
+ timestamp: number;
60
+ }
61
+ /**
62
+ * HMR-related plugin options.
63
+ *
64
+ * Defaults preserve the existing `HMR invalidated N tokens: …` log line.
65
+ * Adopters with custom dev tooling (Discord webhooks, structured JSON
66
+ * logs, in-app overlays) override `onInvalidation`; tests / CI dev runs
67
+ * usually want `silent: true`.
68
+ */
69
+ interface HmrOptions {
70
+ /**
71
+ * Suppress the built-in console log entirely. The `kickjs:hmr` HMR
72
+ * event is still broadcast — DevTools / Swagger UI continue to react
73
+ * to invalidations, only the terminal stays quiet.
74
+ */
75
+ silent?: boolean;
76
+ /**
77
+ * Replace the built-in dev-console line with a custom function. When
78
+ * provided, the default log is *not* printed (your function owns the
79
+ * channel). Returning a string emits a single `console.log`; returning
80
+ * `undefined` / `void` suppresses output entirely so you can route
81
+ * elsewhere (pino, OTel span events, websocket, etc.).
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * kickjsVitePlugin({
86
+ * hmr: {
87
+ * onInvalidation: ({ tokens }) => `↻ rebuilt: ${tokens.join(' · ')}`,
88
+ * },
89
+ * })
90
+ * ```
91
+ */
92
+ onInvalidation?: (context: HmrInvalidationContext) => string | undefined | void;
42
93
  }
43
94
  /**
44
95
  * Shared context passed between all KickJS sub-plugins.
@@ -58,8 +109,7 @@ interface PluginContext {
58
109
  * variable changes without a manual restart.
59
110
  *
60
111
  * Lives in `@forinda/kickjs-vite` so all Vite-only concerns are in one
61
- * place — the older re-export from `@forinda/kickjs-config` is kept as
62
- * a deprecated thin shim for back-compat.
112
+ * place.
63
113
  *
64
114
  * @example
65
115
  * ```ts
@@ -102,5 +152,5 @@ declare function envWatchPlugin(): Plugin;
102
152
  */
103
153
  declare function kickjsVitePlugin(options?: KickJSPluginOptions): Plugin[];
104
154
  //#endregion
105
- export { type KickJSPluginOptions, type PluginContext, envWatchPlugin, kickjsVitePlugin };
155
+ export { type HmrInvalidationContext, type HmrOptions, type KickJSPluginOptions, type PluginContext, envWatchPlugin, kickjsVitePlugin };
106
156
  //# sourceMappingURL=index.d.mts.map
@@ -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;;;;;;;;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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/env-watch-plugin.ts","../src/index.ts"],"mappings":";;;;;;;AAuBA;;;;;;;;;AA8BA;;;;;AAeA;;;;;UA7CiB,mBAAA;EAqEY;;;;AAO7B;;;;;;;;AC5EA;EDcE,KAAA;;EAGA,GAAA,GAAM,UAAA;AAAA;;;;AE+DR;;;;;;;UFlDiB,sBAAA;EEkD0D;EFhDzE,MAAA;;EAEA,SAAA;AAAA;;;;;;;;;UAWe,UAAA;;;;;;EAMf,MAAA;;;;;;;;;;;;;;;;;EAkBA,cAAA,IAAkB,OAAA,EAAS,sBAAA;AAAA;;;;;UAOZ,aAAA;;EAEf,KAAA;;EAEA,IAAA;AAAA;;;;;AAhFF;;;;;;;;;AA8BA;;;;;AAeA;;iBC7CgB,cAAA,CAAA,GAAkB,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBCgFlB,gBAAA,CAAiB,OAAA,GAAS,mBAAA,GAA2B,MAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-vite v3.2.0
2
+ * @forinda/kickjs-vite v4.1.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -39,8 +39,6 @@ function kickjsCorePlugin(ctx) {
39
39
  optimizeDeps: { noDiscovery: true },
40
40
  ssr: { external: [
41
41
  "@forinda/kickjs",
42
- "@forinda/kickjs-core",
43
- "@forinda/kickjs-http",
44
42
  "express",
45
43
  "reflect-metadata"
46
44
  ] }
@@ -221,13 +219,68 @@ function invalidateVirtualApp(server) {
221
219
  //#endregion
222
220
  //#region src/hmr-plugin.ts
223
221
  /**
222
+ * Test whether a watcher event refers to a file we should react to.
223
+ *
224
+ * Vite's chokidar fires for everything under the project root. We only
225
+ * want code that the running Express app could realistically depend on:
226
+ *
227
+ * - **Source extensions only.** `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`,
228
+ * `.cts`, `.mjs`, `.cjs`. JSON / lockfiles / images / typegen
229
+ * declaration files are noise.
230
+ * - **Skip `.d.ts` declaration files.** They're regenerated by typegen
231
+ * on every save and would create an invalidation loop with the
232
+ * typegen watcher in `kick dev`.
233
+ * - **Skip well-known noise directories.** `node_modules`, `dist`, the
234
+ * `.kickjs` typegen output, `.git`, `coverage`, OS swap files.
235
+ *
236
+ * Errs on the inclusive side: a file that is genuinely unused but still
237
+ * lives under `src/` causes a cheap virtual-app invalidation, no worse
238
+ * than `kick dev`'s typegen sweep already does on the same event.
239
+ */
240
+ function isProjectSource(file) {
241
+ if (!/\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)$/.test(file)) return false;
242
+ if (file.endsWith(".d.ts") || file.endsWith(".d.mts") || file.endsWith(".d.cts")) return false;
243
+ if (file.includes("/node_modules/") || file.includes("/.kickjs/") || file.includes("/dist/") || file.includes("/.git/") || file.includes("/coverage/")) return false;
244
+ return true;
245
+ }
246
+ /** Strip directory components for a friendlier dev-console label. */
247
+ function basename(file) {
248
+ const i = file.lastIndexOf("/");
249
+ return i === -1 ? file : file.slice(i + 1);
250
+ }
251
+ /**
224
252
  * Regex patterns that detect KickJS decorator usage in source files.
225
- * Matches `@Service()`, `@Controller('/path')`, `@Repository()`, etc.
253
+ * Matches `@Service()`, `@Controller()`, `@Repository()`, etc.
226
254
  * followed by an exported class declaration.
227
255
  *
228
256
  * Pattern: decorator → optional whitespace → export? class ClassName
229
257
  */
230
258
  const DECORATOR_CLASS_REGEX = /@(?:Service|Controller|Repository|Injectable|Component)\s*\([^)]*\)\s*\n?\s*(?:export\s+)?class\s+(\w+)/g;
259
+ /**
260
+ * Regex patterns that detect the v4 factory-style declarations:
261
+ * `defineAdapter`, `definePlugin`, `defineContextDecorator`. These
262
+ * produce values rather than decorated classes, so the class regex
263
+ * above misses them and changes to plugin/adapter/contributor files
264
+ * silently bypass kickjs HMR.
265
+ *
266
+ * Pattern: `export const Name = defineAdapter|definePlugin|defineContextDecorator(`
267
+ * — captures the constant's name so it can be surfaced in the dev
268
+ * console alongside the rest of the invalidation batch.
269
+ */
270
+ const FACTORY_DECL_REGEX = /(?:export\s+)?const\s+(\w+)\s*(?::[^=]+)?=\s*(defineAdapter|definePlugin|defineContextDecorator)\s*[<(]/g;
271
+ /** Substrings that bail out the transform scanner before regex work. */
272
+ const FACTORY_KEYWORDS = [
273
+ "defineAdapter",
274
+ "definePlugin",
275
+ "defineContextDecorator"
276
+ ];
277
+ const DECORATOR_KEYWORDS = [
278
+ "@Service",
279
+ "@Controller",
280
+ "@Repository",
281
+ "@Injectable",
282
+ "@Component"
283
+ ];
231
284
  /** Debounce time for batching multiple file changes into one invalidation */
232
285
  const DEBOUNCE_MS = 150;
233
286
  /**
@@ -240,7 +293,7 @@ const DEBOUNCE_MS = 150;
240
293
  * @param ctx - Shared plugin context
241
294
  * @returns Vite plugin
242
295
  */
243
- function kickjsHmrPlugin(ctx) {
296
+ function kickjsHmrPlugin(ctx, hmrOptions = {}) {
244
297
  /**
245
298
  * Maps file paths to the DI token names defined in them.
246
299
  * Built up during transform(), read during handleHotUpdate().
@@ -260,25 +313,27 @@ function kickjsHmrPlugin(ctx) {
260
313
  transform(code, id) {
261
314
  if (!/\.[tj]sx?$/.test(id)) return null;
262
315
  if (id.includes("node_modules")) return null;
263
- if (!code.includes("@Service") && !code.includes("@Controller") && !code.includes("@Repository") && !code.includes("@Injectable") && !code.includes("@Component")) {
316
+ const hasDecorator = DECORATOR_KEYWORDS.some((kw) => code.includes(kw));
317
+ const hasFactory = FACTORY_KEYWORDS.some((kw) => code.includes(kw));
318
+ if (!hasDecorator && !hasFactory) {
264
319
  if (fileTokenMap.has(id)) fileTokenMap.delete(id);
265
320
  return null;
266
321
  }
267
322
  const tokens = [];
268
- let match;
269
- DECORATOR_CLASS_REGEX.lastIndex = 0;
270
- while ((match = DECORATOR_CLASS_REGEX.exec(code)) !== null) tokens.push(match[1]);
323
+ if (hasDecorator) for (const m of code.matchAll(DECORATOR_CLASS_REGEX)) tokens.push(m[1]);
324
+ if (hasFactory) for (const m of code.matchAll(FACTORY_DECL_REGEX)) tokens.push(`${m[1]} (${m[2]})`);
271
325
  if (tokens.length > 0) fileTokenMap.set(id, tokens);
272
326
  else if (fileTokenMap.has(id)) fileTokenMap.delete(id);
273
327
  return null;
274
328
  },
275
329
  handleHotUpdate({ file, server }) {
330
+ if (!isProjectSource(file)) return;
276
331
  const tokens = fileTokenMap.get(file);
277
- if (!tokens || tokens.length === 0) return;
278
- for (const t of tokens) pendingTokens.add(t);
332
+ if (tokens && tokens.length > 0) for (const t of tokens) pendingTokens.add(t);
333
+ else pendingTokens.add(basename(file));
279
334
  if (debounceTimer) clearTimeout(debounceTimer);
280
335
  debounceTimer = setTimeout(() => {
281
- flushInvalidation(server, pendingTokens);
336
+ flushInvalidation(server, pendingTokens, hmrOptions);
282
337
  pendingTokens = /* @__PURE__ */ new Set();
283
338
  }, DEBOUNCE_MS);
284
339
  return [];
@@ -296,9 +351,10 @@ function kickjsHmrPlugin(ctx) {
296
351
  * @param server - Vite dev server
297
352
  * @param tokens - Set of DI token names to invalidate
298
353
  */
299
- function flushInvalidation(server, tokens) {
354
+ function flushInvalidation(server, tokens, hmrOptions) {
300
355
  const batch = [...tokens];
301
356
  if (batch.length === 0) return;
357
+ const timestamp = Date.now();
302
358
  const container = globalThis.__kickjs_container;
303
359
  if (container && typeof container.invalidate === "function") for (const token of batch) container.invalidate(token);
304
360
  const vmod = server.moduleGraph.getModuleById(RESOLVED_APP);
@@ -308,9 +364,18 @@ function flushInvalidation(server, tokens) {
308
364
  event: "kickjs:hmr",
309
365
  data: {
310
366
  tokens: batch,
311
- timestamp: Date.now()
367
+ timestamp
312
368
  }
313
369
  });
370
+ if (hmrOptions.silent) return;
371
+ if (hmrOptions.onInvalidation) {
372
+ const out = hmrOptions.onInvalidation({
373
+ tokens: batch,
374
+ timestamp
375
+ });
376
+ if (typeof out === "string") console.log(out);
377
+ return;
378
+ }
314
379
  const names = batch.join(", ");
315
380
  const label = batch.length === 1 ? "1 token" : `${batch.length} tokens`;
316
381
  console.log(` ${green("HMR")} invalidated ${label}: ${names}`);
@@ -327,8 +392,7 @@ function green(text) {
327
392
  * variable changes without a manual restart.
328
393
  *
329
394
  * Lives in `@forinda/kickjs-vite` so all Vite-only concerns are in one
330
- * place — the older re-export from `@forinda/kickjs-config` is kept as
331
- * a deprecated thin shim for back-compat.
395
+ * place.
332
396
  *
333
397
  * @example
334
398
  * ```ts
@@ -485,7 +549,7 @@ function kickjsVitePlugin(options = {}) {
485
549
  },
486
550
  kickjsCorePlugin(ctx),
487
551
  kickjsModuleDiscoveryPlugin(ctx),
488
- kickjsHmrPlugin(ctx),
552
+ kickjsHmrPlugin(ctx, options.hmr),
489
553
  kickjsVirtualModulesPlugin(ctx),
490
554
  kickjsDevServerPlugin(ctx)
491
555
  ];
@@ -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(userConfig, _env) {\n // Respect explicit server.port from user's vite.config.ts.\n // Only inject .env PORT when the user hasn't set one.\n const explicitPort = userConfig.server?.port\n const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined\n const port = explicitPort ?? envPort ?? 3000\n\n return {\n server: {\n port,\n },\n\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 SSR\n * renderers 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)\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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"}
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(userConfig, _env) {\n // Respect explicit server.port from user's vite.config.ts.\n // Only inject .env PORT when the user hasn't set one.\n const explicitPort = userConfig.server?.port\n const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined\n const port = explicitPort ?? envPort ?? 3000\n\n return {\n server: {\n port,\n },\n\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: ['@forinda/kickjs', 'express', 'reflect-metadata'],\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 SSR\n * renderers 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)\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 { HmrOptions, PluginContext } from './types'\nimport { RESOLVED_APP } from './virtual-modules'\n\n/**\n * Test whether a watcher event refers to a file we should react to.\n *\n * Vite's chokidar fires for everything under the project root. We only\n * want code that the running Express app could realistically depend on:\n *\n * - **Source extensions only.** `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`,\n * `.cts`, `.mjs`, `.cjs`. JSON / lockfiles / images / typegen\n * declaration files are noise.\n * - **Skip `.d.ts` declaration files.** They're regenerated by typegen\n * on every save and would create an invalidation loop with the\n * typegen watcher in `kick dev`.\n * - **Skip well-known noise directories.** `node_modules`, `dist`, the\n * `.kickjs` typegen output, `.git`, `coverage`, OS swap files.\n *\n * Errs on the inclusive side: a file that is genuinely unused but still\n * lives under `src/` causes a cheap virtual-app invalidation, no worse\n * than `kick dev`'s typegen sweep already does on the same event.\n */\nfunction isProjectSource(file: string): boolean {\n if (!/\\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)$/.test(file)) return false\n if (file.endsWith('.d.ts') || file.endsWith('.d.mts') || file.endsWith('.d.cts')) return false\n if (\n file.includes('/node_modules/') ||\n file.includes('/.kickjs/') ||\n file.includes('/dist/') ||\n file.includes('/.git/') ||\n file.includes('/coverage/')\n ) {\n return false\n }\n return true\n}\n\n/** Strip directory components for a friendlier dev-console label. */\nfunction basename(file: string): string {\n const i = file.lastIndexOf('/')\n return i === -1 ? file : file.slice(i + 1)\n}\n\n/**\n * Regex patterns that detect KickJS decorator usage in source files.\n * Matches `@Service()`, `@Controller()`, `@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/**\n * Regex patterns that detect the v4 factory-style declarations:\n * `defineAdapter`, `definePlugin`, `defineContextDecorator`. These\n * produce values rather than decorated classes, so the class regex\n * above misses them and changes to plugin/adapter/contributor files\n * silently bypass kickjs HMR.\n *\n * Pattern: `export const Name = defineAdapter|definePlugin|defineContextDecorator(`\n * — captures the constant's name so it can be surfaced in the dev\n * console alongside the rest of the invalidation batch.\n */\nconst FACTORY_DECL_REGEX =\n /(?:export\\s+)?const\\s+(\\w+)\\s*(?::[^=]+)?=\\s*(defineAdapter|definePlugin|defineContextDecorator)\\s*[<(]/g\n\n/** Substrings that bail out the transform scanner before regex work. */\nconst FACTORY_KEYWORDS = ['defineAdapter', 'definePlugin', 'defineContextDecorator'] as const\nconst DECORATOR_KEYWORDS = [\n '@Service',\n '@Controller',\n '@Repository',\n '@Injectable',\n '@Component',\n] as const\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, hmrOptions: HmrOptions = {}): 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 const hasDecorator = DECORATOR_KEYWORDS.some((kw) => code.includes(kw))\n const hasFactory = FACTORY_KEYWORDS.some((kw) => code.includes(kw))\n\n // Quick bail-out: nothing kickjs-relevant in the file\n if (!hasDecorator && !hasFactory) {\n if (fileTokenMap.has(id)) fileTokenMap.delete(id)\n return null\n }\n\n const tokens: string[] = []\n\n if (hasDecorator) {\n for (const m of code.matchAll(DECORATOR_CLASS_REGEX)) {\n tokens.push(m[1])\n }\n }\n\n if (hasFactory) {\n // Tag each entry with the factory kind so the HMR log line\n // distinguishes a controller-class invalidation from an adapter\n // / plugin / contributor reload — the second kind never maps\n // back to a DI token, but it still needs the virtual app\n // re-evaluated so bootstrap re-imports the new value.\n for (const m of code.matchAll(FACTORY_DECL_REGEX)) {\n tokens.push(`${m[1]} (${m[2]})`)\n }\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 // Reject anything outside the project's source surface — chokidar\n // also fires for `.kickjs/` typegen output, `dist/`, and editor\n // swap files that have nothing to do with the running app.\n if (!isProjectSource(file)) return\n\n const tokens = fileTokenMap.get(file)\n\n // Files with kickjs decorators / factories: accumulate their token\n // names so the dev console can name what changed. Files without a\n // tracked token (plain helpers, pure utilities, side-effect modules\n // like `hello.ts`) still trigger an invalidation — just labelled\n // by the file's basename so the user sees something happened.\n if (tokens && tokens.length > 0) {\n for (const t of tokens) pendingTokens.add(t)\n } else {\n pendingTokens.add(basename(file))\n }\n\n // Debounce: flush after 150ms of quiet\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n flushInvalidation(server, pendingTokens, hmrOptions)\n pendingTokens = new Set()\n }, DEBOUNCE_MS)\n\n // Tell Vite we handled it — don't do default full-page reload.\n // `flushInvalidation` always invalidates the virtual app module,\n // so the next ssrLoadModule re-imports the changed file via the\n // dev-server's middleware shim.\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(\n server: ViteDevServer,\n tokens: Set<string>,\n hmrOptions: HmrOptions,\n): void {\n const batch = [...tokens]\n if (batch.length === 0) return\n const timestamp = Date.now()\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 },\n })\n\n // 4. Dev-console log — three modes, in priority order:\n // a. `silent: true` → no log\n // b. `onInvalidation` provided → custom function controls the line\n // (returning a string emits one console.log, void/undefined =\n // suppresses, useful when piping to a structured logger or\n // overlay)\n // c. neither set → default `HMR invalidated N tokens: …` line\n if (hmrOptions.silent) return\n\n if (hmrOptions.onInvalidation) {\n const out = hmrOptions.onInvalidation({ tokens: batch, timestamp })\n if (typeof out === 'string') console.log(out)\n return\n }\n\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.\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, options.hmr),\n kickjsVirtualModulesPlugin(ctx),\n kickjsDevServerPlugin(ctx),\n ]\n}\n\n// Re-export types for consumers\nexport type {\n KickJSPluginOptions,\n PluginContext,\n HmrOptions,\n HmrInvalidationContext,\n} 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": "3.2.0",
3
+ "version": "4.1.0",
4
4
  "description": "Vite plugin for KickJS — dev server integration, HMR, virtual modules, httpServer piping",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -20,13 +20,10 @@
20
20
  "@forinda/kickjs",
21
21
  "@forinda/kickjs-auth",
22
22
  "@forinda/kickjs-cli",
23
- "@forinda/kickjs-config",
24
- "@forinda/kickjs-core",
25
23
  "@forinda/kickjs-cron",
26
24
  "@forinda/kickjs-devtools",
27
25
  "@forinda/kickjs-drizzle",
28
26
  "@forinda/kickjs-graphql",
29
- "@forinda/kickjs-http",
30
27
  "@forinda/kickjs-mailer",
31
28
  "@forinda/kickjs-multi-tenant",
32
29
  "@forinda/kickjs-notifications",
@@ -75,12 +72,12 @@
75
72
  },
76
73
  "devDependencies": {
77
74
  "@types/express": "^5.0.6",
78
- "@types/node": "^25.0.0",
75
+ "@types/node": "^25.6.0",
79
76
  "express": "^5.1.0",
80
- "typescript": "^5.9.2",
81
- "vite": "^8.0.3",
82
- "vitest": "^4.1.2",
83
- "@forinda/kickjs": "3.2.0"
77
+ "typescript": "^6.0.3",
78
+ "vite": "^8.0.9",
79
+ "vitest": "^4.1.5",
80
+ "@forinda/kickjs": "4.1.0"
84
81
  },
85
82
  "publishConfig": {
86
83
  "access": "public"