@zerotal/core 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/api-surface.md +56 -355
  3. package/package.json +2 -1
  4. package/src/application/Application.ts +6 -0
  5. package/src/application/currentApp.ts +7 -1
  6. package/src/command/OutputWriter.ts +5 -1
  7. package/src/command/builtin/DeployCommand.ts +72 -1
  8. package/src/command/builtin/TestCommand.ts +9 -2
  9. package/src/command/startZerotal.ts +34 -0
  10. package/src/config/ConfigLoader.ts +5 -1
  11. package/src/config/DeployConfig.ts +33 -0
  12. package/src/config/index.ts +6 -1
  13. package/src/config/registry.ts +5 -1
  14. package/src/config/validation.ts +10 -2
  15. package/src/conventions/ConventionLoader.ts +5 -1
  16. package/src/dev/CssPlugins.ts +11 -1
  17. package/src/dev/DevBuildHook.ts +10 -2
  18. package/src/dev/DevDeck.ts +6 -0
  19. package/src/dev/DevOrchestrator.ts +2 -0
  20. package/src/dev/DevProcess.ts +17 -3
  21. package/src/dev/DevReloadMiddleware.ts +7 -1
  22. package/src/dev/DevSupervisor.ts +22 -4
  23. package/src/dev/bootBuild.ts +9 -1
  24. package/src/dev/reloadClient.ts +2 -0
  25. package/src/dev/startDevMode.ts +7 -1
  26. package/src/doctor/AppDoctor.ts +43 -1
  27. package/src/doctor/throttleIdentity.ts +114 -0
  28. package/src/errors/RuntimeMismatchError.ts +21 -0
  29. package/src/errors/index.ts +1 -0
  30. package/src/events/Emitter.ts +5 -1
  31. package/src/helpers/html.ts +2 -0
  32. package/src/helpers/markdown.ts +7 -1
  33. package/src/helpers/pageElements.ts +2 -0
  34. package/src/http/HttpClient.ts +5 -1
  35. package/src/http/Uri.ts +5 -1
  36. package/src/http/negotiate.ts +5 -1
  37. package/src/http/originGuard.ts +2 -0
  38. package/src/http/sniffContentType.ts +7 -1
  39. package/src/macros/config.macro.ts +2 -0
  40. package/src/metrics/HttpMetrics.ts +10 -2
  41. package/src/middleware/BaseMiddleware.ts +20 -0
  42. package/src/pipeline/ContextRegistry.ts +12 -2
  43. package/src/pipeline/HttpContext.ts +4 -0
  44. package/src/pipeline/currentPage.ts +4 -0
  45. package/src/pipeline/types.ts +2 -0
  46. package/src/router/FileRouter.ts +20 -2
  47. package/src/router/Route.ts +3 -0
  48. package/src/router/Router.ts +2 -0
  49. package/src/router/domain.ts +1 -0
  50. package/src/router/registry.ts +12 -2
  51. package/src/shared/format.ts +135 -0
  52. package/src/shared/index.ts +34 -0
  53. package/src/storage/StorageFilesMiddleware.ts +2 -0
  54. package/src/support/cookie.ts +7 -1
  55. package/src/support/runtime.ts +136 -0
  56. package/src/view/FileRouteResolver.ts +2 -0
  57. package/src/view/jsx-runtime.ts +4 -0
@@ -96,6 +96,8 @@ async function _discoverConfig(dir: string): Promise<Record<string, Record<strin
96
96
  * "web" → prefix: "", middleware: ["web"]
97
97
  * "api" → prefix: "/api", middleware: ["api"]
98
98
  * Custom keys must declare both `prefix` and `middleware` explicitly.
99
+ *
100
+ * @internal
99
101
  */
100
102
  export type RoutingEntry = string | { file: string; prefix?: string; middleware?: MiddlewareInput };
101
103
 
@@ -105,6 +107,8 @@ export type RoutingConfig = Record<string, RoutingEntry>;
105
107
  /**
106
108
  * A single entry in a `fileBasedRouting()` config map.
107
109
  * Same semantics as `RoutingEntry` but points to a directory instead of a file.
110
+ *
111
+ * @internal
108
112
  */
109
113
  export type FileRoutingEntry =
110
114
  string | { dir: string; prefix?: string; middleware?: MiddlewareInput };
@@ -271,6 +275,8 @@ export interface WebSocketHandlers {
271
275
  /**
272
276
  * Installs application-scoped state and returns a teardown function that
273
277
  * restores the previous state when the application is reset.
278
+ *
279
+ * @internal
274
280
  */
275
281
  export type AppScopeInstaller = () => () => void;
276
282
 
@@ -51,7 +51,11 @@ export function currentApp(): Application {
51
51
  return app;
52
52
  }
53
53
 
54
- /** The current application, or `undefined` when none is available. Safe off-app (CLI bootstrap, tests). */
54
+ /**
55
+ * The current application, or `undefined` when none is available. Safe off-app (CLI bootstrap, tests).
56
+ *
57
+ * @internal
58
+ */
55
59
  export function tryCurrentApp(): Application | undefined {
56
60
  return _scope.getStore() ?? _default;
57
61
  }
@@ -67,6 +71,8 @@ export function tryCurrentApp(): Application | undefined {
67
71
  * // Run a migration against a second app's config without disturbing the default.
68
72
  * await withApp(secondApp, () => secondApp.container.make("db"));
69
73
  * ```
74
+ *
75
+ * @internal
70
76
  */
71
77
  export function withApp<T>(app: Application, fn: () => T): T {
72
78
  return _scope.run(app, fn);
@@ -13,7 +13,11 @@ export interface OutputWriter {
13
13
  writeError(message: string): void;
14
14
  }
15
15
 
16
- /** Default writer — output goes to the terminal */
16
+ /**
17
+ * Default writer — output goes to the terminal
18
+ *
19
+ * @internal
20
+ */
17
21
  export class TerminalWriter implements OutputWriter {
18
22
  // Explicit constructor so JSC's function-coverage counter attributes
19
23
  // the new TerminalWriter() call to this entry.
@@ -26,7 +26,11 @@ import { runDoctor } from "../../doctor/AppDoctor.ts";
26
26
  import { probeTransport } from "../../doctor/TransportProbe.ts";
27
27
  import { runConfigValidators } from "../../config/validation.ts";
28
28
  import { deployEnv } from "../../support/env.ts";
29
- import { DEFAULT_DEPLOY_STEPS, type DeployTarget } from "../../config/DeployConfig.ts";
29
+ import {
30
+ CONVENTIONAL_PREFLIGHT_COMMAND,
31
+ DEFAULT_DEPLOY_STEPS,
32
+ type DeployTarget,
33
+ } from "../../config/DeployConfig.ts";
30
34
 
31
35
  /**
32
36
  * The pipeline. Subclassed per target by {@link makeDeployCommand}, which is what
@@ -129,6 +133,70 @@ export abstract class DeployCommand extends Command {
129
133
  this.line(`✓ config is valid for a ${target} deployment`);
130
134
 
131
135
  await this._doctor(app, "preflight");
136
+ await this._appPreflight(app, target);
137
+ }
138
+
139
+ /**
140
+ * Run the app's own preflight commands — the refusals the framework is not in a
141
+ * position to make.
142
+ *
143
+ * Last inside preflight, so the framework's cheap structural checks have already
144
+ * spoken; still before every step, so nothing has been built or migrated when one
145
+ * of these says no.
146
+ */
147
+ private async _appPreflight(app: Application, target: string): Promise<void> {
148
+ const commands = this._preflightCommands(app, target);
149
+ if (commands.length === 0) return;
150
+
151
+ const runner = app.container.makeSync("commands") as CommandRunner;
152
+ for (const name of commands) {
153
+ const { code, output } = await runner.callInProcess([name]);
154
+ if (output.trim()) this.line(output.trimEnd());
155
+ if (code !== 0) {
156
+ throw new Error(
157
+ `${name} refused this release. Nothing has been built or migrated — ` +
158
+ `fix what it reported and deploy again.`,
159
+ );
160
+ }
161
+ this.line(`✓ ${name} passed`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * The preflight commands for this target: what `config/deploy.ts` declares, or
167
+ * the conventional `release:check` when the app registers one.
168
+ *
169
+ * A declared name that is not registered is an error rather than a skip. That is
170
+ * the opposite of how `steps` treats an absent command, and deliberately so: a
171
+ * missing `inertia:build` means the app has no Inertia, while a missing gate
172
+ * means the gate is not running, which is exactly the state this feature exists
173
+ * to make impossible.
174
+ */
175
+ private _preflightCommands(app: Application, target: string): string[] {
176
+ let runner: CommandRunner | undefined;
177
+ try {
178
+ runner = app.container.makeSync("commands") as CommandRunner | undefined;
179
+ } catch {
180
+ runner = undefined;
181
+ }
182
+ // Same reasoning as `_steps`: with no registry to ask, nothing can be
183
+ // confirmed present. Returning nothing is the honest answer, and a declared
184
+ // preflight will be checked again by `_appPreflight` when it goes to run one.
185
+ if (!runner?.has) return [];
186
+
187
+ const declared = this._target(app, target)?.preflight;
188
+ if (declared === undefined) {
189
+ return runner.has(CONVENTIONAL_PREFLIGHT_COMMAND) ? [CONVENTIONAL_PREFLIGHT_COMMAND] : [];
190
+ }
191
+
192
+ const missing = declared.filter((name) => !runner.has(name));
193
+ if (missing.length > 0) {
194
+ throw new Error(
195
+ `config/deploy.ts declares preflight command(s) that are not registered: ` +
196
+ `${missing.join(", ")}. A gate that silently does not run is worse than no gate.`,
197
+ );
198
+ }
199
+ return [...declared];
132
200
  }
133
201
 
134
202
  // ── Phase 2 — build and migrate ─────────────────────────────────────────────
@@ -267,6 +335,9 @@ export abstract class DeployCommand extends Command {
267
335
  `APP_ENV ${actual || "(unset)"}${actual === target ? "" : ` ✗ expected ${target}`}`,
268
336
  );
269
337
  this.line(`preflight config validators, then ${"doctor"} checks`);
338
+ for (const name of this._preflightCommands(app, target)) {
339
+ this.line(`preflight ${name}`);
340
+ }
270
341
  for (const name of steps) {
271
342
  const skipped = name === "migrate" && this.flags["skip-migrations"] === true;
272
343
  this.line(`step ${name}${skipped ? " (skipped: --skip-migrations)" : ""}`);
@@ -1,4 +1,5 @@
1
1
  import { Command } from "../Command.ts";
2
+ import { bunBinary } from "../../support/runtime.ts";
2
3
 
3
4
  /**
4
5
  * `bun zt test [pattern] [flags]` — runs the test suite in the test environment.
@@ -121,10 +122,16 @@ export class TestCommand extends Command {
121
122
  const timeout = (this.flags["timeout"] as number | undefined) || TestCommand.DEFAULT_TIMEOUT_MS;
122
123
  bunArguments.push(`--timeout=${timeout}`);
123
124
 
124
- this.dim(`APP_ENV=test ZT_DB_URL=${dbUrl}`);
125
+ this.dim(`APP_ENV=test ZT_DB_URL=${dbUrl} bun ${Bun.version}`);
125
126
  this.dim(`bun ${bunArguments.join(" ")}\n`);
126
127
 
127
- const subprocess = Bun.spawn(["bun", ...bunArguments], {
128
+ // `bunBinary()`, not `"bun"`. The child of a command whose entire job is to run
129
+ // this app's tests must be the runtime this app is served by, and `"bun"` is
130
+ // resolved against PATH — so `node_modules/.bin/bun zt test` used to satisfy every
131
+ // check in the parent process and then hand the suite to whatever the shell had.
132
+ // The parent already refuses to boot on a runtime mismatch; spawning its own
133
+ // binary is what extends that guarantee to the process the assertions run in.
134
+ const subprocess = Bun.spawn([bunBinary(), ...bunArguments], {
128
135
  stdout: "inherit",
129
136
  stderr: "inherit",
130
137
  env: {
@@ -7,6 +7,12 @@
7
7
  import { CommandRunner } from "./CommandRunner.ts";
8
8
  import { configLoader } from "../config/ConfigLoader.ts";
9
9
  import { setAppEnv } from "../helpers/index.ts";
10
+ import {
11
+ runtimeMismatch,
12
+ runtimeMismatchAllowed,
13
+ runtimeMismatchMessage,
14
+ } from "../support/runtime.ts";
15
+ import { RuntimeMismatchError } from "../errors/RuntimeMismatchError.ts";
10
16
  import type { Application } from "../application/Application.ts";
11
17
 
12
18
  /** Options for {@link startZerotal}. */
@@ -35,6 +41,11 @@ export async function startZerotal(
35
41
  loadApp: () => Promise<{ default: Application }>,
36
42
  options: StartZerotalOptions = {},
37
43
  ): Promise<void> {
44
+ // Before anything else, because everything after this is an assertion about a
45
+ // runtime — and if there are two of them, which one made the assertion is the
46
+ // first thing worth knowing.
47
+ assertOneRuntime();
48
+
38
49
  // Load + validate config synchronously — safe before the app boots.
39
50
  const config = configLoader(options.configDir ?? "./config");
40
51
  config.validate();
@@ -53,3 +64,26 @@ export async function startZerotal(
53
64
  await runner.boot();
54
65
  await runner.run(process.argv.slice(2));
55
66
  }
67
+
68
+ /**
69
+ * Refuse to run when the project has two Bun runtimes in it.
70
+ *
71
+ * At the top of the entry point rather than in a check somebody remembers to run,
72
+ * because the value is in it being unmissable: the failure it prevents is a suite
73
+ * that passes about the wrong binary, and nothing downstream of here can notice
74
+ * that. A project with no `node_modules/bun` — most of them — never sees this.
75
+ *
76
+ * @throws When the running Bun and the installed one disagree, unless
77
+ * `ZT_ALLOW_RUNTIME_MISMATCH` is set, which downgrades it to a warning on stderr.
78
+ * @internal
79
+ */
80
+ function assertOneRuntime(): void {
81
+ const mismatch = runtimeMismatch();
82
+ if (!mismatch) return;
83
+ const message = runtimeMismatchMessage(mismatch);
84
+ if (runtimeMismatchAllowed()) {
85
+ console.warn(`\n⚠ ${message}\n`);
86
+ return;
87
+ }
88
+ throw new RuntimeMismatchError(message, mismatch);
89
+ }
@@ -9,7 +9,11 @@ import { frameworkLog } from "../logger/frameworkLog.ts";
9
9
 
10
10
  const requireModule = createRequire(import.meta.url);
11
11
 
12
- /** Namespace → config object map (filename without extension is the namespace). */
12
+ /**
13
+ * Namespace → config object map (filename without extension is the namespace).
14
+ *
15
+ * @internal
16
+ */
13
17
  export type ConfigMap = Record<string, Record<string, unknown>>;
14
18
  type Validator = (value: Record<string, unknown>) => void;
15
19
 
@@ -35,6 +35,19 @@
35
35
  */
36
36
  export const DEFAULT_DEPLOY_STEPS: readonly string[] = ["assets:build", "inertia:build", "migrate"];
37
37
 
38
+ /**
39
+ * The command a deploy runs before anything else, when the app defines one.
40
+ *
41
+ * A convention rather than a required declaration, because the alternative is the
42
+ * failure this exists to prevent: an app writes a preflight command, forgets to
43
+ * wire it into the pipeline, and every refusal that command knows how to make sits
44
+ * behind something nobody is obliged to run. **A gate nothing calls is a comment.**
45
+ *
46
+ * Register a command by this name and the pipeline finds it. Declare `preflight`
47
+ * on a target to run something else, or to run more than one.
48
+ */
49
+ export const CONVENTIONAL_PREFLIGHT_COMMAND = "release:check";
50
+
38
51
  /** One environment this app is released to. */
39
52
  export interface DeployTarget {
40
53
  /**
@@ -47,6 +60,26 @@ export interface DeployTarget {
47
60
  * {@link DEFAULT_DEPLOY_STEPS}. Names a `zt` command per entry.
48
61
  */
49
62
  steps?: readonly string[];
63
+ /**
64
+ * App-owned commands that run in the preflight phase — after the framework's
65
+ * own config validators and `doctor`, and before any step that mutates
66
+ * anything. A non-zero exit refuses the release.
67
+ *
68
+ * This is the slot for the checks only the app can make. The framework can tell
69
+ * you the `APP_KEY` is the one from `.env.example`; it cannot tell you this
70
+ * workspace has no cancellation policy, or that the owner account is still on
71
+ * the password the installer issued it. Those refusals are yours, and until
72
+ * there was somewhere to put them they lived in a command nobody ran.
73
+ *
74
+ * Defaults to {@link CONVENTIONAL_PREFLIGHT_COMMAND} when that command is
75
+ * registered, and to nothing when it is not.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * production: { preflight: ["release:check", "assets:verify"] }
80
+ * ```
81
+ */
82
+ preflight?: readonly string[];
50
83
  }
51
84
 
52
85
  /** The `deploy` config namespace. */
@@ -36,7 +36,12 @@ export type {
36
36
  AppAssetsConfig,
37
37
  AssetLoaderKind,
38
38
  } from "./AppConfig.ts";
39
- export { DeployConfig, DEFAULT_DEPLOY_STEPS, DEFAULT_DEPLOY_TARGETS } from "./DeployConfig.ts";
39
+ export {
40
+ DeployConfig,
41
+ DEFAULT_DEPLOY_STEPS,
42
+ DEFAULT_DEPLOY_TARGETS,
43
+ CONVENTIONAL_PREFLIGHT_COMMAND,
44
+ } from "./DeployConfig.ts";
40
45
  export type { DeployConfigShape, DeployTarget } from "./DeployConfig.ts";
41
46
  export type { ConfigRegistry, ConfigPath, ConfigValue } from "./registry.ts";
42
47
  export { ConfigValidationError } from "./validation.ts";
@@ -53,7 +53,11 @@ type ValueAt<T, P extends string> = P extends `${infer Head}.${infer Rest}`
53
53
  ? T[P]
54
54
  : unknown;
55
55
 
56
- /** Every valid dot-path string across the registered config namespaces. */
56
+ /**
57
+ * Every valid dot-path string across the registered config namespaces.
58
+ *
59
+ * @internal
60
+ */
57
61
  export type ConfigPath = Leaves<ConfigRegistry>;
58
62
 
59
63
  /** The value type stored at a given config dot-path. */
@@ -29,7 +29,11 @@ export interface ConfigIssue {
29
29
  message: string;
30
30
  }
31
31
 
32
- /** Context handed to a config validator. */
32
+ /**
33
+ * Context handed to a config validator.
34
+ *
35
+ * @internal
36
+ */
33
37
  export interface ConfigValidationContext {
34
38
  /** The namespace being validated (e.g. `"session"`). */
35
39
  namespace: string;
@@ -52,7 +56,11 @@ export type ConfigValidator = (
52
56
  ctx: ConfigValidationContext,
53
57
  ) => ConfigIssue[] | void;
54
58
 
55
- /** A validator paired with the namespace it guards, as held on the application. */
59
+ /**
60
+ * A validator paired with the namespace it guards, as held on the application.
61
+ *
62
+ * @internal
63
+ */
56
64
  export interface RegisteredConfigValidator {
57
65
  namespace: string;
58
66
  validate: ConfigValidator;
@@ -12,7 +12,11 @@ import type { Application } from "../application/Application.ts";
12
12
  import type { AppEnvironment } from "../provider/ServiceProvider.ts";
13
13
  import { frameworkLog } from "../logger/frameworkLog.ts";
14
14
 
15
- /** The context a concern receives while registering discovered modules. */
15
+ /**
16
+ * The context a concern receives while registering discovered modules.
17
+ *
18
+ * @internal
19
+ */
16
20
  export interface ConcernContext {
17
21
  app: Application;
18
22
  env: AppEnvironment;
@@ -40,6 +40,8 @@ function _scanRoots(cwd: string): string[] {
40
40
  *
41
41
  * Returns an empty array when `bun-plugin-tailwind` is not installed.
42
42
  * The caller falls back to a `bunx @tailwindcss/cli` subprocess.
43
+ *
44
+ * @internal
43
45
  */
44
46
  export async function detectCssPlugins(cwd: string): Promise<BunPlugin[]> {
45
47
  try {
@@ -60,6 +62,8 @@ export async function detectCssPlugins(cwd: string): Promise<BunPlugin[]> {
60
62
  * @param input Absolute path to the CSS source (e.g. `${cwd}/resources/css/app.css`)
61
63
  * @param outdir Absolute path to the output directory (e.g. `${cwd}/public/css`)
62
64
  * @param minify Whether to minify the output (true in production)
65
+ *
66
+ * @internal
63
67
  */
64
68
  export async function buildCssBundle(
65
69
  input: string,
@@ -155,6 +159,8 @@ export async function buildCssBundle(
155
159
  * @param input Absolute path to the JS/TS entry (e.g. `${cwd}/resources/js/app.js`)
156
160
  * @param outdir Absolute path to the output directory (e.g. `${cwd}/public/js`)
157
161
  * @param minify Whether to minify the output (true in production)
162
+ *
163
+ * @internal
158
164
  */
159
165
  export async function buildJsBundle(
160
166
  input: string,
@@ -193,7 +199,11 @@ function _basename(filePath: string): string {
193
199
  return filePath.split("/").at(-1) ?? filePath;
194
200
  }
195
201
 
196
- /** The resolved `app.assets` config block (see {@link AppAssetsConfig}). */
202
+ /**
203
+ * The resolved `app.assets` config block (see {@link AppAssetsConfig}).
204
+ *
205
+ * @internal
206
+ */
197
207
  export interface AssetBuildConfig {
198
208
  entrypoint: string | string[];
199
209
  outDir: string;
@@ -12,7 +12,11 @@
12
12
  * server cheerfully reported "rebuilding… ✓ ready" on every change.
13
13
  */
14
14
 
15
- /** Outcome of a frontend build: whether it succeeded and any collected logs. */
15
+ /**
16
+ * Outcome of a frontend build: whether it succeeded and any collected logs.
17
+ *
18
+ * @internal
19
+ */
16
20
  export interface BuildResult {
17
21
  success: boolean;
18
22
  logs?: unknown[];
@@ -25,7 +29,11 @@ export interface BuildResult {
25
29
  skipped?: boolean;
26
30
  }
27
31
 
28
- /** A frontend build routine that resolves once the build finishes. */
32
+ /**
33
+ * A frontend build routine that resolves once the build finishes.
34
+ *
35
+ * @internal
36
+ */
29
37
  export type BuildHookFn = () => Promise<BuildResult>;
30
38
 
31
39
  const _hooks = new Map<string, BuildHookFn>();
@@ -51,6 +51,7 @@ export interface Deck {
51
51
  stop(): void;
52
52
  }
53
53
 
54
+ /** @internal */
54
55
  export interface DeckOptions {
55
56
  writer: OutputWriter;
56
57
  /** Restart the named process — bound to `r`. */
@@ -90,6 +91,8 @@ export interface DeckStdin {
90
91
  * read keys from. A process with a TTY on stdout but not stdin (some CI
91
92
  * runners, `zt dev < /dev/null`) would render a tab bar nobody could ever
92
93
  * switch, so it gets stream instead.
94
+ *
95
+ * @internal
93
96
  */
94
97
  export function createDeck(options: DeckOptions): Deck {
95
98
  const stdout = options.stdout ?? process.stdout;
@@ -108,6 +111,8 @@ export function createDeck(options: DeckOptions): Deck {
108
111
  * Colour is applied only when stdout is a terminal. Piped to a file this emits
109
112
  * nothing but text, which is the point — a log with escape codes in it is a log
110
113
  * you have to clean before you can read it.
114
+ *
115
+ * @internal
111
116
  */
112
117
  export class StreamDeck implements Deck {
113
118
  private _width = 0;
@@ -181,6 +186,7 @@ interface Card {
181
186
  scroll: number;
182
187
  }
183
188
 
189
+ /** @internal */
184
190
  export class TabsDeck implements Deck {
185
191
  private readonly _cards: Card[] = [];
186
192
  private readonly _index = new Map<string, Card>();
@@ -14,6 +14,8 @@ import { ZEROTAL_VERSION, installedCoreVersion } from "../support/version.ts";
14
14
  * All optional, and all defaulting to the console. Without them the orchestrator
15
15
  * behaves exactly as it did before the deck existed — which is what keeps the
16
16
  * plain `bun --watch` path and the existing tests honest.
17
+ *
18
+ * @internal
17
19
  */
18
20
  export interface DevOrchestratorHooks {
19
21
  /**
@@ -69,13 +69,21 @@ export interface DevProcessDefinition {
69
69
  color?: DevProcessColor;
70
70
  }
71
71
 
72
- /** The colours a deck tab can take. Named, not ANSI codes, so the deck owns the rendering. */
72
+ /**
73
+ * The colours a deck tab can take. Named, not ANSI codes, so the deck owns the rendering.
74
+ *
75
+ * @internal
76
+ */
73
77
  export type DevProcessColor = "cyan" | "magenta" | "yellow" | "green" | "blue" | "red";
74
78
 
75
79
  /** Assigned in order to processes that did not pick a colour. */
76
80
  const _PALETTE: DevProcessColor[] = ["cyan", "magenta", "yellow", "green", "blue", "red"];
77
81
 
78
- /** A definition with every default filled in and its argv settled. */
82
+ /**
83
+ * A definition with every default filled in and its argv settled.
84
+ *
85
+ * @internal
86
+ */
79
87
  export interface ResolvedDevProcess {
80
88
  name: string;
81
89
  label: string;
@@ -95,7 +103,11 @@ export interface ResolvedDevProcess {
95
103
  registrant: string;
96
104
  }
97
105
 
98
- /** The `app.dev` config block. */
106
+ /**
107
+ * The `app.dev` config block.
108
+ *
109
+ * @internal
110
+ */
99
111
  export interface DevConfigShape {
100
112
  /** App-level processes, registered after every provider's. */
101
113
  processes?: DevProcessDefinition[];
@@ -123,6 +135,8 @@ interface ConfigReader {
123
135
  * @param app A booted application — providers must have run, since `enabled`
124
136
  * and a `command` thunk are allowed to read config.
125
137
  * @param config The config manager, read for `app.dev`.
138
+ *
139
+ * @internal
126
140
  */
127
141
  export async function collectDevProcesses(
128
142
  app: ProviderHost,
@@ -19,7 +19,11 @@ import type { HttpContext } from "../pipeline/HttpContext.ts";
19
19
  import { BaseMiddleware } from "../middleware/BaseMiddleware.ts";
20
20
  import { DEV_RELOAD_CLIENT } from "./reloadClient.ts";
21
21
 
22
- /** Produces an HTML fragment to inject before `</body>` for the given request. */
22
+ /**
23
+ * Produces an HTML fragment to inject before `</body>` for the given request.
24
+ *
25
+ * @internal
26
+ */
23
27
  export type DevHtmlSnippet = (ctx: HttpContext) => string;
24
28
 
25
29
  interface RegisteredSnippet {
@@ -39,6 +43,8 @@ const _snippets: RegisteredSnippet[] = [];
39
43
  * registerDevHtmlSnippet("devtools", (ctx) =>
40
44
  * ctx.url.pathname.startsWith("/__zerotal") ? "" : `<script src="/__zerotal/devtools/client.js"></script>`,
41
45
  * );
46
+ *
47
+ * @internal
42
48
  */
43
49
  export function registerDevHtmlSnippet(name: string, fn: DevHtmlSnippet): void {
44
50
  if (_snippets.some((s) => s.name === name)) return;
@@ -16,7 +16,11 @@
16
16
  */
17
17
  import type { ResolvedDevProcess } from "./DevProcess.ts";
18
18
 
19
- /** What the supervisor needs back from whatever it spawned. */
19
+ /**
20
+ * What the supervisor needs back from whatever it spawned.
21
+ *
22
+ * @internal
23
+ */
20
24
  export interface DevChild {
21
25
  readonly stdout: ReadableStream<Uint8Array> | null;
22
26
  readonly stderr: ReadableStream<Uint8Array> | null;
@@ -24,16 +28,28 @@ export interface DevChild {
24
28
  kill(signal?: number | NodeJS.Signals): void;
25
29
  }
26
30
 
27
- /** How the supervisor starts a child. Injected so tests need no real processes. */
31
+ /**
32
+ * How the supervisor starts a child. Injected so tests need no real processes.
33
+ *
34
+ * @internal
35
+ */
28
36
  export type DevSpawnFn = (
29
37
  argv: string[],
30
38
  options: { cwd: string; env: Record<string, string | undefined> },
31
39
  ) => DevChild;
32
40
 
33
- /** Where a process is in its life. */
41
+ /**
42
+ * Where a process is in its life.
43
+ *
44
+ * @internal
45
+ */
34
46
  export type DevProcessState = "starting" | "running" | "restarting" | "exited" | "parked";
35
47
 
36
- /** A process as the deck sees it. */
48
+ /**
49
+ * A process as the deck sees it.
50
+ *
51
+ * @internal
52
+ */
37
53
  export interface DevProcessStatus {
38
54
  name: string;
39
55
  label: string;
@@ -53,6 +69,7 @@ export interface DevSupervisorHooks {
53
69
  onState?: (status: DevProcessStatus) => void;
54
70
  }
55
71
 
72
+ /** @internal */
56
73
  export interface DevSupervisorOptions extends DevSupervisorHooks {
57
74
  cwd: string;
58
75
  env?: Record<string, string | undefined>;
@@ -92,6 +109,7 @@ interface Entry {
92
109
  healthyTimer?: ReturnType<typeof setTimeout> | undefined;
93
110
  }
94
111
 
112
+ /** @internal */
95
113
  export class DevSupervisor {
96
114
  private readonly _entries = new Map<string, Entry>();
97
115
  private readonly _cwd: string;
@@ -23,7 +23,11 @@
23
23
  import { mkdir, unlink, writeFile } from "node:fs/promises";
24
24
  import { isProdLike } from "../support/env.ts";
25
25
 
26
- /** What to do about a boot-time asset build, and why. */
26
+ /**
27
+ * What to do about a boot-time asset build, and why.
28
+ *
29
+ * @internal
30
+ */
27
31
  export interface BootBuildDecision {
28
32
  /** Whether to run the build. */
29
33
  build: boolean;
@@ -37,6 +41,8 @@ export interface BootBuildDecision {
37
41
  * Tested by writing rather than by reading a permission bit: the thing that makes
38
42
  * `public/` unwritable in the failure this guards against is `ProtectSystem=strict`, a
39
43
  * mount-level restriction that a mode check does not see.
44
+ *
45
+ * @internal
40
46
  */
41
47
  export async function isWritableDir(dir: string): Promise<boolean> {
42
48
  const probe = `${dir}/.zerotal-write-probe-${process.pid}`;
@@ -68,6 +74,8 @@ export async function isWritableDir(dir: string): Promise<boolean> {
68
74
  * if (!decision.build) console.info(decision.reason);
69
75
  * else await build();
70
76
  * ```
77
+ *
78
+ * @internal
71
79
  */
72
80
  export async function bootBuildDecision(
73
81
  outDirs: string[],
@@ -19,6 +19,8 @@
19
19
  * Exported as a string because it is injected inline: `DevReloadMiddleware`
20
20
  * rewrites HTML responses with it for every view layer, and Inertia bakes it
21
21
  * into its cached template so streamed pages need no body rewrite.
22
+ *
23
+ * @internal
22
24
  */
23
25
  export const DEV_RELOAD_CLIENT: string =
24
26
  `<script>(function(){` +
@@ -39,7 +39,11 @@ export interface StartDevModeOptions {
39
39
  deckMode?: "tabs" | "stream" | undefined;
40
40
  }
41
41
 
42
- /** The server's card. Named so `--only=server` and `--without=server` read naturally. */
42
+ /**
43
+ * The server's card. Named so `--only=server` and `--without=server` read naturally.
44
+ *
45
+ * @internal
46
+ */
43
47
  export const SERVER_PROCESS_NAME = "server";
44
48
 
45
49
  /**
@@ -47,6 +51,8 @@ export const SERVER_PROCESS_NAME = "server";
47
51
  *
48
52
  * Never returns: the orchestrator parks on an unresolved promise and the exit
49
53
  * happens in its signal handlers, after the deck has restored the terminal.
54
+ *
55
+ * @internal
50
56
  */
51
57
  export async function startDevMode(options: StartDevModeOptions): Promise<void> {
52
58
  const supervised = options.processes.filter((entry) => entry.name !== SERVER_PROCESS_NAME);