@coralai/sps-cli 0.41.2 → 0.43.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/README.md +34 -3
- package/dist/commands/cardAdd.d.ts +1 -1
- package/dist/commands/cardAdd.d.ts.map +1 -1
- package/dist/commands/cardAdd.js +16 -6
- package/dist/commands/cardAdd.js.map +1 -1
- package/dist/commands/cardDashboard.js +1 -1
- package/dist/commands/cardDashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -314
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hookCommand.d.ts.map +1 -1
- package/dist/commands/hookCommand.js +6 -7
- package/dist/commands/hookCommand.js.map +1 -1
- package/dist/commands/pmCommand.js +1 -1
- package/dist/commands/pmCommand.js.map +1 -1
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +60 -37
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -30
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/commands/tick.js +1 -1
- package/dist/commands/tick.js.map +1 -1
- package/dist/core/checklist.d.ts +22 -0
- package/dist/core/checklist.d.ts.map +1 -0
- package/dist/core/checklist.js +38 -0
- package/dist/core/checklist.js.map +1 -0
- package/dist/core/checklist.test.d.ts +2 -0
- package/dist/core/checklist.test.d.ts.map +1 -0
- package/dist/core/checklist.test.js +74 -0
- package/dist/core/checklist.test.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +7 -4
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/context.d.ts +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/engines/EventHandler.test.js +3 -3
- package/dist/engines/EventHandler.test.js.map +1 -1
- package/dist/engines/MonitorEngine.js +2 -2
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/engines/SchedulerEngine.js +1 -1
- package/dist/engines/SchedulerEngine.js.map +1 -1
- package/dist/engines/StageEngine.js +3 -3
- package/dist/engines/StageEngine.js.map +1 -1
- package/dist/engines/engine-pipeline-adapter.test.js +2 -2
- package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
- package/dist/interfaces/TaskBackend.d.ts +3 -1
- package/dist/interfaces/TaskBackend.d.ts.map +1 -1
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/dist/models/types.d.ts +16 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.js +28 -5
- package/dist/providers/MarkdownTaskBackend.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +5 -7
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
- package/project-template/.claude/hooks/start.sh +44 -0
- package/project-template/.claude/settings.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# TypeScript — Errors
|
|
2
|
+
|
|
3
|
+
Error classes, `Result` types, catch semantics. For general strategy (when to raise vs return, where to log), see `coding-standards/references/error-strategy.md`.
|
|
4
|
+
|
|
5
|
+
## Custom error classes
|
|
6
|
+
|
|
7
|
+
Subclass `Error`. Always set `name`. Use `cause` to chain.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export class AppError extends Error {
|
|
11
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = 'AppError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class NotFoundError extends AppError {
|
|
18
|
+
constructor(resource: string) {
|
|
19
|
+
super(`${resource} not found`);
|
|
20
|
+
this.name = 'NotFoundError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ValidationError extends AppError {
|
|
25
|
+
constructor(public issues: { path: string; message: string }[]) {
|
|
26
|
+
super('validation failed');
|
|
27
|
+
this.name = 'ValidationError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Why `name`: stack traces use it, and `instanceof` can be unreliable across realms / duplicated module loads — the name string survives.
|
|
33
|
+
|
|
34
|
+
## Chain with `cause`
|
|
35
|
+
|
|
36
|
+
Preserve the original error (available in every modern runtime).
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
try {
|
|
40
|
+
await db.query(...);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
throw new AppError('failed to load user', { cause: e });
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Stack and `.cause` are both available for debugging / logging.
|
|
47
|
+
|
|
48
|
+
## Catch `unknown`
|
|
49
|
+
|
|
50
|
+
`catch (e)` in modern TS types `e` as `unknown`. Narrow before use.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
try { ... } catch (e) {
|
|
54
|
+
if (e instanceof NotFoundError) return null;
|
|
55
|
+
if (e instanceof ValidationError) return { errors: e.issues };
|
|
56
|
+
throw e; // propagate the rest
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Never assume `e instanceof Error` without checking. Anything can be thrown — strings, numbers, `{}`, `null`.
|
|
61
|
+
|
|
62
|
+
## `Result` types — optional, not always
|
|
63
|
+
|
|
64
|
+
For operations where a failure is **expected**, a `Result` type makes the possibility visible in the signature and skips `try/catch`.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
type Result<T, E> =
|
|
68
|
+
| { ok: true; value: T }
|
|
69
|
+
| { ok: false; error: E };
|
|
70
|
+
|
|
71
|
+
async function findUser(id: string): Promise<Result<User, 'not_found' | 'db_down'>> {
|
|
72
|
+
try {
|
|
73
|
+
const u = await db.users.find(id);
|
|
74
|
+
if (!u) return { ok: false, error: 'not_found' };
|
|
75
|
+
return { ok: true, value: u };
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { ok: false, error: 'db_down' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const r = await findUser(id);
|
|
82
|
+
if (!r.ok) return handle(r.error);
|
|
83
|
+
use(r.value);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Libraries (`neverthrow`, `true-myth`) give you chainable `Result`s with more ergonomics.
|
|
87
|
+
|
|
88
|
+
**When NOT to use**: if every caller re-throws the error anyway, `Result` just adds noise. Use it when the choice is local and important.
|
|
89
|
+
|
|
90
|
+
## Mapping errors at the edge
|
|
91
|
+
|
|
92
|
+
Internal: throw freely. Edge (HTTP handler, CLI main, queue consumer): translate into response.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// The one translation layer
|
|
96
|
+
export function toHttpResponse(e: unknown): Response {
|
|
97
|
+
if (e instanceof ValidationError)
|
|
98
|
+
return json({ errors: e.issues }, { status: 422 });
|
|
99
|
+
if (e instanceof NotFoundError)
|
|
100
|
+
return json({ error: e.message }, { status: 404 });
|
|
101
|
+
if (e instanceof AuthError)
|
|
102
|
+
return json({ error: 'unauthorized' }, { status: 401 });
|
|
103
|
+
|
|
104
|
+
log.error('unexpected', { err: e });
|
|
105
|
+
return json({ error: 'internal error' }, { status: 500 });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
One place for the mapping. Handlers just throw.
|
|
110
|
+
|
|
111
|
+
## Never silently swallow
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// ❌ swallow
|
|
115
|
+
try { doThing(); } catch { /* oops, silently ignored */ }
|
|
116
|
+
|
|
117
|
+
// ✅ log and continue if that's really what you want
|
|
118
|
+
try { doThing(); }
|
|
119
|
+
catch (e) { log.warn('doThing failed', { err: e }); }
|
|
120
|
+
|
|
121
|
+
// ✅ best: the caller decides
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## `finally` for cleanup
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const conn = await pool.acquire();
|
|
128
|
+
try {
|
|
129
|
+
await work(conn);
|
|
130
|
+
} finally {
|
|
131
|
+
conn.release();
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Don't put business logic in `finally` — it runs even on error.
|
|
136
|
+
|
|
137
|
+
## Assertion helpers
|
|
138
|
+
|
|
139
|
+
Cheap way to narrow + fail fast.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export function invariant(c: unknown, msg: string): asserts c {
|
|
143
|
+
if (!c) throw new AppError(`invariant failed: ${msg}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
invariant(user.active, 'user must be active by this point');
|
|
147
|
+
user.email; // narrowed; no null check needed
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Keep messages short and specific. `"user must be active"` beats `"check failed"`.
|
|
151
|
+
|
|
152
|
+
## Error handling across async boundaries
|
|
153
|
+
|
|
154
|
+
An error in an async function becomes a rejected promise. Don't mix sync `throw` with async rejection in a way callers can't predict.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// ❌ sometimes throws synchronously, sometimes rejects
|
|
158
|
+
async function bad(x: unknown) {
|
|
159
|
+
if (!x) throw new Error('bad'); // rejection (we're in async)
|
|
160
|
+
return await f(x); // rejection
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function worse(x: unknown) {
|
|
164
|
+
if (!x) throw new Error('bad'); // sync throw
|
|
165
|
+
return f(x); // returns a rejected promise
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ✅ consistently one or the other
|
|
169
|
+
async function good(x: unknown) {
|
|
170
|
+
if (!x) throw new Error('bad');
|
|
171
|
+
return await f(x);
|
|
172
|
+
}
|
|
173
|
+
// callers can always `await good(x)` and catch both cases with try/catch
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Validation at boundaries
|
|
177
|
+
|
|
178
|
+
Validate at parse time, never deep inside.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { z } from 'zod';
|
|
182
|
+
|
|
183
|
+
const Body = z.object({ email: z.string().email(), age: z.number().int().min(0) });
|
|
184
|
+
|
|
185
|
+
export async function POST(req: Request) {
|
|
186
|
+
let input;
|
|
187
|
+
try {
|
|
188
|
+
input = Body.parse(await req.json());
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return json({ errors: (e as z.ZodError).issues }, { status: 422 });
|
|
191
|
+
}
|
|
192
|
+
// inside the function, `input` is strongly typed and trusted
|
|
193
|
+
await service.signup(input);
|
|
194
|
+
return json({ ok: true });
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Anti-patterns
|
|
199
|
+
|
|
200
|
+
| Anti-pattern | Fix |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `throw 'bad'` (string) | Always `throw new Error(...)` — strings lose stack |
|
|
203
|
+
| `catch (e: any)` | Use `catch (e)` (unknown) + narrow |
|
|
204
|
+
| `catch { return null }` | Log + handle specifically, or rethrow |
|
|
205
|
+
| Checking error with `.message.includes('...')` | Use instanceof + a typed error class |
|
|
206
|
+
| `Promise.reject('...')` with a string | Reject with an `Error` |
|
|
207
|
+
| Over-broad error class hierarchy with 40 subtypes | Usually 5–8 is enough |
|
|
208
|
+
| Error messages leaking PII / secrets | Redact before logging; never in client response |
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# TypeScript — Idioms
|
|
2
|
+
|
|
3
|
+
Destructuring, optional chaining, nullish coalescing, modules.
|
|
4
|
+
|
|
5
|
+
## `const` / `let` / `var`
|
|
6
|
+
|
|
7
|
+
`const` by default. `let` only when reassigning. Never `var` — it has function scope and hoisting surprises.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const MAX = 10;
|
|
11
|
+
let count = 0;
|
|
12
|
+
for (const x of xs) count += x;
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Destructuring
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Object
|
|
19
|
+
const { id, email, name = 'unknown' } = user;
|
|
20
|
+
|
|
21
|
+
// Array
|
|
22
|
+
const [first, second, ...rest] = xs;
|
|
23
|
+
|
|
24
|
+
// Rename + default
|
|
25
|
+
const { id: userId, role = 'user' } = req;
|
|
26
|
+
|
|
27
|
+
// Nested
|
|
28
|
+
const { user: { id, email } } = response;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Avoid deep destructuring across many levels — it becomes hard to read. Two levels max.
|
|
32
|
+
|
|
33
|
+
## Optional chaining (`?.`) and nullish coalescing (`??`)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const city = user?.address?.city; // undefined if any part is null/undef
|
|
37
|
+
const port = config.port ?? 3000; // only falls back on null/undef
|
|
38
|
+
const items = maybeResponse?.items ?? [];
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Don't confuse `??` with `||`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const port = config.port || 3000; // also falls back on 0, '', false
|
|
45
|
+
const port = config.port ?? 3000; // only on null/undef
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `||` when any falsy value should fall back; `??` when only "nothing" should.
|
|
49
|
+
|
|
50
|
+
## Spread & rest
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Spread — copy + override
|
|
54
|
+
const updated = { ...user, email: newEmail };
|
|
55
|
+
const concat = [...xs, ...ys];
|
|
56
|
+
|
|
57
|
+
// Rest in params
|
|
58
|
+
function log(first: string, ...rest: unknown[]) { ... }
|
|
59
|
+
|
|
60
|
+
// Rest in destructuring
|
|
61
|
+
const { password, ...publicUser } = user;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Rest destructuring is a cheap way to drop a field (`publicUser` above).
|
|
65
|
+
|
|
66
|
+
## Arrow vs. `function`
|
|
67
|
+
|
|
68
|
+
- Arrow for callbacks, one-liners, closures that need lexical `this`.
|
|
69
|
+
- `function` for top-level named functions that might be hoisted or recursive.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const double = (x: number) => x * 2;
|
|
73
|
+
|
|
74
|
+
function fibonacci(n: number): number {
|
|
75
|
+
return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Immutable updates
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// ❌ mutation
|
|
83
|
+
user.email = newEmail;
|
|
84
|
+
xs.push(item);
|
|
85
|
+
|
|
86
|
+
// ✅ new value
|
|
87
|
+
const updated = { ...user, email: newEmail };
|
|
88
|
+
const next = [...xs, item];
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Mutation is cheap but makes change hard to trace. In frontends (React, Vue) and any concurrent code, immutable updates are usually required, not optional.
|
|
92
|
+
|
|
93
|
+
Use `structuredClone(x)` (global, Node 17+, browsers) for deep clones. Avoid `JSON.parse(JSON.stringify(x))` — loses dates, maps, undefined.
|
|
94
|
+
|
|
95
|
+
## Modules — ESM, named exports
|
|
96
|
+
|
|
97
|
+
Use ESM (`import` / `export`) everywhere. No `require` in new code (TS emits `require` if the target is CommonJS; that's a toolchain choice, not a source style).
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// ✅ named exports — grep-friendly, IDEs auto-import
|
|
101
|
+
export function parse(raw: string): Config { ... }
|
|
102
|
+
export const DEFAULT_TIMEOUT = 5000;
|
|
103
|
+
|
|
104
|
+
// ❌ default export + anonymous
|
|
105
|
+
export default function (raw: string): Config { ... }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Default exports make renames invisible (`import Foo from './x'`) and break auto-import. Use named exports and let the import name match the export name.
|
|
109
|
+
|
|
110
|
+
Allow `default` only when the language / framework demands it (React lazy routes, JSX component files by convention — team choice).
|
|
111
|
+
|
|
112
|
+
## `for...of` over `.forEach`
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// ✅ — supports `await`, `break`, `continue`, `return`
|
|
116
|
+
for (const x of xs) {
|
|
117
|
+
await process(x);
|
|
118
|
+
if (done) break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ⚠️ .forEach ignores `return` and can't be awaited
|
|
122
|
+
xs.forEach(async x => await process(x)); // fires in parallel; next line doesn't wait
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`forEach` is fine for pure synchronous side effects on small arrays. For anything else, `for...of`.
|
|
126
|
+
|
|
127
|
+
## Map / Set over object / array when keys aren't known strings
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// ❌ object-as-map
|
|
131
|
+
const counts: Record<string, number> = {};
|
|
132
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
133
|
+
// keys stringified; `constructor`, `__proto__` are footguns
|
|
134
|
+
|
|
135
|
+
// ✅ Map
|
|
136
|
+
const counts = new Map<string, number>();
|
|
137
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Use `Map` when keys are dynamic, need iteration in insertion order, or aren't strings. Use `Set` for unique values.
|
|
141
|
+
|
|
142
|
+
## JSON at boundaries
|
|
143
|
+
|
|
144
|
+
Parse with a schema; never trust `JSON.parse(raw) as Config`.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// ❌
|
|
148
|
+
const config: Config = JSON.parse(raw); // wrong at runtime; TS can't stop it
|
|
149
|
+
|
|
150
|
+
// ✅
|
|
151
|
+
const config = ConfigSchema.parse(JSON.parse(raw)); // zod throws on bad data
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## String templates
|
|
155
|
+
|
|
156
|
+
Backticks for interpolation and multi-line.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const msg = `Hello ${name}, you have ${count} messages`;
|
|
160
|
+
|
|
161
|
+
const html = `
|
|
162
|
+
<div>
|
|
163
|
+
${items.map(i => `<li>${i.name}</li>`).join('')}
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For anything user-controlled rendered into HTML/SQL/shell, you need escaping, not templates. Templates are for formatting, not safety.
|
|
169
|
+
|
|
170
|
+
## Narrowing with `in`
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
function area(s: Circle | Square) {
|
|
174
|
+
if ('radius' in s) return Math.PI * s.radius ** 2;
|
|
175
|
+
return s.side ** 2;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`in` narrows to the variant that has the property. Works well with discriminated unions too, but `.kind` / `.type` discriminators are clearer.
|
|
180
|
+
|
|
181
|
+
## Short-circuit guards
|
|
182
|
+
|
|
183
|
+
Prefer guards at the top to deep nesting.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
function process(user: User | null) {
|
|
187
|
+
if (!user) return null;
|
|
188
|
+
if (!user.active) return null;
|
|
189
|
+
return doWork(user);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Assertion functions
|
|
194
|
+
|
|
195
|
+
Marks a path as unreachable without narrowing.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
function assert(c: unknown, msg: string): asserts c {
|
|
199
|
+
if (!c) throw new Error(msg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const x: string | null = maybe();
|
|
203
|
+
assert(x !== null, 'x must be set by now');
|
|
204
|
+
x.toLowerCase(); // narrowed to string
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## `bigint`, `Date`, and friends
|
|
208
|
+
|
|
209
|
+
- `Date` has sharp edges (month 0-indexed, mutation). Prefer a lib (`date-fns`, `luxon`, native `Temporal` when widely available).
|
|
210
|
+
- `bigint` for integers > 2^53. Mixing `bigint` and `number` is a type error — good.
|
|
211
|
+
- `Symbol` — useful for unique keys and well-known protocols. Rarely needed in application code.
|
|
212
|
+
|
|
213
|
+
## `void` vs `undefined` in return types
|
|
214
|
+
|
|
215
|
+
- `void` for "I don't care what you return" (callbacks whose return is ignored).
|
|
216
|
+
- `undefined` when the function explicitly returns nothing.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
function subscribe(cb: () => void) { ... }
|
|
220
|
+
// subscriber may return something; we ignore it.
|
|
221
|
+
|
|
222
|
+
function log(msg: string): undefined {
|
|
223
|
+
console.log(msg);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Trivia, but CR-worthy in library code.
|
|
229
|
+
|
|
230
|
+
## `this` — lexical or bound, never `function` inside callbacks
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// ❌ `this` is whatever called the callback
|
|
234
|
+
btn.addEventListener('click', function() { this.count++ });
|
|
235
|
+
|
|
236
|
+
// ✅ lexical `this` via arrow
|
|
237
|
+
btn.addEventListener('click', () => { this.count++ });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
In classes, use `.bind(this)` at construction time, or use arrow-method field syntax:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
class X {
|
|
244
|
+
handle = () => { /* `this` is X, always */ };
|
|
245
|
+
}
|
|
246
|
+
```
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# TypeScript — Testing
|
|
2
|
+
|
|
3
|
+
Vitest / Jest, mocking, fixtures. For TDD cycle and general philosophy, see `coding-standards/references/tdd.md`.
|
|
4
|
+
|
|
5
|
+
## Runner: Vitest > Jest (for new projects)
|
|
6
|
+
|
|
7
|
+
Vitest is faster, ESM-native, and shares config with Vite. Jest is still fine on existing code.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// vitest.config.ts
|
|
11
|
+
import { defineConfig } from 'vitest/config';
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
test: {
|
|
14
|
+
coverage: { provider: 'v8', reporter: ['text', 'html'], thresholds: { lines: 80 } },
|
|
15
|
+
globals: false, // import describe/it/expect explicitly
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## File layout
|
|
21
|
+
|
|
22
|
+
| Convention | Pattern |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Colocated | `src/user.ts` + `src/user.test.ts` |
|
|
25
|
+
| Separated | `src/user.ts` + `tests/user.test.ts` |
|
|
26
|
+
|
|
27
|
+
Colocated is easier to maintain; separated is easier to exclude from production bundles (if your bundler doesn't already tree-shake tests). Pick one and be consistent.
|
|
28
|
+
|
|
29
|
+
## Structure
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
33
|
+
import { UserService } from './user-service';
|
|
34
|
+
|
|
35
|
+
describe('UserService', () => {
|
|
36
|
+
let service: UserService;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
service = new UserService(new InMemoryUserRepo());
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('creates a user with a generated id', async () => {
|
|
43
|
+
const u = await service.create({ name: 'A', email: 'a@x.com' });
|
|
44
|
+
expect(u.id).toBeTypeOf('string');
|
|
45
|
+
expect(u.name).toBe('A');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects empty email', async () => {
|
|
49
|
+
await expect(service.create({ name: 'A', email: '' }))
|
|
50
|
+
.rejects.toThrow(ValidationError);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Test names describe behaviour, not implementation. `creates a user with a generated id`, not `test_1`.
|
|
56
|
+
|
|
57
|
+
## Assertions
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
expect(value).toBe(expected); // strict equality (===)
|
|
61
|
+
expect(value).toEqual(expected); // deep equality
|
|
62
|
+
expect(value).toStrictEqual(expected); // deep + prototype + undefined
|
|
63
|
+
|
|
64
|
+
expect(fn).toThrow(TypeError);
|
|
65
|
+
expect(fn).toThrow(/invalid email/);
|
|
66
|
+
await expect(promise).rejects.toThrow();
|
|
67
|
+
|
|
68
|
+
expect(array).toContain(item);
|
|
69
|
+
expect(obj).toMatchObject({ name: 'A' }); // partial match
|
|
70
|
+
|
|
71
|
+
expect(value).toSatisfy(v => v > 0 && v < 10);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`toBe` on objects compares references, almost always wrong. Use `toEqual`.
|
|
75
|
+
|
|
76
|
+
## Mocking
|
|
77
|
+
|
|
78
|
+
Prefer fakes (real implementations with in-memory backing) over mocks. Mocks drift from the thing they imitate.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// ✅ fake — behaves like a repo, just in memory
|
|
82
|
+
class InMemoryUserRepo implements UserRepository {
|
|
83
|
+
private users = new Map<string, User>();
|
|
84
|
+
async findById(id: string) { return this.users.get(id) ?? null; }
|
|
85
|
+
async save(u: User) { this.users.set(u.id, u); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ⚠️ mock — easy for one test, painful when the repo grows
|
|
89
|
+
const mockRepo = {
|
|
90
|
+
findById: vi.fn().mockResolvedValue(null),
|
|
91
|
+
save: vi.fn(),
|
|
92
|
+
} as unknown as UserRepository;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When you do mock:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { vi } from 'vitest';
|
|
99
|
+
|
|
100
|
+
const sendEmail = vi.fn();
|
|
101
|
+
vi.mock('./email', () => ({ sendEmail }));
|
|
102
|
+
|
|
103
|
+
it('sends welcome email', async () => {
|
|
104
|
+
await service.signup('a@x.com');
|
|
105
|
+
expect(sendEmail).toHaveBeenCalledWith({ to: 'a@x.com', template: 'welcome' });
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`vi.mock` hoists — the import is replaced everywhere the module is used.
|
|
110
|
+
|
|
111
|
+
## Fixtures — inject what the test needs
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
function makeUser(overrides: Partial<User> = {}): User {
|
|
115
|
+
return { id: 'u_1', email: 'a@x.com', active: true, ...overrides };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it('rejects inactive users', () => {
|
|
119
|
+
const u = makeUser({ active: false });
|
|
120
|
+
expect(() => assertActive(u)).toThrow();
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Factories beat hard-coded objects. Default in place, override per test.
|
|
125
|
+
|
|
126
|
+
## Parameterized tests
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
describe.each([
|
|
130
|
+
{ a: 1, b: 2, sum: 3 },
|
|
131
|
+
{ a: 0, b: 0, sum: 0 },
|
|
132
|
+
{ a: -1, b: 1, sum: 0 },
|
|
133
|
+
])('add($a, $b)', ({ a, b, sum }) => {
|
|
134
|
+
it(`returns ${sum}`, () => expect(add(a, b)).toBe(sum));
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `it.each` for simpler cases.
|
|
139
|
+
|
|
140
|
+
## Async tests
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
it('fetches', async () => {
|
|
144
|
+
const u = await findUser('u_1');
|
|
145
|
+
expect(u).not.toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Rejections
|
|
149
|
+
await expect(findUser('bad')).rejects.toThrow(NotFoundError);
|
|
150
|
+
|
|
151
|
+
// Don't forget `await`; a missing `await` on a rejecting promise is a silent false pass
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Fake timers for time-based code:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
vi.useFakeTimers();
|
|
158
|
+
const p = sleep(1000).then(() => 'done');
|
|
159
|
+
vi.advanceTimersByTime(1000);
|
|
160
|
+
await expect(p).resolves.toBe('done');
|
|
161
|
+
vi.useRealTimers();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Snapshot tests — use sparingly
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
expect(renderEmail(user)).toMatchSnapshot();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Good for large structural output. Bad as a lazy "assert-something" catch-all — a stale snapshot silently legitimizes bugs.
|
|
171
|
+
|
|
172
|
+
Review every snapshot change deliberately. If you're running `--update-snapshots` as a reflex, they've lost their value.
|
|
173
|
+
|
|
174
|
+
## Integration tests
|
|
175
|
+
|
|
176
|
+
Hit real dependencies (DB, Redis, HTTP) where feasible. Testcontainers makes this portable.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// tests/integration/user.test.ts
|
|
180
|
+
import { GenericContainer } from 'testcontainers';
|
|
181
|
+
|
|
182
|
+
let pg;
|
|
183
|
+
beforeAll(async () => {
|
|
184
|
+
pg = await new GenericContainer('postgres:16-alpine')
|
|
185
|
+
.withEnvironment({ POSTGRES_PASSWORD: 'test' })
|
|
186
|
+
.withExposedPorts(5432)
|
|
187
|
+
.start();
|
|
188
|
+
// set DB_URL from pg.getMappedPort(5432)
|
|
189
|
+
});
|
|
190
|
+
afterAll(() => pg.stop());
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Integration tests give confidence that unit tests alone can't. Keep them separate from unit tests (`tests/integration/**`) so CI can run them in a different stage.
|
|
194
|
+
|
|
195
|
+
## Coverage — a floor, not a goal
|
|
196
|
+
|
|
197
|
+
See `coding-standards/references/tdd.md` for coverage targets. Chasing 100% coverage with meaningless assertions hurts more than it helps.
|
|
198
|
+
|
|
199
|
+
Enforce in CI:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// vitest.config.ts
|
|
203
|
+
test: {
|
|
204
|
+
coverage: {
|
|
205
|
+
thresholds: {
|
|
206
|
+
lines: 80,
|
|
207
|
+
functions: 80,
|
|
208
|
+
branches: 70,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Anti-patterns
|
|
215
|
+
|
|
216
|
+
| Anti-pattern | Fix |
|
|
217
|
+
|---|---|
|
|
218
|
+
| Tests that sleep for real seconds | Fake timers |
|
|
219
|
+
| `it.only` committed to main | CI rule: reject `.only` in `test`/`it`/`describe` |
|
|
220
|
+
| Shared mutable state between tests | Reset in `beforeEach` |
|
|
221
|
+
| Testing by spying on console.log | Test observable behaviour, not debug output |
|
|
222
|
+
| `expect(x).toEqual(x)` | Tautology; no signal |
|
|
223
|
+
| Over-mocking: 10 mocks to test 20 lines | Refactor so the unit is easier to test |
|
|
224
|
+
| Network in unit tests | Use fakes; move to integration suite |
|
|
225
|
+
| Snapshot tests for volatile output (dates, uuids) | Redact or use deterministic fixtures |
|