@zigrivers/scaffold 3.7.0 → 3.9.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 +113 -8
- package/content/knowledge/browser-extension/browser-extension-architecture.md +195 -0
- package/content/knowledge/browser-extension/browser-extension-content-scripts.md +264 -0
- package/content/knowledge/browser-extension/browser-extension-conventions.md +156 -0
- package/content/knowledge/browser-extension/browser-extension-cross-browser.md +229 -0
- package/content/knowledge/browser-extension/browser-extension-dev-environment.md +247 -0
- package/content/knowledge/browser-extension/browser-extension-manifest.md +220 -0
- package/content/knowledge/browser-extension/browser-extension-project-structure.md +183 -0
- package/content/knowledge/browser-extension/browser-extension-requirements.md +107 -0
- package/content/knowledge/browser-extension/browser-extension-security.md +202 -0
- package/content/knowledge/browser-extension/browser-extension-service-workers.md +265 -0
- package/content/knowledge/browser-extension/browser-extension-store-submission.md +155 -0
- package/content/knowledge/browser-extension/browser-extension-testing.md +270 -0
- package/content/knowledge/data-pipeline/data-pipeline-architecture.md +175 -0
- package/content/knowledge/data-pipeline/data-pipeline-batch-patterns.md +263 -0
- package/content/knowledge/data-pipeline/data-pipeline-conventions.md +176 -0
- package/content/knowledge/data-pipeline/data-pipeline-dev-environment.md +350 -0
- package/content/knowledge/data-pipeline/data-pipeline-orchestration.md +291 -0
- package/content/knowledge/data-pipeline/data-pipeline-project-structure.md +257 -0
- package/content/knowledge/data-pipeline/data-pipeline-quality.md +324 -0
- package/content/knowledge/data-pipeline/data-pipeline-requirements.md +145 -0
- package/content/knowledge/data-pipeline/data-pipeline-schema-management.md +295 -0
- package/content/knowledge/data-pipeline/data-pipeline-security.md +326 -0
- package/content/knowledge/data-pipeline/data-pipeline-streaming-patterns.md +280 -0
- package/content/knowledge/data-pipeline/data-pipeline-testing.md +406 -0
- package/content/knowledge/library/library-api-design.md +306 -0
- package/content/knowledge/library/library-architecture.md +247 -0
- package/content/knowledge/library/library-bundling.md +244 -0
- package/content/knowledge/library/library-conventions.md +229 -0
- package/content/knowledge/library/library-dev-environment.md +220 -0
- package/content/knowledge/library/library-documentation.md +300 -0
- package/content/knowledge/library/library-project-structure.md +237 -0
- package/content/knowledge/library/library-requirements.md +173 -0
- package/content/knowledge/library/library-security.md +257 -0
- package/content/knowledge/library/library-testing.md +319 -0
- package/content/knowledge/library/library-type-definitions.md +284 -0
- package/content/knowledge/library/library-versioning.md +300 -0
- package/content/knowledge/ml/ml-architecture.md +172 -0
- package/content/knowledge/ml/ml-conventions.md +209 -0
- package/content/knowledge/ml/ml-dev-environment.md +299 -0
- package/content/knowledge/ml/ml-experiment-tracking.md +285 -0
- package/content/knowledge/ml/ml-model-evaluation.md +256 -0
- package/content/knowledge/ml/ml-observability.md +253 -0
- package/content/knowledge/ml/ml-project-structure.md +216 -0
- package/content/knowledge/ml/ml-requirements.md +138 -0
- package/content/knowledge/ml/ml-security.md +188 -0
- package/content/knowledge/ml/ml-serving-patterns.md +243 -0
- package/content/knowledge/ml/ml-testing.md +301 -0
- package/content/knowledge/ml/ml-training-patterns.md +269 -0
- package/content/knowledge/mobile-app/mobile-app-architecture.md +283 -0
- package/content/knowledge/mobile-app/mobile-app-conventions.md +180 -0
- package/content/knowledge/mobile-app/mobile-app-deployment.md +298 -0
- package/content/knowledge/mobile-app/mobile-app-dev-environment.md +257 -0
- package/content/knowledge/mobile-app/mobile-app-distribution.md +264 -0
- package/content/knowledge/mobile-app/mobile-app-observability.md +317 -0
- package/content/knowledge/mobile-app/mobile-app-offline-patterns.md +311 -0
- package/content/knowledge/mobile-app/mobile-app-project-structure.md +245 -0
- package/content/knowledge/mobile-app/mobile-app-push-notifications.md +321 -0
- package/content/knowledge/mobile-app/mobile-app-requirements.md +147 -0
- package/content/knowledge/mobile-app/mobile-app-security.md +338 -0
- package/content/knowledge/mobile-app/mobile-app-testing.md +400 -0
- package/content/methodology/browser-extension-overlay.yml +82 -0
- package/content/methodology/data-pipeline-overlay.yml +70 -0
- package/content/methodology/library-overlay.yml +67 -0
- package/content/methodology/ml-overlay.yml +70 -0
- package/content/methodology/mobile-app-overlay.yml +71 -0
- package/dist/cli/commands/init.d.ts +22 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +202 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +190 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +1456 -80
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +87 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +312 -3
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +55 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +2 -1
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -1
- package/dist/e2e/project-type-overlays.test.js +780 -14
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/types/config.d.ts +16 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/wizard/questions.d.ts +28 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +127 -1
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +224 -4
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +22 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +28 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-api-design
|
|
3
|
+
description: Public surface design, method signatures, error contracts, and extension points for published library APIs
|
|
4
|
+
topics: [library, api-design, public-surface, method-signatures, error-contracts, extension-points]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Library API design is the highest-leverage activity in library development. A well-designed API makes correct usage easy and incorrect usage hard, survives multiple major versions without fundamental restructuring, and communicates intent through its shape alone. Poor API design cannot be fixed without breaking changes — every naming mistake, parameter order error, and missing overload becomes permanent once consumers adopt it. Design APIs from the consumer's perspective first, implementation second.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Design APIs by writing consumer call sites before writing implementation. Prefer named options objects over positional parameters beyond two arguments. Return values should be typed as specifically as possible — avoid returning `any` or overly wide union types. Error contracts must be explicit: document what each function throws, when, and why. Provide extension points through composition (options injection, middleware, plugins) rather than inheritance. Make the happy path obvious and the error path impossible to ignore.
|
|
12
|
+
|
|
13
|
+
Core principles:
|
|
14
|
+
- Pit-of-success design: the obvious way to use the API is the correct way
|
|
15
|
+
- Named options objects for 3+ parameters
|
|
16
|
+
- Explicit error contracts (typed throws, documented in JSDoc)
|
|
17
|
+
- Overloads for genuinely different call signatures
|
|
18
|
+
- Consistent return type patterns (never `T | undefined` when you can overload)
|
|
19
|
+
|
|
20
|
+
## Deep Guidance
|
|
21
|
+
|
|
22
|
+
### Write Call Sites First
|
|
23
|
+
|
|
24
|
+
Before implementing any function, write the code that will call it:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Step 1: Write how consumers will use this
|
|
28
|
+
import { parseConfig } from 'my-library'
|
|
29
|
+
|
|
30
|
+
// Use case 1: parse a string, get typed config
|
|
31
|
+
const config = parseConfig(rawString)
|
|
32
|
+
|
|
33
|
+
// Use case 2: parse with strict mode
|
|
34
|
+
const config = parseConfig(rawString, { strict: true })
|
|
35
|
+
|
|
36
|
+
// Use case 3: parse a file path (different input)
|
|
37
|
+
const config = await parseConfigFile('./config.toml')
|
|
38
|
+
|
|
39
|
+
// Use case 4: handle parse errors gracefully
|
|
40
|
+
try {
|
|
41
|
+
const config = parseConfig(rawString)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof ParseError) {
|
|
44
|
+
console.error(`Parse failed at line ${err.line}: ${err.message}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Step 2: Now design the API to make this work naturally
|
|
49
|
+
export function parseConfig(input: string, options?: ParseOptions): Config
|
|
50
|
+
export async function parseConfigFile(path: string, options?: ParseOptions): Promise<Config>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This technique reveals usability issues before any code is written.
|
|
54
|
+
|
|
55
|
+
### Options Object Pattern
|
|
56
|
+
|
|
57
|
+
Positional parameters beyond two create cognitive load and fragile call sites:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// BAD: positional parameters — order is arbitrary, easy to mix up
|
|
61
|
+
function connect(host: string, port: number, timeout: number, ssl: boolean, retries: number): Client
|
|
62
|
+
|
|
63
|
+
// Called as: connect('localhost', 5432, 30000, true, 3)
|
|
64
|
+
// Which is timeout and which is retries? Must check signature every time.
|
|
65
|
+
|
|
66
|
+
// GOOD: named options object
|
|
67
|
+
interface ConnectOptions {
|
|
68
|
+
host: string
|
|
69
|
+
port: number
|
|
70
|
+
timeout?: number // ms, default: 30000
|
|
71
|
+
ssl?: boolean // default: false
|
|
72
|
+
retries?: number // default: 3
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function connect(options: ConnectOptions): Client
|
|
76
|
+
// connect({ host: 'localhost', port: 5432, ssl: true })
|
|
77
|
+
// Self-documenting call site. New options add without breaking callers.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Options object rules:**
|
|
81
|
+
- All options beyond the first two required parameters go in an options object
|
|
82
|
+
- Options should have sensible defaults (document the defaults in JSDoc)
|
|
83
|
+
- Required options stay required; don't make everything optional
|
|
84
|
+
- Never use boolean flags that change behavior fundamentally — use discriminated unions
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// BAD: boolean flag that means completely different behavior
|
|
88
|
+
function parse(input: string, isFile: boolean): Config
|
|
89
|
+
|
|
90
|
+
// GOOD: separate functions or discriminated union
|
|
91
|
+
function parseString(input: string): Config
|
|
92
|
+
function parseFile(path: string): Promise<Config>
|
|
93
|
+
// Or:
|
|
94
|
+
type ParseInput = { type: 'string'; value: string } | { type: 'file'; path: string }
|
|
95
|
+
function parse(input: ParseInput): Config | Promise<Config>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Method Signature Design
|
|
99
|
+
|
|
100
|
+
**Overloads for genuinely different signatures:**
|
|
101
|
+
```typescript
|
|
102
|
+
// Overloads allow TypeScript to narrow the return type based on input
|
|
103
|
+
function parse(input: string): Config
|
|
104
|
+
function parse(input: string, options: { async: true }): Promise<Config>
|
|
105
|
+
function parse(input: string, options: { async: false }): Config
|
|
106
|
+
function parse(input: string, options?: ParseOptions): Config | Promise<Config> {
|
|
107
|
+
// implementation
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use overloads sparingly. Each overload is a commitment to maintain that signature. If you find yourself needing many overloads, reconsider whether the API should be split into separate functions.
|
|
112
|
+
|
|
113
|
+
**Generic constraints — add them when they add value:**
|
|
114
|
+
```typescript
|
|
115
|
+
// Good: generic constraint enables type narrowing
|
|
116
|
+
function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
|
|
117
|
+
|
|
118
|
+
// Bad: generic without constraint is less type-safe
|
|
119
|
+
function pick<T, K>(obj: T, keys: K[]): any
|
|
120
|
+
|
|
121
|
+
// Bad: generic where it adds no value (always the same type)
|
|
122
|
+
function identity<T>(value: T): T // Fine for teaching, rarely needed in practice
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Return type specificity:**
|
|
126
|
+
```typescript
|
|
127
|
+
// BAD: too wide
|
|
128
|
+
function getUser(id: string): object
|
|
129
|
+
|
|
130
|
+
// BAD: any
|
|
131
|
+
function parseYAML(input: string): any
|
|
132
|
+
|
|
133
|
+
// GOOD: specific types
|
|
134
|
+
function getUser(id: string): User | null // null when not found
|
|
135
|
+
function parseYAML<T = unknown>(input: string): T // generic with default
|
|
136
|
+
|
|
137
|
+
// BEST when the shape is known:
|
|
138
|
+
interface User {
|
|
139
|
+
id: string
|
|
140
|
+
name: string
|
|
141
|
+
email: string
|
|
142
|
+
createdAt: Date
|
|
143
|
+
}
|
|
144
|
+
function getUser(id: string): User | null
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Error Contracts
|
|
148
|
+
|
|
149
|
+
Every public function must have a documented error contract. Document errors in JSDoc and in a separate errors section of the API documentation.
|
|
150
|
+
|
|
151
|
+
**JSDoc error documentation:**
|
|
152
|
+
```typescript
|
|
153
|
+
/**
|
|
154
|
+
* Parse a configuration string into a typed Config object.
|
|
155
|
+
*
|
|
156
|
+
* @param input - TOML-formatted configuration string
|
|
157
|
+
* @param options - Parsing options
|
|
158
|
+
* @returns Parsed and validated Config object
|
|
159
|
+
*
|
|
160
|
+
* @throws {ParseError} If the input string is not valid TOML.
|
|
161
|
+
* `ParseError.line` and `ParseError.column` indicate the error location.
|
|
162
|
+
* @throws {ValidationError} If the parsed config fails schema validation.
|
|
163
|
+
* `ValidationError.errors` contains the list of validation failures.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* try {
|
|
168
|
+
* const config = parseConfig('[server]\nhost = "localhost"')
|
|
169
|
+
* } catch (err) {
|
|
170
|
+
* if (err instanceof ParseError) {
|
|
171
|
+
* console.error(`Syntax error at line ${err.line}`)
|
|
172
|
+
* }
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export function parseConfig(input: string, options?: ParseOptions): Config
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Result type pattern (alternative to throws):**
|
|
180
|
+
For APIs where errors are expected and should be handled inline, a Result type is cleaner than throws:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
export type Result<T, E extends Error = Error> =
|
|
184
|
+
| { ok: true; value: T }
|
|
185
|
+
| { ok: false; error: E }
|
|
186
|
+
|
|
187
|
+
export function tryParseConfig(input: string): Result<Config, ParseError | ValidationError> {
|
|
188
|
+
try {
|
|
189
|
+
return { ok: true, value: parseConfig(input) }
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return { ok: false, error: err as ParseError | ValidationError }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Consumer usage:
|
|
196
|
+
const result = tryParseConfig(input)
|
|
197
|
+
if (result.ok) {
|
|
198
|
+
console.log(result.value.server.host)
|
|
199
|
+
} else {
|
|
200
|
+
console.error(result.error.message)
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Provide both patterns when the use case warrants: throwing for "should not fail" paths, Result type for "expected to sometimes fail" paths.
|
|
205
|
+
|
|
206
|
+
### Extension Points
|
|
207
|
+
|
|
208
|
+
Design extension points that don't require forking or subclassing:
|
|
209
|
+
|
|
210
|
+
**Middleware pattern for transform pipelines:**
|
|
211
|
+
```typescript
|
|
212
|
+
export interface ParseMiddleware {
|
|
213
|
+
(input: string, next: (input: string) => Config): Config
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createParser(middlewares: ParseMiddleware[] = []): Parser {
|
|
217
|
+
return {
|
|
218
|
+
parse(input: string): Config {
|
|
219
|
+
const chain = middlewares.reduceRight(
|
|
220
|
+
(next: (i: string) => Config, mw) => (i: string) => mw(i, next),
|
|
221
|
+
parseRaw
|
|
222
|
+
)
|
|
223
|
+
return chain(input)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Consumer adds preprocessing:
|
|
229
|
+
const parser = createParser([
|
|
230
|
+
(input, next) => next(input.trim().toLowerCase()),
|
|
231
|
+
(input, next) => {
|
|
232
|
+
const result = next(input)
|
|
233
|
+
return { ...result, source: 'custom' }
|
|
234
|
+
}
|
|
235
|
+
])
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Hook pattern for lifecycle events:**
|
|
239
|
+
```typescript
|
|
240
|
+
export interface ClientHooks {
|
|
241
|
+
beforeRequest?: (req: Request) => Request | Promise<Request>
|
|
242
|
+
afterResponse?: (res: Response) => Response | Promise<Response>
|
|
243
|
+
onError?: (err: Error) => void
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function createClient(options: ClientOptions & { hooks?: ClientHooks }): Client
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Avoid class inheritance as extension:**
|
|
250
|
+
```typescript
|
|
251
|
+
// BAD: forces consumers to subclass
|
|
252
|
+
class BaseClient {
|
|
253
|
+
protected abstract buildRequest(options: RequestOptions): Request
|
|
254
|
+
// Consumers must extend to customize
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// GOOD: inject the behavior
|
|
258
|
+
type RequestBuilder = (options: RequestOptions) => Request
|
|
259
|
+
|
|
260
|
+
function createClient(options: {
|
|
261
|
+
buildRequest?: RequestBuilder
|
|
262
|
+
}): Client
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Subclassing creates tight coupling between the consumer and the library's internal class hierarchy. Every internal restructuring becomes a breaking change.
|
|
266
|
+
|
|
267
|
+
### Fluent API Design
|
|
268
|
+
|
|
269
|
+
Fluent APIs (method chaining) improve readability for configuration-heavy builders:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Query builder example
|
|
273
|
+
export class QueryBuilder<T> {
|
|
274
|
+
private _where: WhereClause[] = []
|
|
275
|
+
private _orderBy: OrderClause[] = []
|
|
276
|
+
private _limit?: number
|
|
277
|
+
|
|
278
|
+
where(field: keyof T, op: Operator, value: unknown): this {
|
|
279
|
+
this._where.push({ field: field as string, op, value })
|
|
280
|
+
return this
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
orderBy(field: keyof T, direction: 'asc' | 'desc' = 'asc'): this {
|
|
284
|
+
this._orderBy.push({ field: field as string, direction })
|
|
285
|
+
return this
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
limit(n: number): this {
|
|
289
|
+
this._limit = n
|
|
290
|
+
return this
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
build(): Query<T> {
|
|
294
|
+
return { where: this._where, orderBy: this._orderBy, limit: this._limit }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Consumer:
|
|
299
|
+
const query = new QueryBuilder<User>()
|
|
300
|
+
.where('active', '=', true)
|
|
301
|
+
.orderBy('createdAt', 'desc')
|
|
302
|
+
.limit(10)
|
|
303
|
+
.build()
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Use fluent APIs for builders and configuration DSLs. Avoid them for operational functions — `parseConfig(input).validate().execute()` is harder to debug than three explicit function calls.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-architecture
|
|
3
|
+
description: Module design, dependency minimization, tree-shaking enablement, and plugin patterns for published libraries
|
|
4
|
+
topics: [library, architecture, modules, tree-shaking, plugins, dependencies, design]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Library architecture is constrained by two forces that do not apply to applications: the consumer's bundle size budget and the consumer's dependency graph. Every architectural decision must account for what the library adds to consumers who import it — not just the feature complexity it solves. A well-architected library is modular by default, has minimal or zero runtime dependencies, enables tree-shaking so consumers pay only for what they use, and provides extension points that don't require forking the library.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Design libraries as collections of composable, independently tree-shakeable units. The root module is a barrel export; each feature module is independently importable. Minimize runtime dependencies to zero where possible — every dependency you add becomes a transitive dependency for every consumer. Enable tree-shaking by using ES modules, avoiding side effects, and ensuring the `"sideEffects": false` field is accurate. Plugin patterns allow extension without coupling; prefer dependency injection and factory functions over class hierarchies.
|
|
12
|
+
|
|
13
|
+
Core architectural decisions:
|
|
14
|
+
- Module boundary: each exported function/class is its own module file, barrel at root
|
|
15
|
+
- Dependency strategy: zero runtime deps preferred; peer deps for framework integration
|
|
16
|
+
- Side effects: none at module load time; `"sideEffects": false` in package.json
|
|
17
|
+
- Extension pattern: plugin factory or dependency injection, not subclassing
|
|
18
|
+
- Error strategy: typed error classes, not string codes; never swallow errors
|
|
19
|
+
|
|
20
|
+
## Deep Guidance
|
|
21
|
+
|
|
22
|
+
### Module Boundary Design
|
|
23
|
+
|
|
24
|
+
Each public export should have a clear, single responsibility. The module structure mirrors the API surface:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
├── index.ts # Barrel: re-exports all public APIs
|
|
29
|
+
├── parser.ts # parseConfig, ParseOptions, ParseError
|
|
30
|
+
├── validator.ts # validateSchema, ValidationResult, ValidationError
|
|
31
|
+
├── client.ts # createClient, ClientOptions, Client interface
|
|
32
|
+
├── types.ts # Shared types used by multiple modules
|
|
33
|
+
├── errors.ts # Base error classes
|
|
34
|
+
└── internal/
|
|
35
|
+
├── cache.ts # Internal LRU cache (not exported)
|
|
36
|
+
├── http.ts # Internal HTTP utilities
|
|
37
|
+
└── utils.ts # Internal pure utilities
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**The key constraint:** modules in `internal/` must never be imported by consumers. Enforce this with an ESLint rule:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
// .eslintrc.json
|
|
44
|
+
{
|
|
45
|
+
"rules": {
|
|
46
|
+
"no-restricted-imports": ["error", {
|
|
47
|
+
"patterns": ["*/internal/*"]
|
|
48
|
+
}]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This rule is for consumer code, not for library internals. The library's own modules can freely import from `internal/`.
|
|
54
|
+
|
|
55
|
+
### Dependency Minimization
|
|
56
|
+
|
|
57
|
+
Every runtime dependency you add has costs:
|
|
58
|
+
1. Adds to install size for all consumers
|
|
59
|
+
2. Creates a version conflict risk (consumer uses different version of same dep)
|
|
60
|
+
3. Introduces a supply chain attack surface
|
|
61
|
+
4. Creates license compliance requirements for consumers
|
|
62
|
+
|
|
63
|
+
**Target: zero runtime dependencies for utility libraries.** If the library's value is transforming data, parsing strings, or providing utilities, it should have no runtime dependencies.
|
|
64
|
+
|
|
65
|
+
**When dependencies are justified:**
|
|
66
|
+
- The dependency solves a genuinely hard problem with no reasonable alternative (cryptography, date parsing)
|
|
67
|
+
- The dependency is a peer dependency the consumer already has (React, Vue, Node.js built-ins)
|
|
68
|
+
- The functionality would require thousands of lines of well-tested code to replicate
|
|
69
|
+
|
|
70
|
+
**Inlining vs. depending:**
|
|
71
|
+
For small, stable utilities (10-50 lines), consider inlining instead of depending:
|
|
72
|
+
```typescript
|
|
73
|
+
// Instead of: import { clamp } from 'lodash-es'
|
|
74
|
+
// Inline it:
|
|
75
|
+
function clamp(value: number, min: number, max: number): number {
|
|
76
|
+
return Math.min(Math.max(value, min), max)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For large, complex utilities (crypto, parsing), depend on the established library.
|
|
81
|
+
|
|
82
|
+
### Tree-Shaking Enablement
|
|
83
|
+
|
|
84
|
+
Tree-shaking requires the bundler to statically analyze which exports are used. Three requirements:
|
|
85
|
+
|
|
86
|
+
**1. ES module syntax throughout:**
|
|
87
|
+
```typescript
|
|
88
|
+
// Good — static, analyzable
|
|
89
|
+
export function parseConfig(input: string): Config { ... }
|
|
90
|
+
export { validateSchema } from './validator'
|
|
91
|
+
|
|
92
|
+
// Bad — dynamic, blocks tree-shaking
|
|
93
|
+
module.exports = { parseConfig } // CommonJS
|
|
94
|
+
exports[functionName] = fn // Dynamic export key
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**2. No side effects at module load time:**
|
|
98
|
+
```typescript
|
|
99
|
+
// BAD: side effect on import — breaks tree-shaking
|
|
100
|
+
const cache = new Map()
|
|
101
|
+
globalThis.__myLibCache = cache // Pollutes global on import
|
|
102
|
+
console.log('my-library loaded') // Log on import
|
|
103
|
+
|
|
104
|
+
// GOOD: factory function — no side effect until called
|
|
105
|
+
export function createCache(): Map<string, unknown> {
|
|
106
|
+
return new Map()
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**3. Accurate `sideEffects` field:**
|
|
111
|
+
```json
|
|
112
|
+
// package.json — only if genuinely no side effects
|
|
113
|
+
{ "sideEffects": false }
|
|
114
|
+
|
|
115
|
+
// If CSS imports or polyfills have side effects:
|
|
116
|
+
{ "sideEffects": ["*.css", "src/polyfills.js"] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Verify tree-shaking works:**
|
|
120
|
+
```bash
|
|
121
|
+
# Use bundle-buddy, rollup-plugin-visualizer, or bundlephobia to verify
|
|
122
|
+
npx bundlephobia my-library@1.0.0
|
|
123
|
+
|
|
124
|
+
# Or build a minimal consumer and check the output bundle size
|
|
125
|
+
# A consumer that only imports parseConfig should not include validateSchema code
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Plugin Architecture Patterns
|
|
129
|
+
|
|
130
|
+
Plugins allow consumers to extend the library without modifying it. Three patterns in increasing flexibility order:
|
|
131
|
+
|
|
132
|
+
**Pattern 1: Factory function with options (simplest):**
|
|
133
|
+
```typescript
|
|
134
|
+
export interface ClientOptions {
|
|
135
|
+
transport?: Transport // Consumer provides custom transport
|
|
136
|
+
serializer?: Serializer
|
|
137
|
+
logger?: Logger
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createClient(options: ClientOptions = {}): Client {
|
|
141
|
+
const transport = options.transport ?? defaultHttpTransport()
|
|
142
|
+
const serializer = options.serializer ?? jsonSerializer()
|
|
143
|
+
const logger = options.logger ?? noopLogger()
|
|
144
|
+
return new ClientImpl(transport, serializer, logger)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Consumer can inject their own transport:
|
|
148
|
+
const client = createClient({
|
|
149
|
+
transport: myCustomTransport,
|
|
150
|
+
logger: pinoLogger
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This is dependency injection — the simplest, most testable plugin pattern.
|
|
155
|
+
|
|
156
|
+
**Pattern 2: Plugin registry (for named, composable extensions):**
|
|
157
|
+
```typescript
|
|
158
|
+
export interface Plugin {
|
|
159
|
+
name: string
|
|
160
|
+
setup(context: PluginContext): void | Promise<void>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface PluginContext {
|
|
164
|
+
registerTransform(name: string, fn: TransformFn): void
|
|
165
|
+
registerValidator(name: string, fn: ValidatorFn): void
|
|
166
|
+
onParse(hook: ParseHook): void
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createClient(options: { plugins?: Plugin[] } = {}): Client {
|
|
170
|
+
const context = createPluginContext()
|
|
171
|
+
for (const plugin of (options.plugins ?? [])) {
|
|
172
|
+
plugin.setup(context)
|
|
173
|
+
}
|
|
174
|
+
return new ClientImpl(context)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Consumer uses plugins:
|
|
178
|
+
import { myPlugin } from 'my-library-plugin-example'
|
|
179
|
+
const client = createClient({ plugins: [myPlugin()] })
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Pattern 3: Middleware chain (for transform pipelines):**
|
|
183
|
+
```typescript
|
|
184
|
+
export type Middleware<T> = (value: T, next: (value: T) => T) => T
|
|
185
|
+
|
|
186
|
+
export function createPipeline<T>(middlewares: Middleware<T>[]): (value: T) => T {
|
|
187
|
+
return (initial: T) => {
|
|
188
|
+
const chain = middlewares.reduceRight<(value: T) => T>(
|
|
189
|
+
(next, middleware) => (value) => middleware(value, next),
|
|
190
|
+
(value) => value
|
|
191
|
+
)
|
|
192
|
+
return chain(initial)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**What to avoid:**
|
|
198
|
+
- Class inheritance as extension mechanism (consumers must subclass to customize — creates tight coupling)
|
|
199
|
+
- Singleton registries (one global registry makes testing and isolation difficult)
|
|
200
|
+
- Monkey-patching as extension (fragile, hidden coupling)
|
|
201
|
+
|
|
202
|
+
### Error Architecture
|
|
203
|
+
|
|
204
|
+
Typed errors are part of the public API contract:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// errors.ts — exported as part of public API
|
|
208
|
+
export class LibraryError extends Error {
|
|
209
|
+
constructor(message: string, public readonly code: ErrorCode) {
|
|
210
|
+
super(message)
|
|
211
|
+
this.name = 'LibraryError'
|
|
212
|
+
// Maintain proper prototype chain in transpiled environments
|
|
213
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export class ParseError extends LibraryError {
|
|
218
|
+
constructor(
|
|
219
|
+
message: string,
|
|
220
|
+
public readonly line: number,
|
|
221
|
+
public readonly column: number,
|
|
222
|
+
public readonly source?: string
|
|
223
|
+
) {
|
|
224
|
+
super(message, 'PARSE_ERROR')
|
|
225
|
+
this.name = 'ParseError'
|
|
226
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type ErrorCode = 'PARSE_ERROR' | 'VALIDATION_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT'
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Never throw raw `Error` objects from library code — consumers cannot distinguish library errors from their own errors. Always throw typed errors with enough context to diagnose the problem.
|
|
234
|
+
|
|
235
|
+
### Avoiding Circular Dependencies
|
|
236
|
+
|
|
237
|
+
Circular dependencies in libraries cause subtle initialization order bugs. Enforce absence with ESLint:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"rules": {
|
|
242
|
+
"import/no-cycle": ["error", { "maxDepth": 10 }]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
When you detect a circular dependency, the usual fix is extracting shared types to `types.ts` (which has no imports) rather than rearranging imports.
|