@aljazmc/config 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/README.md +24 -2
- package/bin/index.js +210 -0
- package/bin/types/config.d.ts +31 -0
- package/bin/types/ignore.d.ts +5 -0
- package/bin/types/includes.d.ts +6 -0
- package/bin/types/index.d.ts +4 -0
- package/bin/types/pattern.d.ts +5 -0
- package/package.json +34 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aljazmc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# > config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared, dependency-free configuration loader, file finder and glob matcher, bundled
|
|
4
|
+
into every `@aljazmc` package and published for their types.
|
|
5
|
+
|
|
6
|
+
Each package ships its defaults in `src/config.ts`. On run, `<package>.config.ts`
|
|
7
|
+
(or `.js`) in the current directory, if present, overrides them key by key.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// testero.config.js
|
|
11
|
+
export default {
|
|
12
|
+
ignore: [
|
|
13
|
+
"node_modules", // name: any file or directory called node_modules
|
|
14
|
+
"packages/testero/tests", // path: relative to root, with its contents
|
|
15
|
+
"*.log", // globs: * and ? within a segment, ** across segments, {a,b} alternatives
|
|
16
|
+
"packages/*/bin",
|
|
17
|
+
"**/tmp/*.ts",
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`config.files()` lists the files that are not ignored, and `config.sources()`
|
|
23
|
+
reads the text ones. `Includes` matches `includes` globs like formatero and lintero use them, where
|
|
24
|
+
the last matching glob decides and `!` excludes. `config.custom` holds what the
|
|
25
|
+
config file set, before the defaults were merged in.
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// shared/config/src/config.ts
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
// shared/config/src/pattern.ts
|
|
10
|
+
var Pattern = class _Pattern {
|
|
11
|
+
static {
|
|
12
|
+
__name(this, "Pattern");
|
|
13
|
+
}
|
|
14
|
+
#anchored;
|
|
15
|
+
#regex;
|
|
16
|
+
constructor(source) {
|
|
17
|
+
const glob = source.replace(/^\.?\/+|\/+$/g, "");
|
|
18
|
+
this.#anchored = glob.includes("/");
|
|
19
|
+
this.#regex = new RegExp(`^${_Pattern.#toRegex(glob)}$`);
|
|
20
|
+
}
|
|
21
|
+
matches(path) {
|
|
22
|
+
const segments = path.split("/");
|
|
23
|
+
return segments.some(
|
|
24
|
+
(segment, index) => this.#regex.test(
|
|
25
|
+
this.#anchored ? segments.slice(0, index + 1).join("/") : segment
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
/** Translates `**`, `*`, `?` and `{a,b}` alternatives, other characters match themselves. */
|
|
30
|
+
static #toRegex(glob) {
|
|
31
|
+
const tokens = {
|
|
32
|
+
"**/": "(?:.*/)?",
|
|
33
|
+
"**": ".*",
|
|
34
|
+
"*": "[^/]*",
|
|
35
|
+
"?": "[^/]"
|
|
36
|
+
};
|
|
37
|
+
let regex = "";
|
|
38
|
+
let braces = 0;
|
|
39
|
+
for (const [token] of glob.matchAll(/\*\*\/|\*\*|[\s\S]/g)) {
|
|
40
|
+
if (token in tokens) regex += tokens[token] ?? token;
|
|
41
|
+
else if (token === "{") {
|
|
42
|
+
braces++;
|
|
43
|
+
regex += "(?:";
|
|
44
|
+
} else if (token === "}" && braces > 0) {
|
|
45
|
+
braces--;
|
|
46
|
+
regex += ")";
|
|
47
|
+
} else if (token === "," && braces > 0) regex += "|";
|
|
48
|
+
else regex += token.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
49
|
+
}
|
|
50
|
+
return regex;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// shared/config/src/ignore.ts
|
|
55
|
+
var Ignore = class {
|
|
56
|
+
static {
|
|
57
|
+
__name(this, "Ignore");
|
|
58
|
+
}
|
|
59
|
+
#patterns;
|
|
60
|
+
constructor(sources) {
|
|
61
|
+
this.#patterns = sources.map((source) => new Pattern(source));
|
|
62
|
+
}
|
|
63
|
+
matches(path) {
|
|
64
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
65
|
+
return this.#patterns.some((pattern) => pattern.matches(normalized));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// shared/config/src/config.ts
|
|
70
|
+
var Config = class _Config {
|
|
71
|
+
static {
|
|
72
|
+
__name(this, "Config");
|
|
73
|
+
}
|
|
74
|
+
static extensions = ["ts", "js"];
|
|
75
|
+
root;
|
|
76
|
+
ignore;
|
|
77
|
+
rules;
|
|
78
|
+
/** What the config file set, before the defaults were merged in. */
|
|
79
|
+
custom;
|
|
80
|
+
constructor(options, root = process.cwd(), custom = {}) {
|
|
81
|
+
this.root = root;
|
|
82
|
+
this.ignore = new Ignore(options.ignore);
|
|
83
|
+
this.rules = options.rules;
|
|
84
|
+
this.custom = custom;
|
|
85
|
+
}
|
|
86
|
+
static async load(name, defaults, root = process.cwd()) {
|
|
87
|
+
for (const extension of _Config.extensions) {
|
|
88
|
+
const file = join(root, `${name}.config.${extension}`);
|
|
89
|
+
if (existsSync(file)) {
|
|
90
|
+
const { default: options } = await import(pathToFileURL(file).href);
|
|
91
|
+
return new _Config(
|
|
92
|
+
_Config.merge(defaults, options),
|
|
93
|
+
root,
|
|
94
|
+
options
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new _Config(defaults, root);
|
|
99
|
+
}
|
|
100
|
+
/** Handles `--config`, writing the defaults to a TypeScript config file, or with `--config js` a JavaScript one. Returns whether it was given. */
|
|
101
|
+
static init(name, defaults, argv = process.argv.slice(2), root = process.cwd()) {
|
|
102
|
+
const index = argv.indexOf("--config");
|
|
103
|
+
if (index === -1) return false;
|
|
104
|
+
const next = argv[index + 1];
|
|
105
|
+
const extension = next === void 0 || next.startsWith("-") ? "ts" : next;
|
|
106
|
+
try {
|
|
107
|
+
console.log(
|
|
108
|
+
`created: ${_Config.export(name, defaults, extension, root)}`
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(error instanceof Error ? error.message : error);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/** Writes the defaults to `<name>.config.<extension>` in the root, unless a config file is there already. */
|
|
117
|
+
static export(name, defaults, extension = "ts", root = process.cwd()) {
|
|
118
|
+
if (!_Config.extensions.includes(extension))
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unknown config file type ${extension}, use ts or js.`
|
|
121
|
+
);
|
|
122
|
+
for (const existing of _Config.extensions) {
|
|
123
|
+
const file2 = `${name}.config.${existing}`;
|
|
124
|
+
if (existsSync(join(root, file2)))
|
|
125
|
+
throw new Error(`${file2} already exists.`);
|
|
126
|
+
}
|
|
127
|
+
const file = `${name}.config.${extension}`;
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(root, file),
|
|
130
|
+
`export default ${_Config.#literal(defaults)};
|
|
131
|
+
`
|
|
132
|
+
);
|
|
133
|
+
return file;
|
|
134
|
+
}
|
|
135
|
+
/** A value as JavaScript source, objects one property per line, short arrays on one line. */
|
|
136
|
+
static #literal(value, indent = "") {
|
|
137
|
+
const inner = `${indent} `;
|
|
138
|
+
const lines = /* @__PURE__ */ __name((items, open, close) => items.length === 0 ? `${open}${close}` : `${open}
|
|
139
|
+
${items.map((item) => `${inner}${item},
|
|
140
|
+
`).join("")}${indent}${close}`, "lines");
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
const items = value.map((item) => _Config.#literal(item, inner));
|
|
143
|
+
const line = `[${items.join(", ")}]`;
|
|
144
|
+
return line.length <= 60 && !line.includes("\n") ? line : lines(items, "[", "]");
|
|
145
|
+
}
|
|
146
|
+
if (value?.constructor === Object)
|
|
147
|
+
return lines(
|
|
148
|
+
Object.entries(value).map(
|
|
149
|
+
([key, item]) => `${/^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key)}: ${_Config.#literal(item, inner)}`
|
|
150
|
+
),
|
|
151
|
+
"{",
|
|
152
|
+
"}"
|
|
153
|
+
);
|
|
154
|
+
return JSON.stringify(value);
|
|
155
|
+
}
|
|
156
|
+
/** Deep merges plain objects, anything else in override replaces base. */
|
|
157
|
+
static merge(base, override) {
|
|
158
|
+
const plain = /* @__PURE__ */ __name((value) => value?.constructor === Object, "plain");
|
|
159
|
+
if (!plain(base) || !plain(override)) return override ?? base;
|
|
160
|
+
const merged = { ...base };
|
|
161
|
+
for (const [key, value] of Object.entries(override))
|
|
162
|
+
merged[key] = _Config.merge(base[key], value);
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
*files(directory = "") {
|
|
166
|
+
const entries = readdirSync(join(this.root, directory), {
|
|
167
|
+
withFileTypes: true
|
|
168
|
+
});
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
171
|
+
if (this.ignore.matches(path)) continue;
|
|
172
|
+
if (entry.isDirectory()) yield* this.files(path);
|
|
173
|
+
else yield path;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Text files that are not ignored and that accepts takes, binaries are skipped. */
|
|
177
|
+
*sources(accepts = () => true) {
|
|
178
|
+
for (const file of this.files()) {
|
|
179
|
+
if (!accepts(file)) continue;
|
|
180
|
+
const path = join(this.root, file);
|
|
181
|
+
const text = readFileSync(path, "utf8");
|
|
182
|
+
if (!text.includes("\0")) yield { file, path, text };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// shared/config/src/includes.ts
|
|
188
|
+
var Includes = class {
|
|
189
|
+
static {
|
|
190
|
+
__name(this, "Includes");
|
|
191
|
+
}
|
|
192
|
+
#globs;
|
|
193
|
+
constructor(globs) {
|
|
194
|
+
this.#globs = globs.map((glob) => ({
|
|
195
|
+
pattern: new Pattern(glob.replace(/^!/, "")),
|
|
196
|
+
included: !glob.startsWith("!")
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
matches(file) {
|
|
200
|
+
let included = false;
|
|
201
|
+
for (const { pattern, included: positive } of this.#globs)
|
|
202
|
+
if (pattern.matches(file)) included = positive;
|
|
203
|
+
return included;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
export {
|
|
207
|
+
Includes,
|
|
208
|
+
Pattern,
|
|
209
|
+
Config as default
|
|
210
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Ignore from "./ignore.js";
|
|
2
|
+
export interface Options<Rules extends object = object> {
|
|
3
|
+
ignore: string[];
|
|
4
|
+
rules: Rules;
|
|
5
|
+
}
|
|
6
|
+
/** A text file found by a tool, with its path relative to the root. */
|
|
7
|
+
export interface Source {
|
|
8
|
+
file: string;
|
|
9
|
+
path: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export default class Config<Rules extends object = object> {
|
|
13
|
+
#private;
|
|
14
|
+
static readonly extensions: string[];
|
|
15
|
+
readonly root: string;
|
|
16
|
+
readonly ignore: Ignore;
|
|
17
|
+
readonly rules: Rules;
|
|
18
|
+
/** What the config file set, before the defaults were merged in. */
|
|
19
|
+
readonly custom: Partial<Options<Rules>>;
|
|
20
|
+
constructor(options: Options<Rules>, root?: string, custom?: Partial<Options<Rules>>);
|
|
21
|
+
static load<Rules extends object>(name: string, defaults: Options<Rules>, root?: string): Promise<Config<Rules>>;
|
|
22
|
+
/** Handles `--config`, writing the defaults to a TypeScript config file, or with `--config js` a JavaScript one. Returns whether it was given. */
|
|
23
|
+
static init(name: string, defaults: Options, argv?: string[], root?: string): boolean;
|
|
24
|
+
/** Writes the defaults to `<name>.config.<extension>` in the root, unless a config file is there already. */
|
|
25
|
+
static export(name: string, defaults: Options, extension?: string, root?: string): string;
|
|
26
|
+
/** Deep merges plain objects, anything else in override replaces base. */
|
|
27
|
+
static merge<T>(base: T, override: unknown): T;
|
|
28
|
+
files(directory?: string): Generator<string>;
|
|
29
|
+
/** Text files that are not ignored and that accepts takes, binaries are skipped. */
|
|
30
|
+
sources(accepts?: (file: string) => boolean): Generator<Source>;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aljazmc/config",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Shared, dependency-free configuration loader, file finder and glob matcher for @aljazmc packages.",
|
|
5
|
+
"author": "aljazmc",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/aljazmc/aljazmc.git",
|
|
11
|
+
"directory": "shared/config"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.18.0 || ^24.2.0 || ^25 || ^26"
|
|
15
|
+
},
|
|
16
|
+
"types": "./bin/types/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./bin/types/index.d.ts",
|
|
20
|
+
"default": "./bin/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./bin/types/index.d.ts",
|
|
28
|
+
"default": "./bin/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@jest/globals": "^30.5.2",
|
|
35
|
+
"@types/node": "^26.6.6"
|
|
36
|
+
}
|
|
6
37
|
}
|