@zerotal/core 1.0.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 (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,71 @@
1
+ import { Command } from "../Command.ts";
2
+
3
+ /**
4
+ * `bun zt status` — shows live metrics from the running server.
5
+ *
6
+ * Fetches GET /__zerotal/health from the running server and pretty-prints the
7
+ * metrics: uptime, pending HTTP requests, open WebSockets, memory, Bun version.
8
+ *
9
+ * The health endpoint is auto-enabled in development. For production, set
10
+ * `health: true` in config/app.ts.
11
+ *
12
+ * @category Diagnostics
13
+ */
14
+ export class StatusCommand extends Command {
15
+ static commandName = "status";
16
+ static description = "Show live metrics from the running server";
17
+ static needsApp = false;
18
+ static args = [];
19
+ static flags = [
20
+ {
21
+ name: "port",
22
+ short: "p",
23
+ type: "number" as const,
24
+ description: "Port the server is listening on",
25
+ default: 3000,
26
+ },
27
+ {
28
+ name: "host",
29
+ type: "string" as const,
30
+ description: "Server hostname",
31
+ default: "localhost",
32
+ },
33
+ ];
34
+
35
+ async run(): Promise<void> {
36
+ const port = this.flags["port"] as number;
37
+ const host = this.flags["host"] as string;
38
+ const url = `http://${host}:${port}/__zerotal/health`;
39
+
40
+ let data: Record<string, unknown>;
41
+ try {
42
+ const res = await fetch(url);
43
+ if (!res.ok) {
44
+ this.error(`Health endpoint returned ${res.status}.`);
45
+ this.dim("Make sure app.health is enabled and the server is running.");
46
+ process.exit(1);
47
+ }
48
+ data = (await res.json()) as Record<string, unknown>;
49
+ } catch {
50
+ this.error(`Could not reach ${url}`);
51
+ this.dim("Start the server with: bun zerotal.ts serve");
52
+ process.exit(1);
53
+ }
54
+
55
+ const memory = data.memory as { heapUsed?: number; rss?: number } | undefined;
56
+ const formatMegabytes = (bytes: number | undefined) =>
57
+ bytes !== undefined ? `${Math.round(bytes / 1024 / 1024)} MB` : "—";
58
+
59
+ this.section("Zerotal Server Status");
60
+ this.table([
61
+ ["Status", String(data.status ?? "?")],
62
+ ["Bun", String(data.version ?? "?")],
63
+ ["Uptime", `${Math.floor(Number(data.uptime ?? 0))} s`],
64
+ ["Pending requests", String(data.pendingRequests ?? 0)],
65
+ ["Open WebSockets", String(data.pendingWebSockets ?? 0)],
66
+ ["Heap used", formatMegabytes(memory?.heapUsed)],
67
+ ["RSS", formatMegabytes(memory?.rss)],
68
+ ]);
69
+ this.newLine();
70
+ }
71
+ }
@@ -0,0 +1,172 @@
1
+ import { Command } from "../Command.ts";
2
+
3
+ /**
4
+ * `bun zt test [pattern] [flags]` — runs the test suite in the test environment.
5
+ *
6
+ * Wraps `bun test` with proper app-environment setup:
7
+ * - Sets APP_ENV=test before spawning so config loads correctly.
8
+ * - Passes ZT_DB_URL to the child process; the @zerotal/testing/preload module
9
+ * reads it and wires up the DB connection before each test file runs, so
10
+ * withDatabase() and DB.table() work without manual beforeAll boilerplate.
11
+ * - If @zerotal/testing is not installed the preload is silently skipped —
12
+ * tests that use createTestApp() still work without it.
13
+ *
14
+ * All positional args and recognised flags are forwarded to bun test.
15
+ *
16
+ * @example
17
+ * ```bash
18
+ * bun zt test # run all tests
19
+ * bun zt test src/models # filter by path
20
+ * bun zt test --coverage # with coverage
21
+ * bun zt test --watch # watch mode
22
+ * ```
23
+ *
24
+ * @category Testing
25
+ */
26
+ export class TestCommand extends Command {
27
+ static commandName = "test";
28
+ static description = "Run the test suite in test environment";
29
+ static needsApp = false;
30
+
31
+ static override args = [{ name: "pattern", required: false, default: "" }];
32
+
33
+ static override flags = [
34
+ {
35
+ name: "coverage",
36
+ type: "boolean" as const,
37
+ description: "Collect test coverage",
38
+ default: false,
39
+ },
40
+ {
41
+ name: "watch",
42
+ short: "w",
43
+ type: "boolean" as const,
44
+ description: "Watch for file changes and re-run",
45
+ default: false,
46
+ },
47
+ {
48
+ name: "timeout",
49
+ type: "number" as const,
50
+ description: "Per-test timeout in milliseconds",
51
+ default: 0,
52
+ },
53
+ {
54
+ name: "bail",
55
+ type: "boolean" as const,
56
+ description: "Stop after first failure",
57
+ default: false,
58
+ },
59
+ {
60
+ name: "migrate",
61
+ type: "boolean" as const,
62
+ description: "Run database/migrations against the test database before the suite",
63
+ default: false,
64
+ },
65
+ ];
66
+
67
+ async run(): Promise<void> {
68
+ // Resolve DB URL — prefer an explicit ZT_DB_URL, then DATABASE_URL,
69
+ // then fall back to :memory: (each createTestApp() call gets its own
70
+ // in-process SQLite; preload is a no-op for that case anyway).
71
+ const dbUrl = Bun.env["ZT_DB_URL"] ?? Bun.env["DATABASE_URL"] ?? ":memory:";
72
+
73
+ if (this.flags["migrate"]) {
74
+ const migrated = await _migrateTestDatabase(dbUrl);
75
+ if (migrated === null) {
76
+ this.error("--migrate needs @zerotal/orm installed in this project.");
77
+ return;
78
+ }
79
+ this.dim(
80
+ migrated.length > 0
81
+ ? `Migrated ${migrated.length} migration(s) into ${dbUrl}`
82
+ : `Test database schema already up to date`,
83
+ );
84
+ }
85
+
86
+ const bunArguments: string[] = ["test"];
87
+
88
+ // Only add --preload when @zerotal/testing is resolvable. If it isn't
89
+ // installed, skip the preload rather than crashing bun test startup.
90
+ const preloadPath = _resolvePreload(process.cwd());
91
+ if (preloadPath) {
92
+ bunArguments.push("--preload", preloadPath);
93
+ } else {
94
+ this.warn(
95
+ "@zerotal/testing not found — skipping DB preload (add it to devDependencies for withDatabase() auto-setup)",
96
+ );
97
+ }
98
+
99
+ const pattern = this.args["pattern"];
100
+ if (pattern) bunArguments.push(pattern);
101
+
102
+ if (this.flags["coverage"]) bunArguments.push("--coverage");
103
+ if (this.flags["watch"]) bunArguments.push("--watch");
104
+ if (this.flags["bail"]) bunArguments.push("--bail");
105
+
106
+ const timeout = this.flags["timeout"] as number | undefined;
107
+ if (timeout && timeout > 0) bunArguments.push(`--timeout=${timeout}`);
108
+
109
+ this.dim(`APP_ENV=test ZT_DB_URL=${dbUrl}`);
110
+ this.dim(`bun ${bunArguments.join(" ")}\n`);
111
+
112
+ const subprocess = Bun.spawn(["bun", ...bunArguments], {
113
+ stdout: "inherit",
114
+ stderr: "inherit",
115
+ env: {
116
+ ...Bun.env,
117
+ APP_ENV: "test",
118
+ ZT_DB_URL: dbUrl,
119
+ } as Record<string, string | undefined>,
120
+ });
121
+
122
+ const code = await subprocess.exited;
123
+ // Mirror bun test's exit code — non-zero means failures or errors.
124
+ if (code !== 0) process.exit(code);
125
+ }
126
+ }
127
+
128
+ /** Try to resolve @zerotal/testing/preload from the app's working directory. */
129
+ function _resolvePreload(cwd: string): string | null {
130
+ try {
131
+ return Bun.resolveSync("@zerotal/testing/preload", cwd);
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Apply `database/migrations` to the test database before the suite starts.
139
+ *
140
+ * An `:memory:` database belongs to whichever process opened it, so there is
141
+ * nothing for a parent process to migrate — each test builds its own schema with
142
+ * `refreshDatabase({ migrate: true })`. This is for the file-backed or server
143
+ * databases where migrating once up front is both possible and much faster than
144
+ * migrating per test file.
145
+ *
146
+ * @returns The migrations applied, or `null` when the ORM is not installed.
147
+ */
148
+ async function _migrateTestDatabase(dbUrl: string): Promise<string[] | null> {
149
+ if (dbUrl === ":memory:") return [];
150
+ try {
151
+ const { SQL } = await import("bun");
152
+ // Core does not depend on the ORM — this reaches for it only when the app
153
+ // being tested has one installed. The specifier lives in a variable so it
154
+ // stays a runtime lookup rather than a compile-time dependency.
155
+ const ormSpecifier = "@zerotal/orm";
156
+ const orm = (await import(ormSpecifier)) as {
157
+ MigrationRunner: new (o: { connection: unknown }) => {
158
+ runFromDirectory(dir: string): Promise<string[]>;
159
+ };
160
+ _setDbConnection(conn: unknown): void;
161
+ _setBaseModelConnection(conn: unknown): void;
162
+ };
163
+ const connection = new SQL(dbUrl);
164
+ // A migration's `up()` reaches for the ambient connection through `Schema`,
165
+ // so it has to be installed, not just handed to the runner.
166
+ orm._setDbConnection(connection);
167
+ orm._setBaseModelConnection(connection);
168
+ return await new orm.MigrationRunner({ connection }).runFromDirectory("database/migrations");
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `worker` command, which boots the app in the worker environment to
3
+ * process background jobs.
4
+ */
5
+ import { Command } from "../Command.ts";
6
+
7
+ /**
8
+ * `bun zt worker` — boots the app in the worker environment to process
9
+ * background jobs. Aliased as `queue:work`.
10
+ *
11
+ * @category Serving
12
+ */
13
+ export class WorkerCommand extends Command {
14
+ static commandName = "worker";
15
+ static description = "Start the background job worker process";
16
+ static aliases = ["queue:work"];
17
+ static needsApp = true;
18
+
19
+ async run(): Promise<void> {
20
+ // Job classes self-register via the `jobs` convention concern during boot
21
+ // (bootAsWorker → conventions import every app/jobs/*.ts) — no codegen needed.
22
+ this.info("Starting Zerotal worker...");
23
+ await (this.app as import("../../application/Application.ts").Application).bootAsWorker();
24
+ // Keep the process alive — bootAsWorker() registers SIGTERM/SIGINT handlers.
25
+ await new Promise<never>(() => {});
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Built-in CLI commands shipped with Zerotal, published as the
3
+ * `@zerotal/core/commands` subpath.
4
+ *
5
+ * These are the command classes the framework registers automatically during
6
+ * {@link CommandRunner.boot} — the `make:*` scaffolders, `serve`/`worker`
7
+ * process launchers, and diagnostics like `status`, `reload`, and `route:list`.
8
+ * They are all subclasses of the {@link Command} base class and are invoked
9
+ * through the scaffolded app's CLI entry point, `zt.ts`, as `bun zt <command>`.
10
+ *
11
+ * Which commands are available depends on the boot mode: `serve`, `reload`,
12
+ * `status`, and `route:list` are registered in every environment, while the
13
+ * scaffolding and worker commands are only registered outside `web` mode.
14
+ *
15
+ * @example
16
+ * ```bash
17
+ * # Start the dev server
18
+ * bun zt serve --dev
19
+ *
20
+ * # Scaffold a controller and list the registered routes
21
+ * bun zt make:controller PostController --resource
22
+ * bun zt route:list
23
+ * ```
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+ export { ServeCommand } from "./ServeCommand.ts";
28
+ export { ReplCommand } from "./ReplCommand.ts";
29
+ export { StartCommand } from "./StartCommand.ts";
30
+ export { WorkerCommand } from "./WorkerCommand.ts";
31
+ export { CompileCommand } from "./CompileCommand.ts";
32
+ export { KeyGenerateCommand } from "./KeyGenerateCommand.ts";
33
+ export { ReloadCommand } from "./ReloadCommand.ts";
34
+ export { StatusCommand } from "./StatusCommand.ts";
35
+ export { MakeControllerCommand } from "./MakeControllerCommand.ts";
36
+ export { MakeMiddlewareCommand } from "./MakeMiddlewareCommand.ts";
37
+ export { MakeCommandCommand } from "./MakeCommandCommand.ts";
38
+ export { MakeRequestCommand } from "./MakeRequestCommand.ts";
39
+ export { MakeEventCommand } from "./MakeEventCommand.ts";
40
+ export { MakeListenerCommand } from "./MakeListenerCommand.ts";
41
+ export { MakeJobCommand } from "./MakeJobCommand.ts";
42
+
43
+ export { MakePolicyCommand } from "./MakePolicyCommand.ts";
44
+ export { MakeNotificationCommand } from "./MakeNotificationCommand.ts";
45
+ export { MakeObserverCommand } from "./MakeObserverCommand.ts";
46
+ export { MakeResourceCommand } from "./MakeResourceCommand.ts";
47
+ export { MakeTestCommand } from "./MakeTestCommand.ts";
48
+ export { TestCommand } from "./TestCommand.ts";
49
+ export { RouteListCommand } from "./RouteListCommand.ts";
50
+ export { MakeProviderCommand } from "./MakeProviderCommand.ts";
51
+ export { CssBuildCommand } from "./CssBuildCommand.ts";
52
+ export { LintPackagesCommand } from "./LintPackagesCommand.ts";
53
+ export { MakePackageCommand } from "./MakePackageCommand.ts";
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bun
2
+ // worker.ts
3
+ // ─────────────────────────────────────────────────────────────────
4
+ // Background worker process — runs alongside index.ts in production.
5
+ // Handles queue jobs, scheduled tasks, and long-running background work.
6
+ //
7
+ // Start: bun worker.ts
8
+ // The process stays alive until SIGTERM/SIGINT.
9
+ // ─────────────────────────────────────────────────────────────────
10
+ import app from './bootstrap/app.ts';
11
+
12
+ await app.bootAsWorker();
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bun
2
+ // zerotal.ts
3
+ // ─────────────────────────────────────────────────────────────────
4
+ // DO NOT MODIFY THIS FILE.
5
+ // This file is managed by the Zerotal framework.
6
+ //
7
+ // To add custom commands, create classes in app/commands/ and
8
+ // register them in your AppServiceProvider.onBooted():
9
+ //
10
+ // override async onBooted(): Promise<void> {
11
+ // if (this.app._env === 'console') {
12
+ // const runner = this.app.container.makeSync('commands');
13
+ // const { MyCommand } = await import('../commands/MyCommand.ts');
14
+ // runner.register(MyCommand);
15
+ // }
16
+ // }
17
+ //
18
+ // Help: bun zerotal.ts list
19
+ // bun zerotal.ts help <command>
20
+ // ─────────────────────────────────────────────────────────────────
21
+ import app from './bootstrap/app.ts';
22
+ import { CommandRunner } from '@zerotal/core';
23
+
24
+ const runner = new CommandRunner(app);
25
+ await runner.boot();
26
+ await runner.run(process.argv.slice(2));
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The single entry point an app's `zt.ts` calls. Encapsulates the full CLI
3
+ * boot sequence — config load + validate, runtime-env resolution, app import,
4
+ * and command dispatch — so app entry files stay a one-liner and the
5
+ * orchestration lives in one place.
6
+ */
7
+ import { CommandRunner } from "./CommandRunner.ts";
8
+ import { configLoader } from "../config/ConfigLoader.ts";
9
+ import { setAppEnv } from "../helpers/index.ts";
10
+ import type { Application } from "../application/Application.ts";
11
+
12
+ /** Options for {@link startZerotal}. */
13
+ export interface StartZerotalOptions {
14
+ /** Directory to load config from, resolved against the cwd. Default: `./config`. */
15
+ configDir?: string;
16
+ }
17
+
18
+ /**
19
+ * Boot and run the Zerotal CLI for an app.
20
+ *
21
+ * `loadApp` must **dynamically import** the app's bootstrap module
22
+ * (e.g. `() => import("./bootstrap/app")`). Passing a thunk rather than the app
23
+ * itself is deliberate and load-bearing:
24
+ * - it guarantees `APP_ENV` is set (from the command) *before* the app module
25
+ * evaluates, since ES imports are hoisted; and
26
+ * - the literal `import("./bootstrap/app")` stays statically analysable, so
27
+ * `bun build --compile` bundles the app into the production binary.
28
+ *
29
+ * @example
30
+ * // zt.ts
31
+ * import { startZerotal } from "@zerotal/core";
32
+ * await startZerotal(() => import("./bootstrap/app"));
33
+ */
34
+ export async function startZerotal(
35
+ loadApp: () => Promise<{ default: Application }>,
36
+ options: StartZerotalOptions = {},
37
+ ): Promise<void> {
38
+ // Load + validate config synchronously — safe before the app boots.
39
+ const config = configLoader(options.configDir ?? "./config");
40
+ config.validate();
41
+
42
+ // Resolve the runtime boot mode (web / worker / console) from the command
43
+ // BEFORE importing the app module, which reads APP_ENV at evaluation time.
44
+ const command = process.argv[2] ?? "";
45
+ setAppEnv(command);
46
+
47
+ // Import the app now that APP_ENV is locked in, then inject the loaded config.
48
+ // (Job classes self-register during boot via the `jobs` convention concern.)
49
+ const { default: app } = await loadApp();
50
+ app.useConfig(config.all());
51
+
52
+ const runner = new CommandRunner(app);
53
+ await runner.boot();
54
+ await runner.run(process.argv.slice(2));
55
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The application-level configuration shape and its factory: the `app`
3
+ * namespace that every Zerotal app carries (identity, server/TLS, middleware
4
+ * defaults, conventions), with sensible defaults applied over caller overrides.
5
+ */
6
+ import { deepMerge } from "../support/deepMerge.ts";
7
+ import type { HealthConfigShape } from "../health/Health.ts";
8
+
9
+ /** TLS certificate and key paths that enable HTTPS when provided. */
10
+ export interface AppTlsConfig {
11
+ /** Path to the TLS certificate file. */
12
+ cert: string;
13
+ /** Path to the TLS private key file. */
14
+ key: string;
15
+ }
16
+
17
+ /** Auto-discovery (convention) settings. See docs/conventions.md. */
18
+ export interface ConventionsConfig {
19
+ /** Master switch for convention-based auto-registration. Default: true */
20
+ enabled: boolean;
21
+ /** Per-concern directory overrides, relative to the app root. */
22
+ paths: {
23
+ providers: string;
24
+ middleware: string;
25
+ models: string;
26
+ observers: string;
27
+ policies: string;
28
+ listeners: string;
29
+ events: string;
30
+ jobs: string;
31
+ schedules: string;
32
+ validators: string;
33
+ };
34
+ }
35
+
36
+ /** App-level CORS defaults (consumed by `CorsMiddleware` when registered without options). */
37
+ export interface AppCorsConfig {
38
+ origin: string | string[];
39
+ credentials: boolean;
40
+ }
41
+
42
+ /** App-level rate-limit defaults (consumed by `ThrottleMiddleware`). */
43
+ export interface AppThrottleConfig {
44
+ maxAttempts: number;
45
+ windowSeconds: number;
46
+ }
47
+
48
+ /** App-level security-header defaults (consumed by `SecureHeadersMiddleware`). */
49
+ export interface AppSecureHeadersConfig {
50
+ frameOptions: "DENY" | "SAMEORIGIN";
51
+ }
52
+
53
+ /**
54
+ * Front-end asset bundling. When set, `bun zerotal serve` bundles the
55
+ * entrypoint(s) with Bun's native bundler before starting (and rebuilds them on
56
+ * change under `serve --dev`). A JS/TS entry may `import` its CSS — Bun emits a
57
+ * sibling stylesheet. Tailwind v4 is picked up automatically when
58
+ * `bun-plugin-tailwind` is installed in the app.
59
+ */
60
+ export interface AppAssetsConfig {
61
+ /** Entry file(s) to bundle, relative to the app root (e.g. `resources/js/app.ts`). */
62
+ entrypoint: string | string[];
63
+ /** Output directory for built bundles, relative to the app root. Default: `public`. */
64
+ outDir: string;
65
+ /** URL prefix the built assets are served under. Default: `/`. */
66
+ prefix: string;
67
+ /** Minify output. Default: true in production, false otherwise. */
68
+ minify: boolean;
69
+ }
70
+
71
+ /**
72
+ * Default request-body ceiling: 8 MiB.
73
+ *
74
+ * Chosen to comfortably fit a JSON API payload, a form post and an ordinary image upload
75
+ * while keeping the worst case a single request can pin in memory to something a small
76
+ * instance survives under concurrency. Bun's own 128 MiB default does not.
77
+ */
78
+ export const DEFAULT_MAX_REQUEST_BODY_SIZE = 8 * 1024 * 1024;
79
+
80
+ /**
81
+ * CORS defaults: same-origin only.
82
+ *
83
+ * `origin: "*"` was the previous default, which hands every browser on the internet a
84
+ * read of any endpoint that does not separately require a credential. An app that wants
85
+ * cross-origin access names the origins it means — the empty list is the honest default
86
+ * because the framework cannot know them.
87
+ */
88
+ const DEFAULT_CORS: AppCorsConfig = { origin: [], credentials: false };
89
+ const DEFAULT_THROTTLE: AppThrottleConfig = { maxAttempts: 120, windowSeconds: 60 };
90
+ const DEFAULT_SECURE_HEADERS: AppSecureHeadersConfig = { frameOptions: "SAMEORIGIN" };
91
+
92
+ const DEFAULT_CONVENTION_PATHS: ConventionsConfig["paths"] = {
93
+ providers: "app/providers",
94
+ middleware: "app/middleware",
95
+ models: "app/models",
96
+ observers: "app/observers",
97
+ policies: "app/policies",
98
+ listeners: "app/listeners",
99
+ events: "app/events",
100
+ jobs: "app/jobs",
101
+ schedules: "app/schedules",
102
+ validators: "app/validators",
103
+ };
104
+
105
+ // ── Full shape (resolved — what the config store holds after AppConfig()) ────
106
+
107
+ /** The fully resolved `app` configuration as held in the config store. */
108
+ export interface AppConfigShape {
109
+ // ── Identity ──────────────────────────────────────────────────────────────
110
+ /** Human-readable application name. */
111
+ name: string;
112
+ /** Runtime environment string. Mirrors APP_ENV. */
113
+ env: string;
114
+ /** Application secret key used for signing sessions / tokens. */
115
+ key: string;
116
+ /** Show verbose error pages and stack traces. */
117
+ debug: boolean;
118
+ /** Canonical public URL (e.g. https://myapp.com). */
119
+ url: string;
120
+ /** Default HTTP port. Overridden by --port CLI flag. Default: 3000 */
121
+ port: number;
122
+ /** Application locale used for date/number formatting. Default: 'en' */
123
+ locale: string;
124
+ /** Application timezone. Default: 'UTC' */
125
+ timezone: string;
126
+
127
+ // ── Server / TLS ──────────────────────────────────────────────────────────
128
+ /** Enable HTTP/3 over QUIC. Requires a valid tls config. Default: false */
129
+ http3: boolean;
130
+ /** TLS certificate + key for HTTPS. Enables HTTPS when set. */
131
+ tls?: AppTlsConfig;
132
+ /**
133
+ * Largest request body the server will accept, in bytes. Default: 8 MiB.
134
+ *
135
+ * Bodies are fully buffered before a handler sees them — a multipart upload
136
+ * materialises every file in RAM before `ctx.file()` reads a byte — so this is the
137
+ * ceiling on memory a single request can claim. Bun's own default is 128 MiB, which
138
+ * twenty concurrent POSTs turn into 2.5 GB resident on any route, authenticated or not.
139
+ *
140
+ * Raise it for an app that genuinely accepts large uploads, and prefer raising it on the
141
+ * upload route's proxy rather than globally.
142
+ */
143
+ maxRequestBodySize: number;
144
+ /**
145
+ * Health endpoint configuration. Pass an object to configure it
146
+ * (`enabled` / `path` / `secret` / `showDetails`); `enabled` defaults to on
147
+ * outside production. A bare `true`/`false` is shorthand for enable/disable
148
+ * with the other defaults. Default: `false`.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * health: {
153
+ * enabled: true, // default: !production
154
+ * path: "/health", // default: '/health'
155
+ * secret: env("HEALTH_KEY"), // required in production
156
+ * showDetails: true, // false → bare { "status": "ok" }
157
+ * }
158
+ * ```
159
+ */
160
+ health: boolean | HealthConfigShape;
161
+
162
+ // ── Middleware defaults ───────────────────────────────────────────────────
163
+ /** CORS defaults applied by `CorsMiddleware` when registered without explicit options. */
164
+ cors: AppCorsConfig;
165
+ /** Rate-limit defaults applied by `ThrottleMiddleware`. */
166
+ throttle: AppThrottleConfig;
167
+ /** Security-header defaults applied by `SecureHeadersMiddleware`. */
168
+ secureHeaders: AppSecureHeadersConfig;
169
+
170
+ // ── Assets ────────────────────────────────────────────────────────────────
171
+ /** Front-end asset bundling, built on `serve`. Omitted means no asset build. */
172
+ assets?: AppAssetsConfig;
173
+
174
+ // ── Conventions ───────────────────────────────────────────────────────────
175
+ /** Auto-discovery settings (enabled + per-concern paths). */
176
+ conventions: ConventionsConfig;
177
+ }
178
+
179
+ // ── AppConfig factory ─────────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Build the application-level config with sensible defaults.
183
+ *
184
+ * Pass overrides to customise any setting. Every field has a default that works
185
+ * out of the box so the minimum viable config is just `AppConfig({})`.
186
+ *
187
+ * @param options Partial overrides; every field has a working default.
188
+ * @returns The fully resolved {@link AppConfigShape} with defaults applied.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * // config/app.ts
193
+ * import { AppConfig, env } from '@zerotal/core';
194
+ *
195
+ * export default AppConfig({
196
+ * name: 'My App',
197
+ * url: env('APP_URL', 'http://localhost:3000'),
198
+ * });
199
+ * ```
200
+ */
201
+ export function AppConfig(options: {
202
+ name?: string;
203
+ env?: string;
204
+ key?: string;
205
+ debug?: boolean;
206
+ url?: string;
207
+ port?: number;
208
+ locale?: string;
209
+ timezone?: string;
210
+ http3?: boolean;
211
+ tls?: AppTlsConfig;
212
+ maxRequestBodySize?: number;
213
+ health?: boolean | HealthConfigShape;
214
+ cors?: Partial<AppCorsConfig>;
215
+ throttle?: Partial<AppThrottleConfig>;
216
+ secureHeaders?: Partial<AppSecureHeadersConfig>;
217
+ assets?: { entrypoint: string | string[]; outDir?: string; prefix?: string; minify?: boolean };
218
+ conventions?: { enabled?: boolean; paths?: Partial<ConventionsConfig["paths"]> };
219
+ }): AppConfigShape {
220
+ // Resolve env-derived defaults, then deep-merge the caller's overrides so partial nested
221
+ // overrides (e.g. `conventions.paths.models`) keep every other default in place.
222
+ const defaults: AppConfigShape = {
223
+ name: "Zerotal App",
224
+ env: Bun.env["APP_ENV"] ?? "development",
225
+ key: Bun.env["APP_KEY"] ?? "",
226
+ debug: Bun.env["APP_DEBUG"] !== "false",
227
+ url: Bun.env["APP_URL"] ?? "http://localhost:3000",
228
+ port: 3000,
229
+ locale: "en",
230
+ timezone: "UTC",
231
+ http3: false,
232
+ maxRequestBodySize: DEFAULT_MAX_REQUEST_BODY_SIZE,
233
+ health: false,
234
+ cors: DEFAULT_CORS,
235
+ throttle: DEFAULT_THROTTLE,
236
+ secureHeaders: DEFAULT_SECURE_HEADERS,
237
+ conventions: { enabled: true, paths: DEFAULT_CONVENTION_PATHS },
238
+ };
239
+ const resolved = deepMerge(defaults, options as Partial<AppConfigShape>);
240
+
241
+ // Normalise the optional `assets` block: fill outDir/prefix/minify defaults
242
+ // only when the app actually declares an asset entrypoint.
243
+ if (options.assets) {
244
+ const isProduction = resolved.env === "production" || resolved.env === "prod";
245
+ resolved.assets = {
246
+ entrypoint: options.assets.entrypoint,
247
+ outDir: options.assets.outDir ?? "public",
248
+ prefix: options.assets.prefix ?? "/",
249
+ minify: options.assets.minify ?? isProduction,
250
+ };
251
+ }
252
+ return resolved;
253
+ }