@zigrivers/scaffold 3.6.0 → 3.8.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 +127 -12
- package/content/knowledge/backend/backend-api-design.md +103 -0
- package/content/knowledge/backend/backend-architecture.md +100 -0
- package/content/knowledge/backend/backend-async-patterns.md +101 -0
- package/content/knowledge/backend/backend-auth-patterns.md +100 -0
- package/content/knowledge/backend/backend-conventions.md +105 -0
- package/content/knowledge/backend/backend-data-modeling.md +102 -0
- package/content/knowledge/backend/backend-deployment.md +100 -0
- package/content/knowledge/backend/backend-dev-environment.md +102 -0
- package/content/knowledge/backend/backend-observability.md +102 -0
- package/content/knowledge/backend/backend-project-structure.md +100 -0
- package/content/knowledge/backend/backend-requirements.md +103 -0
- package/content/knowledge/backend/backend-security.md +104 -0
- package/content/knowledge/backend/backend-testing.md +101 -0
- package/content/knowledge/backend/backend-worker-patterns.md +100 -0
- package/content/knowledge/cli/cli-architecture.md +101 -0
- package/content/knowledge/cli/cli-conventions.md +117 -0
- package/content/knowledge/cli/cli-dev-environment.md +121 -0
- package/content/knowledge/cli/cli-distribution-patterns.md +106 -0
- package/content/knowledge/cli/cli-interactivity-patterns.md +116 -0
- package/content/knowledge/cli/cli-output-patterns.md +107 -0
- package/content/knowledge/cli/cli-project-structure.md +124 -0
- package/content/knowledge/cli/cli-requirements.md +101 -0
- package/content/knowledge/cli/cli-shell-integration.md +130 -0
- package/content/knowledge/cli/cli-testing.md +134 -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/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/knowledge/web-app/web-app-api-patterns.md +224 -0
- package/content/knowledge/web-app/web-app-architecture.md +116 -0
- package/content/knowledge/web-app/web-app-auth-patterns.md +256 -0
- package/content/knowledge/web-app/web-app-conventions.md +121 -0
- package/content/knowledge/web-app/web-app-data-patterns.md +218 -0
- package/content/knowledge/web-app/web-app-deployment-workflow.md +143 -0
- package/content/knowledge/web-app/web-app-deployment.md +134 -0
- package/content/knowledge/web-app/web-app-design-system.md +158 -0
- package/content/knowledge/web-app/web-app-dev-environment.md +173 -0
- package/content/knowledge/web-app/web-app-observability.md +221 -0
- package/content/knowledge/web-app/web-app-project-structure.md +160 -0
- package/content/knowledge/web-app/web-app-rendering-strategies.md +133 -0
- package/content/knowledge/web-app/web-app-requirements.md +112 -0
- package/content/knowledge/web-app/web-app-security.md +193 -0
- package/content/knowledge/web-app/web-app-session-patterns.md +214 -0
- package/content/knowledge/web-app/web-app-testing.md +249 -0
- package/content/knowledge/web-app/web-app-ux-patterns.md +162 -0
- package/content/methodology/backend-overlay.yml +73 -0
- package/content/methodology/cli-overlay.yml +69 -0
- package/content/methodology/library-overlay.yml +67 -0
- package/content/methodology/mobile-app-overlay.yml +71 -0
- package/content/methodology/web-app-overlay.yml +79 -0
- package/dist/cli/commands/init.d.ts +21 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +261 -13
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +206 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/config/schema.d.ts +1392 -64
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +82 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config/schema.test.js +302 -1
- package/dist/config/schema.test.js.map +1 -1
- package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
- package/dist/core/assembly/overlay-loader.js +2 -1
- package/dist/core/assembly/overlay-loader.js.map +1 -1
- package/dist/core/assembly/overlay-loader.test.js +56 -0
- package/dist/core/assembly/overlay-loader.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.js +1 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.d.ts +16 -0
- package/dist/e2e/project-type-overlays.test.d.ts.map +1 -0
- package/dist/e2e/project-type-overlays.test.js +834 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -0
- package/dist/types/config.d.ts +19 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/wizard/questions.d.ts +27 -1
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +142 -3
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +206 -8
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.d.ts +21 -0
- package/dist/wizard/wizard.d.ts.map +1 -1
- package/dist/wizard/wizard.js +27 -1
- package/dist/wizard/wizard.js.map +1 -1
- package/package.json +1 -1
- package/dist/types/wizard.d.ts +0 -14
- package/dist/types/wizard.d.ts.map +0 -1
- package/dist/types/wizard.js +0 -2
- package/dist/types/wizard.js.map +0 -1
|
@@ -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.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: library-bundling
|
|
3
|
+
description: ESM/CJS dual publishing, package.json exports map, bundler configuration, and tree-shaking verification for libraries
|
|
4
|
+
topics: [library, bundling, esm, cjs, dual-publishing, exports-map, tree-shaking, tsup, rollup]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Library bundling solves the problem of serving multiple module systems from one codebase. The JavaScript ecosystem is mid-transition from CommonJS to ES modules, and libraries must serve both until the transition completes. Getting bundling wrong produces libraries that fail to import in certain environments, cause dual-package hazards (two instances of the same library loaded simultaneously), or defeat tree-shaking and inflate consumer bundle sizes.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Use a bundler (tsup or rollup) rather than raw TypeScript compilation for libraries that need dual ESM/CJS output. The `package.json` exports map is the canonical module resolution contract — define it precisely with condition precedence (types before default, import before require). Set `"sideEffects": false` when true to enable aggressive tree-shaking. Test module resolution in real consumer environments, not just in your build output. ESM-only is acceptable if your minimum supported environment supports it; document this clearly.
|
|
12
|
+
|
|
13
|
+
Key bundling decisions:
|
|
14
|
+
- Output formats: ESM + CJS for maximum compatibility; ESM-only for modern toolchains
|
|
15
|
+
- File extensions: `.js`/`.cjs` to signal format explicitly
|
|
16
|
+
- Declaration files: emitted alongside each output, not separately
|
|
17
|
+
- Exports map: precise condition ordering (types, import, require, default)
|
|
18
|
+
- Tree-shaking: `sideEffects: false` + ES module output + no barrel-file anti-patterns
|
|
19
|
+
|
|
20
|
+
## Deep Guidance
|
|
21
|
+
|
|
22
|
+
### Choosing a Bundler
|
|
23
|
+
|
|
24
|
+
**tsup (recommended for most libraries):**
|
|
25
|
+
tsup is a TypeScript-first bundler built on esbuild. Fast, opinionated, handles dual ESM/CJS output with declaration files:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// tsup.config.ts
|
|
29
|
+
import { defineConfig } from 'tsup'
|
|
30
|
+
|
|
31
|
+
export default defineConfig({
|
|
32
|
+
entry: ['src/index.ts'],
|
|
33
|
+
format: ['esm', 'cjs'],
|
|
34
|
+
dts: true, // Emit .d.ts declaration files
|
|
35
|
+
sourcemap: true,
|
|
36
|
+
clean: true, // Clean dist/ before each build
|
|
37
|
+
splitting: false, // Keep single output file per format
|
|
38
|
+
treeshake: true,
|
|
39
|
+
outExtension({ format }) {
|
|
40
|
+
return {
|
|
41
|
+
js: format === 'cjs' ? '.cjs' : '.js'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Output:
|
|
48
|
+
```
|
|
49
|
+
dist/
|
|
50
|
+
├── index.js # ESM
|
|
51
|
+
├── index.cjs # CJS
|
|
52
|
+
├── index.d.ts # TypeScript declarations
|
|
53
|
+
└── index.d.cts # CJS declarations (tsup generates automatically)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**rollup (when you need fine-grained control):**
|
|
57
|
+
```javascript
|
|
58
|
+
// rollup.config.mjs
|
|
59
|
+
import typescript from '@rollup/plugin-typescript'
|
|
60
|
+
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
|
61
|
+
|
|
62
|
+
export default [
|
|
63
|
+
{
|
|
64
|
+
input: 'src/index.ts',
|
|
65
|
+
output: { file: 'dist/index.js', format: 'es', sourcemap: true },
|
|
66
|
+
plugins: [nodeResolve(), typescript({ declaration: false })]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
input: 'src/index.ts',
|
|
70
|
+
output: { file: 'dist/index.cjs', format: 'cjs', sourcemap: true, exports: 'named' },
|
|
71
|
+
plugins: [nodeResolve(), typescript({ declaration: false })]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**tsc only (when bundling is unnecessary):**
|
|
77
|
+
If the library has no dependencies to bundle, raw `tsc` with separate CJS and ESM configs works. Use this for pure type libraries or libraries where consumers handle bundling:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# ESM
|
|
81
|
+
tsc -p tsconfig.json
|
|
82
|
+
|
|
83
|
+
# CJS (separate tsconfig)
|
|
84
|
+
tsc -p tsconfig.cjs.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Exports Map Configuration
|
|
88
|
+
|
|
89
|
+
The `exports` field in `package.json` is the definitive module resolution spec for Node.js 12+ and modern bundlers. Define it precisely:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"exports": {
|
|
94
|
+
".": {
|
|
95
|
+
"import": {
|
|
96
|
+
"types": "./dist/index.d.ts",
|
|
97
|
+
"default": "./dist/index.js"
|
|
98
|
+
},
|
|
99
|
+
"require": {
|
|
100
|
+
"types": "./dist/index.d.cts",
|
|
101
|
+
"default": "./dist/index.cjs"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"./plugins": {
|
|
105
|
+
"import": {
|
|
106
|
+
"types": "./dist/plugins/index.d.ts",
|
|
107
|
+
"default": "./dist/plugins/index.js"
|
|
108
|
+
},
|
|
109
|
+
"require": {
|
|
110
|
+
"types": "./dist/plugins/index.d.cts",
|
|
111
|
+
"default": "./dist/plugins/index.cjs"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"./package.json": "./package.json"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Condition ordering matters:**
|
|
120
|
+
- `types` must come before `default` so TypeScript resolves declarations correctly
|
|
121
|
+
- `import` before `require` (ESM preferred when both are available)
|
|
122
|
+
- `default` as the final fallback
|
|
123
|
+
|
|
124
|
+
**Legacy fields for older tooling:**
|
|
125
|
+
Keep `"main"` and `"module"` for older bundlers and Node versions that don't support `exports`:
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"main": "./dist/index.cjs",
|
|
129
|
+
"module": "./dist/index.js",
|
|
130
|
+
"types": "./dist/index.d.ts"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Bundlers like webpack 4 and older rollup configurations use `"module"` for ESM. Modern tooling uses `exports`.
|
|
135
|
+
|
|
136
|
+
### Dual Package Hazard
|
|
137
|
+
|
|
138
|
+
The dual package hazard occurs when both ESM and CJS versions of the same library are loaded in the same process, creating two instances of what should be a singleton. Symptoms: `instanceof` checks fail, shared state doesn't sync, plugin registrations disappear.
|
|
139
|
+
|
|
140
|
+
**Prevention strategies:**
|
|
141
|
+
|
|
142
|
+
1. **State in the ESM version only:**
|
|
143
|
+
```javascript
|
|
144
|
+
// dist/index.cjs — CJS wrapper that re-exports the ESM version
|
|
145
|
+
// This ensures only one module instance regardless of import style
|
|
146
|
+
const mod = await import('./index.js')
|
|
147
|
+
module.exports = mod
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
2. **Stateless library design (best):**
|
|
151
|
+
Design the library with no module-level state. Factory functions create instances; there is no singleton. With no shared state, dual loading is harmless:
|
|
152
|
+
```typescript
|
|
153
|
+
// NO module-level state — safe for dual loading
|
|
154
|
+
export function createCache(): Cache { return new Map() }
|
|
155
|
+
export function parseConfig(input: string): Config { /* pure function */ }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
3. **Wrapper CJS file:**
|
|
159
|
+
```javascript
|
|
160
|
+
// dist/index.cjs — thin CJS wrapper
|
|
161
|
+
'use strict'
|
|
162
|
+
const mod = require('./index.js') // This won't work if index.js is ESM
|
|
163
|
+
// Use a proper wrapper instead:
|
|
164
|
+
Object.assign(exports, require('./index-cjs-impl.cjs'))
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
For complex libraries with state, use approach 1 or design as approach 2.
|
|
168
|
+
|
|
169
|
+
### Tree-Shaking Verification
|
|
170
|
+
|
|
171
|
+
After building, verify that tree-shaking actually works:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Install a fresh consumer project and import one function
|
|
175
|
+
mkdir /tmp/tree-shake-test && cd /tmp/tree-shake-test
|
|
176
|
+
npm init -y
|
|
177
|
+
npm install my-library@file:/path/to/library
|
|
178
|
+
|
|
179
|
+
cat > index.js << 'EOF'
|
|
180
|
+
import { parseConfig } from 'my-library'
|
|
181
|
+
const config = parseConfig('[server]\nhost = "localhost"')
|
|
182
|
+
console.log(config)
|
|
183
|
+
EOF
|
|
184
|
+
|
|
185
|
+
# Bundle with rollup and check output size
|
|
186
|
+
npx rollup index.js --format iife --bundle > bundle.js
|
|
187
|
+
wc -c bundle.js
|
|
188
|
+
|
|
189
|
+
# If the bundle is larger than just parseConfig + its dependencies,
|
|
190
|
+
# tree-shaking is not working — investigate the sideEffects field
|
|
191
|
+
# and ES module output
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Common tree-shaking failures:**
|
|
195
|
+
- CommonJS output used (bundler can't statically analyze)
|
|
196
|
+
- `sideEffects: true` in package.json (prevents dead code elimination)
|
|
197
|
+
- Barrel files that import everything (forces all code into bundle)
|
|
198
|
+
- `export * from './large-module'` at root when consumers only use one export
|
|
199
|
+
|
|
200
|
+
**Subpath exports enable opt-in tree-shaking at the feature level:**
|
|
201
|
+
```typescript
|
|
202
|
+
// Consumer only needs the validator — zero parser code in bundle
|
|
203
|
+
import { validateSchema } from 'my-library/validators'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Bundle Size Budgets
|
|
207
|
+
|
|
208
|
+
Define a bundle size budget for browser-targeted libraries:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
// package.json
|
|
212
|
+
{
|
|
213
|
+
"size-limit": [
|
|
214
|
+
{
|
|
215
|
+
"path": "./dist/index.js",
|
|
216
|
+
"limit": "10 kB"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"path": "./dist/plugins/index.js",
|
|
220
|
+
"limit": "5 kB"
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Check with size-limit
|
|
228
|
+
npx size-limit
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Enforce in CI: if a PR increases bundle size beyond the budget, fail the check. This prevents gradual size bloat.
|
|
232
|
+
|
|
233
|
+
### Source Maps
|
|
234
|
+
|
|
235
|
+
Always emit source maps. They enable consumers to debug into the library source when troubleshooting:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// tsup.config.ts
|
|
239
|
+
export default defineConfig({
|
|
240
|
+
sourcemap: true, // Emits .js.map alongside .js
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Source maps should be included in the published package (`dist/*.map`). They don't significantly affect install size but dramatically improve debugging.
|