@ac-kit/app-config 0.1.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024-2026 Anthony Champagne and contributors
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @ac-kit/app-config
2
+
3
+ Loading a command-line tool's configuration file, wherever the user put it.
4
+
5
+ A CLI is expected to find its config in any of a dozen places — `package.json`,
6
+ a dotfile, a `.config/` directory, with or without an extension — and to let one
7
+ config `extends` another, the way `tsconfig.json` and `.eslintrc` do. This does
8
+ both: `cosmiconfig` performs the search, and this resolves the `extends` chain,
9
+ merges it over your defaults, and hands back a validated object with `extends`
10
+ already removed.
11
+
12
+ ```typescript
13
+ import { loadCliConfig } from "@ac-kit/app-config";
14
+
15
+ const config = await loadCliConfig(
16
+ "mytool",
17
+ (value) => myToolConfigSchema.parse(value),
18
+ { outDir: "dist", strict: true },
19
+ );
20
+ ```
21
+
22
+ `loadCliConfig` throws when no configuration file is found; the parser you pass
23
+ decides what an invalid one does.
24
+
25
+ Also exposes `resolveExtendableConfig` and `resolveExtendsValue` for resolving
26
+ an `extends` chain you already loaded yourself, and `editorconfig` helpers for
27
+ reading `.editorconfig` settings.
28
+
29
+ Node only: it reads the filesystem.
30
+
31
+ See the
32
+ [generated API docs](https://anthochamp.github.io/node-essentials/api/app-config/)
33
+ for the full reference.
@@ -0,0 +1,27 @@
1
+ import { type EditorConfigProperties, type EditorConfigSource } from "@ac-kit/format-editorconfig";
2
+ /** Options for {@link readEditorConfigSources}. */
3
+ export type EditorConfigWalkOptions = {
4
+ /**
5
+ * Directory to stop at, even if no `root = true` was found. Defaults to the
6
+ * filesystem root.
7
+ */
8
+ readonly stopAt?: string;
9
+ };
10
+ /**
11
+ * Reads every `.editorconfig` governing `filePath`, outermost first.
12
+ *
13
+ * Ascends from the file's own directory, stopping at the first file declaring
14
+ * `root = true` (that file included) or at
15
+ * {@link EditorConfigWalkOptions.stopAt}. A directory with no `.editorconfig`,
16
+ * or one that cannot be read, is skipped — the search is best-effort by design,
17
+ * since an unreadable parent directory is a permissions fact rather than a
18
+ * configuration error.
19
+ *
20
+ * This is the host-bound half of EditorConfig support;
21
+ * `@ac-kit/format-editorconfig` owns the parsing and resolution and stays
22
+ * portable.
23
+ */
24
+ export declare function readEditorConfigSources(filePath: string, options?: EditorConfigWalkOptions): Promise<EditorConfigSource[]>;
25
+ /** Resolves the EditorConfig properties that apply to `filePath` on disk. */
26
+ export declare function readEditorConfig(filePath: string, options?: EditorConfigWalkOptions): Promise<EditorConfigProperties>;
27
+ //# sourceMappingURL=editorconfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorconfig.d.ts","sourceRoot":"","sources":["../src/editorconfig.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EAGvB,MAAM,6BAA6B,CAAC;AAIrC,mDAAmD;AACnD,MAAM,MAAM,uBAAuB,GAAG;IACrC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,CAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,uBAAuB,GAC/B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAoC/B;AAED,6EAA6E;AAC7E,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,uBAAuB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAGjC"}
@@ -0,0 +1,61 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parseEditorConfig, resolveEditorConfig, } from "@ac-kit/format-editorconfig";
4
+ const FILE_NAME_ = ".editorconfig";
5
+ /**
6
+ * Reads every `.editorconfig` governing `filePath`, outermost first.
7
+ *
8
+ * Ascends from the file's own directory, stopping at the first file declaring
9
+ * `root = true` (that file included) or at
10
+ * {@link EditorConfigWalkOptions.stopAt}. A directory with no `.editorconfig`,
11
+ * or one that cannot be read, is skipped — the search is best-effort by design,
12
+ * since an unreadable parent directory is a permissions fact rather than a
13
+ * configuration error.
14
+ *
15
+ * This is the host-bound half of EditorConfig support;
16
+ * `@ac-kit/format-editorconfig` owns the parsing and resolution and stays
17
+ * portable.
18
+ */
19
+ export async function readEditorConfigSources(filePath, options) {
20
+ const absolute = path.resolve(filePath);
21
+ const stopAt = options?.stopAt ? path.resolve(options.stopAt) : undefined;
22
+ const sources = [];
23
+ let directory = path.dirname(absolute);
24
+ for (;;) {
25
+ let contents;
26
+ try {
27
+ contents = await readFile(path.join(directory, FILE_NAME_), "utf-8");
28
+ }
29
+ catch {
30
+ contents = undefined;
31
+ }
32
+ if (contents !== undefined) {
33
+ const file = parseEditorConfig(contents);
34
+ // Collected innermost first, reversed below: the caller's contract is
35
+ // outermost first, and the root is only discovered on the way up.
36
+ sources.push({ directory: toPosix(directory), file });
37
+ if (file.root) {
38
+ break;
39
+ }
40
+ }
41
+ if (stopAt !== undefined && directory === stopAt) {
42
+ break;
43
+ }
44
+ const parent = path.dirname(directory);
45
+ if (parent === directory) {
46
+ break;
47
+ }
48
+ directory = parent;
49
+ }
50
+ return sources.reverse();
51
+ }
52
+ /** Resolves the EditorConfig properties that apply to `filePath` on disk. */
53
+ export async function readEditorConfig(filePath, options) {
54
+ const sources = await readEditorConfigSources(filePath, options);
55
+ return resolveEditorConfig(sources, toPosix(path.resolve(filePath)));
56
+ }
57
+ /** Section globs are always `/`-separated, whatever the host uses. */
58
+ function toPosix(value) {
59
+ return value.split(path.sep).join("/");
60
+ }
61
+ //# sourceMappingURL=editorconfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorconfig.js","sourceRoot":"","sources":["../src/editorconfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAGN,iBAAiB,EACjB,mBAAmB,GACnB,MAAM,6BAA6B,CAAC;AAErC,MAAM,UAAU,GAAG,eAAe,CAAC;AAWnC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC5C,QAAgB,EAChB,OAAiC;IAEjC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1E,MAAM,OAAO,GAAyB,EAAE,CAAC;IACzC,IAAI,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEvC,SAAS,CAAC;QACT,IAAI,QAA4B,CAAC;QACjC,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACR,QAAQ,GAAG,SAAS,CAAC;QACtB,CAAC;QAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;YACzC,sEAAsE;YACtE,kEAAkE;YAClE,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACf,MAAM;YACP,CAAC;QACF,CAAC;QAED,IAAI,MAAM,KAAK,SAAS,IAAI,SAAS,KAAK,MAAM,EAAE,CAAC;YAClD,MAAM;QACP,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM;QACP,CAAC;QACD,SAAS,GAAG,MAAM,CAAC;IACpB,CAAC;IAED,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;AAC1B,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,QAAgB,EAChB,OAAiC;IAEjC,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjE,OAAO,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,sEAAsE;AACtE,SAAS,OAAO,CAAC,KAAa;IAC7B,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,62 @@
1
+ import type { UnknownRecord } from "type-fest";
2
+ /**
3
+ * A configuration object that can extend other configurations via the "extends"
4
+ * property.
5
+ *
6
+ * The "extends" property can be a string or an array of strings, each
7
+ * representing a module to extend from. The modules will be resolved and merged
8
+ * into the current configuration.
9
+ *
10
+ * Additional properties can be defined as needed.
11
+ */
12
+ export type ExtendableConfig = UnknownRecord & {
13
+ /**
14
+ * A module or an array of modules to extend from.
15
+ *
16
+ * Each module can be: - A package name (e.g. "my-config-package"). - A scoped
17
+ * package name (e.g. "@my-scope/my-config-package"). - A relative or absolute
18
+ * path to a configuration file.
19
+ *
20
+ * The modules will be resolved and merged in the order they are listed.
21
+ */
22
+ extends?: string | string[];
23
+ };
24
+ /**
25
+ * A parser function to validate and transform an extendable configuration
26
+ * object.
27
+ *
28
+ * This function should take the raw configuration data (e.g. from a JSON file
29
+ * or module) and return a valid `ExtendableConfig` object. It can also throw an
30
+ * error if the data is invalid.
31
+ *
32
+ * @param data The raw configuration data to parse.
33
+ * @returns The parsed and validated `ExtendableConfig` object.
34
+ */
35
+ export type ExtendableConfigParser = (data: unknown) => ExtendableConfig;
36
+ /** Options for resolving an extendable configuration. */
37
+ export type ResolveExtendableConfigOptions = {
38
+ /**
39
+ * Arguments to pass to configuration modules that export a function.
40
+ *
41
+ * If a configuration module exports a function, it will be called with these
42
+ * arguments to obtain the configuration object.
43
+ *
44
+ * Default is an empty array (no arguments).
45
+ */
46
+ moduleFunctionCallArgs?: unknown[];
47
+ /** A callback function that is called when a circular dependency is detected. */
48
+ onCircularDependency?: ((moduleId: string, modulePath: string, configPath: string) => void) | null;
49
+ };
50
+ /**
51
+ * Resolve an extendable configuration (with "extends"-property).
52
+ *
53
+ * @param configData The configuration data to resolve.
54
+ * @param configPath The path of the configuration file (used for resolving
55
+ * relative paths).
56
+ * @param configParser The parser function to parse configuration modules.
57
+ * @param configId The configuration ID (e.g. "eslint", "babel", etc.).
58
+ * @param options The options for resolving the configuration.
59
+ * @returns The resolved configuration data, without the "extends"-property.
60
+ */
61
+ export declare function resolveExtendableConfig<T extends ExtendableConfig>(configData: T, configPath: string, configParser: ExtendableConfigParser, configId: string, options?: ResolveExtendableConfigOptions): Promise<Omit<T, "extends">>;
62
+ //# sourceMappingURL=resolve-extendable-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-extendable-config.d.ts","sourceRoot":"","sources":["../../src/extendable-config/resolve-extendable-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAI/C;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG;IAC9C;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,gBAAgB,CAAC;AAEzE,yDAAyD;AACzD,MAAM,MAAM,8BAA8B,GAAG;IAC5C;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,EAAE,CAAC;IAEnC,iFAAiF;IACjF,oBAAoB,CAAC,EAClB,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC,GACpE,IAAI,CAAC;CACR,CAAC;AAQF;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,SAAS,gBAAgB,EACvE,UAAU,EAAE,CAAC,EACb,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,sBAAsB,EACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,8BAA8B,GACtC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAe7B"}
@@ -0,0 +1,94 @@
1
+ import * as path from "node:path";
2
+ import { alwaysIterable, clone, defaults, mergeInplace } from "@ac-kit/core";
3
+ import { resolveExtendsValue } from "./resolve-extends-value.js";
4
+ const RESOLVE_EXTENDABLE_CONFIG_DEFAULT_OPTIONS = {
5
+ moduleFunctionCallArgs: [],
6
+ onCircularDependency: null,
7
+ };
8
+ /**
9
+ * Resolve an extendable configuration (with "extends"-property).
10
+ *
11
+ * @param configData The configuration data to resolve.
12
+ * @param configPath The path of the configuration file (used for resolving
13
+ * relative paths).
14
+ * @param configParser The parser function to parse configuration modules.
15
+ * @param configId The configuration ID (e.g. "eslint", "babel", etc.).
16
+ * @param options The options for resolving the configuration.
17
+ * @returns The resolved configuration data, without the "extends"-property.
18
+ */
19
+ export async function resolveExtendableConfig(configData, configPath, configParser, configId, options) {
20
+ const effectiveOptions = defaults(options, RESOLVE_EXTENDABLE_CONFIG_DEFAULT_OPTIONS);
21
+ return internalResolveExtendableConfig(configData, configPath, configParser, configId, effectiveOptions, new Set(), new Set());
22
+ }
23
+ /**
24
+ * @param configData The configuration data to resolve.
25
+ * @param configPath The path of the configuration file (used for resolving
26
+ * relative paths).
27
+ * @param configParser The parser function to parse configuration modules.
28
+ * @param configId The configuration ID (e.g. "eslint", "babel", etc.).
29
+ * @param options The options for resolving the configuration.
30
+ * @param globalList A set of already resolved module paths (to avoid
31
+ * duplicates).
32
+ * @param branchList A set of module paths in the current resolution branch (to
33
+ * detect circular dependencies).
34
+ * @returns The resolved configuration data, without the "extends"-property.
35
+ * @internal
36
+ *
37
+ * Internal function to recursively resolve an extendable configuration.
38
+ */
39
+ async function internalResolveExtendableConfig(configData, configPath, configParser, configId, options, globalList, branchList) {
40
+ const { extends: extendsData, ...restConfig } = configData;
41
+ const extendsValues = alwaysIterable(extendsData ?? []);
42
+ const finalConfig = clone(restConfig, { recursive: true });
43
+ for (const extendsValue of extendsValues) {
44
+ let moduleId;
45
+ let modulePath;
46
+ try {
47
+ const result = resolveExtendsValue(extendsValue, configId, [configPath]);
48
+ if (!result) {
49
+ throw new Error("not found");
50
+ }
51
+ moduleId = result.id;
52
+ modulePath = result.path;
53
+ }
54
+ catch (error) {
55
+ throw new Error(`resolve module "${extendsValue}" (referenced from "${configPath}")`, {
56
+ cause: error,
57
+ });
58
+ }
59
+ if (branchList.has(modulePath)) {
60
+ options.onCircularDependency?.(moduleId, modulePath, configPath);
61
+ continue;
62
+ }
63
+ if (globalList.has(modulePath)) {
64
+ continue;
65
+ }
66
+ globalList.add(modulePath);
67
+ const module = await import(modulePath);
68
+ let moduleData;
69
+ if (typeof module.default === "function") {
70
+ moduleData = module.default(...options.moduleFunctionCallArgs);
71
+ }
72
+ else if (typeof module === "function") {
73
+ moduleData = module(...options.moduleFunctionCallArgs);
74
+ }
75
+ else {
76
+ moduleData = module.default || module;
77
+ }
78
+ let moduleConfig;
79
+ try {
80
+ moduleConfig = configParser(moduleData);
81
+ }
82
+ catch (error) {
83
+ throw new Error(`validate config from module "${moduleId}" (referenced from "${configPath}")`, { cause: error });
84
+ }
85
+ const resolvedConfig = await internalResolveExtendableConfig(moduleConfig, path.dirname(modulePath), configParser, configId, options, globalList, new Set([...branchList, modulePath]));
86
+ void mergeInplace(finalConfig, resolvedConfig, {
87
+ recursive: true,
88
+ cloneSource: true,
89
+ arrayMergeMode: "spread",
90
+ });
91
+ }
92
+ return finalConfig;
93
+ }
94
+ //# sourceMappingURL=resolve-extendable-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-extendable-config.js","sourceRoot":"","sources":["../../src/extendable-config/resolve-extendable-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @param value The "extends"-property value to resolve.
3
+ * @param configId The configuration ID (e.g. "eslint", "babel", etc.).
4
+ * @param searchPaths The paths to use for module resolution.
5
+ * @returns The resolve entry name and the resolved module name, or null if not
6
+ * found.
7
+ * @internal
8
+ *
9
+ * Resolve the value of an "extends" property in a configuration file.
10
+ *
11
+ * Rules:
12
+ * 1) If the value starts with a @, it is a scoped package. If it does not contain
13
+ * a / after the scope, add /<configId>-config. Then check if the package is
14
+ * resolvable. If not, return null.
15
+ * 2) If the value does not start with a @,
16
+ * - First try to resolve @<configId>/config-<value>.
17
+ * - If that fails, try to resolve <configId>-config-<value> (old style).
18
+ * - If that fails, try to resolve <value> as is.
19
+ * - If all attempts fail, return null.
20
+ */
21
+ export declare function resolveExtendsValue(value: string, configId: string, searchPaths?: string[]): {
22
+ id: string;
23
+ path: string;
24
+ } | null;
25
+ //# sourceMappingURL=resolve-extends-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-extends-value.d.ts","sourceRoot":"","sources":["../../src/extendable-config/resolve-extends-value.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,EAAE,GACpB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwCrC"}
@@ -0,0 +1,54 @@
1
+ import { resolveModule } from "@ac-kit/node";
2
+ /**
3
+ * @param value The "extends"-property value to resolve.
4
+ * @param configId The configuration ID (e.g. "eslint", "babel", etc.).
5
+ * @param searchPaths The paths to use for module resolution.
6
+ * @returns The resolve entry name and the resolved module name, or null if not
7
+ * found.
8
+ * @internal
9
+ *
10
+ * Resolve the value of an "extends" property in a configuration file.
11
+ *
12
+ * Rules:
13
+ * 1) If the value starts with a @, it is a scoped package. If it does not contain
14
+ * a / after the scope, add /<configId>-config. Then check if the package is
15
+ * resolvable. If not, return null.
16
+ * 2) If the value does not start with a @,
17
+ * - First try to resolve @<configId>/config-<value>.
18
+ * - If that fails, try to resolve <configId>-config-<value> (old style).
19
+ * - If that fails, try to resolve <value> as is.
20
+ * - If all attempts fail, return null.
21
+ */
22
+ export function resolveExtendsValue(value, configId, searchPaths) {
23
+ let path;
24
+ if (value[0] === "@") {
25
+ if (!value.includes("/")) {
26
+ value += `/${configId}-config`;
27
+ }
28
+ path = resolveModule(value, searchPaths);
29
+ if (path) {
30
+ return { id: value, path };
31
+ }
32
+ return null;
33
+ }
34
+ const officialMissingPart = `@${configId}/config`;
35
+ let testName = `${officialMissingPart}-${value}`;
36
+ path = resolveModule(testName, searchPaths);
37
+ if (path) {
38
+ return { id: testName, path };
39
+ }
40
+ const oldStyleOfficialMissingPart = `${configId}-config`;
41
+ if (!value.startsWith(oldStyleOfficialMissingPart)) {
42
+ testName = `${oldStyleOfficialMissingPart}-${value}`;
43
+ path = resolveModule(testName, searchPaths);
44
+ if (path) {
45
+ return { id: testName, path };
46
+ }
47
+ }
48
+ path = resolveModule(value, searchPaths);
49
+ if (path) {
50
+ return { id: value, path };
51
+ }
52
+ return null;
53
+ }
54
+ //# sourceMappingURL=resolve-extends-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-extends-value.js","sourceRoot":"","sources":["../../src/extendable-config/resolve-extends-value.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,mBAAmB,CAClC,KAAa,EACb,QAAgB,EAChB,WAAsB;IAEtB,IAAI,IAAmB,CAAC;IAExB,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,KAAK,IAAI,IAAI,QAAQ,SAAS,CAAC;QAChC,CAAC;QAED,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QACzC,IAAI,IAAI,EAAE,CAAC;YACV,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAC5B,CAAC;QAED,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,mBAAmB,GAAG,IAAI,QAAQ,SAAS,CAAC;IAClD,IAAI,QAAQ,GAAG,GAAG,mBAAmB,IAAI,KAAK,EAAE,CAAC;IAEjD,IAAI,GAAG,aAAa,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAC5C,IAAI,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,2BAA2B,GAAG,GAAG,QAAQ,SAAS,CAAC;IACzD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,2BAA2B,CAAC,EAAE,CAAC;QACpD,QAAQ,GAAG,GAAG,2BAA2B,IAAI,KAAK,EAAE,CAAC;QAErD,IAAI,GAAG,aAAa,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAC5C,IAAI,IAAI,EAAE,CAAC;YACV,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC/B,CAAC;IACF,CAAC;IAED,IAAI,GAAG,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IACzC,IAAI,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from "./editorconfig.js";
2
+ export * from "./extendable-config/resolve-extendable-config.js";
3
+ export * from "./extendable-config/resolve-extends-value.js";
4
+ export * from "./load-config.js";
5
+ export * from "./resolve-config-value.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kDAAkD,CAAC;AACjE,cAAc,8CAA8C,CAAC;AAC7D,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./editorconfig.js";
2
+ export * from "./extendable-config/resolve-extendable-config.js";
3
+ export * from "./extendable-config/resolve-extends-value.js";
4
+ export * from "./load-config.js";
5
+ export * from "./resolve-config-value.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kDAAkD,CAAC;AACjE,cAAc,8CAA8C,CAAC;AAC7D,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { PartialDeep } from "type-fest";
2
+ import { type ExtendableConfig, type ExtendableConfigParser, type ResolveExtendableConfigOptions } from "./extendable-config/resolve-extendable-config.js";
3
+ /** Options for loading CLI configuration. */
4
+ export type LoadConfigOptions = {
5
+ /** Options for resolving "extends" field in the configuration. */
6
+ resolveExtendsOptions?: ResolveExtendableConfigOptions;
7
+ };
8
+ /**
9
+ * Load a CLI configuration file using cosmiconfig, supporting "extends" field.
10
+ *
11
+ * @param configId The configuration ID (name) to search for.
12
+ * @param configParser A parser to validate/transform the config object.
13
+ * @param defaultConfig Default configuration values.
14
+ * @param options Optional settings for loading and resolving the config.
15
+ * @returns The resolved configuration object without the "extends" field.
16
+ */
17
+ export declare function loadCliConfig<T extends ExtendableConfig>(configId: string, configParser: ExtendableConfigParser, defaultConfig: PartialDeep<T>, options?: LoadConfigOptions): Promise<Omit<T, "extends">>;
18
+ //# sourceMappingURL=load-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,8BAA8B,EAEnC,MAAM,kDAAkD,CAAC;AAE1D,6CAA6C;AAC7C,MAAM,MAAM,iBAAiB,GAAG;IAC/B,kEAAkE;IAClE,qBAAqB,CAAC,EAAE,8BAA8B,CAAC;CACvD,CAAC;AAMF;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,CAAC,SAAS,gBAAgB,EAC7D,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,sBAAsB,EACpC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,EAC7B,OAAO,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAkB7B"}
@@ -0,0 +1,28 @@
1
+ import * as path from "node:path";
2
+ import { defaults, merge } from "@ac-kit/core";
3
+ import { cosmiconfig } from "cosmiconfig";
4
+ import { resolveExtendableConfig, } from "./extendable-config/resolve-extendable-config.js";
5
+ const DEFAULT_LOAD_CONFIG_OPTIONS = {
6
+ resolveExtendsOptions: {},
7
+ };
8
+ /**
9
+ * Load a CLI configuration file using cosmiconfig, supporting "extends" field.
10
+ *
11
+ * @param configId The configuration ID (name) to search for.
12
+ * @param configParser A parser to validate/transform the config object.
13
+ * @param defaultConfig Default configuration values.
14
+ * @param options Optional settings for loading and resolving the config.
15
+ * @returns The resolved configuration object without the "extends" field.
16
+ */
17
+ export async function loadCliConfig(configId, configParser, defaultConfig, options) {
18
+ const effectiveOptions = defaults(options, DEFAULT_LOAD_CONFIG_OPTIONS);
19
+ const searchResult = await cosmiconfig(configId).search();
20
+ if (!searchResult) {
21
+ throw new Error(`no configuration found`);
22
+ }
23
+ return resolveExtendableConfig(merge(defaultConfig, searchResult.config, {
24
+ recursive: true,
25
+ cloneSource: true,
26
+ }), path.dirname(searchResult.filepath), configParser, configId, effectiveOptions.resolveExtendsOptions);
27
+ }
28
+ //# sourceMappingURL=load-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../src/load-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,EAIN,uBAAuB,GACvB,MAAM,kDAAkD,CAAC;AAQ1D,MAAM,2BAA2B,GAAgC;IAChE,qBAAqB,EAAE,EAAE;CACzB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,QAAgB,EAChB,YAAoC,EACpC,aAA6B,EAC7B,OAA2B;IAE3B,MAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,EAAE,2BAA2B,CAAC,CAAC;IAExE,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1D,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,uBAAuB,CAC7B,KAAK,CAAC,aAAa,EAAE,YAAY,CAAC,MAAM,EAAE;QACzC,SAAS,EAAE,IAAI;QACf,WAAW,EAAE,IAAI;KACjB,CAAC,EACF,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,EACnC,YAAY,EACZ,QAAQ,EACR,gBAAgB,CAAC,qBAAqB,CACtC,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { MaybePromiseLike, type MaybeAsyncCallable } from "@ac-kit/core";
2
+ /**
3
+ * Resolve a configuration value that may be a static value or a function
4
+ * returning the value.
5
+ *
6
+ * If it's a function, it will be called with the provided arguments. If it's a
7
+ * static value, it will be returned as-is.
8
+ *
9
+ * This allows configuration options to be defined either as fixed values or as
10
+ * functions that compute the value dynamically.
11
+ *
12
+ * @param configValue The configuration value or function to resolve.
13
+ * @param args Arguments to pass if the configValue is a function.
14
+ * @returns The resolved configuration value, possibly a Promise if async.
15
+ */
16
+ export declare function resolveConfigValue<A extends unknown[], R>(configValue: MaybeAsyncCallable<A, R> | R, ...args: A): MaybePromiseLike<R>;
17
+ //# sourceMappingURL=resolve-config-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-config-value.d.ts","sourceRoot":"","sources":["../src/resolve-config-value.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EACxD,WAAW,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EACzC,GAAG,IAAI,EAAE,CAAC,GACR,gBAAgB,CAAC,CAAC,CAAC,CAMrB"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Resolve a configuration value that may be a static value or a function
3
+ * returning the value.
4
+ *
5
+ * If it's a function, it will be called with the provided arguments. If it's a
6
+ * static value, it will be returned as-is.
7
+ *
8
+ * This allows configuration options to be defined either as fixed values or as
9
+ * functions that compute the value dynamically.
10
+ *
11
+ * @param configValue The configuration value or function to resolve.
12
+ * @param args Arguments to pass if the configValue is a function.
13
+ * @returns The resolved configuration value, possibly a Promise if async.
14
+ */
15
+ export function resolveConfigValue(configValue, ...args) {
16
+ if (typeof configValue === "function") {
17
+ return configValue(...args);
18
+ }
19
+ return configValue;
20
+ }
21
+ //# sourceMappingURL=resolve-config-value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-config-value.js","sourceRoot":"","sources":["../src/resolve-config-value.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CACjC,WAAyC,EACzC,GAAG,IAAO;IAEV,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;QACvC,OAAQ,WAAwC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,WAAW,CAAC;AACpB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@ac-kit/app-config",
3
+ "version": "0.1.0",
4
+ "description": "CLI configuration loading, built on cosmiconfig.",
5
+ "keywords": [
6
+ "cli",
7
+ "config",
8
+ "configuration",
9
+ "cosmiconfig"
10
+ ],
11
+ "homepage": "https://anthochamp.github.io/node-essentials/api/app-config/",
12
+ "bugs": {
13
+ "url": "https://github.com/anthochamp/node-essentials/issues"
14
+ },
15
+ "license": "BSD-3-Clause",
16
+ "author": "Anthony Champagne <dev@anthonychampagne.fr>",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/anthochamp/node-essentials.git",
20
+ "directory": "packages/app-config"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "main": "./dist/index.js",
28
+ "publishConfig": {
29
+ "main": "./dist/index.js",
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "compile": "tsc -p ./tsconfig.build.json -outDir dist",
34
+ "compile:check": "tsc -noEmit -p ./tsconfig.json",
35
+ "clean": "rimraf dist",
36
+ "build": "yarn clean && yarn compile",
37
+ "test": "yarn run -T vitest run",
38
+ "prepack": "yarn build"
39
+ },
40
+ "dependencies": {
41
+ "@ac-kit/core": "0.1.0",
42
+ "@ac-kit/format-editorconfig": "^0.1.0",
43
+ "@ac-kit/node": "0.1.0",
44
+ "@types/node": "^26.5.1",
45
+ "cosmiconfig": "^10.0.0",
46
+ "type-fest": "^5.8.0"
47
+ },
48
+ "devDependencies": {
49
+ "@ac-kit/tsconfig": "0.1.0",
50
+ "@ac-kit/typedoc-config": "0.1.0",
51
+ "@typescript/native": "typescript@7.0.2",
52
+ "rimraf": "6.1.3",
53
+ "typedoc": "0.28.20",
54
+ "typescript": "@typescript/typescript6@6.0.2",
55
+ "vite": "8.3.0",
56
+ "vitest": "5.0.0",
57
+ "vitest-mock-extended": "5.1.1"
58
+ },
59
+ "engines": {
60
+ "node": ">=24"
61
+ }
62
+ }