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/README.MD +446 -91
- package/dist/index.d.mts +420 -101
- package/dist/index.mjs +774 -344
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -12
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
|
-
|
|
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/
|
|
202
|
-
|
|
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
|
|
206
|
-
for (const p of patterns) for (const cwd of opts.cwd) {
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
+
function* locateUpBody(pattern, options) {
|
|
286
309
|
let walk = startWalk(options);
|
|
287
310
|
while (true) {
|
|
288
|
-
const found =
|
|
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
|
-
|
|
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/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
324
|
-
return this.parse(
|
|
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,
|
|
350
|
+
throw wrapLoaderError(e, input);
|
|
327
351
|
}
|
|
328
352
|
}
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
|
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/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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/
|
|
382
|
-
function
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
};
|
|
391
|
-
return data;
|
|
477
|
+
var JSONWriter = class extends TextFileWriter {
|
|
478
|
+
options;
|
|
479
|
+
constructor(options = {}) {
|
|
480
|
+
super();
|
|
481
|
+
this.options = options;
|
|
392
482
|
}
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
556
|
+
return await this.load(input);
|
|
455
557
|
} catch (e) {
|
|
456
558
|
if (isUnrecoverableError(e)) throw e;
|
|
457
|
-
if (isTsNodeRuntimeEnvironment())
|
|
458
|
-
|
|
559
|
+
if (isTsNodeRuntimeEnvironment()) return this.loadSync(input);
|
|
560
|
+
return this.instance(input);
|
|
459
561
|
}
|
|
460
|
-
return toModuleRecord(output);
|
|
461
562
|
}
|
|
462
|
-
|
|
463
|
-
let output;
|
|
563
|
+
readSync(input) {
|
|
464
564
|
try {
|
|
465
|
-
|
|
565
|
+
return this.loadSync(input);
|
|
466
566
|
} catch (e) {
|
|
467
567
|
if (isUnrecoverableError(e)) throw e;
|
|
468
|
-
|
|
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/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
923
|
+
throw this.unknownExtensionError(filePath);
|
|
564
924
|
}
|
|
565
|
-
|
|
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
|
-
|
|
927
|
+
yield* op(() => writer.write(filePath, plain), () => writer.writeSync(filePath, plain));
|
|
573
928
|
} catch (e) {
|
|
574
|
-
throw
|
|
929
|
+
throw wrapWriteError(e, filePath);
|
|
575
930
|
}
|
|
576
931
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
594
|
-
|
|
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
|
|
600
|
-
} else
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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/
|
|
1038
|
+
//#region src/format/registry/singleton.ts
|
|
648
1039
|
let instance;
|
|
649
|
-
function
|
|
1040
|
+
function useFormatRegistry() {
|
|
650
1041
|
if (typeof instance !== "undefined") return instance;
|
|
651
|
-
instance = new
|
|
1042
|
+
instance = new FormatRegistry();
|
|
652
1043
|
return instance;
|
|
653
1044
|
}
|
|
654
1045
|
//#endregion
|
|
655
|
-
//#region src/
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
665
|
-
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
|
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 {
|
|
1086
|
+
* import { setModuleReader } from 'locter';
|
|
686
1087
|
*
|
|
687
|
-
*
|
|
1088
|
+
* setModuleReader({
|
|
688
1089
|
* load: (id) => import(id),
|
|
689
1090
|
* });
|
|
690
1091
|
* ```
|
|
691
1092
|
*/
|
|
692
|
-
function
|
|
693
|
-
|
|
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/
|
|
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
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|