@canonical/code-standards 0.1.0 → 0.1.1
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/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/workflows/ci.yml +40 -0
- package/biome.json +6 -0
- package/bun.lock +200 -0
- package/data/code.ttl +208 -167
- package/data/css.ttl +110 -91
- package/data/icons.ttl +186 -150
- package/data/packaging.ttl +428 -170
- package/data/react.ttl +306 -244
- package/data/rust.ttl +563 -467
- package/data/storybook.ttl +108 -90
- package/data/styling.ttl +40 -40
- package/data/tsdoc.ttl +111 -86
- package/data/turtle.ttl +89 -68
- package/definitions/CodeStandard.ttl +28 -20
- package/docs/code.md +37 -327
- package/docs/css.md +20 -19
- package/docs/icons.md +37 -41
- package/docs/index.md +2 -1
- package/docs/packaging.md +643 -0
- package/docs/react.md +54 -58
- package/docs/rust.md +92 -158
- package/docs/storybook.md +18 -20
- package/docs/styling.md +8 -8
- package/docs/tsdoc.md +16 -16
- package/docs/turtle.md +15 -15
- package/package.json +16 -2
- package/skills/add-standard/SKILL.md +83 -47
- package/src/scripts/generate-docs.ts +95 -13
- package/src/scripts/index.ts +4 -2
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# Packaging Standards
|
|
2
|
+
|
|
3
|
+
Standards for packaging development.
|
|
4
|
+
|
|
5
|
+
## packaging/application/structure
|
|
6
|
+
|
|
7
|
+
Application packages (CLIs, servers, MCP tools, build scripts, workers) execute behavior through entry points. They are not imported as dependencies by other packages. Their internal code must be organized around execution domains (commands, routes, tools, operations) rather than library conventions like `src/lib/` or barrel re-exports. Each domain folder must have its own barrel file (`index.ts`) that defines the domain's internal API — sibling domains import through the barrel, never from individual files.
|
|
8
|
+
|
|
9
|
+
### Do
|
|
10
|
+
|
|
11
|
+
Recognise an application package by its `package.json` signals — `bin` field, `private: true`, no `main`/`exports`
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"name": "@scope/my-cli",
|
|
15
|
+
"private": true,
|
|
16
|
+
"bin": { "my-cli": "./dist/index.js" }
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Organize code around execution domains. Each domain folder gets its own barrel (`index.ts`) that exposes the domain's internal API to sibling domains
|
|
21
|
+
```
|
|
22
|
+
packages/my-cli/
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── commands/ # CLI command handlers
|
|
25
|
+
│ │ ├── build.ts
|
|
26
|
+
│ │ ├── deploy.ts
|
|
27
|
+
│ │ └── index.ts # Barrel: exports all commands
|
|
28
|
+
│ ├── operations/ # Shared internal logic across commands
|
|
29
|
+
│ │ ├── resolveConfig.ts
|
|
30
|
+
│ │ ├── runValidation.ts
|
|
31
|
+
│ │ └── index.ts # Barrel: exports shared operations
|
|
32
|
+
│ ├── utils/ # Internal helpers
|
|
33
|
+
│ │ ├── formatOutput.ts
|
|
34
|
+
│ │ └── index.ts # Barrel: exports utilities
|
|
35
|
+
│ └── index.ts # Entry point (bootstraps CLI)
|
|
36
|
+
└── package.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Import from sibling domains through their barrel, not from individual files
|
|
40
|
+
```typescript
|
|
41
|
+
// src/commands/deploy.ts
|
|
42
|
+
import { resolveConfig, runValidation } from "../operations/index.js";
|
|
43
|
+
import { formatOutput } from "../utils/index.js";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use domain-appropriate folder names that reflect what the code does — `commands/`, `tools/`, `routes/`, `operations/`, `handlers/`
|
|
47
|
+
```
|
|
48
|
+
packages/my-mcp/
|
|
49
|
+
├── src/
|
|
50
|
+
│ ├── tools/ # MCP tool handlers
|
|
51
|
+
│ │ ├── query.ts
|
|
52
|
+
│ │ ├── mutate.ts
|
|
53
|
+
│ │ └── index.ts # Barrel: exports tool handlers
|
|
54
|
+
│ ├── operations/ # Shared internal logic
|
|
55
|
+
│ │ ├── executeQuery.ts
|
|
56
|
+
│ │ └── index.ts # Barrel: exports shared operations
|
|
57
|
+
│ └── index.ts # MCP server entry point
|
|
58
|
+
└── package.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For **hybrid** packages (primarily application but exporting a small reusable surface), isolate the exported surface in `src/lib/` following library rules while keeping the rest as application structure
|
|
62
|
+
```
|
|
63
|
+
packages/my-mcp/
|
|
64
|
+
├── src/
|
|
65
|
+
│ ├── lib/ # Only the reusable exported surface
|
|
66
|
+
│ │ ├── types.ts
|
|
67
|
+
│ │ └── index.ts # Barrel: public API for external consumers
|
|
68
|
+
│ ├── tools/ # MCP tool handlers (not exported)
|
|
69
|
+
│ │ ├── query.ts
|
|
70
|
+
│ │ ├── mutate.ts
|
|
71
|
+
│ │ └── index.ts # Barrel: exports tool handlers
|
|
72
|
+
│ ├── operations/ # Shared internal logic
|
|
73
|
+
│ │ ├── executeQuery.ts
|
|
74
|
+
│ │ └── index.ts # Barrel: exports shared operations
|
|
75
|
+
│ └── index.ts # MCP server entry point
|
|
76
|
+
└── package.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Don't
|
|
80
|
+
|
|
81
|
+
Apply library structure (`src/lib/`, barrel re-exports) to application packages
|
|
82
|
+
```
|
|
83
|
+
// Bad: CLI tool forced into library layout
|
|
84
|
+
packages/my-cli/
|
|
85
|
+
├── src/
|
|
86
|
+
│ ├── lib/ # Wrong: this is a CLI, not a library
|
|
87
|
+
│ │ ├── commands/
|
|
88
|
+
│ │ └── index.ts # Barrel re-export — nobody imports this
|
|
89
|
+
│ └── index.ts
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Create a `src/lib/` folder for internal shared operations. Use domain-appropriate names instead
|
|
93
|
+
```
|
|
94
|
+
// Bad: "lib" implies external consumption
|
|
95
|
+
packages/my-cli/
|
|
96
|
+
├── src/
|
|
97
|
+
│ ├── lib/
|
|
98
|
+
│ │ └── resolveConfig.ts # Only used internally
|
|
99
|
+
|
|
100
|
+
// Good: "operations" or "shared" makes intent clear
|
|
101
|
+
packages/my-cli/
|
|
102
|
+
├── src/
|
|
103
|
+
│ ├── operations/
|
|
104
|
+
│ │ └── resolveConfig.ts # Internal shared logic
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Import directly from files inside a sibling domain — always go through the domain barrel
|
|
108
|
+
```typescript
|
|
109
|
+
// Bad: reaching into a sibling domain's internals
|
|
110
|
+
import { resolveConfig } from "../operations/resolveConfig.js";
|
|
111
|
+
import { formatOutput } from "../utils/formatOutput.js";
|
|
112
|
+
|
|
113
|
+
// Good: import through the domain barrel
|
|
114
|
+
import { resolveConfig } from "../operations/index.js";
|
|
115
|
+
import { formatOutput } from "../utils/index.js";
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Omit barrels from domain folders
|
|
119
|
+
```
|
|
120
|
+
// Bad: no barrels — every consumer imports individual files
|
|
121
|
+
packages/my-cli/
|
|
122
|
+
├── src/
|
|
123
|
+
│ ├── commands/
|
|
124
|
+
│ │ ├── build.ts
|
|
125
|
+
│ │ └── deploy.ts # No index.ts — siblings must know file names
|
|
126
|
+
│ ├── operations/
|
|
127
|
+
│ │ ├── resolveConfig.ts
|
|
128
|
+
│ │ └── runValidation.ts # No index.ts — fragile cross-domain imports
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Default to library structure just because the package lives in a monorepo
|
|
132
|
+
```
|
|
133
|
+
// Bad: reflexively adding lib/ to a build script package
|
|
134
|
+
packages/build-tools/
|
|
135
|
+
├── src/
|
|
136
|
+
│ ├── lib/ # This is a build script, not a library
|
|
137
|
+
│ │ └── runBuild.ts
|
|
138
|
+
│ └── index.ts
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## packaging/export/barrel
|
|
144
|
+
|
|
145
|
+
Every domain folder must have a barrel file (`index.ts`) that defines its API surface. In library packages, barrels define the public API for external consumers. In application packages, barrels define the internal API between sibling domains. Within a domain, implementation files import from siblings directly; cross-domain imports always go through the barrel.
|
|
146
|
+
|
|
147
|
+
### Do
|
|
148
|
+
|
|
149
|
+
Use a barrel at the package entry point to define the public API (library packages)
|
|
150
|
+
```typescript
|
|
151
|
+
// src/index.ts
|
|
152
|
+
export { Button } from "./components/Button.js";
|
|
153
|
+
export type { ButtonProps } from "./components/Button.types.js";
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Use a barrel at each domain folder to define the domain's API (application packages)
|
|
157
|
+
```typescript
|
|
158
|
+
// src/operations/index.ts
|
|
159
|
+
export { resolveConfig } from "./resolveConfig.js";
|
|
160
|
+
export { runValidation } from "./runValidation.js";
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Import internal implementation code within a domain from the owning file directly
|
|
164
|
+
```typescript
|
|
165
|
+
// src/build/buildTheme.ts — same domain, import the file
|
|
166
|
+
import { computeDeltas } from "./computeDeltas.js";
|
|
167
|
+
import { recoverPrimitiveRef } from "./recoverPrimitiveRef.js";
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Import cross-domain code through the sibling domain's barrel
|
|
171
|
+
```typescript
|
|
172
|
+
// src/commands/deploy.ts — different domain, import through barrel
|
|
173
|
+
import { resolveConfig } from "../operations/index.js";
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Keep compatibility barrels thin and documented as public facades
|
|
177
|
+
```typescript
|
|
178
|
+
/** Public compatibility surface. */
|
|
179
|
+
export { computeDeltas } from "./computeDeltas.js";
|
|
180
|
+
export { formatDelta } from "./formatDelta.js";
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Don't
|
|
184
|
+
|
|
185
|
+
Route internal same-domain code through its own barrel
|
|
186
|
+
```typescript
|
|
187
|
+
// Bad: internal implementation depends on its own barrel
|
|
188
|
+
// src/build/applyTheme.ts
|
|
189
|
+
import { computeDeltas, recoverPrimitiveRef } from "./index.js";
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Import cross-domain code by reaching into individual files
|
|
193
|
+
```typescript
|
|
194
|
+
// Bad: bypassing the domain barrel
|
|
195
|
+
import { resolveConfig } from "../operations/resolveConfig.js";
|
|
196
|
+
|
|
197
|
+
// Good: through the barrel
|
|
198
|
+
import { resolveConfig } from "../operations/index.js";
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Hide domain ownership behind convenience barrels
|
|
202
|
+
```typescript
|
|
203
|
+
// Bad: types appear to belong to context.ts instead of their owning domains
|
|
204
|
+
import type { Artifact, CSSNode, OverlayToken } from "./context.js";
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Omit the barrel from a domain folder
|
|
208
|
+
```
|
|
209
|
+
// Bad: no index.ts — consumers must know internal file names
|
|
210
|
+
src/operations/
|
|
211
|
+
├── resolveConfig.ts
|
|
212
|
+
└── runValidation.ts
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## packaging/export/declaration
|
|
218
|
+
|
|
219
|
+
Exports must be declared on the definition itself (e.g. `export default function` or `export const`), not gathered at the end of the file. When a wrapper (HOC, decorator, middleware) is applied, declare the unwrapped binding with `const`, then export the wrapped result as default on a separate line.
|
|
220
|
+
|
|
221
|
+
### Do
|
|
222
|
+
|
|
223
|
+
Declare the export directly on the function or class definition
|
|
224
|
+
```typescript
|
|
225
|
+
// formatCurrency.ts
|
|
226
|
+
export default function formatCurrency(value: number): string {
|
|
227
|
+
return `$${value.toFixed(2)}`;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Declare the export directly on a named constant
|
|
232
|
+
```typescript
|
|
233
|
+
// MAX_RETRIES.ts
|
|
234
|
+
export const MAX_RETRIES = 3;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
When a wrapper is involved, declare the raw binding with `const`, then export the wrapped result as default
|
|
238
|
+
```typescript
|
|
239
|
+
// UserProfile.tsx
|
|
240
|
+
const UserProfile = ({ user }: UserProfileProps) => {
|
|
241
|
+
return <div>{user.name}</div>;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default memo(UserProfile);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Apply the same pattern for higher-order functions and middleware
|
|
248
|
+
```typescript
|
|
249
|
+
// connectDatabase.ts
|
|
250
|
+
const connectDatabase = (config: DbConfig): Connection => {
|
|
251
|
+
return new Connection(config);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export default withRetry(connectDatabase);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Don't
|
|
258
|
+
|
|
259
|
+
Define a symbol and then export it at the end of the file
|
|
260
|
+
```typescript
|
|
261
|
+
// Bad: export is separated from definition
|
|
262
|
+
function formatCurrency(value: number): string {
|
|
263
|
+
return `$${value.toFixed(2)}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export default formatCurrency;
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Gather multiple exports at the bottom of the file
|
|
270
|
+
```typescript
|
|
271
|
+
// Bad: exports collected at the end
|
|
272
|
+
const helperA = () => {};
|
|
273
|
+
const helperB = () => {};
|
|
274
|
+
|
|
275
|
+
export { helperA, helperB };
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Use `export default` on the raw binding when a wrapper is needed
|
|
279
|
+
```typescript
|
|
280
|
+
// Bad: exports the unwrapped version, then wraps elsewhere
|
|
281
|
+
export default function UserProfile({ user }: UserProfileProps) {
|
|
282
|
+
return <div>{user.name}</div>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// consumer.ts — has to wrap it themselves
|
|
286
|
+
import UserProfile from "./UserProfile.js";
|
|
287
|
+
const Memoized = memo(UserProfile);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## packaging/export/shape
|
|
293
|
+
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
### Do
|
|
297
|
+
|
|
298
|
+
Use a single default export for files implementing a single component or function
|
|
299
|
+
```typescript
|
|
300
|
+
// ComponentName.tsx
|
|
301
|
+
export default ComponentName;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Name atomic function files after the function they export
|
|
305
|
+
```typescript
|
|
306
|
+
// assignElement.ts
|
|
307
|
+
export default function assignElement(target: Element, source: Partial<Element>) {
|
|
308
|
+
// ...
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// formatCurrency.ts
|
|
312
|
+
export default function formatCurrency(value: number) {
|
|
313
|
+
// ...
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Use multiple named exports for files providing a public API or a collection of related items, and ensure all exports have the same type
|
|
318
|
+
```typescript
|
|
319
|
+
// index.ts
|
|
320
|
+
export { ComponentA, ComponentB };
|
|
321
|
+
|
|
322
|
+
// types.ts
|
|
323
|
+
export type TypeA = { ... };
|
|
324
|
+
export type TypeB = { ... };
|
|
325
|
+
|
|
326
|
+
// Consistent export shape:
|
|
327
|
+
export const myFuncA = (value: string) => {};
|
|
328
|
+
export const myFuncB = (value: string) => {};
|
|
329
|
+
export const myFuncC = (value: string) => {};
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Don't
|
|
333
|
+
|
|
334
|
+
Mix default and unrelated named exports in a way that confuses the file's purpose
|
|
335
|
+
```typescript
|
|
336
|
+
export default ComponentName;
|
|
337
|
+
export const helper = () => {};
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Name an atomic function file with a name that doesn't match its exported function
|
|
341
|
+
```typescript
|
|
342
|
+
// helpers.ts — wrong: generic name instead of function name
|
|
343
|
+
export default function assignElement(target: Element, source: Partial<Element>) {
|
|
344
|
+
// ...
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// utils.ts — wrong: generic name instead of function name
|
|
348
|
+
export default function formatCurrency(value: number) {
|
|
349
|
+
// ...
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Provide multiple unrelated exports from a file meant for a single domain
|
|
354
|
+
```typescript
|
|
355
|
+
export default debounce;
|
|
356
|
+
export const throttle = () => {};
|
|
357
|
+
export const logger = () => {};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Export objects of different types or shapes from the same file
|
|
361
|
+
```typescript
|
|
362
|
+
export const transformer = (value: string) => {};
|
|
363
|
+
export const reducer = (map: string[]) => {};
|
|
364
|
+
class ABC {}
|
|
365
|
+
export { ABC };
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## packaging/import/cross-domain
|
|
371
|
+
|
|
372
|
+
Intra-package cross-domain imports must use Node.js subpath imports (the `imports` field in `package.json`) with the `#` prefix convention. Each `#` alias points directly to the domain's barrel file — no wildcard splats, no path suffixes, no extension mapping. With `moduleResolution: "nodenext"` (or `"node16"`), bare specifiers like `error/index.js` are treated as package names, not path-relative imports. The `paths` compiler option is a manual alias table — brittle and verbose. Subpath imports are the official Node.js mechanism, work natively with Node, Bun, vitest, and tsc, and require no plugins. TypeScript resolves them when `resolvePackageJsonImports` is enabled (on by default with `moduleResolution` set to `node16`, `nodenext`, or `bundler`).
|
|
373
|
+
|
|
374
|
+
### Do
|
|
375
|
+
|
|
376
|
+
Map each `#` alias directly to the domain barrel in `package.json` — no wildcards, no path suffixes
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"imports": {
|
|
380
|
+
"#config": "./src/config/index.ts",
|
|
381
|
+
"#error": "./src/error/index.ts",
|
|
382
|
+
"#pipeline": "./src/pipeline/index.ts",
|
|
383
|
+
"#package-manager": "./src/package-manager/index.ts",
|
|
384
|
+
"#constants": "./src/constants.ts"
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Import cross-domain modules using the `#` alias — clean, no path suffixes
|
|
390
|
+
```typescript
|
|
391
|
+
// src/pipeline/runPipeline.ts
|
|
392
|
+
import { PragmaError } from "#error";
|
|
393
|
+
import { resolveConfig } from "#config";
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Use `moduleResolution:
|
|
397
|
+
```json
|
|
398
|
+
{
|
|
399
|
+
"compilerOptions": {
|
|
400
|
+
"moduleResolution": "nodenext"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Use conditional subpath imports when you need different resolutions for different environments
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"imports": {
|
|
409
|
+
"#db": {
|
|
410
|
+
"development": "./src/db/mock.ts",
|
|
411
|
+
"default": "./src/db/real.ts"
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Don't
|
|
418
|
+
|
|
419
|
+
Use wildcard splats or path suffixes in `#` aliases — point directly to the barrel instead
|
|
420
|
+
```json
|
|
421
|
+
// Bad: wildcard mapping forces consumers to know internal file structure
|
|
422
|
+
{
|
|
423
|
+
"imports": {
|
|
424
|
+
"#config/*": "./src/config/*",
|
|
425
|
+
"#error/*": "./src/error/*"
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Bad: path suffix leaks barrel structure into every import site
|
|
430
|
+
import { PragmaError } from "#error/index.js";
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Use bare specifiers without the `#` prefix for intra-package imports — they resolve as package names under `nodenext`
|
|
434
|
+
```typescript
|
|
435
|
+
// Bad: "error/index.js" resolves as the npm package "error", not ./src/error/
|
|
436
|
+
import { PragmaError } from "error/index.js";
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Use `paths` in `tsconfig.json` as a substitute for subpath imports — it's a manual alias table that doesn't participate in Node.js resolution
|
|
440
|
+
```json
|
|
441
|
+
// Bad: brittle, verbose, requires a bundler or ts-patch to work at runtime
|
|
442
|
+
{
|
|
443
|
+
"compilerOptions": {
|
|
444
|
+
"baseUrl": ".",
|
|
445
|
+
"paths": {
|
|
446
|
+
"@error/*": ["src/error/*"],
|
|
447
|
+
"@config/*": ["src/config/*"],
|
|
448
|
+
"@pipeline/*": ["src/pipeline/*"]
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Use deep relative paths to reach across domains — they break when folders move and obscure the dependency graph
|
|
455
|
+
```typescript
|
|
456
|
+
// Bad: fragile, hard to refactor
|
|
457
|
+
import { PragmaError } from "../../error/PragmaError.js";
|
|
458
|
+
import { resolveConfig } from "../../../config/resolveConfig.js";
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Import specific files through the `#` alias — go through the barrel instead
|
|
462
|
+
```typescript
|
|
463
|
+
// Bad: bypasses the barrel, couples to internal file structure
|
|
464
|
+
import { readConfig } from "#config/readConfig.js";
|
|
465
|
+
|
|
466
|
+
// Good: import through the barrel
|
|
467
|
+
import { readConfig } from "#config";
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## packaging/library/structure
|
|
473
|
+
|
|
474
|
+
Library packages export reusable code (components, utilities, hooks, types) for consumption by other packages. They must organize their exportable code in a `src/lib/` folder and re-export through a root barrel. This convention ensures consistency across the monorepo and clearly separates public API code from non-exported concerns like storybook configuration or test utilities. Each domain folder within `lib/` must have its own barrel (`index.ts`).
|
|
475
|
+
|
|
476
|
+
### Do
|
|
477
|
+
|
|
478
|
+
Recognise a library package by its `package.json` signals — `main`/`exports` field, publishable, no `bin`
|
|
479
|
+
```json
|
|
480
|
+
{
|
|
481
|
+
"name": "@scope/design-system",
|
|
482
|
+
"main": "./dist/index.js",
|
|
483
|
+
"exports": { ".": "./dist/index.js" }
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Place all reusable/exportable code in `src/lib/`, with a barrel in each domain folder
|
|
488
|
+
```
|
|
489
|
+
packages/my-package/
|
|
490
|
+
├── src/
|
|
491
|
+
│ ├── lib/
|
|
492
|
+
│ │ ├── Button/
|
|
493
|
+
│ │ │ ├── Button.tsx
|
|
494
|
+
│ │ │ ├── Button.tests.tsx
|
|
495
|
+
│ │ │ ├── types.ts
|
|
496
|
+
│ │ │ └── index.ts # Barrel: exports Button public API
|
|
497
|
+
│ │ ├── hooks/
|
|
498
|
+
│ │ │ ├── useToggle.ts
|
|
499
|
+
│ │ │ └── index.ts # Barrel: exports hooks
|
|
500
|
+
│ │ ├── types/
|
|
501
|
+
│ │ │ └── index.ts # Barrel: exports shared types
|
|
502
|
+
│ │ └── index.ts # Barrel: lib public API
|
|
503
|
+
│ ├── storybook/ # Storybook-specific files (not exported)
|
|
504
|
+
│ └── index.ts # Re-exports from lib
|
|
505
|
+
└── package.json
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Re-export from the lib folder in the package entry point
|
|
509
|
+
```typescript
|
|
510
|
+
// src/index.ts
|
|
511
|
+
export * from "./lib/index.js";
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Use the lib barrel to compose the package's public API from domain barrels
|
|
515
|
+
```typescript
|
|
516
|
+
// src/lib/index.ts
|
|
517
|
+
export * from "./Button/index.js";
|
|
518
|
+
export * from "./hooks/index.js";
|
|
519
|
+
export type * from "./types/index.js";
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Don't
|
|
523
|
+
|
|
524
|
+
Use alternative folder names like `ui`, `components`, or `utils` for exportable code at the package level
|
|
525
|
+
```
|
|
526
|
+
// Bad: Using 'ui' instead of 'lib'
|
|
527
|
+
packages/my-package/
|
|
528
|
+
├── src/
|
|
529
|
+
│ ├── ui/ # Wrong: should be 'lib'
|
|
530
|
+
│ │ └── Button/
|
|
531
|
+
│ └── index.ts
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Mix exportable and non-exportable code at the same level
|
|
535
|
+
```
|
|
536
|
+
// Bad: Mixing concerns
|
|
537
|
+
packages/my-package/
|
|
538
|
+
├── src/
|
|
539
|
+
│ ├── Button/ # Component mixed with...
|
|
540
|
+
│ ├── storybook/ # ...non-exportable storybook config
|
|
541
|
+
│ └── test-utils/ # ...and test utilities
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Create deeply nested lib-like structures; keep lib at the top level of src
|
|
545
|
+
```
|
|
546
|
+
// Bad: Nested lib folders
|
|
547
|
+
packages/my-package/
|
|
548
|
+
├── src/
|
|
549
|
+
│ ├── features/
|
|
550
|
+
│ │ └── lib/ # Wrong: lib should be at src level
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Omit barrels from domain folders within lib
|
|
554
|
+
```
|
|
555
|
+
// Bad: no barrels — consumers must know individual file paths
|
|
556
|
+
packages/my-package/
|
|
557
|
+
├── src/
|
|
558
|
+
│ ├── lib/
|
|
559
|
+
│ │ ├── Button/
|
|
560
|
+
│ │ │ ├── Button.tsx
|
|
561
|
+
│ │ │ └── types.ts # No index.ts — fragile imports
|
|
562
|
+
│ │ ├── hooks/
|
|
563
|
+
│ │ │ └── useToggle.ts # No index.ts
|
|
564
|
+
│ │ └── index.ts
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## packaging/naming/single-export-file
|
|
570
|
+
|
|
571
|
+
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).
|
|
572
|
+
|
|
573
|
+
### Do
|
|
574
|
+
|
|
575
|
+
Name files after the single function they export
|
|
576
|
+
```typescript
|
|
577
|
+
// isAccepted.ts
|
|
578
|
+
export const isAccepted = (status: string): boolean => {
|
|
579
|
+
return status === "accepted";
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// formatCurrency.ts
|
|
583
|
+
export default function formatCurrency(value: number): string {
|
|
584
|
+
return `$${value.toFixed(2)}`;
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Name files after the single type or interface they export
|
|
589
|
+
```typescript
|
|
590
|
+
// ConnectionConfig.ts
|
|
591
|
+
export interface ConnectionConfig {
|
|
592
|
+
host: string;
|
|
593
|
+
port: number;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// UserRole.ts
|
|
597
|
+
export type UserRole = "admin" | "editor" | "viewer";
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Name files after the single class they export
|
|
601
|
+
```typescript
|
|
602
|
+
// EventEmitter.ts
|
|
603
|
+
export class EventEmitter {
|
|
604
|
+
// ...
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Name files after the single constant they export
|
|
609
|
+
```typescript
|
|
610
|
+
// DEFAULT_TIMEOUT.ts
|
|
611
|
+
export const DEFAULT_TIMEOUT = 5000;
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Don't
|
|
615
|
+
|
|
616
|
+
Use generic names like `helpers.ts`, `utils.ts`, or `types.ts` for files with a single export
|
|
617
|
+
```typescript
|
|
618
|
+
// helpers.ts — wrong: should be isAccepted.ts
|
|
619
|
+
export const isAccepted = (status: string): boolean => {
|
|
620
|
+
return status === "accepted";
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// utils.ts — wrong: should be formatCurrency.ts
|
|
624
|
+
export default function formatCurrency(value: number): string {
|
|
625
|
+
return `$${value.toFixed(2)}`;
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Use names that describe the domain instead of the export
|
|
630
|
+
```typescript
|
|
631
|
+
// validation.ts — wrong: should be isAccepted.ts
|
|
632
|
+
export const isAccepted = (status: string): boolean => {
|
|
633
|
+
return status === "accepted";
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// network.ts — wrong: should be ConnectionConfig.ts
|
|
637
|
+
export interface ConnectionConfig {
|
|
638
|
+
host: string;
|
|
639
|
+
port: number;
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|