@canonical/code-standards 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +8 -0
- package/README.md +297 -0
- package/data/code.ttl +437 -0
- package/data/css.ttl +265 -0
- package/data/icons.ttl +359 -0
- package/data/packaging.ttl +464 -0
- package/data/react.ttl +752 -0
- package/data/rust.ttl +1806 -0
- package/data/storybook.ttl +403 -0
- package/data/styling.ttl +165 -0
- package/data/tsdoc.ttl +216 -0
- package/data/turtle.ttl +179 -0
- package/definitions/CodeStandard.ttl +80 -0
- package/docs/code.md +720 -0
- package/docs/css.md +275 -0
- package/docs/icons.md +367 -0
- package/docs/index.md +15 -0
- package/docs/react.md +766 -0
- package/docs/rust.md +1784 -0
- package/docs/storybook.md +413 -0
- package/docs/styling.md +163 -0
- package/docs/tsdoc.md +213 -0
- package/docs/turtle.md +179 -0
- package/package.json +9 -0
- package/skills/add-standard/SKILL.md +288 -0
- package/src/scripts/generate-docs.ts +131 -0
- package/src/scripts/index.ts +19 -0
package/docs/code.md
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
# Code Standards
|
|
2
|
+
|
|
3
|
+
Standards for code development.
|
|
4
|
+
|
|
5
|
+
## code/api/stability
|
|
6
|
+
|
|
7
|
+
Experimental APIs must be marked with the @experimental JSDoc tag in type definition files. The tag must include a description of what is experimental.
|
|
8
|
+
|
|
9
|
+
### Do
|
|
10
|
+
|
|
11
|
+
(Do) Mark an experimental interface with a clear @experimental JSDoc tag and description.
|
|
12
|
+
```typescript
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for the data processing pipeline.
|
|
15
|
+
* @experimental The streaming API is experimental and may change
|
|
16
|
+
* in future releases. Currently, it only supports JSON data.
|
|
17
|
+
*/
|
|
18
|
+
interface PipelineConfig {
|
|
19
|
+
// ...
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
(Do) Add an @experimental tag to a specific property with context about its experimental status.
|
|
24
|
+
```typescript
|
|
25
|
+
interface PipelineConfig {
|
|
26
|
+
/**
|
|
27
|
+
* @experimental The custom transformers API is in beta, and the interface
|
|
28
|
+
* may change to support stronger type validation.
|
|
29
|
+
*/
|
|
30
|
+
transformers?: DataTransformer[];
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
(Do) Describe the experimental status and any future plans for the API.
|
|
35
|
+
```typescript
|
|
36
|
+
interface CacheConfig {
|
|
37
|
+
/**
|
|
38
|
+
* @experimental The distributed cache API is experimental and will be
|
|
39
|
+
* replaced with a new consensus-based implementation in v2.1.
|
|
40
|
+
*/
|
|
41
|
+
distributed?: boolean;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Don't
|
|
46
|
+
|
|
47
|
+
(Don't) Use the @experimental tag without an explanation.
|
|
48
|
+
```typescript
|
|
49
|
+
interface ProcessorConfig {
|
|
50
|
+
/** @experimental */ // Bad: No context provided.
|
|
51
|
+
streaming?: boolean;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
(Don't) Use the @experimental tag without describing what is experimental.
|
|
56
|
+
```typescript
|
|
57
|
+
/**
|
|
58
|
+
* @experimental // Bad: No description of what is experimental.
|
|
59
|
+
*/
|
|
60
|
+
interface QueueConfig {
|
|
61
|
+
processor: (item: any) => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## code/constants/file
|
|
68
|
+
|
|
69
|
+
Domain constants must be collected in a `constants.ts` file using named exports (no default export). The file must be colocated at the level of the domain that owns the constants — inside the domain folder when one exists. Constants files always use named exports so consumers can import exactly what they need. Single-use constants that are only referenced in one file may be declared inline in that file instead of being extracted to `constants.ts`.
|
|
70
|
+
|
|
71
|
+
### Do
|
|
72
|
+
|
|
73
|
+
(Do) Create a `constants.ts` file with named exports at the domain level:
|
|
74
|
+
```typescript
|
|
75
|
+
// features/pricing/constants.ts
|
|
76
|
+
export const DEFAULT_CURRENCY = "USD";
|
|
77
|
+
export const TAX_RATE = 0.21;
|
|
78
|
+
export const MAX_DISCOUNT_PERCENT = 50;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
(Do) Colocate `constants.ts` inside the domain folder it belongs to:
|
|
82
|
+
```
|
|
83
|
+
features/
|
|
84
|
+
├── pricing/
|
|
85
|
+
│ ├── constants.ts # Domain constants live here
|
|
86
|
+
│ ├── calculatePrice.ts
|
|
87
|
+
│ └── index.ts
|
|
88
|
+
├── auth/
|
|
89
|
+
│ ├── constants.ts # Auth-specific constants
|
|
90
|
+
│ ├── validateToken.ts
|
|
91
|
+
│ └── index.ts
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
(Do) Import constants directly from the owning domain's `constants.ts`:
|
|
95
|
+
```typescript
|
|
96
|
+
// features/pricing/calculatePrice.ts
|
|
97
|
+
import { TAX_RATE, DEFAULT_CURRENCY } from "./constants.js";
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
(Do) Keep all exports in `constants.ts` as named exports with the same shape (plain values):
|
|
101
|
+
```typescript
|
|
102
|
+
// constants.ts
|
|
103
|
+
export const RECONNECT_INTERVAL_MS = 5000;
|
|
104
|
+
export const MAX_RETRIES = 3;
|
|
105
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
(Do) Keep single-use constants inline in the file that uses them:
|
|
109
|
+
```typescript
|
|
110
|
+
// features/pricing/applyDiscount.ts
|
|
111
|
+
const MAX_DISCOUNT_PERCENT = 50;
|
|
112
|
+
|
|
113
|
+
export default function applyDiscount(price: number, rate: number): number {
|
|
114
|
+
const capped = Math.min(rate, MAX_DISCOUNT_PERCENT / 100);
|
|
115
|
+
return price * (1 - capped);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Don't
|
|
120
|
+
|
|
121
|
+
(Don't) Use a default export in a constants file:
|
|
122
|
+
```typescript
|
|
123
|
+
// Bad: default export forces consumers to name the import
|
|
124
|
+
export default {
|
|
125
|
+
DEFAULT_CURRENCY: "USD",
|
|
126
|
+
TAX_RATE: 0.21,
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
(Don't) Scatter constants across unrelated files instead of collecting them in `constants.ts`:
|
|
131
|
+
```typescript
|
|
132
|
+
// Bad: constants buried inside implementation files
|
|
133
|
+
// calculatePrice.ts
|
|
134
|
+
export const TAX_RATE = 0.21;
|
|
135
|
+
const calculatePrice = (base: number) => base * (1 + TAX_RATE);
|
|
136
|
+
export default calculatePrice;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
(Don't) Place constants far from the domain that owns them:
|
|
140
|
+
```typescript
|
|
141
|
+
// Bad: pricing constants in a top-level shared file
|
|
142
|
+
// src/constants.ts
|
|
143
|
+
export const TAX_RATE = 0.21; // Belongs in pricing/
|
|
144
|
+
export const MAX_RETRIES = 3; // Belongs in network/
|
|
145
|
+
export const SESSION_TTL_MS = 3600; // Belongs in auth/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
(Don't) Mix constants with functions, types, or other non-constant exports:
|
|
149
|
+
```typescript
|
|
150
|
+
// Bad: constants.ts should only contain constant values
|
|
151
|
+
export const MAX_RETRIES = 3;
|
|
152
|
+
export const formatRetryMessage = (n: number) => `Retry ${n} of ${MAX_RETRIES}`;
|
|
153
|
+
export type RetryConfig = { max: number };
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## code/export/barrel-public-api
|
|
159
|
+
|
|
160
|
+
Barrel files must exist only to define a public API surface. Internal implementation code must import concrete modules directly rather than routing through local barrels. Public barrels may exist at package entry points or at explicitly public subdomain boundaries.
|
|
161
|
+
|
|
162
|
+
### Do
|
|
163
|
+
|
|
164
|
+
(Do) Use a barrel for a package entry point or explicit public API:
|
|
165
|
+
```typescript
|
|
166
|
+
// src/index.ts
|
|
167
|
+
export { Button } from "./components/Button.js";
|
|
168
|
+
export type { ButtonProps } from "./components/Button.types.js";
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
(Do) Import internal implementation code from the owning file:
|
|
172
|
+
```typescript
|
|
173
|
+
// src/build/buildTheme.ts
|
|
174
|
+
import { computeDeltas } from "./computeDeltas.js";
|
|
175
|
+
import { recoverPrimitiveRef } from "./recoverPrimitiveRef.js";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
(Do) Keep compatibility barrels thin and documented as public facades:
|
|
179
|
+
```typescript
|
|
180
|
+
/** Public compatibility surface. */
|
|
181
|
+
export { computeDeltas } from "./computeDeltas.js";
|
|
182
|
+
export { formatDelta } from "./formatDelta.js";
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Don't
|
|
186
|
+
|
|
187
|
+
(Don't) Route internal code through a sibling barrel just because it exists:
|
|
188
|
+
```typescript
|
|
189
|
+
// Bad: internal implementation depends on a local barrel
|
|
190
|
+
import { computeDeltas, recoverPrimitiveRef } from "./index.js";
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
(Don't) Hide domain ownership behind convenience barrels:
|
|
194
|
+
```typescript
|
|
195
|
+
// Bad: types appear to belong to context.ts instead of their owning domains
|
|
196
|
+
import type { Artifact, CSSNode, OverlayToken } from "./context.js";
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
(Don't) Create folder barrels for implementation-only directories with no public API contract:
|
|
200
|
+
```typescript
|
|
201
|
+
// build/helpers/index.ts
|
|
202
|
+
export * from "./parse.js";
|
|
203
|
+
export * from "./format.js";
|
|
204
|
+
export * from "./walk.js";
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## code/export/shape
|
|
210
|
+
|
|
211
|
+
Files must use either a single default export or multiple named exports. When using multiple named exports, all exports must have the same type or shape.
|
|
212
|
+
|
|
213
|
+
### Do
|
|
214
|
+
|
|
215
|
+
(Do) Use a single default export for files implementing a single component or function:
|
|
216
|
+
```typescript
|
|
217
|
+
// ComponentName.tsx
|
|
218
|
+
export default ComponentName;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
(Do) Name atomic function files after the function they export:
|
|
222
|
+
```typescript
|
|
223
|
+
// assignElement.ts
|
|
224
|
+
export default function assignElement(target: Element, source: Partial<Element>) {
|
|
225
|
+
// ...
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// formatCurrency.ts
|
|
229
|
+
export default function formatCurrency(value: number) {
|
|
230
|
+
// ...
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
(Do) Use multiple named exports for files providing a public API or a collection of related items, and ensure all exports have the same type:
|
|
235
|
+
```typescript
|
|
236
|
+
// index.ts
|
|
237
|
+
export { ComponentA, ComponentB };
|
|
238
|
+
|
|
239
|
+
// types.ts
|
|
240
|
+
export type TypeA = { ... };
|
|
241
|
+
export type TypeB = { ... };
|
|
242
|
+
|
|
243
|
+
// Consistent export shape:
|
|
244
|
+
export const myFuncA = (value: string) => {};
|
|
245
|
+
export const myFuncB = (value: string) => {};
|
|
246
|
+
export const myFuncC = (value: string) => {};
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Don't
|
|
250
|
+
|
|
251
|
+
(Don't) Mix default and unrelated named exports in a way that confuses the file's purpose:
|
|
252
|
+
```typescript
|
|
253
|
+
export default ComponentName;
|
|
254
|
+
export const helper = () => {};
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
(Don't) Name an atomic function file with a name that doesn't match its exported function:
|
|
258
|
+
```typescript
|
|
259
|
+
// helpers.ts — wrong: generic name instead of function name
|
|
260
|
+
export default function assignElement(target: Element, source: Partial<Element>) {
|
|
261
|
+
// ...
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// utils.ts — wrong: generic name instead of function name
|
|
265
|
+
export default function formatCurrency(value: number) {
|
|
266
|
+
// ...
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
(Don't) Provide multiple unrelated exports from a file meant for a single domain:
|
|
271
|
+
```typescript
|
|
272
|
+
export default debounce;
|
|
273
|
+
export const throttle = () => {};
|
|
274
|
+
export const logger = () => {};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
(Don't) Export objects of different types or shapes from the same file:
|
|
278
|
+
```typescript
|
|
279
|
+
export const transformer = (value: string) => {};
|
|
280
|
+
export const reducer = (map: string[]) => {};
|
|
281
|
+
class ABC {}
|
|
282
|
+
export { ABC };
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## code/function/composition
|
|
288
|
+
|
|
289
|
+
Each function must handle exactly one specific task. Complex operations must be broken down into smaller, single-purpose functions.
|
|
290
|
+
|
|
291
|
+
### Do
|
|
292
|
+
|
|
293
|
+
(Do) Define functions with a single, clear responsibility.
|
|
294
|
+
```typescript
|
|
295
|
+
const validateEmail = (email: string): boolean => {
|
|
296
|
+
return /^[^@]+@[^@]+\\.[^@]+$/.test(email);
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
(Do) Compose single-purpose functions to build complex logic.
|
|
301
|
+
```typescript
|
|
302
|
+
const validatePassword = (password: string): boolean => {
|
|
303
|
+
return password.length >= 8;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const validateForm = (email: string, password:string): boolean => {
|
|
307
|
+
return validateEmail(email) && validatePassword(password);
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Don't
|
|
312
|
+
|
|
313
|
+
(Don't) Mix multiple responsibilities in a single function.
|
|
314
|
+
```typescript
|
|
315
|
+
const processUserData = (user: any) => {
|
|
316
|
+
// Validates user data
|
|
317
|
+
// Transforms the data
|
|
318
|
+
// Saves to the database
|
|
319
|
+
// Sends a notification
|
|
320
|
+
};
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## code/function/location
|
|
326
|
+
|
|
327
|
+
Functions must be defined as close to their broadest point of use as possible. Shared utilities must be placed in a common location, and each function must serve a single feature or responsibility.
|
|
328
|
+
|
|
329
|
+
### Do
|
|
330
|
+
|
|
331
|
+
(Do) Place functions in the file or module where they are most broadly used. If a function is shared across multiple components or modules, place it in a common utility location.
|
|
332
|
+
```typescript
|
|
333
|
+
// utils/math.ts
|
|
334
|
+
export function calculateSum(a: number, b: number): number {
|
|
335
|
+
return a + b;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Used in multiple places
|
|
339
|
+
import { calculateSum } from './utils/math.js';
|
|
340
|
+
|
|
341
|
+
// For single-use functions, keep them close to their usage:
|
|
342
|
+
```typescript
|
|
343
|
+
// services/UserService.ts
|
|
344
|
+
const formatUserName = (user: User) => `${user.firstName} ${user.lastName}`;
|
|
345
|
+
|
|
346
|
+
const getProfileData = (user: User) => {
|
|
347
|
+
const formattedName = formatUserName(user);
|
|
348
|
+
return {
|
|
349
|
+
...user,
|
|
350
|
+
formattedName,
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
### Don't
|
|
355
|
+
|
|
356
|
+
(Don't) Place functions far from where they are used, or in a generic location if only used in one place.
|
|
357
|
+
```typescript
|
|
358
|
+
// utils/validators.ts
|
|
359
|
+
export const validateUser = (user: User) => { ... }; // Only used in one component
|
|
360
|
+
|
|
361
|
+
// services/UserService.ts
|
|
362
|
+
import { validateUser } from '../utils/validators.js'; // Less ideal if only used here
|
|
363
|
+
|
|
364
|
+
// Don't mix unrelated functions in a single file:
|
|
365
|
+
```typescript
|
|
366
|
+
export const mathFunc = () => {};
|
|
367
|
+
export const stringFunc = () => {};
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## code/function/purity
|
|
372
|
+
|
|
373
|
+
Functions should be pure where possible. A pure function returns the same output for the same input and does not modify external state or perform I/O operations. If a function must perform side effects (e.g., I/O in CLI tools), it must be explicitly annotated and documented with the reason for impurity using the @note tag in TSDoc/JSDoc.
|
|
374
|
+
|
|
375
|
+
### Do
|
|
376
|
+
|
|
377
|
+
(Do) Create pure functions that rely only on their inputs to compute the output.
|
|
378
|
+
```typescript
|
|
379
|
+
const calculatePrice = (basePrice: number, taxRate: number): number => {
|
|
380
|
+
return basePrice * (1 + taxRate);
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
(Do) Annotate and document impure functions that perform I/O or modify external state, explaining why impurity is necessary using @note.
|
|
385
|
+
```typescript
|
|
386
|
+
/**
|
|
387
|
+
* Writes data to the file system.
|
|
388
|
+
* @note - This function is impure - it modifies the file system.
|
|
389
|
+
*/
|
|
390
|
+
function writeOutputToFile(data: string, path: string) {
|
|
391
|
+
fs.writeFileSync(path, data);
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Don't
|
|
396
|
+
|
|
397
|
+
(Don't) Create impure functions without annotation or documentation.
|
|
398
|
+
```typescript
|
|
399
|
+
let total = 0;
|
|
400
|
+
const addToTotal = (value: number) => {
|
|
401
|
+
total += value; // Modifies external state, not documented
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
(Don't) Hide side effects in functions that appear pure.
|
|
406
|
+
```typescript
|
|
407
|
+
function getConfig() {
|
|
408
|
+
// Reads from disk without annotation
|
|
409
|
+
return fs.readFileSync('config.json');
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## code/naming/function-verb
|
|
416
|
+
|
|
417
|
+
All function and method names must start with a verb that describes the action the function performs. Names like `pathToX` or `dataForUser` describe a result, not an action — use `findPathToX` or `fetchDataForUser` instead. This makes the function's intent immediately clear and distinguishes functions from plain values or constants.
|
|
418
|
+
|
|
419
|
+
### Do
|
|
420
|
+
|
|
421
|
+
(Do) Start function names with an action verb:
|
|
422
|
+
```typescript
|
|
423
|
+
const findPathToModule = (name: string): string => { /* ... */ };
|
|
424
|
+
const fetchDataForUser = (userId: string): Promise<User> => { /* ... */ };
|
|
425
|
+
const parseConfigFile = (path: string): Config => { /* ... */ };
|
|
426
|
+
const calculateTotalPrice = (items: Item[]): number => { /* ... */ };
|
|
427
|
+
const isValidEmail = (email: string): boolean => { /* ... */ };
|
|
428
|
+
const hasPermission = (user: User, action: string): boolean => { /* ... */ };
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
(Do) Use descriptive verb prefixes that convey the operation:
|
|
432
|
+
```typescript
|
|
433
|
+
// Retrieval: get, fetch, find, resolve, load, read
|
|
434
|
+
const getUser = (id: string): User => { /* ... */ };
|
|
435
|
+
const fetchOrders = (): Promise<Order[]> => { /* ... */ };
|
|
436
|
+
const findMatchingRule = (rules: Rule[], input: string): Rule | undefined => { /* ... */ };
|
|
437
|
+
|
|
438
|
+
// Transformation: format, parse, convert, transform, map, normalize
|
|
439
|
+
const formatDate = (date: Date): string => { /* ... */ };
|
|
440
|
+
const parseResponse = (raw: string): Response => { /* ... */ };
|
|
441
|
+
|
|
442
|
+
// Validation: is, has, can, should, validate, check
|
|
443
|
+
const isActive = (user: User): boolean => { /* ... */ };
|
|
444
|
+
const hasExpired = (token: Token): boolean => { /* ... */ };
|
|
445
|
+
const validateInput = (data: unknown): data is FormData => { /* ... */ };
|
|
446
|
+
|
|
447
|
+
// Mutation: set, update, add, remove, delete, reset, clear
|
|
448
|
+
const setTheme = (theme: Theme): void => { /* ... */ };
|
|
449
|
+
const removeItem = (id: string): void => { /* ... */ };
|
|
450
|
+
|
|
451
|
+
// Creation: create, build, generate, compose, make
|
|
452
|
+
const createConnection = (config: Config): Connection => { /* ... */ };
|
|
453
|
+
const buildQuery = (params: Params): string => { /* ... */ };
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Don't
|
|
457
|
+
|
|
458
|
+
(Don't) Name functions as nouns or noun phrases — they read like values, not actions:
|
|
459
|
+
```typescript
|
|
460
|
+
// Bad: reads like a variable, not a function
|
|
461
|
+
const pathToModule = (name: string): string => { /* ... */ };
|
|
462
|
+
// Good: findPathToModule
|
|
463
|
+
|
|
464
|
+
const dataForUser = (userId: string): Promise<User> => { /* ... */ };
|
|
465
|
+
// Good: fetchDataForUser
|
|
466
|
+
|
|
467
|
+
const userPermissions = (user: User): Permission[] => { /* ... */ };
|
|
468
|
+
// Good: getPermissions or listPermissions
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
(Don't) Use vague or non-descriptive verbs that don't convey meaning:
|
|
472
|
+
```typescript
|
|
473
|
+
// Bad: "do" and "process" are too vague on their own
|
|
474
|
+
const doEmail = (email: string) => { /* ... */ };
|
|
475
|
+
// Good: sendEmail, validateEmail, formatEmail
|
|
476
|
+
|
|
477
|
+
const processUser = (user: User) => { /* ... */ };
|
|
478
|
+
// Good: activateUser, deactivateUser, updateUser
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## code/naming/single-export-file
|
|
484
|
+
|
|
485
|
+
Files that contain a single export must be named after that export. This applies to functions, classes, types, constants, and any other single-export module. The file name must match the exported identifier exactly, preserving its casing (camelCase for functions/variables, PascalCase for classes/types/components).
|
|
486
|
+
|
|
487
|
+
### Do
|
|
488
|
+
|
|
489
|
+
(Do) Name files after the single function they export:
|
|
490
|
+
```typescript
|
|
491
|
+
// isAccepted.ts
|
|
492
|
+
export const isAccepted = (status: string): boolean => {
|
|
493
|
+
return status === "accepted";
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// formatCurrency.ts
|
|
497
|
+
export default function formatCurrency(value: number): string {
|
|
498
|
+
return `$${value.toFixed(2)}`;
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
(Do) Name files after the single type or interface they export:
|
|
503
|
+
```typescript
|
|
504
|
+
// ConnectionConfig.ts
|
|
505
|
+
export interface ConnectionConfig {
|
|
506
|
+
host: string;
|
|
507
|
+
port: number;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// UserRole.ts
|
|
511
|
+
export type UserRole = "admin" | "editor" | "viewer";
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
(Do) Name files after the single class they export:
|
|
515
|
+
```typescript
|
|
516
|
+
// EventEmitter.ts
|
|
517
|
+
export class EventEmitter {
|
|
518
|
+
// ...
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
(Do) Name files after the single constant they export:
|
|
523
|
+
```typescript
|
|
524
|
+
// DEFAULT_TIMEOUT.ts
|
|
525
|
+
export const DEFAULT_TIMEOUT = 5000;
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Don't
|
|
529
|
+
|
|
530
|
+
(Don't) Use generic names like `helpers.ts`, `utils.ts`, or `types.ts` for files with a single export:
|
|
531
|
+
```typescript
|
|
532
|
+
// helpers.ts — wrong: should be isAccepted.ts
|
|
533
|
+
export const isAccepted = (status: string): boolean => {
|
|
534
|
+
return status === "accepted";
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// utils.ts — wrong: should be formatCurrency.ts
|
|
538
|
+
export default function formatCurrency(value: number): string {
|
|
539
|
+
return `$${value.toFixed(2)}`;
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
(Don't) Use names that describe the domain instead of the export:
|
|
544
|
+
```typescript
|
|
545
|
+
// validation.ts — wrong: should be isAccepted.ts
|
|
546
|
+
export const isAccepted = (status: string): boolean => {
|
|
547
|
+
return status === "accepted";
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// network.ts — wrong: should be ConnectionConfig.ts
|
|
551
|
+
export interface ConnectionConfig {
|
|
552
|
+
host: string;
|
|
553
|
+
port: number;
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## code/package/lib-folder
|
|
560
|
+
|
|
561
|
+
Packages that export reusable logic (components, utilities, hooks, types) must organize their exportable code in a `lib` folder within the `src` directory. This convention ensures consistency across the monorepo and clearly identifies code that is part of the package's public API.
|
|
562
|
+
|
|
563
|
+
### Do
|
|
564
|
+
|
|
565
|
+
(Do) Place all reusable/exportable code in `src/lib/`:
|
|
566
|
+
```
|
|
567
|
+
packages/my-package/
|
|
568
|
+
├── src/
|
|
569
|
+
│ ├── lib/
|
|
570
|
+
│ │ ├── Button/
|
|
571
|
+
│ │ │ ├── Button.tsx
|
|
572
|
+
│ │ │ ├── Button.tests.tsx
|
|
573
|
+
│ │ │ ├── types.ts
|
|
574
|
+
│ │ │ └── index.ts
|
|
575
|
+
│ │ ├── hooks/
|
|
576
|
+
│ │ │ └── useToggle.ts
|
|
577
|
+
│ │ ├── types/
|
|
578
|
+
│ │ │ └── index.ts
|
|
579
|
+
│ │ └── index.ts
|
|
580
|
+
│ ├── storybook/ # Storybook-specific files (not exported)
|
|
581
|
+
│ └── index.ts # Re-exports from lib
|
|
582
|
+
└── package.json
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
(Do) Re-export from the lib folder in the package entry point:
|
|
586
|
+
```typescript
|
|
587
|
+
// src/index.ts
|
|
588
|
+
export * from "./lib/index.js";
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
(Do) Use the lib folder for components, hooks, utilities, types, and any other code meant to be consumed by package users:
|
|
592
|
+
```typescript
|
|
593
|
+
// src/lib/index.ts
|
|
594
|
+
export * from "./Button/index.js";
|
|
595
|
+
export * from "./hooks/index.js";
|
|
596
|
+
export type * from "./types/index.js";
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Don't
|
|
600
|
+
|
|
601
|
+
(Don't) Use alternative folder names like `ui`, `components`, or `utils` for exportable code at the package level:
|
|
602
|
+
```
|
|
603
|
+
// Bad: Using 'ui' instead of 'lib'
|
|
604
|
+
packages/my-package/
|
|
605
|
+
├── src/
|
|
606
|
+
│ ├── ui/ # Wrong: should be 'lib'
|
|
607
|
+
│ │ └── Button/
|
|
608
|
+
│ └── index.ts
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
(Don't) Mix exportable and non-exportable code at the same level:
|
|
612
|
+
```
|
|
613
|
+
// Bad: Mixing concerns
|
|
614
|
+
packages/my-package/
|
|
615
|
+
├── src/
|
|
616
|
+
│ ├── Button/ # Component mixed with...
|
|
617
|
+
│ ├── storybook/ # ...non-exportable storybook config
|
|
618
|
+
│ └── test-utils/ # ...and test utilities
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
(Don't) Create deeply nested lib-like structures; keep lib at the top level of src:
|
|
622
|
+
```
|
|
623
|
+
// Bad: Nested lib folders
|
|
624
|
+
packages/my-package/
|
|
625
|
+
├── src/
|
|
626
|
+
│ ├── features/
|
|
627
|
+
│ │ └── lib/ # Wrong: lib should be at src level
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## code/types/file
|
|
633
|
+
|
|
634
|
+
Reusable domain types must be collected in a `types.ts` file using named exports (no default export), colocated at the level of the domain that owns them. Types that are only used in a single file may be declared inline in that file instead of being extracted to `types.ts`.
|
|
635
|
+
|
|
636
|
+
### Do
|
|
637
|
+
|
|
638
|
+
(Do) Create a `types.ts` file with named exports for types shared across the domain:
|
|
639
|
+
```typescript
|
|
640
|
+
// features/pricing/types.ts
|
|
641
|
+
export type Currency = "USD" | "EUR" | "GBP";
|
|
642
|
+
export interface PriceBreakdown {
|
|
643
|
+
base: number;
|
|
644
|
+
tax: number;
|
|
645
|
+
total: number;
|
|
646
|
+
}
|
|
647
|
+
export type DiscountStrategy = "percentage" | "fixed";
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
(Do) Colocate `types.ts` inside the domain folder it belongs to:
|
|
651
|
+
```
|
|
652
|
+
features/
|
|
653
|
+
├── pricing/
|
|
654
|
+
│ ├── types.ts # Shared domain types live here
|
|
655
|
+
│ ├── constants.ts
|
|
656
|
+
│ ├── calculatePrice.ts
|
|
657
|
+
│ └── index.ts
|
|
658
|
+
├── auth/
|
|
659
|
+
│ ├── types.ts # Auth-specific types
|
|
660
|
+
│ ├── validateToken.ts
|
|
661
|
+
│ └── index.ts
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
(Do) Keep single-use types inline in the file that uses them:
|
|
665
|
+
```typescript
|
|
666
|
+
// features/pricing/applyDiscount.ts
|
|
667
|
+
type DiscountResult = { discounted: number; savings: number };
|
|
668
|
+
|
|
669
|
+
export default function applyDiscount(price: number, rate: number): DiscountResult {
|
|
670
|
+
const savings = price * rate;
|
|
671
|
+
return { discounted: price - savings, savings };
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
(Do) Import shared types from the owning domain's `types.ts`:
|
|
676
|
+
```typescript
|
|
677
|
+
// features/pricing/calculatePrice.ts
|
|
678
|
+
import type { Currency, PriceBreakdown } from "./types.js";
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Don't
|
|
682
|
+
|
|
683
|
+
(Don't) Use a default export in a types file:
|
|
684
|
+
```typescript
|
|
685
|
+
// Bad: default export for a collection of types
|
|
686
|
+
export default interface PriceBreakdown {
|
|
687
|
+
base: number;
|
|
688
|
+
tax: number;
|
|
689
|
+
total: number;
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
(Don't) Extract single-use types to `types.ts` when they are only used in one file:
|
|
694
|
+
```typescript
|
|
695
|
+
// Bad: types.ts contains a type only used in applyDiscount.ts
|
|
696
|
+
// types.ts
|
|
697
|
+
export type DiscountResult = { discounted: number; savings: number };
|
|
698
|
+
|
|
699
|
+
// applyDiscount.ts
|
|
700
|
+
import type { DiscountResult } from "./types.js"; // Unnecessary indirection
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
(Don't) Place types far from the domain that owns them:
|
|
704
|
+
```typescript
|
|
705
|
+
// Bad: all types dumped in a top-level shared file
|
|
706
|
+
// src/types.ts
|
|
707
|
+
export type Currency = "USD" | "EUR" | "GBP"; // Belongs in pricing/
|
|
708
|
+
export type AuthToken = { jwt: string }; // Belongs in auth/
|
|
709
|
+
export type RetryPolicy = { max: number }; // Belongs in network/
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
(Don't) Mix types with functions, constants, or other non-type exports:
|
|
713
|
+
```typescript
|
|
714
|
+
// Bad: types.ts should only contain type declarations
|
|
715
|
+
export type Currency = "USD" | "EUR" | "GBP";
|
|
716
|
+
export const DEFAULT_CURRENCY: Currency = "USD";
|
|
717
|
+
export const formatCurrency = (value: number, currency: Currency) => `${currency} ${value}`;
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|