@kosmojs/dev 0.4.4 → 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 +20 -19
- package/pkg/chassis.js +696 -291
- package/pkg/chassis.js.map +1 -1
- package/pkg/index.d.ts +16 -16
- package/pkg/index.js +8436 -9947
- package/pkg/index.js.map +1 -1
- package/pkg/preview.d.ts +2 -2
- package/pkg/runner.d.ts +2 -15
- package/pkg/templates/run.d.ts +0 -1
package/pkg/chassis.js
CHANGED
|
@@ -1,309 +1,714 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { styleText } from "node:util";
|
|
3
|
+
import { createAliasPatterns, mergeConfigs, pathExists, pathResolver, renderToFile, routesFactory, spinnerFactory, vitePlugins } from "@kosmojs/lib";
|
|
4
|
+
import { basename, dirname, join, posix, relative, resolve } from "node:path";
|
|
5
|
+
import { defaults } from "@kosmojs/core";
|
|
6
|
+
import crc from "crc/crc32";
|
|
7
|
+
import { build, createServer, transformWithOxc } from "vite";
|
|
8
|
+
import { pathToRegexp } from "path-to-regexp";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { watch } from "chokidar";
|
|
15
|
+
//#region src/cache.ts
|
|
3
16
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
17
|
+
* Read the installed package.json at runtime to get the actual version.
|
|
18
|
+
* A static `import ... with { type: "json" }` would be inlined by the
|
|
19
|
+
* bundler with the pre-bump version, defeating the point.
|
|
20
|
+
* */
|
|
21
|
+
var self = JSON.parse(readFileSync(createRequire(import.meta.url).resolve("@kosmojs/dev/package.json"), "utf-8"));
|
|
22
|
+
var cacheFactory = (route, sourceFolder, extraContext) => {
|
|
23
|
+
const cacheFile = pathResolver(sourceFolder).createPath.libApi(dirname(route.file), "cache.json");
|
|
24
|
+
const validateCache = async (cache) => {
|
|
25
|
+
if (!cache?.hash) return;
|
|
26
|
+
if (!cache.typeDeclarations || !cache.referencedFiles) return;
|
|
27
|
+
const hash = await generateFileHash(route.fileFullpath, { ...extraContext });
|
|
28
|
+
if (!identicalHashSum(cache.hash, hash)) return;
|
|
29
|
+
for (const [file, hash] of Object.entries(cache.referencedFiles)) if (!identicalHashSum(hash, await generateFileHash(resolve(sourceFolder.root, file)))) return;
|
|
30
|
+
return cache;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
async get(opt) {
|
|
34
|
+
if (await pathExists(cacheFile)) try {
|
|
35
|
+
const cache = JSON.parse(await readFile(cacheFile, "utf8"));
|
|
36
|
+
return opt?.validate ? validateCache(cache) : cache;
|
|
37
|
+
} catch (_e) {}
|
|
38
|
+
},
|
|
39
|
+
async set(cache) {
|
|
40
|
+
const hash = await generateFileHash(route.fileFullpath, { ...extraContext });
|
|
41
|
+
const referencedFiles = {};
|
|
42
|
+
for (const file of cache.referencedFiles) referencedFiles[file.replace(`${sourceFolder.root}/`, "")] = await generateFileHash(file);
|
|
43
|
+
const value = {
|
|
44
|
+
...cache,
|
|
45
|
+
hash,
|
|
46
|
+
referencedFiles
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* use renderToFile to skip the write when nothing changed.
|
|
50
|
+
* Two kosmo processes on one project resolve the same routes to the same bytes,
|
|
51
|
+
* so this keeps them from rewriting each other's cache files.
|
|
52
|
+
* */
|
|
53
|
+
await renderToFile(cacheFile, JSON.stringify(value, void 0, 2), {}, { render: (content) => content });
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
43
57
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
var generateFileHash = async (file, extraContext) => {
|
|
59
|
+
let fileContent;
|
|
60
|
+
try {
|
|
61
|
+
fileContent = await readFile(file, "utf8");
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
return fileContent ? crc(JSON.stringify({
|
|
66
|
+
...extraContext,
|
|
67
|
+
[self.cacheVersion]: fileContent
|
|
68
|
+
})) : 0;
|
|
47
69
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
name: string;
|
|
51
|
-
segments: [base: string, api?: string];
|
|
52
|
-
listener: NodeListener;
|
|
70
|
+
var identicalHashSum = (a, b) => {
|
|
71
|
+
return a === b;
|
|
53
72
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
buffer: Buffer;
|
|
57
|
-
|
|
73
|
+
//#endregion
|
|
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 * */\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
|
+
//#endregion
|
|
77
|
+
//#region src/runner.ts
|
|
78
|
+
var folderManifestFactory = (sourceFolder) => {
|
|
79
|
+
const { name, config: { frontend, backend } } = sourceFolder;
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
...frontend?.base ? { frontend: {
|
|
83
|
+
base: frontend.base,
|
|
84
|
+
ssr: frontend?.ssr ? true : false
|
|
85
|
+
} } : {},
|
|
86
|
+
...backend?.base ? { backend: {
|
|
87
|
+
base: backend.base,
|
|
88
|
+
aliasPatterns: createAliasPatterns(backend?.alias)
|
|
89
|
+
} } : {}
|
|
90
|
+
};
|
|
58
91
|
};
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
var writeFolderManifest = async (sourceFolder) => {
|
|
93
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
94
|
+
const dir = createPath.distDir();
|
|
95
|
+
await mkdir(dir, { recursive: true });
|
|
96
|
+
await writeFile(resolve(dir, "kosmo.json"), JSON.stringify(folderManifestFactory(sourceFolder), void 0, 2), "utf8");
|
|
64
97
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&").replace(/\\/+$/, "");
|
|
98
|
+
var runnerPath = ({ root, distDir }) => {
|
|
99
|
+
return resolve(root, distDir, "run.js");
|
|
68
100
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
var runnerCode;
|
|
102
|
+
/**
|
|
103
|
+
* The template is authored as TypeScript so it typechecks with the package;
|
|
104
|
+
* the deployed file is plain JS. Stripping runs through the oxc transform
|
|
105
|
+
* that vite already ships, once per process - preview redeploys after every rebuild.
|
|
106
|
+
* */
|
|
107
|
+
var runnerCodeFactory = async () => {
|
|
108
|
+
if (!runnerCode) {
|
|
109
|
+
const { code } = await transformWithOxc(run_default, "run.ts", { sourcemap: false });
|
|
110
|
+
runnerCode = code;
|
|
111
|
+
}
|
|
112
|
+
return runnerCode;
|
|
74
113
|
};
|
|
75
|
-
|
|
76
114
|
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const weight = base.split("/").filter(Boolean).length;
|
|
86
|
-
return api ? weight + 5 : weight;
|
|
115
|
+
* run.js is static - it holds no folder-specific data -
|
|
116
|
+
* so rewriting it on every build, partial builds included, is idempotent.
|
|
117
|
+
* */
|
|
118
|
+
var deployRunner = async (projectSettings) => {
|
|
119
|
+
const file = runnerPath(projectSettings);
|
|
120
|
+
await mkdir(resolve(file, ".."), { recursive: true });
|
|
121
|
+
await writeFile(file, await runnerCodeFactory(), "utf8");
|
|
122
|
+
return file;
|
|
87
123
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/preview.ts
|
|
126
|
+
var DEBOUNCE_MS = 300;
|
|
127
|
+
var IGNORED_FILE = /(^\.|~$|\.swp$|\.tmp$)/;
|
|
128
|
+
/**
|
|
129
|
+
* Preview = production build + watcher + `dist/run.js`.
|
|
130
|
+
*
|
|
131
|
+
* The runner is a child process rather than an in-process import:
|
|
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`.
|
|
134
|
+
*
|
|
135
|
+
* Watching is chokidar over the `src/<folder>` trees - no Vite server,
|
|
136
|
+
* no module graph: the unit of work is "rebuild this folder", so folder-level
|
|
137
|
+
* granularity is all the watcher needs to provide.
|
|
138
|
+
* */
|
|
139
|
+
var previewFactory = async (projectSettings, build) => {
|
|
140
|
+
const { root, previewPort, sourceFolders } = projectSettings;
|
|
141
|
+
const runner = runnerPath(projectSettings);
|
|
142
|
+
const log = (...args) => {
|
|
143
|
+
console.log(styleText("dim", "[preview]"), ...args);
|
|
144
|
+
};
|
|
145
|
+
let child;
|
|
146
|
+
const startRunner = () => {
|
|
147
|
+
const proc = spawn(process.execPath, [
|
|
148
|
+
"--enable-source-maps",
|
|
149
|
+
runner,
|
|
150
|
+
"--port",
|
|
151
|
+
String(previewPort)
|
|
152
|
+
], {
|
|
153
|
+
cwd: root,
|
|
154
|
+
stdio: "inherit"
|
|
155
|
+
});
|
|
156
|
+
proc.on("exit", (code, signal) => {
|
|
157
|
+
if (child === proc) {
|
|
158
|
+
child = void 0;
|
|
159
|
+
log(styleText("red", `runner exited (${signal ?? code})`));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
child = proc;
|
|
163
|
+
};
|
|
164
|
+
const stopRunner = () => {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const proc = child;
|
|
167
|
+
if (!proc || proc.exitCode !== null) {
|
|
168
|
+
child = void 0;
|
|
169
|
+
resolve();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
child = void 0;
|
|
173
|
+
proc.once("exit", () => resolve());
|
|
174
|
+
proc.kill("SIGTERM");
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const restartRunner = async () => {
|
|
178
|
+
await stopRunner();
|
|
179
|
+
startRunner();
|
|
180
|
+
};
|
|
181
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
182
|
+
let building = false;
|
|
183
|
+
let timer;
|
|
184
|
+
const rebuild = async () => {
|
|
185
|
+
if (building) return;
|
|
186
|
+
building = true;
|
|
187
|
+
try {
|
|
188
|
+
while (dirty.size) {
|
|
189
|
+
const folders = [...dirty];
|
|
190
|
+
dirty.clear();
|
|
191
|
+
let failed = false;
|
|
192
|
+
for (const sourceFolder of folders) {
|
|
193
|
+
log(`rebuilding ${styleText("blue", sourceFolder.name)}`);
|
|
194
|
+
try {
|
|
195
|
+
await build(sourceFolder);
|
|
196
|
+
await writeFolderManifest(sourceFolder);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
failed = true;
|
|
199
|
+
console.error(styleText("red", `${sourceFolder.name}: build failed`));
|
|
200
|
+
console.error(error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await deployRunner(projectSettings);
|
|
204
|
+
if (failed) log(styleText("yellow", "keeping previous build running"));
|
|
205
|
+
else await restartRunner();
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
building = false;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const schedule = (sourceFolder, file) => {
|
|
212
|
+
if (process.env.DEBUG) log(`event ${sourceFolder.name}: ${file} (building: ${building})`);
|
|
213
|
+
if (IGNORED_FILE.test(basename(file))) return;
|
|
214
|
+
dirty.add(sourceFolder);
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
timer = setTimeout(rebuild, DEBOUNCE_MS);
|
|
217
|
+
};
|
|
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 [];
|
|
227
|
+
});
|
|
228
|
+
if (!watchedFolders.length) return async () => {};
|
|
229
|
+
const watcher = watch(watchedFolders.map(({ dir }) => dir), { ignoreInitial: true });
|
|
230
|
+
watcher.on("all", (_event, path) => {
|
|
231
|
+
const { sourceFolder } = watchedFolders.find(({ dir }) => {
|
|
232
|
+
return path === dir || path.startsWith(posix.join(dir, "/"));
|
|
233
|
+
}) || {};
|
|
234
|
+
if (sourceFolder) schedule(sourceFolder, path);
|
|
235
|
+
});
|
|
236
|
+
watcher.on("error", (error) => {
|
|
237
|
+
console.error(styleText("red", "watcher failed"));
|
|
238
|
+
console.error(error);
|
|
239
|
+
});
|
|
240
|
+
for (const { dir } of watchedFolders) log(`watching ${styleText("blue", relative(root, dir))}`);
|
|
241
|
+
startRunner();
|
|
242
|
+
const teardown = async () => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
await watcher.close();
|
|
245
|
+
await stopRunner();
|
|
246
|
+
};
|
|
247
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, async () => {
|
|
248
|
+
await teardown();
|
|
249
|
+
process.exit(0);
|
|
250
|
+
});
|
|
251
|
+
return teardown;
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/chassis.ts
|
|
255
|
+
var chassis_default = async (projectSettings) => {
|
|
256
|
+
const { devPort, command } = projectSettings;
|
|
257
|
+
for (const sourceFolder of projectSettings.sourceFolders) for (const generator of sourceFolder.generators) {
|
|
258
|
+
if (!generator.meta?.name || typeof generator.factory !== "function") throw new Error(`${sourceFolder.name}: Unrecognized generator - must be created via defineGenerator()`);
|
|
259
|
+
try {
|
|
260
|
+
await generator.factory(sourceFolder).seed();
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error(styleText("red", `${sourceFolder.name}: ${generator.meta.name} generator failed to seed`));
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (command === "build" || command === "preview") {
|
|
267
|
+
for (const sourceFolder of projectSettings.sourceFolders) {
|
|
268
|
+
await buildSourceFolder(sourceFolder);
|
|
269
|
+
if (sourceFolder.config.frontend || sourceFolder.config.backend) await writeFolderManifest(sourceFolder);
|
|
270
|
+
}
|
|
271
|
+
await deployRunner(projectSettings);
|
|
272
|
+
if (command === "build") return async () => {};
|
|
273
|
+
return previewFactory(projectSettings, buildSourceFolder);
|
|
274
|
+
}
|
|
275
|
+
const requestHandlers = [];
|
|
276
|
+
const teardownHandlers = [];
|
|
277
|
+
const eventMap = {};
|
|
278
|
+
for (const sourceFolder of projectSettings.sourceFolders) if (sourceFolder.config.frontend || sourceFolder.config.backend) eventMap[sourceFolder.name] = await eventFactory(sourceFolder);
|
|
279
|
+
let port = await findFreePort(devPort);
|
|
280
|
+
for (const sourceFolder of projectSettings.sourceFolders) {
|
|
281
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
282
|
+
const { frontend, backend, sidecar } = sourceFolder.config;
|
|
283
|
+
const requestMatchers = matchersFactory(sourceFolder);
|
|
284
|
+
const plugins = [
|
|
285
|
+
vitePlugins.tsconfigPaths(sourceFolder),
|
|
286
|
+
vitePlugins.nodePrefix(),
|
|
287
|
+
vitePlugins.virtualModules(sourceFolder, {
|
|
288
|
+
kind: "csr",
|
|
289
|
+
command
|
|
290
|
+
})
|
|
291
|
+
];
|
|
292
|
+
if (frontend) {
|
|
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
|
|
297
|
+
}), {
|
|
298
|
+
base: frontend.base,
|
|
299
|
+
root: createPath.src(),
|
|
300
|
+
cacheDir: cacheDir(sourceFolder, command, "client"),
|
|
301
|
+
plugins,
|
|
302
|
+
server: {
|
|
303
|
+
port: port++,
|
|
304
|
+
middlewareMode: true,
|
|
305
|
+
hmr: { port: port++ }
|
|
306
|
+
}
|
|
307
|
+
}));
|
|
308
|
+
for (const [evt, handler] of Object.entries(eventMap[sourceFolder.name])) viteServer.watcher.on(evt, handler);
|
|
309
|
+
requestHandlers.push([
|
|
310
|
+
frontend.base,
|
|
311
|
+
() => requestMatchers.frontend,
|
|
312
|
+
() => viteServer.middlewares
|
|
313
|
+
]);
|
|
314
|
+
teardownHandlers.push(viteServer.close);
|
|
315
|
+
}
|
|
316
|
+
if (backend) {
|
|
317
|
+
const generator = sourceFolder.generators.find((e) => e.meta.slot === "backend");
|
|
318
|
+
const viteServer = await createServer(mergeConfigs(backend.viteConfig, generator?.factory(sourceFolder).viteConfig?.({
|
|
319
|
+
kind: "backend",
|
|
320
|
+
command
|
|
321
|
+
}), {
|
|
322
|
+
root: createPath.src(),
|
|
323
|
+
appType: "custom",
|
|
324
|
+
cacheDir: cacheDir(sourceFolder, command, "backend"),
|
|
325
|
+
plugins,
|
|
326
|
+
server: {
|
|
327
|
+
port: port++,
|
|
328
|
+
middlewareMode: true,
|
|
329
|
+
hmr: { port: port++ }
|
|
330
|
+
},
|
|
331
|
+
resolve: { conditions: ["node"] },
|
|
332
|
+
environments: { backend: { resolve: { conditions: ["node"] } } }
|
|
333
|
+
}));
|
|
334
|
+
const env = viteServer.environments.backend;
|
|
335
|
+
const loadDevSetup = async () => {
|
|
336
|
+
env.runner.clearCache();
|
|
337
|
+
return env.runner.import(join(defaults.apiDir, "dev.ts")).then((e) => e.default);
|
|
338
|
+
};
|
|
339
|
+
let devSetup = await loadDevSetup();
|
|
340
|
+
for (const [evt, handler] of Object.entries(eventMap[sourceFolder.name])) viteServer.watcher.on(evt, async (file) => {
|
|
341
|
+
if (env.moduleGraph.getModulesByFile(file)?.size) try {
|
|
342
|
+
await handler(file);
|
|
343
|
+
await devSetup?.teardownHandler?.();
|
|
344
|
+
devSetup = await loadDevSetup();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(styleText("red", `${sourceFolder.name}: backend reload failed`));
|
|
347
|
+
console.error(error);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
requestHandlers.push([
|
|
351
|
+
backend.base,
|
|
352
|
+
() => devSetup.requestMatcher || requestMatchers.backend,
|
|
353
|
+
() => devSetup.requestHandler()
|
|
354
|
+
]);
|
|
355
|
+
teardownHandlers.push(viteServer.close);
|
|
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
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Sorting is essential to ensure more specific paths are matched before broader ones.
|
|
425
|
+
*
|
|
426
|
+
* Correct sort order:
|
|
427
|
+
* 1. /admin/api - 10 + 3, most specific
|
|
428
|
+
* 3. /admin - 6 + 2
|
|
429
|
+
* 2. /api - 4 + 2
|
|
430
|
+
* 4. / - 1 + 2
|
|
431
|
+
* */
|
|
432
|
+
const requestHandlerWeight = ([path]) => {
|
|
433
|
+
return path.length + path.split("/").filter(Boolean).length;
|
|
434
|
+
};
|
|
435
|
+
const handlers = requestHandlers.sort((a, b) => requestHandlerWeight(b) - requestHandlerWeight(a));
|
|
436
|
+
const httpServer = http.createServer((req, res) => {
|
|
437
|
+
for (const [, matcherFactory, handlerFactory] of handlers) {
|
|
438
|
+
const [matcher, handler] = [matcherFactory(), handlerFactory()];
|
|
439
|
+
if (matcher(req)) {
|
|
440
|
+
handler(req, res);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
445
|
+
res.end("<h1>404: Not Found</h1>");
|
|
446
|
+
});
|
|
447
|
+
httpServer.on("error", (error) => {
|
|
448
|
+
console.error(styleText("red", `Failed to start dev server on port ${devPort}`));
|
|
449
|
+
console.error(error.message);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
});
|
|
452
|
+
httpServer.listen(devPort);
|
|
453
|
+
teardownHandlers.push(async () => {
|
|
454
|
+
httpServer.close();
|
|
455
|
+
});
|
|
456
|
+
return async () => {
|
|
457
|
+
for (const handler of teardownHandlers) await handler();
|
|
458
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
var cacheDir = ({ root, name }, command, mode) => {
|
|
462
|
+
return resolve(root, `var/.vite/${name}/${command}/${mode}`);
|
|
109
463
|
};
|
|
110
|
-
|
|
111
464
|
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
465
|
+
* Production build of one source folder - generators, client bundle, api bundle,
|
|
466
|
+
* then the generators' post-build steps (ssr, ssg).
|
|
467
|
+
* Shared by `build` and `preview`: preview is a production build that reruns on change,
|
|
468
|
+
* so generators are told `command: "build"` and the build cache is the same.
|
|
469
|
+
* */
|
|
470
|
+
var buildSourceFolder = async (sourceFolder) => {
|
|
471
|
+
const command = "build";
|
|
472
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
473
|
+
const { frontend, backend, sidecar } = sourceFolder.config;
|
|
474
|
+
const resolvedRoutes = [];
|
|
475
|
+
if (!sidecar) {
|
|
476
|
+
const { resolvers } = await routesFactory(sourceFolder, cacheFactory);
|
|
477
|
+
const spinner = spinnerFactory(`${sourceFolder.name}: resolving routes`);
|
|
478
|
+
for (const { name, handler } of resolvers.values()) {
|
|
479
|
+
spinner.append(`[ ${resolvedRoutes.length + 1} of ${resolvers.size} ] ${name}`);
|
|
480
|
+
resolvedRoutes.push(await handler());
|
|
481
|
+
}
|
|
482
|
+
spinner.succeed("ready ✨");
|
|
483
|
+
}
|
|
484
|
+
const plugins = [
|
|
485
|
+
vitePlugins.tsconfigPaths(sourceFolder),
|
|
486
|
+
vitePlugins.nodePrefix(),
|
|
487
|
+
vitePlugins.virtualModules(sourceFolder, {
|
|
488
|
+
kind: "csr",
|
|
489
|
+
command
|
|
490
|
+
})
|
|
491
|
+
];
|
|
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",
|
|
497
|
+
command
|
|
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
|
+
}
|
|
510
|
+
if (backend) {
|
|
511
|
+
const generator = sourceFolder.generators.find((e) => e.meta.slot === "backend");
|
|
512
|
+
await build(mergeConfigs(backend.viteConfig, generator?.factory(sourceFolder).viteConfig?.({
|
|
513
|
+
kind: "backend",
|
|
514
|
+
command
|
|
515
|
+
}), {
|
|
516
|
+
base: "./",
|
|
517
|
+
root: createPath.src(),
|
|
518
|
+
appType: "custom",
|
|
519
|
+
plugins,
|
|
520
|
+
resolve: { conditions: ["node"] },
|
|
521
|
+
build: {
|
|
522
|
+
ssr: true,
|
|
523
|
+
target: "esnext",
|
|
524
|
+
sourcemap: true,
|
|
525
|
+
emptyOutDir: true,
|
|
526
|
+
rolldownOptions: {
|
|
527
|
+
input: [
|
|
528
|
+
createPath.api("app.ts"),
|
|
529
|
+
createPath.api("server.ts"),
|
|
530
|
+
createPath.lib("@api/listener.ts")
|
|
531
|
+
],
|
|
532
|
+
output: {
|
|
533
|
+
dir: createPath.distDir("api"),
|
|
534
|
+
format: "esm"
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
cacheDir: cacheDir(sourceFolder, command, "backend")
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
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);
|
|
177
566
|
};
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
567
|
+
var eventFactory = async (sourceFolder) => {
|
|
568
|
+
const { resolvers, resolversFactory } = await routesFactory(sourceFolder, cacheFactory);
|
|
569
|
+
const { createPath } = pathResolver(sourceFolder);
|
|
570
|
+
const generators = [];
|
|
571
|
+
for (const generator of sourceFolder.generators) {
|
|
572
|
+
const factory = generator.factory(sourceFolder);
|
|
573
|
+
generators.push({
|
|
574
|
+
name: generator.meta.name,
|
|
575
|
+
factory
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
const resolvedRoutes = /* @__PURE__ */ new Map();
|
|
579
|
+
const runGenerators = async (event) => {
|
|
580
|
+
/**
|
|
581
|
+
* Watch handlers receive the full list of entries
|
|
582
|
+
* and should process only those whose source file or dependencies were updated.
|
|
583
|
+
* */
|
|
584
|
+
const entries = Array.from(resolvedRoutes.values());
|
|
585
|
+
for (const { name, factory } of generators) try {
|
|
586
|
+
await factory.watch?.(entries, event);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error(styleText("red", `${sourceFolder.name}: ${name} generator failed`));
|
|
589
|
+
if (event) console.error(event);
|
|
590
|
+
console.error(error);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const updateResolvedEntry = async (file) => {
|
|
594
|
+
const resolver = resolvers.get(file);
|
|
595
|
+
if (!resolver) return;
|
|
596
|
+
try {
|
|
597
|
+
const resolvedEntry = await resolver.handler(file);
|
|
598
|
+
resolvedRoutes.set(resolvedEntry.entry.fileFullpath, resolvedEntry);
|
|
599
|
+
return resolvedEntry;
|
|
600
|
+
} catch (error) {
|
|
601
|
+
const route = file.replace(`${createPath.api()}/`, "");
|
|
602
|
+
console.error(styleText("red", `${sourceFolder.name}: ${route} route resolution failed`));
|
|
603
|
+
console.error(error);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
{
|
|
608
|
+
const spinner = spinnerFactory(`${sourceFolder.name}: resolving routes`);
|
|
609
|
+
for (const { name, handler } of resolvers.values()) {
|
|
610
|
+
spinner.append(`[ ${resolvedRoutes.size + 1} of ${resolvers.size} ] ${name}`);
|
|
611
|
+
const route = await handler();
|
|
612
|
+
resolvedRoutes.set(route.entry.fileFullpath, route);
|
|
613
|
+
}
|
|
614
|
+
spinner.succeed("ready ✨");
|
|
615
|
+
}
|
|
616
|
+
await runGenerators();
|
|
617
|
+
return {
|
|
618
|
+
async add(file) {
|
|
619
|
+
const [resolver] = resolversFactory([file]).values();
|
|
620
|
+
if (!resolver) return;
|
|
621
|
+
resolvers.set(file, resolver);
|
|
622
|
+
if (await updateResolvedEntry(file)) await runGenerators({
|
|
623
|
+
kind: "create",
|
|
624
|
+
file
|
|
625
|
+
});
|
|
626
|
+
},
|
|
627
|
+
async change(file) {
|
|
628
|
+
if (resolvedRoutes.has(file)) await updateResolvedEntry(file);
|
|
629
|
+
else {
|
|
630
|
+
const relatedRoutes = resolvedRoutes.values().flatMap(({ kind, entry }) => {
|
|
631
|
+
return kind === "apiRoute" ? entry.referencedFiles.includes(file) ? [entry] : [] : [];
|
|
632
|
+
});
|
|
633
|
+
for (const route of relatedRoutes) await updateResolvedEntry(route.fileFullpath);
|
|
634
|
+
}
|
|
635
|
+
await runGenerators({
|
|
636
|
+
kind: "update",
|
|
637
|
+
file
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
async unlink(file) {
|
|
641
|
+
resolvers.delete(file);
|
|
642
|
+
resolvedRoutes.delete(file);
|
|
643
|
+
await runGenerators({
|
|
644
|
+
kind: "delete",
|
|
645
|
+
file
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
};
|
|
224
649
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
res.writeHead(404, { "Content-Type": "text/html" });
|
|
250
|
-
res.end("<h1>404: Not Found</h1>");
|
|
251
|
-
};
|
|
650
|
+
var matchersFactory = ({ config }) => {
|
|
651
|
+
const [frontendBase, backendBase] = [config.frontend?.base, config.backend?.base];
|
|
652
|
+
const backendAliasPatterns = backendBase ? createAliasPatterns(config.backend?.alias).map((pattern) => {
|
|
653
|
+
return pathToRegexp(posix.join("/", pattern)).regexp;
|
|
654
|
+
}) : [];
|
|
655
|
+
const prefixMatch = (base, path) => {
|
|
656
|
+
return path === base || path.startsWith(posix.join(base, "/"));
|
|
657
|
+
};
|
|
658
|
+
const backendMatch = (path) => {
|
|
659
|
+
if (!backendBase) return false;
|
|
660
|
+
return prefixMatch(backendBase, path) ? true : backendAliasPatterns.some((r) => r.test(path) || false);
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
frontend(req) {
|
|
664
|
+
if (!frontendBase) return false;
|
|
665
|
+
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
666
|
+
return backendMatch(pathname) ? false : prefixMatch(frontendBase, pathname);
|
|
667
|
+
},
|
|
668
|
+
backend(req) {
|
|
669
|
+
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
670
|
+
return backendMatch(pathname);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
252
673
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
server.listen(sock || Number(port), async () => {
|
|
280
|
-
if (sock) {
|
|
281
|
-
// let a reverse proxy running as another user connect
|
|
282
|
-
await chmod(sock, 0o777);
|
|
283
|
-
}
|
|
284
|
-
console.log("\\n -> Server Started\\n");
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
return server;
|
|
674
|
+
var findFreePort = async (devPort) => {
|
|
675
|
+
let minPort = 0;
|
|
676
|
+
let maxPort = 0;
|
|
677
|
+
for (const n of [
|
|
678
|
+
3,
|
|
679
|
+
2,
|
|
680
|
+
1,
|
|
681
|
+
""
|
|
682
|
+
]) {
|
|
683
|
+
minPort = Number(`${n}${devPort}`) + 100;
|
|
684
|
+
maxPort = minPort + 100;
|
|
685
|
+
if (maxPort < 65e3) break;
|
|
686
|
+
}
|
|
687
|
+
if (maxPort >= 65e3) throw new Error("the devPort in package.json should be less than 64000");
|
|
688
|
+
const range = maxPort - minPort + 1;
|
|
689
|
+
const startOffset = Math.floor(Math.random() * range);
|
|
690
|
+
const freePort = await Array.from({ length: range }, (_, i) => {
|
|
691
|
+
return minPort + (startOffset + i) % range;
|
|
692
|
+
}).reduce(async (prevPromise, port) => {
|
|
693
|
+
const freePort = await prevPromise;
|
|
694
|
+
if (freePort) return freePort;
|
|
695
|
+
return await isPortFree(port) ? port : void 0;
|
|
696
|
+
}, Promise.resolve(void 0));
|
|
697
|
+
if (!freePort) throw new Error(`No free ports found in range ${minPort}-${maxPort}`);
|
|
698
|
+
return freePort;
|
|
288
699
|
};
|
|
700
|
+
var isPortFree = (port) => {
|
|
701
|
+
return new Promise((resolve) => {
|
|
702
|
+
const server = net.createServer();
|
|
703
|
+
server.once("error", () => resolve(false));
|
|
704
|
+
server.once("listening", () => {
|
|
705
|
+
server.close();
|
|
706
|
+
resolve(true);
|
|
707
|
+
});
|
|
708
|
+
server.listen(port, "127.0.0.1");
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
//#endregion
|
|
712
|
+
export { chassis_default as default };
|
|
289
713
|
|
|
290
|
-
if (pathToFileURL(process.argv[1] || "").href === import.meta.url) {
|
|
291
|
-
const {
|
|
292
|
-
values: { port, sock },
|
|
293
|
-
} = parseArgs({
|
|
294
|
-
options: {
|
|
295
|
-
port: { type: "string", short: "p" },
|
|
296
|
-
sock: { type: "string", short: "s" },
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
await startServer({ port, sock });
|
|
302
|
-
} catch (error) {
|
|
303
|
-
console.error("Failed starting server");
|
|
304
|
-
console.error(error);
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
`,P=e=>{let{generators:t}=e.config,n=e=>t.some(t=>t.meta.slot===e),r=n(`frontend`);return{name:e.name,base:e.config.base,apiBase:e.config.apiBase,api:n(`backend`),client:r,ssr:r&&n(`ssr`)}},F=async e=>{let{createPath:t}=f(e),n=t.distDir();await x(n,{recursive:!0}),await C(a(n,`kosmo.json`),JSON.stringify(P(e),void 0,2),`utf8`)},I=({root:e,distDir:t})=>a(e,t,`run.js`),L,R=async()=>{if(!L){let{code:e}=await b(N,`run.ts`,{sourcemap:!1});L=e}return L},z=async e=>{let t=I(e);return await x(a(t,`..`),{recursive:!0}),await C(t,await R(),`utf8`),t},B=300,V=/(^\.|~$|\.swp$|\.tmp$)/,H=async(e,n)=>{let{root:r,previewPort:a,sourceFolders:c}=e,l=I(e),u=(...e)=>{console.log(s(`dim`,`[preview]`),...e)},d,p=()=>{let e=D(process.execPath,[`--enable-source-maps`,l,`--port`,String(a)],{cwd:r,stdio:`inherit`});e.on(`exit`,(t,n)=>{d===e&&(d=void 0,u(s(`red`,`runner exited (${n??t})`)))}),d=e},m=()=>new Promise(e=>{let t=d;if(!t||t.exitCode!==null){d=void 0,e();return}d=void 0,t.once(`exit`,()=>e()),t.kill(`SIGTERM`)}),h=async()=>{await m(),p()},g=new Set,_=!1,v,y=async()=>{if(!_){_=!0;try{for(;g.size;){let t=[...g];g.clear();let r=!1;for(let e of t){u(`rebuilding ${s(`blue`,e.name)}`);try{await n(e),await F(e)}catch(t){r=!0,console.error(s(`red`,`${e.name}: build failed`)),console.error(t)}}await z(e),r?u(s(`yellow`,`keeping previous build running`)):await h()}}finally{_=!1}}},b=(e,n)=>{process.env.DEBUG&&u(`event ${e.name}: ${n} (building: ${_})`),!V.test(t(n))&&(g.add(e),clearTimeout(v),v=setTimeout(y,B))},x=c.map(e=>{let{createPath:t}=f(e);return{dir:t.src(),sourceFolder:e}}),S=e=>x.find(({dir:t})=>e===t||e.startsWith(t+o))?.sourceFolder,C=O(x.map(({dir:e})=>e),{ignoreInitial:!0});C.on(`all`,(e,t)=>{let n=S(t);n&&b(n,t)}),C.on(`error`,e=>{console.error(s(`red`,`watcher failed`)),console.error(e)});for(let{dir:e}of x)u(`watching ${s(`blue`,i(r,e))}`);p();let w=async()=>{clearTimeout(v),await C.close(),await m()};for(let e of[`SIGINT`,`SIGTERM`])process.once(e,async()=>{await w(),process.exit(0)});return w},U=async e=>{let{devPort:t,command:n}=e;for(let t of e.sourceFolders)for(let e of t.config.generators){if(!e.meta?.name||typeof e.factory!=`function`)throw Error(`${t.name}: Unrecognized generator - must be created via defineGenerator()`);try{await e.factory(t).start?.()}catch(n){throw console.error(s(`red`,`${t.name}: ${e.meta.name} generator failed to initialize`)),n}}if(n===`build`||n===`preview`){for(let t of e.sourceFolders)await G(t),await F(t);return await z(e),n===`build`?async()=>{}:H(e,G)}let i=[],a=[],o={};for(let t of e.sourceFolders)o[t.name]=await K(t);let d=await Y(t);for(let t of e.sourceFolders){let{createPath:e}=f(t),{generators:p}=t.config,m=J(t),h=[g.tsconfigPaths(t),g.nodePrefix(),g.virtualModules(l(t,p),{kind:`csr`,command:n})],_=p.find(e=>e.meta.slot===`frontend`),v=p.find(e=>e.meta.slot===`backend`);if(_){let r=await y(u(t.config,...p.map(({factory:e})=>e(t).config?.({kind:`client`,command:n})),{root:e.src(),cacheDir:W(t,n,`client`),plugins:h,server:{port:d++,middlewareMode:!0,hmr:{port:d++}}}));for(let[e,n]of Object.entries(o[t.name]))r.watcher.on(e,n);i.push([[t.config.base],()=>m.base,()=>r.middlewares]),a.push(r.close)}if(v){let l=await y(u(v.options,v.factory(t).config?.({kind:`backend`,command:n}),{root:e.src(),appType:`custom`,cacheDir:W(t,n,`backend`),plugins:h,server:{port:d++,middlewareMode:!0,hmr:{port:d++}},resolve:{conditions:[`node`]},environments:{api:{resolve:{conditions:[`node`]}}}})),f=l.environments.api,p=async()=>(f.runner.clearCache(),f.runner.import(r(c.apiDir,`dev.ts`)).then(e=>e.default)),g=await p();for(let[e,n]of Object.entries(o[t.name]))l.watcher.on(e,async e=>{if(f.moduleGraph.getModulesByFile(e)?.size)try{await n(e),await g?.teardownHandler?.(),g=await p()}catch(e){console.error(s(`red`,`${t.name}: api reload failed - keeping previous api program`)),console.error(e)}});i.push([[t.config.base,t.config.apiBase],()=>g.requestMatcher||m.api,()=>g.requestHandler()]),a.push(l.close)}}let p=([e])=>{let[t,n]=e,r=t.split(`/`).filter(Boolean).length;return n?r+5:r},m=i.sort((e,t)=>p(t)-p(e)),h=w.createServer((e,t)=>{for(let[,n,r]of m){let[i,a]=[n(),r()];if(i(e)){a(e,t);return}}t.writeHead(404,{"Content-Type":`text/html`}),t.end(`<h1>404: Not Found</h1>`)});return h.on(`error`,e=>{console.error(s(`red`,`Failed to start dev server on port ${t}`)),console.error(e.message),process.exit(1)}),h.listen(t),a.push(async()=>{h.close()}),async()=>{for(let e of a)await e();await new Promise(e=>setTimeout(e,100))}},W=({root:e,name:t},n,r)=>a(e,`var/.vite/${t}/${n}/${r}`),G=async e=>{let t=`build`,{createPath:n}=f(e),{generators:r}=e.config,i=[];{let{resolvers:t}=await m(e,A),n=h(`${e.name}: resolving routes`);for(let{name:e,handler:r}of t.values())n.append(`[ ${i.length+1} of ${t.size} ] ${e}`),i.push(await r());n.succeed(`ready ✨`)}let a=[g.tsconfigPaths(e),g.nodePrefix(),g.virtualModules(l(e,r),{kind:`csr`,command:t})];for(let t of r)await t.factory(e).build?.(i);let o=r.find(e=>e.meta.slot===`frontend`),s=r.find(e=>e.meta.slot===`backend`);if(o&&await v(u(e.config,...r.map(({factory:n})=>n(e).config?.({kind:`client`,command:t})),{root:n.src(),cacheDir:W(e,t,`client`),plugins:a,build:{outDir:n.distDir(`client`),manifest:!0,emptyOutDir:!0}})),s){let r=n.distDir(`api`);await v(u(s.options,s.factory(e).config?.({kind:`backend`,command:t}),{base:`./`,root:n.src(),appType:`custom`,plugins:a,resolve:{conditions:[`node`]},build:{ssr:!0,target:`esnext`,sourcemap:!0,emptyOutDir:!0,rolldownOptions:{input:[n.api(`app.ts`),n.api(`server.ts`),n.lib(`@api/listener.ts`)],output:{dir:r,format:`esm`}}},cacheDir:W(e,t,`backend`)}))}for(let t of r)await t.factory(e).postBuild?.(i)},K=async e=>{let{resolvers:t,resolversFactory:n}=await m(e,A),{createPath:r}=f(e),i=[];for(let t of e.config.generators){let n=t.factory(e);i.push({name:t.meta.name,factory:n})}let a=new Map,o=async t=>{let n=Array.from(a.values());for(let{name:r,factory:a}of i)try{await a.watch?.(n,t)}catch(n){console.error(s(`red`,`${e.name}: ${r} generator failed`)),t&&console.error(t),console.error(n)}},c=async n=>{let i=t.get(n);if(i)try{let e=await i.handler(n);return a.set(e.entry.fileFullpath,e),e}catch(t){let i=n.replace(`${r.api()}/`,``);console.error(s(`red`,`${e.name}: ${i} route resolution failed`)),console.error(t);return}};{let n=h(`${e.name}: resolving routes`);for(let{name:e,handler:r}of t.values()){n.append(`[ ${a.size+1} of ${t.size} ] ${e}`);let i=await r();a.set(i.entry.fileFullpath,i)}n.succeed(`ready ✨`)}return await o(),{async add(e){let[r]=n([e]).values();r&&(t.set(e,r),await c(e)&&await o({kind:`create`,file:e}))},async change(e){if(a.has(e))await c(e);else{let t=a.values().flatMap(({kind:t,entry:n})=>t===`apiRoute`&&n.referencedFiles.includes(e)?[n]:[]);for(let e of t)await c(e.fileFullpath)}await o({kind:`update`,file:e})},async unlink(e){t.delete(e),a.delete(e),await o({kind:`delete`,file:e})}}},q=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`).replace(/\/+$/,``),J=({config:e})=>{let t=RegExp(`^${q(e.base)}(?=$|/)`),n=RegExp(`^${q(r(e.base,e.apiBase))}(?=$|/)`);return{base(e){return!n.test(e.url)&&t.test(e.url)},api(e){return n.test(e.url)}}},Y=async e=>{let t=0,n=0;for(let r of[3,2,1,``])if(t=Number(`${r}${e}`)+100,n=t+100,n<65e3)break;if(n>=65e3)throw Error(`the devPort in package.json should be less than 64000`);let r=n-t+1,i=Math.floor(Math.random()*r),a=await Array.from({length:r},(e,n)=>t+(i+n)%r).reduce(async(e,t)=>await e||(await X(t)?t:void 0),Promise.resolve(void 0));if(!a)throw Error(`No free ports found in range ${t}-${n}`);return a},X=e=>new Promise(t=>{let n=T.createServer();n.once(`error`,()=>t(!1)),n.once(`listening`,()=>{n.close(),t(!0)}),n.listen(e,`127.0.0.1`)});export{U as default};
|
|
309
714
|
//# sourceMappingURL=chassis.js.map
|