@skyf0xx/hedgehog 4.1.0 → 4.2.2
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/README.md +21 -1
- package/bin/cli.mjs +108 -8
- package/package.json +2 -2
- package/src/agents/backend-eng.md +62 -16
- package/src/agents/front-end-eng.md +48 -14
- package/src/agents/reviewer.md +9 -4
- package/src/agents/ux-planner.md +2 -2
- package/src/db/next.mjs +95 -8
- package/src/db/status.mjs +140 -15
- package/src/golden-cores/full-stack-app/.env.example +1 -0
- package/src/golden-cores/full-stack-app/apps/api/package.json +19 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.spec.ts +15 -0
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.ts +6 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/feature-modules.ts +8 -0
- package/src/golden-cores/full-stack-app/apps/api/src/main.ts +10 -1
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.app.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.spec.json +36 -0
- package/src/golden-cores/full-stack-app/apps/api/vitest.config.mts +18 -0
- package/src/golden-cores/full-stack-app/apps/web/.env.example +12 -0
- package/src/golden-cores/full-stack-app/apps/web/package.json +3 -0
- package/src/golden-cores/full-stack-app/apps/web/src/components/theme-toggle.spec.tsx +20 -0
- package/src/golden-cores/full-stack-app/apps/web/src/test-setup.ts +1 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.json +6 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.spec.json +37 -0
- package/src/golden-cores/full-stack-app/apps/web/vitest.config.mts +27 -0
- package/src/golden-cores/full-stack-app/core.yaml +11 -3
- package/src/golden-cores/full-stack-app/nx.json +4 -1
- package/src/golden-cores/full-stack-app/package.json +8 -0
- package/src/golden-cores/full-stack-app/packages/config/eslint-base.js +108 -13
- package/src/golden-cores/full-stack-app/packages/config/src/env.schema.spec.ts +14 -1
- package/src/golden-cores/full-stack-app/packages/config/src/env.schema.ts +1 -0
- package/src/golden-cores/full-stack-app/packages/config/src/index.ts +1 -1
- package/src/golden-cores/full-stack-app/packages/db/src/index.ts +2 -1
- package/src/golden-cores/full-stack-app/packages/db/src/lib/db.spec.ts +3 -3
- package/src/golden-cores/full-stack-app/packages/db/src/schema/index.ts +4 -0
- package/src/golden-cores/full-stack-app/pnpm-lock.yaml +8735 -2907
- package/src/golden-cores/full-stack-app/pnpm-workspace.yaml +4 -0
- package/src/golden-cores/full-stack-app/tools/generate-feature-modules.cjs +104 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/generator.ts +283 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/generator.ts +323 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/fields.ts +126 -0
- package/src/golden-cores/full-stack-app/tools/generators/generators.json +42 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/generator.ts +274 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/schema.json +19 -0
- package/src/golden-cores/full-stack-app/tools/generators/lib-shell.ts +124 -0
- package/src/golden-cores/full-stack-app/tools/generators/naming.ts +84 -0
- package/src/golden-cores/full-stack-app/tools/generators/package.json +6 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/generator.ts +298 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/generator.ts +169 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/generator.ts +218 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/generator.ts +194 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tsconfig.base.json +2 -2
- package/src/skills/conventional-commits/SKILL.md +2 -3
- package/src/skills/hedgehog-bootstrap/SKILL.md +25 -7
- package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +135 -20
- package/src/skills/hedgehog-loop/SKILL.md +161 -8
package/src/db/status.mjs
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
// every task meeting that condition, not just the one `hedgehog next`
|
|
12
12
|
// would pick.
|
|
13
13
|
|
|
14
|
+
import { listDebt } from './debt.mjs';
|
|
14
15
|
import { detectDrift, formatDrift } from './drift.mjs';
|
|
16
|
+
import { listFriction } from './friction.mjs';
|
|
17
|
+
import { orphanedOverrides } from './overrides.mjs';
|
|
15
18
|
import { formatMissingRequirements } from './requires.mjs';
|
|
16
19
|
import { readyTasks, heldBackReason } from './ready.mjs';
|
|
17
20
|
|
|
@@ -82,16 +85,49 @@ function loadAttentionTasks(db) {
|
|
|
82
85
|
return db.prepare(ATTENTION_TASKS_SQL).all();
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// Collapses every debt row to one entry per declaring task: { taskId, n }
|
|
89
|
+
// in task-id order. The notes themselves stay out — a status overview
|
|
90
|
+
// that reprints them is `debt list` with extra steps, and the note only
|
|
91
|
+
// becomes actionable in the packet of a task that inherits it, where
|
|
92
|
+
// next.mjs already renders it in full. The task id is the part that has
|
|
93
|
+
// to be here, because it is the argument `debt list <task>` needs and
|
|
94
|
+
// the thing an operator has no other way to guess.
|
|
95
|
+
function loadDebtByTask(db) {
|
|
96
|
+
const byTask = new Map();
|
|
97
|
+
for (const { taskId } of listDebt(db)) {
|
|
98
|
+
byTask.set(taskId, (byTask.get(taskId) ?? 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
return [...byTask.entries()]
|
|
101
|
+
.map(([taskId, n]) => ({ taskId, n }))
|
|
102
|
+
.sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The friction row count, or 0. `listFriction` reads the table
|
|
106
|
+
// unguarded, so a build graph predating it throws here where `listDebt`
|
|
107
|
+
// would return [] — caught rather than propagated for the same reason
|
|
108
|
+
// the CLI tolerates an unparseable core.yaml: status is the command
|
|
109
|
+
// every session starts with, and one absent table must not cost the
|
|
110
|
+
// operator the whole overview. Every other section is still true.
|
|
111
|
+
function countFriction(db) {
|
|
112
|
+
try {
|
|
113
|
+
return listFriction(db).length;
|
|
114
|
+
} catch {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Returns { counts, ready, heldBack, inFlight, attention, drift,
|
|
120
|
+
// orphanedOverrides, debt, frictionCount, total } — counts keyed by
|
|
121
|
+
// every status in the tasks CHECK constraint (present even at zero),
|
|
122
|
+
// ready the full list of currently-pickable tasks, heldBack the subset
|
|
123
|
+
// of those that `hedgehog claim` would skip over right now because they
|
|
124
|
+
// conflict with in-flight work or another ready task ahead of them
|
|
125
|
+
// (ready.mjs's own simulation, reused rather than reimplemented —
|
|
126
|
+
// without this a task can sit in READY indefinitely with no visible
|
|
127
|
+
// reason, the same invisible-stall shape blocked tasks had before
|
|
128
|
+
// `attention` existed), inFlight the tasks currently leased (building or
|
|
129
|
+
// verifying), attention the stalled tasks needing a fix, total the sum
|
|
130
|
+
// across all statuses.
|
|
95
131
|
//
|
|
96
132
|
// `core` is optional and, when given, adds `drift`: the tasks whose
|
|
97
133
|
// layer-derived fields no longer match core.yaml (drift.mjs). It's a
|
|
@@ -103,6 +139,26 @@ function loadAttentionTasks(db) {
|
|
|
103
139
|
// resolve and pass in — detectDrift needs it composed against `core` or
|
|
104
140
|
// every task widened by `.hedgehog/overrides/*.json` reports permanent,
|
|
105
141
|
// spurious drift here.
|
|
142
|
+
//
|
|
143
|
+
// `orphanedOverrides` (overrides.mjs) is the override ids matching no
|
|
144
|
+
// task row. It rides on `status` rather than on `override list` alone
|
|
145
|
+
// because a dead override is invisible by construction — composeScope
|
|
146
|
+
// missing a key is a no-op, so the file widens nothing and raises
|
|
147
|
+
// nothing, and `override list` is a command an operator only runs once
|
|
148
|
+
// they already suspect something. It reads only `tasks`, not core.yaml,
|
|
149
|
+
// so it is computed unconditionally: an override id is stale or it
|
|
150
|
+
// isn't, whether or not a core definition is loadable.
|
|
151
|
+
//
|
|
152
|
+
// `debt` (per declaring task) and `frictionCount` are counts, not
|
|
153
|
+
// listings: both tables are append-only with no resolved column, so
|
|
154
|
+
// there is no "open" subset to filter to and this can only ever report
|
|
155
|
+
// what has been declared. That is the honest reading and also the
|
|
156
|
+
// useful one — a debt note is consumed by whichever downstream task
|
|
157
|
+
// inherits it, and friction is reviewed in a batch by tweaker, so
|
|
158
|
+
// neither is a queue `status` could burn down. What was missing was
|
|
159
|
+
// only the existence signal: `debt list` needs a task id the operator
|
|
160
|
+
// has no way to guess, and `friction list` needs the operator to
|
|
161
|
+
// already suspect there is something to read.
|
|
106
162
|
export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
107
163
|
const counts = countTasksByStatus(db);
|
|
108
164
|
const ready = loadReadyTasks(db);
|
|
@@ -110,8 +166,22 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
|
110
166
|
const inFlight = loadInFlightTasks(db);
|
|
111
167
|
const attention = loadAttentionTasks(db);
|
|
112
168
|
const drift = core ? detectDrift(db, core, { overrides }) : [];
|
|
169
|
+
const orphaned = orphanedOverrides(db, overrides);
|
|
170
|
+
const debt = loadDebtByTask(db);
|
|
171
|
+
const frictionCount = countFriction(db);
|
|
113
172
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
114
|
-
return {
|
|
173
|
+
return {
|
|
174
|
+
counts,
|
|
175
|
+
ready,
|
|
176
|
+
heldBack,
|
|
177
|
+
inFlight,
|
|
178
|
+
attention,
|
|
179
|
+
drift,
|
|
180
|
+
orphanedOverrides: orphaned,
|
|
181
|
+
debt,
|
|
182
|
+
frictionCount,
|
|
183
|
+
total,
|
|
184
|
+
};
|
|
115
185
|
}
|
|
116
186
|
|
|
117
187
|
const BLOCKED_REASON_LABELS = {
|
|
@@ -123,7 +193,9 @@ const BLOCKED_REASON_LABELS = {
|
|
|
123
193
|
// Renders a graphStatus() result into a plain-text overview: counts by
|
|
124
194
|
// status (only non-zero ones, in lifecycle order), any declared binary
|
|
125
195
|
// this environment can't resolve, the ready list, tasks currently in
|
|
126
|
-
// flight, anything needing attention,
|
|
196
|
+
// flight, anything needing attention, core.yaml drift, overrides
|
|
197
|
+
// pointing at no task, and what has been recorded in the two
|
|
198
|
+
// append-only side channels (declared debt, logged friction).
|
|
127
199
|
//
|
|
128
200
|
// `missingRequirements` comes from the core definition rather than the
|
|
129
201
|
// database (src/db/requires.mjs#coreMissingRequirements), so the caller
|
|
@@ -138,6 +210,9 @@ export function formatStatus({
|
|
|
138
210
|
inFlight,
|
|
139
211
|
attention,
|
|
140
212
|
drift,
|
|
213
|
+
orphanedOverrides = [],
|
|
214
|
+
debt = [],
|
|
215
|
+
frictionCount = 0,
|
|
141
216
|
total,
|
|
142
217
|
missingRequirements,
|
|
143
218
|
}) {
|
|
@@ -191,13 +266,63 @@ export function formatStatus({
|
|
|
191
266
|
lines.push(' Fix the work, then: hedgehog retry <task-id> && hedgehog claim <task-id> --owner <owner>');
|
|
192
267
|
}
|
|
193
268
|
|
|
194
|
-
// Last, because
|
|
195
|
-
// of any one task, and because an operator reading top-down should
|
|
196
|
-
// reach
|
|
269
|
+
// Last two, because both are conditions of the graph as a whole rather
|
|
270
|
+
// than of any one task, and because an operator reading top-down should
|
|
271
|
+
// reach them after knowing what's ready and what's stuck.
|
|
197
272
|
if (drift && drift.length > 0) {
|
|
198
273
|
lines.push('');
|
|
199
274
|
lines.push(formatDrift(drift));
|
|
200
275
|
}
|
|
201
276
|
|
|
277
|
+
// Below drift because it's the rarer of the two and never blocks the
|
|
278
|
+
// build: an orphaned override is work the operator asked for that isn't
|
|
279
|
+
// happening, not work the graph is getting wrong. `hedgehog override
|
|
280
|
+
// list` carries the full explanation of how an id goes stale; here the
|
|
281
|
+
// consequence is the whole point, since this is the only place an
|
|
282
|
+
// operator who doesn't already suspect it will ever see it.
|
|
283
|
+
if (orphanedOverrides.length > 0) {
|
|
284
|
+
lines.push('');
|
|
285
|
+
lines.push('ORPHANED OVERRIDES');
|
|
286
|
+
for (const taskId of orphanedOverrides) {
|
|
287
|
+
lines.push(` ${taskId} no task with this id in the build graph`);
|
|
288
|
+
}
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push(' Each widens nothing until the id matches a real task. See: hedgehog override list');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Bottom, below every condition above, and two sections rather than
|
|
294
|
+
// one. Everything above is something wrong — work stalled, a graph
|
|
295
|
+
// that disagrees with its core, an override doing nothing. These two
|
|
296
|
+
// are neither wrong nor actionable here: both tables are append-only,
|
|
297
|
+
// so nothing printed can be cleared by acting on it, and an operator
|
|
298
|
+
// scanning top-down should hit what needs a decision first.
|
|
299
|
+
//
|
|
300
|
+
// Kept apart because they are read by different people at different
|
|
301
|
+
// times through different commands. Debt is in-build traffic between
|
|
302
|
+
// two tasks — it arrives on its own in the inheriting task's packet,
|
|
303
|
+
// and naming the declaring tasks here is only so `debt list <task>`
|
|
304
|
+
// is reachable without guessing the id. Friction is about Hedgehog
|
|
305
|
+
// itself, batch-reviewed by tweaker at the end of a build, and has no
|
|
306
|
+
// per-task reading at all (a row's task_id is nullable). Merging them
|
|
307
|
+
// into one "notes" count would imply a single follow-up action where
|
|
308
|
+
// there are two unrelated ones.
|
|
309
|
+
if (debt.length > 0) {
|
|
310
|
+
const rows = debt.reduce((sum, { n }) => sum + n, 0);
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push(`DECLARED DEBT ${rows}`);
|
|
313
|
+
for (const { taskId, n } of debt) {
|
|
314
|
+
lines.push(` ${taskId} ${n} note${n === 1 ? '' : 's'}`);
|
|
315
|
+
}
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push(' Reaches each dependent task\'s packet on its own. See: hedgehog debt list <task-id>');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (frictionCount > 0) {
|
|
321
|
+
lines.push('');
|
|
322
|
+
lines.push(`FRICTION LOGGED ${frictionCount}`);
|
|
323
|
+
lines.push('');
|
|
324
|
+
lines.push(' Reviewed as a batch at the end of a build. See: hedgehog friction list');
|
|
325
|
+
}
|
|
326
|
+
|
|
202
327
|
return lines.join('\n');
|
|
203
328
|
}
|
|
@@ -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 = [];
|
|
@@ -3,11 +3,20 @@ import { Logger } from 'nestjs-pino';
|
|
|
3
3
|
import { NestFactory } from '@nestjs/core';
|
|
4
4
|
import { AppModule } from './app/app.module';
|
|
5
5
|
|
|
6
|
-
loadEnv();
|
|
6
|
+
const env = loadEnv();
|
|
7
7
|
|
|
8
8
|
async function bootstrap() {
|
|
9
9
|
const app = await NestFactory.create(AppModule, { bufferLogs: true });
|
|
10
10
|
app.useLogger(app.get(Logger));
|
|
11
|
+
// apps/web runs on its own origin in local dev (see the port comment
|
|
12
|
+
// below), so the browser's fetches to this api are cross-origin by
|
|
13
|
+
// default — enable CORS for that origin rather than rediscovering this
|
|
14
|
+
// per project.
|
|
15
|
+
app.enableCors({ origin: env.WEB_ORIGIN });
|
|
16
|
+
// Every route lives under /api. apps/web's NEXT_PUBLIC_API_BASE_URL
|
|
17
|
+
// (apps/web/.env.example) has to carry the same suffix or every request
|
|
18
|
+
// from the browser lands one path segment short of a route that exists —
|
|
19
|
+
// change one and change the other.
|
|
11
20
|
const globalPrefix = 'api';
|
|
12
21
|
app.setGlobalPrefix(globalPrefix);
|
|
13
22
|
// Defaults to 3333, not 3000 — `apps/web`'s `next dev` also defaults to
|
|
@@ -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,12 @@
|
|
|
1
|
+
# Copy to apps/web/.env.local for local dev. Next.js loads env files from
|
|
2
|
+
# the app directory, not the workspace root — the root .env.example is
|
|
3
|
+
# apps/api's, and nothing in this file reaches the server.
|
|
4
|
+
#
|
|
5
|
+
# The /api suffix is apps/api's global route prefix (app.setGlobalPrefix in
|
|
6
|
+
# apps/api/src/main.ts). Without it every request lands one path segment
|
|
7
|
+
# short and 404s against a route that plainly exists. 3333 is apps/api's
|
|
8
|
+
# dev port; apps/web itself runs on 3000.
|
|
9
|
+
#
|
|
10
|
+
# NEXT_PUBLIC_ is required: this value is read in the browser, so Next has
|
|
11
|
+
# to inline it at build time.
|
|
12
|
+
NEXT_PUBLIC_API_BASE_URL=http://localhost:3333/api
|
|
@@ -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
|
+
}));
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
id: full-stack-app
|
|
20
20
|
layers:
|
|
21
21
|
- id: schema
|
|
22
|
-
scope: ["packages/db/src/schema/{module}/**"]
|
|
22
|
+
scope: ["packages/db/src/schema/{module}/**", "packages/db/src/schema/index.ts"]
|
|
23
23
|
verify: "pnpm nx test db -- src/schema/{module}/"
|
|
24
24
|
commit: "feat({module}): schema"
|
|
25
25
|
- id: contract
|
|
@@ -47,11 +47,19 @@ layers:
|
|
|
47
47
|
scope: ["packages/hooks/src/{module}/**"]
|
|
48
48
|
verify: "pnpm nx test hooks -- src/{module}/"
|
|
49
49
|
commit: "feat({module}): hooks"
|
|
50
|
+
# Web only, because apps/mobile only exists when the Mobile add-on is
|
|
51
|
+
# on. A layer scoped to a project that was never scaffolded fails its
|
|
52
|
+
# own verify on every module, so the Mobile clause is added by
|
|
53
|
+
# hedgehog-bootstrap's Expo step — which is also what creates the Nx
|
|
54
|
+
# project the extra verify target needs — rather than shipped here and
|
|
55
|
+
# broken by default.
|
|
50
56
|
- id: screen
|
|
51
57
|
depends_on: hook
|
|
52
|
-
scope: ["apps/web/src/app/{module}/**"
|
|
53
|
-
verify: "pnpm nx test web -- src/app/{module}/
|
|
58
|
+
scope: ["apps/web/src/app/{module}/**"]
|
|
59
|
+
verify: "pnpm nx test web -- src/app/{module}/"
|
|
54
60
|
commit: "feat({module}): screen-web"
|
|
61
|
+
# Workspace-wide by design: the only gate that can see cross-module
|
|
62
|
+
# breakage, since every layer above is parallel and module-scoped.
|
|
55
63
|
- id: join
|
|
56
64
|
depends_on: screen
|
|
57
65
|
scope: ["**"]
|
|
@@ -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",
|