@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.
@@ -0,0 +1,464 @@
1
+ @prefix cs: <http://pragma.canonical.com/codestandards#> .
2
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
3
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
4
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
5
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
6
+
7
+ # Packaging Category
8
+ cs:PackagingCategory a cs:Category ;
9
+ rdfs:label "Packaging"@en ;
10
+ rdfs:comment "Standards for structuring TypeScript/JavaScript packages — archetype classification, folder layout, export shape, barrel files, and file naming"@en ;
11
+ cs:slug "packaging" .
12
+
13
+ # Application Package Structure
14
+ cs:ApplicationPackage a cs:CodeStandard ;
15
+ cs:name "packaging/application/structure" ;
16
+ cs:hasCategory cs:PackagingCategory ;
17
+ cs:description "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." ;
18
+ cs:dos """
19
+ (Do) Recognise an application package by its `package.json` signals — `bin` field, `private: true`, no `main`/`exports`:
20
+ ```json
21
+ {
22
+ "name": "@scope/my-cli",
23
+ "private": true,
24
+ "bin": { "my-cli": "./dist/index.js" }
25
+ }
26
+ ```
27
+
28
+ (Do) Organize code around execution domains. Each domain folder gets its own barrel (`index.ts`) that exposes the domain's internal API to sibling domains:
29
+ ```
30
+ packages/my-cli/
31
+ ├── src/
32
+ │ ├── commands/ # CLI command handlers
33
+ │ │ ├── build.ts
34
+ │ │ ├── deploy.ts
35
+ │ │ └── index.ts # Barrel: exports all commands
36
+ │ ├── operations/ # Shared internal logic across commands
37
+ │ │ ├── resolveConfig.ts
38
+ │ │ ├── runValidation.ts
39
+ │ │ └── index.ts # Barrel: exports shared operations
40
+ │ ├── utils/ # Internal helpers
41
+ │ │ ├── formatOutput.ts
42
+ │ │ └── index.ts # Barrel: exports utilities
43
+ │ └── index.ts # Entry point (bootstraps CLI)
44
+ └── package.json
45
+ ```
46
+
47
+ (Do) Import from sibling domains through their barrel, not from individual files:
48
+ ```typescript
49
+ // src/commands/deploy.ts
50
+ import { resolveConfig, runValidation } from "../operations/index.js";
51
+ import { formatOutput } from "../utils/index.js";
52
+ ```
53
+
54
+ (Do) Use domain-appropriate folder names that reflect what the code does — `commands/`, `tools/`, `routes/`, `operations/`, `handlers/`:
55
+ ```
56
+ packages/my-mcp/
57
+ ├── src/
58
+ │ ├── tools/ # MCP tool handlers
59
+ │ │ ├── query.ts
60
+ │ │ ├── mutate.ts
61
+ │ │ └── index.ts # Barrel: exports tool handlers
62
+ │ ├── operations/ # Shared internal logic
63
+ │ │ ├── executeQuery.ts
64
+ │ │ └── index.ts # Barrel: exports shared operations
65
+ │ └── index.ts # MCP server entry point
66
+ └── package.json
67
+ ```
68
+
69
+ (Do) 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:
70
+ ```
71
+ packages/my-mcp/
72
+ ├── src/
73
+ │ ├── lib/ # Only the reusable exported surface
74
+ │ │ ├── types.ts
75
+ │ │ └── index.ts # Barrel: public API for external consumers
76
+ │ ├── tools/ # MCP tool handlers (not exported)
77
+ │ │ ├── query.ts
78
+ │ │ ├── mutate.ts
79
+ │ │ └── index.ts # Barrel: exports tool handlers
80
+ │ ├── operations/ # Shared internal logic
81
+ │ │ ├── executeQuery.ts
82
+ │ │ └── index.ts # Barrel: exports shared operations
83
+ │ └── index.ts # MCP server entry point
84
+ └── package.json
85
+ ```
86
+ """ ;
87
+ cs:donts """
88
+ (Don't) Apply library structure (`src/lib/`, barrel re-exports) to application packages:
89
+ ```
90
+ // Bad: CLI tool forced into library layout
91
+ packages/my-cli/
92
+ ├── src/
93
+ │ ├── lib/ # Wrong: this is a CLI, not a library
94
+ │ │ ├── commands/
95
+ │ │ └── index.ts # Barrel re-export — nobody imports this
96
+ │ └── index.ts
97
+ ```
98
+
99
+ (Don't) Create a `src/lib/` folder for internal shared operations. Use domain-appropriate names instead:
100
+ ```
101
+ // Bad: "lib" implies external consumption
102
+ packages/my-cli/
103
+ ├── src/
104
+ │ ├── lib/
105
+ │ │ └── resolveConfig.ts # Only used internally
106
+
107
+ // Good: "operations" or "shared" makes intent clear
108
+ packages/my-cli/
109
+ ├── src/
110
+ │ ├── operations/
111
+ │ │ └── resolveConfig.ts # Internal shared logic
112
+ ```
113
+
114
+ (Don't) Import directly from files inside a sibling domain — always go through the domain barrel:
115
+ ```typescript
116
+ // Bad: reaching into a sibling domain's internals
117
+ import { resolveConfig } from "../operations/resolveConfig.js";
118
+ import { formatOutput } from "../utils/formatOutput.js";
119
+
120
+ // Good: import through the domain barrel
121
+ import { resolveConfig } from "../operations/index.js";
122
+ import { formatOutput } from "../utils/index.js";
123
+ ```
124
+
125
+ (Don't) Omit barrels from domain folders:
126
+ ```
127
+ // Bad: no barrels — every consumer imports individual files
128
+ packages/my-cli/
129
+ ├── src/
130
+ │ ├── commands/
131
+ │ │ ├── build.ts
132
+ │ │ └── deploy.ts # No index.ts — siblings must know file names
133
+ │ ├── operations/
134
+ │ │ ├── resolveConfig.ts
135
+ │ │ └── runValidation.ts # No index.ts — fragile cross-domain imports
136
+ ```
137
+
138
+ (Don't) Default to library structure just because the package lives in a monorepo:
139
+ ```
140
+ // Bad: reflexively adding lib/ to a build script package
141
+ packages/build-tools/
142
+ ├── src/
143
+ │ ├── lib/ # This is a build script, not a library
144
+ │ │ └── runBuild.ts
145
+ │ └── index.ts
146
+ ```
147
+ """ .
148
+
149
+ # Library Package Structure
150
+ cs:LibraryPackage a cs:CodeStandard ;
151
+ cs:name "packaging/library/structure" ;
152
+ cs:hasCategory cs:PackagingCategory ;
153
+ cs:description "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`)." ;
154
+ cs:dos """
155
+ (Do) Recognise a library package by its `package.json` signals — `main`/`exports` field, publishable, no `bin`:
156
+ ```json
157
+ {
158
+ "name": "@scope/design-system",
159
+ "main": "./dist/index.js",
160
+ "exports": { ".": "./dist/index.js" }
161
+ }
162
+ ```
163
+
164
+ (Do) Place all reusable/exportable code in `src/lib/`, with a barrel in each domain folder:
165
+ ```
166
+ packages/my-package/
167
+ ├── src/
168
+ │ ├── lib/
169
+ │ │ ├── Button/
170
+ │ │ │ ├── Button.tsx
171
+ │ │ │ ├── Button.tests.tsx
172
+ │ │ │ ├── types.ts
173
+ │ │ │ └── index.ts # Barrel: exports Button public API
174
+ │ │ ├── hooks/
175
+ │ │ │ ├── useToggle.ts
176
+ │ │ │ └── index.ts # Barrel: exports hooks
177
+ │ │ ├── types/
178
+ │ │ │ └── index.ts # Barrel: exports shared types
179
+ │ │ └── index.ts # Barrel: lib public API
180
+ │ ├── storybook/ # Storybook-specific files (not exported)
181
+ │ └── index.ts # Re-exports from lib
182
+ └── package.json
183
+ ```
184
+
185
+ (Do) Re-export from the lib folder in the package entry point:
186
+ ```typescript
187
+ // src/index.ts
188
+ export * from "./lib/index.js";
189
+ ```
190
+
191
+ (Do) Use the lib barrel to compose the package's public API from domain barrels:
192
+ ```typescript
193
+ // src/lib/index.ts
194
+ export * from "./Button/index.js";
195
+ export * from "./hooks/index.js";
196
+ export type * from "./types/index.js";
197
+ ```
198
+ """ ;
199
+ cs:donts """
200
+ (Don't) Use alternative folder names like `ui`, `components`, or `utils` for exportable code at the package level:
201
+ ```
202
+ // Bad: Using 'ui' instead of 'lib'
203
+ packages/my-package/
204
+ ├── src/
205
+ │ ├── ui/ # Wrong: should be 'lib'
206
+ │ │ └── Button/
207
+ │ └── index.ts
208
+ ```
209
+
210
+ (Don't) Mix exportable and non-exportable code at the same level:
211
+ ```
212
+ // Bad: Mixing concerns
213
+ packages/my-package/
214
+ ├── src/
215
+ │ ├── Button/ # Component mixed with...
216
+ │ ├── storybook/ # ...non-exportable storybook config
217
+ │ └── test-utils/ # ...and test utilities
218
+ ```
219
+
220
+ (Don't) Create deeply nested lib-like structures; keep lib at the top level of src:
221
+ ```
222
+ // Bad: Nested lib folders
223
+ packages/my-package/
224
+ ├── src/
225
+ │ ├── features/
226
+ │ │ └── lib/ # Wrong: lib should be at src level
227
+ ```
228
+
229
+ (Don't) Omit barrels from domain folders within lib:
230
+ ```
231
+ // Bad: no barrels — consumers must know individual file paths
232
+ packages/my-package/
233
+ ├── src/
234
+ │ ├── lib/
235
+ │ │ ├── Button/
236
+ │ │ │ ├── Button.tsx
237
+ │ │ │ └── types.ts # No index.ts — fragile imports
238
+ │ │ ├── hooks/
239
+ │ │ │ └── useToggle.ts # No index.ts
240
+ │ │ └── index.ts
241
+ ```
242
+ """ .
243
+
244
+ # Export Shape Standard
245
+ cs:ExportShape a cs:CodeStandard ;
246
+ cs:name "packaging/export/shape" ;
247
+ cs:hasCategory cs:PackagingCategory ;
248
+ cs:description "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." ;
249
+ cs:dos """
250
+ (Do) Use a single default export for files implementing a single component or function:
251
+ ```typescript
252
+ // ComponentName.tsx
253
+ export default ComponentName;
254
+ ```
255
+
256
+ (Do) Name atomic function files after the function they export:
257
+ ```typescript
258
+ // assignElement.ts
259
+ export default function assignElement(target: Element, source: Partial<Element>) {
260
+ // ...
261
+ }
262
+
263
+ // formatCurrency.ts
264
+ export default function formatCurrency(value: number) {
265
+ // ...
266
+ }
267
+ ```
268
+
269
+ (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:
270
+ ```typescript
271
+ // index.ts
272
+ export { ComponentA, ComponentB };
273
+
274
+ // types.ts
275
+ export type TypeA = { ... };
276
+ export type TypeB = { ... };
277
+
278
+ // Consistent export shape:
279
+ export const myFuncA = (value: string) => {};
280
+ export const myFuncB = (value: string) => {};
281
+ export const myFuncC = (value: string) => {};
282
+ ```
283
+ """ ;
284
+ cs:donts """
285
+ (Don't) Mix default and unrelated named exports in a way that confuses the file's purpose:
286
+ ```typescript
287
+ export default ComponentName;
288
+ export const helper = () => {};
289
+ ```
290
+
291
+ (Don't) Name an atomic function file with a name that doesn't match its exported function:
292
+ ```typescript
293
+ // helpers.ts — wrong: generic name instead of function name
294
+ export default function assignElement(target: Element, source: Partial<Element>) {
295
+ // ...
296
+ }
297
+
298
+ // utils.ts — wrong: generic name instead of function name
299
+ export default function formatCurrency(value: number) {
300
+ // ...
301
+ }
302
+ ```
303
+
304
+ (Don't) Provide multiple unrelated exports from a file meant for a single domain:
305
+ ```typescript
306
+ export default debounce;
307
+ export const throttle = () => {};
308
+ export const logger = () => {};
309
+ ```
310
+
311
+ (Don't) Export objects of different types or shapes from the same file:
312
+ ```typescript
313
+ export const transformer = (value: string) => {};
314
+ export const reducer = (map: string[]) => {};
315
+ class ABC {}
316
+ export { ABC };
317
+ ```
318
+ """ .
319
+
320
+ # Barrel File Standard
321
+ cs:BarrelPublicApi a cs:CodeStandard ;
322
+ cs:name "packaging/export/barrel" ;
323
+ cs:hasCategory cs:PackagingCategory ;
324
+ cs:description "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." ;
325
+ cs:dos """
326
+ (Do) Use a barrel at the package entry point to define the public API (library packages):
327
+ ```typescript
328
+ // src/index.ts
329
+ export { Button } from "./components/Button.js";
330
+ export type { ButtonProps } from "./components/Button.types.js";
331
+ ```
332
+
333
+ (Do) Use a barrel at each domain folder to define the domain's API (application packages):
334
+ ```typescript
335
+ // src/operations/index.ts
336
+ export { resolveConfig } from "./resolveConfig.js";
337
+ export { runValidation } from "./runValidation.js";
338
+ ```
339
+
340
+ (Do) Import internal implementation code within a domain from the owning file directly:
341
+ ```typescript
342
+ // src/build/buildTheme.ts — same domain, import the file
343
+ import { computeDeltas } from "./computeDeltas.js";
344
+ import { recoverPrimitiveRef } from "./recoverPrimitiveRef.js";
345
+ ```
346
+
347
+ (Do) Import cross-domain code through the sibling domain's barrel:
348
+ ```typescript
349
+ // src/commands/deploy.ts — different domain, import through barrel
350
+ import { resolveConfig } from "../operations/index.js";
351
+ ```
352
+
353
+ (Do) Keep compatibility barrels thin and documented as public facades:
354
+ ```typescript
355
+ /** Public compatibility surface. */
356
+ export { computeDeltas } from "./computeDeltas.js";
357
+ export { formatDelta } from "./formatDelta.js";
358
+ ```
359
+ """ ;
360
+ cs:donts """
361
+ (Don't) Route internal same-domain code through its own barrel:
362
+ ```typescript
363
+ // Bad: internal implementation depends on its own barrel
364
+ // src/build/applyTheme.ts
365
+ import { computeDeltas, recoverPrimitiveRef } from "./index.js";
366
+ ```
367
+
368
+ (Don't) Import cross-domain code by reaching into individual files:
369
+ ```typescript
370
+ // Bad: bypassing the domain barrel
371
+ import { resolveConfig } from "../operations/resolveConfig.js";
372
+
373
+ // Good: through the barrel
374
+ import { resolveConfig } from "../operations/index.js";
375
+ ```
376
+
377
+ (Don't) Hide domain ownership behind convenience barrels:
378
+ ```typescript
379
+ // Bad: types appear to belong to context.ts instead of their owning domains
380
+ import type { Artifact, CSSNode, OverlayToken } from "./context.js";
381
+ ```
382
+
383
+ (Don't) Omit the barrel from a domain folder:
384
+ ```
385
+ // Bad: no index.ts — consumers must know internal file names
386
+ src/operations/
387
+ ├── resolveConfig.ts
388
+ └── runValidation.ts
389
+ ```
390
+ """ .
391
+
392
+ # Single Export File Naming Standard
393
+ cs:SingleExportFileNaming a cs:CodeStandard ;
394
+ cs:name "packaging/naming/single-export-file" ;
395
+ cs:hasCategory cs:PackagingCategory ;
396
+ cs:description "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)." ;
397
+ cs:dos """
398
+ (Do) Name files after the single function they export:
399
+ ```typescript
400
+ // isAccepted.ts
401
+ export const isAccepted = (status: string): boolean => {
402
+ return status === "accepted";
403
+ };
404
+
405
+ // formatCurrency.ts
406
+ export default function formatCurrency(value: number): string {
407
+ return `$${value.toFixed(2)}`;
408
+ }
409
+ ```
410
+
411
+ (Do) Name files after the single type or interface they export:
412
+ ```typescript
413
+ // ConnectionConfig.ts
414
+ export interface ConnectionConfig {
415
+ host: string;
416
+ port: number;
417
+ }
418
+
419
+ // UserRole.ts
420
+ export type UserRole = "admin" | "editor" | "viewer";
421
+ ```
422
+
423
+ (Do) Name files after the single class they export:
424
+ ```typescript
425
+ // EventEmitter.ts
426
+ export class EventEmitter {
427
+ // ...
428
+ }
429
+ ```
430
+
431
+ (Do) Name files after the single constant they export:
432
+ ```typescript
433
+ // DEFAULT_TIMEOUT.ts
434
+ export const DEFAULT_TIMEOUT = 5000;
435
+ ```
436
+ """ ;
437
+ cs:donts """
438
+ (Don't) Use generic names like `helpers.ts`, `utils.ts`, or `types.ts` for files with a single export:
439
+ ```typescript
440
+ // helpers.ts — wrong: should be isAccepted.ts
441
+ export const isAccepted = (status: string): boolean => {
442
+ return status === "accepted";
443
+ };
444
+
445
+ // utils.ts — wrong: should be formatCurrency.ts
446
+ export default function formatCurrency(value: number): string {
447
+ return `$${value.toFixed(2)}`;
448
+ }
449
+ ```
450
+
451
+ (Don't) Use names that describe the domain instead of the export:
452
+ ```typescript
453
+ // validation.ts — wrong: should be isAccepted.ts
454
+ export const isAccepted = (status: string): boolean => {
455
+ return status === "accepted";
456
+ };
457
+
458
+ // network.ts — wrong: should be ConnectionConfig.ts
459
+ export interface ConnectionConfig {
460
+ host: string;
461
+ port: number;
462
+ }
463
+ ```
464
+ """ .