@vercube/vite 1.2.1 → 1.3.1

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
@@ -73,13 +73,14 @@ node dist/index.mjs # runs the built server
73
73
 
74
74
  ## Options
75
75
 
76
- | Option | Type | Default | Description |
77
- | ------------ | ---------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------- |
78
- | `rootDir` | `string` | Vite `root` | Project root used to resolve `scanDirs` and `setupFile`. |
79
- | `scanDirs` | `string[]` | `['src']` | Directories scanned (recursively) for `@Controller` and `@Injectable` classes. |
80
- | `setupFile` | `string` | `undefined` | Module whose default export `(app: App) => void \| Promise<void>` runs as `createApp`'s setup hook (before init). |
81
- | `runner` | `string` | `'node-worker'` | The `env-runner` runner used to execute server code in dev. |
82
- | `noExternal` | `(string \| RegExp)[]` | `undefined` | Extra package patterns merged after `@vercube/*` for dev and production bundling (see below). |
76
+ | Option | Type | Default | Description |
77
+ | ------------ | ---------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
78
+ | `rootDir` | `string` | Vite `root` | Project root used to resolve `scanDirs` and `setupFile`. |
79
+ | `scanDirs` | `string[]` | `['src']` | Directories scanned (recursively) for `@Controller` and `@Injectable` classes. |
80
+ | `setupFile` | `string` | `undefined` | Module whose default export `(app: App) => void \| Promise<void>` runs as `createApp`'s setup hook (before init). |
81
+ | `appConfig` | `ConfigTypes.Config` | `undefined` | App config passed to `createApp({ cfg })` in the generated entry, so it ships with the build instead of a `vercube.config` read at runtime. Plain data only (written as JSON). |
82
+ | `runner` | `string` | `'node-worker'` | The `env-runner` runner used to execute server code in dev. |
83
+ | `noExternal` | `(string \| RegExp)[]` | `undefined` | Extra package patterns merged after `@vercube/*` for dev and production bundling (see below). |
83
84
 
84
85
  ### `noExternal`
85
86
 
@@ -1,15 +1,16 @@
1
- import { dirname, isAbsolute, resolve } from "pathe";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { dirname, isAbsolute, join, resolve } from "pathe";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
3
4
  import { scanSource } from "@vercube/scan";
4
5
  //#region src/entry.ts
5
6
  /**
6
7
  * Generates the source of the server entry module.
7
8
  *
8
- * The module creates a Vercube app, binds every auto-discovered controller and
9
- * service into its DI container, runs the optional setup file, flushes the
10
- * container queue (which registers routes via decorator initialization), and
11
- * exports the app's `fetch` handler. Imports and binds are deduplicated by class
12
- * name.
9
+ * The module creates a Vercube app (with the plugin's `appConfig`, if any),
10
+ * binds every auto-discovered controller and service into its DI container,
11
+ * runs the optional setup file, flushes the container queue (which registers
12
+ * routes via decorator initialization), and exports the app's `fetch` handler.
13
+ * Imports and binds are deduplicated by class name.
13
14
  *
14
15
  * The entry is written to a real file rather than served as a `\0`-virtual
15
16
  * module: Vite resolves bare imports from virtual modules inconsistently with
@@ -31,7 +32,9 @@ function generateServerEntry(ctx) {
31
32
  const setupBody = [];
32
33
  if (ctx.setupFile) setupBody.push(" await __vercubeSetup__(app);");
33
34
  if (ctx.hasClient) setupBody.push(" if (import.meta.main) {", " const dir = fileURLToPath(new URL('./public', import.meta.url));", " app.container.get(HttpServer).addPlugin(serveStaticFiles(dir));", " }");
34
- if (setupBody.length > 0) lines.push("const app = await createApp({ setup: async (app) => {", ...setupBody, "} });", "");
35
+ const cfg = ctx.pluginConfig.appConfig ? `cfg: ${JSON.stringify(ctx.pluginConfig.appConfig)}` : void 0;
36
+ if (setupBody.length > 0) lines.push(`const app = await createApp({ ${cfg ? `${cfg}, ` : ""}setup: async (app) => {`, ...setupBody, "} });", "");
37
+ else if (cfg) lines.push(`const app = await createApp({ ${cfg} });`, "");
35
38
  else lines.push("const app = await createApp();", "");
36
39
  lines.push(...[...imports.keys()].map((name) => `app.container.bind(${name});`), "");
37
40
  lines.push("app.container.flushQueue();", "");
@@ -49,13 +52,61 @@ function generateServerEntry(ctx) {
49
52
  */
50
53
  function writeServerEntry(ctx) {
51
54
  mkdirSync(dirname(ctx.serverEntry), { recursive: true });
52
- writeFileSync(ctx.serverEntry, generateServerEntry(ctx), "utf8");
55
+ writeFileAtomic(ctx.serverEntry, generateServerEntry(ctx));
56
+ writeDiscoveryManifest(ctx);
57
+ }
58
+ /**
59
+ * Writes a file through a temporary sibling that is renamed into place, so a
60
+ * reader never observes it truncated. The worker can re-import the entry while a
61
+ * rescan is rewriting it, when files are edited in quick succession.
62
+ *
63
+ * @param path - The destination path.
64
+ * @param content - The file content.
65
+ */
66
+ function writeFileAtomic(path, content) {
67
+ const tmp = `${path}.${process.pid}.tmp`;
68
+ writeFileSync(tmp, content, "utf8");
69
+ renameSync(tmp, path);
70
+ }
71
+ /**
72
+ * Writes what the scanner found as JSON, next to the generated server entry.
73
+ *
74
+ * The server entry is executable code keyed only by class name: it discards the
75
+ * file each class came from, the routes read out of the source and the HTTP
76
+ * methods. That information exists at build time and is exactly what a runtime
77
+ * inspector cannot recover, so it is persisted here and picked up by core's
78
+ * `discovery` introspection section.
79
+ *
80
+ * @param ctx - The plugin context holding the discovery result.
81
+ */
82
+ function writeDiscoveryManifest(ctx) {
83
+ const manifest = {
84
+ root: ctx.root,
85
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
86
+ controllers: ctx.controllers.map((entry) => ({
87
+ name: entry.importClassName,
88
+ path: entry.path
89
+ })),
90
+ services: ctx.services.map((entry) => ({
91
+ name: entry.importClassName,
92
+ path: entry.path
93
+ })),
94
+ middlewares: (ctx.middlewares ?? []).map((entry) => ({
95
+ name: entry.importClassName,
96
+ path: entry.path
97
+ })),
98
+ routes: ctx.routes.map((route) => ({
99
+ method: route.method,
100
+ path: route.route,
101
+ controller: route.importClassName,
102
+ file: route.path
103
+ }))
104
+ };
105
+ writeFileAtomic(join(dirname(ctx.serverEntry), "discovery.json"), `${JSON.stringify(manifest, null, 2)}\n`);
53
106
  }
54
107
  //#endregion
55
108
  //#region src/context.ts
56
109
  const DEFAULT_SCAN_DIRS = ["src"];
57
- /** Location of the generated server entry, kept inside node_modules so it stays out of source control and file watchers. */
58
- const SERVER_ENTRY_REL = "node_modules/.vercube/server-entry.mjs";
59
110
  /** Build output directory (relative to root) for the frontend client, served by the built server in production. */
60
111
  const CLIENT_OUT_DIR = "dist/public";
61
112
  /**
@@ -71,12 +122,13 @@ function createContext(pluginConfig) {
71
122
  pluginConfig,
72
123
  root,
73
124
  scanDirs: [],
74
- serverEntry: resolve(root, SERVER_ENTRY_REL),
125
+ serverEntry: serverEntryPath(root),
75
126
  dev: true,
76
127
  hasClient: false,
77
128
  controllers: [],
78
129
  routes: [],
79
- services: []
130
+ services: [],
131
+ middlewares: []
80
132
  };
81
133
  }
82
134
  /**
@@ -89,7 +141,7 @@ function createContext(pluginConfig) {
89
141
  async function setupContext(ctx, options) {
90
142
  ctx.root = ctx.pluginConfig.rootDir ? resolveFrom(options.root, ctx.pluginConfig.rootDir) : options.root;
91
143
  ctx.dev = options.dev;
92
- ctx.serverEntry = resolve(ctx.root, SERVER_ENTRY_REL);
144
+ ctx.serverEntry = serverEntryPath(ctx.root);
93
145
  ctx.hasClient = existsSync(resolve(ctx.root, "index.html"));
94
146
  ctx.scanDirs = (ctx.pluginConfig.scanDirs ?? DEFAULT_SCAN_DIRS).map((dir) => resolveFrom(ctx.root, dir));
95
147
  ctx.setupFile = ctx.pluginConfig.setupFile ? resolveFrom(ctx.root, ctx.pluginConfig.setupFile) : void 0;
@@ -102,13 +154,31 @@ async function setupContext(ctx, options) {
102
154
  * @param ctx - The plugin context to refresh.
103
155
  */
104
156
  async function scanProject(ctx) {
105
- const { controllers, routes, services } = await scanSource({ dirs: ctx.scanDirs });
157
+ const { controllers, routes, services, middlewares } = await scanSource({ dirs: ctx.scanDirs });
106
158
  ctx.controllers = controllers;
107
159
  ctx.routes = routes;
108
160
  ctx.services = services;
161
+ ctx.middlewares = middlewares;
109
162
  writeServerEntry(ctx);
110
163
  }
111
164
  /**
165
+ * Location of the generated server entry, kept inside node_modules so it stays
166
+ * out of source control and file watchers.
167
+ *
168
+ * The file name carries a hash of the project root. The entry imports discovered
169
+ * classes by absolute path, so two dev servers reaching one checkout through
170
+ * different paths (the host and a container with the project mounted at
171
+ * `/var/www`) would otherwise overwrite each other's entry, and whichever reloads
172
+ * next would import paths that do not exist on its side.
173
+ *
174
+ * @param root - The absolute project root.
175
+ * @returns The absolute path of the server entry for that root.
176
+ */
177
+ function serverEntryPath(root) {
178
+ const hash = createHash("sha256").update(root).digest("hex").slice(0, 8);
179
+ return resolve(root, `node_modules/.vercube/server-entry.${hash}.mjs`);
180
+ }
181
+ /**
112
182
  * Resolves `target` against `base` when it is relative, leaving absolute paths untouched.
113
183
  */
114
184
  function resolveFrom(base, target) {
@@ -1,4 +1,4 @@
1
- import { r as scanProject } from "./context-sG04QQCS.mjs";
1
+ import { r as scanProject } from "./context-D4q0XCl9.mjs";
2
2
  import { t as VERCUBE_ENV } from "./types-Cpd32Lkt.mjs";
3
3
  import { createViteHotChannel } from "env-runner/vite";
4
4
  import { watch } from "chokidar";