@zerotal/core 1.4.0 → 1.5.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 (60) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/package.json +1 -1
  3. package/src/application/Application.ts +107 -9
  4. package/src/application/DevErrorPage.ts +82 -0
  5. package/src/application/diagnostics.ts +111 -0
  6. package/src/command/CommandRunner.ts +82 -1
  7. package/src/command/builtin/AssetsBuildCommand.ts +102 -0
  8. package/src/command/builtin/DeployCommand.ts +315 -0
  9. package/src/command/builtin/DevCommand.ts +88 -0
  10. package/src/command/builtin/DoctorCommand.ts +97 -0
  11. package/src/command/builtin/MakeCommandCommand.ts +2 -0
  12. package/src/command/builtin/RouteTypesCommand.ts +56 -0
  13. package/src/command/builtin/ServeCommand.ts +232 -44
  14. package/src/command/builtin/index.ts +5 -0
  15. package/src/command/scaffold/zerotal.ts.txt +2 -10
  16. package/src/config/AppConfig.ts +109 -2
  17. package/src/config/DeployConfig.ts +71 -0
  18. package/src/config/index.ts +2 -0
  19. package/src/config/registry.ts +1 -0
  20. package/src/container/Container.ts +3 -3
  21. package/src/container/inject.ts +3 -2
  22. package/src/context/RequestContext.ts +60 -0
  23. package/src/contracts/session.ts +18 -3
  24. package/src/dev/BuildCache.ts +312 -0
  25. package/src/dev/CssPlugins.ts +93 -7
  26. package/src/dev/DevBuildHook.ts +14 -1
  27. package/src/dev/DevDeck.ts +549 -0
  28. package/src/dev/DevOrchestrator.ts +166 -31
  29. package/src/dev/DevProcess.ts +221 -0
  30. package/src/dev/DevReloadMiddleware.ts +1 -1
  31. package/src/dev/DevSupervisor.ts +363 -0
  32. package/src/dev/bootBuild.ts +94 -0
  33. package/src/dev/index.ts +24 -0
  34. package/src/dev/startDevMode.ts +145 -0
  35. package/src/doctor/AppDoctor.ts +399 -0
  36. package/src/doctor/TransportProbe.ts +169 -0
  37. package/src/events/Emitter.ts +4 -3
  38. package/src/facade/facades/App.ts +10 -2
  39. package/src/helpers/index.ts +23 -1
  40. package/src/helpers/response.ts +18 -8
  41. package/src/http/Uri.ts +7 -3
  42. package/src/http/originGuard.ts +1 -1
  43. package/src/http/url.ts +10 -4
  44. package/src/index.ts +43 -0
  45. package/src/lock/LockManager.ts +190 -14
  46. package/src/lock/drivers/LockDriver.ts +11 -0
  47. package/src/lock/drivers/MemoryLockDriver.ts +21 -1
  48. package/src/lock/drivers/RedisLockDriver.ts +64 -8
  49. package/src/lock/drivers/SqliteLockDriver.ts +13 -0
  50. package/src/lock/errors.ts +26 -0
  51. package/src/lock/facades/Lock.ts +30 -5
  52. package/src/lock/index.ts +2 -2
  53. package/src/macros/config.macro.ts +2 -0
  54. package/src/provider/ServiceProvider.ts +40 -0
  55. package/src/router/Router.ts +111 -13
  56. package/src/router/registry.ts +123 -0
  57. package/src/router/routeTypes.ts +132 -0
  58. package/src/support/classRef.ts +27 -0
  59. package/src/support/env.ts +69 -2
  60. package/src/support/unroutedRoutes.ts +37 -0
@@ -13,8 +13,12 @@
13
13
  * suppressed. Accepts `"production"`, `"prod"`, and `"staging"`,
14
14
  * case-insensitively.
15
15
  *
16
+ * Pass {@link deployEnv}, not `Bun.env["APP_ENV"]` — after `setAppEnv()` the latter
17
+ * holds a runtime mode, and `isProdLike("web")` is `false` for every deployment.
18
+ *
19
+ * @internal
16
20
  * @example
17
- * if (!isProdLike(Bun.env["APP_ENV"] ?? "")) return renderDevErrorPage(error);
21
+ * if (!isProdLike(deployEnv())) return renderDevErrorPage(error);
18
22
  */
19
23
  export function isProdLike(env: string): boolean {
20
24
  const normalized = env.trim().toLowerCase();
@@ -43,6 +47,34 @@ export function isDevSurfaceAllowed(env: string): boolean {
43
47
  */
44
48
  export const DEV_WORKER_ENV_VAR = "ZT_DEV";
45
49
 
50
+ /**
51
+ * Where `setAppEnv()` parks the deployment name before overwriting `APP_ENV`.
52
+ * @internal
53
+ */
54
+ export const DEPLOY_ENV_VAR = "ZT_APP_ENV";
55
+
56
+ /**
57
+ * The deployment name this process was started with — `production`, `staging`,
58
+ * `local`, whatever the operator set — as opposed to the runtime *mode*.
59
+ *
60
+ * `APP_ENV` carries both meanings, and the second one destroys the first:
61
+ * `setAppEnv()` overwrites it with `web` / `console` / `worker` before the app
62
+ * boots, so a gate that asks `isProdLike(Bun.env["APP_ENV"])` after startup is
63
+ * asking whether `"web"` is production and always getting no. That was not
64
+ * theoretical — it silently disabled the weak-`APP_KEY` refusal and left the
65
+ * ORM's N+1 detector wrapping every query in production.
66
+ *
67
+ * `setAppEnv()` now preserves the original value, and this reads it back. Prefer
68
+ * it to `Bun.env["APP_ENV"]` for **any** production decision. Config is an
69
+ * equally correct source where it is available (`config("app.env")`), but this
70
+ * works before config is loaded and in processes that have none.
71
+ *
72
+ * @internal
73
+ */
74
+ export function deployEnv(): string {
75
+ return Bun.env[DEPLOY_ENV_VAR] ?? Bun.env["APP_ENV"] ?? "";
76
+ }
77
+
46
78
  /**
47
79
  * Whether *this process* may expose dev-only surfaces — the stack-trace error
48
80
  * page, the trace inspector, an open monitor panel.
@@ -56,7 +88,9 @@ export const DEV_WORKER_ENV_VAR = "ZT_DEV";
56
88
  * construction. This is the case that carries dev mode, because `APP_ENV`
57
89
  * cannot: `setAppEnv()` overwrites it with a *runtime mode* (`web`,
58
90
  * `worker`, `console`) before the app boots, so by the time any gate reads
59
- * it, whatever deployment name the developer configured is gone.
91
+ * `APP_ENV` it holds the mode rather than the deployment name. (The name
92
+ * itself is not lost — `setAppEnv()` parks it, and {@link deployEnv} reads
93
+ * it back. What is lost is the ability to learn it from `APP_ENV`.)
60
94
  *
61
95
  * - `APP_ENV` still names an explicitly non-production environment. This
62
96
  * covers processes started outside the CLI — the test harness, and any
@@ -69,3 +103,36 @@ export function devSurfacesEnabled(): boolean {
69
103
  if (Bun.env[DEV_WORKER_ENV_VAR] === "1") return true;
70
104
  return isDevSurfaceAllowed(Bun.env["APP_ENV"] ?? "");
71
105
  }
106
+
107
+ /**
108
+ * Whether a `DevOrchestrator` owns asset builds for this process.
109
+ *
110
+ * View providers build their bundles once at boot so assets are ready before
111
+ * the first request. Under `serve --dev` that is redundant three times over:
112
+ * the orchestrator process boots the app (registering hooks, and building), then
113
+ * runs the hooks itself, then spawns a worker that boots the app and builds
114
+ * again. Every backend save paid for two of those.
115
+ *
116
+ * Both processes are recognised, for different reasons:
117
+ *
118
+ * - The supervised worker carries {@link DEV_WORKER_ENV_VAR}. The orchestrator
119
+ * has already built and pruned before spawning it, so its assets are on disk
120
+ * before it binds a port.
121
+ * - The orchestrator itself is recognised from `argv`, not an environment
122
+ * variable, because providers boot *before* `ServeCommand.run()` gets to set
123
+ * one — the flag would always arrive too late to be read.
124
+ *
125
+ * Anything else — a plain `serve`, a queue worker, a test — is unsupervised and
126
+ * still builds at boot.
127
+ *
128
+ * @param env Environment to read; defaults to the process environment.
129
+ * @param argv Command line to read; defaults to the process command line.
130
+ * @internal
131
+ */
132
+ export function isDevOrchestrated(
133
+ env: Record<string, string | undefined> = Bun.env,
134
+ argv: readonly string[] = Bun.argv,
135
+ ): boolean {
136
+ if (env[DEV_WORKER_ENV_VAR] === "1") return true;
137
+ return argv.includes("serve") && argv.includes("--dev");
138
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Unrouted `routes/` directory check.
3
+ *
4
+ * A conventional `routes/index.ts` full of `Router.get(...)` calls does nothing
5
+ * until a `routing()` group imports it — the file typechecks, the calls would
6
+ * execute cleanly, and every path in it 404s in a way that is indistinguishable
7
+ * from a typo'd URL. The scaffold wires only `fileBasedRouting()`, so "I made a
8
+ * routes file" and "it needs registering" are otherwise never connected.
9
+ */
10
+ import { readdirSync } from "node:fs";
11
+ import { resolve, sep } from "node:path";
12
+
13
+ /**
14
+ * A warning message when `<root>/routes` holds route files that no `routing()`
15
+ * group loads, or `null` when the directory is absent, empty, or covered.
16
+ */
17
+ export function unroutedRoutesWarning(root: string, routedFiles: string[]): string | null {
18
+ const dir = resolve(root, "routes");
19
+ let files: string[];
20
+ try {
21
+ files = readdirSync(dir).filter((f) => /\.(ts|js)$/.test(f) && !/\.test\.(ts|js)$/.test(f));
22
+ } catch {
23
+ return null; // no routes/ directory — nothing to warn about
24
+ }
25
+ if (files.length === 0) return null;
26
+ const insideDir = (file: string) => {
27
+ const normalized = resolve(file);
28
+ return normalized === dir || normalized.startsWith(dir + sep);
29
+ };
30
+ if (routedFiles.some(insideDir)) return null;
31
+ const suggestion = files.includes("index.ts") ? "index.ts" : files[0];
32
+ return (
33
+ `routes/ contains ${files.join(", ")} but no routing() group loads it — ` +
34
+ `every route in it will 404. Add .routing("./routes/${suggestion}") to your ` +
35
+ `Application, or remove the directory.`
36
+ );
37
+ }