monorepo-config 0.0.1

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 ADDED
@@ -0,0 +1,71 @@
1
+ # monorepo-config
2
+
3
+ Javascript package to help with managing configs, primarily for monorepos. Uses zod to validate.
4
+
5
+ ## Basic usage
6
+
7
+ ```ts
8
+ import { makeConfig, setConfig } from "monorepo-config";
9
+
10
+ export const CONFIG = makeConfig(z.object({
11
+ foobar: z.string(),
12
+ }));
13
+
14
+ setConfig(CONFIG, { foobar: 'test' });
15
+
16
+ CONFIG.foobar // --> 'test';
17
+ ```
18
+
19
+ Basic functionality is not much more than running `.parse` on your schema, some error reporting and being a bit smart regarding defaults. You can only `setConfig` once, otherwise use `forceOverrideConfig`.
20
+
21
+
22
+ ## File-based usage
23
+
24
+ This is more interesting, and allows you to replace the mess of untyped, unchecked env vars across packages with something more manageable (inspired by [Django](https://docs.djangoproject.com/en/6.0/topics/settings/)).
25
+
26
+ Create a directory `src/configs`. Then create a file `src/configs/schema.ts`:
27
+
28
+ ```ts
29
+ import { makeFileConfig } from "monorepo-config";
30
+
31
+ export const FOO_CONFIG = makeFileConfig({
32
+ envVar: 'FOO_CONFIG',
33
+ directory: import.meta.dirname,
34
+ schema: z.object({
35
+ foo: z.string()
36
+ })
37
+ });
38
+ ```
39
+
40
+ For simplicity, the name of the environment variable and the config object are the same. Now you can define "profiles" in the same directory, like `src/configs/production.ts`:
41
+
42
+ ```ts
43
+ import { setConfig } from "monorepo-config";
44
+ import { FOO_CONFIG } from "./schema.js";
45
+
46
+ setConfig(FOO_CONFIG, {
47
+ foo: 'bar'
48
+ });
49
+ ```
50
+
51
+ Lastly, load the config file based on the environment variable `FOO_CONFIG` in a file `src/configs/config.ts` (or some other file that initializes global variables):
52
+
53
+ ```ts
54
+ import { loadFileIntoConfig } from "monorepo-config";
55
+ import { FOO_CONFIG } from "./schema.js";
56
+
57
+ await loadFileIntoConfig(FOO_CONFIG);
58
+ ```
59
+
60
+ The idea is to use the same environment variable for all packages in your monorepo (this mirrors `DJANGO_SETTINGS_MODULE`).
61
+
62
+
63
+ ## Which to use?
64
+
65
+ You can mix both in your monorepo! Packages representing reusable libraries probably want to stick with the basic usage. Packages that represent apps probably want to define their own config in a file-based manner. In such cases, you are expected to do multiple `setConfig` calls in a "profile": one for the app itself and then multiple `setConfig` calls for the internal libraries the app uses.
66
+
67
+
68
+ ## TODO
69
+
70
+ - [ ] Vite plugin
71
+ - [ ] Explore usage in tests
@@ -0,0 +1,11 @@
1
+ import { setConfig } from "../dist/index.js";
2
+ import { FOO_CONFIG } from "./schema.js";
3
+ import { BAR_CONFIG } from "./schema2.js";
4
+
5
+ setConfig(FOO_CONFIG, {
6
+ foo: 'foooo'
7
+ });
8
+
9
+ setConfig(BAR_CONFIG, {
10
+ bar: 'baaaar'
11
+ });
@@ -0,0 +1,12 @@
1
+
2
+ import z from "zod";
3
+ import { makeFileConfig } from "../dist/filebased.js";
4
+
5
+ export const FOO_CONFIG = makeFileConfig({
6
+ envVar: 'FOO_CONFIG',
7
+ directory: import.meta.dirname,
8
+ schema: z.object({
9
+ foo: z.string()
10
+ })
11
+ });
12
+
@@ -0,0 +1,11 @@
1
+
2
+ import z from "zod";
3
+ import { makeConfig } from "../dist/index.js";
4
+
5
+ export const BAR_CONFIG = makeConfig(z.object({
6
+ bar: z.string().default('this is default'),
7
+ optionalBar: z.string().optional(),
8
+ }));
9
+
10
+ console.assert(BAR_CONFIG.bar === 'this is default', 'bar default');
11
+ console.assert(BAR_CONFIG.optionalBar === undefined, 'bar optional');
@@ -0,0 +1,23 @@
1
+
2
+ import { loadFileIntoConfig } from "../dist/filebased.js";
3
+ import { ConfigError, setConfig } from "../dist/index.js";
4
+ import { FOO_CONFIG } from "./schema.js";
5
+ import { BAR_CONFIG } from "./schema2.js";
6
+
7
+ await loadFileIntoConfig(FOO_CONFIG);
8
+
9
+ console.assert(FOO_CONFIG.foo === 'foooo', 'foo');
10
+ console.assert(BAR_CONFIG.bar === 'baaaar', 'bar');
11
+
12
+ let hadError = false;
13
+ try {
14
+ setConfig(FOO_CONFIG, {
15
+ foo: 'boo'
16
+ });
17
+ } catch (e) {
18
+ hadError = true;
19
+ console.assert(e instanceof ConfigError, `error correct type: ${e}`);
20
+ }
21
+ console.assert(hadError, 'had error');
22
+
23
+ console.log("no failures? everything is ok!");
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "monorepo-config",
3
+ "version": "0.0.1",
4
+ "description": "Managed and validated configs across packages (no deps, but bring your own zod)",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "keywords": [],
8
+ "author": "Evert Heylen",
9
+ "license": "MIT",
10
+ "devDependencies": {
11
+ "@types/node": "^25.0.3",
12
+ "typescript": "^5.9.3",
13
+ "zod": "^4.3.5"
14
+ },
15
+ "exports": {
16
+ "./*": "./dist/*"
17
+ },
18
+ "scripts": {
19
+ "build": "rm -rf dist && tsc",
20
+ "test": "vitest run"
21
+ }
22
+ }
@@ -0,0 +1,2 @@
1
+ onlyBuiltDependencies:
2
+ - esbuild
package/src/basic.ts ADDED
@@ -0,0 +1,67 @@
1
+ import type { ZodObject, input, output } from "zod";
2
+
3
+ export class ConfigError extends Error { }
4
+
5
+ export type Config<ConfigSchema extends ZodObject> = {
6
+ _meta: {
7
+ isUsable: boolean,
8
+ canSet: boolean,
9
+ schema: ConfigSchema,
10
+ }
11
+ } & output<ConfigSchema>;
12
+
13
+ function wrapInProxy<ConfigSchema extends ZodObject>(obj: Config<ConfigSchema>) {
14
+ return new Proxy(obj, {
15
+ get(target, prop, receiver) {
16
+ if (prop === '_meta') {
17
+ return target._meta;
18
+ }
19
+ if (!target._meta.isUsable) {
20
+ throw new Error("Tried to access config but it is not usable yet. Did you forget to set or load the config?");
21
+ }
22
+ return (target as any)[prop];
23
+ }
24
+ });
25
+ }
26
+
27
+ export function makeConfig<ConfigSchema extends ZodObject>(schema: ConfigSchema): Config<ConfigSchema> {
28
+ const zodDefaults = schema.safeParse({});
29
+
30
+ if (zodDefaults.success) {
31
+ // all attributes had defaults
32
+ return wrapInProxy({
33
+ _meta: {
34
+ schema: schema,
35
+ isUsable: true,
36
+ canSet: true,
37
+ },
38
+ ...zodDefaults.data
39
+ });
40
+ } else {
41
+ // zod couldn't handle an empty object
42
+ // @ts-expect-error for simplicity, the types assume that config is always defined.
43
+ return wrapInProxy({
44
+ _meta: {
45
+ schema: schema,
46
+ isUsable: false,
47
+ canSet: true,
48
+ }
49
+ });
50
+ }
51
+ }
52
+
53
+ export function forceOverrideConfig<ConfigSchema extends ZodObject>(config: Config<ConfigSchema>, inputData: input<ConfigSchema>) {
54
+ const outputData = config._meta.schema.parse(inputData);
55
+ Object.assign(config, outputData);
56
+ config._meta.isUsable = true;
57
+ config._meta.canSet = false;
58
+ }
59
+
60
+ export function setConfig<ConfigSchema extends ZodObject>(config: Config<ConfigSchema>, inputData: input<ConfigSchema>) {
61
+ if (config._meta.canSet) {
62
+ forceOverrideConfig(config, inputData);
63
+ } else {
64
+ throw new ConfigError("Can't override config as it is already set");
65
+ }
66
+ }
67
+
@@ -0,0 +1,45 @@
1
+ import type { ZodObject } from "zod";
2
+ import { Config, ConfigError, makeConfig } from "./basic.js";
3
+ import { join } from "path";
4
+
5
+ export type FileConfig<ConfigSchema extends ZodObject> = Config<ConfigSchema> & {
6
+ _meta: {
7
+ configDir: string;
8
+ envVar: string;
9
+ }
10
+ };
11
+
12
+ export function makeFileConfig<ConfigSchema extends ZodObject>(opts: {
13
+ envVar: string, directory: string, schema: ConfigSchema, preventAutoLoad?: boolean
14
+ }): FileConfig<ConfigSchema> {
15
+ const cfg = makeConfig(opts.schema) as FileConfig<ConfigSchema>;
16
+ // imperative overrides here to not lose the Proxy
17
+ cfg._meta.configDir = opts.directory;
18
+ cfg._meta.envVar = opts.envVar;
19
+
20
+ if (!opts.preventAutoLoad) {
21
+ queueMicrotask(async () => {
22
+ try {
23
+ await loadFileIntoConfig(cfg);
24
+ } catch (e) {
25
+ console.error(e);
26
+ process.exit(1);
27
+ }
28
+ });
29
+ }
30
+
31
+ return cfg;
32
+ }
33
+
34
+ export async function loadFileIntoConfig<ConfigSchema extends ZodObject>(config: FileConfig<ConfigSchema>) {
35
+ const config_name = process.env[config._meta.envVar];
36
+ if (config_name === undefined) {
37
+ throw new ConfigError(`Please set the environment variable ${config._meta.envVar}`);
38
+ }
39
+ const path = join(config._meta.configDir, `${config_name}.js`);
40
+ await import(path);
41
+ if (!config._meta.isUsable) {
42
+ console.warn(`Imported ${path} but config was still not marked as usable`);
43
+ }
44
+ }
45
+
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { makeConfig, forceOverrideConfig, setConfig } from "./basic.js";
2
+ export { makeFileConfig, loadFileIntoConfig } from "./filebased.js";
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "es2024",
5
+ "ESNext.Array",
6
+ "ESNext.Collection",
7
+ "ESNext.Iterator",
8
+ "ESNext.Promise"
9
+ ],
10
+ "module": "nodenext",
11
+ "target": "es2024",
12
+
13
+ "strict": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "moduleResolution": "node16",
17
+
18
+ "declaration": true,
19
+ "sourceMap": true,
20
+ "declarationMap": true,
21
+
22
+ "outDir": "./dist",
23
+ "rootDir": "./src"
24
+ },
25
+ "include": ["src/**/*", "__test__"],
26
+ "exclude": ["node_modules", "dist"]
27
+ }