@vercube/vite 1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/context-CfzvWZrR.mjs +117 -0
- package/dist/dev-Bl0akm-q.mjs +157 -0
- package/dist/index.d.mts +90 -0
- package/dist/index.mjs +190 -0
- package/dist/prod-Pc0_j1Kz.mjs +28 -0
- package/dist/runtime/dev-worker.d.mts +26 -0
- package/dist/runtime/dev-worker.mjs +130 -0
- package/dist/types-Cpd32Lkt.mjs +7 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present - Vercube
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @vercube/vite
|
|
2
|
+
|
|
3
|
+
Run a [Vercube](https://github.com/vercube/vercube) server as a [Vite](https://vite.dev) plugin, built on Vite's [Environment API](https://vite.dev/guide/api-environment.html). Your decorator-based controllers run inside an isolated dev worker with fast server-side HMR, and `vite build` produces a runnable server bundle, all with zero-config controller discovery.
|
|
4
|
+
|
|
5
|
+
> **Experimental.** This integration is in an early stage and its API may change between releases.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -D @vercube/vite vite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`experimentalDecorators` must be enabled in your `tsconfig.json`:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"compilerOptions": {
|
|
18
|
+
"experimentalDecorators": true,
|
|
19
|
+
"emitDecoratorMetadata": false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Add the plugin to your `vite.config.ts`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { vercube } from '@vercube/vite';
|
|
30
|
+
import { defineConfig } from 'vite';
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
plugins: [vercube()],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Place a `@Controller` anywhere under `src/`. It is discovered automatically, with no manual registration:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/Controllers/HelloController.ts
|
|
41
|
+
import { Controller, Get, Param } from '@vercube/core';
|
|
42
|
+
|
|
43
|
+
@Controller('/api/hello')
|
|
44
|
+
export default class HelloController {
|
|
45
|
+
@Get('/')
|
|
46
|
+
index() {
|
|
47
|
+
return { message: 'Hello from Vercube + Vite!' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Get('/:name')
|
|
51
|
+
greet(@Param('name') name: string) {
|
|
52
|
+
return { message: `Hello, ${name}!` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
vite # dev server with HMR
|
|
61
|
+
vite build # bundles to dist/index.mjs
|
|
62
|
+
node dist/index.mjs # runs the built server
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
- A dedicated Vite **environment** (`vercube`) runs the server. In dev it executes inside an isolated [`env-runner`](https://www.npmjs.com/package/env-runner) worker via Vite's `ModuleRunner`.
|
|
68
|
+
- Controllers and `@Injectable` services are discovered by scanning your source tree (AST parsing via [`@vercube/scan`](../scan)) and assembled into a generated entry that creates the app, binds the classes, and exports `fetch`.
|
|
69
|
+
- Vercube only claims **the routes you actually define**. Requests matching a discovered route are handed to the worker, everything else falls through to Vite. So the plugin sits next to a frontend (Vue, React, plain JS) that Vite serves on the same server. See [`examples/vite`](../../examples/vite) for a Vite + Vue app calling a Vercube API.
|
|
70
|
+
- Editing or adding controllers triggers a worker reload, so changes are live without a manual restart.
|
|
71
|
+
|
|
72
|
+
`vite build` builds your frontend (if any) and then bundles the Vercube server to `dist/index.mjs`.
|
|
73
|
+
|
|
74
|
+
## Options
|
|
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
|
+
|
|
83
|
+
## Setup file
|
|
84
|
+
|
|
85
|
+
Auto-discovery binds `@Controller` and `@Injectable` classes. Anything it can't infer, such as registering plugins, mounting storage, or binding an interface to an implementation, goes in a `setupFile`. It runs as `createApp`'s `setup` hook, **before** the app initializes, so it is early enough to register plugins:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { vercube } from '@vercube/vite';
|
|
89
|
+
import { defineConfig } from 'vite';
|
|
90
|
+
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
plugins: [vercube({ setupFile: './src/Boot/Setup.ts' })],
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// src/Boot/Setup.ts
|
|
98
|
+
import type { App } from '@vercube/core';
|
|
99
|
+
import { StorageManager } from '@vercube/storage';
|
|
100
|
+
import { MemoryStorage } from '@vercube/storage/drivers/MemoryStorage';
|
|
101
|
+
|
|
102
|
+
export default async function setup(app: App) {
|
|
103
|
+
app.container.bind(StorageManager);
|
|
104
|
+
app.container.get(StorageManager).mount({ storage: MemoryStorage });
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## WebSockets
|
|
109
|
+
|
|
110
|
+
The `@vercube/ws` plugin works in both the dev server and the production build. Register it from your `setupFile` (`app.addPlugin(WebsocketPlugin)`) and define WebSocket controllers as usual (`@Controller` + `@Namespace`). They are auto-discovered. The dev server forwards upgrade handshakes into the worker, and the production server handles them natively through `srvx`.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { dirname, isAbsolute, resolve } from "pathe";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { scanSource } from "@vercube/scan";
|
|
4
|
+
//#region src/entry.ts
|
|
5
|
+
/**
|
|
6
|
+
* Generates the source of the server entry module.
|
|
7
|
+
*
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* The entry is written to a real file rather than served as a `\0`-virtual
|
|
15
|
+
* module: Vite resolves bare imports from virtual modules inconsistently with
|
|
16
|
+
* those from project files, which would load framework packages twice and break
|
|
17
|
+
* Vercube's class-reference DI tokens. A real on-disk module resolves its
|
|
18
|
+
* imports the same way the controllers do, keeping a single instance of each.
|
|
19
|
+
*
|
|
20
|
+
* @param ctx - The plugin context holding discovered controllers/services.
|
|
21
|
+
* @returns The generated module source.
|
|
22
|
+
*/
|
|
23
|
+
function generateServerEntry(ctx) {
|
|
24
|
+
const discovered = [...ctx.controllers, ...ctx.services];
|
|
25
|
+
const imports = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const entry of discovered) if (!imports.has(entry.importClassName)) imports.set(entry.importClassName, entry.import);
|
|
27
|
+
const lines = [ctx.hasClient ? `import { createApp, HttpServer, serveStaticFiles } from '@vercube/core';` : `import { createApp } from '@vercube/core';`];
|
|
28
|
+
if (ctx.hasClient) lines.push(`import { fileURLToPath } from 'node:url';`);
|
|
29
|
+
if (ctx.setupFile) lines.push(`import __vercubeSetup__ from ${JSON.stringify(ctx.setupFile)};`);
|
|
30
|
+
lines.push(...imports.values(), "");
|
|
31
|
+
const setupBody = [];
|
|
32
|
+
if (ctx.setupFile) setupBody.push(" await __vercubeSetup__(app);");
|
|
33
|
+
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
|
+
else lines.push("const app = await createApp();", "");
|
|
36
|
+
lines.push(...[...imports.keys()].map((name) => `app.container.bind(${name});`), "");
|
|
37
|
+
lines.push("app.container.flushQueue();", "");
|
|
38
|
+
lines.push("if (import.meta.main) {", " await app.listen();", "}", "");
|
|
39
|
+
lines.push("export const fetch = app.fetch.bind(app);", "");
|
|
40
|
+
lines.push("export const handleUpgrade = (req, socket, head) => globalThis.__vercube_ws_upgrade__?.(req, socket, head);", "");
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Writes the generated server entry to {@link VercubePluginContext.serverEntry},
|
|
45
|
+
* creating the parent directory if needed.
|
|
46
|
+
*
|
|
47
|
+
* @param ctx - The plugin context.
|
|
48
|
+
*/
|
|
49
|
+
function writeServerEntry(ctx) {
|
|
50
|
+
mkdirSync(dirname(ctx.serverEntry), { recursive: true });
|
|
51
|
+
writeFileSync(ctx.serverEntry, generateServerEntry(ctx), "utf8");
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/context.ts
|
|
55
|
+
const DEFAULT_SCAN_DIRS = ["src"];
|
|
56
|
+
/** Location of the generated server entry, kept inside node_modules so it stays out of source control and file watchers. */
|
|
57
|
+
const SERVER_ENTRY_REL = "node_modules/.vercube/server-entry.mjs";
|
|
58
|
+
/** Build output directory (relative to root) for the frontend client, served by the built server in production. */
|
|
59
|
+
const CLIENT_OUT_DIR = "dist/public";
|
|
60
|
+
/**
|
|
61
|
+
* Creates the initial plugin context from user configuration. Directories are
|
|
62
|
+
* left unresolved until {@link setupContext}, which has access to Vite's root.
|
|
63
|
+
*
|
|
64
|
+
* @param pluginConfig - The user-provided plugin configuration.
|
|
65
|
+
* @returns A fresh, uninitialized plugin context.
|
|
66
|
+
*/
|
|
67
|
+
function createContext(pluginConfig) {
|
|
68
|
+
const root = pluginConfig.rootDir ?? process.cwd();
|
|
69
|
+
return {
|
|
70
|
+
pluginConfig,
|
|
71
|
+
root,
|
|
72
|
+
scanDirs: [],
|
|
73
|
+
serverEntry: resolve(root, SERVER_ENTRY_REL),
|
|
74
|
+
dev: true,
|
|
75
|
+
hasClient: false,
|
|
76
|
+
controllers: [],
|
|
77
|
+
routes: [],
|
|
78
|
+
services: []
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolves the project root, scan directories and setup file against Vite's
|
|
83
|
+
* configuration, runs an initial source scan, and writes the server entry.
|
|
84
|
+
*
|
|
85
|
+
* @param ctx - The plugin context to populate.
|
|
86
|
+
* @param options - Resolution inputs derived from Vite's config.
|
|
87
|
+
*/
|
|
88
|
+
async function setupContext(ctx, options) {
|
|
89
|
+
ctx.root = ctx.pluginConfig.rootDir ? resolveFrom(options.root, ctx.pluginConfig.rootDir) : options.root;
|
|
90
|
+
ctx.dev = options.dev;
|
|
91
|
+
ctx.serverEntry = resolve(ctx.root, SERVER_ENTRY_REL);
|
|
92
|
+
ctx.hasClient = existsSync(resolve(ctx.root, "index.html"));
|
|
93
|
+
ctx.scanDirs = (ctx.pluginConfig.scanDirs ?? DEFAULT_SCAN_DIRS).map((dir) => resolveFrom(ctx.root, dir));
|
|
94
|
+
ctx.setupFile = ctx.pluginConfig.setupFile ? resolveFrom(ctx.root, ctx.pluginConfig.setupFile) : void 0;
|
|
95
|
+
await scanProject(ctx);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Re-scans the project source for controllers and services, then rewrites the
|
|
99
|
+
* server entry. Called on startup and whenever watched scan directories change.
|
|
100
|
+
*
|
|
101
|
+
* @param ctx - The plugin context to refresh.
|
|
102
|
+
*/
|
|
103
|
+
async function scanProject(ctx) {
|
|
104
|
+
const { controllers, routes, services } = await scanSource({ dirs: ctx.scanDirs });
|
|
105
|
+
ctx.controllers = controllers;
|
|
106
|
+
ctx.routes = routes;
|
|
107
|
+
ctx.services = services;
|
|
108
|
+
writeServerEntry(ctx);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves `target` against `base` when it is relative, leaving absolute paths untouched.
|
|
112
|
+
*/
|
|
113
|
+
function resolveFrom(base, target) {
|
|
114
|
+
return isAbsolute(target) ? target : resolve(base, target);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { setupContext as i, createContext as n, scanProject as r, CLIENT_OUT_DIR as t };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { r as scanProject } from "./context-CfzvWZrR.mjs";
|
|
2
|
+
import { t as VERCUBE_ENV } from "./types-Cpd32Lkt.mjs";
|
|
3
|
+
import { createViteHotChannel } from "env-runner/vite";
|
|
4
|
+
import { watch } from "chokidar";
|
|
5
|
+
import { addRoute, createRouter, findRoute } from "rou3";
|
|
6
|
+
import { NodeRequest, sendNodeResponse } from "srvx/node";
|
|
7
|
+
import { DevEnvironment } from "vite";
|
|
8
|
+
//#region src/dev.ts
|
|
9
|
+
/** Chokidar events that imply a controller/service was added or removed. */
|
|
10
|
+
const RELOAD_EVENTS = /* @__PURE__ */ new Set([
|
|
11
|
+
"add",
|
|
12
|
+
"addDir",
|
|
13
|
+
"unlink",
|
|
14
|
+
"unlinkDir"
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Creates a {@link FetchableDevEnvironment} wired to the env-runner worker via a
|
|
18
|
+
* Vite hot channel.
|
|
19
|
+
*
|
|
20
|
+
* @param name - The environment name.
|
|
21
|
+
* @param config - The resolved Vite config.
|
|
22
|
+
* @param devServer - The env-runner manager driving the worker.
|
|
23
|
+
* @param entry - The dev entry the worker should import for this environment.
|
|
24
|
+
* @returns The fetchable dev environment.
|
|
25
|
+
*/
|
|
26
|
+
function createFetchableDevEnvironment(name, config, devServer, entry) {
|
|
27
|
+
return new FetchableDevEnvironment(name, config, {
|
|
28
|
+
hot: true,
|
|
29
|
+
transport: createViteHotChannel(devServer, name)
|
|
30
|
+
}, devServer, entry);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A Vite `DevEnvironment` that can dispatch HTTP requests into the server code
|
|
34
|
+
* running inside the env-runner worker. On `init` it tells the worker which
|
|
35
|
+
* entry module backs this environment.
|
|
36
|
+
*/
|
|
37
|
+
var FetchableDevEnvironment = class extends DevEnvironment {
|
|
38
|
+
devServer;
|
|
39
|
+
#entry;
|
|
40
|
+
constructor(name, config, context, devServer, entry) {
|
|
41
|
+
super(name, config, context);
|
|
42
|
+
this.devServer = devServer;
|
|
43
|
+
this.#entry = entry;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Dispatches a web `Request` into the worker and resolves with its `Response`.
|
|
47
|
+
*
|
|
48
|
+
* @param request - The incoming request.
|
|
49
|
+
* @returns The server response.
|
|
50
|
+
*/
|
|
51
|
+
async dispatchFetch(request) {
|
|
52
|
+
return this.devServer.fetch(request);
|
|
53
|
+
}
|
|
54
|
+
async init(...args) {
|
|
55
|
+
await this.devServer.init?.();
|
|
56
|
+
await super.init(...args);
|
|
57
|
+
this.devServer.sendMessage({
|
|
58
|
+
type: "custom",
|
|
59
|
+
event: "vercube:vite-env",
|
|
60
|
+
data: {
|
|
61
|
+
name: this.name,
|
|
62
|
+
entry: this.#entry
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Wires the Vercube environment into a Vite dev server: forwards HTTP requests
|
|
69
|
+
* into the worker and watches the scan directories so added/removed controllers
|
|
70
|
+
* are re-discovered without a manual restart.
|
|
71
|
+
*
|
|
72
|
+
* The request middleware is installed eagerly (before Vite's own middlewares)
|
|
73
|
+
* so matched API routes are claimed ahead of Vite's SPA/HTML fallback; every
|
|
74
|
+
* other request falls through to Vite.
|
|
75
|
+
*
|
|
76
|
+
* @param ctx - The shared plugin context.
|
|
77
|
+
* @param server - The Vite dev server.
|
|
78
|
+
*/
|
|
79
|
+
async function configureViteDevServer(ctx, server) {
|
|
80
|
+
const env = server.environments[VERCUBE_ENV];
|
|
81
|
+
let matchesRoute = buildRouteMatcher(ctx.routes);
|
|
82
|
+
const reload = debounce(async () => {
|
|
83
|
+
await scanProject(ctx);
|
|
84
|
+
matchesRoute = buildRouteMatcher(ctx.routes);
|
|
85
|
+
env.moduleGraph.invalidateAll();
|
|
86
|
+
env.hot.send({ type: "full-reload" });
|
|
87
|
+
});
|
|
88
|
+
const watcher = watch(ctx.scanDirs, { ignoreInitial: true }).on("all", (event) => {
|
|
89
|
+
if (RELOAD_EVENTS.has(event)) reload();
|
|
90
|
+
});
|
|
91
|
+
server.httpServer?.once("close", () => {
|
|
92
|
+
watcher.close();
|
|
93
|
+
ctx._envRunner?.close();
|
|
94
|
+
});
|
|
95
|
+
server.httpServer?.on("upgrade", (req, socket, head) => {
|
|
96
|
+
const protocol = req.headers["sec-websocket-protocol"];
|
|
97
|
+
if (typeof protocol === "string" && protocol.startsWith("vite-")) return;
|
|
98
|
+
ctx._envRunner?.upgrade?.({ node: {
|
|
99
|
+
req,
|
|
100
|
+
socket,
|
|
101
|
+
head
|
|
102
|
+
} });
|
|
103
|
+
});
|
|
104
|
+
const middleware = async (nodeReq, nodeRes, next) => {
|
|
105
|
+
if (!nodeReq.url || !matchesRoute(nodeReq.url)) return next();
|
|
106
|
+
try {
|
|
107
|
+
const request = new NodeRequest({
|
|
108
|
+
req: nodeReq,
|
|
109
|
+
res: nodeRes
|
|
110
|
+
});
|
|
111
|
+
const response = await env.dispatchFetch(request);
|
|
112
|
+
if (nodeRes.writableEnded || nodeRes.headersSent) return;
|
|
113
|
+
await sendNodeResponse(nodeRes, response);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
next(error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
server.middlewares.use(middleware);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Builds a predicate that tells whether a request URL matches any discovered
|
|
122
|
+
* Vercube route, using the same `rou3` router the framework uses at runtime so
|
|
123
|
+
* params (`:id`) and wildcards (`**`) match identically. Matching is
|
|
124
|
+
* method-agnostic: every method (and CORS preflight) for a defined path is
|
|
125
|
+
* Vercube's, so it can answer `405`/`OPTIONS` itself.
|
|
126
|
+
*
|
|
127
|
+
* @param routes - The discovered HTTP routes.
|
|
128
|
+
* @returns A predicate `(url) => boolean`.
|
|
129
|
+
*/
|
|
130
|
+
function buildRouteMatcher(routes) {
|
|
131
|
+
const router = createRouter();
|
|
132
|
+
const seen = /* @__PURE__ */ new Set();
|
|
133
|
+
for (const { route } of routes) {
|
|
134
|
+
if (seen.has(route)) continue;
|
|
135
|
+
seen.add(route);
|
|
136
|
+
addRoute(router, "ANY", route, true);
|
|
137
|
+
}
|
|
138
|
+
return (url) => {
|
|
139
|
+
return findRoute(router, "ANY", url.split(/[?#]/, 1)[0].replace(/\/+$/, "") || "/") !== void 0;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Minimal trailing debounce: coalesces bursts of file events into a single
|
|
144
|
+
* reload on the next tick.
|
|
145
|
+
*/
|
|
146
|
+
function debounce(fn, delay = 50) {
|
|
147
|
+
let timer;
|
|
148
|
+
return () => {
|
|
149
|
+
if (timer) clearTimeout(timer);
|
|
150
|
+
timer = setTimeout(() => {
|
|
151
|
+
timer = void 0;
|
|
152
|
+
fn();
|
|
153
|
+
}, delay);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { configureViteDevServer, createFetchableDevEnvironment };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { RouteInfo, ServiceInfo } from "@vercube/scan";
|
|
2
|
+
import { RunnerManager } from "env-runner";
|
|
3
|
+
import { Plugin } from "vite";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The Vite environment name under which the Vercube server runs.
|
|
8
|
+
*/
|
|
9
|
+
declare const VERCUBE_ENV = "vercube";
|
|
10
|
+
/**
|
|
11
|
+
* User-facing configuration for the Vercube Vite plugin.
|
|
12
|
+
*/
|
|
13
|
+
interface VercubePluginConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Project root. Defaults to Vite's resolved `root`.
|
|
16
|
+
*/
|
|
17
|
+
rootDir?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Directories (relative to `rootDir`) whose file trees are scanned for
|
|
20
|
+
* controllers, `@Injectable` services and middleware. Defaults to `['src']`.
|
|
21
|
+
*/
|
|
22
|
+
scanDirs?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Path (relative to `rootDir`) to a module whose default export is
|
|
25
|
+
* `(app: App) => void | Promise<void>`. Runs after auto-discovered classes are
|
|
26
|
+
* bound but before the container queue is flushed — use it to mount storage,
|
|
27
|
+
* bind tokens, configure the logger, or register plugins that auto-discovery
|
|
28
|
+
* cannot infer.
|
|
29
|
+
*/
|
|
30
|
+
setupFile?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The runner used to execute server code in the dev environment.
|
|
33
|
+
* Defaults to `node-worker`.
|
|
34
|
+
*/
|
|
35
|
+
runner?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Internal plugin context shared across the plugin's sub-plugins and hooks.
|
|
39
|
+
*/
|
|
40
|
+
interface VercubePluginContext {
|
|
41
|
+
/** The resolved user configuration. */
|
|
42
|
+
pluginConfig: VercubePluginConfig;
|
|
43
|
+
/** Absolute project root. */
|
|
44
|
+
root: string;
|
|
45
|
+
/** Absolute directories scanned for decorated classes. */
|
|
46
|
+
scanDirs: string[];
|
|
47
|
+
/** Absolute path to the setup file, if configured. */
|
|
48
|
+
setupFile?: string;
|
|
49
|
+
/** Absolute path of the generated server entry module loaded by the worker. */
|
|
50
|
+
serverEntry: string;
|
|
51
|
+
/** Whether the plugin is running in dev (serve) mode. */
|
|
52
|
+
dev: boolean;
|
|
53
|
+
/** Whether the project has a frontend (`index.html`) the built server should serve. */
|
|
54
|
+
hasClient: boolean;
|
|
55
|
+
/** Discovered `@Controller` classes (every controller, including WebSocket-only ones). */
|
|
56
|
+
controllers: ServiceInfo[];
|
|
57
|
+
/** Discovered HTTP routes (method + path), used to decide which requests Vercube handles. */
|
|
58
|
+
routes: RouteInfo[];
|
|
59
|
+
/** Discovered injectable services (deduplicated against controllers). */
|
|
60
|
+
services: ServiceInfo[];
|
|
61
|
+
/** The env-runner manager driving the dev worker. */
|
|
62
|
+
_envRunner?: RunnerManager;
|
|
63
|
+
/** Guards concurrent env-runner initialization. */
|
|
64
|
+
_initPromise?: Promise<RunnerManager>;
|
|
65
|
+
/** Maps each Vite environment name to the entry the worker should load. */
|
|
66
|
+
_viteEnvs?: Map<string, string>;
|
|
67
|
+
}
|
|
68
|
+
declare module 'vite' {
|
|
69
|
+
interface UserConfig {
|
|
70
|
+
/** Vercube plugin configuration, mergeable from `vite.config.ts`. */
|
|
71
|
+
vercube?: VercubePluginConfig;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/plugin.d.ts
|
|
76
|
+
/**
|
|
77
|
+
* The Vercube Vite plugin.
|
|
78
|
+
*
|
|
79
|
+
* Runs a Vercube server inside a dedicated Vite environment via the Environment
|
|
80
|
+
* API: controllers and services are auto-discovered from the project source,
|
|
81
|
+
* assembled into a generated virtual entry, and executed in an isolated
|
|
82
|
+
* env-runner worker with HMR. HTTP requests hitting the Vite dev server are
|
|
83
|
+
* forwarded into that worker.
|
|
84
|
+
*
|
|
85
|
+
* @param pluginConfig - Optional plugin configuration.
|
|
86
|
+
* @returns The array of Vite plugins composing the integration.
|
|
87
|
+
*/
|
|
88
|
+
declare function vercube(pluginConfig?: VercubePluginConfig): Plugin[];
|
|
89
|
+
//#endregion
|
|
90
|
+
export { VERCUBE_ENV, type VercubePluginConfig, type VercubePluginContext, vercube };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { i as setupContext, n as createContext, t as CLIENT_OUT_DIR } from "./context-CfzvWZrR.mjs";
|
|
2
|
+
import { t as VERCUBE_ENV } from "./types-Cpd32Lkt.mjs";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { dirname, isAbsolute, resolve } from "pathe";
|
|
5
|
+
import { RunnerManager, loadRunner } from "env-runner";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
/**
|
|
8
|
+
* Absolute path to the worker bootstrap loaded by env-runner.
|
|
9
|
+
*/
|
|
10
|
+
const devWorker = resolve(resolve(dirname(fileURLToPath(import.meta.url)), "runtime"), "dev-worker.mjs");
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/env.ts
|
|
13
|
+
/**
|
|
14
|
+
* Dev-only `resolve.noExternal` patterns. Only `@vercube/*` must stay in Vite's
|
|
15
|
+
* module graph so class-reference DI tokens are not duplicated; other deps (e.g.
|
|
16
|
+
* CJS-only `dotenv`) are left external and loaded via native `import()`.
|
|
17
|
+
*/
|
|
18
|
+
const DEV_NO_EXTERNAL = [/^@vercube\//];
|
|
19
|
+
/**
|
|
20
|
+
* The custom message event used to tell the worker which entry an environment loads.
|
|
21
|
+
*/
|
|
22
|
+
const VITE_ENV_EVENT = "vercube:vite-env";
|
|
23
|
+
/**
|
|
24
|
+
* Builds the Vite environment options for the Vercube server environment.
|
|
25
|
+
*
|
|
26
|
+
* In dev, `createEnvironment` returns a {@link FetchableDevEnvironment} backed by
|
|
27
|
+
* the env-runner worker. In build, the environment bundles the virtual server
|
|
28
|
+
* entry (wired up in the production milestone).
|
|
29
|
+
*
|
|
30
|
+
* @param ctx - The shared plugin context.
|
|
31
|
+
* @returns The environment options for the `vercube` environment.
|
|
32
|
+
*/
|
|
33
|
+
function createVercubeEnvironment(ctx) {
|
|
34
|
+
return {
|
|
35
|
+
consumer: "server",
|
|
36
|
+
resolve: ctx.dev ? { noExternal: DEV_NO_EXTERNAL } : {},
|
|
37
|
+
build: {
|
|
38
|
+
outDir: resolve(ctx.root, "dist"),
|
|
39
|
+
emptyOutDir: false,
|
|
40
|
+
copyPublicDir: false,
|
|
41
|
+
rollupOptions: {
|
|
42
|
+
input: { index: ctx.serverEntry },
|
|
43
|
+
external: isBareSpecifier,
|
|
44
|
+
output: {
|
|
45
|
+
entryFileNames: "[name].mjs",
|
|
46
|
+
chunkFileNames: "[name]-[hash].mjs",
|
|
47
|
+
format: "es"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
dev: { createEnvironment: async (envName, envConfig) => {
|
|
52
|
+
const { createFetchableDevEnvironment } = await import("./dev-Bl0akm-q.mjs");
|
|
53
|
+
const env = createFetchableDevEnvironment(envName, envConfig, getEnvRunner(ctx), ctx.serverEntry);
|
|
54
|
+
(ctx._viteEnvs ??= /* @__PURE__ */ new Map()).set(envName, ctx.serverEntry);
|
|
55
|
+
return env;
|
|
56
|
+
} }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Initializes the env-runner manager that drives the dev worker. Idempotent and
|
|
61
|
+
* concurrency-safe: repeated calls return the same manager. On unexpected worker
|
|
62
|
+
* exit the runner is reloaded up to three times before giving up.
|
|
63
|
+
*
|
|
64
|
+
* @param ctx - The shared plugin context.
|
|
65
|
+
* @returns The initialized runner manager.
|
|
66
|
+
*/
|
|
67
|
+
async function initEnvRunner(ctx) {
|
|
68
|
+
if (ctx._envRunner) return ctx._envRunner;
|
|
69
|
+
if (!ctx._initPromise) ctx._initPromise = (async () => {
|
|
70
|
+
const manager = new RunnerManager();
|
|
71
|
+
let retries = 0;
|
|
72
|
+
manager.onClose((_runner, cause) => {
|
|
73
|
+
if (retries++ < 3) loadRunnerInto(ctx, manager);
|
|
74
|
+
else console.error("[vercube] env runner failed after 3 retries.", cause ? `Last cause: ${cause}` : "");
|
|
75
|
+
});
|
|
76
|
+
manager.onReady(() => {
|
|
77
|
+
retries = 0;
|
|
78
|
+
if (ctx._viteEnvs) for (const [name, entry] of ctx._viteEnvs) manager.sendMessage({
|
|
79
|
+
type: "custom",
|
|
80
|
+
event: VITE_ENV_EVENT,
|
|
81
|
+
data: {
|
|
82
|
+
name,
|
|
83
|
+
entry
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
await loadRunnerInto(ctx, manager);
|
|
88
|
+
ctx._envRunner = manager;
|
|
89
|
+
return manager;
|
|
90
|
+
})().finally(() => {
|
|
91
|
+
ctx._initPromise = void 0;
|
|
92
|
+
});
|
|
93
|
+
return ctx._initPromise;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns the initialized runner manager, throwing if {@link initEnvRunner} has not run.
|
|
97
|
+
*
|
|
98
|
+
* @param ctx - The shared plugin context.
|
|
99
|
+
* @returns The runner manager.
|
|
100
|
+
*/
|
|
101
|
+
function getEnvRunner(ctx) {
|
|
102
|
+
if (!ctx._envRunner) throw new Error("[vercube] Env runner not initialized. Call initEnvRunner() first.");
|
|
103
|
+
return ctx._envRunner;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Returns true for bare module specifiers (npm/workspace packages), which the
|
|
107
|
+
* production build externalizes. Relative paths, absolute paths and virtual
|
|
108
|
+
* (`\0`-prefixed) ids are bundled.
|
|
109
|
+
*
|
|
110
|
+
* @param id - The import specifier to classify.
|
|
111
|
+
* @returns Whether the specifier is a bare dependency.
|
|
112
|
+
*/
|
|
113
|
+
function isBareSpecifier(id) {
|
|
114
|
+
if (id.startsWith("\0") || id.startsWith(".") || isAbsolute(id)) return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Loads (or reloads) the configured runner into the manager.
|
|
119
|
+
*/
|
|
120
|
+
async function loadRunnerInto(ctx, manager) {
|
|
121
|
+
const runner = await loadRunner(ctx.pluginConfig.runner || process.env.VERCUBE_DEV_RUNNER || "node-worker", {
|
|
122
|
+
name: VERCUBE_ENV,
|
|
123
|
+
data: { entry: devWorker }
|
|
124
|
+
});
|
|
125
|
+
await manager.reload(runner);
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/plugin.ts
|
|
129
|
+
/**
|
|
130
|
+
* The Vercube Vite plugin.
|
|
131
|
+
*
|
|
132
|
+
* Runs a Vercube server inside a dedicated Vite environment via the Environment
|
|
133
|
+
* API: controllers and services are auto-discovered from the project source,
|
|
134
|
+
* assembled into a generated virtual entry, and executed in an isolated
|
|
135
|
+
* env-runner worker with HMR. HTTP requests hitting the Vite dev server are
|
|
136
|
+
* forwarded into that worker.
|
|
137
|
+
*
|
|
138
|
+
* @param pluginConfig - Optional plugin configuration.
|
|
139
|
+
* @returns The array of Vite plugins composing the integration.
|
|
140
|
+
*/
|
|
141
|
+
function vercube(pluginConfig = {}) {
|
|
142
|
+
return [vercubeMain(createContext(pluginConfig))];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The primary sub-plugin: defines the Vercube environment, warms the dev worker,
|
|
146
|
+
* and forwards requests to it.
|
|
147
|
+
*/
|
|
148
|
+
function vercubeMain(ctx) {
|
|
149
|
+
let initialized = false;
|
|
150
|
+
return {
|
|
151
|
+
name: "vercube:main",
|
|
152
|
+
sharedDuringBuild: true,
|
|
153
|
+
async config(userConfig, configEnv) {
|
|
154
|
+
if (!initialized) {
|
|
155
|
+
initialized = true;
|
|
156
|
+
ctx.pluginConfig = defu(userConfig.vercube, ctx.pluginConfig);
|
|
157
|
+
await setupContext(ctx, {
|
|
158
|
+
root: userConfig.root ?? process.cwd(),
|
|
159
|
+
dev: configEnv.command === "serve"
|
|
160
|
+
});
|
|
161
|
+
if (ctx.dev) await initEnvRunner(ctx);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
builder: { sharedConfigBuild: true },
|
|
165
|
+
environments: {
|
|
166
|
+
client: { build: { outDir: resolve(ctx.root, CLIENT_OUT_DIR) } },
|
|
167
|
+
[VERCUBE_ENV]: createVercubeEnvironment(ctx)
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
buildApp: {
|
|
172
|
+
order: "post",
|
|
173
|
+
async handler(builder) {
|
|
174
|
+
const { buildEnvironments } = await import("./prod-Pc0_j1Kz.mjs");
|
|
175
|
+
await buildEnvironments(ctx, builder);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
configureServer: async (server) => {
|
|
179
|
+
const { configureViteDevServer } = await import("./dev-Bl0akm-q.mjs");
|
|
180
|
+
return configureViteDevServer(ctx, server);
|
|
181
|
+
},
|
|
182
|
+
hotUpdate() {
|
|
183
|
+
if (this.environment.name !== "vercube") return;
|
|
184
|
+
this.environment.moduleGraph.invalidateAll();
|
|
185
|
+
this.environment.hot.send({ type: "full-reload" });
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { VERCUBE_ENV, vercube };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as VERCUBE_ENV } from "./types-Cpd32Lkt.mjs";
|
|
2
|
+
import { resolve } from "pathe";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
//#region src/prod.ts
|
|
5
|
+
/**
|
|
6
|
+
* Production build orchestration for the `buildApp` hook.
|
|
7
|
+
*
|
|
8
|
+
* Builds every other configured environment first (for example a frontend
|
|
9
|
+
* client added by another Vite plugin), then the Vercube server environment:
|
|
10
|
+
* its generated entry is bundled to `dist/index.mjs`, exporting `fetch` and
|
|
11
|
+
* starting a listener when run directly (`node dist/index.mjs`).
|
|
12
|
+
*
|
|
13
|
+
* @param ctx - The shared plugin context.
|
|
14
|
+
* @param builder - Vite's builder, providing access to the configured environments.
|
|
15
|
+
*/
|
|
16
|
+
async function buildEnvironments(ctx, builder) {
|
|
17
|
+
const server = builder.environments[VERCUBE_ENV];
|
|
18
|
+
if (!server) throw new Error(`[vercube] Environment "${VERCUBE_ENV}" is not configured.`);
|
|
19
|
+
for (const [name, env] of Object.entries(builder.environments)) {
|
|
20
|
+
if (name === "vercube") continue;
|
|
21
|
+
const hasInput = !!env.config.build?.rollupOptions?.input;
|
|
22
|
+
const isClientWithHtml = env.config.consumer === "client" && existsSync(resolve(ctx.root, "index.html"));
|
|
23
|
+
if (hasInput || isClientWithHtml) await builder.build(env);
|
|
24
|
+
}
|
|
25
|
+
await builder.build(server);
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { buildEnvironments };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { UpgradeContext } from "env-runner";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/dev-worker.d.ts
|
|
4
|
+
interface IpcContext {
|
|
5
|
+
sendMessage: (data: unknown) => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* env-runner AppEntry: routes an incoming request to the named environment
|
|
9
|
+
* (defaulting to the Vercube server environment).
|
|
10
|
+
*/
|
|
11
|
+
declare function fetch(req: Request): Promise<Response>;
|
|
12
|
+
/**
|
|
13
|
+
* env-runner AppEntry: forwards a WebSocket upgrade to the Vercube environment.
|
|
14
|
+
*/
|
|
15
|
+
declare function upgrade(context: UpgradeContext): void;
|
|
16
|
+
/**
|
|
17
|
+
* env-runner IPC hooks: opens the message channel, registers environments, and
|
|
18
|
+
* relays module-runner transport messages.
|
|
19
|
+
*/
|
|
20
|
+
declare const ipc: {
|
|
21
|
+
onOpen(ctx: IpcContext): void;
|
|
22
|
+
onMessage(message: any): void;
|
|
23
|
+
onClose(): void;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { fetch, ipc, upgrade };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createViteTransport } from "env-runner/vite";
|
|
2
|
+
import { ESModulesEvaluator, ModuleRunner } from "vite/module-runner";
|
|
3
|
+
//#region src/runtime/dev-worker.ts
|
|
4
|
+
let sendMessage;
|
|
5
|
+
const messageListeners = /* @__PURE__ */ new Set();
|
|
6
|
+
const envs = Object.create(null);
|
|
7
|
+
/**
|
|
8
|
+
* Owns the module runner and loaded entry for a single Vite environment.
|
|
9
|
+
*/
|
|
10
|
+
var ViteEnvRunner = class {
|
|
11
|
+
name;
|
|
12
|
+
runner;
|
|
13
|
+
#entryPath;
|
|
14
|
+
#entry;
|
|
15
|
+
#entryError;
|
|
16
|
+
constructor({ name, entry }) {
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.#entryPath = entry;
|
|
19
|
+
const onMessage = (listener) => void messageListeners.add(listener);
|
|
20
|
+
const transport = createViteTransport((data) => sendMessage?.(data), onMessage, name);
|
|
21
|
+
const debug = typeof process !== "undefined" && process.env?.VERCUBE_DEBUG ? console.debug : void 0;
|
|
22
|
+
this.runner = new ModuleRunner({ transport }, new ESModulesEvaluator(), debug);
|
|
23
|
+
this.reload();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* (Re)imports the entry module, capturing any load-time error to surface on fetch.
|
|
27
|
+
*/
|
|
28
|
+
async reload() {
|
|
29
|
+
try {
|
|
30
|
+
this.#entry = await this.runner.import(this.#entryPath);
|
|
31
|
+
this.#entryError = void 0;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(error);
|
|
34
|
+
this.#entryError = error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Dispatches a request into the entry's `fetch` handler, waiting briefly for a
|
|
39
|
+
* still-loading entry.
|
|
40
|
+
*/
|
|
41
|
+
async fetch(req) {
|
|
42
|
+
for (let i = 0; i < 5 && !(this.#entry || this.#entryError); i++) await new Promise((r) => setTimeout(r, 100 * 2 ** i));
|
|
43
|
+
if (this.#entryError) return renderError(this.#entryError);
|
|
44
|
+
const handler = this.#entry?.fetch ?? this.#entry?.default?.fetch;
|
|
45
|
+
if (!handler) return renderError(/* @__PURE__ */ new Error(`No fetch handler exported from ${this.#entryPath}`));
|
|
46
|
+
try {
|
|
47
|
+
return await handler(req);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return renderError(error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Hands a raw Node HTTP upgrade to the entry's `handleUpgrade`, if it exports
|
|
54
|
+
* one (installed by `@vercube/ws`). No-op otherwise.
|
|
55
|
+
*/
|
|
56
|
+
async upgrade(context) {
|
|
57
|
+
const node = context.node;
|
|
58
|
+
if (node && this.#entry?.handleUpgrade) await this.#entry.handleUpgrade(node.req, node.socket, node.head);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Reloads every registered environment's entry module.
|
|
63
|
+
*/
|
|
64
|
+
async function reloadAll() {
|
|
65
|
+
await Promise.all(Object.values(envs).map((env) => env?.reload()));
|
|
66
|
+
}
|
|
67
|
+
if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
68
|
+
process.on("unhandledRejection", (error) => console.error(error));
|
|
69
|
+
process.on("uncaughtException", (error) => console.error(error));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* env-runner AppEntry: routes an incoming request to the named environment
|
|
73
|
+
* (defaulting to the Vercube server environment).
|
|
74
|
+
*/
|
|
75
|
+
function fetch(req) {
|
|
76
|
+
const name = req?.headers.get("x-vite-env") || "vercube";
|
|
77
|
+
const env = envs[name];
|
|
78
|
+
if (!env) return Promise.resolve(renderError(/* @__PURE__ */ new Error(`Unknown vite environment "${name}"`)));
|
|
79
|
+
return env.fetch(req);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* env-runner AppEntry: forwards a WebSocket upgrade to the Vercube environment.
|
|
83
|
+
*/
|
|
84
|
+
function upgrade(context) {
|
|
85
|
+
envs["vercube"]?.upgrade(context);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* env-runner IPC hooks: opens the message channel, registers environments, and
|
|
89
|
+
* relays module-runner transport messages.
|
|
90
|
+
*/
|
|
91
|
+
const ipc = {
|
|
92
|
+
onOpen(ctx) {
|
|
93
|
+
sendMessage = ctx.sendMessage;
|
|
94
|
+
},
|
|
95
|
+
onMessage(message) {
|
|
96
|
+
if (message?.type === "custom" && message.event === "vercube:vite-env") {
|
|
97
|
+
const { name, entry } = message.data;
|
|
98
|
+
envs[name] ??= new ViteEnvRunner({
|
|
99
|
+
name,
|
|
100
|
+
entry
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (message?.type === "full-reload") {
|
|
105
|
+
reloadAll();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const listener of messageListeners) listener(message);
|
|
109
|
+
},
|
|
110
|
+
onClose() {}
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Renders an error as a JSON HTTP 500 response.
|
|
114
|
+
*/
|
|
115
|
+
function renderError(error) {
|
|
116
|
+
const err = error;
|
|
117
|
+
return new Response(JSON.stringify({
|
|
118
|
+
status: err?.status ?? 500,
|
|
119
|
+
message: err?.message ?? String(error),
|
|
120
|
+
stack: err?.stack?.split("\n").map((l) => l.trim())
|
|
121
|
+
}, null, 2), {
|
|
122
|
+
status: err?.status ?? 500,
|
|
123
|
+
headers: {
|
|
124
|
+
"Content-Type": "application/json",
|
|
125
|
+
"Cache-Control": "no-store"
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { fetch, ipc, upgrade };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vercube/vite",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Vite plugin integrating Vercube with the Vite Environment API",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/vercube/vercube.git",
|
|
8
|
+
"directory": "packages/vite"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.mjs",
|
|
14
|
+
"module": "./dist/index.mjs",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.mjs",
|
|
17
|
+
"./runtime/dev-worker": "./dist/runtime/dev-worker.mjs",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"vercube",
|
|
27
|
+
"vite",
|
|
28
|
+
"vite-plugin",
|
|
29
|
+
"environment-api",
|
|
30
|
+
"framework"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chokidar": "5.0.0",
|
|
34
|
+
"defu": "6.1.7",
|
|
35
|
+
"env-runner": "0.1.9",
|
|
36
|
+
"pathe": "2.0.3",
|
|
37
|
+
"rou3": "0.8.1",
|
|
38
|
+
"srvx": "0.11.17",
|
|
39
|
+
"@vercube/scan": "1.1.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vite": "^8.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vite": "8.0.16"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown --config ./tsdown.config.ts --config-loader=unrun"
|
|
52
|
+
}
|
|
53
|
+
}
|