@znight/cli 0.6.2 → 0.6.4
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 +58 -25
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +17 -6
- package/dist/index.mjs +1 -1
- package/dist/{main-DUyElIVZ.mjs → main-CLoS12u4.mjs} +452 -75
- package/dist/main-CLoS12u4.mjs.map +1 -0
- package/package.json +7 -7
- package/src/commands/dev.ts +23 -6
- package/src/commands/generate.ts +36 -27
- package/src/commands/new.ts +41 -8
- package/src/commands/start.ts +7 -2
- package/src/dev/supervisor.ts +38 -14
- package/src/scaffold/options.ts +37 -5
- package/src/scaffold/templates.ts +379 -24
- package/src/scaffold/versions.ts +2 -0
- package/dist/main-DUyElIVZ.mjs.map +0 -1
|
@@ -1241,6 +1241,12 @@ var AppSupervisor = class {
|
|
|
1241
1241
|
#debounce;
|
|
1242
1242
|
#queue = Promise.resolve();
|
|
1243
1243
|
#queued = false;
|
|
1244
|
+
/** A restart is stopping the previous process before starting the next one. */
|
|
1245
|
+
#restarting = false;
|
|
1246
|
+
/** Bumped by {@link cancelRestart}: a restart requested in an older epoch does not start. */
|
|
1247
|
+
#epoch = 0;
|
|
1248
|
+
/** Epoch of the latest restart request. */
|
|
1249
|
+
#requested = 0;
|
|
1244
1250
|
#closed = false;
|
|
1245
1251
|
#started = 0;
|
|
1246
1252
|
constructor(options) {
|
|
@@ -1271,14 +1277,18 @@ var AppSupervisor = class {
|
|
|
1271
1277
|
}, this.#debounceMs);
|
|
1272
1278
|
}
|
|
1273
1279
|
/**
|
|
1274
|
-
* Cancels
|
|
1275
|
-
*
|
|
1280
|
+
* Cancels the restarts that did not start their process yet (the output they would run turned out
|
|
1281
|
+
* to be broken, e.g. a failed compilation): scheduled, queued, or stopping the previous process
|
|
1282
|
+
* (which still stops). Returns whether one was pending.
|
|
1276
1283
|
*/
|
|
1277
1284
|
cancelRestart() {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1285
|
+
const pending = this.#debounce !== void 0 || this.#queued || this.#restarting;
|
|
1286
|
+
if (this.#debounce !== void 0) {
|
|
1287
|
+
clearTimeout(this.#debounce);
|
|
1288
|
+
this.#debounce = void 0;
|
|
1289
|
+
}
|
|
1290
|
+
this.#epoch++;
|
|
1291
|
+
return pending;
|
|
1282
1292
|
}
|
|
1283
1293
|
/**
|
|
1284
1294
|
* Stops the current process (if any) and starts a new one. Restarts never overlap; a restart
|
|
@@ -1286,13 +1296,20 @@ var AppSupervisor = class {
|
|
|
1286
1296
|
*/
|
|
1287
1297
|
restart() {
|
|
1288
1298
|
if (this.#closed) return this.#queue;
|
|
1299
|
+
this.#requested = this.#epoch;
|
|
1289
1300
|
if (this.#queued) return this.#queue;
|
|
1290
1301
|
this.#queued = true;
|
|
1291
1302
|
this.#queue = this.#queue.then(async () => {
|
|
1292
1303
|
this.#queued = false;
|
|
1293
|
-
if (this.#closed) return;
|
|
1294
|
-
|
|
1295
|
-
|
|
1304
|
+
if (this.#closed || this.#requested !== this.#epoch) return;
|
|
1305
|
+
const epoch = this.#epoch;
|
|
1306
|
+
this.#restarting = true;
|
|
1307
|
+
try {
|
|
1308
|
+
await this.#stopCurrent("SIGTERM");
|
|
1309
|
+
} finally {
|
|
1310
|
+
this.#restarting = false;
|
|
1311
|
+
}
|
|
1312
|
+
if (this.#closed || epoch !== this.#epoch) return;
|
|
1296
1313
|
this.#startNew();
|
|
1297
1314
|
}).catch((error) => {
|
|
1298
1315
|
this.#queued = false;
|
|
@@ -1300,7 +1317,11 @@ var AppSupervisor = class {
|
|
|
1300
1317
|
});
|
|
1301
1318
|
return this.#queue;
|
|
1302
1319
|
}
|
|
1303
|
-
/**
|
|
1320
|
+
/**
|
|
1321
|
+
* Stops the current process with `signal` (SIGKILL after the timeout) and refuses new starts.
|
|
1322
|
+
* `null`: the process already received the signal (a console Ctrl+C on Windows, where `kill()` is
|
|
1323
|
+
* a forced termination): it is only waited for, then SIGKILLed after the timeout.
|
|
1324
|
+
*/
|
|
1304
1325
|
async stop(signal = "SIGTERM") {
|
|
1305
1326
|
this.#closed = true;
|
|
1306
1327
|
if (this.#debounce !== void 0) {
|
|
@@ -1341,7 +1362,7 @@ var AppSupervisor = class {
|
|
|
1341
1362
|
this.#current = void 0;
|
|
1342
1363
|
this.#stopping.add(child);
|
|
1343
1364
|
try {
|
|
1344
|
-
child.kill(signal);
|
|
1365
|
+
if (signal !== null) child.kill(signal);
|
|
1345
1366
|
let timer;
|
|
1346
1367
|
const timeout = new Promise((resolve) => {
|
|
1347
1368
|
timer = setTimeout(() => resolve("timeout"), this.#killTimeoutMs);
|
|
@@ -1817,18 +1838,33 @@ async function printDiagnostics$2(cli, root, diagnostics) {
|
|
|
1817
1838
|
}
|
|
1818
1839
|
cli.reporter.logError(text.trimEnd());
|
|
1819
1840
|
}
|
|
1841
|
+
/**
|
|
1842
|
+
* The signal stopping the application when the CLI received `signal`. On Windows the application
|
|
1843
|
+
* shares the console: it received Ctrl+C and the console closing itself, and `kill()` would be a
|
|
1844
|
+
* forced termination, so it is only waited for (`null`).
|
|
1845
|
+
*/
|
|
1846
|
+
function stopSignal(cli, signal) {
|
|
1847
|
+
if (cli.runtime.platform === "win32" && (signal === "SIGINT" || signal === "SIGHUP")) return null;
|
|
1848
|
+
return signal === "SIGINT" ? "SIGINT" : "SIGTERM";
|
|
1849
|
+
}
|
|
1820
1850
|
function waitForTermination(cli, watcher, supervisor) {
|
|
1821
1851
|
return new Promise((resolveExit) => {
|
|
1822
1852
|
const dispose = onTermination(cli, (signal) => {
|
|
1823
1853
|
if (signal !== "SIGHUP") cli.reporter.info(cli.colors.dim("Stopping... (press Ctrl+C again to force)"));
|
|
1824
1854
|
(async () => {
|
|
1855
|
+
const stopped = supervisor.stop(stopSignal(cli, signal));
|
|
1825
1856
|
try {
|
|
1826
1857
|
await watcher.close();
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
cli.reporter.error(`Cannot close the Vite build watcher: ${errorMessage(error)}`);
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
await stopped;
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
cli.reporter.error(`Cannot stop the application: ${errorMessage(error)}`);
|
|
1831
1865
|
}
|
|
1866
|
+
dispose();
|
|
1867
|
+
resolveExit(0);
|
|
1832
1868
|
})();
|
|
1833
1869
|
}, () => {
|
|
1834
1870
|
supervisor.kill();
|
|
@@ -2032,9 +2068,11 @@ async function watch$1(cli, compiler, options) {
|
|
|
2032
2068
|
roots: []
|
|
2033
2069
|
};
|
|
2034
2070
|
let pending = /* @__PURE__ */ new Set();
|
|
2035
|
-
let running
|
|
2071
|
+
let running;
|
|
2072
|
+
let closed = false;
|
|
2036
2073
|
let timer;
|
|
2037
2074
|
const watchers = new TargetWatchers(cli.services.watch, (file) => {
|
|
2075
|
+
if (closed) return;
|
|
2038
2076
|
const path = realPath(file);
|
|
2039
2077
|
if (!incremental.isRelevantChange(changeKind(path, state.known), path, state.known, state.roots)) return;
|
|
2040
2078
|
pending.add(path);
|
|
@@ -2044,42 +2082,46 @@ async function watch$1(cli, compiler, options) {
|
|
|
2044
2082
|
run(false);
|
|
2045
2083
|
}, 50);
|
|
2046
2084
|
});
|
|
2047
|
-
const run =
|
|
2048
|
-
if (running) return;
|
|
2049
|
-
running =
|
|
2085
|
+
const run = (initial) => {
|
|
2086
|
+
if (running !== void 0 || closed) return running ?? Promise.resolve();
|
|
2087
|
+
running = generations(initial).finally(() => {
|
|
2088
|
+
running = void 0;
|
|
2089
|
+
});
|
|
2090
|
+
return running;
|
|
2091
|
+
};
|
|
2092
|
+
const generations = async (initial) => {
|
|
2050
2093
|
let first = initial;
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
} finally {
|
|
2072
|
-
running = false;
|
|
2073
|
-
}
|
|
2094
|
+
do {
|
|
2095
|
+
const files = pending;
|
|
2096
|
+
pending = /* @__PURE__ */ new Set();
|
|
2097
|
+
const started = performance.now();
|
|
2098
|
+
let known = state.known;
|
|
2099
|
+
try {
|
|
2100
|
+
const result = await incremental.generate(first ? void 0 : classifyChanges(files, state.known));
|
|
2101
|
+
if (result.watchFiles !== void 0) known = new Set(result.watchFiles.map((file) => toPosixPath(resolve(file))));
|
|
2102
|
+
report(cli, compiler, result, root, performance.now() - started);
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
cli.reporter.error(`Generation failed: ${errorMessage(error)}`);
|
|
2105
|
+
}
|
|
2106
|
+
if (closed) return;
|
|
2107
|
+
state = {
|
|
2108
|
+
known,
|
|
2109
|
+
roots: incremental.includeRoots().map(realPath)
|
|
2110
|
+
};
|
|
2111
|
+
watchers.update(watchTargets(realRoot, state.roots, state.known), files);
|
|
2112
|
+
first = false;
|
|
2113
|
+
} while (pending.size > 0 && !closed);
|
|
2074
2114
|
};
|
|
2075
2115
|
await run(true);
|
|
2076
2116
|
cli.reporter.info(cli.colors.dim("Watching for changes... (Ctrl+C to stop)"));
|
|
2077
2117
|
return new Promise((resolveExit) => {
|
|
2078
2118
|
const dispose = onTermination(cli, () => {
|
|
2119
|
+
closed = true;
|
|
2079
2120
|
if (timer !== void 0) clearTimeout(timer);
|
|
2080
2121
|
watchers.close();
|
|
2081
2122
|
dispose();
|
|
2082
|
-
resolveExit(0);
|
|
2123
|
+
const exit = () => resolveExit(0);
|
|
2124
|
+
(running ?? Promise.resolve()).then(exit, exit);
|
|
2083
2125
|
}, () => void 0);
|
|
2084
2126
|
});
|
|
2085
2127
|
}
|
|
@@ -2091,10 +2133,10 @@ const PACKAGE_MANAGERS = [
|
|
|
2091
2133
|
"pnpm",
|
|
2092
2134
|
"yarn"
|
|
2093
2135
|
];
|
|
2094
|
-
/** Every HTTP adapter, in display order. */
|
|
2136
|
+
/** Every HTTP adapter, in display order (the default first). */
|
|
2095
2137
|
const HTTP_ADAPTERS = [
|
|
2096
|
-
"node",
|
|
2097
2138
|
"fastify",
|
|
2139
|
+
"node",
|
|
2098
2140
|
"express",
|
|
2099
2141
|
"hono"
|
|
2100
2142
|
];
|
|
@@ -2104,14 +2146,18 @@ const FEATURES = [
|
|
|
2104
2146
|
"observability",
|
|
2105
2147
|
"messaging",
|
|
2106
2148
|
"schedule",
|
|
2107
|
-
"openapi"
|
|
2149
|
+
"openapi",
|
|
2150
|
+
"graphql",
|
|
2151
|
+
"websocket"
|
|
2108
2152
|
];
|
|
2153
|
+
/** Every OpenAPI reference UI, in display order (the default first). */
|
|
2154
|
+
const OPENAPI_UIS = ["scalar", "swagger"];
|
|
2109
2155
|
/** Features selected when none are given. */
|
|
2110
2156
|
const DEFAULT_FEATURES = ["config", "observability"];
|
|
2111
2157
|
/** Labels shown by the prompts and the help. */
|
|
2112
2158
|
const ADAPTER_LABELS = {
|
|
2113
|
-
node: "node:http (built-in, zero dependency)",
|
|
2114
2159
|
fastify: "Fastify",
|
|
2160
|
+
node: "node:http (built-in, zero dependency)",
|
|
2115
2161
|
express: "Express",
|
|
2116
2162
|
hono: "Hono"
|
|
2117
2163
|
};
|
|
@@ -2121,7 +2167,14 @@ const FEATURE_LABELS = {
|
|
|
2121
2167
|
observability: "Observability: structured logs, metrics, tracing (@znight/observability)",
|
|
2122
2168
|
messaging: "Messaging: message consumers and publishing (@znight/messaging)",
|
|
2123
2169
|
schedule: "Schedule: cron, interval and timeout jobs (@znight/schedule)",
|
|
2124
|
-
openapi: "OpenAPI: openapi.json and an API reference at /docs (@znight/openapi)"
|
|
2170
|
+
openapi: "OpenAPI: openapi.json and an API reference at /docs (@znight/openapi)",
|
|
2171
|
+
graphql: "GraphQL: code-first schema derived from the TypeScript types (@znight/graphql)",
|
|
2172
|
+
websocket: "WebSocket: gateways served on the HTTP port (@znight/websocket)"
|
|
2173
|
+
};
|
|
2174
|
+
/** Labels shown by the prompts. */
|
|
2175
|
+
const OPENAPI_UI_LABELS = {
|
|
2176
|
+
scalar: "Scalar",
|
|
2177
|
+
swagger: "Swagger UI"
|
|
2125
2178
|
};
|
|
2126
2179
|
/**
|
|
2127
2180
|
* Parses `--features` (`config,observability`, `none`, `all`). Returns the features in canonical
|
|
@@ -2223,11 +2276,13 @@ const DEPENDENCY_VERSIONS = {
|
|
|
2223
2276
|
"@types/node": "^24.0.0",
|
|
2224
2277
|
express: "^5.3.0",
|
|
2225
2278
|
fastify: "^5.12.7",
|
|
2279
|
+
graphql: "^17.0.2",
|
|
2226
2280
|
hono: "^4.13.14",
|
|
2227
2281
|
"light-my-request": "^6.8.0",
|
|
2228
2282
|
typescript: "^7.0.0",
|
|
2229
2283
|
vite: "^8.0.0",
|
|
2230
2284
|
vitest: "^4.1.0",
|
|
2285
|
+
ws: "^8.22.0",
|
|
2231
2286
|
zod: "^4.6.0"
|
|
2232
2287
|
};
|
|
2233
2288
|
/** Range of a third-party dependency. */
|
|
@@ -2240,11 +2295,16 @@ function versionOf(name) {
|
|
|
2240
2295
|
function flagsOf(options) {
|
|
2241
2296
|
const has = (feature) => options.features.includes(feature);
|
|
2242
2297
|
return {
|
|
2298
|
+
name: options.name,
|
|
2243
2299
|
config: has("config"),
|
|
2244
2300
|
observability: has("observability"),
|
|
2245
2301
|
messaging: has("messaging"),
|
|
2246
2302
|
schedule: has("schedule"),
|
|
2247
2303
|
openapi: has("openapi"),
|
|
2304
|
+
openapiUi: options.openapiUi ?? "scalar",
|
|
2305
|
+
graphql: has("graphql"),
|
|
2306
|
+
websocket: has("websocket"),
|
|
2307
|
+
docker: options.docker ?? true,
|
|
2248
2308
|
adapter: options.adapter,
|
|
2249
2309
|
pm: options.packageManager,
|
|
2250
2310
|
bun: options.packageManager === "bun"
|
|
@@ -2329,10 +2389,40 @@ function renderProject(options) {
|
|
|
2329
2389
|
path: "src/api.ts",
|
|
2330
2390
|
content: apiDocument(options)
|
|
2331
2391
|
});
|
|
2392
|
+
if (flags.graphql) {
|
|
2393
|
+
files.push({
|
|
2394
|
+
path: "src/app/greeting.resolver.ts",
|
|
2395
|
+
content: greetingResolver()
|
|
2396
|
+
});
|
|
2397
|
+
files.push({
|
|
2398
|
+
path: "test/greeting.resolver.test.ts",
|
|
2399
|
+
content: greetingResolverTest(flags)
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
if (flags.websocket) {
|
|
2403
|
+
files.push({
|
|
2404
|
+
path: "src/app/greeting.gateway.ts",
|
|
2405
|
+
content: greetingGateway()
|
|
2406
|
+
});
|
|
2407
|
+
files.push({
|
|
2408
|
+
path: "test/greeting.gateway.test.ts",
|
|
2409
|
+
content: greetingGatewayTest(flags)
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2332
2412
|
if (flags.bun) files.push({
|
|
2333
2413
|
path: "bunfig.toml",
|
|
2334
2414
|
content: bunfig(flags)
|
|
2335
2415
|
});
|
|
2416
|
+
if (flags.docker) {
|
|
2417
|
+
files.push({
|
|
2418
|
+
path: "Dockerfile",
|
|
2419
|
+
content: dockerfile(flags)
|
|
2420
|
+
});
|
|
2421
|
+
files.push({
|
|
2422
|
+
path: ".dockerignore",
|
|
2423
|
+
content: dockerignore()
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2336
2426
|
if (flags.pm === "yarn") files.push({
|
|
2337
2427
|
path: ".yarnrc.yml",
|
|
2338
2428
|
content: "nodeLinker: node-modules\n"
|
|
@@ -2354,6 +2444,14 @@ function packageJson(options, flags) {
|
|
|
2354
2444
|
if (flags.messaging) dependencies["@znight/messaging"] = znight;
|
|
2355
2445
|
if (flags.schedule) dependencies["@znight/schedule"] = znight;
|
|
2356
2446
|
if (flags.openapi) dependencies["@znight/openapi"] = znight;
|
|
2447
|
+
if (flags.graphql) {
|
|
2448
|
+
dependencies["@znight/graphql"] = znight;
|
|
2449
|
+
dependencies["graphql"] = versionOf("graphql");
|
|
2450
|
+
}
|
|
2451
|
+
if (flags.websocket) {
|
|
2452
|
+
dependencies["@znight/websocket"] = znight;
|
|
2453
|
+
dependencies["ws"] = versionOf("ws");
|
|
2454
|
+
}
|
|
2357
2455
|
if (flags.adapter === "fastify") dependencies["fastify"] = versionOf("fastify");
|
|
2358
2456
|
if (flags.adapter === "express") dependencies["express"] = versionOf("express");
|
|
2359
2457
|
if (flags.adapter === "hono") {
|
|
@@ -2470,6 +2568,100 @@ coverage
|
|
|
2470
2568
|
.env.*.local
|
|
2471
2569
|
`;
|
|
2472
2570
|
}
|
|
2571
|
+
function dockerSteps(pm) {
|
|
2572
|
+
const corepack = (tool) => ["ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0", `RUN corepack enable && corepack install --global ${tool}`];
|
|
2573
|
+
switch (pm) {
|
|
2574
|
+
case "bun": return {
|
|
2575
|
+
manifests: "package.json bun.lock",
|
|
2576
|
+
setup: [],
|
|
2577
|
+
install: "bun install --frozen-lockfile",
|
|
2578
|
+
production: "bun install --frozen-lockfile --production --omit=peer",
|
|
2579
|
+
build: "bun run build"
|
|
2580
|
+
};
|
|
2581
|
+
case "pnpm": return {
|
|
2582
|
+
manifests: "package.json pnpm-lock.yaml",
|
|
2583
|
+
setup: corepack("pnpm@10"),
|
|
2584
|
+
install: "pnpm install --frozen-lockfile",
|
|
2585
|
+
production: "pnpm install --frozen-lockfile --prod",
|
|
2586
|
+
build: "pnpm run build"
|
|
2587
|
+
};
|
|
2588
|
+
case "yarn": return {
|
|
2589
|
+
manifests: "package.json yarn.lock .yarnrc.yml",
|
|
2590
|
+
setup: corepack("yarn@4"),
|
|
2591
|
+
install: "yarn install --immutable",
|
|
2592
|
+
production: "yarn workspaces focus --all --production",
|
|
2593
|
+
build: "yarn build"
|
|
2594
|
+
};
|
|
2595
|
+
default: return {
|
|
2596
|
+
manifests: "package.json package-lock.json",
|
|
2597
|
+
setup: [],
|
|
2598
|
+
install: "npm ci",
|
|
2599
|
+
production: "npm pkg delete devDependencies && npm ci --omit=dev",
|
|
2600
|
+
build: "npm run build"
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
/** A multi-stage image: build with every dependency, run the bundle with the production ones. */
|
|
2605
|
+
function dockerfile(flags) {
|
|
2606
|
+
const steps = dockerSteps(flags.pm);
|
|
2607
|
+
const runtime = flags.bun ? "bun" : "node";
|
|
2608
|
+
const [baseImage, runImage] = flags.bun ? ["oven/bun:1", "oven/bun:1-slim"] : ["node:24-slim", "node:24-slim"];
|
|
2609
|
+
const probe = "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/health').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))";
|
|
2610
|
+
const command = flags.bun ? "[\"bun\", \"--no-env-file\", \"dist/main.js\"]" : "[\"node\", \"--enable-source-maps\", \"dist/main.js\"]";
|
|
2611
|
+
const envFiles = flags.bun ? "\n# --no-env-file: Bun would load .env files into process.env by itself, with its own precedence" : "";
|
|
2612
|
+
return `# syntax=docker/dockerfile:1
|
|
2613
|
+
# Production image: docker build -t ${dockerTag(flags.name)} .
|
|
2614
|
+
# Installs are frozen: install the dependencies once (${flags.pm} install) so that the lockfile exists.
|
|
2615
|
+
|
|
2616
|
+
FROM ${baseImage} AS base
|
|
2617
|
+
${steps.setup.map((line) => `${line}\n`).join("")}WORKDIR /app
|
|
2618
|
+
|
|
2619
|
+
# --- production dependencies only, at the versions of the lockfile ----------------------------------
|
|
2620
|
+
FROM base AS deps
|
|
2621
|
+
COPY ${steps.manifests} ./
|
|
2622
|
+
RUN ${steps.production}
|
|
2623
|
+
|
|
2624
|
+
# --- build: every dependency, since the Znight compiler, Vite and TypeScript run here ---------------
|
|
2625
|
+
FROM base AS build
|
|
2626
|
+
COPY ${steps.manifests} ./
|
|
2627
|
+
RUN ${steps.install}
|
|
2628
|
+
COPY . .
|
|
2629
|
+
# generates the DI container (src/.znight), fails on any wiring error, bundles dist/main.js
|
|
2630
|
+
RUN ${steps.build}
|
|
2631
|
+
|
|
2632
|
+
# --- runtime: the bundle and the production dependencies, as a non-root user ----------------------
|
|
2633
|
+
FROM ${runImage}
|
|
2634
|
+
WORKDIR /app
|
|
2635
|
+
ENV NODE_ENV=production
|
|
2636
|
+
COPY --from=build /app/package.json ./
|
|
2637
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
2638
|
+
COPY --from=build /app/dist ./dist
|
|
2639
|
+
USER ${runtime}
|
|
2640
|
+
EXPOSE 3000
|
|
2641
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
|
|
2642
|
+
CMD ["${runtime}", "-e", "${probe}"]
|
|
2643
|
+
# exec form: the runtime is PID 1 and receives SIGTERM directly (graceful shutdown)${envFiles}
|
|
2644
|
+
CMD ${command}
|
|
2645
|
+
`;
|
|
2646
|
+
}
|
|
2647
|
+
/** A Docker image name for an npm package name (`@acme/api` -> `acme-api`). */
|
|
2648
|
+
function dockerTag(name) {
|
|
2649
|
+
return name.replace(/^@/, "").replace("/", "-");
|
|
2650
|
+
}
|
|
2651
|
+
function dockerignore() {
|
|
2652
|
+
return `# not part of the image: the build installs and generates everything itself
|
|
2653
|
+
node_modules
|
|
2654
|
+
dist
|
|
2655
|
+
coverage
|
|
2656
|
+
**/.znight
|
|
2657
|
+
*.log
|
|
2658
|
+
.git
|
|
2659
|
+
.env
|
|
2660
|
+
.env.*
|
|
2661
|
+
Dockerfile
|
|
2662
|
+
.dockerignore
|
|
2663
|
+
`;
|
|
2664
|
+
}
|
|
2473
2665
|
function adapterImport(adapter) {
|
|
2474
2666
|
switch (adapter) {
|
|
2475
2667
|
case "fastify": return "import { fastifyAdapter } from '@znight/http/fastify'";
|
|
@@ -2485,17 +2677,19 @@ function mainTs(flags) {
|
|
|
2485
2677
|
const imports = [
|
|
2486
2678
|
flags.config ? "import { ConfigPlugin } from '@znight/config'" : void 0,
|
|
2487
2679
|
"import { Application } from '@znight/core'",
|
|
2680
|
+
flags.graphql ? "import { GraphQLPlugin } from '@znight/graphql'" : void 0,
|
|
2488
2681
|
"import { HttpPlugin } from '@znight/http'",
|
|
2489
2682
|
adapterImport(flags.adapter),
|
|
2490
2683
|
flags.messaging ? "import { MessagingPlugin } from '@znight/messaging'" : void 0,
|
|
2491
2684
|
flags.messaging ? "import { memory } from '@znight/messaging/memory'" : void 0,
|
|
2492
2685
|
flags.observability ? "import { ObservabilityPlugin } from '@znight/observability'" : void 0,
|
|
2493
2686
|
flags.openapi ? "import { OpenApiPlugin } from '@znight/openapi'" : void 0,
|
|
2494
|
-
flags.schedule ? "import { SchedulePlugin } from '@znight/schedule'" : void 0
|
|
2687
|
+
flags.schedule ? "import { SchedulePlugin } from '@znight/schedule'" : void 0,
|
|
2688
|
+
flags.websocket ? "import { WebSocketPlugin } from '@znight/websocket'" : void 0
|
|
2495
2689
|
].filter((line) => line !== void 0);
|
|
2496
2690
|
const localImports = ["import { __ZNIGHT_METADATA__ } from './.znight/container.generated.js'", flags.config ? "import { AppConfig } from './app/app.config.js'" : void 0].filter((line) => line !== void 0);
|
|
2497
2691
|
const plugins = [];
|
|
2498
|
-
if (flags.observability) plugins.push(" // first, so that its hooks wrap everything: request logs, metrics (with prom-client), traces", " ObservabilityPlugin.configure(),");
|
|
2692
|
+
if (flags.observability) plugins.push(" // first, so that its hooks wrap everything: request logs, metrics (with prom-client), traces", " ObservabilityPlugin.configure({", " logging: {", ` name: ${quote(flags.name)},`, " },", " // metrics: {", " // // options of the metrics registry", " // },", " // tracing: {", " // // options of the tracer", " // },", " }),");
|
|
2499
2693
|
if (flags.config) plugins.push(" // loads .env files and the environment, validates every @Config class before anything starts", " ConfigPlugin.configure(),");
|
|
2500
2694
|
if (flags.messaging) plugins.push(" // in-memory broker for development: use rabbitmq(), sqs() or kafka() in production", " MessagingPlugin.configure({ driver: memory() }),");
|
|
2501
2695
|
if (flags.schedule) plugins.push(" // before HttpPlugin: plugins stop in reverse order, so HTTP stops first, then running jobs drain", " SchedulePlugin.configure(),");
|
|
@@ -2505,7 +2699,9 @@ function mainTs(flags) {
|
|
|
2505
2699
|
const port = "port: Number(process.env.PORT ?? 3000)";
|
|
2506
2700
|
plugins.push(adapter === void 0 ? ` HttpPlugin.configure({ ${port} }),` : ` HttpPlugin.configure({ adapter: ${adapter}, ${port} }),`);
|
|
2507
2701
|
}
|
|
2508
|
-
if (flags.openapi) plugins.push(
|
|
2702
|
+
if (flags.openapi) plugins.push(` // GET /openapi.json and the API reference at /docs, with ${OPENAPI_UI_LABELS[flags.openapiUi]} (document options: src/api.ts)`, ` OpenApiPlugin.configure({ ui: ${quote(flags.openapiUi)} }),`);
|
|
2703
|
+
if (flags.graphql) plugins.push(" // POST /graphql, GET /graphql (GraphiQL in development): the schema is derived from the resolvers", " GraphQLPlugin.configure(),");
|
|
2704
|
+
if (flags.websocket) plugins.push(" // WebSocket gateways on the HTTP server's port (ws on Node.js, Bun's own WebSocket on Bun)", " WebSocketPlugin.configure(),");
|
|
2509
2705
|
return `${imports.join("\n")}
|
|
2510
2706
|
|
|
2511
2707
|
${localImports.join("\n")}
|
|
@@ -2521,6 +2717,19 @@ ${plugins.join("\n")}
|
|
|
2521
2717
|
await app.start()
|
|
2522
2718
|
`;
|
|
2523
2719
|
}
|
|
2720
|
+
/** A single-quoted TypeScript string literal of `value` (quotes, backslashes and line breaks escaped). */
|
|
2721
|
+
function quote(value) {
|
|
2722
|
+
let out = "'";
|
|
2723
|
+
for (const char of value) {
|
|
2724
|
+
const code = char.codePointAt(0) ?? 0;
|
|
2725
|
+
if (char === "'" || char === "\\") out += `\\${char}`;
|
|
2726
|
+
else if (char === "\n") out += "\\n";
|
|
2727
|
+
else if (char === "\r") out += "\\r";
|
|
2728
|
+
else if (code < 32 || code === 8232 || code === 8233) out += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
2729
|
+
else out += char;
|
|
2730
|
+
}
|
|
2731
|
+
return `${out}'`;
|
|
2732
|
+
}
|
|
2524
2733
|
function appConfig() {
|
|
2525
2734
|
return `import { Config, Env } from '@znight/config'
|
|
2526
2735
|
|
|
@@ -2696,6 +2905,58 @@ function apiDocument(options) {
|
|
|
2696
2905
|
export class Api {}
|
|
2697
2906
|
`;
|
|
2698
2907
|
}
|
|
2908
|
+
function greetingResolver() {
|
|
2909
|
+
return `import { Args, Query, Resolver } from '@znight/graphql'
|
|
2910
|
+
|
|
2911
|
+
import { GreetingService } from './greeting.service.js'
|
|
2912
|
+
|
|
2913
|
+
/** A greeting. */
|
|
2914
|
+
export interface Greeting {
|
|
2915
|
+
readonly message: string
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
/**
|
|
2919
|
+
* GraphQL resolver (code-first): the compiler derives the schema from the TypeScript types and the
|
|
2920
|
+
* JSDoc, \`type Query { greeting(name: String!): Greeting! }\` here.
|
|
2921
|
+
*/
|
|
2922
|
+
@Resolver()
|
|
2923
|
+
export class GreetingResolver {
|
|
2924
|
+
constructor(private readonly greetings: GreetingService) {}
|
|
2925
|
+
|
|
2926
|
+
/**
|
|
2927
|
+
* Greets someone.
|
|
2928
|
+
* @param name Who to greet.
|
|
2929
|
+
*/
|
|
2930
|
+
@Query()
|
|
2931
|
+
greeting(@Args('name') name: string): Greeting {
|
|
2932
|
+
return { message: this.greetings.greet(name) }
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
`;
|
|
2936
|
+
}
|
|
2937
|
+
function greetingGateway() {
|
|
2938
|
+
return `import { Gateway, OnMessage, Payload } from '@znight/websocket'
|
|
2939
|
+
|
|
2940
|
+
import { CreateGreeting } from './greeting.schemas.js'
|
|
2941
|
+
import { GreetingService } from './greeting.service.js'
|
|
2942
|
+
|
|
2943
|
+
/**
|
|
2944
|
+
* WebSocket gateway on ws://localhost:3000/ws, served by the HTTP server. Frames are JSON: the
|
|
2945
|
+
* request {"event": "greet", "data": {"name": "Ada"}, "id": 1} is answered with
|
|
2946
|
+
* {"id": 1, "data": {"message": "Hello, Ada."}}.
|
|
2947
|
+
*/
|
|
2948
|
+
@Gateway('/ws')
|
|
2949
|
+
export class GreetingGateway {
|
|
2950
|
+
constructor(private readonly greetings: GreetingService) {}
|
|
2951
|
+
|
|
2952
|
+
/** The payload is validated by the schema (a BAD_REQUEST error reply otherwise). */
|
|
2953
|
+
@OnMessage('greet')
|
|
2954
|
+
greet(@Payload(CreateGreeting) input: CreateGreeting): { message: string } {
|
|
2955
|
+
return { message: this.greetings.greet(input.name, input.excited) }
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
`;
|
|
2959
|
+
}
|
|
2699
2960
|
function heartbeatJobsTest(flags) {
|
|
2700
2961
|
return `${testImports(flags, "describe, expect, test")}
|
|
2701
2962
|
import { SchedulePlugin, SchedulerRegistry } from '@znight/schedule'
|
|
@@ -2765,21 +3026,37 @@ describe('GreetingService', () => {
|
|
|
2765
3026
|
})
|
|
2766
3027
|
`;
|
|
2767
3028
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
3029
|
+
/** Imports and `plugins` array of a `TestApp` test, with the plugins the providers need. */
|
|
3030
|
+
function testAppPlugins(flags, names, extra = {}) {
|
|
3031
|
+
const imports = [testImports(flags, names)];
|
|
3032
|
+
if (extra.graphql === true) {
|
|
3033
|
+
imports.push("import { GraphQLPlugin } from '@znight/graphql'");
|
|
3034
|
+
imports.push("import { executeOperation } from '@znight/graphql/testing'");
|
|
3035
|
+
}
|
|
3036
|
+
imports.push("import { HttpPlugin } from '@znight/http'");
|
|
2770
3037
|
if (flags.messaging) {
|
|
2771
3038
|
imports.push("import { MessagingPlugin } from '@znight/messaging'");
|
|
2772
3039
|
imports.push("import { memory } from '@znight/messaging/memory'");
|
|
2773
3040
|
}
|
|
2774
3041
|
if (flags.observability) imports.push("import { ObservabilityPlugin } from '@znight/observability'");
|
|
2775
3042
|
imports.push("import { TestApp } from '@znight/testing'");
|
|
3043
|
+
if (extra.websocket === true) imports.push("import { WebSocketPlugin } from '@znight/websocket'");
|
|
2776
3044
|
const plugins = [];
|
|
2777
|
-
if (flags.observability) plugins.push("ObservabilityPlugin.configure({ logging: { level: 'silent' }, metrics:
|
|
3045
|
+
if (flags.observability) plugins.push("ObservabilityPlugin.configure({ logging: { level: 'silent' }, metrics: false })");
|
|
2778
3046
|
if (flags.messaging) plugins.push("MessagingPlugin.configure({ driver: memory() })");
|
|
2779
3047
|
plugins.push("HttpPlugin.configure({ port: 0 })");
|
|
2780
|
-
|
|
3048
|
+
if (extra.graphql === true) plugins.push("GraphQLPlugin.configure()");
|
|
3049
|
+
if (extra.websocket === true) plugins.push("WebSocketPlugin.configure()");
|
|
3050
|
+
const list = plugins.length === 1 ? `[${plugins[0]}]` : `[\n${plugins.map((plugin) => ` ${plugin},`).join("\n")}\n ]`;
|
|
3051
|
+
return {
|
|
3052
|
+
imports: imports.join("\n"),
|
|
3053
|
+
plugins: list
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
function greetingControllerTest(flags) {
|
|
3057
|
+
const { imports, plugins: pluginList } = testAppPlugins(flags, "afterEach, beforeEach, describe, expect, test");
|
|
2781
3058
|
const greeting = "Hello";
|
|
2782
|
-
return `${imports
|
|
3059
|
+
return `${imports}
|
|
2783
3060
|
|
|
2784
3061
|
import { __ZNIGHT_METADATA__ as metadata } from '../src/.znight/container.generated.js'
|
|
2785
3062
|
|
|
@@ -2816,6 +3093,58 @@ describe('GreetingController', () => {
|
|
|
2816
3093
|
})
|
|
2817
3094
|
`;
|
|
2818
3095
|
}
|
|
3096
|
+
function greetingResolverTest(flags) {
|
|
3097
|
+
const { imports, plugins } = testAppPlugins(flags, "describe, expect, test", { graphql: true });
|
|
3098
|
+
return `${imports}
|
|
3099
|
+
|
|
3100
|
+
import { __ZNIGHT_METADATA__ as metadata } from '../src/.znight/container.generated.js'
|
|
3101
|
+
|
|
3102
|
+
// executeOperation() sends the operation through the whole HTTP pipeline, in memory
|
|
3103
|
+
describe('GreetingResolver', () => {
|
|
3104
|
+
test('query { greeting }', async () => {
|
|
3105
|
+
const app = await TestApp.create({
|
|
3106
|
+
metadata,
|
|
3107
|
+
plugins: ${plugins},
|
|
3108
|
+
})
|
|
3109
|
+
try {
|
|
3110
|
+
const response = await executeOperation(app, { query: '{ greeting(name: "Ada") { message } }' })
|
|
3111
|
+
|
|
3112
|
+
expect(response.status).toBe(200)
|
|
3113
|
+
expect(response.errors).toBeUndefined()
|
|
3114
|
+
expect(response.data).toEqual({ greeting: { message: 'Hello, Ada.' } })
|
|
3115
|
+
} finally {
|
|
3116
|
+
await app.close()
|
|
3117
|
+
}
|
|
3118
|
+
})
|
|
3119
|
+
})
|
|
3120
|
+
`;
|
|
3121
|
+
}
|
|
3122
|
+
function greetingGatewayTest(flags) {
|
|
3123
|
+
const { imports, plugins } = testAppPlugins(flags, "describe, expect, test", { websocket: true });
|
|
3124
|
+
return `${imports}
|
|
3125
|
+
|
|
3126
|
+
import { __ZNIGHT_METADATA__ as metadata } from '../src/.znight/container.generated.js'
|
|
3127
|
+
|
|
3128
|
+
// app.connect() opens an in-memory connection, through the real handshake
|
|
3129
|
+
describe('GreetingGateway', () => {
|
|
3130
|
+
test('answers "greet" requests', async () => {
|
|
3131
|
+
const app = await TestApp.create({
|
|
3132
|
+
metadata,
|
|
3133
|
+
plugins: ${plugins},
|
|
3134
|
+
})
|
|
3135
|
+
try {
|
|
3136
|
+
const client = await app.connect('/ws')
|
|
3137
|
+
|
|
3138
|
+
expect(await client.request('greet', { name: 'Ada', excited: true })).toEqual({ message: 'Hello, Ada!' })
|
|
3139
|
+
await expect(client.request('greet', { name: '' })).rejects.toMatchObject({ code: 'BAD_REQUEST' })
|
|
3140
|
+
await client.close()
|
|
3141
|
+
} finally {
|
|
3142
|
+
await app.close()
|
|
3143
|
+
}
|
|
3144
|
+
})
|
|
3145
|
+
})
|
|
3146
|
+
`;
|
|
3147
|
+
}
|
|
2819
3148
|
function readme(options, flags) {
|
|
2820
3149
|
const run = (script) => runScript(flags.pm, script);
|
|
2821
3150
|
const znight = execBinary(flags.pm, "znight");
|
|
@@ -2825,7 +3154,20 @@ function readme(options, flags) {
|
|
|
2825
3154
|
flags.observability ? "- **Observability** with `@znight/observability` (structured logs, metrics, traces)" : void 0,
|
|
2826
3155
|
flags.messaging ? "- **Messaging** with `@znight/messaging` (in-memory broker in `src/main.ts`)" : void 0,
|
|
2827
3156
|
flags.schedule ? "- **Scheduled jobs** with `@znight/schedule` (`src/app/heartbeat.jobs.ts`)" : void 0,
|
|
2828
|
-
flags.openapi ?
|
|
3157
|
+
flags.openapi ? `- **OpenAPI** with \`@znight/openapi\` (\`/openapi.json\`, ${OPENAPI_UI_LABELS[flags.openapiUi]} API reference at \`/docs\`, options in \`src/api.ts\`)` : void 0,
|
|
3158
|
+
flags.graphql ? "- **GraphQL** with `@znight/graphql` (code-first: the schema is derived from `src/app/greeting.resolver.ts`)" : void 0,
|
|
3159
|
+
flags.websocket ? "- **WebSocket** with `@znight/websocket` (`src/app/greeting.gateway.ts`, on the HTTP port at `/ws`)" : void 0
|
|
3160
|
+
].filter((line) => line !== void 0);
|
|
3161
|
+
const app = [
|
|
3162
|
+
flags.config ? "app.config.ts # @Config: settings read from the environment" : void 0,
|
|
3163
|
+
flags.messaging ? "greeting.consumer.ts # @Consumer: handles the \"greetings\" queue" : void 0,
|
|
3164
|
+
"greeting.controller.ts # @Controller: /greetings routes",
|
|
3165
|
+
flags.websocket ? "greeting.gateway.ts # @Gateway: the /ws WebSocket endpoint" : void 0,
|
|
3166
|
+
flags.graphql ? "greeting.resolver.ts # @Resolver: the greeting GraphQL query" : void 0,
|
|
3167
|
+
"greeting.schemas.ts # Zod schemas",
|
|
3168
|
+
"greeting.service.ts # @Injectable",
|
|
3169
|
+
"health.controller.ts # GET /health",
|
|
3170
|
+
flags.schedule ? "heartbeat.jobs.ts # @Cron: a job every minute" : void 0
|
|
2829
3171
|
].filter((line) => line !== void 0);
|
|
2830
3172
|
const tree = [
|
|
2831
3173
|
"src/",
|
|
@@ -2833,16 +3175,11 @@ function readme(options, flags) {
|
|
|
2833
3175
|
flags.openapi ? "├─ api.ts # @OpenApiDocument: title, tags, security of openapi.json" : void 0,
|
|
2834
3176
|
"├─ .znight/ # generated DI container (git-ignored)",
|
|
2835
3177
|
"└─ app/",
|
|
2836
|
-
|
|
2837
|
-
flags.messaging ? " ├─ greeting.consumer.ts # @Consumer: handles the \"greetings\" queue" : void 0,
|
|
2838
|
-
" ├─ greeting.controller.ts # @Controller: /greetings routes",
|
|
2839
|
-
" ├─ greeting.schemas.ts # Zod schemas",
|
|
2840
|
-
" ├─ greeting.service.ts # @Injectable",
|
|
2841
|
-
flags.schedule ? " ├─ health.controller.ts # GET /health" : " └─ health.controller.ts # GET /health",
|
|
2842
|
-
flags.schedule ? " └─ heartbeat.jobs.ts # @Cron: a job every minute" : void 0,
|
|
3178
|
+
...app.map((line, index) => ` ${index === app.length - 1 ? "└─" : "├─"} ${line}`),
|
|
2843
3179
|
"test/ # TestBed (unit) and TestApp (HTTP) tests",
|
|
2844
3180
|
"vite.config.ts # znight() plugin: generates the container, server build",
|
|
2845
|
-
"znight.config.ts # compiler options"
|
|
3181
|
+
"znight.config.ts # compiler options",
|
|
3182
|
+
flags.docker ? "Dockerfile # production image (multi-stage, non-root)" : void 0
|
|
2846
3183
|
].filter((line) => line !== void 0);
|
|
2847
3184
|
const configNote = flags.config ? `
|
|
2848
3185
|
## Configuration
|
|
@@ -2853,6 +3190,20 @@ problem listed.${flags.bun ? `
|
|
|
2853
3190
|
|
|
2854
3191
|
\`bunfig.toml\` turns Bun's own \`.env\` loading off (\`env = false\`): the files are read by
|
|
2855
3192
|
\`@znight/config\`, the same way on Node.js and Bun.` : ""}
|
|
3193
|
+
` : "";
|
|
3194
|
+
const image = dockerTag(options.name);
|
|
3195
|
+
const dockerNote = flags.docker ? `
|
|
3196
|
+
## Docker
|
|
3197
|
+
|
|
3198
|
+
\`\`\`sh
|
|
3199
|
+
docker build -t ${image} .
|
|
3200
|
+
docker run --rm -p 3000:3000 ${image}
|
|
3201
|
+
\`\`\`
|
|
3202
|
+
|
|
3203
|
+
The \`Dockerfile\` builds with every dependency (the Znight compiler runs during the build), then
|
|
3204
|
+
ships \`dist/\` with the production dependencies only, on ${flags.bun ? "Bun" : "Node.js"}, as a non-root user, with
|
|
3205
|
+
\`NODE_ENV=production\` and a health check on \`GET /health\`. Installs are frozen: the lockfile of
|
|
3206
|
+
\`${flags.pm} install\` must exist.
|
|
2856
3207
|
` : "";
|
|
2857
3208
|
const testNote = flags.bun ? "`bun test` regenerates the container first (`bunfig.toml` preloads `@znight/compiler/bun`)." : "Vitest uses `vite.config.ts`, so the `znight()` plugin regenerates the container before the tests.";
|
|
2858
3209
|
const compilerNote = flags.bun ? "the `znight()` Vite plugin, the Bun preload of the tests and `znight generate`" : "the `znight()` Vite plugin (builds and Vitest) and `znight generate`";
|
|
@@ -2879,7 +3230,7 @@ ${flags.openapi ? `| \`${run("openapi")}\` | Write \`openapi.json\` (commit it;
|
|
|
2879
3230
|
${run("dev")}
|
|
2880
3231
|
curl localhost:3000/health
|
|
2881
3232
|
curl localhost:3000/greetings/Ada?excited=true
|
|
2882
|
-
curl -X POST localhost:3000/greetings -H 'content-type: application/json' -d '{"name":"Grace"}'${flags.openapi ? "\ncurl localhost:3000/openapi.json # the API reference is at http://localhost:3000/docs" : ""}
|
|
3233
|
+
curl -X POST localhost:3000/greetings -H 'content-type: application/json' -d '{"name":"Grace"}'${flags.openapi ? "\ncurl localhost:3000/openapi.json # the API reference is at http://localhost:3000/docs" : ""}${flags.graphql ? `\ncurl localhost:3000/graphql -H 'content-type: application/json' -d '{"query":"{ greeting(name: \\"Ada\\") { message } }"}'` : ""}
|
|
2883
3234
|
\`\`\`
|
|
2884
3235
|
|
|
2885
3236
|
## Project structure
|
|
@@ -2896,7 +3247,7 @@ Add code with the generators:
|
|
|
2896
3247
|
${znight} g service users/user # src/users/user.service.ts
|
|
2897
3248
|
${znight} g controller users/user # src/users/user.controller.ts${flags.schedule ? `\n${znight} g job reports/digest # src/reports/digest.jobs.ts` : ""}
|
|
2898
3249
|
\`\`\`
|
|
2899
|
-
${configNote}
|
|
3250
|
+
${configNote}${dockerNote}
|
|
2900
3251
|
## Tests
|
|
2901
3252
|
|
|
2902
3253
|
${testNote}
|
|
@@ -2916,7 +3267,8 @@ const newCommand = defineCommand({
|
|
|
2916
3267
|
aliases: ["create", "init"],
|
|
2917
3268
|
examples: [
|
|
2918
3269
|
"znight new my-service",
|
|
2919
|
-
"znight new my-service --yes --pm pnpm --adapter
|
|
3270
|
+
"znight new my-service --yes --pm pnpm --adapter hono --features config,observability,messaging",
|
|
3271
|
+
"znight new my-api --yes --features config,observability,openapi,graphql,websocket --openapi-ui swagger",
|
|
2920
3272
|
"znight new my-worker --yes --features config,schedule",
|
|
2921
3273
|
"znight new . --yes --no-install"
|
|
2922
3274
|
]
|
|
@@ -2945,17 +3297,27 @@ const newCommand = defineCommand({
|
|
|
2945
3297
|
adapter: {
|
|
2946
3298
|
type: "enum",
|
|
2947
3299
|
options: [
|
|
2948
|
-
"node",
|
|
2949
3300
|
"fastify",
|
|
3301
|
+
"node",
|
|
2950
3302
|
"express",
|
|
2951
3303
|
"hono"
|
|
2952
3304
|
],
|
|
2953
|
-
description: "HTTP adapter (default: node
|
|
3305
|
+
description: "HTTP adapter (default: fastify; node is the built-in node:http server)"
|
|
2954
3306
|
},
|
|
2955
3307
|
features: {
|
|
2956
3308
|
type: "string",
|
|
2957
3309
|
valueHint: "list",
|
|
2958
|
-
description: "Comma-separated features: config, observability, messaging, schedule, openapi, or \"all\" / \"none\" (default: config,observability)"
|
|
3310
|
+
description: "Comma-separated features: config, observability, messaging, schedule, openapi, graphql, websocket, or \"all\" / \"none\" (default: config,observability)"
|
|
3311
|
+
},
|
|
3312
|
+
"openapi-ui": {
|
|
3313
|
+
type: "enum",
|
|
3314
|
+
options: ["scalar", "swagger"],
|
|
3315
|
+
description: "API reference UI of the openapi feature, served at /docs (default: scalar)"
|
|
3316
|
+
},
|
|
3317
|
+
docker: {
|
|
3318
|
+
type: "boolean",
|
|
3319
|
+
default: true,
|
|
3320
|
+
description: "Do not generate a Dockerfile"
|
|
2959
3321
|
},
|
|
2960
3322
|
install: {
|
|
2961
3323
|
type: "boolean",
|
|
@@ -3013,9 +3375,21 @@ const newCommand = defineCommand({
|
|
|
3013
3375
|
value,
|
|
3014
3376
|
label: ADAPTER_LABELS[value]
|
|
3015
3377
|
})),
|
|
3016
|
-
initialValue: "
|
|
3017
|
-
}) ?? "
|
|
3378
|
+
initialValue: "fastify"
|
|
3379
|
+
}) ?? "fastify";
|
|
3018
3380
|
const features = await chooseFeatures(options.features, prompts);
|
|
3381
|
+
const openapiUi = options["openapi-ui"] ?? (features.includes("openapi") ? await prompts?.select({
|
|
3382
|
+
message: "OpenAPI reference UI (served at /docs)?",
|
|
3383
|
+
choices: OPENAPI_UIS.map((value) => ({
|
|
3384
|
+
value,
|
|
3385
|
+
label: OPENAPI_UI_LABELS[value]
|
|
3386
|
+
})),
|
|
3387
|
+
initialValue: "scalar"
|
|
3388
|
+
}) : void 0) ?? "scalar";
|
|
3389
|
+
const docker = prompts === void 0 || hasFlag(rawArgs, "docker") ? options.docker !== false : await prompts.confirm({
|
|
3390
|
+
message: "Add a Dockerfile (production image)?",
|
|
3391
|
+
initialValue: true
|
|
3392
|
+
});
|
|
3019
3393
|
const install = prompts === void 0 || hasFlag(rawArgs, "install") ? options.install !== false : await prompts.confirm({
|
|
3020
3394
|
message: `Install the dependencies with ${packageManager}?`,
|
|
3021
3395
|
initialValue: true
|
|
@@ -3029,6 +3403,8 @@ const newCommand = defineCommand({
|
|
|
3029
3403
|
packageManager,
|
|
3030
3404
|
adapter,
|
|
3031
3405
|
features,
|
|
3406
|
+
openapiUi,
|
|
3407
|
+
docker,
|
|
3032
3408
|
znightVersion: cli.version
|
|
3033
3409
|
});
|
|
3034
3410
|
writeProject(directory, files);
|
|
@@ -3723,8 +4099,9 @@ const startCommand = defineCommand({
|
|
|
3723
4099
|
env,
|
|
3724
4100
|
detached: true
|
|
3725
4101
|
});
|
|
4102
|
+
const consoleEvent = (signal) => cli.runtime.platform === "win32" && (signal === "SIGINT" || signal === "SIGHUP");
|
|
3726
4103
|
const forward = (signal) => () => {
|
|
3727
|
-
child.kill(signal);
|
|
4104
|
+
if (!consoleEvent(signal)) child.kill(signal);
|
|
3728
4105
|
};
|
|
3729
4106
|
const listeners = [
|
|
3730
4107
|
"SIGINT",
|
|
@@ -3885,4 +4262,4 @@ function stackOf(error) {
|
|
|
3885
4262
|
}
|
|
3886
4263
|
export { CancelledError as _, planSchematic as a, UsageError as b, FEATURES as c, AppSupervisor as d, createContext as f, defineCommand as g, loadCompiler as h, findSchematic as i, HTTP_ADAPTERS as l, adaptCompilerModule as m, main as n, renderProject as o, cliVersion as p, SCHEMATICS as r, DEFAULT_FEATURES as s, COMMANDS as t, PACKAGE_MANAGERS as u, CliError as v, ExitCode as y };
|
|
3887
4264
|
|
|
3888
|
-
//# sourceMappingURL=main-
|
|
4265
|
+
//# sourceMappingURL=main-CLoS12u4.mjs.map
|