@zerotal/core 1.7.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,35 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Fixed
12
+
13
+ - **`make:request` generates a file that compiles.** The stub imported `@zerotal/validator`,
14
+ which a scaffolded app does not depend on — it depends on the `zerotal` umbrella — so the
15
+ generated file failed to resolve until the import was changed by hand. It also annotated
16
+ `rules(): Record<string, FieldRule>`, the one thing `FormRequest`'s own docblock warns
17
+ against: `validate()` reads the narrow return type through `ReturnType<T['rules']>`, so the
18
+ annotation widened it back and every validated field arrived as `unknown`, silently, with a
19
+ cast somewhere downstream the first sign of it.
20
+
21
+ - **Every other `make:*` stub names a package the app has too.** The same fault ran through
22
+ nine generators: `make:command`, `make:controller`, `make:middleware` and `make:provider`
23
+ named `@zerotal/core`; `make:job` `@zerotal/queue`; `make:observer` `@zerotal/orm`;
24
+ `make:policy` `@zerotal/auth`; `make:test` `@zerotal/testing`. A scaffolded app depends on
25
+ none of them — it has the `zerotal` umbrella — so each wrote a file that did not resolve.
26
+ They now emit `zerotal` and its subpaths.
27
+
28
+ `make:resource` was worse: `Resource`, `ResourceCollection` and `PaginatedData` are not on
29
+ `@zerotal/core`'s root entry at all, so that stub was broken against the scoped name as
30
+ well. It emits `zerotal/http`, where they live.
31
+
32
+ `make:notification` keeps `@zerotal/notifications`: there is no umbrella subpath for it, and
33
+ the `api` template installs it directly.
34
+
35
+ A single test now runs all thirteen generators and fails on any import that is neither the
36
+ umbrella, a Bun/Node builtin, a relative path, nor one of the scoped packages a template
37
+ actually installs. Each generator's own test had only checked that the output mentioned the
38
+ class being made, which is why none of this showed.
39
+
11
40
  ### Added
12
41
 
13
42
  - **`@zerotal/core/errors`** — a subpath for the error classes, so a module that can run in a
@@ -19,7 +48,6 @@ follows the Zerotal monorepo's unified versioning.
19
48
  The rule this makes workable: **core's root entry is server-only.** Anything that might be
20
49
  bundled for a browser imports from a narrow subpath.
21
50
 
22
-
23
51
  ## [1.7.1] — 2026-08-16
24
52
 
25
53
  ### Changed
package/api-surface.md CHANGED
@@ -316,6 +316,7 @@ class HttpContext = {
316
316
  new <TParams extends Record<string, unknown> = Record<string, string>>(request: Request, container: ScopedResolver): HttpContext<TParams>
317
317
  static fake: (url?: string, init?: RequestInit, container?: ScopedResolver) => HttpContext
318
318
  static tryGet: () => HttpContext | undefined
319
+ __: (key: string, replacements?: Replacements, locale?: string) => string
319
320
  _afterResponseCallbacks: (() => Promise<void>)[]
320
321
  _pageResolver?: (pageName: string) => number | undefined
321
322
  _primeBody: (data: Record<string, unknown>) => void
@@ -368,7 +369,6 @@ class HttpContext = {
368
369
  string: (key: string, fallback?: string) => string | undefined
369
370
  subdomain: (name: string) => string | null
370
371
  subdomains: Record<string, string>
371
- t: (key: string, replacements?: Replacements, locale?: string) => string
372
372
  took: number
373
373
  user?: UserModel | undefined
374
374
  view: { (markup: ViewMarkup, status?: number): void; <P extends Record<string, unknown> = Record<string, never>>(component: (ctx: HttpContext, props: P) => ViewMarkup | Promise<ViewMarkup>, props?: P | undefined, status?: number): void | Promise<void>;}
@@ -1101,6 +1101,7 @@ interface WebhookOptions = {
1101
1101
  interface WebSocketHandlers = {
1102
1102
  close?: (ws: unknown, code: number, reason: string) => void
1103
1103
  drain?: (ws: unknown) => void
1104
+ idleTimeout?: number
1104
1105
  message: (ws: unknown, message: string | Uint8Array) => void
1105
1106
  open?: (ws: unknown) => void
1106
1107
  }
@@ -2649,6 +2650,207 @@ type FieldType = 'string' | 'number' | 'boolean' | 'url' | 'enum' | 'port'
2649
2650
 
2650
2651
  type InferDef = D extends Def<infer T> ? T : never
2651
2652
 
2653
+ ## ./errors `(./src/errors/index.ts)`
2654
+
2655
+ class BadRequestError = {
2656
+ new (message?: string): BadRequestError
2657
+ headers?: Record<string, string>
2658
+ readonly code: string
2659
+ readonly context?: Record<string, unknown> | undefined
2660
+ readonly status: number
2661
+ }
2662
+
2663
+ class BindingNotFoundError = {
2664
+ new (token: string): BindingNotFoundError
2665
+ readonly code: string
2666
+ readonly context?: Record<string, unknown> | undefined
2667
+ readonly status: number
2668
+ }
2669
+
2670
+ class BootCheckError = {
2671
+ new (failures: BootCheckFailure[]): BootCheckError
2672
+ readonly code: string
2673
+ readonly context?: Record<string, unknown> | undefined
2674
+ readonly failures: BootCheckFailure[]
2675
+ readonly status: number
2676
+ }
2677
+
2678
+ class CircularDependencyError = {
2679
+ new (chain: string[]): CircularDependencyError
2680
+ readonly code: string
2681
+ readonly context?: Record<string, unknown> | undefined
2682
+ readonly status: number
2683
+ }
2684
+
2685
+ class ConfigError = {
2686
+ new (message: string): ConfigError
2687
+ readonly code: string
2688
+ readonly context?: Record<string, unknown> | undefined
2689
+ readonly status: number
2690
+ }
2691
+
2692
+ class ConfigValidationError = {
2693
+ new (issues: Array<{ namespace: string; message: string; }>): ConfigValidationError
2694
+ readonly code: string
2695
+ readonly context?: Record<string, unknown> | undefined
2696
+ readonly issues: { namespace: string; message: string;}[]
2697
+ readonly status: number
2698
+ }
2699
+
2700
+ class ConflictError = {
2701
+ new (message?: string): ConflictError
2702
+ headers?: Record<string, string>
2703
+ readonly code: string
2704
+ readonly context?: Record<string, unknown> | undefined
2705
+ readonly status: number
2706
+ }
2707
+
2708
+ class ContainerLockedError = {
2709
+ new (method: string): ContainerLockedError
2710
+ readonly code: string
2711
+ readonly context?: Record<string, unknown> | undefined
2712
+ readonly status: number
2713
+ }
2714
+
2715
+ class ContextOutsideRequestError = {
2716
+ new (): ContextOutsideRequestError
2717
+ headers?: Record<string, string>
2718
+ readonly code: string
2719
+ readonly context?: Record<string, unknown> | undefined
2720
+ readonly status: number
2721
+ }
2722
+
2723
+ class FacadeAccessedBeforeBootError = {
2724
+ new (facadeKey: string): FacadeAccessedBeforeBootError
2725
+ readonly code: string
2726
+ readonly context?: Record<string, unknown> | undefined
2727
+ readonly status: number
2728
+ }
2729
+
2730
+ class FacadeBindingMissingError = {
2731
+ new (facadeKey: string): FacadeBindingMissingError
2732
+ readonly code: string
2733
+ readonly context?: Record<string, unknown> | undefined
2734
+ readonly status: number
2735
+ }
2736
+
2737
+ class ForbiddenError = {
2738
+ new (message?: string): ForbiddenError
2739
+ headers?: Record<string, string>
2740
+ readonly code: string
2741
+ readonly context?: Record<string, unknown> | undefined
2742
+ readonly status: number
2743
+ }
2744
+
2745
+ class GoneError = {
2746
+ new (message?: string): GoneError
2747
+ headers?: Record<string, string>
2748
+ readonly code: string
2749
+ readonly context?: Record<string, unknown> | undefined
2750
+ readonly status: number
2751
+ }
2752
+
2753
+ class HttpError = {
2754
+ new (message: string, status: number, code?: string, headers?: Record<string, string>): HttpError
2755
+ headers?: Record<string, string>
2756
+ readonly code: string
2757
+ readonly context?: Record<string, unknown> | undefined
2758
+ readonly status: number
2759
+ }
2760
+
2761
+ class MethodNotAllowedError = {
2762
+ new (allowed?: string[], message?: string): MethodNotAllowedError
2763
+ headers?: Record<string, string>
2764
+ readonly code: string
2765
+ readonly context?: Record<string, unknown> | undefined
2766
+ readonly status: number
2767
+ }
2768
+
2769
+ class NotFoundError = {
2770
+ new (message?: string): NotFoundError
2771
+ headers?: Record<string, string>
2772
+ readonly code: string
2773
+ readonly context?: Record<string, unknown> | undefined
2774
+ readonly status: number
2775
+ }
2776
+
2777
+ class ScopedAfterFlushError = {
2778
+ new (message: string): ScopedAfterFlushError
2779
+ readonly code: string
2780
+ readonly context?: Record<string, unknown> | undefined
2781
+ readonly status: number
2782
+ }
2783
+
2784
+ class ScopedOutsideRequestError = {
2785
+ new (message: string): ScopedOutsideRequestError
2786
+ readonly code: string
2787
+ readonly context?: Record<string, unknown> | undefined
2788
+ readonly status: number
2789
+ }
2790
+
2791
+ class ServiceUnavailableError = {
2792
+ new (reason?: string, retryAfter?: number): ServiceUnavailableError
2793
+ headers?: Record<string, string>
2794
+ readonly code: string
2795
+ readonly context?: Record<string, unknown> | undefined
2796
+ readonly retryAfter?: number | undefined
2797
+ readonly status: number
2798
+ }
2799
+
2800
+ class SyncResolutionError = {
2801
+ new (message: string): SyncResolutionError
2802
+ readonly code: string
2803
+ readonly context?: Record<string, unknown> | undefined
2804
+ readonly status: number
2805
+ }
2806
+
2807
+ class TooManyRequestsError = {
2808
+ new (retryAfter?: number, message?: string): TooManyRequestsError
2809
+ headers?: Record<string, string>
2810
+ readonly code: string
2811
+ readonly context?: Record<string, unknown> | undefined
2812
+ readonly retryAfter?: number | undefined
2813
+ readonly status: number
2814
+ }
2815
+
2816
+ class UnauthorizedError = {
2817
+ new (message?: string): UnauthorizedError
2818
+ headers?: Record<string, string>
2819
+ readonly code: string
2820
+ readonly context?: Record<string, unknown> | undefined
2821
+ readonly status: number
2822
+ }
2823
+
2824
+ class UnprocessableEntityError = {
2825
+ new (message?: string): UnprocessableEntityError
2826
+ headers?: Record<string, string>
2827
+ readonly code: string
2828
+ readonly context?: Record<string, unknown> | undefined
2829
+ readonly status: number
2830
+ }
2831
+
2832
+ class ValidationError = {
2833
+ new (message: string, errors: Record<string, string[]>): ValidationError
2834
+ headers?: Record<string, string>
2835
+ readonly code: string
2836
+ readonly context?: Record<string, unknown> | undefined
2837
+ readonly errors: Record<string, string[]>
2838
+ readonly status: number
2839
+ }
2840
+
2841
+ class ZerotalError = {
2842
+ new (message: string, code: string, status?: number, context?: Record<string, unknown> | undefined): ZerotalError
2843
+ readonly code: string
2844
+ readonly context?: Record<string, unknown> | undefined
2845
+ readonly status: number
2846
+ }
2847
+
2848
+ interface BootCheckFailure = {
2849
+ provider: string
2850
+ reason: string
2851
+ token: string
2852
+ }
2853
+
2652
2854
  ## ./facades `(./src/facade/facades/index.ts)`
2653
2855
 
2654
2856
  const App = { readonly container: Container; readonly instance: () => Application; readonly environment: () => Application['environment']; readonly isProduction: () => boolean; readonly isLocal: () => boolean; readonly make: <T>(token: BindingToken<T>, consumer?: unknown) => Promise<T>; readonly makeSync: <T>(token: BindingToken<T>) => T; readonly build: <T>(ctor: new (...args: unknown[]) => T) => Promise<T>; readonly tryMake: <K extends keyof ContainerBindings>(token: K) => ContainerBindings[K] | undefined; readonly bound: (token: BindingToken) => boolean; readonly bind: <T>(token: BindingToken<T>, factory: Factory<T>) => Container; readonly singleton: <T>(token: BindingToken<T>, factory: Factory<T>) => Container; readonly scoped: <T>(token: BindingToken<T>, factory: Factory<T>) => Container; readonly value: <T>(token: BindingToken<T>, instance: T) => Container; readonly alias: (from: unknown, to: unknown) => Container; readonly forget: (token: BindingToken) => boolean;}
@@ -3294,14 +3496,37 @@ interface HttpMetricsSnapshot = {
3294
3496
 
3295
3497
  const route = RouteBuilder
3296
3498
 
3499
+ function action = <N extends RouteTarget>(name: N, params?: RouteParamValues | undefined, query?: RouteQuery | undefined) => RouteAction
3500
+
3501
+ function defineRouteMethods = (table: Readonly<Record<string, string>>) => void
3502
+
3297
3503
  function defineRoutes = (table: RouteTable) => void
3298
3504
 
3299
3505
  function hasRoute = (name: string) => boolean
3300
3506
 
3301
3507
  function resetRoutes = () => void
3302
3508
 
3509
+ function routeMethod = (name: string) => string | undefined
3510
+
3511
+ interface RouteAction = {
3512
+ method: string
3513
+ url: string
3514
+ }
3515
+
3516
+ interface RouteMethodRegistry = {}
3517
+
3518
+ type MethodedRouteName = never
3519
+
3520
+ type RouteArgs = [params?: RouteParamValues, query?: RouteQuery]
3521
+
3522
+ type RouteParamValues = { [x: string]: RouteParamValue | readonly RouteParamValue[];}
3523
+
3524
+ type RouteQuery = { [x: string]: string | number | boolean | readonly (string | number | boolean)[] | null | undefined;}
3525
+
3303
3526
  type RouteTable = Readonly<Record<string, string>> | ReadonlyMap<string, string>
3304
3527
 
3528
+ type RouteTarget = string
3529
+
3305
3530
  ## ./security `(./src/security/index.ts)`
3306
3531
 
3307
3532
  class CryptKeyMissingError = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/core",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -14,8 +14,8 @@ export function toKebab(name: string): string {
14
14
 
15
15
  /** Source for a new CLI command class extending `Command`. */
16
16
  export function commandStub(name: string): string {
17
- return `import { Command } from '@zerotal/core';
18
- import type { ArgDef, FlagDef } from '@zerotal/core';
17
+ return `import { Command } from 'zerotal';
18
+ import type { ArgDef, FlagDef } from 'zerotal';
19
19
 
20
20
  export class ${name} extends Command {
21
21
  static commandName = '${toKebab(name)}';
@@ -5,7 +5,7 @@ import { Command } from "../Command.ts";
5
5
 
6
6
  /** Source for a minimal controller with a single `index` action. */
7
7
  export function basicStub(name: string): string {
8
- return `import type { HttpContext } from '@zerotal/core';
8
+ return `import type { HttpContext } from 'zerotal';
9
9
 
10
10
  export class ${name} {
11
11
  async index(ctx: HttpContext): Promise<void> {
@@ -17,7 +17,7 @@ export class ${name} {
17
17
 
18
18
  /** Source for a resourceful controller with full CRUD action stubs. */
19
19
  export function resourceStub(name: string): string {
20
- return `import type { HttpContext } from '@zerotal/core';
20
+ return `import type { HttpContext } from 'zerotal';
21
21
 
22
22
  export class ${name} {
23
23
  async index(ctx: HttpContext): Promise<void> {
@@ -27,7 +27,7 @@ export class MakeJobCommand extends Command {
27
27
  }
28
28
  await Bun.write(
29
29
  path,
30
- `import { Job, JobRegistry } from '@zerotal/queue';
30
+ `import { Job, JobRegistry } from 'zerotal/queue';
31
31
 
32
32
  export class ${name} extends Job {
33
33
  readonly queue = 'default';
@@ -5,7 +5,7 @@ import { Command } from "../Command.ts";
5
5
 
6
6
  /** Source for a pass-through middleware class implementing `Pipe<HttpContext>`. */
7
7
  export function middlewareStub(name: string): string {
8
- return `import type { HttpContext, Pipe, NextFn } from '@zerotal/core';
8
+ return `import type { HttpContext, Pipe, NextFn } from 'zerotal';
9
9
 
10
10
  export class ${name} implements Pipe<HttpContext> {
11
11
  async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
@@ -47,7 +47,7 @@ export class MakeObserverCommand extends Command {
47
47
  }
48
48
 
49
49
  function _stub(name: string, model: string): string {
50
- return `import type { ModelObserver } from '@zerotal/orm';
50
+ return `import type { ModelObserver } from 'zerotal/orm';
51
51
 
52
52
  export class ${name} implements ModelObserver {
53
53
  creating(${model.toLowerCase()}: Record<string, unknown>): void {
@@ -41,7 +41,7 @@ export class MakePolicyCommand extends Command {
41
41
 
42
42
  /** Source for a new authorization policy class extending `Policy`. */
43
43
  export function policyStub(name: string, model: string): string {
44
- return `import { Policy } from '@zerotal/auth';
44
+ return `import { Policy } from 'zerotal/auth';
45
45
  // import type { ${model} } from '../models/${model}.ts';
46
46
  // import type { User } from '../models/User.ts';
47
47
 
@@ -6,7 +6,7 @@ import { Command } from "../Command.ts";
6
6
  import { registerProvider } from "../../build/codemod.ts";
7
7
 
8
8
  function providerStub(name: string): string {
9
- return `import { ServiceProvider } from '@zerotal/core';
9
+ return `import { ServiceProvider } from 'zerotal';
10
10
 
11
11
  export class ${name} extends ServiceProvider {
12
12
  override onRegister(): void {
@@ -31,13 +31,23 @@ export class MakeRequestCommand extends Command {
31
31
  }
32
32
  }
33
33
 
34
+ // Two things this stub deliberately does not do.
35
+ //
36
+ // It imports from `zerotal/validator`, not `@zerotal/validator`: a scaffolded app
37
+ // depends on the umbrella, so the scoped name resolves to nothing and the file it
38
+ // just generated does not compile.
39
+ //
40
+ // And it leaves `rules()` unannotated. `validate()` reads the narrow return type
41
+ // through `ReturnType<T['rules']>`, so writing `Record<string, FieldRule>` there
42
+ // widens it back and every validated field arrives as `unknown` — silently, with
43
+ // the first sign a cast somewhere downstream. `FormRequest`'s own docblock says
44
+ // so; the generator used to emit exactly what it warns against.
34
45
  function stub(name: string): string {
35
- return `import { FormRequest } from '@zerotal/validator';
36
- import type { RuleBuilder } from '@zerotal/validator';
37
- import type { FieldRule } from '@zerotal/validator';
46
+ return `import { FormRequest } from 'zerotal/validator';
47
+ import type { RuleBuilder } from 'zerotal/validator';
38
48
 
39
49
  export class ${name} extends FormRequest {
40
- rules(r: RuleBuilder): Record<string, FieldRule> {
50
+ rules(r: RuleBuilder) {
41
51
  return {
42
52
  // example: title: r.string().min(3).max(255),
43
53
  };
@@ -35,8 +35,8 @@ export class MakeResourceCommand extends Command {
35
35
  function _stub(name: string): string {
36
36
  const model = name.replace(/Resource$/, "");
37
37
  const snake = model.replace(/([A-Z])/g, (char, index) => (index ? "-" : "") + char.toLowerCase());
38
- return `import { Resource, ResourceCollection } from '@zerotal/core';
39
- import type { PaginatedData } from '@zerotal/core';
38
+ return `import { Resource, ResourceCollection } from 'zerotal/http';
39
+ import type { PaginatedData } from 'zerotal/http';
40
40
 
41
41
  export class ${name} extends Resource<${model}> {
42
42
  toArray(): Record<string, unknown> {
@@ -11,8 +11,8 @@ export function featureTestStub(name: string): string {
11
11
  // edit rather than one you have to rewrite.
12
12
  const resource = pluralize(subject.toLowerCase());
13
13
  return `import { describe, it, beforeAll, afterAll } from 'bun:test';
14
- import { migrateDatabase, refreshDatabase, assertDatabaseHas } from '@zerotal/testing';
15
- import type { TestApp } from '@zerotal/testing';
14
+ import { migrateDatabase, refreshDatabase, assertDatabaseHas } from 'zerotal/testing';
15
+ import type { TestApp } from 'zerotal/testing';
16
16
  import { createApp } from '../helpers.ts';
17
17
 
18
18
  let app: TestApp;
@@ -197,10 +197,16 @@ export const route: RouteBuilder = Object.assign(
197
197
  /**
198
198
  * Augmented by `types/routes.generated.ts` with the HTTP method of every named
199
199
  * route, exactly as {@link RouteRegistry} is augmented with their patterns.
200
+ *
201
+ * @internal The generator writes the augmentation; an app never names this.
200
202
  */
201
203
  export interface RouteMethodRegistry {}
202
204
 
203
- /** A name the generated table knows a verb for. */
205
+ /**
206
+ * A name the generated table knows a verb for.
207
+ *
208
+ * @internal Derived from {@link RouteMethodRegistry}, which the generator owns.
209
+ */
204
210
  export type MethodedRouteName = Extract<keyof RouteMethodRegistry, string>;
205
211
 
206
212
  const methodTable = new Map<string, string>();
@@ -217,7 +223,11 @@ export function defineRouteMethods(table: Readonly<Record<string, string>>): voi
217
223
  for (const [name, method] of Object.entries(table)) methodTable.set(name, method);
218
224
  }
219
225
 
220
- /** The verb a named route answers on, or undefined when it was never registered. */
226
+ /**
227
+ * The verb a named route answers on, or undefined when it was never registered.
228
+ *
229
+ * @internal The read side of {@link defineRouteMethods}; apps call `action()`.
230
+ */
221
231
  export function routeMethod(name: string): string | undefined {
222
232
  return methodTable.get(name);
223
233
  }