locter 2.2.1 → 4.0.0-beta.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.
Files changed (39) hide show
  1. package/README.MD +581 -44
  2. package/dist/index.d.mts +563 -0
  3. package/dist/index.mjs +1117 -671
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +36 -40
  6. package/dist/index.cjs +0 -743
  7. package/dist/index.cjs.map +0 -1
  8. package/dist/index.d.ts +0 -3
  9. package/dist/loader/built-in/conf/index.d.ts +0 -1
  10. package/dist/loader/built-in/conf/module.d.ts +0 -6
  11. package/dist/loader/built-in/index.d.ts +0 -3
  12. package/dist/loader/built-in/json/index.d.ts +0 -1
  13. package/dist/loader/built-in/json/module.d.ts +0 -5
  14. package/dist/loader/built-in/module/index.d.ts +0 -3
  15. package/dist/loader/built-in/module/module.d.ts +0 -15
  16. package/dist/loader/built-in/module/type.d.ts +0 -8
  17. package/dist/loader/built-in/module/utils.d.ts +0 -9
  18. package/dist/loader/built-in/yaml/index.d.ts +0 -1
  19. package/dist/loader/built-in/yaml/module.d.ts +0 -5
  20. package/dist/loader/constants.d.ts +0 -6
  21. package/dist/loader/helpers.d.ts +0 -6
  22. package/dist/loader/index.d.ts +0 -3
  23. package/dist/loader/module.d.ts +0 -18
  24. package/dist/loader/singleton.d.ts +0 -2
  25. package/dist/loader/type.d.ts +0 -8
  26. package/dist/locator/async.d.ts +0 -3
  27. package/dist/locator/index.d.ts +0 -4
  28. package/dist/locator/sync.d.ts +0 -3
  29. package/dist/locator/types.d.ts +0 -15
  30. package/dist/locator/utils.d.ts +0 -7
  31. package/dist/utils/error.d.ts +0 -1
  32. package/dist/utils/file-name.d.ts +0 -2
  33. package/dist/utils/file-path.d.ts +0 -1
  34. package/dist/utils/has-property.d.ts +0 -2
  35. package/dist/utils/index.d.ts +0 -8
  36. package/dist/utils/object.d.ts +0 -2
  37. package/dist/utils/runtime.d.ts +0 -2
  38. package/dist/utils/to-array.d.ts +0 -1
  39. package/dist/utils/typescript.d.ts +0 -1
package/dist/index.mjs CHANGED
@@ -1,718 +1,1164 @@
1
- import { destr } from 'destr';
2
- import flat from 'flat';
3
- import fs from 'node:fs';
4
- import fg from 'fast-glob';
5
- import path from 'node:path';
6
- import { BaseError } from 'ebec';
7
- import { createJiti } from 'jiti';
8
- import { pathToFileURL } from 'node:url';
9
- import { parse } from 'yaml';
10
-
11
-
12
- // -- Shims --
13
- import cjsUrl from 'node:url';
14
- import cjsPath from 'node:path';
15
- import cjsModule from 'node:module';
16
- const __filename = cjsUrl.fileURLToPath(import.meta.url);
17
- const __dirname = cjsPath.dirname(__filename);
18
- const require = cjsModule.createRequire(import.meta.url);
1
+ import { createRequire } from "node:module";
2
+ import { BaseError, hasInstanceof, markInstanceof } from "@ebec/core";
3
+ import path from "node:path";
4
+ import fg from "fast-glob";
5
+ import { destr } from "destr";
6
+ import { flatten, unflatten } from "flat";
7
+ import fs from "node:fs";
8
+ import { createJiti } from "jiti";
9
+ import { pathToFileURL } from "node:url";
10
+ import { isMap, isNode, isScalar, parse, parseDocument, stringify } from "yaml";
11
+ //#region src/errors/base.ts
12
+ const LOCTER_ERROR_MARKER = Symbol.for("@locter/error");
13
+ var LocterError = class extends BaseError {
14
+ static [Symbol.hasInstance](input) {
15
+ return hasInstanceof(input, LOCTER_ERROR_MARKER);
16
+ }
17
+ path;
18
+ constructor(input = {}) {
19
+ if (typeof input === "string") super(input);
20
+ else {
21
+ const { path, ...rest } = input;
22
+ super(rest);
23
+ this.path = path;
24
+ }
25
+ markInstanceof(this, LOCTER_ERROR_MARKER);
26
+ }
27
+ };
28
+ //#endregion
29
+ //#region src/errors/load.ts
30
+ const LOCTER_LOAD_ERROR_MARKER = Symbol.for("@locter/load-error");
31
+ var LocterLoadError = class extends LocterError {
32
+ static [Symbol.hasInstance](input) {
33
+ return hasInstanceof(input, LOCTER_LOAD_ERROR_MARKER);
34
+ }
35
+ constructor(input = {}) {
36
+ super(input);
37
+ markInstanceof(this, LOCTER_LOAD_ERROR_MARKER);
38
+ }
39
+ };
40
+ //#endregion
41
+ //#region src/errors/not-found.ts
42
+ const LOCTER_NOT_FOUND_ERROR_MARKER = Symbol.for("@locter/not-found-error");
43
+ var LocterNotFoundError = class extends LocterError {
44
+ static [Symbol.hasInstance](input) {
45
+ return hasInstanceof(input, LOCTER_NOT_FOUND_ERROR_MARKER);
46
+ }
47
+ constructor(input = {}) {
48
+ super(input);
49
+ markInstanceof(this, LOCTER_NOT_FOUND_ERROR_MARKER);
50
+ }
51
+ };
52
+ //#endregion
53
+ //#region src/errors/unknown-extension.ts
54
+ const LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER = Symbol.for("@locter/unknown-extension-error");
55
+ var LocterUnknownExtensionError = class extends LocterError {
56
+ static [Symbol.hasInstance](input) {
57
+ return hasInstanceof(input, LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER);
58
+ }
59
+ constructor(input = {}) {
60
+ super(input);
61
+ markInstanceof(this, LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER);
62
+ }
63
+ };
64
+ //#endregion
65
+ //#region src/utils/has-property.ts
19
66
  function hasOwnProperty(obj, prop) {
20
- return Object.prototype.hasOwnProperty.call(obj, prop);
67
+ return Object.prototype.hasOwnProperty.call(obj, prop);
21
68
  }
22
69
  function hasStringProperty(obj, prop) {
23
- return hasOwnProperty(obj, prop) && typeof obj[prop] === 'string';
70
+ return hasOwnProperty(obj, prop) && typeof obj[prop] === "string";
24
71
  }
25
-
26
- /*
27
- * Copyright (c) 2022.
28
- * Author Peter Placzek (tada5hi)
29
- * For the full copyright and license information,
30
- * view the LICENSE file that was distributed with this source code.
31
- */ function isObject(item) {
32
- return !!item && typeof item === 'object' && !Array.isArray(item);
72
+ //#endregion
73
+ //#region src/utils/object.ts
74
+ function isObject(item) {
75
+ return !!item && typeof item === "object" && !Array.isArray(item);
33
76
  }
34
77
  function isSafeObjectKey(key) {
35
- return key !== '__proto__' && key !== 'prototype' && key !== 'constructor';
36
- }
37
-
38
- function handleException(e) {
39
- if (e instanceof Error) {
40
- throw e;
41
- }
42
- const error = new Error('Can not process thrown exception.');
43
- if (isObject(e)) {
44
- if (hasOwnProperty(e, 'message') && typeof e.message === 'string') {
45
- error.message = e.message;
46
- }
47
- if (hasOwnProperty(e, 'stack') && typeof e.stack === 'string') {
48
- error.stack = e.stack;
49
- }
50
- }
51
- throw error;
52
- }
53
-
54
- function getFileNameExtension(input, allowed) {
55
- const extension = path.extname(input);
56
- if (extension === '' || extension === '.') {
57
- return undefined;
58
- }
59
- if (typeof allowed === 'undefined' || allowed.indexOf(extension) !== -1) {
60
- return extension;
61
- }
62
- return undefined;
63
- }
64
- function removeFileNameExtension(input, extensions) {
65
- const extension = getFileNameExtension(input, extensions);
66
- if (extension) {
67
- return input.substring(0, input.length - extension.length);
68
- }
69
- return input;
78
+ return key !== "__proto__" && key !== "prototype" && key !== "constructor";
70
79
  }
71
-
72
- /*
73
- * Copyright (c) 2022.
74
- * Author Peter Placzek (tada5hi)
75
- * For the full copyright and license information,
76
- * view the LICENSE file that was distributed with this source code.
77
- */ function toArray(input) {
78
- return Array.isArray(input) ? input : [
79
- input
80
- ];
80
+ //#endregion
81
+ //#region src/utils/to-array.ts
82
+ function toArray(input) {
83
+ return Array.isArray(input) ? input : [input];
81
84
  }
82
-
85
+ //#endregion
86
+ //#region src/utils/file-path.ts
83
87
  function isFilePath(input) {
84
- const extension = path.extname(input);
85
- return extension && extension !== '';
88
+ const extension = path.extname(input);
89
+ return extension && extension !== "";
86
90
  }
87
-
88
- /*
89
- * Copyright (c) 2024.
90
- * Author Peter Placzek (tada5hi)
91
- * For the full copyright and license information,
92
- * view the LICENSE file that was distributed with this source code.
93
- */ function isJestRuntimeEnvironment() {
94
- return process.env && process.env.JEST_WORKER_ID !== undefined;
91
+ //#endregion
92
+ //#region src/utils/runtime.ts
93
+ function isJestRuntimeEnvironment() {
94
+ return process.env && process.env.JEST_WORKER_ID !== void 0;
95
+ }
96
+ function isVitestRuntimeEnvironment() {
97
+ return typeof process !== "undefined" && process.env.VITEST === "true";
95
98
  }
96
99
  function isTsNodeRuntimeEnvironment() {
97
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
98
- // @ts-ignore
99
- return !!process[Symbol.for('ts-node.register.instance')];
100
+ return !!process[Symbol.for("ts-node.register.instance")];
100
101
  }
101
-
102
+ const TSX_MARKER_REGEX = /(?:^|=|[\\/])tsx(?:$|[\\/])/;
103
+ function isTsxRuntimeEnvironment() {
104
+ if (typeof process === "undefined") return false;
105
+ if (Array.isArray(process.execArgv) && process.execArgv.some((arg) => TSX_MARKER_REGEX.test(arg))) return true;
106
+ const { _preload_modules: preloadModules } = process;
107
+ return Array.isArray(preloadModules) && preloadModules.some((el) => TSX_MARKER_REGEX.test(el));
108
+ }
109
+ //#endregion
110
+ //#region src/utils/typescript.ts
102
111
  function isTypeScriptError(input) {
103
- if (!isObject(input)) {
104
- return false;
105
- }
106
- if (typeof input.diagnosticCodes !== 'undefined') {
107
- return true;
108
- }
109
- return typeof input.message === 'string' && /TS\d+/.test(input.message);
112
+ if (!isObject(input)) return false;
113
+ if (typeof input.diagnosticCodes !== "undefined") return true;
114
+ return typeof input.message === "string" && /TS\d+/.test(input.message);
110
115
  }
111
-
116
+ //#endregion
117
+ //#region src/errors/write.ts
118
+ const LOCTER_WRITE_ERROR_MARKER = Symbol.for("@locter/write-error");
119
+ var LocterWriteError = class extends LocterError {
120
+ static [Symbol.hasInstance](input) {
121
+ return hasInstanceof(input, LOCTER_WRITE_ERROR_MARKER);
122
+ }
123
+ constructor(input = {}) {
124
+ super(input);
125
+ markInstanceof(this, LOCTER_WRITE_ERROR_MARKER);
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region src/errors/wrap.ts
130
+ const NOT_FOUND_CODES = /* @__PURE__ */ new Set([
131
+ "ENOENT",
132
+ "MODULE_NOT_FOUND",
133
+ "ERR_MODULE_NOT_FOUND"
134
+ ]);
135
+ function wrapLoaderError(input, path) {
136
+ if (input instanceof LocterError) return input;
137
+ const code = isObject(input) && hasStringProperty(input, "code") ? input.code : void 0;
138
+ const message = isObject(input) && hasStringProperty(input, "message") ? input.message : `Failed to load: ${path}`;
139
+ if (code && NOT_FOUND_CODES.has(code)) return new LocterNotFoundError({
140
+ message,
141
+ code,
142
+ cause: input,
143
+ path
144
+ });
145
+ return new LocterLoadError({
146
+ message,
147
+ code,
148
+ cause: input,
149
+ path
150
+ });
151
+ }
152
+ function wrapWriteError(input, path) {
153
+ if (input instanceof LocterError) return input;
154
+ const code = isObject(input) && hasStringProperty(input, "code") ? input.code : void 0;
155
+ return new LocterWriteError({
156
+ message: isObject(input) && hasStringProperty(input, "message") ? input.message : `Failed to write: ${path}`,
157
+ code,
158
+ cause: input,
159
+ path
160
+ });
161
+ }
162
+ //#endregion
163
+ //#region src/utils/twin.ts
164
+ /**
165
+ * Perform one effect inside a twin body: `const x = yield* op(a, s)`.
166
+ */
167
+ function* op(asyncFn, syncFn) {
168
+ return yield {
169
+ async: asyncFn,
170
+ sync: syncFn
171
+ };
172
+ }
173
+ async function runTwinAsync(body) {
174
+ let step = body.next();
175
+ while (!step.done) {
176
+ let result;
177
+ try {
178
+ result = await step.value.async();
179
+ } catch (e) {
180
+ step = body.throw(e);
181
+ continue;
182
+ }
183
+ step = body.next(result);
184
+ }
185
+ return step.value;
186
+ }
187
+ function runTwinSync(body) {
188
+ let step = body.next();
189
+ while (!step.done) {
190
+ let result;
191
+ try {
192
+ result = step.value.sync();
193
+ } catch (e) {
194
+ step = body.throw(e);
195
+ continue;
196
+ }
197
+ step = body.next(result);
198
+ }
199
+ return step.value;
200
+ }
201
+ //#endregion
202
+ //#region src/locator/utils.ts
112
203
  function buildLocatorPatterns(pattern) {
113
- return Array.isArray(pattern) ? pattern : [
114
- pattern
115
- ];
204
+ return Array.isArray(pattern) ? pattern : [pattern];
116
205
  }
117
206
  function buildLocatorOptions(options = {}) {
118
- const paths = options.path ? toArray(options.path) : [];
119
- const ignore = options.ignore ? toArray(options.ignore) : [];
120
- if (paths.length === 0) {
121
- paths.push(process.cwd());
122
- }
123
- let onlyFiles;
124
- let onlyDirectories;
125
- if (options.onlyDirectories === options.onlyFiles) {
126
- onlyDirectories = false;
127
- onlyFiles = !options.onlyDirectories;
128
- } else if (typeof options.onlyFiles === 'undefined') {
129
- onlyDirectories = options.onlyDirectories ?? false;
130
- onlyFiles = !options.onlyDirectories;
131
- } else {
132
- onlyFiles = options.onlyFiles ?? true;
133
- onlyDirectories = !options.onlyFiles;
134
- }
135
- return {
136
- path: paths,
137
- ignore,
138
- onlyDirectories,
139
- onlyFiles
140
- };
207
+ const cwd = options.cwd ? toArray(options.cwd) : [];
208
+ const ignore = options.ignore ? toArray(options.ignore) : [];
209
+ if (cwd.length === 0) cwd.push(process.cwd());
210
+ const onlyDirectories = options.onlyDirectories ?? false;
211
+ return {
212
+ cwd,
213
+ ignore,
214
+ onlyDirectories,
215
+ onlyFiles: onlyDirectories ? false : options.onlyFiles ?? true,
216
+ dot: options.dot ?? false
217
+ };
141
218
  }
142
219
  function pathToLocatorInfo(input, skipResolve) {
143
- if (!skipResolve && !path.isAbsolute(input)) {
144
- input = path.resolve(process.cwd(), input);
145
- }
146
- const info = path.parse(input);
147
- return {
148
- path: info.dir.split('/').join(path.sep),
149
- name: info.name,
150
- extension: info.ext ? info.ext : undefined
151
- };
220
+ if (!skipResolve && !path.isAbsolute(input)) input = path.resolve(process.cwd(), input);
221
+ const info = path.parse(input);
222
+ const directory = info.dir.split("/").join(path.sep);
223
+ const extension = info.ext ? info.ext : void 0;
224
+ const filePath = extension ? path.join(directory, info.name) + extension : path.join(directory, info.name);
225
+ return {
226
+ directory,
227
+ name: info.name,
228
+ extension,
229
+ path: filePath
230
+ };
152
231
  }
153
232
  function isLocatorInfo(input) {
154
- return isObject(input) && typeof input.path === 'string' && typeof input.name === 'string' && typeof input.extension === 'string';
233
+ return isObject(input) && typeof input.directory === "string" && typeof input.name === "string" && (typeof input.extension === "undefined" || typeof input.extension === "string") && typeof input.path === "string";
155
234
  }
156
235
  function buildFilePath(input) {
157
- if (typeof input === 'string') {
158
- return input;
159
- }
160
- if (input.extension) {
161
- return path.join(input.path, input.name) + input.extension;
162
- }
163
- return path.join(input.path, input.name);
164
- }
165
- function buildFilePathWithoutExtension(input) {
166
- let info;
167
- if (typeof input === 'string') {
168
- info = pathToLocatorInfo(input);
169
- } else {
170
- info = input;
171
- }
172
- return path.join(info.path, info.name);
236
+ if (typeof input === "string") return input;
237
+ return input.path;
173
238
  }
174
-
239
+ //#endregion
240
+ //#region src/locator/core.ts
241
+ function buildGlobJobs(pattern, options) {
242
+ const patterns = buildLocatorPatterns(pattern);
243
+ const opts = buildLocatorOptions(options);
244
+ const jobs = [];
245
+ for (const p of patterns) for (const cwd of opts.cwd) jobs.push({
246
+ pattern: p,
247
+ options: {
248
+ absolute: true,
249
+ cwd,
250
+ ignore: opts.ignore,
251
+ onlyFiles: opts.onlyFiles,
252
+ onlyDirectories: opts.onlyDirectories,
253
+ dot: opts.dot
254
+ }
255
+ });
256
+ return jobs;
257
+ }
258
+ function* glob(job) {
259
+ return yield* op(() => fg(job.pattern, job.options), () => fg.sync(job.pattern, job.options));
260
+ }
261
+ function* locateBody(pattern, options) {
262
+ for (const job of buildGlobJobs(pattern, options)) {
263
+ const element = (yield* glob(job)).shift();
264
+ if (element) return pathToLocatorInfo(element, true);
265
+ }
266
+ }
267
+ function* locateManyBody(pattern, options) {
268
+ const items = [];
269
+ for (const job of buildGlobJobs(pattern, options)) {
270
+ const files = yield* glob(job);
271
+ for (const file of files) items.push(pathToLocatorInfo(file, true));
272
+ }
273
+ return items;
274
+ }
275
+ //#endregion
276
+ //#region src/locator/async.ts
175
277
  async function locateMany(pattern, options) {
176
- const patterns = buildLocatorPatterns(pattern);
177
- const opts = buildLocatorOptions(options);
178
- const items = [];
179
- for(let i = 0; i < patterns.length; i++){
180
- for(let j = 0; j < opts.path.length; j++){
181
- const files = await fg(patterns[i], {
182
- absolute: true,
183
- cwd: opts.path[j],
184
- ignore: opts.ignore,
185
- onlyFiles: opts.onlyFiles,
186
- onlyDirectories: opts.onlyDirectories
187
- });
188
- for(let k = 0; k < files.length; k++){
189
- items.push(pathToLocatorInfo(files[k], true));
190
- }
191
- }
192
- }
193
- return items;
278
+ return runTwinAsync(locateManyBody(pattern, options));
194
279
  }
195
280
  async function locate(pattern, options) {
196
- const patterns = buildLocatorPatterns(pattern);
197
- const opts = buildLocatorOptions(options);
198
- for(let i = 0; i < patterns.length; i++){
199
- for(let j = 0; j < opts.path.length; j++){
200
- const files = await fg(patterns[i], {
201
- absolute: true,
202
- cwd: opts.path[j],
203
- ignore: opts.ignore,
204
- onlyFiles: opts.onlyFiles,
205
- onlyDirectories: opts.onlyDirectories
206
- });
207
- const element = files.shift();
208
- if (element) {
209
- return pathToLocatorInfo(element, true);
210
- }
211
- }
212
- }
213
- return undefined;
281
+ return runTwinAsync(locateBody(pattern, options));
214
282
  }
215
-
283
+ //#endregion
284
+ //#region src/locator/sync.ts
216
285
  function locateManySync(pattern, options) {
217
- const patterns = buildLocatorPatterns(pattern);
218
- const opts = buildLocatorOptions(options);
219
- const items = [];
220
- for(let i = 0; i < patterns.length; i++){
221
- for(let j = 0; j < opts.path.length; j++){
222
- const files = fg.sync(patterns[i], {
223
- absolute: true,
224
- cwd: opts.path[j],
225
- ignore: opts.ignore,
226
- onlyFiles: opts.onlyFiles,
227
- onlyDirectories: opts.onlyDirectories
228
- });
229
- for(let k = 0; k < files.length; k++){
230
- items.push(pathToLocatorInfo(files[k], true));
231
- }
232
- }
233
- }
234
- return items;
286
+ return runTwinSync(locateManyBody(pattern, options));
235
287
  }
236
288
  function locateSync(pattern, options) {
237
- const patterns = buildLocatorPatterns(pattern);
238
- const opts = buildLocatorOptions(options);
239
- for(let i = 0; i < patterns.length; i++){
240
- for(let j = 0; j < opts.path.length; j++){
241
- const files = fg.sync(patterns[i], {
242
- absolute: true,
243
- cwd: opts.path[j],
244
- ignore: opts.ignore,
245
- onlyFiles: opts.onlyFiles,
246
- onlyDirectories: opts.onlyDirectories
247
- });
248
- const element = files.shift();
249
- if (element) {
250
- return pathToLocatorInfo(element, true);
251
- }
252
- }
253
- }
254
- return undefined;
289
+ return runTwinSync(locateBody(pattern, options));
255
290
  }
256
-
257
- /*
258
- * Copyright (c) 2022-2023.
259
- * Author Peter Placzek (tada5hi)
260
- * For the full copyright and license information,
261
- * view the LICENSE file that was distributed with this source code.
262
- */ // Based on https://github.com/unjs/rc9 (MIT)
263
- class ConfLoader {
264
- async execute(input) {
265
- const filePath = buildFilePath(input);
266
- try {
267
- const file = await fs.promises.readFile(filePath, {
268
- encoding: 'utf-8'
269
- });
270
- return this.parse(file);
271
- } catch (e) {
272
- return handleException(e);
273
- }
274
- }
275
- executeSync(input) {
276
- const filePath = buildFilePath(input);
277
- try {
278
- const file = fs.readFileSync(filePath, {
279
- encoding: 'utf-8'
280
- });
281
- return this.parse(file);
282
- } catch (e) {
283
- return handleException(e);
284
- }
285
- }
286
- parse(contents) {
287
- const config = {};
288
- const lines = contents.split(/\n|\r|\r\n/);
289
- for(let i = 0; i < lines.length; i++){
290
- const match = lines[i].match(/^\s*([^\s=]+)\s*=\s*(.*)?\s*$/);
291
- if (!match) {
292
- continue;
293
- }
294
- // Key
295
- const key = match[1];
296
- if (!key || !isSafeObjectKey(key)) {
297
- continue;
298
- }
299
- const value = destr(match[2].trim());
300
- if (key.endsWith('[]')) {
301
- const arrKey = key.slice(0, Math.max(0, key.length - 2));
302
- config[arrKey] = (config[arrKey] || []).concat(value);
303
- continue;
304
- }
305
- config[key] = value;
306
- }
307
- return flat.unflatten(config, {
308
- overwrite: true
309
- });
310
- }
291
+ //#endregion
292
+ //#region src/locator/up.ts
293
+ function startWalk(options) {
294
+ const { cwd, stopAt, ...baseOptions } = options;
295
+ const current = path.resolve(cwd ?? process.cwd());
296
+ return {
297
+ current,
298
+ stopAt: stopAt ? path.resolve(current, stopAt) : path.parse(current).root,
299
+ baseOptions
300
+ };
311
301
  }
312
-
302
+ function nextDirectory(current, stopAt) {
303
+ if (current === stopAt) return;
304
+ const parent = path.dirname(current);
305
+ if (parent === current) return;
306
+ return parent;
307
+ }
308
+ function* locateUpBody(pattern, options) {
309
+ let walk = startWalk(options);
310
+ while (true) {
311
+ const found = yield* locateBody(pattern, {
312
+ ...walk.baseOptions,
313
+ cwd: walk.current
314
+ });
315
+ if (found) return found;
316
+ const next = nextDirectory(walk.current, walk.stopAt);
317
+ if (!next) return;
318
+ walk = {
319
+ ...walk,
320
+ current: next
321
+ };
322
+ }
323
+ }
324
+ async function locateUp(pattern, options = {}) {
325
+ return runTwinAsync(locateUpBody(pattern, options));
326
+ }
327
+ function locateUpSync(pattern, options = {}) {
328
+ return runTwinSync(locateUpBody(pattern, options));
329
+ }
330
+ //#endregion
331
+ //#region src/format/text-file/reader.ts
332
+ /**
333
+ * Base class for readers of text-based file formats: reads the file as
334
+ * UTF-8, delegates to `parse`, and wraps I/O and parse errors in typed
335
+ * LocterError subclasses. Subclasses implement `parse` only — both
336
+ * `read` variants are derived from the same body.
337
+ */
338
+ var TextFileReader = class {
339
+ async read(input) {
340
+ return runTwinAsync(this.body(input));
341
+ }
342
+ readSync(input) {
343
+ return runTwinSync(this.body(input));
344
+ }
345
+ *body(input) {
346
+ try {
347
+ const content = yield* op(() => fs.promises.readFile(input, { encoding: "utf-8" }), () => fs.readFileSync(input, { encoding: "utf-8" }));
348
+ return this.parse(content);
349
+ } catch (e) {
350
+ throw wrapLoaderError(e, input);
351
+ }
352
+ }
353
+ };
354
+ //#endregion
355
+ //#region src/format/text-file/writer.ts
356
+ function isNotFound(e) {
357
+ return e instanceof Error && "code" in e && e.code === "ENOENT";
358
+ }
359
+ /**
360
+ * Base class for writers of text-based file formats: serializes via
361
+ * `stringify`, creates missing parent directories, writes the file as
362
+ * UTF-8 (always terminated by a single trailing newline), and wraps
363
+ * failures in LocterWriteError. Subclasses implement `stringify` only —
364
+ * both `write` variants are derived from the same body.
365
+ */
366
+ var TextFileWriter = class {
367
+ /**
368
+ * Opt-in: when true, the body reads the target file first (undefined
369
+ * if absent) and passes its content to `stringify` as `existing` —
370
+ * the hook behind format-preserving write-back (YAML comment
371
+ * grafting, JSON indent detection).
372
+ */
373
+ get usesExistingContent() {
374
+ return false;
375
+ }
376
+ async write(input, value) {
377
+ return runTwinAsync(this.body(input, value));
378
+ }
379
+ writeSync(input, value) {
380
+ runTwinSync(this.body(input, value));
381
+ }
382
+ *body(input, value) {
383
+ const filePath = input;
384
+ try {
385
+ let existing;
386
+ if (this.usesExistingContent) existing = yield* op(() => fs.promises.readFile(filePath, { encoding: "utf-8" }).catch((e) => {
387
+ if (isNotFound(e)) return;
388
+ throw e;
389
+ }), () => {
390
+ try {
391
+ return fs.readFileSync(filePath, { encoding: "utf-8" });
392
+ } catch (e) {
393
+ if (isNotFound(e)) return;
394
+ throw e;
395
+ }
396
+ });
397
+ let content = this.stringify(value, existing);
398
+ if (!content.endsWith("\n")) content += "\n";
399
+ const directory = path.dirname(filePath);
400
+ yield* op(async () => {
401
+ await fs.promises.mkdir(directory, { recursive: true });
402
+ }, () => {
403
+ fs.mkdirSync(directory, { recursive: true });
404
+ });
405
+ yield* op(() => fs.promises.writeFile(filePath, content, { encoding: "utf-8" }), () => fs.writeFileSync(filePath, content, { encoding: "utf-8" }));
406
+ } catch (e) {
407
+ throw wrapWriteError(e, filePath);
408
+ }
409
+ }
410
+ };
411
+ //#endregion
412
+ //#region src/format/built-in/conf/reader.ts
413
+ var ConfReader = class extends TextFileReader {
414
+ parse(contents) {
415
+ const config = {};
416
+ const lines = contents.split(/\n|\r|\r\n/);
417
+ for (const line of lines) {
418
+ const match = line.match(/^\s*([^\s=]+)\s*=\s*(.*)?\s*$/);
419
+ if (!match) continue;
420
+ const key = match[1];
421
+ const rawValue = match[2];
422
+ if (!key || !isSafeObjectKey(key)) continue;
423
+ const value = destr(rawValue ? rawValue.trim() : "");
424
+ if (key.endsWith("[]")) {
425
+ const arrKey = key.slice(0, Math.max(0, key.length - 2));
426
+ config[arrKey] = (config[arrKey] || []).concat(value);
427
+ continue;
428
+ }
429
+ config[key] = value;
430
+ }
431
+ return unflatten(config, { overwrite: true });
432
+ }
433
+ };
434
+ //#endregion
435
+ //#region src/format/built-in/conf/writer.ts
436
+ function serializeValue(value) {
437
+ return typeof value === "string" ? value : JSON.stringify(value);
438
+ }
439
+ /**
440
+ * Serializes an object to `key=value` lines (the inverse of ConfReader):
441
+ * nested objects become dot-separated keys, arrays become repeated
442
+ * `key[]=` lines. Round-trips are structural, not textual — comments,
443
+ * blank lines, and line order of an existing file are not preserved,
444
+ * and a string that parses as another type (e.g. '123') reads back as
445
+ * that type.
446
+ */
447
+ var ConfWriter = class extends TextFileWriter {
448
+ stringify(value) {
449
+ if (!isObject(value)) throw new Error("The value must be an object for the conf format.");
450
+ const flat = flatten(value, { safe: true });
451
+ const lines = [];
452
+ for (const key of Object.keys(flat)) {
453
+ const entry = flat[key];
454
+ if (typeof entry === "undefined") continue;
455
+ if (Array.isArray(entry)) {
456
+ for (const item of entry) lines.push(`${key}[]=${serializeValue(item)}`);
457
+ continue;
458
+ }
459
+ lines.push(`${key}=${serializeValue(entry)}`);
460
+ }
461
+ return lines.join("\n");
462
+ }
463
+ };
464
+ //#endregion
465
+ //#region src/format/built-in/json/reader.ts
466
+ var JSONReader = class extends TextFileReader {
467
+ parse(content) {
468
+ return JSON.parse(content);
469
+ }
470
+ };
471
+ //#endregion
472
+ //#region src/format/built-in/json/writer.ts
473
+ function detectIndent(content) {
474
+ const match = /^([ \t]+)\S/m.exec(content);
475
+ return match ? match[1] : void 0;
476
+ }
477
+ var JSONWriter = class extends TextFileWriter {
478
+ options;
479
+ constructor(options = {}) {
480
+ super();
481
+ this.options = options;
482
+ }
483
+ get usesExistingContent() {
484
+ return this.options.indent === "auto";
485
+ }
486
+ stringify(value, existing) {
487
+ let indent;
488
+ if (this.options.indent === "auto") indent = existing ? detectIndent(existing) : void 0;
489
+ else indent = this.options.indent;
490
+ indent = indent ?? 4;
491
+ const output = JSON.stringify(value, null, indent);
492
+ if (typeof output === "undefined") throw new Error("The value is not JSON-serializable.");
493
+ return output;
494
+ }
495
+ };
496
+ //#endregion
497
+ //#region src/format/built-in/module/constants.ts
498
+ /**
499
+ * Single source of truth for the extensions handled by the module loader:
500
+ * consumed by BOTH the built-in routing registry and the jiti config.
501
+ */
502
+ const MODULE_FILE_EXTENSIONS = [
503
+ ".js",
504
+ ".mjs",
505
+ ".mts",
506
+ ".cjs",
507
+ ".cts",
508
+ ".ts"
509
+ ];
510
+ //#endregion
511
+ //#region src/format/built-in/module/reader.ts
512
+ const require = createRequire(import.meta.url);
513
+ function originalPath(data) {
514
+ return typeof data === "string" ? data : buildFilePath(data);
515
+ }
516
+ function isUnrecoverableError(error) {
517
+ const underlying = error instanceof LocterError ? error.cause : error;
518
+ return underlying instanceof SyntaxError || underlying instanceof ReferenceError || isTypeScriptError(underlying);
519
+ }
520
+ var ModuleReader = class {
521
+ instance;
522
+ loadFn;
523
+ loadSyncFn;
524
+ constructor(options = {}) {
525
+ this.instance = createJiti(void 0, { extensions: [...MODULE_FILE_EXTENSIONS] });
526
+ this.loadFn = options.load;
527
+ this.loadSyncFn = options.loadSync;
528
+ }
529
+ /**
530
+ * Returns the previous configuration, so callers can restore it.
531
+ */
532
+ configure(options) {
533
+ const previous = {
534
+ load: this.loadFn,
535
+ loadSync: this.loadSyncFn
536
+ };
537
+ if ("load" in options) this.loadFn = options.load;
538
+ if ("loadSync" in options) this.loadSyncFn = options.loadSync;
539
+ return previous;
540
+ }
541
+ /**
542
+ * Returns the raw module value (import / require / jiti output) —
543
+ * normalization to a module record happens once, at the
544
+ * FormatRegistry boundary.
545
+ *
546
+ * read/readSync are deliberately NOT derived from a shared body
547
+ * (unlike every other sync/async twin in this package): their
548
+ * recovery paths differ. The async variant falls back to loadSync
549
+ * under ts-node (jiti's async path does not cooperate with ts-node)
550
+ * and to the jiti instance otherwise; the sync variant only has the
551
+ * jiti fallback. The divergence is pinned by the fallback tests in
552
+ * test/unit/format/module.spec.ts.
553
+ */
554
+ async read(input) {
555
+ try {
556
+ return await this.load(input);
557
+ } catch (e) {
558
+ if (isUnrecoverableError(e)) throw e;
559
+ if (isTsNodeRuntimeEnvironment()) return this.loadSync(input);
560
+ return this.instance(input);
561
+ }
562
+ }
563
+ readSync(input) {
564
+ try {
565
+ return this.loadSync(input);
566
+ } catch (e) {
567
+ if (isUnrecoverableError(e)) throw e;
568
+ return this.instance(input);
569
+ }
570
+ }
571
+ async load(data, options = {}) {
572
+ const id = this.build(data, options);
573
+ try {
574
+ if (this.loadFn) return await this.loadFn(id);
575
+ if (isJestRuntimeEnvironment()) return require(id);
576
+ return await import(id);
577
+ } catch (e) {
578
+ /* istanbul ignore next */
579
+ if (!options.withFilePrefix && isObject(e) && hasStringProperty(e, "code") && (e.code === "ERR_UNSUPPORTED_ESM_URL_SCHEME" || e.code === "UNSUPPORTED_ESM_URL_SCHEME")) return this.load(data, {
580
+ ...options,
581
+ withFilePrefix: true
582
+ });
583
+ throw wrapLoaderError(e, originalPath(data));
584
+ }
585
+ }
586
+ loadSync(data, options = {}) {
587
+ const id = this.build(data, options);
588
+ try {
589
+ if (this.loadSyncFn) return this.loadSyncFn(id);
590
+ return require(id);
591
+ } catch (e) {
592
+ throw wrapLoaderError(e, originalPath(data));
593
+ }
594
+ }
595
+ build(data, options = {}) {
596
+ if (isLocatorInfo(data) || isFilePath(data)) {
597
+ if (typeof data !== "string") data = buildFilePath(data);
598
+ if (options.withFilePrefix) data = pathToFileURL(data).href;
599
+ }
600
+ return data;
601
+ }
602
+ };
603
+ //#endregion
604
+ //#region src/format/built-in/module/utils.ts
605
+ /**
606
+ * Brand carried by every record locter's read() produces. Deliberately a
607
+ * module-private Symbol (NOT Symbol.for) so arbitrary parsed data cannot
608
+ * forge it — unlike `__esModule`, its presence is trustworthy. write()
609
+ * uses it to unwrap `.default` on round-trips.
610
+ */
611
+ const MODULE_RECORD = Symbol("locter.record");
612
+ /**
613
+ * True when the value is a record produced by locter's read()/toModuleRecord.
614
+ * Detection is brand-based; a plain object with an `__esModule` key is NOT
615
+ * a module record.
616
+ */
617
+ function isModuleRecord(input) {
618
+ return isObject(input) && MODULE_RECORD in input;
619
+ }
620
+ function brand(input) {
621
+ if (isObject(input) && Object.isExtensible(input)) Object.defineProperty(input, MODULE_RECORD, { value: true });
622
+ return input;
623
+ }
624
+ /**
625
+ * Detect the transpiler interop marker (`__esModule`). Only meaningful for
626
+ * values that came out of a module system — arbitrary parsed data can carry
627
+ * an `__esModule` key without being a module, so callers must decide by
628
+ * provenance whether this check applies (see FormatRegistry).
629
+ */
313
630
  function isESModule(input) {
314
- return isObject(input) && // eslint-disable-next-line no-underscore-dangle
315
- typeof input.__esModule !== 'undefined';
631
+ return isObject(input) && typeof input.__esModule !== "undefined";
632
+ }
633
+ /**
634
+ * Wrap an arbitrary value in a frozen module record: enumerable keys are
635
+ * re-exposed as named exports and `default` is the value itself.
636
+ */
637
+ function createModuleRecord(data) {
638
+ const output = Object.create(null, {
639
+ __esModule: { value: true },
640
+ [Symbol.toStringTag]: { value: "Module" },
641
+ [MODULE_RECORD]: { value: true }
642
+ });
643
+ if (isObject(data)) for (const key in data) {
644
+ if (hasOwnProperty(output, key)) continue;
645
+ let descriptor = Object.getOwnPropertyDescriptor(data, key);
646
+ if (!descriptor || "get" in descriptor || descriptor.writable || descriptor.configurable) descriptor = {
647
+ enumerable: true,
648
+ get() {
649
+ return data[key];
650
+ }
651
+ };
652
+ Object.defineProperty(output, key, descriptor);
653
+ }
654
+ if (!hasOwnProperty(output, "default")) Object.defineProperty(output, "default", {
655
+ value: data,
656
+ enumerable: true
657
+ });
658
+ return Object.freeze(output);
316
659
  }
317
- // https://2ality.com/2017/01/babel-esm-spec-mode.html
660
+ /**
661
+ * Normalize module-loader output to a module record. Values already carrying
662
+ * the `__esModule` marker pass through (module provenance makes the marker
663
+ * trustworthy); everything else is wrapped via createModuleRecord.
664
+ *
665
+ * https://2ality.com/2017/01/babel-esm-spec-mode.html
666
+ */
318
667
  function toModuleRecord(data) {
319
- if (isESModule(data)) {
320
- // @see https://github.com/testing-library/user-event/issues/813
321
- // @see https://stackoverflow.com/questions/62717394/export-default-class-exports-double-nested-default
322
- if (isESModule(data.default) && data.default.default) {
323
- return {
324
- ...data,
325
- default: data.default.default
326
- };
327
- }
328
- return data;
329
- }
330
- const output = Object.create(null, {
331
- __esModule: {
332
- value: true
333
- },
334
- [Symbol.toStringTag]: {
335
- value: 'Module'
336
- }
337
- });
338
- if (isObject(data)) {
339
- // eslint-disable-next-line no-restricted-syntax
340
- for(const key in data){
341
- if (hasOwnProperty(output, key)) {
342
- continue;
343
- }
344
- let descriptor = Object.getOwnPropertyDescriptor(data, key);
345
- if (!descriptor || 'get' in descriptor || descriptor.writable || descriptor.configurable) {
346
- descriptor = {
347
- enumerable: true,
348
- get () {
349
- return data[key];
350
- }
351
- };
352
- }
353
- Object.defineProperty(output, key, descriptor);
354
- }
355
- }
356
- if (!hasOwnProperty(output, 'default')) {
357
- Object.defineProperty(output, 'default', {
358
- value: data,
359
- enumerable: true
360
- });
361
- }
362
- return Object.freeze(output);
668
+ if (isESModule(data)) {
669
+ if (isESModule(data.default) && data.default.default) return brand({
670
+ ...data,
671
+ default: data.default.default
672
+ });
673
+ return brand(data);
674
+ }
675
+ return createModuleRecord(data);
363
676
  }
364
677
  function getModuleExport(data, filterFn) {
365
- const keys = Object.keys(data);
366
- for(let i = 0; i < keys.length; i++){
367
- if (filterFn(keys[i], data[keys[i]])) {
368
- return {
369
- key: keys[i],
370
- value: data[keys[i]]
371
- };
372
- }
373
- }
374
- return undefined;
678
+ const keys = Object.keys(data);
679
+ for (const key of keys) if (filterFn(key, data[key])) return {
680
+ key,
681
+ value: data[key]
682
+ };
375
683
  }
376
-
377
- class ModuleLoader {
378
- async execute(input) {
379
- let output;
380
- try {
381
- output = await this.load(input);
382
- } catch (e) {
383
- if (e instanceof SyntaxError || e instanceof ReferenceError || isTypeScriptError(e)) {
384
- throw e;
385
- }
386
- // jiti + ts-node
387
- // issue: https://github.com/nuxt/bridge/issues/228
388
- if (isTsNodeRuntimeEnvironment()) {
389
- output = this.loadSync(input);
390
- } else {
391
- output = this.instance(input);
392
- }
393
- }
394
- return toModuleRecord(output);
395
- }
396
- executeSync(input) {
397
- let output;
398
- try {
399
- output = this.loadSync(input);
400
- } catch (e) {
401
- if (e instanceof SyntaxError || e instanceof ReferenceError || isTypeScriptError(e)) {
402
- throw e;
403
- }
404
- output = this.instance(input);
405
- }
406
- return toModuleRecord(output);
407
- }
408
- // ---------------------------------------------------------------------------
409
- async load(data, options = {}) {
410
- const id = this.build(data, options);
411
- try {
412
- // segmentation fault
413
- // issue: https://github.com/nodejs/node/issues/35889
414
- if (isJestRuntimeEnvironment()) {
415
- // eslint-disable-next-line global-require,import/no-dynamic-require
416
- return require(id);
417
- }
418
- return await import(id);
419
- } catch (e) {
420
- /* istanbul ignore next */ if (isObject(e) && hasStringProperty(e, 'code')) {
421
- if (!options.withFilePrefix && (e.code === 'ERR_UNSUPPORTED_ESM_URL_SCHEME' || e.code === 'UNSUPPORTED_ESM_URL_SCHEME')) {
422
- return this.load(data, {
423
- ...options,
424
- withFilePrefix: true
425
- });
426
- }
427
- throw new BaseError({
428
- code: e.code,
429
- message: hasStringProperty(e, 'message') ? e.message : undefined,
430
- stack: hasStringProperty(e, 'stack') ? e.stack : undefined
431
- });
432
- }
433
- /* istanbul ignore next */ return handleException(e);
434
- }
435
- }
436
- loadSync(data, options = {}) {
437
- const id = this.build(data, options);
438
- try {
439
- // eslint-disable-next-line global-require,import/no-dynamic-require
440
- return require(id);
441
- } catch (e) {
442
- /* istanbul ignore next */ if (isObject(e) && hasStringProperty(e, 'code')) {
443
- throw new BaseError({
444
- code: e.code,
445
- message: hasStringProperty(e, 'message') ? e.message : undefined,
446
- stack: hasStringProperty(e, 'stack') ? e.stack : undefined
447
- });
448
- }
449
- return handleException(e);
450
- }
451
- }
452
- build(data, options = {}) {
453
- if (isLocatorInfo(data) || isFilePath(data)) {
454
- if (typeof data !== 'string') {
455
- data = buildFilePath(data);
456
- }
457
- if (options.withFilePrefix) {
458
- data = pathToFileURL(data).href;
459
- }
460
- }
461
- return data;
462
- }
463
- constructor(){
464
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
465
- // @ts-ignore
466
- this.instance = createJiti(undefined, {
467
- extensions: [
468
- '.js',
469
- '.mjs',
470
- '.mts',
471
- '.cjs',
472
- '.cts',
473
- '.ts'
474
- ]
475
- });
476
- }
684
+ //#endregion
685
+ //#region src/format/built-in/yaml/reader.ts
686
+ var YAMLReader = class extends TextFileReader {
687
+ parse(content) {
688
+ return parse(content);
689
+ }
690
+ };
691
+ //#endregion
692
+ //#region src/format/built-in/yaml/writer.ts
693
+ function pairKey(key) {
694
+ return isScalar(key) ? String(key.value) : String(key);
477
695
  }
478
-
479
- class JSONLoader {
480
- async execute(input) {
481
- const filePath = buildFilePath(input);
482
- try {
483
- const file = await fs.promises.readFile(filePath, {
484
- encoding: 'utf-8'
485
- });
486
- return JSON.parse(file);
487
- } catch (e) {
488
- return handleException(e);
489
- }
490
- }
491
- executeSync(input) {
492
- const filePath = buildFilePath(input);
493
- try {
494
- const file = fs.readFileSync(filePath, {
495
- encoding: 'utf-8'
496
- });
497
- return JSON.parse(file);
498
- } catch (e) {
499
- return handleException(e);
500
- }
501
- }
696
+ /**
697
+ * Merge a plain object into an existing YAML map node: added/changed keys
698
+ * are set, absent keys are deleted, and nested objects recurse — so
699
+ * comments, anchors, and formatting attached to surviving keys are
700
+ * preserved. Arrays, scalars, and type changes replace the node wholesale
701
+ * (comments inside the replaced subtree are lost).
702
+ */
703
+ function graftMap(doc, node, value) {
704
+ for (const pair of [...node.items]) if (!hasOwnProperty(value, pairKey(pair.key))) node.delete(pair.key);
705
+ for (const key of Object.keys(value)) {
706
+ const entry = value[key];
707
+ const current = node.get(key, true);
708
+ if (isMap(current) && isObject(entry)) {
709
+ graftMap(doc, current, entry);
710
+ continue;
711
+ }
712
+ const replacement = doc.createNode(entry);
713
+ if (isNode(current)) {
714
+ replacement.commentBefore = current.commentBefore;
715
+ replacement.comment = current.comment;
716
+ replacement.spaceBefore = current.spaceBefore;
717
+ }
718
+ node.set(doc.createNode(key), replacement);
719
+ }
502
720
  }
503
-
504
- class YAMLLoader {
505
- async execute(input) {
506
- const filePath = buildFilePath(input);
507
- try {
508
- const file = await fs.promises.readFile(filePath, {
509
- encoding: 'utf-8'
510
- });
511
- return parse(file);
512
- } catch (e) {
513
- return handleException(e);
514
- }
515
- }
516
- executeSync(input) {
517
- const filePath = buildFilePath(input);
518
- try {
519
- const file = fs.readFileSync(filePath, {
520
- encoding: 'utf-8'
521
- });
522
- return parse(file);
523
- } catch (e) {
524
- return handleException(e);
525
- }
526
- }
721
+ /**
722
+ * Writes YAML preserving hand-edited structure where possible: when the
723
+ * target file already exists, the new value is grafted into its parsed
724
+ * document, so comments and anchors attached to surviving keys survive
725
+ * the write-back. New files are plain `yaml.stringify` output. An
726
+ * existing file that fails to parse throws (wrapped as LocterWriteError)
727
+ * instead of being silently overwritten.
728
+ */
729
+ var YAMLWriter = class extends TextFileWriter {
730
+ options;
731
+ constructor(options = {}) {
732
+ super();
733
+ this.options = options;
734
+ }
735
+ get usesExistingContent() {
736
+ return true;
737
+ }
738
+ stringify(value, existing) {
739
+ if (typeof existing === "undefined") return stringify(value, this.options);
740
+ const doc = parseDocument(existing);
741
+ if (doc.errors.length > 0) throw doc.errors[0];
742
+ if (isMap(doc.contents) && isObject(value)) graftMap(doc, doc.contents, value);
743
+ else doc.contents = doc.createNode(value);
744
+ return doc.toString(this.options);
745
+ }
746
+ };
747
+ //#endregion
748
+ //#region src/format/built-in/registry.ts
749
+ /**
750
+ * Every built-in format, in one place. Keys are the format ids; each entry
751
+ * couples routing and construction. The id union, the extension → format
752
+ * table, lazy caching, and the typed `builtInReader()` accessor are all
753
+ * DERIVED from this object. Adding a format = adding one entry.
754
+ *
755
+ * Intentionally NOT re-exported from any barrel (internal API).
756
+ */
757
+ const BUILT_IN_PRESETS = {
758
+ module: {
759
+ extensions: MODULE_FILE_EXTENSIONS,
760
+ reader: () => new ModuleReader()
761
+ },
762
+ conf: {
763
+ extensions: [".conf"],
764
+ reader: () => new ConfReader(),
765
+ writer: () => new ConfWriter()
766
+ },
767
+ json: {
768
+ extensions: [".json"],
769
+ reader: () => new JSONReader(),
770
+ writer: () => new JSONWriter()
771
+ },
772
+ yaml: {
773
+ extensions: [".yml", ".yaml"],
774
+ reader: () => new YAMLReader(),
775
+ writer: () => new YAMLWriter()
776
+ }
777
+ };
778
+ //#endregion
779
+ //#region src/format/registry/module.ts
780
+ function compileSlot(slot) {
781
+ if (typeof slot === "undefined") return;
782
+ if (typeof slot !== "function") return () => slot;
783
+ const factory = slot;
784
+ let cached;
785
+ return () => {
786
+ if (!cached) cached = factory();
787
+ return cached;
788
+ };
527
789
  }
528
-
529
- /*
530
- * Copyright (c) 2023.
531
- * Author Peter Placzek (tada5hi)
532
- * For the full copyright and license information,
533
- * view the LICENSE file that was distributed with this source code.
534
- */ var LoaderId = /*#__PURE__*/ function(LoaderId) {
535
- LoaderId["MODULE"] = "module";
536
- LoaderId["JSON"] = "json";
537
- LoaderId["CONF"] = "conf";
538
- LoaderId["YAML"] = "yaml";
539
- return LoaderId;
540
- }({});
541
-
542
- class LoaderManager {
543
- register(test, loader) {
544
- if (typeof loader !== 'undefined') {
545
- this.rules.push({
546
- test,
547
- loader
548
- });
549
- return;
550
- }
551
- this.rules.push(test);
552
- }
553
- async execute(input) {
554
- const id = this.findLoader(input);
555
- if (!id) {
556
- const info = pathToLocatorInfo(input);
557
- throw new Error(`No loader registered for extension: "${info.extension}"`);
558
- }
559
- const loader = this.resolve(id);
560
- return loader.execute(input);
561
- }
562
- executeSync(input) {
563
- const id = this.findLoader(input);
564
- if (!id) {
565
- const info = pathToLocatorInfo(input);
566
- throw new Error(`No loader registered for extension: ${info.extension || 'unknown'}`);
567
- }
568
- const loader = this.resolve(id);
569
- return loader.executeSync(input);
570
- }
571
- findLoader(input) {
572
- if (!isFilePath(input)) {
573
- return LoaderId.MODULE;
574
- }
575
- const info = pathToLocatorInfo(input);
576
- for(let i = 0; i < this.rules.length; i++){
577
- const { test } = this.rules[i];
578
- if (Array.isArray(test)) {
579
- if (info.extension && test.indexOf(info.extension) !== -1) {
580
- return this.rules[i].loader;
581
- }
582
- } else if (test.test(buildFilePath(info))) {
583
- return this.rules[i].loader;
584
- }
585
- }
586
- return undefined;
587
- }
588
- /**
589
- * Resolve loader by id.
590
- *
591
- * @param id
592
- */ resolve(id) {
593
- if (typeof id !== 'string') {
594
- return id;
595
- }
596
- if (Object.prototype.hasOwnProperty.call(this.loaders, id)) {
597
- return this.loaders[id];
598
- }
599
- let loader;
600
- // built-in
601
- switch(id){
602
- case LoaderId.CONF:
603
- {
604
- loader = new ConfLoader();
605
- break;
606
- }
607
- case LoaderId.MODULE:
608
- {
609
- loader = new ModuleLoader();
610
- break;
611
- }
612
- case LoaderId.JSON:
613
- {
614
- loader = new JSONLoader();
615
- break;
616
- }
617
- case LoaderId.YAML:
618
- {
619
- loader = new YAMLLoader();
620
- break;
621
- }
622
- /* istanbul ignore next */ default:
623
- {
624
- const pluginPath = this.normalizePath(id);
625
- const moduleLoader = this.resolve(LoaderId.MODULE);
626
- loader = moduleLoader.executeSync(pluginPath);
627
- break;
628
- }
629
- }
630
- if (typeof loader !== 'undefined') {
631
- this.loaders[id] = loader;
632
- return loader;
633
- }
634
- throw new Error(`The loader ${id} could not be resolved.`);
635
- }
636
- /* istanbul ignore next */ normalizePath(input) {
637
- if (path.isAbsolute(input) || input.startsWith('./')) {
638
- return input;
639
- }
640
- if (input.startsWith('module:')) {
641
- return input.substring(0, 'module:'.length);
642
- }
643
- if (!input.startsWith('@')) {
644
- return `@locter/${input}`;
645
- }
646
- return input;
647
- }
648
- constructor(){
649
- this.loaders = {};
650
- this.rules = [
651
- {
652
- test: [
653
- '.js',
654
- '.mjs',
655
- '.mts',
656
- '.cjs',
657
- '.cts',
658
- '.ts'
659
- ],
660
- loader: LoaderId.MODULE
661
- },
662
- {
663
- test: [
664
- '.conf'
665
- ],
666
- loader: LoaderId.CONF
667
- },
668
- {
669
- test: [
670
- '.json'
671
- ],
672
- loader: LoaderId.JSON
673
- },
674
- {
675
- test: [
676
- '.yml',
677
- '.yaml'
678
- ],
679
- loader: LoaderId.YAML
680
- }
681
- ];
682
- }
683
- }
684
-
790
+ var FormatRegistry = class {
791
+ /**
792
+ * User rules only — built-ins live in the extension table instead.
793
+ */
794
+ rules;
795
+ /**
796
+ * Lazy per-instance cache of built-in reader instances.
797
+ */
798
+ builtInReaderCache;
799
+ /**
800
+ * Lazy per-instance cache of built-in writer instances.
801
+ */
802
+ builtInWriterCache;
803
+ /**
804
+ * extension → built-in format id, precomputed once from BUILT_IN_PRESETS.
805
+ */
806
+ builtInExtensions;
807
+ ruleCounter;
808
+ constructor(options = {}) {
809
+ this.rules = [];
810
+ this.builtInReaderCache = /* @__PURE__ */ new Map();
811
+ this.builtInWriterCache = /* @__PURE__ */ new Map();
812
+ this.builtInExtensions = /* @__PURE__ */ new Map();
813
+ this.ruleCounter = 0;
814
+ const ids = Object.keys(BUILT_IN_PRESETS);
815
+ for (const id of ids) for (const extension of BUILT_IN_PRESETS[id].extensions) {
816
+ const existing = this.builtInExtensions.get(extension);
817
+ if (existing) throw new LocterError({ message: `Extension ${extension} is claimed by two built-in formats: ${existing}, ${id}.` });
818
+ this.builtInExtensions.set(extension, id);
819
+ }
820
+ if (options.rules) for (const rule of options.rules) this.register(rule);
821
+ }
822
+ register(rule) {
823
+ if (typeof rule.id !== "undefined" && hasOwnProperty(BUILT_IN_PRESETS, rule.id)) throw new LocterError({ message: `The id ${rule.id} is reserved by a built-in format.` });
824
+ if (typeof rule.reader === "undefined" && typeof rule.writer === "undefined") throw new LocterError({ message: "A rule requires at least one of: reader, writer." });
825
+ const id = rule.id ?? this.generateRuleId();
826
+ const entry = this.compile(id, rule);
827
+ const index = this.rules.findIndex((item) => item.id === id);
828
+ if (index === -1) this.rules.push(entry);
829
+ else this.rules[index] = entry;
830
+ return {
831
+ id,
832
+ test: rule.test,
833
+ builtIn: false
834
+ };
835
+ }
836
+ /**
837
+ * Remove a user-registered rule by id. Built-in ids cannot be
838
+ * unregistered (returns false).
839
+ */
840
+ unregister(id) {
841
+ const index = this.rules.findIndex((item) => item.id === id);
842
+ if (index === -1) return false;
843
+ this.rules.splice(index, 1);
844
+ return true;
845
+ }
846
+ has(id) {
847
+ if (this.rules.some((item) => item.id === id)) return true;
848
+ return hasOwnProperty(BUILT_IN_PRESETS, id);
849
+ }
850
+ /**
851
+ * All registrations in effective match order: user rules
852
+ * (registration order) first, then the built-ins.
853
+ */
854
+ entries() {
855
+ const output = this.rules.map((item) => ({
856
+ id: item.id,
857
+ test: item.test,
858
+ builtIn: false
859
+ }));
860
+ const ids = Object.keys(BUILT_IN_PRESETS);
861
+ for (const id of ids) output.push({
862
+ id,
863
+ test: [...BUILT_IN_PRESETS[id].extensions],
864
+ builtIn: true
865
+ });
866
+ return output;
867
+ }
868
+ /**
869
+ * Restore the registry to its construction state: drop all user rules
870
+ * and evict every cached reader instance (including a module reader
871
+ * configured via setModuleReader / configure).
872
+ */
873
+ reset() {
874
+ this.rules = [];
875
+ this.builtInReaderCache.clear();
876
+ this.builtInWriterCache.clear();
877
+ this.ruleCounter = 0;
878
+ }
879
+ async read(input) {
880
+ return runTwinAsync(this.readBody(input));
881
+ }
882
+ readSync(input) {
883
+ return runTwinSync(this.readBody(input));
884
+ }
885
+ /**
886
+ * Shared body of read/readSync: dispatch, read, normalize, wrap.
887
+ */
888
+ *readBody(input) {
889
+ const filePath = buildFilePath(input);
890
+ const reader = this.findReader(filePath);
891
+ if (!reader) throw this.unknownExtensionError(filePath);
892
+ try {
893
+ const output = yield* op(() => reader.read(filePath), () => reader.readSync(filePath));
894
+ return this.toRecord(output, reader);
895
+ } catch (e) {
896
+ throw wrapLoaderError(e, filePath);
897
+ }
898
+ }
899
+ async write(input, value) {
900
+ return runTwinAsync(this.writeBody(input, value));
901
+ }
902
+ writeSync(input, value) {
903
+ runTwinSync(this.writeBody(input, value));
904
+ }
905
+ /**
906
+ * Shared body of write/writeSync: dispatch, unwrap records, write, wrap.
907
+ * The inverse boundary of readBody: a record produced by read() (brand
908
+ * detected) is unwrapped to its `.default` value; anything else is
909
+ * written as-is.
910
+ */
911
+ *writeBody(input, value) {
912
+ const filePath = buildFilePath(input);
913
+ if (!isFilePath(filePath)) throw new LocterWriteError({
914
+ message: `Cannot write to a bare module specifier: ${filePath}`,
915
+ path: filePath
916
+ });
917
+ const writer = this.findWriter(filePath);
918
+ if (!writer) {
919
+ if (this.findReader(filePath)) throw new LocterWriteError({
920
+ message: `The format of extension ${pathToLocatorInfo(filePath).extension} is read-only.`,
921
+ path: filePath
922
+ });
923
+ throw this.unknownExtensionError(filePath);
924
+ }
925
+ const plain = isModuleRecord(value) ? value.default : value;
926
+ try {
927
+ yield* op(() => writer.write(filePath, plain), () => writer.writeSync(filePath, plain));
928
+ } catch (e) {
929
+ throw wrapWriteError(e, filePath);
930
+ }
931
+ }
932
+ /**
933
+ * The single normalization boundary: every read result becomes a module
934
+ * record (`.default` is always the loaded value, top-level keys stay
935
+ * accessible as named exports). Provenance decides how: module-reader
936
+ * output may legitimately already be a record (`__esModule` is meaningful
937
+ * there); any other reader returns arbitrary parsed data, which is always
938
+ * wrapped — even if it happens to contain an `__esModule` key.
939
+ */
940
+ toRecord(output, reader) {
941
+ if (reader instanceof ModuleReader) return toModuleRecord(output);
942
+ return createModuleRecord(output);
943
+ }
944
+ /**
945
+ * Typed accessor for the LIVE built-in reader instance (lazy, cached).
946
+ */
947
+ builtInReader(id) {
948
+ let reader = this.builtInReaderCache.get(id);
949
+ if (!reader) {
950
+ reader = BUILT_IN_PRESETS[id].reader();
951
+ this.builtInReaderCache.set(id, reader);
952
+ }
953
+ return reader;
954
+ }
955
+ /**
956
+ * Typed accessor for the LIVE built-in writer instance (lazy, cached).
957
+ * Only writable format ids are accepted — read-only formats (module)
958
+ * are a compile error.
959
+ */
960
+ builtInWriter(id) {
961
+ let writer = this.builtInWriterCache.get(id);
962
+ if (!writer) {
963
+ writer = BUILT_IN_PRESETS[id].writer();
964
+ this.builtInWriterCache.set(id, writer);
965
+ }
966
+ return writer;
967
+ }
968
+ /**
969
+ * Dispatch order:
970
+ * 1. bare specifier (no extension) → module reader, always
971
+ * 2. user rules, registration order → first match wins (can override built-ins)
972
+ * 3. built-in extension table
973
+ * 4. undefined → read/readSync throw LocterUnknownExtensionError
974
+ */
975
+ findReader(input) {
976
+ if (!isFilePath(input)) return this.builtInReader("module");
977
+ const rule = this.findRule(input, (item) => typeof item.getReader !== "undefined");
978
+ if (rule && rule.getReader) return rule.getReader();
979
+ const id = this.findBuiltInId(input);
980
+ if (id) return this.builtInReader(id);
981
+ }
982
+ /**
983
+ * Write-side dispatch. No bare-specifier step (there is nothing to
984
+ * write to); only rules with a writer slot participate, then built-in
985
+ * presets that declare a writer.
986
+ */
987
+ findWriter(input) {
988
+ if (!isFilePath(input)) return;
989
+ const rule = this.findRule(input, (item) => typeof item.getWriter !== "undefined");
990
+ if (rule && rule.getWriter) return rule.getWriter();
991
+ const id = this.findBuiltInId(input);
992
+ if (id && "writer" in BUILT_IN_PRESETS[id]) return this.builtInWriter(id);
993
+ }
994
+ findRule(input, filter) {
995
+ const info = pathToLocatorInfo(input);
996
+ for (const rule of this.rules) {
997
+ if (!filter(rule)) continue;
998
+ const { test } = rule;
999
+ if (Array.isArray(test)) {
1000
+ if (info.extension && test.includes(info.extension)) return rule;
1001
+ } else {
1002
+ test.lastIndex = 0;
1003
+ const matched = test.test(buildFilePath(info));
1004
+ test.lastIndex = 0;
1005
+ if (matched) return rule;
1006
+ }
1007
+ }
1008
+ }
1009
+ findBuiltInId(input) {
1010
+ const info = pathToLocatorInfo(input);
1011
+ if (!info.extension) return;
1012
+ return this.builtInExtensions.get(info.extension);
1013
+ }
1014
+ unknownExtensionError(input) {
1015
+ return new LocterUnknownExtensionError({
1016
+ message: `No format registered for extension: ${pathToLocatorInfo(input).extension ?? "unknown"}`,
1017
+ path: input
1018
+ });
1019
+ }
1020
+ compile(id, rule) {
1021
+ return {
1022
+ id,
1023
+ test: rule.test,
1024
+ getReader: compileSlot(rule.reader),
1025
+ getWriter: compileSlot(rule.writer)
1026
+ };
1027
+ }
1028
+ generateRuleId() {
1029
+ let id;
1030
+ do {
1031
+ this.ruleCounter++;
1032
+ id = `rule:${this.ruleCounter}`;
1033
+ } while (this.rules.some((item) => item.id === id));
1034
+ return id;
1035
+ }
1036
+ };
1037
+ //#endregion
1038
+ //#region src/format/registry/singleton.ts
685
1039
  let instance;
686
- function useLoader() {
687
- if (typeof instance !== 'undefined') {
688
- return instance;
689
- }
690
- instance = new LoaderManager();
691
- return instance;
1040
+ function useFormatRegistry() {
1041
+ if (typeof instance !== "undefined") return instance;
1042
+ instance = new FormatRegistry();
1043
+ return instance;
692
1044
  }
693
-
694
- function registerLoader(test, loader) {
695
- const manager = useLoader();
696
- if (typeof loader !== 'undefined') {
697
- manager.register(test, loader);
698
- return;
699
- }
700
- manager.register(test);
701
- }
702
- async function load(input) {
703
- const manager = useLoader();
704
- if (typeof input === 'string') {
705
- return manager.execute(input);
706
- }
707
- return manager.execute(buildFilePath(input));
708
- }
709
- function loadSync(input) {
710
- const manager = useLoader();
711
- if (typeof input === 'string') {
712
- return manager.executeSync(input);
713
- }
714
- return manager.executeSync(buildFilePath(input));
1045
+ //#endregion
1046
+ //#region src/format/helpers.ts
1047
+ function registerFormat(rule) {
1048
+ return useFormatRegistry().register(rule);
1049
+ }
1050
+ /**
1051
+ * Remove a rule from the process-global registry by its id
1052
+ * (as returned by registerFormat). Built-in ids cannot be unregistered.
1053
+ */
1054
+ function unregisterFormat(id) {
1055
+ return useFormatRegistry().unregister(id);
1056
+ }
1057
+ async function read(input) {
1058
+ return useFormatRegistry().read(input);
1059
+ }
1060
+ function readSync(input) {
1061
+ return useFormatRegistry().readSync(input);
1062
+ }
1063
+ async function write(input, value) {
1064
+ return useFormatRegistry().write(input, value);
1065
+ }
1066
+ function writeSync(input, value) {
1067
+ useFormatRegistry().writeSync(input, value);
1068
+ }
1069
+ /**
1070
+ * Override the built-in module reader's `import` / `require` calls.
1071
+ *
1072
+ * Useful for test runners (e.g. Vitest) where the dynamic `import()` inside
1073
+ * `node_modules/locter` would bypass the runner's module graph. Calling this
1074
+ * from user code (e.g. a Vitest setup file) lets the runner rewrite the
1075
+ * `import()` so loaded modules share identity with statically imported ones.
1076
+ *
1077
+ * Returns a restore function that re-applies the previous configuration —
1078
+ * handy for scoped overrides (e.g. test teardown). The restore function is
1079
+ * scoped to the exact reader instance it configured: after a registry
1080
+ * reset() (which discards that instance) it becomes a no-op instead of
1081
+ * re-applying stale configuration to the fresh instance.
1082
+ *
1083
+ * @example
1084
+ * ```ts
1085
+ * // vitest setup file
1086
+ * import { setModuleReader } from 'locter';
1087
+ *
1088
+ * setModuleReader({
1089
+ * load: (id) => import(id),
1090
+ * });
1091
+ * ```
1092
+ */
1093
+ function setModuleReader(options) {
1094
+ const reader = useFormatRegistry().builtInReader("module");
1095
+ const previous = reader.configure(options);
1096
+ return () => {
1097
+ reader.configure(previous);
1098
+ };
1099
+ }
1100
+ //#endregion
1101
+ //#region src/format/package-field.ts
1102
+ function extractField(pkg, field) {
1103
+ if (!isObject(pkg) || !hasOwnProperty(pkg, field)) return;
1104
+ return pkg[field];
1105
+ }
1106
+ function* locatePackageBody(options) {
1107
+ if (options.walkUp) return yield* op(() => locateUp("package.json", {
1108
+ cwd: options.cwd,
1109
+ stopAt: options.stopAt
1110
+ }), () => locateUpSync("package.json", {
1111
+ cwd: options.cwd,
1112
+ stopAt: options.stopAt
1113
+ }));
1114
+ return yield* op(() => locate("package.json", { cwd: options.cwd }), () => locateSync("package.json", { cwd: options.cwd }));
1115
+ }
1116
+ function* readPackageFieldBody(field, options) {
1117
+ const info = yield* locatePackageBody(options);
1118
+ if (!info) return;
1119
+ const reader = useFormatRegistry().builtInReader("json");
1120
+ const filePath = buildFilePath(info);
1121
+ try {
1122
+ return extractField(yield* op(() => reader.read(filePath), () => reader.readSync(filePath)), field);
1123
+ } catch (e) {
1124
+ if (e instanceof LocterNotFoundError) return;
1125
+ throw e;
1126
+ }
1127
+ }
1128
+ async function readPackageField(field, options = {}) {
1129
+ return runTwinAsync(readPackageFieldBody(field, options));
1130
+ }
1131
+ function readPackageFieldSync(field, options = {}) {
1132
+ return runTwinSync(readPackageFieldBody(field, options));
1133
+ }
1134
+ function* writePackageFieldBody(field, value, options) {
1135
+ if (!isSafeObjectKey(field)) throw new LocterError({ message: `The field ${field} is not a safe object key.` });
1136
+ const info = yield* locatePackageBody(options);
1137
+ if (!info) throw new LocterNotFoundError({ message: "No package.json could be located." });
1138
+ const reader = useFormatRegistry().builtInReader("json");
1139
+ const filePath = buildFilePath(info);
1140
+ const pkg = yield* op(() => reader.read(filePath), () => reader.readSync(filePath));
1141
+ if (!isObject(pkg)) throw new LocterWriteError({
1142
+ message: "The located package.json does not contain an object.",
1143
+ path: filePath
1144
+ });
1145
+ pkg[field] = value;
1146
+ const writer = new JSONWriter({ indent: "auto" });
1147
+ yield* op(() => writer.write(filePath, pkg), () => writer.writeSync(filePath, pkg));
1148
+ }
1149
+ /**
1150
+ * Set a top-level field of the nearest `package.json` (same locate
1151
+ * semantics as readPackageField) and write it back, preserving the
1152
+ * file's existing indentation. Passing `undefined` removes the field.
1153
+ * Throws LocterNotFoundError when no package.json could be located.
1154
+ */
1155
+ async function writePackageField(field, value, options = {}) {
1156
+ return runTwinAsync(writePackageFieldBody(field, value, options));
1157
+ }
1158
+ function writePackageFieldSync(field, value, options = {}) {
1159
+ runTwinSync(writePackageFieldBody(field, value, options));
715
1160
  }
1161
+ //#endregion
1162
+ export { ConfReader, ConfWriter, FormatRegistry, JSONReader, JSONWriter, LOCTER_ERROR_MARKER, LOCTER_LOAD_ERROR_MARKER, LOCTER_NOT_FOUND_ERROR_MARKER, LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER, LOCTER_WRITE_ERROR_MARKER, LocterError, LocterLoadError, LocterNotFoundError, LocterUnknownExtensionError, LocterWriteError, MODULE_FILE_EXTENSIONS, ModuleReader, TextFileReader, TextFileWriter, YAMLReader, YAMLWriter, buildFilePath, getModuleExport, isJestRuntimeEnvironment, isLocatorInfo, isModuleRecord, isTsNodeRuntimeEnvironment, isTsxRuntimeEnvironment, isVitestRuntimeEnvironment, locate, locateMany, locateManySync, locateSync, locateUp, locateUpSync, read, readPackageField, readPackageFieldSync, readSync, registerFormat, setModuleReader, unregisterFormat, useFormatRegistry, wrapLoaderError, wrapWriteError, write, writePackageField, writePackageFieldSync, writeSync };
716
1163
 
717
- export { ConfLoader, JSONLoader, LoaderManager, ModuleLoader, buildFilePath, buildFilePathWithoutExtension, buildLocatorOptions, buildLocatorPatterns, getFileNameExtension, getModuleExport, handleException, hasOwnProperty, hasStringProperty, isESModule, isFilePath, isJestRuntimeEnvironment, isLocatorInfo, isObject, isSafeObjectKey, isTsNodeRuntimeEnvironment, isTypeScriptError, load, loadSync, locate, locateMany, locateManySync, locateSync, pathToLocatorInfo, registerLoader, removeFileNameExtension, toArray, toModuleRecord };
718
- //# sourceMappingURL=index.mjs.map
1164
+ //# sourceMappingURL=index.mjs.map