@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,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli-shell-integration
|
|
3
|
+
description: Shell completion generation for bash/zsh/fish, man page generation, dotfile conventions, PATH management, and shell aliases
|
|
4
|
+
topics: [cli, shell-integration, completion, man-pages, dotfiles, path-management, aliases]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Shell integration is the difference between a CLI that feels native and one that feels like a foreign object. Completion, man pages, and dotfile patterns are not optional polish — they are the features that determine whether power users adopt the tool or abandon it for something that respects their shell workflow.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Shell integration — completion scripts, man pages, dotfile modifications, and PATH management — determines whether power users adopt the tool or abandon it. Generate completion scripts from the CLI definition for bash, zsh, and fish. Provide an `install-completions` subcommand. When modifying shell startup files, use clearly marked comment blocks and always provide an uninstall command.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### Shell Completion Generation
|
|
16
|
+
|
|
17
|
+
Provide completion scripts for bash, zsh, and fish. Generate them from the CLI definition rather than hand-writing them — they will stay in sync as commands and flags evolve:
|
|
18
|
+
|
|
19
|
+
**Node.js (yargs)**
|
|
20
|
+
```bash
|
|
21
|
+
my-cli completion # Outputs bash/zsh completion script
|
|
22
|
+
my-cli completion >> ~/.bashrc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Cobra (Go)**
|
|
26
|
+
```bash
|
|
27
|
+
my-cli completion bash # bash
|
|
28
|
+
my-cli completion zsh # zsh
|
|
29
|
+
my-cli completion fish # fish
|
|
30
|
+
my-cli completion powershell
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**clap (Rust)** with `clap_complete`:
|
|
34
|
+
```rust
|
|
35
|
+
// Generate at build time and install to /usr/local/share/bash-completion/completions/
|
|
36
|
+
generate(Shell::Bash, &mut cmd, "my-cli", &mut io::stdout());
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**click (Python)**: Use `click-completion` or the built-in `shell_complete` parameter.
|
|
40
|
+
|
|
41
|
+
Completion installation patterns:
|
|
42
|
+
- **bash**: Source from `~/.bashrc` or drop in `/etc/bash_completion.d/` or `~/.bash_completion.d/`
|
|
43
|
+
- **zsh**: Drop in `$fpath` directory, e.g., `/usr/local/share/zsh/site-functions/_my-cli`
|
|
44
|
+
- **fish**: Drop in `~/.config/fish/completions/my-cli.fish`
|
|
45
|
+
|
|
46
|
+
Provide a `my-cli install-completions` subcommand that writes the correct file for the detected shell. Always print the path written and what the user must do to activate it (e.g., restart shell or run `source ~/.bashrc`).
|
|
47
|
+
|
|
48
|
+
### Man Page Generation
|
|
49
|
+
|
|
50
|
+
Man pages are the canonical reference documentation. Generate them from the CLI definition:
|
|
51
|
+
|
|
52
|
+
**Go (cobra)**: `cobra-man` generates man pages from Cobra commands.
|
|
53
|
+
|
|
54
|
+
**Node.js**: `marked-man` converts Markdown to man format. `ronn` (Ruby) converts richly formatted Markdown to man.
|
|
55
|
+
|
|
56
|
+
**Rust (clap)**: `clap_mangen` generates man pages from clap definitions.
|
|
57
|
+
|
|
58
|
+
Man page installation:
|
|
59
|
+
- System-wide: `/usr/local/share/man/man1/my-cli.1`
|
|
60
|
+
- User-local: `~/.local/share/man/man1/my-cli.1`
|
|
61
|
+
- Run `mandb` (Linux) or ensure `$MANPATH` includes the directory
|
|
62
|
+
|
|
63
|
+
Homebrew formulae automatically install man pages if placed in `share/man/man1/`. For npm packages, include a `man` field in `package.json`.
|
|
64
|
+
|
|
65
|
+
### Dotfile Conventions
|
|
66
|
+
|
|
67
|
+
When the CLI modifies shell startup files (`~/.bashrc`, `~/.zshrc`), follow these rules:
|
|
68
|
+
|
|
69
|
+
- Never overwrite startup files — append only
|
|
70
|
+
- Use clearly marked comment blocks:
|
|
71
|
+
```bash
|
|
72
|
+
# >>> my-cli init >>>
|
|
73
|
+
export PATH="$HOME/.my-cli/bin:$PATH"
|
|
74
|
+
eval "$(my-cli shell-init)"
|
|
75
|
+
# <<< my-cli init <<<
|
|
76
|
+
```
|
|
77
|
+
- The markers enable the tool to detect existing installation and idempotently update the block
|
|
78
|
+
- Always provide `my-cli uninstall` or `my-cli shell-remove` that removes exactly these markers
|
|
79
|
+
- Never add content outside the marked block
|
|
80
|
+
|
|
81
|
+
On first install, detect which shell config files exist and are writable: check `$SHELL`, then look for `~/.zshrc`, `~/.bashrc`, `~/.bash_profile` in that order.
|
|
82
|
+
|
|
83
|
+
### PATH Management
|
|
84
|
+
|
|
85
|
+
When the tool installs binaries or shims to a user-local directory:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Typical user-local bin directory
|
|
89
|
+
~/.local/bin/ # XDG standard (Linux/macOS)
|
|
90
|
+
~/.my-cli/bin/ # Tool-specific (when multiple versions coexist)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Check if the directory is already on `$PATH` before suggesting the user add it. If not on `$PATH`, print the exact line to add and which file to add it to:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Add this to ~/.zshrc:
|
|
97
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Do not silently modify PATH for the current process and assume it persists — shell environment changes only persist through startup file modification or explicit user action.
|
|
101
|
+
|
|
102
|
+
### Shell Aliases
|
|
103
|
+
|
|
104
|
+
Provide a mechanism to generate useful shell aliases for common command combinations:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# my-cli generates aliases for common workflows
|
|
108
|
+
my-cli alias generate >> ~/.zshrc
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Aliases should be documented and user-editable. Never require aliases for core functionality — they are convenience, not architecture.
|
|
112
|
+
|
|
113
|
+
### Shell Detection
|
|
114
|
+
|
|
115
|
+
Detect the user's shell reliably:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Most reliable: check $SHELL environment variable
|
|
119
|
+
SHELL_NAME=$(basename "$SHELL")
|
|
120
|
+
|
|
121
|
+
# Fallback: check running process
|
|
122
|
+
ps -p $PPID -o comm= | sed 's/^-//'
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Map shell name to config file:
|
|
126
|
+
- `zsh` → `~/.zshrc`
|
|
127
|
+
- `bash` → `~/.bashrc` (Linux) or `~/.bash_profile` (macOS interactive login shells)
|
|
128
|
+
- `fish` → `~/.config/fish/config.fish`
|
|
129
|
+
|
|
130
|
+
When the shell cannot be detected, print instructions for all three common shells and let the user pick. Never guess and silently write to the wrong file.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cli-testing
|
|
3
|
+
description: CLI integration testing by spawning processes, snapshot testing help text, mock filesystem, environment variable testing, and CI matrix testing
|
|
4
|
+
topics: [cli, testing, integration-tests, snapshot-testing, mock-filesystem, ci-matrix, exit-codes]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
CLI testing requires a different mindset than library testing. The contract being tested is behavioral: given this argv and environment, what does the tool write to stdout, what does it write to stderr, and what exit code does it return? Unit tests for business logic are necessary but not sufficient — integration tests that spawn the actual binary catch the class of bugs that only appear at the boundary between argument parsing and execution.
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
CLI testing requires spawning the actual binary and asserting on stdout, stderr, and exit code. Snapshot test help text to catch accidental regressions. Isolate filesystem tests with temporary directories and mock `$HOME`/`XDG_CONFIG_HOME`. Test across OS/runtime matrices in CI including Windows.
|
|
12
|
+
|
|
13
|
+
## Deep Guidance
|
|
14
|
+
|
|
15
|
+
### Integration Testing by Spawning the Process
|
|
16
|
+
|
|
17
|
+
The most valuable CLI test spawns the actual binary and asserts on stdout, stderr, and exit code:
|
|
18
|
+
|
|
19
|
+
**Node.js (vitest or jest)**
|
|
20
|
+
```typescript
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
|
|
23
|
+
test('build succeeds with valid input', () => {
|
|
24
|
+
const result = execSync('node bin/my-cli build --input fixture.txt', {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
env: { ...process.env, XDG_CONFIG_HOME: tmpDir }
|
|
27
|
+
});
|
|
28
|
+
expect(result).toContain('Build complete');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('exits 2 on unknown flag', () => {
|
|
32
|
+
expect(() =>
|
|
33
|
+
execSync('node bin/my-cli --unknown-flag', { encoding: 'utf8', stdio: 'pipe' })
|
|
34
|
+
).toThrow('exit code 2');
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Rust**
|
|
39
|
+
```rust
|
|
40
|
+
use assert_cmd::Command;
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn build_succeeds() {
|
|
44
|
+
Command::cargo_bin("my-cli")
|
|
45
|
+
.unwrap()
|
|
46
|
+
.arg("build")
|
|
47
|
+
.assert()
|
|
48
|
+
.success()
|
|
49
|
+
.stdout(predicates::str::contains("Build complete"));
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Bats (shell)**
|
|
54
|
+
```bash
|
|
55
|
+
@test "exits 2 on missing required argument" {
|
|
56
|
+
run my-cli deploy
|
|
57
|
+
[ "$status" -eq 2 ]
|
|
58
|
+
[[ "$output" =~ "required" ]]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Always test exit codes. Always test that error output goes to stderr. Always test the success path and the most common failure paths.
|
|
63
|
+
|
|
64
|
+
### Snapshot Testing for Help Text
|
|
65
|
+
|
|
66
|
+
Help text is part of the public API — changes should be intentional. Snapshot tests catch accidental regressions:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
test('help text matches snapshot', () => {
|
|
70
|
+
const { stdout } = execSync('node bin/my-cli --help', { encoding: 'utf8' });
|
|
71
|
+
expect(stdout).toMatchSnapshot();
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Update snapshots intentionally when help text changes. In CI, fail on unexpected snapshot drift. This also catches typos and formatting issues in help output.
|
|
76
|
+
|
|
77
|
+
### Mock Filesystem
|
|
78
|
+
|
|
79
|
+
Tests that interact with the filesystem must be isolated. Use temporary directories or a mock filesystem:
|
|
80
|
+
|
|
81
|
+
**Node.js**
|
|
82
|
+
```typescript
|
|
83
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
84
|
+
import { tmpdir } from 'os';
|
|
85
|
+
|
|
86
|
+
let tmpDir: string;
|
|
87
|
+
beforeEach(() => { tmpDir = mkdtempSync(tmpdir() + '/my-cli-test-'); });
|
|
88
|
+
afterEach(() => { rmSync(tmpDir, { recursive: true }); });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For unit tests that don't need real disk I/O, `memfs` provides an in-memory filesystem that satisfies the Node.js `fs` module interface.
|
|
92
|
+
|
|
93
|
+
**Rust**: Use `tempfile::TempDir`. Tests run in parallel by default — each test gets its own temp directory to avoid interference.
|
|
94
|
+
|
|
95
|
+
**Key rule**: Never write to `$HOME`, `~/.config`, or any real user directory in tests. Set `HOME` and `XDG_CONFIG_HOME` to the temp directory.
|
|
96
|
+
|
|
97
|
+
### Environment Variable Testing
|
|
98
|
+
|
|
99
|
+
Test behavior driven by environment variables:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
test('respects NO_COLOR', () => {
|
|
103
|
+
const result = execSync('node bin/my-cli status', {
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
env: { ...process.env, NO_COLOR: '1' }
|
|
106
|
+
});
|
|
107
|
+
// Assert no ANSI escape sequences in output
|
|
108
|
+
expect(result).not.toMatch(/\x1b\[/);
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Test the full env var precedence chain: flag overrides env var, env var overrides config file. Each level should be independently testable.
|
|
113
|
+
|
|
114
|
+
### CI Matrix Testing
|
|
115
|
+
|
|
116
|
+
Test across multiple operating systems and runtime versions:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
# GitHub Actions
|
|
120
|
+
strategy:
|
|
121
|
+
matrix:
|
|
122
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
123
|
+
node: ['18', '20', '22']
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Windows-specific concerns: path separators (`\` vs `/`), line endings (`\r\n` vs `\n`), `%APPDATA%` vs `$HOME/.config`, and `PATHEXT` for binary extension handling. Test on Windows even if you do not primarily develop on it.
|
|
127
|
+
|
|
128
|
+
### Test Pyramid for CLIs
|
|
129
|
+
|
|
130
|
+
- **Unit tests (fast)**: Business logic in `utils/`, pure functions, config parsing, output formatters
|
|
131
|
+
- **Integration tests (medium)**: Spawn the CLI process, assert stdout/stderr/exit code for key scenarios
|
|
132
|
+
- **End-to-end tests (slow, optional)**: Full workflow against real external services — run in CI on schedule, not on every PR
|
|
133
|
+
|
|
134
|
+
Keep integration tests fast by using fixtures (pre-created files) rather than generating test input dynamically. A full integration test suite should complete in under 30 seconds.
|
|
@@ -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.
|