Skip to content

Commit f4b8e1a

Browse files
committed
fix: decouple configs for this base package
1 parent 1c1fcab commit f4b8e1a

File tree

11 files changed

+450
-183
lines changed

11 files changed

+450
-183
lines changed

.eslintrc.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

.prettierrc.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

README.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Run the function as many times as you like—it is idempotent and only touches f
5454

5555
## Package-driven configuration
5656

57-
Most projects will declare their config files directly in `package.json` and call the bundled CLI as part of
57+
Most projects keep their file definitions in a dedicated module and call the bundled CLI as part of
5858
`postinstall`:
5959

6060
```json
@@ -63,18 +63,33 @@ Most projects will declare their config files directly in `package.json` and cal
6363
"scripts": {
6464
"postinstall": "cpconfig"
6565
},
66-
"cpconfig": {
67-
"files": {
68-
"config/.env.local": { "contents": "API_TOKEN=abc123" },
69-
".secrets.json": { "contents": "{\n \"key\": \"value\"\n}" }
70-
}
66+
"config": {
67+
"cpconfig": "./cpconfig.config.mjs"
7168
}
7269
}
7370
```
7471

7572
The CLI walks up from the current working directory, finds the nearest `package.json`, and reads the `cpconfig`
76-
definition (or `config.cpconfig`). Definitions can either be an object of files (as above) or an object with
77-
`{ "files": { ... }, "options": { ... } }`, mirroring the programmatic API.
73+
definition (preferring `config.cpconfig`). When the value is a string, `cpconfig` resolves and imports that module
74+
using standard Node.js resolution. The module can export either:
75+
76+
- a default or named `config` object with `{ files, options }`;
77+
- a plain object map of files;
78+
- a factory function (sync or async) returning either of the above.
79+
80+
For example, `cpconfig.config.mjs` might look like:
81+
82+
```js
83+
export default {
84+
files: {
85+
'config/.env.local': { contents: 'API_TOKEN=abc123' },
86+
'.secrets.json': { contents: '{\n "key": "value"\n}' },
87+
},
88+
};
89+
```
90+
91+
If you prefer, you can still embed the object directly in `package.json` under either `cpconfig` or `config.cpconfig`.
92+
Module exports simply provide a cleaner home for larger definitions or shared packages.
7893

7994
- Run `npx cpconfig` manually whenever you need to refresh files.
8095
- Add `cpconfig` to `postinstall` to keep developer machines in sync automatically.

eslint.config.mts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import tseslint from "typescript-eslint";
4+
import { defineConfig } from "eslint/config";
5+
6+
export default defineConfig([
7+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
8+
tseslint.configs.recommended,
9+
]);

package.json

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
"description": "A simple zero dependency utility to manage configuration files for node projects across your organization",
55
"main": "build/index.js",
66
"types": "build/index.d.ts",
7-
"bin": {
8-
"cpconfig": "build/cli.js"
9-
},
7+
"bin": "build/cli.js",
108
"author": "Developers <[email protected]>",
119
"license": "UNLICENSED",
1210
"packageManager": "[email protected]",
@@ -43,17 +41,37 @@
4341
"@semantic-release/github"
4442
]
4543
},
44+
"prettier": {
45+
"bracketSpacing": true,
46+
"bracketSameLine": true,
47+
"singleQuote": true,
48+
"trailingComma": "all",
49+
"printWidth": 100,
50+
"overrides": [
51+
{
52+
"files": "*.js",
53+
"options": {
54+
"parser": "babel"
55+
}
56+
}
57+
]
58+
},
4659
"devDependencies": {
60+
"@eslint/js": "^9.39.1",
4761
"@semantic-release/exec": "^7.1.0",
4862
"@semantic-release/github": "^12.0.0",
4963
"@types/node": "^24.9.1",
5064
"@typescript-eslint/eslint-plugin": "^8.46.2",
5165
"@typescript-eslint/parser": "^8.46.2",
52-
"eslint": "^9.38.0",
66+
"eslint": "^9.39.1",
5367
"eslint-config-prettier": "^10.1.8",
5468
"eslint-import-resolver-typescript": "^4.4.4",
5569
"eslint-plugin-import": "^2.32.0",
70+
"globals": "^16.5.0",
71+
"jiti": "^2.6.1",
72+
"prettier": "^3.6.2",
5673
"typescript": "^5.9.3",
74+
"typescript-eslint": "^8.46.3",
5775
"vitest": "^4.0.3"
5876
}
5977
}

src/cli.spec.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ describe('cli', () => {
3838
expect(stderr.toString()).toBe('');
3939

4040
await expect(readFile(path.join(cwd, 'secrets/.env'), 'utf8')).resolves.toBe('SECRET=1');
41-
await expect(readFile(path.join(cwd, 'config/app.json'), 'utf8')).resolves.toBe('{"flag":true}');
41+
await expect(readFile(path.join(cwd, 'config/app.json'), 'utf8')).resolves.toBe(
42+
'{"flag":true}',
43+
);
4244

4345
const gitignore = await readFile(path.join(cwd, '.gitignore'), 'utf8');
4446
expect(gitignore).toContain('secrets/.env');
@@ -48,6 +50,83 @@ describe('cli', () => {
4850
});
4951
});
5052

53+
test('loads configuration from a referenced module', async () => {
54+
await withTempDir(async (cwd) => {
55+
const modulePath = path.join(cwd, 'cpconfig.config.mjs');
56+
57+
await writeFile(
58+
modulePath,
59+
`export default {\n files: {\n 'module-output.txt': { contents: 'from module' }\n }\n};\n`,
60+
);
61+
62+
await writeFile(
63+
path.join(cwd, 'package.json'),
64+
JSON.stringify(
65+
{
66+
...packageTemplate,
67+
config: {
68+
cpconfig: './cpconfig.config.mjs',
69+
},
70+
},
71+
null,
72+
2,
73+
),
74+
);
75+
76+
const stdout = createBuffer();
77+
const stderr = createBuffer();
78+
79+
const exitCode = await runCli([], { cwd, stdout, stderr });
80+
81+
expect(exitCode).toBe(0);
82+
expect(stderr.toString()).toBe('');
83+
84+
await expect(readFile(path.join(cwd, 'module-output.txt'), 'utf8')).resolves.toBe(
85+
'from module',
86+
);
87+
const gitignore = await readFile(path.join(cwd, '.gitignore'), 'utf8');
88+
expect(gitignore).toContain('module-output.txt');
89+
expect(stdout.toString()).toContain('cpconfig apply');
90+
expect(stdout.toString()).toContain('cpconfig.config.mjs');
91+
});
92+
});
93+
94+
test('supports config modules exporting factories', async () => {
95+
await withTempDir(async (cwd) => {
96+
const modulePath = path.join(cwd, 'cpconfig.factory.mjs');
97+
98+
await writeFile(
99+
modulePath,
100+
`export default async function buildConfig() {\n await Promise.resolve();\n return {\n files: {\n 'factory.txt': { contents: 'from factory' }\n },\n options: {\n gitignorePath: 'generated.ignore'\n }\n };\n}\n`,
101+
);
102+
103+
await writeFile(
104+
path.join(cwd, 'package.json'),
105+
JSON.stringify(
106+
{
107+
...packageTemplate,
108+
cpconfig: './cpconfig.factory.mjs',
109+
},
110+
null,
111+
2,
112+
),
113+
);
114+
115+
const stdout = createBuffer();
116+
const stderr = createBuffer();
117+
118+
const exitCode = await runCli([], { cwd, stdout, stderr });
119+
120+
expect(exitCode).toBe(0);
121+
expect(stderr.toString()).toBe('');
122+
123+
await expect(readFile(path.join(cwd, 'factory.txt'), 'utf8')).resolves.toBe('from factory');
124+
const gitignore = await readFile(path.join(cwd, 'generated.ignore'), 'utf8');
125+
expect(gitignore).toContain('factory.txt');
126+
expect(stdout.toString()).toContain('cpconfig.factory.mjs');
127+
});
128+
});
129+
51130
test('supports dry runs without writing files', async () => {
52131
await withTempDir(async (cwd) => {
53132
await writeFile(

0 commit comments

Comments
 (0)