locter 3.0.0 → 4.0.0-beta.1
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 +463 -99
- package/dist/index.d.mts +455 -101
- package/dist/index.mjs +823 -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
|
-
|
|
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
|
+
};
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/format/built-in/module/constants.ts
|
|
498
|
+
/**
|
|
499
|
+
* Single source of truth for the extensions handled by the module loader:
|
|
500
|
+
* consumed by BOTH the built-in routing registry and the jiti config.
|
|
501
|
+
*/
|
|
502
|
+
const MODULE_FILE_EXTENSIONS = [
|
|
503
|
+
".js",
|
|
504
|
+
".mjs",
|
|
505
|
+
".mts",
|
|
506
|
+
".cjs",
|
|
507
|
+
".cts",
|
|
508
|
+
".ts"
|
|
509
|
+
];
|
|
421
510
|
//#endregion
|
|
422
|
-
//#region src/
|
|
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,613 @@ 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;
|
|
514
867
|
}
|
|
515
|
-
|
|
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
|
+
/**
|
|
880
|
+
* Read a file/module RAW: the plain parsed value for data formats
|
|
881
|
+
* (mutable, round-trip-symmetric with write); for modules the
|
|
882
|
+
* normalized module record — a module IS a record, and the
|
|
883
|
+
* normalization only irons out the CJS/ESM interop divergence so
|
|
884
|
+
* both twin variants agree on one shape.
|
|
885
|
+
*/
|
|
886
|
+
async read(input) {
|
|
887
|
+
return runTwinAsync(this.readBody(input));
|
|
888
|
+
}
|
|
889
|
+
readSync(input) {
|
|
890
|
+
return runTwinSync(this.readBody(input));
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Read a file/module and present the result as a normalized module
|
|
894
|
+
* record — the uniform, frozen shape regardless of format:
|
|
895
|
+
* `.default` always holds the loaded value, top-level keys are
|
|
896
|
+
* re-exposed as named exports.
|
|
897
|
+
*/
|
|
898
|
+
async readAsModule(input) {
|
|
899
|
+
return runTwinAsync(this.readAsModuleBody(input));
|
|
900
|
+
}
|
|
901
|
+
readAsModuleSync(input) {
|
|
902
|
+
return runTwinSync(this.readAsModuleBody(input));
|
|
903
|
+
}
|
|
904
|
+
*readBody(input) {
|
|
905
|
+
const { output, reader } = yield* this.executeBody(input);
|
|
906
|
+
if (reader instanceof ModuleReader) return toModuleRecord(output);
|
|
907
|
+
return output;
|
|
908
|
+
}
|
|
909
|
+
*readAsModuleBody(input) {
|
|
910
|
+
const { output, reader } = yield* this.executeBody(input);
|
|
911
|
+
return this.toRecord(output, reader);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Shared dispatch + execution of both read bodies: resolve the
|
|
915
|
+
* reader, run it, wrap errors. Normalization is the caller's call.
|
|
916
|
+
*/
|
|
917
|
+
*executeBody(input) {
|
|
516
918
|
const filePath = buildFilePath(input);
|
|
919
|
+
const reader = this.findReader(filePath);
|
|
920
|
+
if (!reader) throw this.unknownExtensionError(filePath);
|
|
517
921
|
try {
|
|
518
|
-
return
|
|
922
|
+
return {
|
|
923
|
+
output: yield* op(() => reader.read(filePath), () => reader.readSync(filePath)),
|
|
924
|
+
reader
|
|
925
|
+
};
|
|
519
926
|
} catch (e) {
|
|
520
927
|
throw wrapLoaderError(e, filePath);
|
|
521
928
|
}
|
|
522
929
|
}
|
|
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
|
-
];
|
|
930
|
+
async write(input, value) {
|
|
931
|
+
return runTwinAsync(this.writeBody(input, value));
|
|
932
|
+
}
|
|
933
|
+
writeSync(input, value) {
|
|
934
|
+
runTwinSync(this.writeBody(input, value));
|
|
556
935
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
936
|
+
/**
|
|
937
|
+
* Shared body of write/writeSync: dispatch, unwrap records, write, wrap.
|
|
938
|
+
* The inverse boundary of readAsModuleBody: a record produced by
|
|
939
|
+
* readAsModule() (brand detected) is unwrapped to its `.default` value;
|
|
940
|
+
* anything else is written as-is.
|
|
941
|
+
*/
|
|
942
|
+
*writeBody(input, value) {
|
|
943
|
+
const filePath = buildFilePath(input);
|
|
944
|
+
if (!isFilePath(filePath)) throw new LocterWriteError({
|
|
945
|
+
message: `Cannot write to a bare module specifier: ${filePath}`,
|
|
946
|
+
path: filePath
|
|
947
|
+
});
|
|
948
|
+
const writer = this.findWriter(filePath);
|
|
949
|
+
if (!writer) {
|
|
950
|
+
if (this.findReader(filePath)) throw new LocterWriteError({
|
|
951
|
+
message: `The format of extension ${pathToLocatorInfo(filePath).extension} is read-only.`,
|
|
952
|
+
path: filePath
|
|
562
953
|
});
|
|
563
|
-
|
|
954
|
+
throw this.unknownExtensionError(filePath);
|
|
564
955
|
}
|
|
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);
|
|
956
|
+
const plain = isModuleRecord(value) ? value.default : value;
|
|
571
957
|
try {
|
|
572
|
-
|
|
958
|
+
yield* op(() => writer.write(filePath, plain), () => writer.writeSync(filePath, plain));
|
|
573
959
|
} catch (e) {
|
|
574
|
-
throw
|
|
960
|
+
throw wrapWriteError(e, filePath);
|
|
575
961
|
}
|
|
576
962
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
963
|
+
/**
|
|
964
|
+
* The single normalization boundary: every readAsModule result becomes a module
|
|
965
|
+
* record (`.default` is always the loaded value, top-level keys stay
|
|
966
|
+
* accessible as named exports). Provenance decides how: module-reader
|
|
967
|
+
* output may legitimately already be a record (`__esModule` is meaningful
|
|
968
|
+
* there); any other reader returns arbitrary parsed data, which is always
|
|
969
|
+
* wrapped — even if it happens to contain an `__esModule` key.
|
|
970
|
+
*/
|
|
971
|
+
toRecord(output, reader) {
|
|
972
|
+
if (reader instanceof ModuleReader) return toModuleRecord(output);
|
|
973
|
+
return createModuleRecord(output);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Typed accessor for the LIVE built-in reader instance (lazy, cached).
|
|
977
|
+
*/
|
|
978
|
+
builtInReader(id) {
|
|
979
|
+
let reader = this.builtInReaderCache.get(id);
|
|
980
|
+
if (!reader) {
|
|
981
|
+
reader = BUILT_IN_PRESETS[id].reader();
|
|
982
|
+
this.builtInReaderCache.set(id, reader);
|
|
585
983
|
}
|
|
984
|
+
return reader;
|
|
586
985
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
986
|
+
/**
|
|
987
|
+
* Typed accessor for the LIVE built-in writer instance (lazy, cached).
|
|
988
|
+
* Only writable format ids are accepted — read-only formats (module)
|
|
989
|
+
* are a compile error.
|
|
990
|
+
*/
|
|
991
|
+
builtInWriter(id) {
|
|
992
|
+
let writer = this.builtInWriterCache.get(id);
|
|
993
|
+
if (!writer) {
|
|
994
|
+
writer = BUILT_IN_PRESETS[id].writer();
|
|
995
|
+
this.builtInWriterCache.set(id, writer);
|
|
996
|
+
}
|
|
997
|
+
return writer;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Dispatch order:
|
|
1001
|
+
* 1. bare specifier (no extension) → module reader, always
|
|
1002
|
+
* 2. user rules, registration order → first match wins (can override built-ins)
|
|
1003
|
+
* 3. built-in extension table
|
|
1004
|
+
* 4. undefined → read/readSync throw LocterUnknownExtensionError
|
|
1005
|
+
*/
|
|
1006
|
+
findReader(input) {
|
|
1007
|
+
if (!isFilePath(input)) return this.builtInReader("module");
|
|
1008
|
+
const rule = this.findRule(input, (item) => typeof item.getReader !== "undefined");
|
|
1009
|
+
if (rule && rule.getReader) return rule.getReader();
|
|
1010
|
+
const id = this.findBuiltInId(input);
|
|
1011
|
+
if (id) return this.builtInReader(id);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Write-side dispatch. No bare-specifier step (there is nothing to
|
|
1015
|
+
* write to); only rules with a writer slot participate, then built-in
|
|
1016
|
+
* presets that declare a writer.
|
|
1017
|
+
*/
|
|
1018
|
+
findWriter(input) {
|
|
1019
|
+
if (!isFilePath(input)) return;
|
|
1020
|
+
const rule = this.findRule(input, (item) => typeof item.getWriter !== "undefined");
|
|
1021
|
+
if (rule && rule.getWriter) return rule.getWriter();
|
|
1022
|
+
const id = this.findBuiltInId(input);
|
|
1023
|
+
if (id && "writer" in BUILT_IN_PRESETS[id]) return this.builtInWriter(id);
|
|
592
1024
|
}
|
|
593
|
-
|
|
594
|
-
if (!isFilePath(input)) return "module";
|
|
1025
|
+
findRule(input, filter) {
|
|
595
1026
|
const info = pathToLocatorInfo(input);
|
|
596
1027
|
for (const rule of this.rules) {
|
|
1028
|
+
if (!filter(rule)) continue;
|
|
597
1029
|
const { test } = rule;
|
|
598
1030
|
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;
|
|
1031
|
+
if (info.extension && test.includes(info.extension)) return rule;
|
|
1032
|
+
} else {
|
|
1033
|
+
test.lastIndex = 0;
|
|
1034
|
+
const matched = test.test(buildFilePath(info));
|
|
1035
|
+
test.lastIndex = 0;
|
|
1036
|
+
if (matched) return rule;
|
|
630
1037
|
}
|
|
631
1038
|
}
|
|
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
1039
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1040
|
+
findBuiltInId(input) {
|
|
1041
|
+
const info = pathToLocatorInfo(input);
|
|
1042
|
+
if (!info.extension) return;
|
|
1043
|
+
return this.builtInExtensions.get(info.extension);
|
|
1044
|
+
}
|
|
1045
|
+
unknownExtensionError(input) {
|
|
1046
|
+
return new LocterUnknownExtensionError({
|
|
1047
|
+
message: `No format registered for extension: ${pathToLocatorInfo(input).extension ?? "unknown"}`,
|
|
1048
|
+
path: input
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
compile(id, rule) {
|
|
1052
|
+
return {
|
|
1053
|
+
id,
|
|
1054
|
+
test: rule.test,
|
|
1055
|
+
getReader: compileSlot(rule.reader),
|
|
1056
|
+
getWriter: compileSlot(rule.writer)
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
generateRuleId() {
|
|
1060
|
+
let id;
|
|
1061
|
+
do {
|
|
1062
|
+
this.ruleCounter++;
|
|
1063
|
+
id = `rule:${this.ruleCounter}`;
|
|
1064
|
+
} while (this.rules.some((item) => item.id === id));
|
|
1065
|
+
return id;
|
|
644
1066
|
}
|
|
645
1067
|
};
|
|
646
1068
|
//#endregion
|
|
647
|
-
//#region src/
|
|
1069
|
+
//#region src/format/registry/singleton.ts
|
|
648
1070
|
let instance;
|
|
649
|
-
function
|
|
1071
|
+
function useFormatRegistry() {
|
|
650
1072
|
if (typeof instance !== "undefined") return instance;
|
|
651
|
-
instance = new
|
|
1073
|
+
instance = new FormatRegistry();
|
|
652
1074
|
return instance;
|
|
653
1075
|
}
|
|
654
1076
|
//#endregion
|
|
655
|
-
//#region src/
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1077
|
+
//#region src/format/helpers.ts
|
|
1078
|
+
function registerFormat(rule) {
|
|
1079
|
+
return useFormatRegistry().register(rule);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Remove a rule from the process-global registry by its id
|
|
1083
|
+
* (as returned by registerFormat). Built-in ids cannot be unregistered.
|
|
1084
|
+
*/
|
|
1085
|
+
function unregisterFormat(id) {
|
|
1086
|
+
return useFormatRegistry().unregister(id);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Read a file/module RAW: the plain parsed value for data formats
|
|
1090
|
+
* (json/yaml/conf and custom readers — mutable, round-trip-symmetric
|
|
1091
|
+
* with write); for modules the normalized module record (a module IS a
|
|
1092
|
+
* record; normalization only irons out CJS/ESM interop divergence).
|
|
1093
|
+
*/
|
|
1094
|
+
async function read(input) {
|
|
1095
|
+
return useFormatRegistry().read(input);
|
|
1096
|
+
}
|
|
1097
|
+
function readSync(input) {
|
|
1098
|
+
return useFormatRegistry().readSync(input);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Read a file/module and present the result as a normalized module
|
|
1102
|
+
* record — the uniform, frozen shape regardless of format: `.default`
|
|
1103
|
+
* always holds the loaded value, top-level keys are re-exposed as
|
|
1104
|
+
* named exports.
|
|
1105
|
+
*/
|
|
1106
|
+
async function readAsModule(input) {
|
|
1107
|
+
return useFormatRegistry().readAsModule(input);
|
|
1108
|
+
}
|
|
1109
|
+
function readAsModuleSync(input) {
|
|
1110
|
+
return useFormatRegistry().readAsModuleSync(input);
|
|
663
1111
|
}
|
|
664
|
-
async function
|
|
665
|
-
|
|
666
|
-
if (typeof input === "string") return manager.execute(input);
|
|
667
|
-
return manager.execute(buildFilePath(input));
|
|
1112
|
+
async function write(input, value) {
|
|
1113
|
+
return useFormatRegistry().write(input, value);
|
|
668
1114
|
}
|
|
669
|
-
function
|
|
670
|
-
|
|
671
|
-
if (typeof input === "string") return manager.executeSync(input);
|
|
672
|
-
return manager.executeSync(buildFilePath(input));
|
|
1115
|
+
function writeSync(input, value) {
|
|
1116
|
+
useFormatRegistry().writeSync(input, value);
|
|
673
1117
|
}
|
|
674
1118
|
/**
|
|
675
|
-
* Override the built-in module
|
|
1119
|
+
* Override the built-in module reader's `import` / `require` calls.
|
|
676
1120
|
*
|
|
677
1121
|
* Useful for test runners (e.g. Vitest) where the dynamic `import()` inside
|
|
678
1122
|
* `node_modules/locter` would bypass the runner's module graph. Calling this
|
|
679
1123
|
* from user code (e.g. a Vitest setup file) lets the runner rewrite the
|
|
680
1124
|
* `import()` so loaded modules share identity with statically imported ones.
|
|
681
1125
|
*
|
|
1126
|
+
* Returns a restore function that re-applies the previous configuration —
|
|
1127
|
+
* handy for scoped overrides (e.g. test teardown). The restore function is
|
|
1128
|
+
* scoped to the exact reader instance it configured: after a registry
|
|
1129
|
+
* reset() (which discards that instance) it becomes a no-op instead of
|
|
1130
|
+
* re-applying stale configuration to the fresh instance.
|
|
1131
|
+
*
|
|
682
1132
|
* @example
|
|
683
1133
|
* ```ts
|
|
684
1134
|
* // vitest setup file
|
|
685
|
-
* import {
|
|
1135
|
+
* import { setModuleReader } from 'locter';
|
|
686
1136
|
*
|
|
687
|
-
*
|
|
1137
|
+
* setModuleReader({
|
|
688
1138
|
* load: (id) => import(id),
|
|
689
1139
|
* });
|
|
690
1140
|
* ```
|
|
691
1141
|
*/
|
|
692
|
-
function
|
|
693
|
-
|
|
1142
|
+
function setModuleReader(options) {
|
|
1143
|
+
const reader = useFormatRegistry().builtInReader("module");
|
|
1144
|
+
const previous = reader.configure(options);
|
|
1145
|
+
return () => {
|
|
1146
|
+
reader.configure(previous);
|
|
1147
|
+
};
|
|
694
1148
|
}
|
|
695
1149
|
//#endregion
|
|
696
|
-
//#region src/
|
|
1150
|
+
//#region src/format/package-field.ts
|
|
697
1151
|
function extractField(pkg, field) {
|
|
698
1152
|
if (!isObject(pkg) || !hasOwnProperty(pkg, field)) return;
|
|
699
1153
|
return pkg[field];
|
|
700
1154
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (options.walkUp) info = await locateUp("package.json", {
|
|
1155
|
+
function* locatePackageBody(options) {
|
|
1156
|
+
if (options.walkUp) return yield* op(() => locateUp("package.json", {
|
|
704
1157
|
cwd: options.cwd,
|
|
705
1158
|
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", {
|
|
1159
|
+
}), () => locateUpSync("package.json", {
|
|
719
1160
|
cwd: options.cwd,
|
|
720
1161
|
stopAt: options.stopAt
|
|
721
|
-
});
|
|
722
|
-
|
|
1162
|
+
}));
|
|
1163
|
+
return yield* op(() => locate("package.json", { cwd: options.cwd }), () => locateSync("package.json", { cwd: options.cwd }));
|
|
1164
|
+
}
|
|
1165
|
+
function* readPackageFieldBody(field, options) {
|
|
1166
|
+
const info = yield* locatePackageBody(options);
|
|
723
1167
|
if (!info) return;
|
|
1168
|
+
const reader = useFormatRegistry().builtInReader("json");
|
|
1169
|
+
const filePath = buildFilePath(info);
|
|
724
1170
|
try {
|
|
725
|
-
return extractField(
|
|
1171
|
+
return extractField(yield* op(() => reader.read(filePath), () => reader.readSync(filePath)), field);
|
|
726
1172
|
} catch (e) {
|
|
727
1173
|
if (e instanceof LocterNotFoundError) return;
|
|
728
1174
|
throw e;
|
|
729
1175
|
}
|
|
730
1176
|
}
|
|
1177
|
+
async function readPackageField(field, options = {}) {
|
|
1178
|
+
return runTwinAsync(readPackageFieldBody(field, options));
|
|
1179
|
+
}
|
|
1180
|
+
function readPackageFieldSync(field, options = {}) {
|
|
1181
|
+
return runTwinSync(readPackageFieldBody(field, options));
|
|
1182
|
+
}
|
|
1183
|
+
function* writePackageFieldBody(field, value, options) {
|
|
1184
|
+
if (!isSafeObjectKey(field)) throw new LocterError({ message: `The field ${field} is not a safe object key.` });
|
|
1185
|
+
const info = yield* locatePackageBody(options);
|
|
1186
|
+
if (!info) throw new LocterNotFoundError({ message: "No package.json could be located." });
|
|
1187
|
+
const reader = useFormatRegistry().builtInReader("json");
|
|
1188
|
+
const filePath = buildFilePath(info);
|
|
1189
|
+
const pkg = yield* op(() => reader.read(filePath), () => reader.readSync(filePath));
|
|
1190
|
+
if (!isObject(pkg)) throw new LocterWriteError({
|
|
1191
|
+
message: "The located package.json does not contain an object.",
|
|
1192
|
+
path: filePath
|
|
1193
|
+
});
|
|
1194
|
+
pkg[field] = value;
|
|
1195
|
+
const writer = new JSONWriter({ indent: "auto" });
|
|
1196
|
+
yield* op(() => writer.write(filePath, pkg), () => writer.writeSync(filePath, pkg));
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Set a top-level field of the nearest `package.json` (same locate
|
|
1200
|
+
* semantics as readPackageField) and write it back, preserving the
|
|
1201
|
+
* file's existing indentation. Passing `undefined` removes the field.
|
|
1202
|
+
* Throws LocterNotFoundError when no package.json could be located.
|
|
1203
|
+
*/
|
|
1204
|
+
async function writePackageField(field, value, options = {}) {
|
|
1205
|
+
return runTwinAsync(writePackageFieldBody(field, value, options));
|
|
1206
|
+
}
|
|
1207
|
+
function writePackageFieldSync(field, value, options = {}) {
|
|
1208
|
+
runTwinSync(writePackageFieldBody(field, value, options));
|
|
1209
|
+
}
|
|
731
1210
|
//#endregion
|
|
732
|
-
export {
|
|
1211
|
+
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, readAsModule, readAsModuleSync, readPackageField, readPackageFieldSync, readSync, registerFormat, setModuleReader, unregisterFormat, useFormatRegistry, wrapLoaderError, wrapWriteError, write, writePackageField, writePackageFieldSync, writeSync };
|
|
733
1212
|
|
|
734
1213
|
//# sourceMappingURL=index.mjs.map
|