@znight/cli 0.6.2 → 0.6.3

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.
@@ -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 a restart scheduled by {@link scheduleRestart} and not started yet (the output it would
1275
- * run turned out to be broken, e.g. a failed compilation). Returns whether one was pending.
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
- if (this.#debounce === void 0) return false;
1279
- clearTimeout(this.#debounce);
1280
- this.#debounce = void 0;
1281
- return true;
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
- await this.#stopCurrent("SIGTERM");
1295
- if (this.#closed) return;
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
- /** Stops the current process with `signal` (SIGKILL after the timeout) and refuses new starts. */
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
- } finally {
1828
- await supervisor.stop(signal === "SIGINT" ? "SIGINT" : "SIGTERM");
1829
- dispose();
1830
- resolveExit(0);
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 = false;
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 = async (initial) => {
2048
- if (running) return;
2049
- running = true;
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
- try {
2052
- do {
2053
- const files = pending;
2054
- pending = /* @__PURE__ */ new Set();
2055
- const started = performance.now();
2056
- let known = state.known;
2057
- try {
2058
- const result = await incremental.generate(first ? void 0 : classifyChanges(files, state.known));
2059
- if (result.watchFiles !== void 0) known = new Set(result.watchFiles.map((file) => toPosixPath(resolve(file))));
2060
- report(cli, compiler, result, root, performance.now() - started);
2061
- } catch (error) {
2062
- cli.reporter.error(`Generation failed: ${errorMessage(error)}`);
2063
- }
2064
- state = {
2065
- known,
2066
- roots: incremental.includeRoots().map(realPath)
2067
- };
2068
- watchers.update(watchTargets(realRoot, state.roots, state.known), files);
2069
- first = false;
2070
- } while (pending.size > 0);
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(" // GET /openapi.json and the API reference at /docs (document options: src/api.ts)", " OpenApiPlugin.configure(),");
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
- function greetingControllerTest(flags) {
2769
- const imports = [testImports(flags, "afterEach, beforeEach, describe, expect, test"), "import { HttpPlugin } from '@znight/http'"];
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: { server: false } })");
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
- const pluginList = plugins.length === 1 ? `[${plugins[0]}]` : `[\n${plugins.map((plugin) => ` ${plugin},`).join("\n")}\n ]`;
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.join("\n")}
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 ? "- **OpenAPI** with `@znight/openapi` (`/openapi.json`, API reference at `/docs`, options in `src/api.ts`)" : void 0
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
- flags.config ? " ├─ app.config.ts # @Config: settings read from the environment" : void 0,
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 fastify --features config,observability,messaging",
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, the built-in node:http server)"
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: "node"
3017
- }) ?? "node";
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-DUyElIVZ.mjs.map
4265
+ //# sourceMappingURL=main-CLoS12u4.mjs.map