Skip to content

Commit 90fe732

Browse files
authored
Merge pull request #97 from constructive-io/feat/confstash
feat: confstash — standardized project config loading (discovery, extends/presets, layered merge with provenance)
2 parents eea0823 + c3832e9 commit 90fe732

16 files changed

Lines changed: 3926 additions & 5234 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ jobs:
4848
package:
4949
- appstash
5050
- clean-ansi
51+
- confstash
5152
- fetch-api-client
5253
- find-and-require-package-json
5354
- http-errors

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ A comprehensive collection of TypeScript utilities for working with schemas, JSO
2121
|---------|-----|--------|-------------|
2222
| **appstash** | [![npm](https://img.shields.io/npm/v/appstash.svg)](https://www.npmjs.com/package/appstash) | [GitHub](./packages/appstash) | Simple, clean application directory resolution |
2323
| **clean-ansi** | [![npm](https://img.shields.io/npm/v/clean-ansi.svg)](https://www.npmjs.com/package/clean-ansi) | [GitHub](./packages/clean-ansi) | Remove ANSI escape codes from strings |
24+
| **confstash** | [![npm](https://img.shields.io/npm/v/confstash.svg)](https://www.npmjs.com/package/confstash) | [GitHub](./packages/confstash) | Standardized project configuration loading — config files, rc files, presets, and layered merge with provenance |
2425
| **create-gen-app** | [![npm](https://img.shields.io/npm/v/create-gen-app.svg)](https://www.npmjs.com/package/create-gen-app) | [GitHub](./packages/create-gen-app) | Clone and customize template repositories with variable replacement |
2526
| **inquirerer** | [![npm](https://img.shields.io/npm/v/inquirerer.svg)](https://www.npmjs.com/package/inquirerer) | [GitHub](./packages/inquirerer) | TypeScript-first library for building beautiful CLI interfaces with interactive prompts |
2627
| **jsonldjs** | [![npm](https://img.shields.io/npm/v/jsonldjs.svg)](https://www.npmjs.com/package/jsonldjs) | [GitHub](./packages/jsonld-tools) | Powerful JSON-LD builder with comprehensive filtering and subgraph extraction |

packages/confstash/README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# confstash
2+
3+
<p align="center">
4+
<img src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" height="250">
5+
<br />
6+
<strong>standardized project configuration loading</strong>
7+
<br />
8+
<br />
9+
Config files, rc files, presets, and layered merge with provenance
10+
<br />
11+
<br />
12+
<a href="https://github.com/constructive-io/dev-utils/actions/workflows/ci.yml">
13+
<img height="20" src="https://github.com/constructive-io/dev-utils/actions/workflows/ci.yml/badge.svg" />
14+
</a>
15+
<a href="https://github.com/constructive-io/dev-utils/blob/main/LICENSE">
16+
<img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
17+
</a>
18+
</p>
19+
20+
Standardized project configuration loading for CLI tools — the project-level complement to [`appstash`](../appstash) (user-level directories).
21+
22+
Give it a tool name and it discovers `mytool.config.{ts,js,mjs,cjs}`, `.mytoolrc{,.json,.yaml,.yml,.js}`, `mytool.json`, and `package.json` keys via walk-up search, resolves `extends`/preset chains, and merges layered configuration with per-key provenance:
23+
24+
```
25+
defaults -> presets (extends) -> user stash -> project file -> env vars -> runtime overrides
26+
```
27+
28+
## Installation
29+
30+
```bash
31+
npm install confstash
32+
```
33+
34+
## Features
35+
36+
- **Walk-up discovery**: finds config in the current directory or any parent
37+
- **Every format**: `.config.ts/js/mjs/cjs` modules, rc files (JSON/YAML), JSON, `package.json` keys
38+
- **Presets & extends**: named presets, relative paths, or npm packages — recursively, with cycle detection
39+
- **Layered merge**: deterministic precedence with `replace` or `concat` array strategies
40+
- **Provenance**: `explainSync()` tells you which layer supplied every value (`--print-config` UX)
41+
- **Sync and async**: fully synchronous path for CLIs (ESM configs require `load()`)
42+
- **User layer**: optional `~/.<tool>/config/config.json` layer via appstash
43+
- **Typed**: `defineConfig<T>()` + generic loader for full type safety
44+
45+
## Usage
46+
47+
### Basic Usage
48+
49+
```typescript
50+
import { createConfigLoader } from 'confstash';
51+
52+
interface MyConfig {
53+
level: string;
54+
rules: Record<string, string>;
55+
}
56+
57+
const loader = createConfigLoader<MyConfig>({
58+
tool: 'mytool',
59+
defaults: { level: 'medium', rules: {} }
60+
});
61+
62+
const { config, filepath, layers, isEmpty } = loader.loadSync();
63+
// or: await loader.load() — also supports .mjs / ESM configs
64+
```
65+
66+
### Presets and extends
67+
68+
```typescript
69+
const loader = createConfigLoader<MyConfig>({
70+
tool: 'mytool',
71+
presets: {
72+
'mytool:recommended': { level: 'medium', rules: { A1: 'error' } },
73+
'mytool:strict': { extends: 'mytool:recommended', level: 'high' }
74+
}
75+
});
76+
```
77+
78+
```jsonc
79+
// .mytoolrc.json
80+
{
81+
"extends": "mytool:strict", // or "./team-preset.js", or "some-npm-pkg"
82+
"rules": { "A1": "off" }
83+
}
84+
```
85+
86+
### Typed config authoring
87+
88+
```typescript
89+
// mytool.config.ts
90+
import { defineConfig } from 'confstash';
91+
92+
export default defineConfig({
93+
extends: 'mytool:recommended',
94+
level: 'high'
95+
});
96+
```
97+
98+
### Environment and CLI layers
99+
100+
```typescript
101+
const loader = createConfigLoader<MyConfig>({
102+
tool: 'mytool',
103+
envLayer: (env) => (env.MYTOOL_LEVEL ? { level: env.MYTOOL_LEVEL } : {}),
104+
userStash: true // include ~/.mytool/config/config.json (via appstash)
105+
});
106+
107+
const { config } = loader.loadSync({
108+
overrides: parsedCliFlags // highest precedence
109+
});
110+
```
111+
112+
### Provenance / print-config
113+
114+
```typescript
115+
for (const entry of loader.explainSync()) {
116+
console.log(`${entry.path} = ${JSON.stringify(entry.value)} (${entry.source}: ${entry.origin})`);
117+
}
118+
// level = "high" (file: /repo/mytool.config.ts)
119+
// rules.A1 = "error" (preset: mytool:recommended)
120+
```
121+
122+
### Custom search places
123+
124+
```typescript
125+
// pgpm-compatible discovery
126+
const loader = createConfigLoader({
127+
tool: 'pgpm',
128+
searchPlaces: ['pgpm.config.js', 'pgpm.json'],
129+
arrayMerge: 'replace'
130+
});
131+
```
132+
133+
## API
134+
135+
### `createConfigLoader<T>(options)`
136+
137+
**Options:**
138+
- `tool` (string): tool name; drives default search places and the user stash directory
139+
- `searchPlaces` (SearchPlace[]): filenames or `{ packageJson: key }` entries, in precedence order
140+
- `defaults` (Partial<T>): lowest-precedence layer
141+
- `presets` (Record<string, Partial<T>>): named presets resolvable via `extends`
142+
- `envLayer` ((env) => Partial<T>): map environment variables into a layer
143+
- `userStash` (boolean): include `~/.<tool>/config/config.json` (default `false`)
144+
- `arrayMerge` (`'replace' | 'concat'`): array strategy (default `'replace'`)
145+
- `validate` ((config: T) => T | void): validate/normalize the merged result
146+
- `walkUp` (boolean): search parent directories (default `true`)
147+
148+
**Returns:** `ConfigLoader<T>` with `load(params)`, `loadSync(params)`, `explainSync(params)`, `searchPlaces`.
149+
150+
**Load params:** `cwd`, `overrides`, `configFile` (skip discovery), `env`.
151+
152+
### Utilities
153+
154+
- `defineConfig<T>(config)` — identity helper for typed config files
155+
- `defaultSearchPlaces(tool)` — the derived search place list
156+
- `findConfigSync(startDir, searchPlaces, walkUp?)` / `findUpDir(startDir, filename)` — discovery primitives
157+
- `loadFileSync(found)` / `loadFile(found)` — format-aware file loading
158+
- `deepMerge(target, source, arrayMerge?)` / `mergeLayers(layers)` / `explainLayers(layers)` — merge primitives
159+
- `ConfigLoadError` — thrown for unreadable/invalid config files
160+
161+
## Related
162+
163+
- [`appstash`](../appstash) — user-level directories (`~/.<tool>/{config,cache,data,logs}`) and the optional `userStash` layer

0 commit comments

Comments
 (0)