create-warlock 5.37.1 → 5.39.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 CHANGED
@@ -4,6 +4,27 @@ All notable changes to `create-warlock` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.39.0 - 2026-10-26
8
+
9
+ ### Fixed
10
+
11
+ - `npm install` in a fresh scaffold no longer crashes on npm 10.9 (the npm bundled with Node 22) with `Cannot read properties of null (reading 'edgesOut')`: the template pins Vitest 5.0.5, because the Vitest releases before it trip that npm bug on their own. The `edgesOut` hint stays as a last resort.
12
+
13
+ ## 5.38.0 - 2026-10-25
14
+
15
+ ### Added
16
+
17
+ - The web template ships `src/web/forbidden.page.tsx` and `forbidden.setup.ts` (styled by `forbidden.css`, imported from `app.css`): the page a 401/403 on a page route renders instead of raw JSON. The template README and the `web-code-standards` skill describe it.
18
+ - The starter ships `src/config/web.ts` with `moduleBoundary: "error"`, so browser code importing a server-only module file fails the client build in new apps (the framework default for existing apps is `"warn"`).
19
+
20
+ ### Changed
21
+
22
+ - The generated testing guidance and code standards use the `*.unit.spec.ts` / `*.unit.test.ts` convention for unit specs (the `test` feature's `unit` and `integration` Vitest projects pick files by that infix).
23
+ - The starter boots the test framework once per Vitest worker: `src/config/tests.ts` sets `lifetime: "worker"` and `resetDatabase: true` (`WARLOCK_TEST_LIFETIME=file` switches back), and `src/config/database.ts` (and the PostgreSQL variant) uses one database per Vitest worker, `<DB_NAME>_test_<worker id>`. With `--features=test` the generated unit project fails a spec that imports a connector or boots the app, and the integration project runs with `isolate: false`. The PostgreSQL variant needs those databases created and migrated first.
24
+ - The template's `testing-strategy` skill describes the per-file reset, the `setupTest({ lifetime: "file" })` opt-out, the unit-project guard, `WL9024`, the boot profile and Vitest 5's `clearMocks` default.
25
+ - The starter app keeps translations in JSON: `app/auth/utils/locales.json` and `app/shared/utils/locales/{validation,http,attributes}.json` replace the two `utils/locales.ts` files, and `docs/new-module.md` describes `locales.json` and `locales/<feature>.json`.
26
+ - The template's `module-boundaries`, `code-standards` and `web-code-standards` skills describe small exported types co-located in `*.service.ts`, optional barrels (and the opt-in `WL9023` lint), the browser-safe surface of an app module and the `*.client.ts(x)` boundary.
27
+
7
28
  ## 5.37.1 - 2026-10-24
8
29
 
9
30
  ### Changed
package/llms-full.txt CHANGED
@@ -285,7 +285,7 @@ features. A database-free scaffold needs no external database.
285
285
  `npx create-warlock my-project --yes --features=test` delegates to `warlock add test`:
286
286
  unit `*.spec.ts` outside `tests/` runs without Warlock setup, integration `*.test.ts`
287
287
  and specs under `tests/` use framework setup and the generated cleanup runner.
288
- The template pins Vitest 5.0.3. See the generated `testing-strategy` skill for
288
+ The template pins Vitest 5.0.5. See the generated `testing-strategy` skill for
289
289
  `setupTest({ mode: "unit", importModule })`, opt-in `tests.lifetime: "worker"`,
290
290
  `tests.resetBetweenFiles`, profiling and the changed-tests release gate.
291
291
 
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@clack/prompts": "^0.8.2",
15
15
  "@mongez/copper": "^2.1.3",
16
- "@warlock.js/fs": "5.37.1",
16
+ "@warlock.js/fs": "5.39.0",
17
17
  "@mongez/reinforcements": "^4.0.2",
18
18
  "cross-spawn": "^7.0.3",
19
19
  "rimraf": "^6.0.1",
@@ -22,7 +22,7 @@
22
22
  "engines": {
23
23
  "node": ">=22.15"
24
24
  },
25
- "version": "5.37.1",
25
+ "version": "5.39.0",
26
26
  "type": "module",
27
27
  "main": "./esm/index.mjs",
28
28
  "module": "./esm/index.mjs",
package/skills/SKILL.md CHANGED
@@ -277,7 +277,7 @@ features. A database-free scaffold needs no external database.
277
277
  `npx create-warlock my-project --yes --features=test` delegates to `warlock add test`:
278
278
  unit `*.spec.ts` outside `tests/` runs without Warlock setup, integration `*.test.ts`
279
279
  and specs under `tests/` use framework setup and the generated cleanup runner.
280
- The template pins Vitest 5.0.3. See the generated `testing-strategy` skill for
280
+ The template pins Vitest 5.0.5. See the generated `testing-strategy` skill for
281
281
  `setupTest({ mode: "unit", importModule })`, opt-in `tests.lifetime: "worker"`,
282
282
  `tests.resetBetweenFiles`, profiling and the changed-tests release gate.
283
283
 
@@ -70,7 +70,7 @@ automatically on `postinstall`).
70
70
  schemas, authentication, and secrets handling.
71
71
  - **observability-and-resilience** — structured logging, retries, timeouts, and
72
72
  avoiding N+1 work.
73
- - **testing-strategy** — Vitest test pyramid with co-located `*.spec.ts` files.
73
+ - **testing-strategy** — Vitest test pyramid with co-located `*.unit.spec.ts` unit files and `*.test.ts` integration files.
74
74
  - **git-workflow** — conventional commits, branching, PRs, and CI gates.
75
75
 
76
76
  ## Commands
@@ -108,6 +108,7 @@ pages served by the same process as the API:
108
108
  - `src/web/root.tsx` — the HTML document every page renders inside
109
109
  - `src/web/home/index.page.tsx` — the UI at `/`; `index.setup.ts` owns its config (including `config.metadata`), loader, and registration
110
110
  - `src/web/404.page.tsx` — the not-found UI; `404.setup.ts` owns its `config.metadata`
111
+ - `src/web/forbidden.page.tsx` — the UI for a 401/403 on a page route (`requireUser()`), never raw JSON; `forbidden.setup.ts` owns its `config.metadata`
111
112
  - `src/web/root.setup.ts` — document policy, paired with the `root.tsx` component
112
113
  - `*.page.tsx` files under `src/web` are pages; the folder path is the URL
113
114
 
@@ -42,7 +42,8 @@ src/app/[module-name]/
42
42
  ├── types/ # TypeScript type definitions
43
43
  │ └── *.ts
44
44
  └── utils/ # Module-specific utilities
45
- ├── locales.ts # Translations (auto-imported by Warlock.js)
45
+ ├── locales.json # Translations (auto-loaded by Warlock.js)
46
+ ├── locales/ # Optional per-feature translation files (<feature>.json)
46
47
  └── flags.ts # Constants/flags
47
48
  ```
48
49
 
@@ -352,7 +353,7 @@ export const cleanup = [event];
352
353
  export const cleanup = () => event.unsbcribe();
353
354
  ```
354
355
 
355
- **Note:** Files in the `events/` folder are auto-imported, similar to `routes.ts`, `main.ts`, and `utils/locales.ts`. Just create the file and it will be loaded automatically.
356
+ **Note:** Files in the `events/` folder are auto-imported, similar to `routes.ts`, `main.ts`, and `utils/locales.json`. Just create the file and it will be loaded automatically.
356
357
 
357
358
  ### 11. services/mail/ (or separate mail/ folder)
358
359
 
@@ -443,25 +444,27 @@ export type UserRole = "admin" | "user" | "guest";
443
444
 
444
445
  ### 14. utils/
445
446
 
446
- Module-specific utility functions. **Must include `locales.ts`** which is automatically imported by Warlock.js.
447
+ Module-specific utility functions. Translations live in `locales.json`, which Warlock.js loads automatically.
447
448
 
448
- **utils/locales.ts (Auto-imported):**
449
+ **utils/locales.json (Auto-loaded):**
449
450
 
450
- ```typescript
451
- import { groupedTranslations } from "@mongez/localization";
452
-
453
- groupedTranslations("moduleName", {
454
- key1: {
455
- en: "English translation",
456
- ar: "Arabic translation",
451
+ ```json
452
+ {
453
+ "key1": {
454
+ "en": "English translation",
455
+ "ar": "Arabic translation"
457
456
  },
458
- invalidCredentials: {
459
- en: "Invalid credentials",
460
- ar: "بيانات الدخول غير صحيحة",
461
- },
462
- });
457
+ "invalidCredentials": {
458
+ "en": "Invalid credentials",
459
+ "ar": "بيانات الدخول غير صحيحة"
460
+ }
461
+ }
463
462
  ```
464
463
 
464
+ Keys are registered under the module name (`moduleName.key1`); add `"$group": "name"` at the top to pick another group. Every key needs every locale in `app.localeCodes`; `npx warlock check` reports a missing one.
465
+
466
+ When several people work in one module, put each feature's translations in `utils/locales/<feature>.json` instead of growing one file. All of the module's files are merged at load time, and a key may live in only one of them.
467
+
465
468
  **utils/flags.ts (Constants):**
466
469
 
467
470
  ```typescript
@@ -488,7 +491,7 @@ export function formatUserName(user: User): string {
488
491
  }
489
492
  ```
490
493
 
491
- **Note:** The `utils/locales.ts` file is automatically imported by Warlock.js, similar to `routes.ts`, `main.ts`, and files in the `events/` folder.
494
+ **Note:** `utils/locales.json` and `utils/locales/*.json` are loaded automatically by Warlock.js, similar to `routes.ts`, `main.ts`, and files in the `events/` folder. The older `utils/locales.ts` is deprecated; `npx warlock doctor --fix` converts it.
492
495
 
493
496
  ## Best Practices
494
497
 
@@ -540,7 +543,7 @@ export function formatUserName(user: User): string {
540
543
 
541
544
  - `main.ts` - Module entry point
542
545
  - `routes.ts` - Route definitions
543
- - `utils/locales.ts` - Translations
546
+ - `utils/locales.json` and `utils/locales/*.json` - Translations
544
547
  - All files in `events/` folder - Event handlers
545
548
  - No need to manually import these files elsewhere
546
549
 
@@ -46,7 +46,7 @@
46
46
  "@mongez/agent-kit": "^1.3.1",
47
47
  "@types/node": "^25.0.3",
48
48
  "typescript": "7.0.2",
49
- "vitest": "5.0.3",
49
+ "vitest": "5.0.5",
50
50
  "oxlint": "1.88.0",
51
51
  "oxfmt": "0.73.0"
52
52
  }
@@ -134,9 +134,11 @@ import { OrderStatus } from "app/orders/types/order.type";
134
134
  import { listUsersService } from "../../users/services";
135
135
  ```
136
136
 
137
- ### 2.2 Barrel files — sub-folder level, not module level
137
+ ### 2.2 Barrel files — optional, sub-folder level, never module level
138
138
 
139
- Barrels (`index.ts`) make a module easier to consume **at the sub-folder level** — `services/`, `utils/`, `types/`. They become a problem when you put one at the **module root** and turn it into a giant re-export of everything inside.
139
+ Barrels are optional. A module works with none: import from the file that defines the export. A hand-maintained barrel is a file every feature has to edit, which is where merge conflicts and circular imports come from, so add one only where a folder has several files that other modules consume together. Turn on the opt-in `WL9023` rule (`"rules": { "WL9023": "warning" }` in `.warlock-check.json`) to flag barrels, and set `generate: { barrels: false }` in `warlock.config.ts` so generators write none.
140
+
141
+ Where you do use them, barrels (`index.ts`) make a module easier to consume **at the sub-folder level** — `services/`, `utils/`, `types/`. They become a problem when you put one at the **module root** and turn it into a giant re-export of everything inside.
140
142
 
141
143
  The right shape:
142
144
 
@@ -163,6 +165,22 @@ import { listUsersService, formatUserName, User } from "app/users";
163
165
 
164
166
  The module-root barrel is the one that causes circular imports and tree-shaking misses. This recommendation applies to server app modules under `src/app/**`. Web feature folders under `src/web/**` have no barrels: imports point to the exact component or hook file so client bundles stay lean and server-only code is never pulled through a barrel. See the `web-code-standards` skill.
165
167
 
168
+ ### 2.2.1 Small types beside the code that uses them
169
+
170
+ A `*.service.ts`, `*.repository.ts` or `*.controller.ts` file may export the small input and output types it is written around, next to the function:
171
+
172
+ ```typescript
173
+ // app/orders/services/create-order.service.ts
174
+ export type CreateOrderInput = { customerId: string; lines: OrderLine[] };
175
+ export type CreateOrderResult = { id: string; total: number };
176
+
177
+ export async function createOrderService(input: CreateOrderInput): Promise<CreateOrderResult> {
178
+ // ...
179
+ }
180
+ ```
181
+
182
+ `warlock check` raises nothing for this, and another module or a page can share the type with `import type { CreateOrderInput } from "app/orders/services/create-order.service"` (a type-only import is erased, so it never pulls the service into the browser). Move a type to `types/` or `schema/` when several files need it, or when the browser needs the value and not only the type. Do not make a file per type. Web UI is unchanged: one component per file, logic in `*.setup.ts`.
183
+
166
184
  ### 2.3 No file extension in import paths
167
185
 
168
186
  Always bare: `from "./order.service"`, never `from "./order.service.ts"` or `.js`.
@@ -188,7 +206,8 @@ Always bare: `from "./order.service"`, never `from "./order.service.ts"` or `.js
188
206
  | `.setup.ts` | Page, layout or root policy and server exports | `index.setup.ts` |
189
207
  | `use-*.ts` | Named feature hook | `use-contact-form.ts` |
190
208
  | `.resource.ts` | Output mapper | `user.resource.ts` |
191
- | `.spec.ts` / `.test.ts` | Co-located test | `cart.service.spec.ts` |
209
+ | `.unit.spec.ts` | Co-located unit test (no app boot) | `cart.service.unit.spec.ts` |
210
+ | `.spec.ts` / `.test.ts` | Co-located integration test (booted app) | `cart.service.test.ts` |
192
211
 
193
212
  **One primary export per file.** If a `.contract.ts` also defines closely-related data types used only by that contract, that's fine — but the primary export is the interface.
194
213
 
@@ -567,7 +586,7 @@ A secret with no safe default (a payment key, a signing secret) should fail loud
567
586
 
568
587
  ## 10. Testing
569
588
 
570
- - **Vitest**, co-located: `cart.service.ts` → `cart.service.spec.ts` in the same folder.
589
+ - **Vitest**, co-located: `cart.service.ts` → `cart.service.unit.spec.ts` in the same folder (the `.unit` infix selects the unit project; a plain `.spec.ts` or `.test.ts` runs in integration).
571
590
  - **Test the public surface.** If a private method needs testing, the public method that calls it does — test through it. If you can't reach the behaviour from outside, it shouldn't exist.
572
591
  - **Arrange / Act / Assert** structure per test. Blank lines between phases. One assertion target per test where practical.
573
592
  - **Mock at the boundary.** Use `MockSDK` from `@warlock.js/ai` for AI calls. Mock HTTP at the HTTP client level, not deep inside services.
@@ -51,8 +51,8 @@ src/app/<module>/
51
51
  controllers/ ← HTTP handlers (one per endpoint)
52
52
  <verb>-<noun>.controller.ts
53
53
  services/ ← business logic (one per use case)
54
- index.ts ← sub-folder barrel ✅
55
- <verb>-<noun>.service.ts
54
+ index.ts ← optional sub-folder barrel
55
+ <verb>-<noun>.service.ts ← may export its small input/output types
56
56
  models/
57
57
  <noun>/ ← model folder (Cascade convention)
58
58
  <noun>.model.ts
@@ -66,10 +66,10 @@ src/app/<module>/
66
66
  resources/ ← output mappers (defineResource)
67
67
  <noun>.resource.ts
68
68
  types/ ← TypeScript types and enums
69
- index.ts ← sub-folder barrel ✅
69
+ index.ts ← optional sub-folder barrel
70
70
  <noun>-status.type.ts
71
71
  utils/ ← pure helpers used inside the module
72
- index.ts ← sub-folder barrel ✅
72
+ index.ts ← optional sub-folder barrel
73
73
  <verb>-<noun>.ts
74
74
  events/ ← if the module emits events
75
75
  <noun>-<verb>.event.ts
@@ -83,17 +83,19 @@ src/app/<module>/
83
83
 
84
84
  Don't create `events/` if the module emits nothing. Don't create `errors/` until you have an error to put there. The layout is a vocabulary, not a checklist.
85
85
 
86
- ### 2.2 No module-root `index.ts`
86
+ ### 2.2 No module-root `index.ts`; sub-folder barrels are optional
87
87
 
88
88
  This is the one absolute rule:
89
89
 
90
90
  ```
91
91
  ❌ src/app/orders/index.ts ← banned
92
- ✅ src/app/orders/services/index.ts ← good (sub-folder barrel)
93
- ✅ src/app/orders/utils/index.ts ← good
94
- ✅ src/app/orders/types/index.ts ← good
92
+ ✅ src/app/orders/services/index.ts ← allowed (optional sub-folder barrel)
93
+ ✅ src/app/orders/utils/index.ts ← allowed
94
+ ✅ src/app/orders/types/index.ts ← allowed
95
95
  ```
96
96
 
97
+ A module needs no barrel at all; importing from the file that defines the export is fine. `rules: { "WL9023": "warning" }` in `.warlock-check.json` flags hand-maintained barrels, and `generate: { barrels: false }` in `warlock.config.ts` stops generators from writing them.
98
+
97
99
  See the `code-standards` skill § 2.2 for the reasoning.
98
100
 
99
101
  ---
@@ -102,13 +104,29 @@ See the `code-standards` skill § 2.2 for the reasoning.
102
104
 
103
105
  ### 3.1 Public (importable from other modules)
104
106
 
105
- - `src/app/<module>/services` (the sub-folder barrel)
106
- - `src/app/<module>/utils` (the sub-folder barrel)
107
- - `src/app/<module>/types` (the sub-folder barrel)
107
+ - `src/app/<module>/services` (the sub-folder barrel, or the service file when there is none)
108
+ - `src/app/<module>/utils` (the sub-folder barrel, or the file)
109
+ - `src/app/<module>/types` (the sub-folder barrel, or the file)
108
110
  - `src/app/<module>/resources/<noun>.resource.ts`
109
111
  - `src/app/<module>/models/<noun>` (the model folder — for `BelongsTo("Name")` registrations and typed references)
110
112
  - `src/app/<module>/errors/<noun>-<reason>.error.ts`
111
113
 
114
+ ### 3.1.1 Browser-safe (importable from browser code)
115
+
116
+ `src/app/<module>/` is server code. Code the client build reaches (pages, layouts, components) may import from a module **only**:
117
+
118
+ - `*.client.ts` / `*.client.tsx` files
119
+ - schema files: `*.schema.ts`, or anything under `schema/`
120
+ - type files: `*.types.ts`, `*.d.ts`, or anything under `types/`
121
+ - the module's `web/` folder
122
+ - non-code files (JSON, CSS)
123
+
124
+ Anything else (a service, repository, controller, model, route file, a plain helper) fails the client build with the import chain (this app sets `web.moduleBoundary: "error"` in `src/config/web.ts`; the framework default is `"warn"`, which only reports). `import type { CreateOrderInput } from "app/orders/services/create-order.service"` is always fine because a type-only import is erased. `web.moduleBoundary` in `src/config/web.ts` is `"error"` by default, `"warn"` while migrating, `"off"` to disable.
125
+
126
+ ### 3.1.2 `*.client.*` is browser code
127
+
128
+ A `*.client.ts(x)` file may be imported by pages, layouts and components; it still renders on the server unless the component is wrapped in `<ClientOnly>`. Controllers, services, repositories, models, routes, connectors and `*.server.ts` files must never import it (`npx warlock check` reports `WL9022` with the import chain).
129
+
112
130
  ### 3.2 Private (not importable from other modules)
113
131
 
114
132
  - `src/app/<module>/controllers/*` — controllers serve HTTP; nobody else should call them
@@ -279,7 +297,7 @@ When you create a new module:
279
297
  2. **Create the folder.** `mkdir src/app/<noun>`.
280
298
  3. **Add `README.md`** at the module root. One paragraph: what the module owns, who owns the code, public surface.
281
299
  4. **Scaffold only what you need.** Don't pre-create `events/` and `errors/` if you won't use them today.
282
- 5. **Add sub-folder barrels** only where you have 2+ files. A single service doesn't need a `services/index.ts` yet — but it will the moment a second service lands.
300
+ 5. **Add sub-folder barrels** only if the team wants them; they are optional, and `generate: { barrels: false }` makes generators write none.
283
301
  6. **Wire `routes.ts`** for any HTTP endpoints, register events for any emitted signals.
284
302
  7. **Verify no module-root `index.ts`** was created.
285
303
 
@@ -292,7 +310,8 @@ Before merging a change that adds, restructures, or imports across modules:
292
310
  - [ ] New module is named after a single domain noun
293
311
  - [ ] Standard folder layout from § 2 (only the folders the module needs)
294
312
  - [ ] No module-root `index.ts` created
295
- - [ ] Sub-folder barrels exist for `services/`, `utils/`, `types/` where applicable
313
+ - [ ] Barrels, if any, are sub-folder barrels; none is required
314
+ - [ ] Browser code imports from `app/<module>/` only `*.client.*`, schema, types and `web/` files
296
315
  - [ ] No imports from another module's `controllers/`, `repositories/`, `schema/`
297
316
  - [ ] No circular imports between modules (Oxlint `no-cycle` clean)
298
317
  - [ ] Cross-module communication uses direct service call or `@mongez/events` — never shared mutable globals
@@ -25,7 +25,7 @@ When the `test` feature is selected at scaffold time, both land automatically (V
25
25
 
26
26
  ### 0.1 If testing is enabled, tests are mandatory — not optional
27
27
 
28
- - **Every service** ships a co-located unit spec: `place-order.service.ts` → `place-order.service.spec.ts`. Cover the happy path, every branch, and every error path. Mock the repository — the service's logic is the unit under test.
28
+ - **Every service** ships a co-located unit spec: `place-order.service.ts` → `place-order.service.unit.spec.ts`. Cover the happy path, every branch, and every error path. Mock the repository — the service's logic is the unit under test.
29
29
  - **Every HTTP controller / request handler** ships a test: a unit test with mocked services for its branch / validation / authorization logic, **or** an integration test through the route when the wiring is the point. A new endpoint without a test is an incomplete endpoint.
30
30
 
31
31
  Write the spec in the **same change** that adds the service or controller. A reviewer treats a missing spec as a blocking defect, not a follow-up.
@@ -299,7 +299,7 @@ Run against a recorded fixture in CI (fast, deterministic). Run against the real
299
299
 
300
300
  ### 9.2 Where they live
301
301
 
302
- Co-located with the integration: `src/app/payments/clients/stripe-client.contract.spec.ts`.
302
+ Co-located with the integration: `src/app/payments/clients/stripe-client.contract.spec.ts` (an integration spec, because it has no `.unit`).
303
303
 
304
304
  ---
305
305
 
@@ -308,13 +308,13 @@ Co-located with the integration: `src/app/payments/clients/stripe-client.contrac
308
308
  ```
309
309
  src/app/orders/services/
310
310
  place-order.service.ts
311
- place-order.service.spec.ts ← unit test, mocks repository
312
- place-order.service.test.ts ← integration, real DB
311
+ place-order.service.unit.spec.ts ← unit test, mocks repository
312
+ place-order.service.test.ts ← integration, real DB
313
313
  ```
314
314
 
315
- - `.spec.ts` — default suffix; covers unit tests
316
- - `.test.ts` or a spec inside `tests/` — integration, real I/O (DB, queue)
317
- - Full-system journeys also use `.test.ts` in the integration project.
315
+ - `.unit.spec.ts` / `.unit.test.ts` — the unit project: no app boot, no setup files
316
+ - every other `.spec.ts` / `.test.ts`, and any file inside `tests/` — integration, with the booted app and real I/O (DB, queue)
317
+ - Full-system journeys are plain `.test.ts` files in the integration project.
318
318
 
319
319
  Tests live next to the file under test, always. Never in a separate `__tests__/` or `test/` folder — colocation makes the link obvious and survives refactors.
320
320
 
@@ -324,7 +324,7 @@ Tests live next to the file under test, always. Never in a separate `__tests__/`
324
324
 
325
325
  Before merging a change that should include tests (testing enabled — see § 0):
326
326
 
327
- - [ ] Every new/changed **service** has a co-located `.service.spec.ts`
327
+ - [ ] Every new/changed **service** has a co-located `.service.unit.spec.ts`
328
328
  - [ ] Every new/changed **HTTP controller / request handler** has a test (unit with mocked services, or integration through the route)
329
329
  - [ ] New public function has at least one test
330
330
  - [ ] Branches / error paths / edge cases covered
@@ -342,29 +342,42 @@ Before merging a change that should include tests (testing enabled — see § 0)
342
342
  ## Runtime cost and release gates
343
343
 
344
344
  With `--features=test`, create-warlock delegates to `warlock add test` to generate
345
- `vite.config.ts`: the `unit` project runs `src/**/*.spec.ts` outside `tests/`
346
- without framework or HTTP setup; `integration` runs `*.test.ts` and specs under
347
- `tests/` with `src/test-setup.ts`, global HTTP setup and `src/test-runner.ts`.
345
+ `vite.config.ts`: the `unit` project runs `src/**/*.unit.{spec,test}.ts`
346
+ without framework or HTTP setup; `integration` runs every other `*.spec.ts` and
347
+ `*.test.ts` and anything under `tests/` with `src/test-setup.ts`, global HTTP setup and `src/test-runner.ts`.
348
348
  Prefer `npx vitest run --project unit` during the pure service loop.
349
349
 
350
350
  When a unit needs config values, call
351
351
  `setupTest({ mode: "unit", importModule: (file) => import(file) })` from
352
352
  `@warlock.js/core/tests` and pair it with `afterAll(teardownTest)`. Config loads
353
353
  once per module graph, without hooks, connectors, native loader or fakes.
354
- Most unit specs need no setup call.
355
-
356
- Integration setup defaults to `tests.lifetime: "file"`. For reuse, set
357
- `lifetime: "worker"` in `src/config/tests.ts`, change the integration project to
358
- `pool: "forks", isolate: false`, keep the generated runner, and use `vitest run`.
359
- Watch, threads and isolated graphs are refused. Keep `afterAll(teardownTest)`:
360
- it marks file boundaries while the runner closes connectors at worker exit.
361
- `warlock add test` generates `src/config/tests.ts` so `WARLOCK_TEST_LIFETIME=worker`
362
- switches `lifetime` to `"worker"`, turns on `resetDatabase` (truncates every table or
363
- collection before each file, keeping the migrations ledger) and flips the integration
364
- project to `isolate: false`. Use `workerDatabaseName(base)` from
365
- `@warlock.js/core/tests` so each worker has its own database. When setup dominates a
366
- file-lifetime run, the profiler summary recommends this. Servers from
367
- `createTestHttpServer()` a file leaves open are closed before the next file.
354
+ Most unit specs need no setup call. The `.unit` infix is the only switch: a plain `.spec.ts` boots the app.
355
+
356
+ Integration setup boots the framework once per Vitest worker (`lifetime: "worker"`
357
+ in `tests`): the generated `src/config/tests.ts` also turns on `resetDatabase` (truncates
358
+ every table or collection before each file, keeping the migrations ledger), the
359
+ integration project has `pool: "forks", isolate: false` with the generated runner, and
360
+ `src/config/database.ts` gives each worker its own database
361
+ (`<DB_NAME>_test_<worker id>`). Use `vitest run`; watch, threads and isolated graphs are
362
+ refused. Keep `afterAll(teardownTest)`: it marks file boundaries while the runner closes
363
+ connectors at worker exit. `WARLOCK_TEST_LIFETIME=file` boots once per file instead
364
+ (slower, with isolated module state).
365
+
366
+ Before each file the runtime closes `createTestHttpServer()` servers the previous file
367
+ left open, clears the request context, restores the route table to the booted app's,
368
+ flushes a real in-memory cache, reinstalls the fakes (mail, queue, cache, storage, fake
369
+ clock, ids) and truncates the database. A spec that changes global configuration these
370
+ resets do not undo opts its file out with
371
+ `setupTest({ importModule: (file) => import(file), lifetime: "file" })`.
372
+
373
+ The `unit` project fails a spec that imports a connector, `connectorsManager`,
374
+ `bootstrap`, a test-server helper, or calls `setupTest()` without `{ mode: "unit" }`.
375
+ `warlock check` reports `WL9024` (a warning) for an integration spec that imports nothing
376
+ needing the app: rename it to `<name>.unit.spec.ts`.
377
+
378
+ Vitest 5 defaults `clearMocks` to true, which also clears the calls a module made when it
379
+ was imported (a scheduler registering jobs from `main.ts`). To assert those calls, import
380
+ the module inside the test after the mocks are set up, or set `clearMocks: false`.
368
381
  Set `tests.resetBetweenFiles` to an async application-owned cleanup callback;
369
382
  it runs before every file, including the first, after selected fakes, clock and
370
383
  IDs reset. Each worker needs a separate database/schema. Module singletons and
@@ -373,7 +386,9 @@ storage is private and does not mirror the shared HTTP server's fake files.
373
386
 
374
387
  Use `tests.profileSetup: true` or
375
388
  `WARLOCK_TEST_PROFILE=1 npx vitest run --project integration` for file timings and
376
- summaries per worker. Pass `file: expect.getState().testPath` in the setup call.
389
+ summaries per worker; the run summary lists the boot phases (config, translations,
390
+ connectors one by one, fakes, routes, reset, teardown), the HTTP test server's boot and
391
+ the slowest files, and `WARLOCK_TEST_PROFILE_JSON=profile.json` writes them as JSON. Pass `file: expect.getState().testPath` in the setup call.
377
392
  `getTestRuntimeStats()` exposes bootstrap and file counts. Generated configs use
378
393
  `recommendedTestWorkers()` from `@warlock.js/core/vite` to respect visible cgroup
379
394
  v2 CPU quotas; reduce `maxWorkers` further for database or memory contention.
@@ -47,13 +47,24 @@ both places. Put component props in that dedicated type module by default;
47
47
  keep a tiny private prop type beside a component only when extracting it would
48
48
  make the feature harder to read.
49
49
 
50
+ Browser code may import from `app/<module>/...` only `*.client.ts(x)` files,
51
+ schema (`*.schema.ts`, `schema/`), types (`*.types.ts`, `types/`) and the
52
+ module's `web/` folder; other files of a module are server code and fail the
53
+ client build with the import chain. Use `import type` to share the input and
54
+ output types a service exports. A `*.client.ts(x)` file may be imported by
55
+ pages and components (it still renders on the server unless wrapped in
56
+ `<ClientOnly>`) and never by server-only code; `warlock check` reports `WL9022`
57
+ for that. These rules do not change the web layout: one component per file,
58
+ logic in `*.setup.ts` and `use-*.ts`.
59
+
50
60
  ## 1. Keep the page contract, make the implementation thin
51
61
 
52
62
  Page logic lives in the matching `.setup.ts` file: `config`, `loader`,
53
63
  `register`, and `action`/`actions`. The UI file owns its named default
54
64
  component; re-export a framework-required named component such as `ErrorBoundary`
55
65
  from that component's own file. Pair `layout.tsx` with
56
- `layout.setup.ts`, `root.tsx` with `root.setup.ts`, and `error.page.tsx` with
66
+ `layout.setup.ts`, `root.tsx` with `root.setup.ts`, `forbidden.page.tsx` with
67
+ `forbidden.setup.ts`, and `error.page.tsx` with
57
68
  `error.setup.ts`. Use type-only imports from setup for loader/action inference.
58
69
  The framework projects browser-safe `register()` code to the client. Put route,
59
70
  cache, validation, middleware, metadata, and sitemap policy inside `config`;
@@ -0,0 +1,64 @@
1
+ {
2
+ "invalidCredentials": {
3
+ "en": "Invalid email or password",
4
+ "ar": "البريد الالكتروني أو كلمة المرور غير صحيحة"
5
+ },
6
+ "loggedOut": {
7
+ "en": "Logged out successfully",
8
+ "ar": "تم تسجيل الخروج بنجاح"
9
+ },
10
+ "loggedOutAll": {
11
+ "en": "Logged out from all devices",
12
+ "ar": "تم تسجيل الخروج من جميع الأجهزة"
13
+ },
14
+ "refreshTokenRequired": {
15
+ "en": "Refresh token is required",
16
+ "ar": "رمز التحديث مطلوب"
17
+ },
18
+ "invalidRefreshToken": {
19
+ "en": "Invalid or expired refresh token",
20
+ "ar": "رمز التحديث غير صالح أو منتهي الصلاحية"
21
+ },
22
+ "userNotFound": {
23
+ "en": "User not found",
24
+ "ar": "المستخدم غير موجود"
25
+ },
26
+ "otpSent": {
27
+ "en": "Verification code sent",
28
+ "ar": "تم إرسال رمز التحقق"
29
+ },
30
+ "missingOtp": {
31
+ "en": "Verification code not found",
32
+ "ar": "رمز التحقق غير موجود"
33
+ },
34
+ "otpInvalid": {
35
+ "en": "Invalid verification code",
36
+ "ar": "رمز التحقق غير صالح"
37
+ },
38
+ "otpExpired": {
39
+ "en": "Verification code has expired",
40
+ "ar": "رمز التحقق منتهي الصلاحية"
41
+ },
42
+ "otpMaxAttempts": {
43
+ "en": "Too many failed attempts",
44
+ "ar": "عدد المحاولات تجاوز الحد المسموح"
45
+ },
46
+ "passwordResetSuccess": {
47
+ "en": "Password reset successfully",
48
+ "ar": "تم إعادة تعيين كلمة المرور بنجاح"
49
+ },
50
+ "errors": {
51
+ "missingAccessToken": {
52
+ "en": "Access token is required",
53
+ "ar": "رمز الوصول مطلوب"
54
+ },
55
+ "invalidAccessToken": {
56
+ "en": "Invalid or expired access token",
57
+ "ar": "رمز الوصول غير صالح أو منتهي الصلاحية"
58
+ },
59
+ "unauthorized": {
60
+ "en": "You are not authorized to access this resource",
61
+ "ar": "غير مصرح لك بالوصول إلى هذا المورد"
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$group": "attributes",
3
+ "email": {
4
+ "en": "Email",
5
+ "ar": "البريد الإلكتروني"
6
+ },
7
+ "phoneNumber": {
8
+ "en": "Phone Number",
9
+ "ar": "رقم الهاتف"
10
+ },
11
+ "firstName": {
12
+ "en": "First Name",
13
+ "ar": "الاسم الأول"
14
+ },
15
+ "lastName": {
16
+ "en": "Last Name",
17
+ "ar": "الاسم الأخير"
18
+ },
19
+ "password": {
20
+ "en": "Password",
21
+ "ar": "كلمة المرور"
22
+ },
23
+ "name": {
24
+ "en": "Name",
25
+ "ar": "الاسم"
26
+ },
27
+ "image": {
28
+ "en": "Image",
29
+ "ar": "الصورة"
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$group": "http",
3
+ "rateLimitExceeded": {
4
+ "en": "Too many requests. Please slow down and try again shortly.",
5
+ "ar": "عدد كبير من الطلبات. يرجى التمهل وإعادة المحاولة قريباً."
6
+ },
7
+ "concurrencyLimitReached": {
8
+ "en": "This endpoint is busy. Please retry in a moment.",
9
+ "ar": "هذه النقطة الطرفية مشغولة. يرجى إعادة المحاولة بعد قليل."
10
+ },
11
+ "bodyTooLarge": {
12
+ "en": "Request body is too large.",
13
+ "ar": "حجم الطلب كبير جداً."
14
+ },
15
+ "idempotencyKeyInvalid": {
16
+ "en": "Idempotency-Key header is malformed.",
17
+ "ar": "ترويسة Idempotency-Key غير صالحة."
18
+ },
19
+ "idempotencyKeyConflict": {
20
+ "en": "Idempotency-Key was reused with a different request body.",
21
+ "ar": "تم إعادة استخدام Idempotency-Key مع طلب مختلف."
22
+ },
23
+ "ipForbidden": {
24
+ "en": "Access denied for your IP address.",
25
+ "ar": "تم رفض الوصول من عنوان IP الخاص بك."
26
+ },
27
+ "maintenance": {
28
+ "en": "Service is temporarily unavailable for maintenance.",
29
+ "ar": "الخدمة غير متاحة مؤقتاً للصيانة."
30
+ }
31
+ }