@epsilon-asi/foundation 0.0.11 → 0.0.12

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.
@@ -105,4 +105,29 @@ export interface SaveOptions extends Omit<SerializeOptions, "format" | "filePath
105
105
  /** File permission bits for the written file, e.g. 0o600 for secrets. */
106
106
  mode?: number;
107
107
  }
108
+ /** Where a layer was found: the start directory, one of its ancestors, or a fallback path. */
109
+ export type LayerOrigin = "local" | "ancestor" | "fallback";
110
+ /** One file discovered by discoverLayers(). */
111
+ export interface FileLayer {
112
+ /** Absolute path of the file. */
113
+ filePath: string;
114
+ origin: LayerOrigin;
115
+ }
116
+ export interface DiscoverOptions {
117
+ /** Directory the upward walk starts from. Default: process.cwd(). */
118
+ startDir?: string;
119
+ /** Last directory the walk visits (inclusive). Default: the filesystem root. */
120
+ stopDir?: string;
121
+ /** Paths checked after the walk, in order; they rank below every directory match. `~/` is expanded. */
122
+ fallbacks?: readonly string[];
123
+ }
124
+ /** Options for loadLayeredFile(): discovery, plus load options applied to every layer. */
125
+ export interface LayeredLoadOptions extends Omit<LoadOptions, "filePath">, DiscoverOptions {
126
+ }
127
+ export interface LayeredResult<T> {
128
+ /** The merged, validated value. */
129
+ value: T;
130
+ /** The layers that were merged, highest precedence first. */
131
+ layers: FileLayer[];
132
+ }
108
133
  export declare const EXTENSIONS: Record<string, FileFormat>;
@@ -1,8 +1,19 @@
1
1
  import * as z from "zod";
2
- import { FileFormat, LoadOptions, ParseOptions, SaveOptions, SerializeOptions } from "./types";
2
+ import { FileFormat, LoadOptions, ParseOptions, SafeResult, SaveOptions, SerializeOptions } from "./types";
3
3
  declare const MISSING: unique symbol;
4
4
  export declare function label(filePath?: string): string;
5
5
  export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
6
+ /**
7
+ * Recursively merge plain objects; later sources win. `undefined` values and non-object sources are
8
+ * skipped, arrays and scalars are replaced (not concatenated), and no source is mutated.
9
+ */
10
+ export declare function deepMerge<T extends Record<string, unknown> = Record<string, unknown>>(...sources: readonly unknown[]): T;
11
+ /** Replace a leading `~` with the current user's home directory. */
12
+ export declare function expandHome(filePath: string): string;
13
+ export declare function pathExists(filePath: string): Promise<boolean>;
14
+ /** Run `run`, turning a thrown SchemaFileError into a failed SafeResult. Other errors propagate. */
15
+ export declare function safely<T>(run: () => Promise<T>): Promise<SafeResult<T>>;
16
+ export declare function safelySync<T>(run: () => T): SafeResult<T>;
6
17
  export declare function decodeText(text: string, format: FileFormat, opts: ParseOptions): unknown;
7
18
  export declare function formatFromExtension(filePath: string): FileFormat | undefined;
8
19
  /**
@@ -16,6 +27,9 @@ export declare function toRaw(text: string | typeof MISSING, opts: LoadOptions):
16
27
  };
17
28
  export declare function readTextAsync(filePath: string, opts: LoadOptions): Promise<string | typeof MISSING>;
18
29
  export declare function readTextSync(filePath: string, opts: LoadOptions): string | typeof MISSING;
30
+ /** Read, decode and preprocess a file: everything loadFile does short of validation. */
31
+ export declare function readRawAsync(filePath: string, options: LoadOptions): Promise<unknown>;
32
+ export declare function readRawSync(filePath: string, options: LoadOptions): unknown;
19
33
  export declare function runPreprocess(raw: unknown, format: FileFormat, opts: ParseOptions): Promise<unknown>;
20
34
  export declare function runPreprocessSync(raw: unknown, format: FileFormat, opts: ParseOptions): unknown;
21
35
  export declare function validateAsync<S extends z.ZodType>(schema: S, raw: unknown, opts: ParseOptions): Promise<z.output<S>>;
@@ -47,12 +47,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.label = label;
49
49
  exports.isPlainObject = isPlainObject;
50
+ exports.deepMerge = deepMerge;
51
+ exports.expandHome = expandHome;
52
+ exports.pathExists = pathExists;
53
+ exports.safely = safely;
54
+ exports.safelySync = safelySync;
50
55
  exports.decodeText = decodeText;
51
56
  exports.formatFromExtension = formatFromExtension;
52
57
  exports.resolveFormat = resolveFormat;
53
58
  exports.toRaw = toRaw;
54
59
  exports.readTextAsync = readTextAsync;
55
60
  exports.readTextSync = readTextSync;
61
+ exports.readRawAsync = readRawAsync;
62
+ exports.readRawSync = readRawSync;
56
63
  exports.runPreprocess = runPreprocess;
57
64
  exports.runPreprocessSync = runPreprocessSync;
58
65
  exports.validateAsync = validateAsync;
@@ -65,6 +72,7 @@ exports.writeTextSync = writeTextSync;
65
72
  const YAML = __importStar(require("yaml"));
66
73
  const z = __importStar(require("zod"));
67
74
  const promises_1 = require("node:fs/promises");
75
+ const node_os_1 = __importDefault(require("node:os"));
68
76
  const node_path_1 = __importDefault(require("node:path"));
69
77
  const node_fs_1 = require("node:fs");
70
78
  const types_1 = require("./types");
@@ -81,6 +89,62 @@ function isPlainObject(v) {
81
89
  const proto = Object.getPrototypeOf(v);
82
90
  return proto === Object.prototype || proto === null;
83
91
  }
92
+ /**
93
+ * Recursively merge plain objects; later sources win. `undefined` values and non-object sources are
94
+ * skipped, arrays and scalars are replaced (not concatenated), and no source is mutated.
95
+ */
96
+ function deepMerge(...sources) {
97
+ const result = {};
98
+ for (const source of sources) {
99
+ if (!isPlainObject(source))
100
+ continue;
101
+ for (const [key, value] of Object.entries(source)) {
102
+ if (value === undefined)
103
+ continue;
104
+ const existing = result[key];
105
+ // defineProperty, not assignment: a parsed "__proto__" key must stay data, not swap the prototype.
106
+ Object.defineProperty(result, key, {
107
+ value: isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value,
108
+ enumerable: true,
109
+ writable: true,
110
+ configurable: true,
111
+ });
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+ /** Replace a leading `~` with the current user's home directory. */
117
+ function expandHome(filePath) {
118
+ if (filePath === "~")
119
+ return node_os_1.default.homedir();
120
+ return /^~[\\/]/.test(filePath) ? node_path_1.default.join(node_os_1.default.homedir(), filePath.slice(2)) : filePath;
121
+ }
122
+ function pathExists(filePath) {
123
+ return (0, promises_1.access)(filePath).then(() => true, () => false);
124
+ }
125
+ /** Run `run`, turning a thrown SchemaFileError into a failed SafeResult. Other errors propagate. */
126
+ function safely(run) {
127
+ return __awaiter(this, void 0, void 0, function* () {
128
+ try {
129
+ return { success: true, data: yield run() };
130
+ }
131
+ catch (error) {
132
+ if (error instanceof types_1.SchemaFileError)
133
+ return { success: false, error };
134
+ throw error;
135
+ }
136
+ });
137
+ }
138
+ function safelySync(run) {
139
+ try {
140
+ return { success: true, data: run() };
141
+ }
142
+ catch (error) {
143
+ if (error instanceof types_1.SchemaFileError)
144
+ return { success: false, error };
145
+ throw error;
146
+ }
147
+ }
84
148
  function isNotFound(e) {
85
149
  return (e === null || e === void 0 ? void 0 : e.code) === "ENOENT";
86
150
  }
@@ -147,6 +211,19 @@ function readTextSync(filePath, opts) {
147
211
  throw new types_1.SchemaFileError("read", `Failed to read ${filePath}: ${errMsg(cause)}`, { filePath, cause });
148
212
  }
149
213
  }
214
+ /** Read, decode and preprocess a file: everything loadFile does short of validation. */
215
+ function readRawAsync(filePath, options) {
216
+ return __awaiter(this, void 0, void 0, function* () {
217
+ const opts = Object.assign(Object.assign({}, options), { filePath });
218
+ const { raw, format } = toRaw(yield readTextAsync(filePath, opts), opts);
219
+ return runPreprocess(raw, format, opts);
220
+ });
221
+ }
222
+ function readRawSync(filePath, options) {
223
+ const opts = Object.assign(Object.assign({}, options), { filePath });
224
+ const { raw, format } = toRaw(readTextSync(filePath, opts), opts);
225
+ return runPreprocessSync(raw, format, opts);
226
+ }
150
227
  function runPreprocess(raw, format, opts) {
151
228
  return __awaiter(this, void 0, void 0, function* () {
152
229
  if (!opts.preprocess)
@@ -348,7 +425,7 @@ function writeTextAsync(filePath, text, opts) {
348
425
  try {
349
426
  if (opts.createDirs !== false)
350
427
  yield (0, promises_1.mkdir)(node_path_1.default.dirname(filePath), { recursive: true });
351
- if (opts.overwrite === false && (yield (0, promises_1.access)(filePath).then(() => true, () => false))) {
428
+ if (opts.overwrite === false && (yield pathExists(filePath))) {
352
429
  throw new types_1.SchemaFileError("write", `Refusing to overwrite existing file ${filePath}`, { filePath });
353
430
  }
354
431
  const base = { encoding: (_a = opts.encoding) !== null && _a !== void 0 ? _a : "utf8", mode: opts.mode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epsilon-asi/foundation",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "",
5
5
  "mainDir": "./",
6
6
  "types": "./build/index.d.ts",
@@ -31,10 +31,10 @@
31
31
  "testEnvironment": "node",
32
32
  "coverageThreshold": {
33
33
  "global": {
34
- "branches": 80,
35
- "functions": 80,
36
- "lines": 80,
37
- "statements": 80
34
+ "branches": 90,
35
+ "functions": 90,
36
+ "lines": 90,
37
+ "statements": 90
38
38
  }
39
39
  },
40
40
  "testMatch": [
@@ -73,6 +73,7 @@
73
73
  "author": "",
74
74
  "license": "SEE LICENSE IN LICENSE",
75
75
  "dependencies": {
76
+ "dotenv": "^17.4.2",
76
77
  "yaml": "^2.9.1"
77
78
  },
78
79
  "peerDependencies": {