@skyf0xx/hedgehog 4.2.0 → 4.2.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.
Files changed (46) hide show
  1. package/README.md +20 -0
  2. package/bin/cli.mjs +12 -0
  3. package/package.json +2 -2
  4. package/src/agents/backend-eng.md +30 -16
  5. package/src/agents/front-end-eng.md +29 -12
  6. package/src/db/next.mjs +46 -8
  7. package/src/golden-cores/full-stack-app/apps/api/package.json +19 -1
  8. package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.spec.ts +15 -0
  9. package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.ts +6 -1
  10. package/src/golden-cores/full-stack-app/apps/api/src/app/feature-modules.ts +8 -0
  11. package/src/golden-cores/full-stack-app/apps/api/tsconfig.app.json +3 -0
  12. package/src/golden-cores/full-stack-app/apps/api/tsconfig.json +3 -0
  13. package/src/golden-cores/full-stack-app/apps/api/tsconfig.spec.json +36 -0
  14. package/src/golden-cores/full-stack-app/apps/api/vitest.config.mts +18 -0
  15. package/src/golden-cores/full-stack-app/apps/web/src/components/theme-toggle.spec.tsx +20 -0
  16. package/src/golden-cores/full-stack-app/apps/web/src/test-setup.ts +1 -0
  17. package/src/golden-cores/full-stack-app/apps/web/tsconfig.json +6 -0
  18. package/src/golden-cores/full-stack-app/apps/web/tsconfig.spec.json +37 -0
  19. package/src/golden-cores/full-stack-app/apps/web/vitest.config.mts +27 -0
  20. package/src/golden-cores/full-stack-app/nx.json +4 -1
  21. package/src/golden-cores/full-stack-app/package.json +8 -0
  22. package/src/golden-cores/full-stack-app/pnpm-lock.yaml +8735 -2907
  23. package/src/golden-cores/full-stack-app/tools/generate-feature-modules.cjs +104 -0
  24. package/src/golden-cores/full-stack-app/tools/generators/contract/generator.ts +283 -0
  25. package/src/golden-cores/full-stack-app/tools/generators/contract/schema.json +20 -0
  26. package/src/golden-cores/full-stack-app/tools/generators/controller/generator.ts +323 -0
  27. package/src/golden-cores/full-stack-app/tools/generators/controller/schema.json +20 -0
  28. package/src/golden-cores/full-stack-app/tools/generators/fields.ts +126 -0
  29. package/src/golden-cores/full-stack-app/tools/generators/generators.json +42 -0
  30. package/src/golden-cores/full-stack-app/tools/generators/hook/generator.ts +274 -0
  31. package/src/golden-cores/full-stack-app/tools/generators/hook/schema.json +19 -0
  32. package/src/golden-cores/full-stack-app/tools/generators/lib-shell.ts +124 -0
  33. package/src/golden-cores/full-stack-app/tools/generators/naming.ts +84 -0
  34. package/src/golden-cores/full-stack-app/tools/generators/package.json +6 -0
  35. package/src/golden-cores/full-stack-app/tools/generators/repository/generator.ts +298 -0
  36. package/src/golden-cores/full-stack-app/tools/generators/repository/schema.json +15 -0
  37. package/src/golden-cores/full-stack-app/tools/generators/schema/generator.ts +169 -0
  38. package/src/golden-cores/full-stack-app/tools/generators/schema/schema.json +20 -0
  39. package/src/golden-cores/full-stack-app/tools/generators/screen/generator.ts +218 -0
  40. package/src/golden-cores/full-stack-app/tools/generators/screen/schema.json +15 -0
  41. package/src/golden-cores/full-stack-app/tools/generators/service/generator.ts +194 -0
  42. package/src/golden-cores/full-stack-app/tools/generators/service/schema.json +15 -0
  43. package/src/skills/hedgehog-authored-loop/SKILL.md +1 -1
  44. package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +72 -7
  45. package/src/skills/hedgehog-loop/SKILL.md +123 -55
  46. package/src/templates/CLAUDE.md +3 -2
package/README.md CHANGED
@@ -68,6 +68,19 @@ Every dependency is explicit, so Hedgehog knows which tasks can run in parallel.
68
68
 
69
69
  Agents fan out to give you great outcomes at **faster speeds**.
70
70
 
71
+ ## Deterministic Code Generation
72
+
73
+ Where a piece of code has one correct shape, Hedgehog generates it instead of asking the AI to write it freehand.
74
+
75
+ ![Deterministic code generation](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/deterministic.png)
76
+
77
+ - **Generators over freehand**: schemas, boilerplate, and scaffolding come from generators that create the code
78
+ - **Faster**: no reasoning required to produce a known shape
79
+ - **More correct**: the same input always produces the same, tested output
80
+ - **Fewer tokens**: nothing spent generating code that a template already covers
81
+
82
+ The AI is reserved for the parts a generator can't cover.
83
+
71
84
  ## What Hedgehog builds
72
85
 
73
86
  ### Full-stack applications
@@ -138,6 +151,13 @@ npx @skyf0xx/hedgehog init
138
151
 
139
152
  Then open your coding agent and describe what you want to build.
140
153
 
154
+ The golden cores print a `pnpm install` step as part of their next steps.
155
+ On a fresh project with no warm pnpm store, that first install can take
156
+ several minutes — it's pulling a full monorepo toolchain (Nx, webpack,
157
+ sass-embedded, Playwright, etc.) and, on first commit, running the commit
158
+ gate against the whole workspace. A quiet stretch of output during that
159
+ step is expected, not a hang.
160
+
141
161
  ### Coding agents
142
162
 
143
163
  Hedgehog installs for **Claude Code** by default. Add a host flag to
package/bin/cli.mjs CHANGED
@@ -504,6 +504,18 @@ async function init({ force, core, explicitCore, host = DEFAULT_HOST, hostOnly =
504
504
  // runs the whole workspace (see lefthook.yml), so expect it to take
505
505
  // as long as a full typecheck/lint/test — that is the gate working.
506
506
  console.log(` 1. ${bold('pnpm install')}`);
507
+ console.log(
508
+ dim(' First run can take several minutes — full dependency install plus'),
509
+ );
510
+ console.log(dim(' the commit gate running against the whole workspace. This is'));
511
+ console.log(dim(' expected; let it finish rather than treating a quiet stretch as stuck.'));
512
+ console.log(
513
+ dim(' If an agent is running this: tell the user up front that first install'),
514
+ );
515
+ console.log(
516
+ dim(' can take several minutes and why, then check on progress every ~30s'),
517
+ );
518
+ console.log(dim(' instead of polling tightly or narrating the wait.'));
507
519
  console.log(` 2. ${bold('git add -A && git commit -m "chore: install Hedgehog"')}`);
508
520
  console.log(` 3. Open ${HOSTS[host].label} and describe what you want to build.`);
509
521
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "4.2.0",
3
+ "version": "4.2.3",
4
4
  "description": "Install the Hedgehog build discipline (agents + skills) into a repo, for Claude Code, Cursor, or Gemini CLI.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -44,4 +44,4 @@
44
44
  "hedgehog"
45
45
  ],
46
46
  "license": "MIT"
47
- }
47
+ }
@@ -34,10 +34,19 @@ build exactly what its ALLOWED SCOPE names, one layer at a time, gated by
34
34
  Use `nx-run-tasks` (build/lint/test/typecheck), `nx-workspace` (inspecting
35
35
  project/target config), `nx-generate` (scaffolding a new library/app), and
36
36
  `link-workspace-packages` (wiring a new package into a consumer) as
37
- needed. A layer's first-arrival package shell (`contract`, `repository`,
38
- `service`) is a generator call, not a hand-copy of a sibling package —
39
- `hedgehog-loop`'s "First arrival in a package" section names the exact
40
- command and tags for each.
37
+ needed.
38
+
39
+ **Every layer you own starts from its generator in `tools/generators/`** —
40
+ one per layer (`schema`, `contract`, `repository`, `service`,
41
+ `controller`), each landing that layer's package shell, `nx.tags`,
42
+ port-discipline file suffixes, Nest module/controller pair, and barrel
43
+ wiring in one step. The claimed packet's LAYER SHAPE section prints the
44
+ exact command for the layer you're on; `hedgehog-loop`'s "Scaffolding a
45
+ layer" section owns the full flag contract and the workspace wiring a new
46
+ package needs. Generate first, then author this entity's delta — the field
47
+ list and its types, and the business rules below — on top. A hand-copy of
48
+ a sibling module is the drift `hedgehog verify`'s lint step then has to
49
+ catch.
41
50
 
42
51
  ## Core Responsibilities
43
52
 
@@ -98,24 +107,29 @@ command and tags for each.
98
107
  `complete` — no need to re-derive that by hand. Cross-module FK
99
108
  targets should already have their own schema landed (the packet's
100
109
  dependencies guarantee this); check before writing the FK column.
101
- 2. Build exactly one layer, matching the packet's ALLOWED SCOPE. Run
102
- typecheck, lint, and test yourself as a sanity check before reporting
103
- back necessary, not sufficient. If this layer also has to create the
104
- package it lands in (the first module through `contract` creates
105
- `packages/contracts`), its shell files sit outside the packet's ALLOWED
106
- SCOPE and `hedgehog verify` will leave them uncommitted stop and say
107
- so before building, so the scope can be widened for this one task
110
+ 2. Build exactly one layer, matching the packet's ALLOWED SCOPE: run its
111
+ generator, then author this entity's delta. Run typecheck, lint, and
112
+ test yourself as a sanity check before reporting back necessary, not
113
+ sufficient. If this layer also has to create the package it lands in
114
+ (the first module through `contract` creates `packages/contracts`), the
115
+ shell files its generator lands sit outside the packet's ALLOWED SCOPE
116
+ and `hedgehog verify` will leave them uncommitted stop and say so
117
+ before building, so the scope can be widened for this one task
108
118
  (`hedgehog-loop`, "First arrival in a package"). Don't build against a
109
- scope you already know won't commit your work. That section also owns
110
- the workspace wiring a new package needs (`pnpm install`, `pnpm nx
111
- sync`, and its own `chore(workspace):` commit) follow it rather than
112
- leaving the package unlinked for a later layer to trip over.
119
+ scope you already know won't commit your work. If the layer wires a new
120
+ or newly-linked package into the workspace, run `pnpm install` and
121
+ `pnpm nx sync` yourself (`hedgehog-loop`, "Scaffolding a layer") and
122
+ name the shared files that changed (typically `pnpm-lock.yaml`, root
123
+ `tsconfig.json`) in your report — the orchestrating session commits
124
+ them separately, since you report but never commit (next step).
113
125
  3. **Report the work as done; do not commit it yourself.** Per the build
114
126
  graph's design, an agent reporting success never moves a task — only
115
127
  `hedgehog verify <task-id>`'s passing exit code does. It checks your
116
128
  changes against the packet's ALLOWED SCOPE, re-runs the real
117
129
  verification command, and on a pass writes the commit (the packet's
118
- exact Conventional Commit message) itself.
130
+ exact Conventional Commit message) itself. Any shared workspace files
131
+ you flagged in step 2 are a separate commit the orchestrating session
132
+ makes before dispatching `hedgehog verify`, not something you commit.
119
133
  4. One layer at a time — never start the next layer before
120
134
  `hedgehog verify` reports the current one `complete`.
121
135
  5. Once `hedgehog verify` reports the `controller` layer (and any bundled
@@ -32,9 +32,20 @@ before the next starts.
32
32
  Use `nx-run-tasks` (build/lint/test/typecheck), `nx-workspace` (inspecting
33
33
  project/target config), `nx-generate` (scaffolding a new library/app), and
34
34
  `link-workspace-packages` (wiring a new package into a consumer) as
35
- needed. The `hook` layer's first-arrival package shell is a generator
36
- call, not a hand-copy of a sibling package — `hedgehog-loop`'s "First
37
- arrival in a package" section names the exact command and tags.
35
+ needed.
36
+
37
+ **Both layers you own start from their generator in `tools/generators/`**
38
+ `hook` lands `packages/hooks`'s shell, `nx.tags`, the query hook set, and
39
+ its barrel wiring; `screen` lands the route, the screen component wired to
40
+ that hook, and its test file. The claimed packet's LAYER SHAPE section
41
+ prints the exact command for the layer you're on; `hedgehog-loop`'s
42
+ "Scaffolding a layer" section owns the full flag contract and the
43
+ workspace wiring a new package needs. Generate first, then author this
44
+ entity's delta on top. The `screen` generator is skeleton-only by
45
+ design — placeholders for the list, filter shell, empty state, and form,
46
+ with layout, information hierarchy, and interaction pattern left to
47
+ `ux-planner`'s rationale and your build, so no two modules' screens come
48
+ out identical before anyone decided they should.
38
49
 
39
50
  If the screen step calls for animation or motion — entrances, sequencing,
40
51
  scroll-driven effects, drag, SVG/morph effects — use GSAP, loading the
@@ -93,18 +104,24 @@ don't reach for a second one.
93
104
  no such confirmation, stop — you're being asked to build Phase B
94
105
  early.
95
106
  2. Build the hook against the contract client, matching the packet's
96
- ALLOWED SCOPE. Run typecheck, lint, and test yourself as a sanity
97
- check before reporting back necessary, not sufficient. On the first
98
- module through this layer, the hook also creates `packages/hooks`, and
99
- that package's shell sits outside the packet's ALLOWED SCOPE —
100
- `hedgehog verify` would leave it uncommitted. Stop and say so before
101
- building, so the scope can be widened for this one task
102
- (`hedgehog-loop`, "First arrival in a package") — that section also
103
- owns the workspace wiring the new package needs.
107
+ ALLOWED SCOPE: run its generator, then author this entity's delta. Run
108
+ typecheck, lint, and test yourself as a sanity check before reporting
109
+ back necessary, not sufficient. On the first module through this
110
+ layer, the hook also creates `packages/hooks`, and that package's shell
111
+ sits outside the packet's ALLOWED SCOPE `hedgehog verify` would leave
112
+ it uncommitted. Stop and say so before building, so the scope can be
113
+ widened for this one task (`hedgehog-loop`, "First arrival in a
114
+ package"); run `pnpm install` and `pnpm nx sync` yourself
115
+ (`hedgehog-loop`, "Scaffolding a layer") to wire the new package into
116
+ the workspace, and name the shared files that changed (typically
117
+ `pnpm-lock.yaml`, root `tsconfig.json`) in your report — the
118
+ orchestrating session commits them separately (next step).
104
119
  3. **Report the work as done; do not commit it yourself.** Only
105
120
  `hedgehog verify <task-id>`'s passing exit code moves the task to
106
121
  `complete` and writes the commit (the packet's exact Conventional
107
- Commit message).
122
+ Commit message). Any shared workspace files you flagged in step 2 are a
123
+ separate commit the orchestrating session makes before dispatching
124
+ `hedgehog verify`, not something you commit.
108
125
  4. Build the screen consuming the hook the same way — packet, build,
109
126
  report, `hedgehog verify`.
110
127
  5. One layer at a time — `hook` fully `complete` before the `screen`
package/src/db/next.mjs CHANGED
@@ -241,14 +241,50 @@ export function taskStatusLine(task) {
241
241
  // — printing it in the packet turns that failure into a pre-flight fact
242
242
  // instead of something `nx lint` teaches the agent after the fact. Keyed
243
243
  // by layer id, not module, since the shape is the same for every module.
244
+ //
245
+ // `scaffold` is the same kind of fact one step earlier: the generator in
246
+ // `tools/generators/` every layer starts from (hedgehog-loop, "Scaffolding
247
+ // a layer", which owns the flag contract these lines instantiate). It sits
248
+ // here rather than in RELEVANT RULES because that section is per-intent —
249
+ // identical across every layer of an intent, by plan.mjs's requirement
250
+ // linkage — while the command differs per layer, which is exactly what
251
+ // LAYER SHAPE already keys on.
244
252
  const FULL_STACK_APP_LAYER_TAGS = {
245
- schema: { tags: ['scope:db', 'type:adapter'], dependsOnTags: [] },
246
- contract: { tags: ['scope:contracts', 'type:contract'], dependsOnTags: ['type:adapter', 'type:util'] },
247
- repository: { tags: ['scope:{module}', 'type:adapter'], dependsOnTags: ['type:adapter', 'type:contract', 'type:util'] },
248
- service: { tags: ['scope:{module}', 'type:service'], dependsOnTags: ['type:adapter', 'type:contract', 'type:util'] },
249
- controller: { tags: ['scope:api'], dependsOnTags: ['type:adapter', 'type:service', 'type:contract', 'type:util'] },
250
- hook: { tags: ['scope:hooks', 'type:hook'], dependsOnTags: ['type:contract', 'type:util'] },
251
- screen: { tags: ['scope:web'], dependsOnTags: ['scope:contracts', 'scope:hooks', 'scope:shared', 'type:util'] },
253
+ schema: {
254
+ tags: ['scope:db', 'type:adapter'],
255
+ dependsOnTags: [],
256
+ scaffold: "nx g ./tools/generators:schema --module={module} --fields='<name:type,...>'",
257
+ },
258
+ contract: {
259
+ tags: ['scope:contracts', 'type:contract'],
260
+ dependsOnTags: ['type:adapter', 'type:util'],
261
+ scaffold: "nx g ./tools/generators:contract --module={module} --fields='<the schema layer's list>'",
262
+ },
263
+ repository: {
264
+ tags: ['scope:{module}', 'type:adapter'],
265
+ dependsOnTags: ['type:adapter', 'type:contract', 'type:util'],
266
+ scaffold: 'nx g ./tools/generators:repository --module={module}',
267
+ },
268
+ service: {
269
+ tags: ['scope:{module}', 'type:service'],
270
+ dependsOnTags: ['type:adapter', 'type:contract', 'type:util'],
271
+ scaffold: 'nx g ./tools/generators:service --module={module}',
272
+ },
273
+ controller: {
274
+ tags: ['scope:api'],
275
+ dependsOnTags: ['type:adapter', 'type:service', 'type:contract', 'type:util'],
276
+ scaffold: "nx g ./tools/generators:controller --module={module} --fields='<the schema layer's list>'",
277
+ },
278
+ hook: {
279
+ tags: ['scope:hooks', 'type:hook'],
280
+ dependsOnTags: ['type:contract', 'type:util'],
281
+ scaffold: 'nx g ./tools/generators:hook --module={module} [--toggleField=<boolField>]',
282
+ },
283
+ screen: {
284
+ tags: ['scope:web'],
285
+ dependsOnTags: ['scope:contracts', 'scope:hooks', 'scope:shared', 'type:util'],
286
+ scaffold: 'nx g ./tools/generators:screen --module={module} (skeleton only — UX is yours)',
287
+ },
252
288
  };
253
289
 
254
290
  // A LAYER SHAPE section for full-stack-app tasks only (`coreId ===
@@ -267,10 +303,12 @@ function layerShapeLines(task, coreId) {
267
303
  } else {
268
304
  lines.push(` may depend on tags: ${shape.dependsOnTags.join(', ')}`);
269
305
  }
306
+ lines.push(` scaffold from: ${shape.scaffold.replace('{module}', task.module)}`);
270
307
  lines.push(
271
308
  " Confirm against packages/config/eslint-base.js's depConstraints before",
272
309
  " writing an import — a mismatch here is a pre-flight fact, not a lint",
273
- ' failure to discover later.',
310
+ ' failure to discover later. Start from the generator and author only',
311
+ " this entity's delta on top of what it lands.",
274
312
  );
275
313
  return lines;
276
314
  }
@@ -4,8 +4,25 @@
4
4
  "private": true,
5
5
  "nx": {
6
6
  "targets": {
7
+ "generate-feature-modules": {
8
+ "executor": "nx:run-commands",
9
+ "cache": true,
10
+ "inputs": [
11
+ "{projectRoot}/src/app/*/*.module.ts"
12
+ ],
13
+ "outputs": [
14
+ "{projectRoot}/src/app/feature-modules.ts"
15
+ ],
16
+ "options": {
17
+ "command": "node tools/generate-feature-modules.cjs",
18
+ "cwd": "{workspaceRoot}"
19
+ }
20
+ },
7
21
  "build": {
8
22
  "executor": "nx:run-commands",
23
+ "dependsOn": [
24
+ "generate-feature-modules"
25
+ ],
9
26
  "options": {
10
27
  "command": "webpack-cli build",
11
28
  "env": {
@@ -91,7 +108,8 @@
91
108
  "tslib": "^2.3.0",
92
109
  "nestjs-pino": "^4.6.1",
93
110
  "pino-http": "^11.0.0",
94
- "config": "workspace:*"
111
+ "config": "workspace:*",
112
+ "db": "workspace:*"
95
113
  },
96
114
  "devDependencies": {
97
115
  "@nestjs/testing": "^11.0.0"
@@ -0,0 +1,15 @@
1
+ import { Test } from '@nestjs/testing';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { AppModule } from './app.module';
4
+
5
+ describe('AppModule', () => {
6
+ it('compiles with the health check controller wired', async () => {
7
+ const moduleRef = await Test.createTestingModule({
8
+ imports: [AppModule],
9
+ }).compile();
10
+
11
+ expect(moduleRef).toBeDefined();
12
+
13
+ await moduleRef.close();
14
+ });
15
+ });
@@ -1,9 +1,14 @@
1
1
  import { Module } from '@nestjs/common';
2
2
  import { LoggerModule } from 'nestjs-pino';
3
3
  import { HealthController } from './health.controller';
4
+ // Generated by tools/generate-feature-modules.cjs (see that file's header)
5
+ // from every apps/api/src/app/*/*.module.ts on disk — never hand-edited,
6
+ // so no domain module's controller task ever edits this file or
7
+ // app.module.ts itself to register.
8
+ import { featureModules } from './feature-modules';
4
9
 
5
10
  @Module({
6
- imports: [LoggerModule.forRoot()],
11
+ imports: [LoggerModule.forRoot(), ...featureModules],
7
12
  controllers: [HealthController],
8
13
  providers: [],
9
14
  })
@@ -0,0 +1,8 @@
1
+ // GENERATED FILE — do not hand-edit. Regenerated by
2
+ // tools/generate-feature-modules.cjs (the generate-feature-modules Nx
3
+ // target, which test and build depend on) from every
4
+ // apps/api/src/app/*/*.module.ts file on disk. A module directory's
5
+ // controller layer only ever creates its own *.module.ts inside its own
6
+ // directory — never edits this file.
7
+
8
+ export const featureModules = [];
@@ -14,6 +14,9 @@
14
14
  "references": [
15
15
  {
16
16
  "path": "../../packages/config/tsconfig.lib.json"
17
+ },
18
+ {
19
+ "path": "../../packages/db/tsconfig.lib.json"
17
20
  }
18
21
  ]
19
22
  }
@@ -5,6 +5,9 @@
5
5
  "references": [
6
6
  {
7
7
  "path": "./tsconfig.app.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
8
11
  }
9
12
  ]
10
13
  }
@@ -0,0 +1,36 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/vitest",
5
+ "types": [
6
+ "vitest/globals",
7
+ "vitest/importMeta",
8
+ "vite/client",
9
+ "node",
10
+ "vitest"
11
+ ],
12
+ "experimentalDecorators": true,
13
+ "emitDecoratorMetadata": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": [
17
+ "vite.config.ts",
18
+ "vite.config.mts",
19
+ "vitest.config.ts",
20
+ "vitest.config.mts",
21
+ "src/**/*.test.ts",
22
+ "src/**/*.spec.ts",
23
+ "src/**/*.test.tsx",
24
+ "src/**/*.spec.tsx",
25
+ "src/**/*.test.js",
26
+ "src/**/*.spec.js",
27
+ "src/**/*.test.jsx",
28
+ "src/**/*.spec.jsx",
29
+ "src/**/*.d.ts"
30
+ ],
31
+ "references": [
32
+ {
33
+ "path": "./tsconfig.app.json"
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig(() => ({
4
+ root: __dirname,
5
+ cacheDir: '../../node_modules/.vite/apps/api',
6
+ test: {
7
+ name: 'api',
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ reporters: ['default'],
13
+ coverage: {
14
+ reportsDirectory: './test-output/vitest/coverage',
15
+ provider: 'v8' as const,
16
+ },
17
+ },
18
+ }));
@@ -0,0 +1,20 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { ThemeToggle } from './theme-toggle';
5
+
6
+ describe('ThemeToggle', () => {
7
+ it('renders a toggle button and flips the document theme class on click', async () => {
8
+ const user = userEvent.setup();
9
+ render(<ThemeToggle />);
10
+
11
+ const toggle = screen.getByRole('button', { name: /toggle theme/i });
12
+ expect(toggle).toBeInTheDocument();
13
+
14
+ await user.click(toggle);
15
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
16
+
17
+ await user.click(toggle);
18
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
19
+ });
20
+ });
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom/vitest';
@@ -41,8 +41,11 @@
41
41
  "node_modules",
42
42
  "jest.config.ts",
43
43
  "jest.config.cts",
44
+ "src/test-setup.ts",
44
45
  "src/**/*.spec.ts",
45
46
  "src/**/*.test.ts",
47
+ "src/**/*.spec.tsx",
48
+ "src/**/*.test.tsx",
46
49
  ".next",
47
50
  "eslint.config.js",
48
51
  "eslint.config.cjs",
@@ -51,6 +54,9 @@
51
54
  "references": [
52
55
  {
53
56
  "path": "../../packages/config"
57
+ },
58
+ {
59
+ "path": "./tsconfig.spec.json"
54
60
  }
55
61
  ]
56
62
  }
@@ -0,0 +1,37 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/vitest",
5
+ "jsx": "preserve",
6
+ "types": [
7
+ "vitest/globals",
8
+ "vitest/importMeta",
9
+ "vite/client",
10
+ "node",
11
+ "vitest",
12
+ "@testing-library/jest-dom"
13
+ ],
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": [
17
+ "vite.config.ts",
18
+ "vite.config.mts",
19
+ "vitest.config.ts",
20
+ "vitest.config.mts",
21
+ "src/test-setup.ts",
22
+ "src/**/*.test.ts",
23
+ "src/**/*.spec.ts",
24
+ "src/**/*.test.tsx",
25
+ "src/**/*.spec.tsx",
26
+ "src/**/*.test.js",
27
+ "src/**/*.spec.js",
28
+ "src/**/*.test.jsx",
29
+ "src/**/*.spec.jsx",
30
+ "src/**/*.d.ts"
31
+ ],
32
+ "references": [
33
+ {
34
+ "path": "../../packages/config"
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import react from '@vitejs/plugin-react';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export default defineConfig(() => ({
6
+ root: __dirname,
7
+ cacheDir: '../../node_modules/.vite/apps/web',
8
+ plugins: [react()],
9
+ resolve: {
10
+ alias: {
11
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
12
+ },
13
+ },
14
+ test: {
15
+ name: 'web',
16
+ watch: false,
17
+ globals: true,
18
+ environment: 'jsdom',
19
+ setupFiles: ['./src/test-setup.ts'],
20
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
21
+ reporters: ['default'],
22
+ coverage: {
23
+ reportsDirectory: './test-output/vitest/coverage',
24
+ provider: 'v8' as const,
25
+ },
26
+ },
27
+ }));
@@ -6,7 +6,10 @@
6
6
  },
7
7
  "test": {
8
8
  "cache": true,
9
- "dependsOn": ["^build"]
9
+ "dependsOn": ["^build", "generate-feature-modules"]
10
+ },
11
+ "typecheck": {
12
+ "dependsOn": ["generate-feature-modules"]
10
13
  },
11
14
  "lint": {
12
15
  "cache": true
@@ -29,12 +29,16 @@
29
29
  "@swc/cli": "~0.8.1",
30
30
  "@swc/core": "~1.15.5",
31
31
  "@swc/helpers": "~0.5.18",
32
+ "@testing-library/jest-dom": "^7.0.1",
33
+ "@testing-library/react": "^16.3.2",
34
+ "@testing-library/user-event": "^14.6.4",
32
35
  "@types/node": "^22.0.0",
33
36
  "@types/pg": "^8.20.0",
34
37
  "@types/react": "^19.0.0",
35
38
  "@types/react-dom": "^19.0.0",
36
39
  "@typescript-eslint/eslint-plugin": "^8.65.0",
37
40
  "@typescript-eslint/parser": "^8.65.0",
41
+ "@vitejs/plugin-react": "^6.0.5",
38
42
  "@vitest/coverage-v8": "~4.1.0",
39
43
  "drizzle-kit": "^0.31.10",
40
44
  "drizzle-zod": "^0.8.3",
@@ -45,6 +49,7 @@
45
49
  "eslint-plugin-playwright": "^1.6.2",
46
50
  "eslint-plugin-react": "^7.35.0",
47
51
  "eslint-plugin-react-hooks": "5.0.0",
52
+ "jsdom": "^30.0.1",
48
53
  "lefthook": "^2.1.10",
49
54
  "nx": "23.1.0",
50
55
  "prettier": "~3.6.2",
@@ -74,6 +79,9 @@
74
79
  "zod": "^4.4.3"
75
80
  },
76
81
  "pnpm": {
82
+ "onlyBuiltDependencies": [
83
+ "lefthook"
84
+ ],
77
85
  "overrides": {
78
86
  "esbuild": "0.25.12",
79
87
  "axios": "^1.18.0",