locter 4.0.0-beta.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -28,7 +28,7 @@ var LocterError = class extends BaseError {
28
28
  //#endregion
29
29
  //#region src/errors/load.ts
30
30
  const LOCTER_LOAD_ERROR_MARKER = Symbol.for("@locter/load-error");
31
- var LocterLoadError = class extends LocterError {
31
+ var LoadError = class extends LocterError {
32
32
  static [Symbol.hasInstance](input) {
33
33
  return hasInstanceof(input, LOCTER_LOAD_ERROR_MARKER);
34
34
  }
@@ -40,7 +40,7 @@ var LocterLoadError = class extends LocterError {
40
40
  //#endregion
41
41
  //#region src/errors/not-found.ts
42
42
  const LOCTER_NOT_FOUND_ERROR_MARKER = Symbol.for("@locter/not-found-error");
43
- var LocterNotFoundError = class extends LocterError {
43
+ var NotFoundError = class extends LocterError {
44
44
  static [Symbol.hasInstance](input) {
45
45
  return hasInstanceof(input, LOCTER_NOT_FOUND_ERROR_MARKER);
46
46
  }
@@ -52,7 +52,7 @@ var LocterNotFoundError = class extends LocterError {
52
52
  //#endregion
53
53
  //#region src/errors/unknown-extension.ts
54
54
  const LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER = Symbol.for("@locter/unknown-extension-error");
55
- var LocterUnknownExtensionError = class extends LocterError {
55
+ var UnknownExtensionError = class extends LocterError {
56
56
  static [Symbol.hasInstance](input) {
57
57
  return hasInstanceof(input, LOCTER_UNKNOWN_EXTENSION_ERROR_MARKER);
58
58
  }
@@ -128,7 +128,7 @@ function isTypeScriptError(input) {
128
128
  //#endregion
129
129
  //#region src/errors/write.ts
130
130
  const LOCTER_WRITE_ERROR_MARKER = Symbol.for("@locter/write-error");
131
- var LocterWriteError = class extends LocterError {
131
+ var WriteError = class extends LocterError {
132
132
  static [Symbol.hasInstance](input) {
133
133
  return hasInstanceof(input, LOCTER_WRITE_ERROR_MARKER);
134
134
  }
@@ -148,13 +148,13 @@ function wrapLoaderError(input, path) {
148
148
  if (input instanceof LocterError) return input;
149
149
  const code = isObject(input) && hasStringProperty(input, "code") ? input.code : void 0;
150
150
  const message = isObject(input) && hasStringProperty(input, "message") ? input.message : `Failed to load: ${path}`;
151
- if (code && NOT_FOUND_CODES.has(code)) return new LocterNotFoundError({
151
+ if (code && NOT_FOUND_CODES.has(code)) return new NotFoundError({
152
152
  message,
153
153
  code,
154
154
  cause: input,
155
155
  path
156
156
  });
157
- return new LocterLoadError({
157
+ return new LoadError({
158
158
  message,
159
159
  code,
160
160
  cause: input,
@@ -164,7 +164,7 @@ function wrapLoaderError(input, path) {
164
164
  function wrapWriteError(input, path) {
165
165
  if (input instanceof LocterError) return input;
166
166
  const code = isObject(input) && hasStringProperty(input, "code") ? input.code : void 0;
167
- return new LocterWriteError({
167
+ return new WriteError({
168
168
  message: isObject(input) && hasStringProperty(input, "message") ? input.message : `Failed to write: ${path}`,
169
169
  code,
170
170
  cause: input,
@@ -372,7 +372,7 @@ function isNotFound(e) {
372
372
  * Base class for writers of text-based file formats: serializes via
373
373
  * `stringify`, creates missing parent directories, writes the file as
374
374
  * UTF-8 (always terminated by a single trailing newline), and wraps
375
- * failures in LocterWriteError. Subclasses implement `stringify` only —
375
+ * failures in WriteError. Subclasses implement `stringify` only —
376
376
  * both `write` variants are derived from the same body.
377
377
  */
378
378
  var TextFileWriter = class {
@@ -444,11 +444,39 @@ var ConfReader = class extends TextFileReader {
444
444
  }
445
445
  };
446
446
  //#endregion
447
- //#region src/format/built-in/conf/writer.ts
448
- function serializeValue(value) {
449
- return typeof value === "string" ? value : JSON.stringify(value);
447
+ //#region src/format/value.ts
448
+ /**
449
+ * Lenient VALUE codec (env-var / key-value philosophy): primitives
450
+ * serialize to their bare string form, RegExp and BigInt to their
451
+ * string representation (also nested), everything else to JSON.
452
+ * The counterpart of deserializeValue — used per value by the conf
453
+ * format, and exported for consumers building lenient custom formats.
454
+ */
455
+ function serializeValue(input) {
456
+ if (typeof input === "string") return input;
457
+ if (typeof input === "undefined") return "undefined";
458
+ if (input === null) return "null";
459
+ if (typeof input === "boolean") return input ? "true" : "false";
460
+ if (typeof input === "number") return `${input}`;
461
+ if (typeof input === "bigint") return input.toString();
462
+ if (input instanceof RegExp) return input.toString();
463
+ return JSON.stringify(input, (_key, item) => {
464
+ if (item instanceof RegExp) return item.toString();
465
+ if (typeof item === "bigint") return item.toString();
466
+ return item;
467
+ }) ?? String(input);
450
468
  }
451
469
  /**
470
+ * Lenient parse of a serialized value: coerces 'true'/'123'/'null'/
471
+ * 'undefined' to their primitive, parses JSON payloads, and falls back
472
+ * to returning the raw string — it never throws.
473
+ */
474
+ function deserializeValue(input) {
475
+ return destr(input);
476
+ }
477
+ //#endregion
478
+ //#region src/format/built-in/conf/writer.ts
479
+ /**
452
480
  * Serializes an object to `key=value` lines (the inverse of ConfReader):
453
481
  * nested objects become dot-separated keys, arrays become repeated
454
482
  * `key[]=` lines. Round-trips are structural, not textual — comments,
@@ -694,6 +722,32 @@ function getModuleExport(data, filterFn) {
694
722
  };
695
723
  }
696
724
  //#endregion
725
+ //#region src/format/built-in/text/reader.ts
726
+ /**
727
+ * Raw text: the file content, as-is. No value coercion — a .txt
728
+ * containing '123' reads as the string '123' (use deserializeValue
729
+ * for lenient value semantics).
730
+ */
731
+ var TextReader = class extends TextFileReader {
732
+ parse(content) {
733
+ return content;
734
+ }
735
+ };
736
+ //#endregion
737
+ //#region src/format/built-in/text/writer.ts
738
+ /**
739
+ * Raw text: accepts strings only — coercing arbitrary values would be
740
+ * lossy ([object Object]); serialize explicitly (e.g. serializeValue)
741
+ * before writing. Output ends with a single trailing newline, like
742
+ * every text writer.
743
+ */
744
+ var TextWriter = class extends TextFileWriter {
745
+ stringify(value) {
746
+ if (typeof value !== "string") throw new Error("The value must be a string for the text format.");
747
+ return value;
748
+ }
749
+ };
750
+ //#endregion
697
751
  //#region src/format/built-in/yaml/reader.ts
698
752
  var YAMLReader = class extends TextFileReader {
699
753
  parse(content) {
@@ -735,7 +789,7 @@ function graftMap(doc, node, value) {
735
789
  * target file already exists, the new value is grafted into its parsed
736
790
  * document, so comments and anchors attached to surviving keys survive
737
791
  * the write-back. New files are plain `yaml.stringify` output. An
738
- * existing file that fails to parse throws (wrapped as LocterWriteError)
792
+ * existing file that fails to parse throws (wrapped as WriteError)
739
793
  * instead of being silently overwritten.
740
794
  */
741
795
  var YAMLWriter = class extends TextFileWriter {
@@ -785,6 +839,11 @@ const BUILT_IN_PRESETS = {
785
839
  extensions: [".yml", ".yaml"],
786
840
  reader: () => new YAMLReader(),
787
841
  writer: () => new YAMLWriter()
842
+ },
843
+ text: {
844
+ extensions: [".txt"],
845
+ reader: () => new TextReader(),
846
+ writer: () => new TextWriter()
788
847
  }
789
848
  };
790
849
  //#endregion
@@ -894,12 +953,16 @@ var FormatRegistry = class {
894
953
  * normalized module record — a module IS a record, and the
895
954
  * normalization only irons out the CJS/ESM interop divergence so
896
955
  * both twin variants agree on one shape.
956
+ *
957
+ * options.format names a registered format id to use instead of
958
+ * extension dispatch (e.g. { format: 'text' } reads module source
959
+ * without evaluating it).
897
960
  */
898
- async read(input) {
899
- return runTwinAsync(this.readBody(input));
961
+ async read(input, options = {}) {
962
+ return runTwinAsync(this.readBody(input, options));
900
963
  }
901
- readSync(input) {
902
- return runTwinSync(this.readBody(input));
964
+ readSync(input, options = {}) {
965
+ return runTwinSync(this.readBody(input, options));
903
966
  }
904
967
  /**
905
968
  * Read a file/module and present the result as a normalized module
@@ -907,28 +970,30 @@ var FormatRegistry = class {
907
970
  * `.default` always holds the loaded value, top-level keys are
908
971
  * re-exposed as named exports.
909
972
  */
910
- async readAsModule(input) {
911
- return runTwinAsync(this.readAsModuleBody(input));
973
+ async readAsModule(input, options = {}) {
974
+ return runTwinAsync(this.readAsModuleBody(input, options));
912
975
  }
913
- readAsModuleSync(input) {
914
- return runTwinSync(this.readAsModuleBody(input));
976
+ readAsModuleSync(input, options = {}) {
977
+ return runTwinSync(this.readAsModuleBody(input, options));
915
978
  }
916
- *readBody(input) {
917
- const { output, reader } = yield* this.executeBody(input);
979
+ *readBody(input, options) {
980
+ const { output, reader } = yield* this.executeBody(input, options);
918
981
  if (reader instanceof ModuleReader) return toModuleRecord(output);
919
982
  return output;
920
983
  }
921
- *readAsModuleBody(input) {
922
- const { output, reader } = yield* this.executeBody(input);
984
+ *readAsModuleBody(input, options) {
985
+ const { output, reader } = yield* this.executeBody(input, options);
923
986
  return this.toRecord(output, reader);
924
987
  }
925
988
  /**
926
989
  * Shared dispatch + execution of both read bodies: resolve the
927
990
  * reader, run it, wrap errors. Normalization is the caller's call.
928
991
  */
929
- *executeBody(input) {
992
+ *executeBody(input, options) {
930
993
  const filePath = buildFilePath(input);
931
- const reader = this.findReader(filePath);
994
+ let reader;
995
+ if (typeof options.format === "undefined") reader = this.findReader(filePath);
996
+ else reader = this.resolveReader(options.format);
932
997
  if (!reader) throw this.unknownExtensionError(filePath);
933
998
  try {
934
999
  return {
@@ -939,32 +1004,45 @@ var FormatRegistry = class {
939
1004
  throw wrapLoaderError(e, filePath);
940
1005
  }
941
1006
  }
942
- async write(input, value) {
943
- return runTwinAsync(this.writeBody(input, value));
1007
+ /**
1008
+ * options.format names a registered format id to use instead of
1009
+ * extension dispatch — see writeBody for the boundary semantics.
1010
+ */
1011
+ async write(input, value, options = {}) {
1012
+ return runTwinAsync(this.writeBody(input, value, options));
944
1013
  }
945
- writeSync(input, value) {
946
- runTwinSync(this.writeBody(input, value));
1014
+ writeSync(input, value, options = {}) {
1015
+ runTwinSync(this.writeBody(input, value, options));
947
1016
  }
948
1017
  /**
949
1018
  * Shared body of write/writeSync: dispatch, unwrap records, write, wrap.
950
1019
  * The inverse boundary of readAsModuleBody: a record produced by
951
1020
  * readAsModule() (brand detected) is unwrapped to its `.default` value;
952
1021
  * anything else is written as-is.
1022
+ *
1023
+ * An explicit options.format skips extension dispatch entirely —
1024
+ * including the bare-specifier guard: naming the format resolves the
1025
+ * ambiguity the guard exists for, so extensionless paths become
1026
+ * writable.
953
1027
  */
954
- *writeBody(input, value) {
1028
+ *writeBody(input, value, options) {
955
1029
  const filePath = buildFilePath(input);
956
- if (!isFilePath(filePath)) throw new LocterWriteError({
957
- message: `Cannot write to a bare module specifier: ${filePath}`,
958
- path: filePath
959
- });
960
- const writer = this.findWriter(filePath);
961
- if (!writer) {
962
- if (this.findReader(filePath)) throw new LocterWriteError({
963
- message: `The format of extension ${pathToLocatorInfo(filePath).extension} is read-only.`,
1030
+ let writer;
1031
+ if (typeof options.format === "undefined") {
1032
+ if (!isFilePath(filePath)) throw new WriteError({
1033
+ message: `Cannot write to a bare module specifier: ${filePath}`,
964
1034
  path: filePath
965
1035
  });
966
- throw this.unknownExtensionError(filePath);
967
- }
1036
+ const found = this.findWriter(filePath);
1037
+ if (!found) {
1038
+ if (this.findReader(filePath)) throw new WriteError({
1039
+ message: `The format of extension ${pathToLocatorInfo(filePath).extension} is read-only.`,
1040
+ path: filePath
1041
+ });
1042
+ throw this.unknownExtensionError(filePath);
1043
+ }
1044
+ writer = found;
1045
+ } else writer = this.resolveWriter(options.format, filePath);
968
1046
  const plain = isModuleRecord(value) ? value.default : value;
969
1047
  try {
970
1048
  yield* op(() => writer.write(filePath, plain), () => writer.writeSync(filePath, plain));
@@ -1013,7 +1091,7 @@ var FormatRegistry = class {
1013
1091
  * 1. bare specifier (no extension) → module reader, always
1014
1092
  * 2. user rules, registration order → first match wins (can override built-ins)
1015
1093
  * 3. built-in extension table
1016
- * 4. undefined → read/readSync throw LocterUnknownExtensionError
1094
+ * 4. undefined → read/readSync throw UnknownExtensionError
1017
1095
  */
1018
1096
  findReader(input) {
1019
1097
  if (!isFilePath(input)) return this.builtInReader("module");
@@ -1034,6 +1112,43 @@ var FormatRegistry = class {
1034
1112
  const id = this.findBuiltInId(input);
1035
1113
  if (id && "writer" in BUILT_IN_PRESETS[id]) return this.builtInWriter(id);
1036
1114
  }
1115
+ /**
1116
+ * Resolve a reader by EXPLICIT format id (the per-call override) —
1117
+ * a user rule id or a built-in id — bypassing extension dispatch.
1118
+ * Unknown ids and writer-only rules throw.
1119
+ */
1120
+ resolveReader(id) {
1121
+ const rule = this.rules.find((item) => item.id === id);
1122
+ if (rule) {
1123
+ if (rule.getReader) return rule.getReader();
1124
+ throw new LocterError({ message: `The format ${id} has no reader.` });
1125
+ }
1126
+ if (hasOwnProperty(BUILT_IN_PRESETS, id)) return this.builtInReader(id);
1127
+ throw this.unknownFormatError(id);
1128
+ }
1129
+ /**
1130
+ * Write-side counterpart of resolveReader. Reader-only rules and
1131
+ * read-only built-ins (module) throw WriteError, mirroring
1132
+ * what extension dispatch throws for read-only extensions.
1133
+ */
1134
+ resolveWriter(id, path) {
1135
+ const rule = this.rules.find((item) => item.id === id);
1136
+ if (rule) {
1137
+ if (rule.getWriter) return rule.getWriter();
1138
+ throw new WriteError({
1139
+ message: `The format ${id} is read-only.`,
1140
+ path
1141
+ });
1142
+ }
1143
+ if (hasOwnProperty(BUILT_IN_PRESETS, id)) {
1144
+ if ("writer" in BUILT_IN_PRESETS[id]) return this.builtInWriter(id);
1145
+ throw new WriteError({
1146
+ message: `The format ${id} is read-only.`,
1147
+ path
1148
+ });
1149
+ }
1150
+ throw this.unknownFormatError(id);
1151
+ }
1037
1152
  findRule(input, filter) {
1038
1153
  const info = pathToLocatorInfo(input);
1039
1154
  for (const rule of this.rules) {
@@ -1055,11 +1170,14 @@ var FormatRegistry = class {
1055
1170
  return this.builtInExtensions.get(info.extension);
1056
1171
  }
1057
1172
  unknownExtensionError(input) {
1058
- return new LocterUnknownExtensionError({
1173
+ return new UnknownExtensionError({
1059
1174
  message: `No format registered for extension: ${pathToLocatorInfo(input).extension ?? "unknown"}`,
1060
1175
  path: input
1061
1176
  });
1062
1177
  }
1178
+ unknownFormatError(id) {
1179
+ return new LocterError({ message: `No format registered with id: ${id}` });
1180
+ }
1063
1181
  compile(id, rule) {
1064
1182
  return {
1065
1183
  id,
@@ -1102,12 +1220,16 @@ function unregisterFormat(id) {
1102
1220
  * (json/yaml/conf and custom readers — mutable, round-trip-symmetric
1103
1221
  * with write); for modules the normalized module record (a module IS a
1104
1222
  * record; normalization only irons out CJS/ESM interop divergence).
1223
+ *
1224
+ * options.format forces a registered format id instead of extension
1225
+ * dispatch — e.g. { format: 'text' } reads a module file's source
1226
+ * WITHOUT evaluating it.
1105
1227
  */
1106
- async function read(input) {
1107
- return useFormatRegistry().read(input);
1228
+ async function read(input, options) {
1229
+ return useFormatRegistry().read(input, options);
1108
1230
  }
1109
- function readSync(input) {
1110
- return useFormatRegistry().readSync(input);
1231
+ function readSync(input, options) {
1232
+ return useFormatRegistry().readSync(input, options);
1111
1233
  }
1112
1234
  /**
1113
1235
  * Read a file/module and present the result as a normalized module
@@ -1115,17 +1237,22 @@ function readSync(input) {
1115
1237
  * always holds the loaded value, top-level keys are re-exposed as
1116
1238
  * named exports.
1117
1239
  */
1118
- async function readAsModule(input) {
1119
- return useFormatRegistry().readAsModule(input);
1240
+ async function readAsModule(input, options) {
1241
+ return useFormatRegistry().readAsModule(input, options);
1120
1242
  }
1121
- function readAsModuleSync(input) {
1122
- return useFormatRegistry().readAsModuleSync(input);
1243
+ function readAsModuleSync(input, options) {
1244
+ return useFormatRegistry().readAsModuleSync(input, options);
1123
1245
  }
1124
- async function write(input, value) {
1125
- return useFormatRegistry().write(input, value);
1246
+ /**
1247
+ * options.format forces a registered format id instead of extension
1248
+ * dispatch — this also lifts the bare-specifier guard (extensionless
1249
+ * paths) and the read-only block on module extensions.
1250
+ */
1251
+ async function write(input, value, options) {
1252
+ return useFormatRegistry().write(input, value, options);
1126
1253
  }
1127
- function writeSync(input, value) {
1128
- useFormatRegistry().writeSync(input, value);
1254
+ function writeSync(input, value, options) {
1255
+ useFormatRegistry().writeSync(input, value, options);
1129
1256
  }
1130
1257
  /**
1131
1258
  * Override the built-in module reader's `import` / `require` calls.
@@ -1182,7 +1309,7 @@ function* readPackageFieldBody(field, options) {
1182
1309
  try {
1183
1310
  return extractField(yield* op(() => reader.read(filePath), () => reader.readSync(filePath)), field);
1184
1311
  } catch (e) {
1185
- if (e instanceof LocterNotFoundError) return;
1312
+ if (e instanceof NotFoundError) return;
1186
1313
  throw e;
1187
1314
  }
1188
1315
  }
@@ -1195,11 +1322,11 @@ function readPackageFieldSync(field, options = {}) {
1195
1322
  function* writePackageFieldBody(field, value, options) {
1196
1323
  if (!isSafeObjectKey(field)) throw new LocterError({ message: `The field ${field} is not a safe object key.` });
1197
1324
  const info = yield* locatePackageBody(options);
1198
- if (!info) throw new LocterNotFoundError({ message: "No package.json could be located." });
1325
+ if (!info) throw new NotFoundError({ message: "No package.json could be located." });
1199
1326
  const reader = useFormatRegistry().builtInReader("json");
1200
1327
  const filePath = buildFilePath(info);
1201
1328
  const pkg = yield* op(() => reader.read(filePath), () => reader.readSync(filePath));
1202
- if (!isObject(pkg)) throw new LocterWriteError({
1329
+ if (!isObject(pkg)) throw new WriteError({
1203
1330
  message: "The located package.json does not contain an object.",
1204
1331
  path: filePath
1205
1332
  });
@@ -1211,7 +1338,7 @@ function* writePackageFieldBody(field, value, options) {
1211
1338
  * Set a top-level field of the nearest `package.json` (same locate
1212
1339
  * semantics as readPackageField) and write it back, preserving the
1213
1340
  * file's existing indentation. Passing `undefined` removes the field.
1214
- * Throws LocterNotFoundError when no package.json could be located.
1341
+ * Throws NotFoundError when no package.json could be located.
1215
1342
  */
1216
1343
  async function writePackageField(field, value, options = {}) {
1217
1344
  return runTwinAsync(writePackageFieldBody(field, value, options));
@@ -1220,6 +1347,6 @@ function writePackageFieldSync(field, value, options = {}) {
1220
1347
  runTwinSync(writePackageFieldBody(field, value, options));
1221
1348
  }
1222
1349
  //#endregion
1223
- 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, getFileNameExtension, getModuleExport, isJestRuntimeEnvironment, isLocatorInfo, isModuleRecord, isTsNodeRuntimeEnvironment, isTsxRuntimeEnvironment, isVitestRuntimeEnvironment, locate, locateMany, locateManySync, locateSync, locateUp, locateUpSync, read, readAsModule, readAsModuleSync, readPackageField, readPackageFieldSync, readSync, registerFormat, removeFileNameExtension, setModuleReader, unregisterFormat, useFormatRegistry, wrapLoaderError, wrapWriteError, write, writePackageField, writePackageFieldSync, writeSync };
1350
+ 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, LoadError, LocterError, MODULE_FILE_EXTENSIONS, ModuleReader, NotFoundError, TextFileReader, TextFileWriter, TextReader, TextWriter, UnknownExtensionError, WriteError, YAMLReader, YAMLWriter, buildFilePath, deserializeValue, getFileNameExtension, getModuleExport, isJestRuntimeEnvironment, isLocatorInfo, isModuleRecord, isTsNodeRuntimeEnvironment, isTsxRuntimeEnvironment, isVitestRuntimeEnvironment, locate, locateMany, locateManySync, locateSync, locateUp, locateUpSync, read, readAsModule, readAsModuleSync, readPackageField, readPackageFieldSync, readSync, registerFormat, removeFileNameExtension, serializeValue, setModuleReader, unregisterFormat, useFormatRegistry, wrapLoaderError, wrapWriteError, write, writePackageField, writePackageFieldSync, writeSync };
1224
1351
 
1225
1352
  //# sourceMappingURL=index.mjs.map