@kosmojs/dev 0.5.0 → 0.5.2
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 +38 -2
- package/package.json +18 -17
- package/pkg/chassis.js +143 -53
- package/pkg/chassis.js.map +1 -1
- package/pkg/index.d.ts +1 -1
- package/pkg/index.js +96 -186
- package/pkg/index.js.map +1 -1
- package/pkg/preview.d.ts +2 -2
- package/pkg/templates/run.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
# @kosmojs/dev
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Wires KosmoJS into Vite and runs the generators that produce `lib/`.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
Added to your project automatically by `npm create kosmo`.
|
|
8
|
+
|
|
9
|
+
To add it by hand:
|
|
10
|
+
|
|
7
11
|
```sh
|
|
8
12
|
npm install -D @kosmojs/dev
|
|
9
13
|
```
|
|
10
14
|
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
Watches each source folder and regenerates its `lib/` directory as routes,
|
|
18
|
+
middleware and validation types change - the typed fetch clients, the route tables,
|
|
19
|
+
the framework entry points, the `_/` aliases.
|
|
20
|
+
|
|
21
|
+
It also hosts the generators. Every framework integration ships as a separate
|
|
22
|
+
package under [`generators/`](https://github.com/kosmojs/kosmo/tree/main/generators),
|
|
23
|
+
and they arrive as dependencies of this package rather than of your project,
|
|
24
|
+
so a project's `package.json` stays short no matter how many stacks it uses.
|
|
25
|
+
|
|
26
|
+
Which generators run is decided by `kosmo.config.ts`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
frontend: { stack: "react", base: "/" },
|
|
31
|
+
backend: { stack: "hono", base: "/api" },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
`lib/` is generated output. It is gitignored, safe to delete, and rebuilt on the next run -
|
|
38
|
+
never edit it or import from it by relative path.
|
|
39
|
+
|
|
40
|
+
There is no `vite.config.ts` in a KosmoJS project.
|
|
41
|
+
Vite's `UserConfig` goes in the `viteConfig` key of the `frontend` and `backend` blocks,
|
|
42
|
+
and the stack plugin is supplied by the generator, so adding it yourself to `viteConfig.plugins` runs the transform twice.
|
|
43
|
+
|
|
11
44
|
## Documentation
|
|
12
45
|
|
|
13
|
-
|
|
46
|
+
- [Configuration](https://kosmojs.dev/essentials/config.html)
|
|
47
|
+
- [Why code generation](https://kosmojs.dev/essentials/why-codegen.html)
|
|
48
|
+
|
|
49
|
+
Full documentation at [kosmojs.dev](https://kosmojs.dev).
|
|
14
50
|
|
|
15
51
|
## License
|
|
16
52
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@kosmojs/dev",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
|
+
"description": "Vite integration and code-generation host for KosmoJS projects",
|
|
5
6
|
"cacheVersion": "001",
|
|
6
7
|
"author": "Slee Woo",
|
|
7
8
|
"license": "MIT",
|
|
@@ -25,8 +26,8 @@
|
|
|
25
26
|
"./package.json": "./package.json"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@kosmojs/core": "^0.5.
|
|
29
|
-
"@kosmojs/lib": "^0.5.
|
|
29
|
+
"@kosmojs/core": "^0.5.2",
|
|
30
|
+
"@kosmojs/lib": "^0.5.2",
|
|
30
31
|
"@mdx-js/rollup": "^3.1.1",
|
|
31
32
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
32
33
|
"@vitejs/plugin-react": "^6.1.1",
|
|
@@ -41,20 +42,20 @@
|
|
|
41
42
|
"yaml": "^2.9.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
|
-
"@kosmojs/core-generator": "^0.
|
|
45
|
-
"@kosmojs/fetch-generator": "^0.
|
|
46
|
-
"@kosmojs/h3-generator": "^0.
|
|
47
|
-
"@kosmojs/hono-generator": "^0.
|
|
48
|
-
"@kosmojs/koa-generator": "^0.
|
|
49
|
-
"@kosmojs/mdx-generator": "^0.
|
|
50
|
-
"@kosmojs/openapi-generator": "^0.
|
|
51
|
-
"@kosmojs/react-generator": "^0.
|
|
52
|
-
"@kosmojs/solid-generator": "^0.
|
|
53
|
-
"@kosmojs/ssg-generator": "^0.
|
|
54
|
-
"@kosmojs/ssr-generator": "^0.
|
|
55
|
-
"@kosmojs/svelte-generator": "^0.
|
|
56
|
-
"@kosmojs/typebox-generator": "^0.
|
|
57
|
-
"@kosmojs/vue-generator": "^0.
|
|
45
|
+
"@kosmojs/core-generator": "^0.0.0",
|
|
46
|
+
"@kosmojs/fetch-generator": "^0.0.0",
|
|
47
|
+
"@kosmojs/h3-generator": "^0.0.0",
|
|
48
|
+
"@kosmojs/hono-generator": "^0.0.0",
|
|
49
|
+
"@kosmojs/koa-generator": "^0.0.0",
|
|
50
|
+
"@kosmojs/mdx-generator": "^0.0.0",
|
|
51
|
+
"@kosmojs/openapi-generator": "^0.0.0",
|
|
52
|
+
"@kosmojs/react-generator": "^0.0.0",
|
|
53
|
+
"@kosmojs/solid-generator": "^0.0.0",
|
|
54
|
+
"@kosmojs/ssg-generator": "^0.0.0",
|
|
55
|
+
"@kosmojs/ssr-generator": "^0.0.0",
|
|
56
|
+
"@kosmojs/svelte-generator": "^0.0.0",
|
|
57
|
+
"@kosmojs/typebox-generator": "^0.0.0",
|
|
58
|
+
"@kosmojs/vue-generator": "^0.0.0"
|
|
58
59
|
},
|
|
59
60
|
"scripts": {
|
|
60
61
|
"build": "wsbuild src/index.ts src/chassis.ts"
|
package/pkg/chassis.js
CHANGED
|
@@ -72,19 +72,21 @@ var identicalHashSum = (a, b) => {
|
|
|
72
72
|
};
|
|
73
73
|
//#endregion
|
|
74
74
|
//#region src/templates/run.ts?raw
|
|
75
|
-
var run_default = "#!/usr/bin/env node\n\n/**\n * Serves every built source folder from a single process.\n *\n * Written by `kosmo build` into the dist directory; `kosmo preview` restarts it after each rebuild.\n *\n * node dist/run.js -p 4556\n * node dist/run.js -s /tmp/app.sock\n *\n * Folders are discovered at startup from `dist/<folder>/kosmo.json`,\n * so a partial build (`kosmo build admin`) never leaves a stale folder table behind,\n * and removing a folder's dist directory is enough to stop serving it.\n *\n * Per folder:\n * - SSR folders mount `ssr/server.js` - one listener for pages, assets and the bundled API.\n * - CSR folders mount `api/listener.js` for the API and serve `client/` statically from memory,\n * with the SPA fallback to index.html for unmatched page URLs.\n *\n * Authored as TypeScript, deployed with types stripped;\n *
|
|
75
|
+
var run_default = "#!/usr/bin/env node\n\n/**\n * Serves every built source folder from a single process.\n *\n * Written by `kosmo build` into the dist directory; `kosmo preview` restarts it after each rebuild.\n *\n * node dist/run.js -p 4556\n * node dist/run.js -s /tmp/app.sock\n *\n * Folders are discovered at startup from `dist/<folder>/kosmo.json`,\n * so a partial build (`kosmo build admin`) never leaves a stale folder table behind,\n * and removing a folder's dist directory is enough to stop serving it.\n *\n * Per folder:\n * - SSR folders mount `ssr/server.js` - one listener for pages, assets and the bundled API.\n * - CSR folders mount `api/listener.js` for the API and serve `client/` statically from memory,\n * with the SPA fallback to index.html for unmatched page URLs.\n *\n * Authored as TypeScript, deployed with types stripped;\n * */\n\nimport {\n access,\n chmod,\n constants,\n readdir,\n readFile,\n unlink,\n} from \"node:fs/promises\";\nimport type { IncomingMessage, Server, ServerResponse } from \"node:http\";\nimport { createServer } from \"node:http\";\nimport { extname, join, posix, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { pathToRegexp } from \"path-to-regexp\";\n\nimport { MIME_TYPES, type SourceFolderManifest } from \"@kosmojs/core\";\n\ntype NodeListener = (req: IncomingMessage, res: ServerResponse) => void;\n\ntype Folder = SourceFolderManifest & {\n dir: string;\n};\n\ntype Handler = {\n name: string;\n base: string;\n aliasPatterns: Array<RegExp>;\n listener: NodeListener;\n};\n\ntype StaticFile = {\n buffer: Buffer;\n headers: Record<string, string>;\n};\n\nconst ROOT = import.meta.dirname;\n\nconst contentTypeFor = (file: string): string => {\n return MIME_TYPES[extname(file).toLowerCase()] || \"application/octet-stream\";\n};\n\nconst handlerWeight = ({ base }: Handler): number => {\n return base.length + base.split(\"/\").filter(Boolean).length;\n};\n\nconst readFolders = async (): Promise<Array<Folder>> => {\n const folders: Array<Folder> = [];\n\n for (const entry of await readdir(ROOT, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const dir = join(ROOT, entry.name);\n const manifestFile = join(dir, \"kosmo.json\");\n\n const manifestFileExists = await access(manifestFile, constants.F_OK).then(\n () => true,\n () => false,\n );\n\n if (!manifestFileExists) {\n // not a kosmo folder, ignore\n continue;\n }\n\n const { default: manifest } = await import(manifestFile, {\n with: { type: \"json\" },\n });\n\n folders.push({ dir, ...(manifest as SourceFolderManifest) });\n }\n\n return folders;\n};\n\n/**\n * In-memory static server for a CSR folder's `client/` output.\n * Everything is read once at startup; Vite hashes the files under `assets/`,\n * so those are served as immutable, the rest as revalidate-always.\n * */\nconst createStaticListener = async (\n dir: string,\n base: string,\n): Promise<NodeListener> => {\n const files = new Map<string, StaticFile>();\n\n const walk = async (path: string, prefix: string) => {\n for (const entry of await readdir(path, { withFileTypes: true })) {\n // Vite's build manifest is not a public file\n if (entry.name === \".vite\") {\n continue;\n }\n\n const file = join(path, entry.name);\n const url = posix.join(prefix, entry.name);\n\n if (entry.isDirectory()) {\n await walk(file, url);\n continue;\n }\n\n const buffer = await readFile(file);\n\n files.set(url, {\n buffer,\n headers: {\n \"Content-Type\": contentTypeFor(entry.name),\n \"Content-Length\": String(buffer.length),\n \"Cache-Control\": url.startsWith(posix.join(base, \"assets/\"))\n ? \"public, max-age=31536000, immutable\"\n : \"no-cache\",\n },\n });\n }\n };\n\n await walk(dir, base);\n\n const index = files.get(posix.join(base, \"index.html\"));\n\n return (req, res) => {\n const { pathname } = new URL(req.url ?? \"/\", \"http://localhost\");\n\n if (![\"GET\", \"HEAD\"].includes(req.method ?? \"\")) {\n res.writeHead(405, { Allow: \"GET, HEAD\" });\n res.end();\n return;\n }\n\n // a real file wins; anything else is a client route, resolved by the client router\n const file = files.get(pathname) || index;\n\n if (!file) {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"404: Not Found\");\n return;\n }\n\n res.writeHead(200, file.headers);\n res.end(req.method === \"HEAD\" ? undefined : file.buffer);\n };\n};\n\nconst mountFolders = async (\n folders: Array<Folder>,\n): Promise<Array<Handler>> => {\n const handlers: Array<Handler> = [];\n\n for (const { dir, name, frontend, backend } of folders) {\n const aliasPatterns =\n backend?.aliasPatterns.map((alias) => {\n return pathToRegexp(posix.join(\"/\", alias)).regexp;\n }) || [];\n\n if (frontend?.ssr) {\n // ssr/server.js bundles the backend\n const { createListener } = (await import(\n resolve(dir, \"ssr\", \"server.js\")\n )) as { createListener: () => Promise<NodeListener> };\n\n const listener = await createListener();\n\n handlers.push({\n name,\n base: frontend.base as string,\n aliasPatterns: [],\n listener,\n });\n\n if (backend) {\n handlers.push({\n name,\n base: backend.base,\n aliasPatterns: aliasPatterns,\n listener,\n });\n }\n\n continue;\n }\n\n if (backend) {\n const { default: listener } = (await import(\n join(dir, \"api\", \"listener.js\")\n )) as { default: NodeListener };\n handlers.push({\n name,\n base: backend.base,\n aliasPatterns: aliasPatterns,\n listener,\n });\n }\n\n if (frontend) {\n const listener = await createStaticListener(\n join(dir, \"client\"),\n frontend.base,\n );\n handlers.push({ name, base: frontend.base, aliasPatterns: [], listener });\n }\n }\n\n return handlers.sort((a, b) => handlerWeight(b) - handlerWeight(a));\n};\n\nexport const createListener = async (): Promise<NodeListener> => {\n const folders = await readFolders();\n\n if (!folders.length) {\n throw new Error(`No built source folders found in ${ROOT}`);\n }\n\n const handlers = await mountFolders(folders);\n\n for (const { name, base } of handlers) {\n console.log(` ${base.padEnd(24)} -> ${name}`);\n }\n\n return (req, res) => {\n const { pathname } = new URL(req.url ?? \"/\", \"http://localhost\");\n\n for (const { base, aliasPatterns, listener } of handlers) {\n if (\n pathname === base ||\n pathname.startsWith(posix.join(base, \"/\")) ||\n aliasPatterns.some((r) => r.test(pathname))\n ) {\n listener(req, res);\n return;\n }\n }\n\n res.writeHead(404, { \"Content-Type\": \"text/html\" });\n res.end(\"<h1>404: Not Found</h1>\");\n };\n};\n\nexport const startServer = async ({\n port,\n sock,\n}: {\n port?: string | number | undefined;\n sock?: string | undefined;\n}): Promise<Server> => {\n if (![port, sock].some(Boolean)) {\n throw new Error(\"Please provide either -p/--port or -s/--sock\");\n }\n\n if (sock) {\n await unlink(sock).catch((error) => {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw error;\n }\n });\n }\n\n console.log(\n `\\n -> Starting server [ ${sock ? `sock: ${sock}` : `port: ${port}`} ]\\n`,\n );\n\n const server = createServer(await createListener());\n\n server.listen(sock || Number(port), async () => {\n if (sock) {\n // let a reverse proxy running as another user connect\n await chmod(sock, 0o777);\n }\n console.log(\"\\n -> Server Started\\n\");\n });\n\n return server;\n};\n\nif (process.argv[1] === import.meta.url) {\n const {\n values: { port, sock },\n } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\" },\n sock: { type: \"string\", short: \"s\" },\n },\n });\n\n try {\n await startServer({ port, sock });\n } catch (error) {\n console.error(\"Failed starting server\");\n console.error(error);\n process.exit(1);\n }\n}\n";
|
|
76
76
|
//#endregion
|
|
77
77
|
//#region src/runner.ts
|
|
78
78
|
var folderManifestFactory = (sourceFolder) => {
|
|
79
79
|
const { name, config: { frontend, backend } } = sourceFolder;
|
|
80
80
|
return {
|
|
81
81
|
name,
|
|
82
|
-
...frontend?.base ? { frontend: {
|
|
82
|
+
...frontend?.base ? { frontend: {
|
|
83
|
+
base: frontend.base,
|
|
84
|
+
ssr: frontend?.ssr ? true : false
|
|
85
|
+
} } : {},
|
|
83
86
|
...backend?.base ? { backend: {
|
|
84
87
|
base: backend.base,
|
|
85
88
|
aliasPatterns: createAliasPatterns(backend?.alias)
|
|
86
|
-
} } : {}
|
|
87
|
-
ssr: frontend?.ssr ? true : false
|
|
89
|
+
} } : {}
|
|
88
90
|
};
|
|
89
91
|
};
|
|
90
92
|
var writeFolderManifest = async (sourceFolder) => {
|
|
@@ -127,8 +129,8 @@ var IGNORED_FILE = /(^\.|~$|\.swp$|\.tmp$)/;
|
|
|
127
129
|
* Preview = production build + watcher + `dist/run.js`.
|
|
128
130
|
*
|
|
129
131
|
* The runner is a child process rather than an in-process import:
|
|
130
|
-
* a fresh process is the only way to reload an ESM graph,
|
|
131
|
-
* what production does - `node dist/run.js`.
|
|
132
|
+
* a fresh process is the only way to reload an ESM graph,
|
|
133
|
+
* and it is exactly what production does - `node dist/run.js`.
|
|
132
134
|
*
|
|
133
135
|
* Watching is chokidar over the `src/<folder>` trees - no Vite server,
|
|
134
136
|
* no module graph: the unit of work is "rebuild this folder", so folder-level
|
|
@@ -213,13 +215,17 @@ var previewFactory = async (projectSettings, build) => {
|
|
|
213
215
|
clearTimeout(timer);
|
|
214
216
|
timer = setTimeout(rebuild, DEBOUNCE_MS);
|
|
215
217
|
};
|
|
216
|
-
const watchedFolders = sourceFolders.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
const watchedFolders = sourceFolders.flatMap((sourceFolder) => {
|
|
219
|
+
if (sourceFolder.config.frontend || sourceFolder.config.backend) {
|
|
220
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
221
|
+
return [{
|
|
222
|
+
dir: createPath.src(),
|
|
223
|
+
sourceFolder
|
|
224
|
+
}];
|
|
225
|
+
}
|
|
226
|
+
return [];
|
|
222
227
|
});
|
|
228
|
+
if (!watchedFolders.length) return async () => {};
|
|
223
229
|
const watcher = watch(watchedFolders.map(({ dir }) => dir), { ignoreInitial: true });
|
|
224
230
|
watcher.on("all", (_event, path) => {
|
|
225
231
|
const { sourceFolder } = watchedFolders.find(({ dir }) => {
|
|
@@ -248,19 +254,19 @@ var previewFactory = async (projectSettings, build) => {
|
|
|
248
254
|
//#region src/chassis.ts
|
|
249
255
|
var chassis_default = async (projectSettings) => {
|
|
250
256
|
const { devPort, command } = projectSettings;
|
|
251
|
-
for (const sourceFolder of projectSettings.sourceFolders) for (const generator of sourceFolder.
|
|
257
|
+
for (const sourceFolder of projectSettings.sourceFolders) for (const generator of sourceFolder.generators) {
|
|
252
258
|
if (!generator.meta?.name || typeof generator.factory !== "function") throw new Error(`${sourceFolder.name}: Unrecognized generator - must be created via defineGenerator()`);
|
|
253
259
|
try {
|
|
254
|
-
await generator.factory(sourceFolder).
|
|
260
|
+
await generator.factory(sourceFolder).seed();
|
|
255
261
|
} catch (error) {
|
|
256
|
-
console.error(styleText("red", `${sourceFolder.name}: ${generator.meta.name} generator failed to
|
|
262
|
+
console.error(styleText("red", `${sourceFolder.name}: ${generator.meta.name} generator failed to seed`));
|
|
257
263
|
throw error;
|
|
258
264
|
}
|
|
259
265
|
}
|
|
260
266
|
if (command === "build" || command === "preview") {
|
|
261
267
|
for (const sourceFolder of projectSettings.sourceFolders) {
|
|
262
268
|
await buildSourceFolder(sourceFolder);
|
|
263
|
-
await writeFolderManifest(sourceFolder);
|
|
269
|
+
if (sourceFolder.config.frontend || sourceFolder.config.backend) await writeFolderManifest(sourceFolder);
|
|
264
270
|
}
|
|
265
271
|
await deployRunner(projectSettings);
|
|
266
272
|
if (command === "build") return async () => {};
|
|
@@ -269,11 +275,11 @@ var chassis_default = async (projectSettings) => {
|
|
|
269
275
|
const requestHandlers = [];
|
|
270
276
|
const teardownHandlers = [];
|
|
271
277
|
const eventMap = {};
|
|
272
|
-
for (const sourceFolder of projectSettings.sourceFolders) eventMap[sourceFolder.name] = await eventFactory(sourceFolder);
|
|
278
|
+
for (const sourceFolder of projectSettings.sourceFolders) if (sourceFolder.config.frontend || sourceFolder.config.backend) eventMap[sourceFolder.name] = await eventFactory(sourceFolder);
|
|
273
279
|
let port = await findFreePort(devPort);
|
|
274
280
|
for (const sourceFolder of projectSettings.sourceFolders) {
|
|
275
281
|
const { createPath } = pathResolver(sourceFolder);
|
|
276
|
-
const {
|
|
282
|
+
const { frontend, backend, sidecar } = sourceFolder.config;
|
|
277
283
|
const requestMatchers = matchersFactory(sourceFolder);
|
|
278
284
|
const plugins = [
|
|
279
285
|
vitePlugins.tsconfigPaths(sourceFolder),
|
|
@@ -284,11 +290,10 @@ var chassis_default = async (projectSettings) => {
|
|
|
284
290
|
})
|
|
285
291
|
];
|
|
286
292
|
if (frontend) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
});
|
|
293
|
+
const generator = sourceFolder.generators.find((e) => e.meta.slot === "frontend");
|
|
294
|
+
const viteServer = await createServer(mergeConfigs(frontend?.viteConfig, generator?.factory(sourceFolder).viteConfig?.({
|
|
295
|
+
kind: "frontend",
|
|
296
|
+
command
|
|
292
297
|
}), {
|
|
293
298
|
base: frontend.base,
|
|
294
299
|
root: createPath.src(),
|
|
@@ -309,7 +314,7 @@ var chassis_default = async (projectSettings) => {
|
|
|
309
314
|
teardownHandlers.push(viteServer.close);
|
|
310
315
|
}
|
|
311
316
|
if (backend) {
|
|
312
|
-
const generator = generators.find((e) => e.meta.slot === "backend");
|
|
317
|
+
const generator = sourceFolder.generators.find((e) => e.meta.slot === "backend");
|
|
313
318
|
const viteServer = await createServer(mergeConfigs(backend.viteConfig, generator?.factory(sourceFolder).viteConfig?.({
|
|
314
319
|
kind: "backend",
|
|
315
320
|
command
|
|
@@ -324,9 +329,9 @@ var chassis_default = async (projectSettings) => {
|
|
|
324
329
|
hmr: { port: port++ }
|
|
325
330
|
},
|
|
326
331
|
resolve: { conditions: ["node"] },
|
|
327
|
-
environments: {
|
|
332
|
+
environments: { backend: { resolve: { conditions: ["node"] } } }
|
|
328
333
|
}));
|
|
329
|
-
const env = viteServer.environments.
|
|
334
|
+
const env = viteServer.environments.backend;
|
|
330
335
|
const loadDevSetup = async () => {
|
|
331
336
|
env.runner.clearCache();
|
|
332
337
|
return env.runner.import(join(defaults.apiDir, "dev.ts")).then((e) => e.default);
|
|
@@ -338,12 +343,7 @@ var chassis_default = async (projectSettings) => {
|
|
|
338
343
|
await devSetup?.teardownHandler?.();
|
|
339
344
|
devSetup = await loadDevSetup();
|
|
340
345
|
} catch (error) {
|
|
341
|
-
|
|
342
|
-
* A transiently-invalid module (half-saved file, atomic rewrite mid-flight, syntax error)
|
|
343
|
-
* must not take the dev server down: an uncaught rejection here is fatal to the whole process.
|
|
344
|
-
* Keep serving the previous api program; the next successful change reloads it.
|
|
345
|
-
* */
|
|
346
|
-
console.error(styleText("red", `${sourceFolder.name}: api reload failed - keeping previous api program`));
|
|
346
|
+
console.error(styleText("red", `${sourceFolder.name}: backend reload failed`));
|
|
347
347
|
console.error(error);
|
|
348
348
|
}
|
|
349
349
|
});
|
|
@@ -354,6 +354,71 @@ var chassis_default = async (projectSettings) => {
|
|
|
354
354
|
]);
|
|
355
355
|
teardownHandlers.push(viteServer.close);
|
|
356
356
|
}
|
|
357
|
+
if (sidecar?.serve) {
|
|
358
|
+
/**
|
|
359
|
+
* Assigned once the service is up; the reload plugin below is installed during `createServer`,
|
|
360
|
+
* so the hook has to reach it through a binding rather than a closure over something that does not exist yet.
|
|
361
|
+
* */
|
|
362
|
+
let reload = async () => {};
|
|
363
|
+
/**
|
|
364
|
+
* Restarting on a raw `watcher.on("change")` reads the module graph before Vite has invalidated it,
|
|
365
|
+
* and the service comes back up running the source as it was before the save.
|
|
366
|
+
*
|
|
367
|
+
* `hotUpdate` is called after that invalidation.
|
|
368
|
+
* Returning `[]` says the update is handled and leaves Vite nothing to propagate.
|
|
369
|
+
*
|
|
370
|
+
* The hook runs once per environment; nothing but `sidecar` resolves a module on this server.
|
|
371
|
+
* */
|
|
372
|
+
const reloadPlugin = {
|
|
373
|
+
name: "kosmo:sidecar-reload",
|
|
374
|
+
hotUpdate({ modules }) {
|
|
375
|
+
if (modules.length) reload();
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const viteServer = await createServer(mergeConfigs(sidecar.viteConfig, {
|
|
380
|
+
root: createPath.src(),
|
|
381
|
+
appType: "custom",
|
|
382
|
+
cacheDir: cacheDir(sourceFolder, command, "sidecar"),
|
|
383
|
+
plugins: [...plugins, reloadPlugin],
|
|
384
|
+
server: { hmr: true },
|
|
385
|
+
resolve: { conditions: ["node"] },
|
|
386
|
+
environments: { sidecar: { resolve: { conditions: ["node"] } } }
|
|
387
|
+
}));
|
|
388
|
+
const env = viteServer.environments.sidecar;
|
|
389
|
+
const loadService = async () => {
|
|
390
|
+
env.runner.clearCache();
|
|
391
|
+
return env.runner.import(sidecar.entry).then((e) => e.default);
|
|
392
|
+
};
|
|
393
|
+
let service = await loadService();
|
|
394
|
+
let close = await service.start();
|
|
395
|
+
reload = async () => {
|
|
396
|
+
try {
|
|
397
|
+
/**
|
|
398
|
+
* Load before tearing anything down: a save that cannot compile throws here,
|
|
399
|
+
* and the running service is left untouched rather than closed with nothing to replace it -
|
|
400
|
+
* which is unrecoverable for a service whose close function cannot run twice.
|
|
401
|
+
*
|
|
402
|
+
* Starting still happens after closing, so a service holding a port frees it before the new one binds.
|
|
403
|
+
* */
|
|
404
|
+
const next = await loadService();
|
|
405
|
+
await service.teardown?.();
|
|
406
|
+
await close();
|
|
407
|
+
/**
|
|
408
|
+
* Nothing is running between here and `start()`, so drop the closer:
|
|
409
|
+
* a `start()` that throws would otherwise leave the next reload calling it a second time,
|
|
410
|
+
* which for a real one throws too - and the sidecar never comes back.
|
|
411
|
+
* */
|
|
412
|
+
close = async () => {};
|
|
413
|
+
service = next;
|
|
414
|
+
close = await service.start();
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error(styleText("red", `${sourceFolder.name}: sidecar reload failed`));
|
|
417
|
+
console.error(error);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
teardownHandlers.push(viteServer.close);
|
|
421
|
+
}
|
|
357
422
|
}
|
|
358
423
|
/**
|
|
359
424
|
* Sorting is essential to ensure more specific paths are matched before broader ones.
|
|
@@ -405,9 +470,9 @@ var cacheDir = ({ root, name }, command, mode) => {
|
|
|
405
470
|
var buildSourceFolder = async (sourceFolder) => {
|
|
406
471
|
const command = "build";
|
|
407
472
|
const { createPath } = pathResolver(sourceFolder);
|
|
408
|
-
const {
|
|
473
|
+
const { frontend, backend, sidecar } = sourceFolder.config;
|
|
409
474
|
const resolvedRoutes = [];
|
|
410
|
-
{
|
|
475
|
+
if (!sidecar) {
|
|
411
476
|
const { resolvers } = await routesFactory(sourceFolder, cacheFactory);
|
|
412
477
|
const spinner = spinnerFactory(`${sourceFolder.name}: resolving routes`);
|
|
413
478
|
for (const { name, handler } of resolvers.values()) {
|
|
@@ -424,25 +489,26 @@ var buildSourceFolder = async (sourceFolder) => {
|
|
|
424
489
|
command
|
|
425
490
|
})
|
|
426
491
|
];
|
|
427
|
-
for (const generator of generators) await generator.factory(sourceFolder).build?.(resolvedRoutes);
|
|
428
|
-
if (frontend)
|
|
429
|
-
|
|
430
|
-
|
|
492
|
+
for (const generator of sourceFolder.generators) await generator.factory(sourceFolder).build?.(resolvedRoutes);
|
|
493
|
+
if (frontend) {
|
|
494
|
+
const generator = sourceFolder.generators.find((e) => e.meta.slot === "frontend");
|
|
495
|
+
await build(mergeConfigs(frontend.viteConfig, generator?.factory(sourceFolder).viteConfig?.({
|
|
496
|
+
kind: "frontend",
|
|
431
497
|
command
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
}
|
|
498
|
+
}), {
|
|
499
|
+
base: frontend.base,
|
|
500
|
+
root: createPath.src(),
|
|
501
|
+
cacheDir: cacheDir(sourceFolder, command, "client"),
|
|
502
|
+
plugins,
|
|
503
|
+
build: {
|
|
504
|
+
outDir: createPath.distDir("client"),
|
|
505
|
+
manifest: true,
|
|
506
|
+
emptyOutDir: true
|
|
507
|
+
}
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
444
510
|
if (backend) {
|
|
445
|
-
const generator = generators.find((e) => e.meta.slot === "backend");
|
|
511
|
+
const generator = sourceFolder.generators.find((e) => e.meta.slot === "backend");
|
|
446
512
|
await build(mergeConfigs(backend.viteConfig, generator?.factory(sourceFolder).viteConfig?.({
|
|
447
513
|
kind: "backend",
|
|
448
514
|
command
|
|
@@ -472,13 +538,37 @@ var buildSourceFolder = async (sourceFolder) => {
|
|
|
472
538
|
cacheDir: cacheDir(sourceFolder, command, "backend")
|
|
473
539
|
}));
|
|
474
540
|
}
|
|
475
|
-
|
|
541
|
+
if (sidecar) await build(mergeConfigs(sidecar.viteConfig, {
|
|
542
|
+
base: "./",
|
|
543
|
+
root: createPath.src(),
|
|
544
|
+
appType: "custom",
|
|
545
|
+
plugins,
|
|
546
|
+
resolve: { conditions: ["node"] },
|
|
547
|
+
build: {
|
|
548
|
+
ssr: true,
|
|
549
|
+
target: "esnext",
|
|
550
|
+
sourcemap: true,
|
|
551
|
+
emptyOutDir: true,
|
|
552
|
+
rolldownOptions: {
|
|
553
|
+
input: {
|
|
554
|
+
entry: createPath.src(sidecar.entry),
|
|
555
|
+
...sidecar.run ? { run: sidecar.run } : {}
|
|
556
|
+
},
|
|
557
|
+
output: {
|
|
558
|
+
dir: createPath.distDir("sidecar"),
|
|
559
|
+
format: "esm"
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
cacheDir: cacheDir(sourceFolder, command, "sidecar")
|
|
564
|
+
}));
|
|
565
|
+
for (const generator of sourceFolder.generators) await generator.factory(sourceFolder).postBuild?.(resolvedRoutes);
|
|
476
566
|
};
|
|
477
567
|
var eventFactory = async (sourceFolder) => {
|
|
478
568
|
const { resolvers, resolversFactory } = await routesFactory(sourceFolder, cacheFactory);
|
|
479
569
|
const { createPath } = pathResolver(sourceFolder);
|
|
480
570
|
const generators = [];
|
|
481
|
-
for (const generator of sourceFolder.
|
|
571
|
+
for (const generator of sourceFolder.generators) {
|
|
482
572
|
const factory = generator.factory(sourceFolder);
|
|
483
573
|
generators.push({
|
|
484
574
|
name: generator.meta.name,
|