@warlock.js/core 5.0.1 → 5.1.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 +47 -0
- package/esm/cli/cli-commands.manager.mjs +36 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +207 -0
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs +5 -2
- package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/connectors.check.mjs +38 -12
- package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +8 -4
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +77 -11
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/index.mjs +5 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +116 -53
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +27 -9
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/routes.check.mjs +51 -12
- package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/doctor-command.action.mjs +16 -6
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor/run-checks.mjs +15 -6
- package/esm/cli/commands/doctor/run-checks.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +17 -9
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/connectors/describe-server-address.mjs +89 -6
- package/esm/connectors/describe-server-address.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts +18 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +36 -3
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +1 -4
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +3 -7
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +1 -1
- package/esm/dev-server/ready-block.mjs +62 -0
- package/esm/dev-server/ready-block.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +2 -575
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +76 -0
- package/esm/generations/features/access.feature.mjs.map +1 -0
- package/esm/generations/features/ai-anthropic.feature.mjs +10 -0
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -0
- package/esm/generations/features/ai-bedrock.feature.mjs +10 -0
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -0
- package/esm/generations/features/ai-google.feature.mjs +10 -0
- package/esm/generations/features/ai-google.feature.mjs.map +1 -0
- package/esm/generations/features/ai-ollama.feature.mjs +10 -0
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -0
- package/esm/generations/features/ai-openai.feature.mjs +10 -0
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -0
- package/esm/generations/features/ai-panoptic.feature.mjs +13 -0
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -0
- package/esm/generations/features/ai-tools.feature.mjs +13 -0
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -0
- package/esm/generations/features/ai-workspace.feature.mjs +13 -0
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -0
- package/esm/generations/features/ai.feature.mjs +15 -0
- package/esm/generations/features/ai.feature.mjs.map +1 -0
- package/esm/generations/features/herald.feature.mjs +19 -0
- package/esm/generations/features/herald.feature.mjs.map +1 -0
- package/esm/generations/features/image.feature.mjs +9 -0
- package/esm/generations/features/image.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +72 -0
- package/esm/generations/features/index.mjs.map +1 -0
- package/esm/generations/features/mail.feature.mjs +10 -0
- package/esm/generations/features/mail.feature.mjs.map +1 -0
- package/esm/generations/features/mongodb.feature.mjs +9 -0
- package/esm/generations/features/mongodb.feature.mjs.map +1 -0
- package/esm/generations/features/mysql.feature.mjs +9 -0
- package/esm/generations/features/mysql.feature.mjs.map +1 -0
- package/esm/generations/features/notifications.feature.mjs +43 -0
- package/esm/generations/features/notifications.feature.mjs.map +1 -0
- package/esm/generations/features/postgres.feature.mjs +9 -0
- package/esm/generations/features/postgres.feature.mjs.map +1 -0
- package/esm/generations/features/react-email.feature.mjs +70 -0
- package/esm/generations/features/react-email.feature.mjs.map +1 -0
- package/esm/generations/features/react.feature.mjs +16 -0
- package/esm/generations/features/react.feature.mjs.map +1 -0
- package/esm/generations/features/redis.feature.mjs +9 -0
- package/esm/generations/features/redis.feature.mjs.map +1 -0
- package/esm/generations/features/s3.feature.mjs +13 -0
- package/esm/generations/features/s3.feature.mjs.map +1 -0
- package/esm/generations/features/scheduler.feature.mjs +9 -0
- package/esm/generations/features/scheduler.feature.mjs.map +1 -0
- package/esm/generations/features/ses.feature.mjs +9 -0
- package/esm/generations/features/ses.feature.mjs.map +1 -0
- package/esm/generations/features/shadcn.feature.mjs +397 -0
- package/esm/generations/features/shadcn.feature.mjs.map +1 -0
- package/esm/generations/features/shared/link-ai-package-import.mjs +37 -0
- package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -0
- package/esm/generations/features/shared/migration-timestamp.mjs +17 -0
- package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -0
- package/esm/generations/features/socket.feature.mjs +15 -0
- package/esm/generations/features/socket.feature.mjs.map +1 -0
- package/esm/generations/features/tailwind.feature.mjs +174 -0
- package/esm/generations/features/tailwind.feature.mjs.map +1 -0
- package/esm/generations/features/test.feature.mjs +96 -0
- package/esm/generations/features/test.feature.mjs.map +1 -0
- package/esm/generations/features/web.feature.mjs +179 -0
- package/esm/generations/features/web.feature.mjs.map +1 -0
- package/esm/generations/stubs.mjs +95 -8
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -0
- package/esm/http/index.mjs +1 -0
- package/esm/http/ready-report.d.mts +56 -0
- package/esm/http/ready-report.d.mts.map +1 -0
- package/esm/http/ready-report.mjs +24 -0
- package/esm/http/ready-report.mjs.map +1 -0
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +2 -1
- package/llms-full.txt +322 -1
- package/llms.txt +2 -0
- package/package.json +12 -12
- package/skills/use-request-locals/SKILL.md +24 -1
- package/skills/warlock-conventions/SKILL.md +5 -1
package/llms-full.txt
CHANGED
|
@@ -2833,6 +2833,152 @@ router.get(["/health", "/healthz"], healthController);
|
|
|
2833
2833
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — module layout, the `guarded` convention, path aliases.
|
|
2834
2834
|
|
|
2835
2835
|
|
|
2836
|
+
## request-memo `@warlock.js/core/request-memo/SKILL.md`
|
|
2837
|
+
|
|
2838
|
+
---
|
|
2839
|
+
name: request-memo
|
|
2840
|
+
description: 'Memoize async work for one HTTP request with `requestMemo<T>(key, fn)` — the v5 replacement for removed `fromRequest`, with single-flight promise sharing, settled-success reuse, rejection eviction, and no cross-request fallback. Triggers: `requestMemo`, `fromRequest`, `fromRequest removed`, `request-scoped cache`, `single-flight`, `current request memo`; "migrate off fromRequest", "load this once per request", "deduplicate concurrent loaders", "cache a repository lookup during one request"; typical import `import { requestMemo } from "@warlock.js/core"`. Skip: middleware-written request state — `@warlock.js/core/use-request-locals/SKILL.md`; process-wide or cross-request caching — `@warlock.js/core/use-middleware/SKILL.md`; competing patterns: dynamic `request[key]` properties, module-global `Map`, payload `request.get()` / `request.set()`.'
|
|
2841
|
+
---
|
|
2842
|
+
|
|
2843
|
+
# Warlock — memoize work for one request
|
|
2844
|
+
|
|
2845
|
+
`requestMemo()` is the v5 replacement for the removed `fromRequest()`. Use it when several code paths in the same HTTP request may need the same asynchronously computed value and the work should run once.
|
|
2846
|
+
|
|
2847
|
+
## The exact signature
|
|
2848
|
+
|
|
2849
|
+
From `core/src/http/context/request-memo.ts`:
|
|
2850
|
+
|
|
2851
|
+
```ts
|
|
2852
|
+
export function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T>
|
|
2853
|
+
```
|
|
2854
|
+
|
|
2855
|
+
Import it from the package root:
|
|
2856
|
+
|
|
2857
|
+
```ts
|
|
2858
|
+
import { requestMemo } from "@warlock.js/core";
|
|
2859
|
+
```
|
|
2860
|
+
|
|
2861
|
+
`fn` takes no arguments and must return a promise. Close over the current request or the inputs needed by the loader.
|
|
2862
|
+
|
|
2863
|
+
## Migrate from v4 to v5
|
|
2864
|
+
|
|
2865
|
+
### Before — v4 `fromRequest()`
|
|
2866
|
+
|
|
2867
|
+
```ts title="src/app/tenants/utils/current-tenant.ts"
|
|
2868
|
+
import { fromRequest, type Request } from "@warlock.js/core";
|
|
2869
|
+
|
|
2870
|
+
type Tenant = { id: string };
|
|
2871
|
+
|
|
2872
|
+
async function loadTenant(id: string): Promise<Tenant> {
|
|
2873
|
+
return { id };
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
export function currentTenant(request: Request): Promise<Tenant> {
|
|
2877
|
+
const tenantId = String(request.header("x-tenant-id") ?? "");
|
|
2878
|
+
|
|
2879
|
+
return fromRequest("tenants.current", () => loadTenant(tenantId));
|
|
2880
|
+
}
|
|
2881
|
+
```
|
|
2882
|
+
|
|
2883
|
+
### After — v5 `requestMemo()`
|
|
2884
|
+
|
|
2885
|
+
```ts title="src/app/tenants/utils/current-tenant.ts"
|
|
2886
|
+
import { requestMemo, type Request } from "@warlock.js/core";
|
|
2887
|
+
|
|
2888
|
+
type Tenant = { id: string };
|
|
2889
|
+
|
|
2890
|
+
async function loadTenant(id: string): Promise<Tenant> {
|
|
2891
|
+
return { id };
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
export function currentTenant(request: Request): Promise<Tenant> {
|
|
2895
|
+
const tenantId = String(request.header("x-tenant-id") ?? "");
|
|
2896
|
+
|
|
2897
|
+
return requestMemo("tenants.current", () => loadTenant(tenantId));
|
|
2898
|
+
}
|
|
2899
|
+
```
|
|
2900
|
+
|
|
2901
|
+
The call-site migration is usually the import and function name. The complete v5 example above also shows the callback-shape migration: if a v4 callback used the `Request` argument supplied by `fromRequest`, capture the surrounding `request` variable because `requestMemo`'s `fn` receives no arguments.
|
|
2902
|
+
|
|
2903
|
+
## Single-flight behavior
|
|
2904
|
+
|
|
2905
|
+
Within one request, the first call for a key invokes `fn` and stores its promise. Every concurrent caller using that key receives that exact promise, so parallel consumers do not duplicate the work. After the promise resolves, later calls during the same request keep receiving the cached successful promise.
|
|
2906
|
+
|
|
2907
|
+
```ts title="src/app/catalog/controllers/show-product.controller.ts"
|
|
2908
|
+
import { requestMemo, type RequestHandler } from "@warlock.js/core";
|
|
2909
|
+
|
|
2910
|
+
let loads = 0;
|
|
2911
|
+
|
|
2912
|
+
async function loadProduct(): Promise<{ id: string }> {
|
|
2913
|
+
loads += 1;
|
|
2914
|
+
return { id: "product-1" };
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
export const showProductController: RequestHandler = async ({ response }) => {
|
|
2918
|
+
const first = requestMemo("catalog.product", loadProduct);
|
|
2919
|
+
const second = requestMemo("catalog.product", loadProduct);
|
|
2920
|
+
const [product, sameProduct] = await Promise.all([first, second]);
|
|
2921
|
+
|
|
2922
|
+
return response.success({ product, sameProduct, loads });
|
|
2923
|
+
};
|
|
2924
|
+
```
|
|
2925
|
+
|
|
2926
|
+
For that request, `first === second` and `loads` is `1`.
|
|
2927
|
+
|
|
2928
|
+
A rejection is not cached. The rejected entry is deleted immediately, so a later call with the same key retries `fn`. Callers already sharing the rejected promise all observe that rejection.
|
|
2929
|
+
|
|
2930
|
+
## Lifetime and isolation
|
|
2931
|
+
|
|
2932
|
+
The memo table is attached indirectly to the current request-context store through a `WeakMap`. Each incoming request gets a fresh store, so:
|
|
2933
|
+
|
|
2934
|
+
- two concurrent requests never share entries, even when they use the same key;
|
|
2935
|
+
- resolved values live only for the request's context lifetime;
|
|
2936
|
+
- the store and its memo entries are eligible for garbage collection after the request ends;
|
|
2937
|
+
- no value is written onto the `Request` object.
|
|
2938
|
+
|
|
2939
|
+
Calling `requestMemo()` outside the HTTP request pipeline throws synchronously. It deliberately has no process-global fallback. For data meant to survive across requests, use an explicit application cache instead.
|
|
2940
|
+
|
|
2941
|
+
## Why it replaces `fromRequest()`
|
|
2942
|
+
|
|
2943
|
+
`fromRequest()` memoized through dynamic `request[key]` properties. That only type-checked because v4's `Request` had a `[key: string]: any` index signature. v5 removed both the unsafe index signature and `fromRequest()`; `requestMemo()` keeps the one-request lifetime without mutating `Request` or turning values into `any`.
|
|
2944
|
+
|
|
2945
|
+
The new helper also provides real single-flight behavior: it stores the in-flight promise. The old helper awaited its callback before writing the result onto `Request`, so concurrent v4 callers could run the same callback more than once.
|
|
2946
|
+
|
|
2947
|
+
## Key design
|
|
2948
|
+
|
|
2949
|
+
Keys share one string namespace within a request. Use stable, namespaced keys and include every input that changes the result:
|
|
2950
|
+
|
|
2951
|
+
```ts
|
|
2952
|
+
import { requestMemo } from "@warlock.js/core";
|
|
2953
|
+
|
|
2954
|
+
type Product = { id: string };
|
|
2955
|
+
|
|
2956
|
+
async function findProduct(id: string): Promise<Product> {
|
|
2957
|
+
return { id };
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
export function productById(id: string): Promise<Product> {
|
|
2961
|
+
return requestMemo(`catalog.product:${id}`, () => findProduct(id));
|
|
2962
|
+
}
|
|
2963
|
+
```
|
|
2964
|
+
|
|
2965
|
+
Reusing one key for different result types is a caller bug: TypeScript cannot compare the generic type arguments used at separate call sites.
|
|
2966
|
+
|
|
2967
|
+
## Gotchas
|
|
2968
|
+
|
|
2969
|
+
- **Do not call it during bootstrap, in a CLI command, or from a background job.** There is no active HTTP request context there, so it throws.
|
|
2970
|
+
- **Do not omit result-changing inputs from the key.** `catalog.product` is wrong when different IDs may be loaded during one request; use `catalog.product:${id}`.
|
|
2971
|
+
- **Do not use `request.set()` as a replacement.** It writes to the request input payload read by `all()`, `input()`, and `validated()`.
|
|
2972
|
+
- **Failures retry.** If repeated failures must also be cached, this primitive is not that policy; catch and convert the failure into a successful result deliberately, or use another cache.
|
|
2973
|
+
- **Use `request.locals` for state written by middleware and read downstream.** Memoization and request attachment are separate jobs.
|
|
2974
|
+
|
|
2975
|
+
## See also
|
|
2976
|
+
|
|
2977
|
+
- [`use-request-locals/SKILL.md`](../use-request-locals/SKILL.md) — typed per-request data written by middleware.
|
|
2978
|
+
- [`write-middleware/SKILL.md`](../write-middleware/SKILL.md) — authoring and registering middleware.
|
|
2979
|
+
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — consuming request-scoped values from controllers.
|
|
2980
|
+
|
|
2981
|
+
|
|
2836
2982
|
## resolve-path `@warlock.js/core/resolve-path/SKILL.md`
|
|
2837
2983
|
|
|
2838
2984
|
---
|
|
@@ -7232,6 +7378,177 @@ The third arg is a context object — `{ allValues, dateFormat, ... }` — for f
|
|
|
7232
7378
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — controller → service → repository → model layering.
|
|
7233
7379
|
|
|
7234
7380
|
|
|
7381
|
+
## use-request-locals `@warlock.js/core/use-request-locals/SKILL.md`
|
|
7382
|
+
|
|
7383
|
+
---
|
|
7384
|
+
name: use-request-locals
|
|
7385
|
+
description: 'Carry typed, server-only data through one HTTP request with `request.locals`, usually written by middleware and read by downstream middleware or controllers. Augment `RequestLocals` in the module that owns each key; v5 no longer permits arbitrary `request.foo` properties. Triggers: `request.locals`, `RequestLocals`, `request.post`, `request.organization`, `Property does not exist on type Request`, `Request index signature`; "attach data to a request", "share middleware data with a controller", "type request locals", "migrate dynamic request properties"; typical type augmentation `declare module "@warlock.js/core" { interface RequestLocals { ... } }`. Skip: computed-on-demand single-flight values and removed `fromRequest` — `@warlock.js/core/request-memo/SKILL.md`; middleware mechanics — `@warlock.js/core/write-middleware/SKILL.md`; authenticated user typing — augment `RequestUser`, not `RequestLocals`; competing patterns: `(request as any).foo`, `request.set()`, module-global mutable state.'
|
|
7386
|
+
---
|
|
7387
|
+
|
|
7388
|
+
# Warlock — use typed request locals
|
|
7389
|
+
|
|
7390
|
+
`request.locals` is the v5 home for private data that middleware writes and downstream code reads during the same request. It is separate from client input and starts as a fresh object on every `Request` instance.
|
|
7391
|
+
|
|
7392
|
+
## The exact declarations
|
|
7393
|
+
|
|
7394
|
+
From `core/src/http/types.ts`:
|
|
7395
|
+
|
|
7396
|
+
```ts
|
|
7397
|
+
export interface RequestLocals {}
|
|
7398
|
+
```
|
|
7399
|
+
|
|
7400
|
+
From `core/src/http/request.ts`:
|
|
7401
|
+
|
|
7402
|
+
```ts
|
|
7403
|
+
public locals: RequestLocals = {};
|
|
7404
|
+
```
|
|
7405
|
+
|
|
7406
|
+
`RequestLocals` is intentionally empty and augmentable. It has no index signature: declare owned keys explicitly so reads and writes have their real types instead of `any`.
|
|
7407
|
+
|
|
7408
|
+
## Migrate a middleware attachment from v4 to v5
|
|
7409
|
+
|
|
7410
|
+
### Before — v4 dynamic `Request` property
|
|
7411
|
+
|
|
7412
|
+
```ts title="src/app/observability/middleware/request-timing.middleware.ts"
|
|
7413
|
+
import type { Middleware } from "@warlock.js/core";
|
|
7414
|
+
|
|
7415
|
+
export const requestTimingMiddleware: Middleware = request => {
|
|
7416
|
+
request.startedAt = Date.now();
|
|
7417
|
+
};
|
|
7418
|
+
```
|
|
7419
|
+
|
|
7420
|
+
```ts title="src/app/observability/controllers/timing.controller.ts"
|
|
7421
|
+
import type { RequestHandler } from "@warlock.js/core";
|
|
7422
|
+
|
|
7423
|
+
export const timingController: RequestHandler = async (request, response) => {
|
|
7424
|
+
return response.success({ elapsedMs: Date.now() - request.startedAt });
|
|
7425
|
+
};
|
|
7426
|
+
```
|
|
7427
|
+
|
|
7428
|
+
Those arbitrary properties compiled in v4 because `Request` had a `[key: string]: any` index signature. That signature is gone in v5.
|
|
7429
|
+
|
|
7430
|
+
### After — v5 typed `request.locals`
|
|
7431
|
+
|
|
7432
|
+
Declare the key once in the feature that owns and writes it:
|
|
7433
|
+
|
|
7434
|
+
```ts title="src/app/observability/request-locals.d.ts"
|
|
7435
|
+
declare module "@warlock.js/core" {
|
|
7436
|
+
interface RequestLocals {
|
|
7437
|
+
startedAt?: number;
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
|
|
7441
|
+
export {};
|
|
7442
|
+
```
|
|
7443
|
+
|
|
7444
|
+
Write it in middleware:
|
|
7445
|
+
|
|
7446
|
+
```ts title="src/app/observability/middleware/request-timing.middleware.ts"
|
|
7447
|
+
import type { Middleware } from "@warlock.js/core";
|
|
7448
|
+
|
|
7449
|
+
export const requestTimingMiddleware: Middleware = ({ request }) => {
|
|
7450
|
+
request.locals.startedAt = Date.now();
|
|
7451
|
+
};
|
|
7452
|
+
```
|
|
7453
|
+
|
|
7454
|
+
Read it downstream:
|
|
7455
|
+
|
|
7456
|
+
```ts title="src/app/observability/controllers/timing.controller.ts"
|
|
7457
|
+
import type { RequestHandler } from "@warlock.js/core";
|
|
7458
|
+
|
|
7459
|
+
export const timingController: RequestHandler = async ({ request, response }) => {
|
|
7460
|
+
const startedAt = request.locals.startedAt;
|
|
7461
|
+
|
|
7462
|
+
if (startedAt === undefined) {
|
|
7463
|
+
return response.serverError({ error: "request timing middleware did not run" });
|
|
7464
|
+
}
|
|
7465
|
+
|
|
7466
|
+
return response.success({ elapsedMs: Date.now() - startedAt });
|
|
7467
|
+
};
|
|
7468
|
+
```
|
|
7469
|
+
|
|
7470
|
+
The `?` is honest about runtime ordering: the type exists everywhere, but the value exists only after the middleware runs. Narrow it downstream unless every construction path guarantees initialization and your project deliberately declares the key as required.
|
|
7471
|
+
|
|
7472
|
+
## Augment with an application type
|
|
7473
|
+
|
|
7474
|
+
An augmentation file may import the value's type. Keeping the declaration beside the writer makes ownership visible and prevents unrelated packages from claiming the same key.
|
|
7475
|
+
|
|
7476
|
+
```ts title="src/app/organizations/request-locals.d.ts"
|
|
7477
|
+
import type { Organization } from "./models/organization";
|
|
7478
|
+
|
|
7479
|
+
declare module "@warlock.js/core" {
|
|
7480
|
+
interface RequestLocals {
|
|
7481
|
+
organization?: Organization;
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7485
|
+
export {};
|
|
7486
|
+
```
|
|
7487
|
+
|
|
7488
|
+
The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported.
|
|
7489
|
+
|
|
7490
|
+
## Where the augmentation file goes
|
|
7491
|
+
|
|
7492
|
+
A new project scaffolds `src/typings.d.ts` and lists it explicitly in `tsconfig.json`'s `include`. That is the sanctioned home for application-wide augmentations — `RequestLocals` and `RequestUser` both ship there as empty `interface` declarations with the reasoning written above them.
|
|
7493
|
+
|
|
7494
|
+
```ts title="src/typings.d.ts"
|
|
7495
|
+
declare module "@warlock.js/core" {
|
|
7496
|
+
interface RequestLocals {}
|
|
7497
|
+
|
|
7498
|
+
interface RequestUser {}
|
|
7499
|
+
}
|
|
7500
|
+
|
|
7501
|
+
export {};
|
|
7502
|
+
```
|
|
7503
|
+
|
|
7504
|
+
Feature-local files such as `src/app/organizations/request-locals.d.ts` are equally valid and keep ownership next to the middleware that writes the key; the scaffold's `include` covers all of `src`. Use `src/typings.d.ts` for declarations no single feature owns.
|
|
7505
|
+
|
|
7506
|
+
Two rules the scaffold's own comments spell out, and both bite silently:
|
|
7507
|
+
|
|
7508
|
+
- **Keep the trailing `export {}`.** `declare module "x"` inside a file with no top-level import or export declares an *ambient* module, which REPLACES `@warlock.js/core`'s real typings instead of merging into them — every framework export vanishes. The `export {}` is what makes the file a module and the block an augmentation. It is not an unused statement to clean up.
|
|
7509
|
+
- **Keep them `interface`, not `type`.** This project otherwise prefers `type`; these are the named exception, because declaration merging is interface-only. `type RequestUser = { ... }` is a duplicate-identifier error, not an augmentation.
|
|
7510
|
+
|
|
7511
|
+
On a project scaffolded before 5.1 there is no `src/typings.d.ts`, and `tsconfig.json` carries `"typeRoots": ["./src/typings.d.ts"]` — wrong twice, since `typeRoots` takes directories of `@types` packages rather than files, and that file did not exist. Drop the `typeRoots` entry, create the file, and list it under `include`.
|
|
7512
|
+
|
|
7513
|
+
## What belongs in `locals`
|
|
7514
|
+
|
|
7515
|
+
Good fits are values explicitly produced by one stage and consumed by later stages:
|
|
7516
|
+
|
|
7517
|
+
- a resolved organization, session, feature flag, or authorization subject;
|
|
7518
|
+
- request timing or tracing metadata;
|
|
7519
|
+
- a model loaded by validation or routing middleware;
|
|
7520
|
+
- private server state that must not appear in request input.
|
|
7521
|
+
|
|
7522
|
+
`locals` is not a cache API. If consumers should be able to ask for a value independently and concurrent calls must collapse into one loader, use `requestMemo()` instead.
|
|
7523
|
+
|
|
7524
|
+
## Isolation from input and other requests
|
|
7525
|
+
|
|
7526
|
+
`Request` initializes `locals` with `{}` for each new request. Request instances are not pooled, so values cannot carry into the next request.
|
|
7527
|
+
|
|
7528
|
+
Writing `request.locals.organization` does not affect `request.all()`, `request.input()`, or `request.validated()`. By contrast, `request.set()` writes into the input payload's `all` bag, so it is the wrong replacement for a private v4 attachment.
|
|
7529
|
+
|
|
7530
|
+
## Choosing the extension surface
|
|
7531
|
+
|
|
7532
|
+
- Use `request.locals` for per-request data written by middleware and read downstream.
|
|
7533
|
+
- Use `requestMemo(key, fn)` for lazily computed, single-flight work scoped to the active request.
|
|
7534
|
+
- Augment `Request` itself only for a genuine new typed member that also has a runtime implementation.
|
|
7535
|
+
- Augment `RequestUser` for the authenticated `request.user` shape; do not duplicate it under locals merely to work around typing.
|
|
7536
|
+
|
|
7537
|
+
## Gotchas
|
|
7538
|
+
|
|
7539
|
+
- **Do not restore `(request as any).post`.** It recreates the exact unchecked behavior v5 removed.
|
|
7540
|
+
- **Do not add `[key: string]: any` to `RequestLocals`.** Declare each key with its actual type; otherwise every typo becomes valid again.
|
|
7541
|
+
- **Do not centralize keys owned by unrelated features.** The middleware or package that writes a key should own its augmentation.
|
|
7542
|
+
- **Do not assume an optional local was initialized.** Middleware registration and ordering are runtime concerns; narrow the value or fail clearly downstream.
|
|
7543
|
+
- **Avoid key collisions across augmentations.** Interface merging requires repeated property declarations to agree, but stable feature-prefixed names are clearer for generic metadata.
|
|
7544
|
+
|
|
7545
|
+
## See also
|
|
7546
|
+
|
|
7547
|
+
- [`request-memo/SKILL.md`](../request-memo/SKILL.md) — the `fromRequest()` migration and single-flight request memoization.
|
|
7548
|
+
- [`write-middleware/SKILL.md`](../write-middleware/SKILL.md) — middleware execution and registration.
|
|
7549
|
+
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — reading locals in downstream controllers.
|
|
7550
|
+
|
|
7551
|
+
|
|
7235
7552
|
## validate-input `@warlock.js/core/validate-input/SKILL.md`
|
|
7236
7553
|
|
|
7237
7554
|
---
|
|
@@ -7651,9 +7968,13 @@ import { Product } from "../../../products/models/product";
|
|
|
7651
7968
|
|
|
7652
7969
|
Within the same module, use relative paths (`./`, `../`).
|
|
7653
7970
|
|
|
7971
|
+
## Type augmentations
|
|
7972
|
+
|
|
7973
|
+
`src/typings.d.ts` is the project's module-augmentation file. It ships with the scaffold, is listed explicitly in `tsconfig.json`'s `include`, and is where application-wide `declare module "@warlock.js/core"` blocks live — `RequestLocals` and `RequestUser` are already there as empty interfaces. Keep its trailing `export {}` (without it the block declares an ambient module and erases the framework's real typings) and keep the augmented declarations as `interface`, the one sanctioned exception to this project's prefer-`type` rule, since declaration merging is interface-only. See [`use-request-locals`](../use-request-locals/SKILL.md).
|
|
7974
|
+
|
|
7654
7975
|
## Decorators
|
|
7655
7976
|
|
|
7656
|
-
Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators":
|
|
7977
|
+
Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators": false` — these are native Stage-3 decorators, not legacy. Never set that flag to `true`; see [`lower-stage3-decorators`](../lower-stage3-decorators/SKILL.md).
|
|
7657
7978
|
|
|
7658
7979
|
## See also
|
|
7659
7980
|
|
package/llms.txt
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- [lower-stage3-decorators](@warlock.js/core/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.
|
|
21
21
|
- [process-image](@warlock.js/core/process-image/SKILL.md): Transform images with the `Image` class — resize, crop, rotate, format, quality, watermark, blur, etc. — using a deferred pipeline that runs only at `save()` / `toBuffer()` / `toBase64()` / `toDataUrl()` time. Requires sharp via `warlock add image`. Triggers: `Image`, `Image.fromFile`, `Image.fromBuffer`, `Image.fromUrl`, `.resize`, `.crop`, `.watermark`, `.toBuffer`, `.toDataUrl`, `.apply`; "resize an image", "generate a thumbnail", "watermark a product photo", "build an image pipeline"; typical import `import { Image } from "@warlock.js/core"`. Skip: multipart upload entry — `@warlock.js/core/upload-file/SKILL.md`; storage persistence — `@warlock.js/core/store-file/SKILL.md`; competing libs `sharp` direct, `jimp`, `imagemagick`, `gm`.
|
|
22
22
|
- [register-route](@warlock.js/core/register-route/SKILL.md): Register HTTP routes via @warlock.js/core's router — single routes, prefix groups, middleware-guarded blocks, and RESTful resource chains. Routes always live in `src/app/<module>/routes.ts`. Triggers: `router.get`, `router.post`, `router.prefix`, `router.group`, `router.route`, `guarded`; "add a route", "wire a controller to a URL", "group routes by prefix", "register a RESTful resource"; typical import `import { router } from "@warlock.js/core"`. Skip: handler shape — `@warlock.js/core/create-controller/SKILL.md`; CRUD chain details — `@warlock.js/core/build-restful/SKILL.md`; middleware authoring — `@warlock.js/core/write-middleware/SKILL.md`; competing libs `express`, `fastify`, `koa`, `@nestjs/common`.
|
|
23
|
+
- [request-memo](@warlock.js/core/request-memo/SKILL.md): Memoize async work for one HTTP request with `requestMemo<T>(key, fn)` — the v5 replacement for removed `fromRequest`, with single-flight promise sharing, settled-success reuse, rejection eviction, and no cross-request fallback. Triggers: `requestMemo`, `fromRequest`, `fromRequest removed`, `request-scoped cache`, `single-flight`, `current request memo`; "migrate off fromRequest", "load this once per request", "deduplicate concurrent loaders", "cache a repository lookup during one request"; typical import `import { requestMemo } from "@warlock.js/core"`. Skip: middleware-written request state — `@warlock.js/core/use-request-locals/SKILL.md`; process-wide or cross-request caching — `@warlock.js/core/use-middleware/SKILL.md`; competing patterns: dynamic `request[key]` properties, module-global `Map`, payload `request.get()` / `request.set()`.
|
|
23
24
|
- [resolve-path](@warlock.js/core/resolve-path/SKILL.md): Path helpers anchored at `process.cwd()` — `rootPath`, `srcPath`, `appPath`, `configPath`, `publicPath`, `storagePath`, `uploadsPath`, `cachePath`, `logsPath`, `tempPath`, `warlockPath`, `sanitizePath`. Optional `uploads.root` config overrides the uploads anchor. Triggers: `appPath`, `configPath`, `uploadsPath`, `storagePath`, `publicPath`, `cachePath`, `logsPath`, `tempPath`, `sanitizePath`, `paths`; "resolve a path inside src/app", "absolute upload destination", "sanitize a user filename", "ship uploads to a mounted volume"; typical import `import { appPath, uploadsPath } from "@warlock.js/core"`. Skip: HTTP URL helpers — `@warlock.js/core/build-url/SKILL.md`; app metadata — `@warlock.js/core/use-app-context/SKILL.md`; storage abstraction — `@warlock.js/core/store-file/SKILL.md`; competing patterns: `path.join(process.cwd(), ...)`, hand-rolled directory constants.
|
|
24
25
|
- [retry-operation](@warlock.js/core/retry-operation/SKILL.md): Wrap a flaky operation with `retry(fn, options)` — now provided by `@mongez/reinforcements` (not `@warlock.js/core`). `attempts` total tries, `delay` + `backoff` (linear/exponential/fn), `maxDelay`, `jitter`, `shouldRetry` to bail on permanent errors, `signal` to cancel, plus `retryable()` to pre-bind options. Triggers: `retry`, `retryable`, `RetryOptions`, `attempts`, `backoff`, `jitter`, `maxDelay`, `shouldRetry`, `signal`; "retry a flaky API call", "handle transient errors", "exponential backoff with jitter", "wrap an external request"; typical import `import { retry } from "@mongez/reinforcements"`. Skip: timing the retried op — `@warlock.js/core/benchmark-code/SKILL.md`; use-case-level `retry` option — `@warlock.js/core/write-use-case/SKILL.md`; competing libs `p-retry`, `async-retry`, `cockatiel`.
|
|
25
26
|
- [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
- [use-middleware](@warlock.js/core/use-middleware/SKILL.md): Attach built-in HTTP middleware to routes via the `middleware` namespace from `@warlock.js/core` — rateLimit, concurrencyLimit, maxBodySize, idempotency, maintenance, ipFilter, cache. Plus `X-Request-Id` correlation, wired automatically. Triggers: `middleware.rateLimit`, `middleware.concurrencyLimit`, `middleware.maxBodySize`, `middleware.idempotency`, `middleware.maintenance`, `middleware.ipFilter`, `middleware.cache`, `X-Request-Id`, `Idempotency-Key`; "add rate limiting", "dedupe writes by idempotency key", "cap concurrent requests", "block IPs", "cache a GET response"; typical import `import { middleware } from "@warlock.js/core"`. Skip: author custom middleware — `@warlock.js/core/write-middleware/SKILL.md`; cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `@fastify/rate-limit` direct, `express-rate-limit`, `helmet`.
|
|
36
37
|
- [use-model-transformers](@warlock.js/core/use-model-transformers/SKILL.md): Three schema-side helpers — `useHashedPassword()` (bcrypt on save) attaches via `.addTransformer(...)`; `useComputedSlug(field?, scope?)` (auto-slug from another field) and `useComputedModel(callback)` (arbitrary computed-on-save value) attach via `v.computed(...)`. Triggers: `useHashedPassword`, `useComputedSlug`, `useComputedModel`, `.addTransformer`, `v.computed`, `ComputedCallback`; "auto-hash a password field", "auto-slug from title on save", "derive a value at write time", "declarative model transformers"; typical import `import { useHashedPassword, useComputedSlug } from "@warlock.js/core"`. Skip: bcrypt setup details — `@warlock.js/core/hash-password/SKILL.md`; repository writes — `@warlock.js/core/use-repository/SKILL.md`; output filtering — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: manual `await hashPassword(input)` in services, ORM lifecycle hooks.
|
|
37
38
|
- [use-repository](@warlock.js/core/use-repository/SKILL.md): Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()`, the active/cached/cursor variants, and the `filterBy`-aware aggregates `sum()`/`avg()`/`min()`/`max()`/`groupBy()`/`aggregate()`. Triggers: `RepositoryManager`, `FilterRules`, `RepositoryOptions`, `.list`, `.listCached`, `.find`, `.findCached`, `.create`, `.update`, `.delete`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.aggregate`, `simpleSelectColumns`; "create a repository", "filter rules for a list endpoint", "cursor vs page pagination", "cached vs uncached read", "sum/avg/group-by with filters"; typical import `import { RepositoryManager } from "@warlock.js/core"`. Skip: cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; use-case pipelines — `@warlock.js/core/write-use-case/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing libs `typeorm` Repository, `prisma.client.<model>`, `@nestjs/typeorm`.
|
|
39
|
+
- [use-request-locals](@warlock.js/core/use-request-locals/SKILL.md): Carry typed, server-only data through one HTTP request with `request.locals`, usually written by middleware and read by downstream middleware or controllers. Augment `RequestLocals` in the module that owns each key; v5 no longer permits arbitrary `request.foo` properties. Triggers: `request.locals`, `RequestLocals`, `request.post`, `request.organization`, `Property does not exist on type Request`, `Request index signature`; "attach data to a request", "share middleware data with a controller", "type request locals", "migrate dynamic request properties"; typical type augmentation `declare module "@warlock.js/core" { interface RequestLocals { ... } }`. Skip: computed-on-demand single-flight values and removed `fromRequest` — `@warlock.js/core/request-memo/SKILL.md`; middleware mechanics — `@warlock.js/core/write-middleware/SKILL.md`; authenticated user typing — augment `RequestUser`, not `RequestLocals`; competing patterns: `(request as any).foo`, `request.set()`, module-global mutable state.
|
|
38
40
|
- [validate-input](@warlock.js/core/validate-input/SKILL.md): Author seal schemas, attach them to controllers via `controller.validation = { schema }`, infer types via `Infer<typeof schema>`, and layer DB-aware (`unique`/`exists`) and file validators on top. Triggers: `v.object`, `v.string`, `v.email`, `Infer`, `controller.validation`, `.unique`, `.exists`, `uniqueExceptCurrentId`, `request.validated`; "validate a request body", "attach a schema to a controller", "DB-aware unique rule", "infer schema types"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: schema authoring foundations — `@warlock.js/seal/seal-basics/SKILL.md`; controller wiring — `@warlock.js/core/create-controller/SKILL.md`; file rules deep-dive — `@warlock.js/core/upload-file/SKILL.md`; competing libs `zod`, `joi`, `yup`, `class-validator`.
|
|
39
41
|
- [warlock-conventions](@warlock.js/core/warlock-conventions/SKILL.md): Framework-wide invariants for projects built on @warlock.js/core — module layout, canonical imports, layered flow, file naming, and the non-negotiable rules every other warlock skill assumes. Triggers: `src/app/<module>`, `routes.ts`, `main.ts`, `Request<TSchema>`, `RequestHandler`, `GuardedRequestHandler`, `app/<module>/...`; "where do files go in this project", "canonical Warlock imports", "module layout rules", "controller-service-repository layering"; typical import `import { router, type RequestHandler } from "@warlock.js/core"`. Skip: scaffold a new module — `@warlock.js/core/create-module/SKILL.md`; route shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing patterns: `express` ad-hoc layouts, `@nestjs/common` decorator-driven structure.
|
|
40
42
|
- [warlock-doctor](@warlock.js/core/warlock-doctor/SKILL.md): Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `yarn warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.
|
package/package.json
CHANGED
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.4",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/auth": "5.0
|
|
29
|
-
"@warlock.js/cache": "5.0
|
|
30
|
-
"@warlock.js/cascade": "5.0
|
|
31
|
-
"@warlock.js/context": "5.0
|
|
32
|
-
"@warlock.js/logger": "5.0
|
|
33
|
-
"@warlock.js/seal": "5.0
|
|
34
|
-
"@warlock.js/fs": "5.0
|
|
28
|
+
"@warlock.js/auth": "5.1.0",
|
|
29
|
+
"@warlock.js/cache": "5.1.0",
|
|
30
|
+
"@warlock.js/cascade": "5.1.0",
|
|
31
|
+
"@warlock.js/context": "5.1.0",
|
|
32
|
+
"@warlock.js/logger": "5.1.0",
|
|
33
|
+
"@warlock.js/seal": "5.1.0",
|
|
34
|
+
"@warlock.js/fs": "5.1.0",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"dayjs": "^1.11.19",
|
|
37
37
|
"es-module-lexer": "^2.0.0",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"react": "^19.2.3",
|
|
58
58
|
"react-dom": "^19.2.3",
|
|
59
59
|
"@react-email/render": "^2.0.5",
|
|
60
|
-
"@warlock.js/herald": "5.0
|
|
61
|
-
"@warlock.js/ai": "5.0
|
|
62
|
-
"@warlock.js/access": "5.0
|
|
63
|
-
"@warlock.js/notifications": "5.0
|
|
60
|
+
"@warlock.js/herald": "5.1.0",
|
|
61
|
+
"@warlock.js/ai": "5.1.0",
|
|
62
|
+
"@warlock.js/access": "5.1.0",
|
|
63
|
+
"@warlock.js/notifications": "5.1.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
66
66
|
"sharp": {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
],
|
|
124
124
|
"author": "hassanzohdy",
|
|
125
125
|
"license": "MIT",
|
|
126
|
-
"version": "5.0
|
|
126
|
+
"version": "5.1.0",
|
|
127
127
|
"type": "module",
|
|
128
128
|
"main": "./esm/index.mjs",
|
|
129
129
|
"module": "./esm/index.mjs",
|
|
@@ -103,7 +103,30 @@ declare module "@warlock.js/core" {
|
|
|
103
103
|
export {};
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported.
|
|
106
|
+
The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported.
|
|
107
|
+
|
|
108
|
+
## Where the augmentation file goes
|
|
109
|
+
|
|
110
|
+
A new project scaffolds `src/typings.d.ts` and lists it explicitly in `tsconfig.json`'s `include`. That is the sanctioned home for application-wide augmentations — `RequestLocals` and `RequestUser` both ship there as empty `interface` declarations with the reasoning written above them.
|
|
111
|
+
|
|
112
|
+
```ts title="src/typings.d.ts"
|
|
113
|
+
declare module "@warlock.js/core" {
|
|
114
|
+
interface RequestLocals {}
|
|
115
|
+
|
|
116
|
+
interface RequestUser {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Feature-local files such as `src/app/organizations/request-locals.d.ts` are equally valid and keep ownership next to the middleware that writes the key; the scaffold's `include` covers all of `src`. Use `src/typings.d.ts` for declarations no single feature owns.
|
|
123
|
+
|
|
124
|
+
Two rules the scaffold's own comments spell out, and both bite silently:
|
|
125
|
+
|
|
126
|
+
- **Keep the trailing `export {}`.** `declare module "x"` inside a file with no top-level import or export declares an *ambient* module, which REPLACES `@warlock.js/core`'s real typings instead of merging into them — every framework export vanishes. The `export {}` is what makes the file a module and the block an augmentation. It is not an unused statement to clean up.
|
|
127
|
+
- **Keep them `interface`, not `type`.** This project otherwise prefers `type`; these are the named exception, because declaration merging is interface-only. `type RequestUser = { ... }` is a duplicate-identifier error, not an augmentation.
|
|
128
|
+
|
|
129
|
+
On a project scaffolded before 5.1 there is no `src/typings.d.ts`, and `tsconfig.json` carries `"typeRoots": ["./src/typings.d.ts"]` — wrong twice, since `typeRoots` takes directories of `@types` packages rather than files, and that file did not exist. Drop the `typeRoots` entry, create the file, and list it under `include`.
|
|
107
130
|
|
|
108
131
|
## What belongs in `locals`
|
|
109
132
|
|
|
@@ -114,9 +114,13 @@ import { Product } from "../../../products/models/product";
|
|
|
114
114
|
|
|
115
115
|
Within the same module, use relative paths (`./`, `../`).
|
|
116
116
|
|
|
117
|
+
## Type augmentations
|
|
118
|
+
|
|
119
|
+
`src/typings.d.ts` is the project's module-augmentation file. It ships with the scaffold, is listed explicitly in `tsconfig.json`'s `include`, and is where application-wide `declare module "@warlock.js/core"` blocks live — `RequestLocals` and `RequestUser` are already there as empty interfaces. Keep its trailing `export {}` (without it the block declares an ambient module and erases the framework's real typings) and keep the augmented declarations as `interface`, the one sanctioned exception to this project's prefer-`type` rule, since declaration merging is interface-only. See [`use-request-locals`](../use-request-locals/SKILL.md).
|
|
120
|
+
|
|
117
121
|
## Decorators
|
|
118
122
|
|
|
119
|
-
Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators":
|
|
123
|
+
Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasMany` / `@MorphTo` for relations. The scaffolded `tsconfig.json` has `"experimentalDecorators": false` — these are native Stage-3 decorators, not legacy. Never set that flag to `true`; see [`lower-stage3-decorators`](../lower-stage3-decorators/SKILL.md).
|
|
120
124
|
|
|
121
125
|
## See also
|
|
122
126
|
|