@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.
- package/CHANGELOG.md +87 -0
- package/api-surface.md +56 -355
- package/package.json +2 -1
- package/src/application/Application.ts +6 -0
- package/src/application/currentApp.ts +7 -1
- package/src/command/OutputWriter.ts +5 -1
- package/src/command/builtin/DeployCommand.ts +72 -1
- package/src/command/builtin/TestCommand.ts +9 -2
- package/src/command/startZerotal.ts +34 -0
- package/src/config/ConfigLoader.ts +5 -1
- package/src/config/DeployConfig.ts +33 -0
- package/src/config/index.ts +6 -1
- package/src/config/registry.ts +5 -1
- package/src/config/validation.ts +10 -2
- package/src/conventions/ConventionLoader.ts +5 -1
- package/src/dev/CssPlugins.ts +11 -1
- package/src/dev/DevBuildHook.ts +10 -2
- package/src/dev/DevDeck.ts +6 -0
- package/src/dev/DevOrchestrator.ts +2 -0
- package/src/dev/DevProcess.ts +17 -3
- package/src/dev/DevReloadMiddleware.ts +7 -1
- package/src/dev/DevSupervisor.ts +22 -4
- package/src/dev/bootBuild.ts +9 -1
- package/src/dev/reloadClient.ts +2 -0
- package/src/dev/startDevMode.ts +7 -1
- package/src/doctor/AppDoctor.ts +43 -1
- package/src/doctor/throttleIdentity.ts +114 -0
- package/src/errors/RuntimeMismatchError.ts +21 -0
- package/src/errors/index.ts +1 -0
- package/src/events/Emitter.ts +5 -1
- package/src/helpers/html.ts +2 -0
- package/src/helpers/markdown.ts +7 -1
- package/src/helpers/pageElements.ts +2 -0
- package/src/http/HttpClient.ts +5 -1
- package/src/http/Uri.ts +5 -1
- package/src/http/negotiate.ts +5 -1
- package/src/http/originGuard.ts +2 -0
- package/src/http/sniffContentType.ts +7 -1
- package/src/macros/config.macro.ts +2 -0
- package/src/metrics/HttpMetrics.ts +10 -2
- package/src/middleware/BaseMiddleware.ts +20 -0
- package/src/pipeline/ContextRegistry.ts +12 -2
- package/src/pipeline/HttpContext.ts +4 -0
- package/src/pipeline/currentPage.ts +4 -0
- package/src/pipeline/types.ts +2 -0
- package/src/router/FileRouter.ts +20 -2
- package/src/router/Route.ts +3 -0
- package/src/router/Router.ts +2 -0
- package/src/router/domain.ts +1 -0
- package/src/router/registry.ts +12 -2
- package/src/shared/format.ts +135 -0
- package/src/shared/index.ts +34 -0
- package/src/storage/StorageFilesMiddleware.ts +2 -0
- package/src/support/cookie.ts +7 -1
- package/src/support/runtime.ts +136 -0
- package/src/view/FileRouteResolver.ts +2 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
|
|
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
|
-
/**
|
|
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. */
|
package/src/config/index.ts
CHANGED
|
@@ -36,7 +36,12 @@ export type {
|
|
|
36
36
|
AppAssetsConfig,
|
|
37
37
|
AssetLoaderKind,
|
|
38
38
|
} from "./AppConfig.ts";
|
|
39
|
-
export {
|
|
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";
|
package/src/config/registry.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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. */
|
package/src/config/validation.ts
CHANGED
|
@@ -29,7 +29,11 @@ export interface ConfigIssue {
|
|
|
29
29
|
message: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/src/dev/CssPlugins.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/src/dev/DevBuildHook.ts
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* server cheerfully reported "rebuilding… ✓ ready" on every change.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/**
|
|
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
|
-
/**
|
|
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>();
|
package/src/dev/DevDeck.ts
CHANGED
|
@@ -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
|
/**
|
package/src/dev/DevProcess.ts
CHANGED
|
@@ -69,13 +69,21 @@ export interface DevProcessDefinition {
|
|
|
69
69
|
color?: DevProcessColor;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/src/dev/DevSupervisor.ts
CHANGED
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { ResolvedDevProcess } from "./DevProcess.ts";
|
|
18
18
|
|
|
19
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/src/dev/bootBuild.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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[],
|
package/src/dev/reloadClient.ts
CHANGED
|
@@ -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(){` +
|
package/src/dev/startDevMode.ts
CHANGED
|
@@ -39,7 +39,11 @@ export interface StartDevModeOptions {
|
|
|
39
39
|
deckMode?: "tabs" | "stream" | undefined;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
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);
|