locter 3.0.0 → 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.
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { BaseError, hasInstanceof, markInstanceof } from "@ebec/core";
3
3
  import path from "node:path";
4
+ import fg from "fast-glob";
4
5
  import { destr } from "destr";
5
- import { unflatten } from "flat";
6
+ import { flatten, unflatten } from "flat";
6
7
  import fs from "node:fs";
7
- import fg from "fast-glob";
8
8
  import { createJiti } from "jiti";
9
9
  import { pathToFileURL } from "node:url";
10
- import { parse } from "yaml";
10
+ import { isMap, isNode, isScalar, parse, parseDocument, stringify } from "yaml";
11
11
  //#region src/errors/base.ts
12
12
  const LOCTER_ERROR_MARKER = Symbol.for("@locter/error");
13
13
  var LocterError = class extends BaseError {
@@ -62,18 +62,6 @@ var LocterUnknownExtensionError = class extends LocterError {
62
62
  }
63
63
  };
64
64
  //#endregion
65
- //#region src/utils/file-name.ts
66
- function getFileNameExtension(input, allowed) {
67
- const extension = path.extname(input);
68
- if (extension === "" || extension === ".") return;
69
- if (typeof allowed === "undefined" || allowed.includes(extension)) return extension;
70
- }
71
- function removeFileNameExtension(input, extensions) {
72
- const extension = getFileNameExtension(input, extensions);
73
- if (extension) return input.substring(0, input.length - extension.length);
74
- return input;
75
- }
76
- //#endregion
77
65
  //#region src/utils/has-property.ts
78
66
  function hasOwnProperty(obj, prop) {
79
67
  return Object.prototype.hasOwnProperty.call(obj, prop);
@@ -111,6 +99,13 @@ function isVitestRuntimeEnvironment() {
111
99
  function isTsNodeRuntimeEnvironment() {
112
100
  return !!process[Symbol.for("ts-node.register.instance")];
113
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
+ }
114
109
  //#endregion
115
110
  //#region src/utils/typescript.ts
116
111
  function isTypeScriptError(input) {
@@ -119,8 +114,20 @@ function isTypeScriptError(input) {
119
114
  return typeof input.message === "string" && /TS\d+/.test(input.message);
120
115
  }
121
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
122
129
  //#region src/errors/wrap.ts
123
- const NOT_FOUND_CODES = new Set([
130
+ const NOT_FOUND_CODES = /* @__PURE__ */ new Set([
124
131
  "ENOENT",
125
132
  "MODULE_NOT_FOUND",
126
133
  "ERR_MODULE_NOT_FOUND"
@@ -142,6 +149,55 @@ function wrapLoaderError(input, path) {
142
149
  path
143
150
  });
144
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
+ }
145
201
  //#endregion
146
202
  //#region src/locator/utils.ts
147
203
  function buildLocatorPatterns(pattern) {
@@ -151,23 +207,12 @@ function buildLocatorOptions(options = {}) {
151
207
  const cwd = options.cwd ? toArray(options.cwd) : [];
152
208
  const ignore = options.ignore ? toArray(options.ignore) : [];
153
209
  if (cwd.length === 0) cwd.push(process.cwd());
154
- let onlyFiles;
155
- let onlyDirectories;
156
- if (options.onlyDirectories === options.onlyFiles) {
157
- onlyDirectories = false;
158
- onlyFiles = !options.onlyDirectories;
159
- } else if (typeof options.onlyFiles === "undefined") {
160
- onlyDirectories = options.onlyDirectories ?? false;
161
- onlyFiles = !options.onlyDirectories;
162
- } else {
163
- onlyFiles = options.onlyFiles ?? true;
164
- onlyDirectories = !options.onlyFiles;
165
- }
210
+ const onlyDirectories = options.onlyDirectories ?? false;
166
211
  return {
167
212
  cwd,
168
213
  ignore,
169
214
  onlyDirectories,
170
- onlyFiles,
215
+ onlyFiles: onlyDirectories ? false : options.onlyFiles ?? true,
171
216
  dot: options.dot ?? false
172
217
  };
173
218
  }
@@ -191,79 +236,57 @@ function buildFilePath(input) {
191
236
  if (typeof input === "string") return input;
192
237
  return input.path;
193
238
  }
194
- function buildFilePathWithoutExtension(input) {
195
- let info;
196
- if (typeof input === "string") info = pathToLocatorInfo(input);
197
- else info = input;
198
- return path.join(info.directory, info.name);
199
- }
200
239
  //#endregion
201
- //#region src/locator/async.ts
202
- async function locateMany(pattern, options) {
240
+ //#region src/locator/core.ts
241
+ function buildGlobJobs(pattern, options) {
203
242
  const patterns = buildLocatorPatterns(pattern);
204
243
  const opts = buildLocatorOptions(options);
205
- const items = [];
206
- for (const p of patterns) for (const cwd of opts.cwd) {
207
- const files = await fg(p, {
244
+ const jobs = [];
245
+ for (const p of patterns) for (const cwd of opts.cwd) jobs.push({
246
+ pattern: p,
247
+ options: {
208
248
  absolute: true,
209
249
  cwd,
210
250
  ignore: opts.ignore,
211
251
  onlyFiles: opts.onlyFiles,
212
252
  onlyDirectories: opts.onlyDirectories,
213
253
  dot: opts.dot
214
- });
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);
215
271
  for (const file of files) items.push(pathToLocatorInfo(file, true));
216
272
  }
217
273
  return items;
218
274
  }
275
+ //#endregion
276
+ //#region src/locator/async.ts
277
+ async function locateMany(pattern, options) {
278
+ return runTwinAsync(locateManyBody(pattern, options));
279
+ }
219
280
  async function locate(pattern, options) {
220
- const patterns = buildLocatorPatterns(pattern);
221
- const opts = buildLocatorOptions(options);
222
- for (const p of patterns) for (const cwd of opts.cwd) {
223
- const element = (await fg(p, {
224
- absolute: true,
225
- cwd,
226
- ignore: opts.ignore,
227
- onlyFiles: opts.onlyFiles,
228
- onlyDirectories: opts.onlyDirectories,
229
- dot: opts.dot
230
- })).shift();
231
- if (element) return pathToLocatorInfo(element, true);
232
- }
281
+ return runTwinAsync(locateBody(pattern, options));
233
282
  }
234
283
  //#endregion
235
284
  //#region src/locator/sync.ts
236
285
  function locateManySync(pattern, options) {
237
- const patterns = buildLocatorPatterns(pattern);
238
- const opts = buildLocatorOptions(options);
239
- const items = [];
240
- for (const p of patterns) for (const cwd of opts.cwd) {
241
- const files = fg.sync(p, {
242
- absolute: true,
243
- cwd,
244
- ignore: opts.ignore,
245
- onlyFiles: opts.onlyFiles,
246
- onlyDirectories: opts.onlyDirectories,
247
- dot: opts.dot
248
- });
249
- for (const file of files) items.push(pathToLocatorInfo(file, true));
250
- }
251
- return items;
286
+ return runTwinSync(locateManyBody(pattern, options));
252
287
  }
253
288
  function locateSync(pattern, options) {
254
- const patterns = buildLocatorPatterns(pattern);
255
- const opts = buildLocatorOptions(options);
256
- for (const p of patterns) for (const cwd of opts.cwd) {
257
- const element = fg.sync(p, {
258
- absolute: true,
259
- cwd,
260
- ignore: opts.ignore,
261
- onlyFiles: opts.onlyFiles,
262
- onlyDirectories: opts.onlyDirectories,
263
- dot: opts.dot
264
- }).shift();
265
- if (element) return pathToLocatorInfo(element, true);
266
- }
289
+ return runTwinSync(locateBody(pattern, options));
267
290
  }
268
291
  //#endregion
269
292
  //#region src/locator/up.ts
@@ -282,10 +305,10 @@ function nextDirectory(current, stopAt) {
282
305
  if (parent === current) return;
283
306
  return parent;
284
307
  }
285
- async function locateUp(pattern, options = {}) {
308
+ function* locateUpBody(pattern, options) {
286
309
  let walk = startWalk(options);
287
310
  while (true) {
288
- const found = await locate(pattern, {
311
+ const found = yield* locateBody(pattern, {
289
312
  ...walk.baseOptions,
290
313
  cwd: walk.current
291
314
  });
@@ -298,43 +321,96 @@ async function locateUp(pattern, options = {}) {
298
321
  };
299
322
  }
300
323
  }
324
+ async function locateUp(pattern, options = {}) {
325
+ return runTwinAsync(locateUpBody(pattern, options));
326
+ }
301
327
  function locateUpSync(pattern, options = {}) {
302
- let walk = startWalk(options);
303
- while (true) {
304
- const found = locateSync(pattern, {
305
- ...walk.baseOptions,
306
- cwd: walk.current
307
- });
308
- if (found) return found;
309
- const next = nextDirectory(walk.current, walk.stopAt);
310
- if (!next) return;
311
- walk = {
312
- ...walk,
313
- current: next
314
- };
315
- }
328
+ return runTwinSync(locateUpBody(pattern, options));
316
329
  }
317
330
  //#endregion
318
- //#region src/loader/built-in/conf/module.ts
319
- var ConfLoader = class {
320
- async execute(input) {
321
- const filePath = buildFilePath(input);
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) {
322
346
  try {
323
- const file = await fs.promises.readFile(filePath, { encoding: "utf-8" });
324
- return this.parse(file);
347
+ const content = yield* op(() => fs.promises.readFile(input, { encoding: "utf-8" }), () => fs.readFileSync(input, { encoding: "utf-8" }));
348
+ return this.parse(content);
325
349
  } catch (e) {
326
- throw wrapLoaderError(e, filePath);
350
+ throw wrapLoaderError(e, input);
327
351
  }
328
352
  }
329
- executeSync(input) {
330
- const filePath = buildFilePath(input);
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;
331
384
  try {
332
- const file = fs.readFileSync(filePath, { encoding: "utf-8" });
333
- return this.parse(file);
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" }));
334
406
  } catch (e) {
335
- throw wrapLoaderError(e, filePath);
407
+ throw wrapWriteError(e, filePath);
336
408
  }
337
409
  }
410
+ };
411
+ //#endregion
412
+ //#region src/format/built-in/conf/reader.ts
413
+ var ConfReader = class extends TextFileReader {
338
414
  parse(contents) {
339
415
  const config = {};
340
416
  const lines = contents.split(/\n|\r|\r\n/);
@@ -356,70 +432,83 @@ var ConfLoader = class {
356
432
  }
357
433
  };
358
434
  //#endregion
359
- //#region src/loader/built-in/json/module.ts
360
- var JSONLoader = class {
361
- async execute(input) {
362
- const filePath = buildFilePath(input);
363
- try {
364
- const file = await fs.promises.readFile(filePath, { encoding: "utf-8" });
365
- return JSON.parse(file);
366
- } catch (e) {
367
- throw wrapLoaderError(e, filePath);
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)}`);
368
460
  }
461
+ return lines.join("\n");
369
462
  }
370
- executeSync(input) {
371
- const filePath = buildFilePath(input);
372
- try {
373
- const file = fs.readFileSync(filePath, { encoding: "utf-8" });
374
- return JSON.parse(file);
375
- } catch (e) {
376
- throw wrapLoaderError(e, filePath);
377
- }
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);
378
469
  }
379
470
  };
380
471
  //#endregion
381
- //#region src/loader/built-in/module/utils.ts
382
- function isESModule(input) {
383
- return isObject(input) && typeof input.__esModule !== "undefined";
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;
384
476
  }
385
- function toModuleRecord(data) {
386
- if (isESModule(data)) {
387
- if (isESModule(data.default) && data.default.default) return {
388
- ...data,
389
- default: data.default.default
390
- };
391
- return data;
477
+ var JSONWriter = class extends TextFileWriter {
478
+ options;
479
+ constructor(options = {}) {
480
+ super();
481
+ this.options = options;
392
482
  }
393
- const output = Object.create(null, {
394
- __esModule: { value: true },
395
- [Symbol.toStringTag]: { value: "Module" }
396
- });
397
- if (isObject(data)) for (const key in data) {
398
- if (hasOwnProperty(output, key)) continue;
399
- let descriptor = Object.getOwnPropertyDescriptor(data, key);
400
- if (!descriptor || "get" in descriptor || descriptor.writable || descriptor.configurable) descriptor = {
401
- enumerable: true,
402
- get() {
403
- return data[key];
404
- }
405
- };
406
- Object.defineProperty(output, key, descriptor);
483
+ get usesExistingContent() {
484
+ return this.options.indent === "auto";
407
485
  }
408
- if (!hasOwnProperty(output, "default")) Object.defineProperty(output, "default", {
409
- value: data,
410
- enumerable: true
411
- });
412
- return Object.freeze(output);
413
- }
414
- function getModuleExport(data, filterFn) {
415
- const keys = Object.keys(data);
416
- for (const key of keys) if (filterFn(key, data[key])) return {
417
- key,
418
- value: data[key]
419
- };
420
- }
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
+ };
421
496
  //#endregion
422
- //#region src/loader/built-in/module/module.ts
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
423
512
  const require = createRequire(import.meta.url);
424
513
  function originalPath(data) {
425
514
  return typeof data === "string" ? data : buildFilePath(data);
@@ -428,46 +517,56 @@ function isUnrecoverableError(error) {
428
517
  const underlying = error instanceof LocterError ? error.cause : error;
429
518
  return underlying instanceof SyntaxError || underlying instanceof ReferenceError || isTypeScriptError(underlying);
430
519
  }
431
- var ModuleLoader = class {
520
+ var ModuleReader = class {
432
521
  instance;
433
522
  loadFn;
434
523
  loadSyncFn;
435
524
  constructor(options = {}) {
436
- this.instance = createJiti(void 0, { extensions: [
437
- ".js",
438
- ".mjs",
439
- ".mts",
440
- ".cjs",
441
- ".cts",
442
- ".ts"
443
- ] });
525
+ this.instance = createJiti(void 0, { extensions: [...MODULE_FILE_EXTENSIONS] });
444
526
  this.loadFn = options.load;
445
527
  this.loadSyncFn = options.loadSync;
446
528
  }
529
+ /**
530
+ * Returns the previous configuration, so callers can restore it.
531
+ */
447
532
  configure(options) {
533
+ const previous = {
534
+ load: this.loadFn,
535
+ loadSync: this.loadSyncFn
536
+ };
448
537
  if ("load" in options) this.loadFn = options.load;
449
538
  if ("loadSync" in options) this.loadSyncFn = options.loadSync;
539
+ return previous;
450
540
  }
451
- async execute(input) {
452
- let output;
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) {
453
555
  try {
454
- output = await this.load(input);
556
+ return await this.load(input);
455
557
  } catch (e) {
456
558
  if (isUnrecoverableError(e)) throw e;
457
- if (isTsNodeRuntimeEnvironment()) output = this.loadSync(input);
458
- else output = this.instance(input);
559
+ if (isTsNodeRuntimeEnvironment()) return this.loadSync(input);
560
+ return this.instance(input);
459
561
  }
460
- return toModuleRecord(output);
461
562
  }
462
- executeSync(input) {
463
- let output;
563
+ readSync(input) {
464
564
  try {
465
- output = this.loadSync(input);
565
+ return this.loadSync(input);
466
566
  } catch (e) {
467
567
  if (isUnrecoverableError(e)) throw e;
468
- output = this.instance(input);
568
+ return this.instance(input);
469
569
  }
470
- return toModuleRecord(output);
471
570
  }
472
571
  async load(data, options = {}) {
473
572
  const id = this.build(data, options);
@@ -502,233 +601,564 @@ var ModuleLoader = class {
502
601
  }
503
602
  };
504
603
  //#endregion
505
- //#region src/loader/built-in/yaml/module.ts
506
- var YAMLLoader = class {
507
- async execute(input) {
508
- const filePath = buildFilePath(input);
509
- try {
510
- return parse(await fs.promises.readFile(filePath, { encoding: "utf-8" }));
511
- } catch (e) {
512
- throw wrapLoaderError(e, filePath);
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
+ */
630
+ function isESModule(input) {
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);
659
+ }
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
+ */
667
+ function toModuleRecord(data) {
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);
676
+ }
677
+ function getModuleExport(data, filterFn) {
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
+ };
683
+ }
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);
695
+ }
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
+ }
720
+ }
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
+ };
789
+ }
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);
513
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));
514
881
  }
515
- executeSync(input) {
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) {
516
889
  const filePath = buildFilePath(input);
890
+ const reader = this.findReader(filePath);
891
+ if (!reader) throw this.unknownExtensionError(filePath);
517
892
  try {
518
- return parse(fs.readFileSync(filePath, { encoding: "utf-8" }));
893
+ const output = yield* op(() => reader.read(filePath), () => reader.readSync(filePath));
894
+ return this.toRecord(output, reader);
519
895
  } catch (e) {
520
896
  throw wrapLoaderError(e, filePath);
521
897
  }
522
898
  }
523
- };
524
- //#endregion
525
- //#region src/loader/module.ts
526
- var LoaderManager = class {
527
- loaders;
528
- rules;
529
- constructor() {
530
- this.loaders = {};
531
- this.rules = [
532
- {
533
- test: [
534
- ".js",
535
- ".mjs",
536
- ".mts",
537
- ".cjs",
538
- ".cts",
539
- ".ts"
540
- ],
541
- loader: "module"
542
- },
543
- {
544
- test: [".conf"],
545
- loader: "conf"
546
- },
547
- {
548
- test: [".json"],
549
- loader: "json"
550
- },
551
- {
552
- test: [".yml", ".yaml"],
553
- loader: "yaml"
554
- }
555
- ];
899
+ async write(input, value) {
900
+ return runTwinAsync(this.writeBody(input, value));
901
+ }
902
+ writeSync(input, value) {
903
+ runTwinSync(this.writeBody(input, value));
556
904
  }
557
- register(test, loader) {
558
- if (typeof loader !== "undefined") {
559
- this.rules.push({
560
- test,
561
- loader
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
562
922
  });
563
- return;
923
+ throw this.unknownExtensionError(filePath);
564
924
  }
565
- this.rules.push(test);
566
- }
567
- async execute(input) {
568
- const id = this.findLoader(input);
569
- if (!id) throw this.unknownExtensionError(input);
570
- const loader = this.resolve(id);
925
+ const plain = isModuleRecord(value) ? value.default : value;
571
926
  try {
572
- return await loader.execute(input);
927
+ yield* op(() => writer.write(filePath, plain), () => writer.writeSync(filePath, plain));
573
928
  } catch (e) {
574
- throw wrapLoaderError(e, input);
929
+ throw wrapWriteError(e, filePath);
575
930
  }
576
931
  }
577
- executeSync(input) {
578
- const id = this.findLoader(input);
579
- if (!id) throw this.unknownExtensionError(input);
580
- const loader = this.resolve(id);
581
- try {
582
- return loader.executeSync(input);
583
- } catch (e) {
584
- throw wrapLoaderError(e, input);
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);
585
952
  }
953
+ return reader;
586
954
  }
587
- unknownExtensionError(input) {
588
- return new LocterUnknownExtensionError({
589
- message: `No loader registered for extension: ${pathToLocatorInfo(input).extension ?? "unknown"}`,
590
- path: input
591
- });
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);
592
981
  }
593
- findLoader(input) {
594
- if (!isFilePath(input)) return "module";
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) {
595
995
  const info = pathToLocatorInfo(input);
596
996
  for (const rule of this.rules) {
997
+ if (!filter(rule)) continue;
597
998
  const { test } = rule;
598
999
  if (Array.isArray(test)) {
599
- if (info.extension && test.includes(info.extension)) return rule.loader;
600
- } else if (test.test(buildFilePath(info))) return rule.loader;
601
- }
602
- }
603
- /**
604
- * Resolve loader by id.
605
- *
606
- * @param id
607
- */
608
- resolve(id) {
609
- if (typeof id !== "string") return id;
610
- if (Object.prototype.hasOwnProperty.call(this.loaders, id)) return this.loaders[id];
611
- let loader;
612
- switch (id) {
613
- case "conf":
614
- loader = new ConfLoader();
615
- break;
616
- case "module":
617
- loader = new ModuleLoader();
618
- break;
619
- case "json":
620
- loader = new JSONLoader();
621
- break;
622
- case "yaml":
623
- loader = new YAMLLoader();
624
- break;
625
- /* istanbul ignore next */
626
- default: {
627
- const pluginPath = this.normalizePath(id);
628
- loader = this.resolve("module").executeSync(pluginPath);
629
- break;
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;
630
1006
  }
631
1007
  }
632
- if (typeof loader !== "undefined") {
633
- this.loaders[id] = loader;
634
- return loader;
635
- }
636
- throw new LocterError({ message: `The loader ${id} could not be resolved.` });
637
1008
  }
638
- /* istanbul ignore next */
639
- normalizePath(input) {
640
- if (path.isAbsolute(input) || input.startsWith("./")) return input;
641
- if (input.startsWith("module:")) return input.substring(0, 7);
642
- if (!input.startsWith("@")) return `@locter/${input}`;
643
- return input;
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;
644
1035
  }
645
1036
  };
646
1037
  //#endregion
647
- //#region src/loader/singleton.ts
1038
+ //#region src/format/registry/singleton.ts
648
1039
  let instance;
649
- function useLoader() {
1040
+ function useFormatRegistry() {
650
1041
  if (typeof instance !== "undefined") return instance;
651
- instance = new LoaderManager();
1042
+ instance = new FormatRegistry();
652
1043
  return instance;
653
1044
  }
654
1045
  //#endregion
655
- //#region src/loader/helpers.ts
656
- function registerLoader(test, loader) {
657
- const manager = useLoader();
658
- if (typeof loader !== "undefined") {
659
- manager.register(test, loader);
660
- return;
661
- }
662
- manager.register(test);
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);
663
1056
  }
664
- async function load(input) {
665
- const manager = useLoader();
666
- if (typeof input === "string") return manager.execute(input);
667
- return manager.execute(buildFilePath(input));
1057
+ async function read(input) {
1058
+ return useFormatRegistry().read(input);
668
1059
  }
669
- function loadSync(input) {
670
- const manager = useLoader();
671
- if (typeof input === "string") return manager.executeSync(input);
672
- return manager.executeSync(buildFilePath(input));
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);
673
1068
  }
674
1069
  /**
675
- * Override the built-in module loader's `import` / `require` calls.
1070
+ * Override the built-in module reader's `import` / `require` calls.
676
1071
  *
677
1072
  * Useful for test runners (e.g. Vitest) where the dynamic `import()` inside
678
1073
  * `node_modules/locter` would bypass the runner's module graph. Calling this
679
1074
  * from user code (e.g. a Vitest setup file) lets the runner rewrite the
680
1075
  * `import()` so loaded modules share identity with statically imported ones.
681
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
+ *
682
1083
  * @example
683
1084
  * ```ts
684
1085
  * // vitest setup file
685
- * import { setModuleLoader } from 'locter';
1086
+ * import { setModuleReader } from 'locter';
686
1087
  *
687
- * setModuleLoader({
1088
+ * setModuleReader({
688
1089
  * load: (id) => import(id),
689
1090
  * });
690
1091
  * ```
691
1092
  */
692
- function setModuleLoader(options) {
693
- useLoader().resolve("module").configure(options);
1093
+ function setModuleReader(options) {
1094
+ const reader = useFormatRegistry().builtInReader("module");
1095
+ const previous = reader.configure(options);
1096
+ return () => {
1097
+ reader.configure(previous);
1098
+ };
694
1099
  }
695
1100
  //#endregion
696
- //#region src/loader/package-field.ts
1101
+ //#region src/format/package-field.ts
697
1102
  function extractField(pkg, field) {
698
1103
  if (!isObject(pkg) || !hasOwnProperty(pkg, field)) return;
699
1104
  return pkg[field];
700
1105
  }
701
- async function loadPackageField(field, options = {}) {
702
- let info;
703
- if (options.walkUp) info = await locateUp("package.json", {
1106
+ function* locatePackageBody(options) {
1107
+ if (options.walkUp) return yield* op(() => locateUp("package.json", {
704
1108
  cwd: options.cwd,
705
1109
  stopAt: options.stopAt
706
- });
707
- else info = await locate("package.json", { cwd: options.cwd });
708
- if (!info) return;
709
- try {
710
- return extractField(await load(info), field);
711
- } catch (e) {
712
- if (e instanceof LocterNotFoundError) return;
713
- throw e;
714
- }
715
- }
716
- function loadPackageFieldSync(field, options = {}) {
717
- let info;
718
- if (options.walkUp) info = locateUpSync("package.json", {
1110
+ }), () => locateUpSync("package.json", {
719
1111
  cwd: options.cwd,
720
1112
  stopAt: options.stopAt
721
- });
722
- else info = locateSync("package.json", { cwd: options.cwd });
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);
723
1118
  if (!info) return;
1119
+ const reader = useFormatRegistry().builtInReader("json");
1120
+ const filePath = buildFilePath(info);
724
1121
  try {
725
- return extractField(loadSync(info), field);
1122
+ return extractField(yield* op(() => reader.read(filePath), () => reader.readSync(filePath)), field);
726
1123
  } catch (e) {
727
1124
  if (e instanceof LocterNotFoundError) return;
728
1125
  throw e;
729
1126
  }
730
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));
1160
+ }
731
1161
  //#endregion
732
- export { ConfLoader, JSONLoader, LOCTER_ERROR_MARKER, LOCTER_LOAD_ERROR_MARKER, LOCTER_NOT_FOUND_ERROR_MARKER, LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER, LoaderManager, LocterError, LocterLoadError, LocterNotFoundError, LocterUnknownExtensionError, ModuleLoader, YAMLLoader, buildFilePath, buildFilePathWithoutExtension, buildLocatorOptions, buildLocatorPatterns, getFileNameExtension, getModuleExport, hasOwnProperty, hasStringProperty, isESModule, isFilePath, isJestRuntimeEnvironment, isLocatorInfo, isObject, isSafeObjectKey, isTsNodeRuntimeEnvironment, isTypeScriptError, isVitestRuntimeEnvironment, load, loadPackageField, loadPackageFieldSync, loadSync, locate, locateMany, locateManySync, locateSync, locateUp, locateUpSync, pathToLocatorInfo, registerLoader, removeFileNameExtension, setModuleLoader, toArray, toModuleRecord, wrapLoaderError };
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 };
733
1163
 
734
1164
  //# sourceMappingURL=index.mjs.map