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/README.MD
CHANGED
|
@@ -1,19 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/logo.svg" alt="locter" width="130" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">locter</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>Locate, read & write files โ one API.</b><br>
|
|
9
|
+
Glob-based file discovery plus a pluggable format layer for JSON, YAML,
|
|
10
|
+
<code>.conf</code> and TS/ESM/CJS modules โ every operation sync <i>and</i> async.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://www.npmjs.com/package/locter"><img src="https://badge.fury.io/js/locter.svg" alt="npm version" /></a>
|
|
15
|
+
<a href="https://github.com/tada5hi/locter/actions/workflows/main.yml"><img src="https://github.com/tada5hi/locter/actions/workflows/main.yml/badge.svg" alt="CI" /></a>
|
|
16
|
+
<a href="https://codecov.io/gh/Tada5hi/locter"><img src="https://codecov.io/gh/Tada5hi/locter/branch/master/graph/badge.svg?token=4KNSG8L13V" alt="codecov" /></a>
|
|
17
|
+
<a href="https://snyk.io/test/github/Tada5hi/locter?targetFile=package.json"><img src="https://snyk.io/test/github/Tada5hi/locter/badge.svg?targetFile=package.json" alt="Known Vulnerabilities" /></a>
|
|
18
|
+
<a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white" alt="Conventional Commits" /></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="#installation"><b>Installation</b></a>
|
|
23
|
+
ยท
|
|
24
|
+
<a href="#usage">Usage</a>
|
|
25
|
+
ยท
|
|
26
|
+
<a href="#formats">Formats</a>
|
|
27
|
+
ยท
|
|
28
|
+
<a href="#errors">Errors</a>
|
|
29
|
+
ยท
|
|
30
|
+
<a href="#migration">Migration</a>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- ๐ **Locate** โ `locate`, `locateMany`, `locateUp`: fast-glob discovery returning structured `LocatorInfo` records.
|
|
38
|
+
- ๐ **Read** โ one `read()` for JSON, YAML, `.conf`, and JS/TS modules in any runtime (powered by `jiti`); every result normalized to a module record.
|
|
39
|
+
- โ๏ธ **Write** โ `write()` values back without pulling serializers into your app: comment-preserving YAML, indent-preserving JSON, rc9-style `.conf`.
|
|
40
|
+
- ๐ **Sync twins** โ every operation ships as `fn` / `fnSync`, derived from one shared implementation.
|
|
41
|
+
- ๐งฉ **Extensible** โ register custom formats with independent reader/writer slots; override built-ins per extension.
|
|
42
|
+
- ๐จ **Typed errors** โ `LocterNotFoundError`, `LocterLoadError`, `LocterWriteError`, โฆ each carrying `path` and `cause`.
|
|
10
43
|
|
|
11
44
|
**Table of Contents**
|
|
12
45
|
|
|
13
46
|
- [Installation](#installation)
|
|
14
47
|
- [Usage](#usage)
|
|
15
48
|
- [Locator](#locator)
|
|
16
|
-
- [
|
|
49
|
+
- [Reader](#reader)
|
|
50
|
+
- [Writer](#writer)
|
|
51
|
+
- [Formats](#formats)
|
|
52
|
+
- [Errors](#errors)
|
|
53
|
+
- [Runtime environments](#runtime-environments)
|
|
54
|
+
- [Migration](#migration)
|
|
55
|
+
- [Migrating from 3.x](#migrating-from-3x)
|
|
56
|
+
- [Migrating from 2.x](#migrating-from-2x)
|
|
17
57
|
- [License](#license)
|
|
18
58
|
|
|
19
59
|
## Installation
|
|
@@ -129,74 +169,107 @@ A synchronous variant `locateUpSync` is also available.
|
|
|
129
169
|
| `onlyDirectories` | `boolean` | `false` | Match directories only |
|
|
130
170
|
| `dot` | `boolean` | `false` | Match dotfiles (e.g. `.env`, `.npmrc`) for wildcard patterns |
|
|
131
171
|
|
|
172
|
+
If both flags are set, `onlyDirectories: true` takes precedence over `onlyFiles`; when neither restricts the match (`onlyFiles: false`), files **and** directories are returned.
|
|
173
|
+
|
|
132
174
|
`locateUp` / `locateUpSync` take the same options except `cwd` must be a single `string`, and additionally accept `stopAt?: string` (inclusive ceiling for the walk).
|
|
133
175
|
|
|
134
|
-
###
|
|
176
|
+
### Reader
|
|
135
177
|
|
|
136
|
-
The `
|
|
178
|
+
The `read` method loads a file/module in an asynchronous fashion.
|
|
137
179
|
Either a string or the output of the locate/locateSync method can be passed as argument.
|
|
138
180
|
|
|
139
181
|
```typescript
|
|
140
|
-
import {
|
|
182
|
+
import { read, locate } from 'locter';
|
|
141
183
|
|
|
142
184
|
(async () => {
|
|
143
185
|
const file = await locate(
|
|
144
186
|
'files/example.{js,.ts}'
|
|
145
187
|
);
|
|
146
188
|
|
|
147
|
-
let content = await
|
|
189
|
+
let content = await read(file);
|
|
148
190
|
console.log(content);
|
|
149
191
|
// ...
|
|
150
192
|
|
|
151
|
-
content = await
|
|
193
|
+
content = await read('...');
|
|
152
194
|
console.log(content);
|
|
153
195
|
// ...
|
|
154
196
|
})
|
|
155
197
|
```
|
|
156
198
|
|
|
157
|
-
There is also a synchronous method called `
|
|
199
|
+
There is also a synchronous method called `readSync`.
|
|
158
200
|
|
|
159
201
|
```typescript
|
|
160
|
-
import {
|
|
202
|
+
import { readSync, locateSync } from 'locter';
|
|
161
203
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
);
|
|
204
|
+
const file = locateSync(
|
|
205
|
+
'files/example.{js,.ts}'
|
|
206
|
+
);
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// ...
|
|
208
|
+
const content = readSync(file);
|
|
209
|
+
```
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
Bare module specifiers (no file extension) are routed to the module reader, so
|
|
212
|
+
`read('yaml')` behaves like `import('yaml')` โ note that reading a module
|
|
213
|
+
**executes** it.
|
|
214
|
+
|
|
215
|
+
**Module records**
|
|
216
|
+
|
|
217
|
+
Every `read` / `readSync` result is a **module record** โ a frozen,
|
|
218
|
+
null-prototype object shaped like an ES module namespace: `default` always
|
|
219
|
+
holds the loaded value, and when that value is an object, its top-level keys
|
|
220
|
+
are re-exposed as named exports.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { read } from 'locter';
|
|
224
|
+
|
|
225
|
+
(async () => {
|
|
226
|
+
const pkg = await read('package.json');
|
|
227
|
+
|
|
228
|
+
console.log(pkg.default); // the full parsed JSON value
|
|
229
|
+
console.log(pkg.version); // top-level keys are named exports
|
|
174
230
|
})
|
|
175
231
|
```
|
|
176
232
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
233
|
+
For JavaScript/TypeScript files the module's own namespace passes through
|
|
234
|
+
unchanged; the output of every other reader (built-in JSON/YAML/Conf and
|
|
235
|
+
user-registered ones) is always wrapped โ even if the parsed data happens to
|
|
236
|
+
contain an `__esModule` key. The record is read-only: to mutate the loaded
|
|
237
|
+
data, work with `.default`.
|
|
238
|
+
|
|
239
|
+
Records additionally carry a private, non-forgeable brand: `isModuleRecord`
|
|
240
|
+
tells records produced by `read()` apart from arbitrary data, and
|
|
241
|
+
[`write`](#writer) uses the brand to unwrap `.default` automatically on
|
|
242
|
+
write-back.
|
|
243
|
+
|
|
244
|
+
To pick an export by predicate, the `getModuleExport` helper is available:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { getModuleExport, read } from 'locter';
|
|
248
|
+
|
|
249
|
+
(async () => {
|
|
250
|
+
const record = await read('files/example.ts');
|
|
251
|
+
|
|
252
|
+
const found = getModuleExport(record, (key) => key === 'myExport');
|
|
253
|
+
// โ { key: 'myExport', value: ... }, or undefined if no key matches
|
|
254
|
+
})
|
|
255
|
+
```
|
|
183
256
|
|
|
184
257
|
**Reading a `package.json` field**
|
|
185
258
|
|
|
186
|
-
`
|
|
259
|
+
`readPackageField` reads a top-level field from the nearest `package.json` โ handy for the common `pkg.<your-config-key>` config-loading fallback (vite's `pkg.vite`, eslint's `pkg.eslintConfig`, etc.):
|
|
187
260
|
|
|
188
261
|
```typescript
|
|
189
|
-
import {
|
|
262
|
+
import { readPackageField } from 'locter';
|
|
190
263
|
|
|
191
264
|
(async () => {
|
|
192
265
|
const cwd = process.cwd();
|
|
193
266
|
|
|
194
|
-
const value = await
|
|
267
|
+
const value = await readPackageField<{ entry: string }>('myapp', { cwd });
|
|
195
268
|
// โ the value of the `myapp` field, or undefined if package.json
|
|
196
269
|
// or the field is absent.
|
|
197
270
|
|
|
198
271
|
// Walk parent directories until a package.json is found:
|
|
199
|
-
const parentValue = await
|
|
272
|
+
const parentValue = await readPackageField('myapp', {
|
|
200
273
|
cwd,
|
|
201
274
|
walkUp: true,
|
|
202
275
|
stopAt: '/repo', // optional ceiling, inclusive
|
|
@@ -204,33 +277,201 @@ import { loadPackageField } from 'locter';
|
|
|
204
277
|
})
|
|
205
278
|
```
|
|
206
279
|
|
|
207
|
-
Throws `LocterLoadError` if the resolved `package.json` is malformed. A sync variant `
|
|
280
|
+
Throws `LocterLoadError` if the resolved `package.json` is malformed. A sync variant `readPackageFieldSync` is also available.
|
|
281
|
+
|
|
282
|
+
### Writer
|
|
283
|
+
|
|
284
|
+
The `write` method serializes a value to a file, dispatched by extension like
|
|
285
|
+
`read` โ no need to pull `yaml` or a `.conf` serializer into the consuming
|
|
286
|
+
application:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { read, write } from 'locter';
|
|
290
|
+
|
|
291
|
+
(async () => {
|
|
292
|
+
const config = await read('config.yml');
|
|
293
|
+
config.default.port = 8080;
|
|
294
|
+
|
|
295
|
+
// records produced by read() are unwrapped automatically โ
|
|
296
|
+
// the file receives the plain value, not the record wrapper
|
|
297
|
+
await write('config.yml', config);
|
|
298
|
+
|
|
299
|
+
// plain values work just as well
|
|
300
|
+
await write('generated/settings.json', { port: 8080 });
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
A synchronous variant is also available: `writeSync`.
|
|
305
|
+
|
|
306
|
+
Write semantics:
|
|
307
|
+
|
|
308
|
+
- **Missing parent directories are created** (`mkdir -p`).
|
|
309
|
+
- Output always ends with a single trailing newline.
|
|
310
|
+
- Values that are records produced by `read()` (detected via the private
|
|
311
|
+
brand โ an `__esModule` key in plain data does **not** count) are unwrapped
|
|
312
|
+
to their `.default` value before serialization.
|
|
313
|
+
- Module formats (`.js`, `.ts`, โฆ) are **read-only**: `write('config.ts', โฆ)`
|
|
314
|
+
throws `LocterWriteError`, as does writing to a bare module specifier.
|
|
315
|
+
|
|
316
|
+
Per-format behavior:
|
|
317
|
+
|
|
318
|
+
- **JSON** โ 4-space indent by default; configurable per instance:
|
|
319
|
+
`new JSONWriter({ indent })` accepts a number of spaces, a literal indent
|
|
320
|
+
string, or `'auto'` (detect and keep the indentation of the existing file).
|
|
321
|
+
- **YAML** โ *comment-preserving* write-back: when the target file exists, the
|
|
322
|
+
value is grafted into the parsed document, so comments and anchors attached
|
|
323
|
+
to surviving keys survive. Arrays and type-changed nodes are replaced
|
|
324
|
+
wholesale. A corrupt existing file throws instead of being overwritten.
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
# port used by the dev server
|
|
328
|
+
port: 3000
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
await write('config.yml', { port: 8080 });
|
|
333
|
+
```
|
|
208
334
|
|
|
209
|
-
|
|
335
|
+
```yaml
|
|
336
|
+
# port used by the dev server
|
|
337
|
+
port: 8080
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
- **Conf** โ the inverse of the reader: nested objects become dot-separated
|
|
341
|
+
`key=value` lines, arrays repeated `key[]=` lines. Round-trips are
|
|
342
|
+
structural, not textual: comments and line order are dropped, and a string
|
|
343
|
+
that parses as another type (`'123'`) reads back as that type (`123`).
|
|
344
|
+
|
|
345
|
+
**Writing a `package.json` field**
|
|
346
|
+
|
|
347
|
+
`writePackageField` is the write-side companion of `readPackageField` (same
|
|
348
|
+
`cwd` / `walkUp` / `stopAt` semantics): it sets a top-level field of the
|
|
349
|
+
nearest `package.json` and writes it back **preserving the file's existing
|
|
350
|
+
indentation**.
|
|
210
351
|
|
|
211
352
|
```typescript
|
|
212
|
-
import {
|
|
353
|
+
import { writePackageField } from 'locter';
|
|
354
|
+
|
|
355
|
+
(async () => {
|
|
356
|
+
await writePackageField('myapp', { entry: 'src' }, { cwd: process.cwd() });
|
|
357
|
+
|
|
358
|
+
// passing undefined removes the field
|
|
359
|
+
await writePackageField('myapp', undefined);
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Throws `LocterNotFoundError` when no `package.json` could be located. A sync
|
|
364
|
+
variant `writePackageFieldSync` is also available.
|
|
365
|
+
|
|
366
|
+
### Formats
|
|
367
|
+
|
|
368
|
+
A format couples an extension set with a reader and an optional writer. The
|
|
369
|
+
built-in formats are pre-registered:
|
|
213
370
|
|
|
214
|
-
|
|
215
|
-
|
|
371
|
+
- **conf** (`.conf`): `ConfReader` + `ConfWriter`
|
|
372
|
+
- **json** (`.json`): `JSONReader` + `JSONWriter`
|
|
373
|
+
- **yaml** (`.yml`, `.yaml`): `YAMLReader` + `YAMLWriter`
|
|
374
|
+
- **module** (`.js`, `.mjs`, `.mts`, `.cjs`, `.cts`, `.ts`): `ModuleReader`
|
|
375
|
+
(read-only) โ loads modules independent of the environment (cjs or esm).
|
|
216
376
|
|
|
377
|
+
To register a format for other file types, use `registerFormat`. Rules are
|
|
378
|
+
matched in registration order, **before** the built-ins โ registering an
|
|
379
|
+
extension a built-in claims (e.g. `.json`) overrides it. The reader and writer
|
|
380
|
+
slots are independent: a reader-only rule overrides how `.json` is read
|
|
381
|
+
without shadowing the built-in JSON writer (and vice versa).
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { registerFormat } from 'locter';
|
|
385
|
+
|
|
386
|
+
registerFormat({
|
|
387
|
+
test: ['.ext'],
|
|
388
|
+
reader: {
|
|
389
|
+
async read(input: string) {
|
|
390
|
+
// ...
|
|
391
|
+
},
|
|
392
|
+
readSync(input: string) {
|
|
393
|
+
// ...
|
|
394
|
+
},
|
|
217
395
|
},
|
|
218
|
-
|
|
396
|
+
writer: {
|
|
397
|
+
async write(path: string, value: unknown) {
|
|
398
|
+
// ...
|
|
399
|
+
},
|
|
400
|
+
writeSync(path: string, value: unknown) {
|
|
401
|
+
// ...
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
For text-based formats, extending the abstract `TextFileReader` /
|
|
408
|
+
`TextFileWriter` base classes is simpler than implementing the `IReader` /
|
|
409
|
+
`IWriter` ports by hand: the bases own UTF-8 I/O, typed error wrapping, and
|
|
410
|
+
derive the sync and async surface from a single implementation โ only
|
|
411
|
+
`parse(content)` respectively `stringify(value)` must be provided. The
|
|
412
|
+
built-in JSON/YAML/Conf formats are implemented this way.
|
|
219
413
|
|
|
414
|
+
```typescript
|
|
415
|
+
import { TextFileReader, TextFileWriter, registerFormat } from 'locter';
|
|
416
|
+
import { parse, stringify } from 'smol-toml';
|
|
417
|
+
|
|
418
|
+
class TomlReader extends TextFileReader {
|
|
419
|
+
parse(content: string) {
|
|
420
|
+
return parse(content);
|
|
220
421
|
}
|
|
221
|
-
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
class TomlWriter extends TextFileWriter {
|
|
425
|
+
stringify(value: unknown) {
|
|
426
|
+
return stringify(value);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
registerFormat({
|
|
431
|
+
test: ['.toml'],
|
|
432
|
+
reader: () => new TomlReader(),
|
|
433
|
+
writer: () => new TomlWriter(),
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Instead of instances, the slots accept factory functions (as above) โ a
|
|
438
|
+
factory is invoked lazily on the first matching input and the first
|
|
439
|
+
successfully constructed instance is cached (a factory that throws is retried
|
|
440
|
+
on the next match).
|
|
441
|
+
|
|
442
|
+
Every rule has a stable id (auto-generated, or set explicitly). Registering an
|
|
443
|
+
existing id replaces that rule in place; built-in ids (`module`, `conf`,
|
|
444
|
+
`json`, `yaml`) are reserved. The registry can be inspected and unwound:
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
import { registerFormat, unregisterFormat, useFormatRegistry } from 'locter';
|
|
448
|
+
|
|
449
|
+
const registration = registerFormat({ test: ['.json5'], reader: json5Reader });
|
|
450
|
+
unregisterFormat(registration.id); // remove it again
|
|
451
|
+
|
|
452
|
+
registerFormat({ id: 'json5', test: ['.json5'], reader: otherReader }); // replace by id
|
|
453
|
+
|
|
454
|
+
const registry = useFormatRegistry(); // the process-global FormatRegistry
|
|
455
|
+
registry.entries(); // [{ id, test, builtIn }, ...] in match order
|
|
456
|
+
registry.has('json'); // true (built-in)
|
|
457
|
+
registry.reset(); // drop all user rules + cached instances
|
|
222
458
|
```
|
|
223
459
|
|
|
460
|
+
The global registry belongs to the **application**. Libraries that need custom
|
|
461
|
+
formats should create their own isolated instance instead of mutating the
|
|
462
|
+
singleton: `new FormatRegistry({ rules: [...] })`.
|
|
463
|
+
|
|
224
464
|
### Errors
|
|
225
465
|
|
|
226
|
-
`
|
|
466
|
+
`read` / `write` (and every built-in format) throw a typed subclass of
|
|
227
467
|
`LocterError`, so callers can distinguish failure modes without substring
|
|
228
468
|
matching on `.message`:
|
|
229
469
|
|
|
230
470
|
| Class | When it's thrown |
|
|
231
471
|
|--------------------------------|---------------------------------------------------------|
|
|
232
472
|
| `LocterNotFoundError` | File or module does not exist (`ENOENT`, `MODULE_NOT_FOUND`, `ERR_MODULE_NOT_FOUND`) |
|
|
233
|
-
| `LocterLoadError` | A
|
|
473
|
+
| `LocterLoadError` | A reader threw โ parse error, runtime/eval error, etc. |
|
|
474
|
+
| `LocterWriteError` | A write failed โ serialization error, filesystem error, read-only format, corrupt existing YAML target |
|
|
234
475
|
| `LocterUnknownExtensionError` | No rule matched the file's extension |
|
|
235
476
|
| `LocterError` | Base class; matches all of the above |
|
|
236
477
|
|
|
@@ -238,10 +479,10 @@ Each error exposes the offending `path` and preserves the underlying error
|
|
|
238
479
|
on `cause`:
|
|
239
480
|
|
|
240
481
|
```typescript
|
|
241
|
-
import {
|
|
482
|
+
import { read, LocterNotFoundError, LocterLoadError } from 'locter';
|
|
242
483
|
|
|
243
484
|
try {
|
|
244
|
-
await
|
|
485
|
+
await read('config.json');
|
|
245
486
|
} catch (err) {
|
|
246
487
|
if (err instanceof LocterNotFoundError) {
|
|
247
488
|
console.error(`Config file not found: ${err.path}`);
|
|
@@ -253,56 +494,30 @@ try {
|
|
|
253
494
|
}
|
|
254
495
|
```
|
|
255
496
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
### Migrating from 2.x
|
|
497
|
+
Note that on the write side `ENOENT` stays a `LocterWriteError` โ a missing
|
|
498
|
+
parent directory is a write failure, not a lookup miss (and `write` creates
|
|
499
|
+
missing parents anyway).
|
|
261
500
|
|
|
262
|
-
|
|
501
|
+
`LocterError` extends [`BaseError`](https://github.com/Tada5hi/ebec) from `@ebec/core`, so each instance also carries a `code` (auto-derived from the class name unless overridden โ e.g. `'ENOENT'` for `LocterNotFoundError`s thrown by the JSON reader) and a `toJSON()` for structured logging.
|
|
263
502
|
|
|
264
|
-
|
|
265
|
-
- import { load, locate } from 'locter';
|
|
266
|
-
+ import { load, locate, LocterNotFoundError, LocterLoadError } from 'locter';
|
|
267
|
-
|
|
268
|
-
// 1. LocatorOptions.path โ cwd
|
|
269
|
-
- await locate('config.json', { path: process.cwd() });
|
|
270
|
-
+ await locate('config.json', { cwd: process.cwd() });
|
|
271
|
-
|
|
272
|
-
// 2. LocatorInfo.path now holds the FULL file path (it was the
|
|
273
|
-
// containing directory in 2.x). buildFilePath(info) still works in
|
|
274
|
-
// both versions but is now a near-identity for LocatorInfo inputs.
|
|
275
|
-
- const filePath = buildFilePath(info); // 2.x: composed from .path + .name + .extension
|
|
276
|
-
+ const filePath = info.path; // 3.x: just the path field
|
|
277
|
-
+ const dir = info.directory; // 3.x: containing directory (the old `info.path`)
|
|
278
|
-
|
|
279
|
-
// 3. load() / loadSync() (and every built-in loader) now throw
|
|
280
|
-
// LocterError subclasses instead of generic Error / ebec BaseError.
|
|
281
|
-
try {
|
|
282
|
-
await load('config.json');
|
|
283
|
-
- } catch (err) {
|
|
284
|
-
- if (err.message.includes('ENOENT')) { /* โฆ */ }
|
|
285
|
-
+ } catch (err) {
|
|
286
|
-
+ if (err instanceof LocterNotFoundError) { /* โฆ */ }
|
|
287
|
-
+ else if (err instanceof LocterLoadError) { /* parse error: err.cause */ }
|
|
288
|
-
+ else throw err;
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Additionally:
|
|
293
|
-
|
|
294
|
-
- The `ebec` runtime dependency is replaced by `@ebec/core`. If you used to import `BaseError` from `ebec`, switch to `@ebec/core`.
|
|
295
|
-
- The package is now ESM-only (no `require('locter')`). This change shipped in the 2.xโ3.x toolchain modernization commit; mentioned here for completeness.
|
|
503
|
+
Each error class also publishes its `Symbol.for(...)` marker (`LOCTER_ERROR_MARKER`, `LOCTER_NOT_FOUND_ERROR_MARKER`, โฆ), which makes `instanceof` work across realms โ useful when consumers end up with duplicate copies of locter in their dependency tree.
|
|
296
504
|
|
|
297
505
|
### Runtime environments
|
|
298
506
|
|
|
299
|
-
Locter ships with
|
|
507
|
+
Locter ships with runtime detection helpers:
|
|
300
508
|
|
|
301
509
|
- `isJestRuntimeEnvironment()` โ true when running under Jest. Built-in: the
|
|
302
|
-
`
|
|
510
|
+
`ModuleReader` falls back to `require()` under Jest to avoid a known
|
|
303
511
|
segmentation fault ([nodejs/node#35889](https://github.com/nodejs/node/issues/35889)).
|
|
304
512
|
- `isVitestRuntimeEnvironment()` โ true when running under Vitest (checks
|
|
305
513
|
`process.env.VITEST === 'true'`).
|
|
514
|
+
- `isTsNodeRuntimeEnvironment()` โ true when a `ts-node` register instance is
|
|
515
|
+
active. Built-in: the `ModuleReader`'s async path falls back to its sync
|
|
516
|
+
loading under ts-node.
|
|
517
|
+
- `isTsxRuntimeEnvironment()` โ true when the process was started through
|
|
518
|
+
[tsx](https://tsx.is) (`tsx script.ts`, `node --import tsx`, or a resolved
|
|
519
|
+
`.../node_modules/tsx/...` loader path). Useful for callers that need to
|
|
520
|
+
know whether TypeScript sources can be executed directly.
|
|
306
521
|
|
|
307
522
|
#### Using locter with Vitest
|
|
308
523
|
|
|
@@ -313,18 +528,27 @@ code statically imports a module and locter dynamically loads the same module
|
|
|
313
528
|
(or a transitive dependency of it). This breaks class identity for libraries
|
|
314
529
|
that rely on it (e.g. TypeORM entity metadata).
|
|
315
530
|
|
|
316
|
-
Use `
|
|
531
|
+
Use `setModuleReader` to inject an `import` call from user space so Vitest can
|
|
317
532
|
rewrite it:
|
|
318
533
|
|
|
319
534
|
```typescript
|
|
320
535
|
// vitest setup file
|
|
321
|
-
import {
|
|
536
|
+
import { setModuleReader } from 'locter';
|
|
322
537
|
|
|
323
|
-
|
|
538
|
+
setModuleReader({
|
|
324
539
|
load: (id) => import(id),
|
|
325
540
|
});
|
|
326
541
|
```
|
|
327
542
|
|
|
543
|
+
`setModuleReader` returns a restore function that re-applies the previous
|
|
544
|
+
configuration โ useful for scoped overrides:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
const restore = setModuleReader({ load: (id) => import(id) });
|
|
548
|
+
// ...
|
|
549
|
+
restore();
|
|
550
|
+
```
|
|
551
|
+
|
|
328
552
|
Alternatively, the same effect can be achieved by inlining locter in
|
|
329
553
|
`vitest.config.ts`:
|
|
330
554
|
|
|
@@ -340,6 +564,137 @@ export default defineConfig({
|
|
|
340
564
|
});
|
|
341
565
|
```
|
|
342
566
|
|
|
567
|
+
## Migration
|
|
568
|
+
|
|
569
|
+
### Migrating from 3.x
|
|
570
|
+
|
|
571
|
+
The 4.0 release renames the loader subsystem around the **format** concept
|
|
572
|
+
(read + write), reworks registration and dispatch, and normalizes results.
|
|
573
|
+
The list below is exhaustive โ no other public APIs changed.
|
|
574
|
+
|
|
575
|
+
**Renames** (clean break, no deprecated aliases):
|
|
576
|
+
|
|
577
|
+
| 3.x | 4.x |
|
|
578
|
+
|--------------------------------------|--------------------------------------------|
|
|
579
|
+
| `load` / `loadSync` | `read` / `readSync` |
|
|
580
|
+
| `registerLoader` / `unregisterLoader`| `registerFormat` / `unregisterFormat` |
|
|
581
|
+
| `LoaderManager` | `FormatRegistry` |
|
|
582
|
+
| `useLoader` | `useFormatRegistry` |
|
|
583
|
+
| `Loader` type (`execute`/`executeSync`) | `IReader` (`read`/`readSync`) + `IWriter` (`write`/`writeSync`) |
|
|
584
|
+
| `JSONLoader` / `YAMLLoader` / `ConfLoader` | `JSONReader`+`JSONWriter` / `YAMLReader`+`YAMLWriter` / `ConfReader`+`ConfWriter` |
|
|
585
|
+
| `ModuleLoader` | `ModuleReader` |
|
|
586
|
+
| `setModuleLoader` | `setModuleReader` |
|
|
587
|
+
| `loadPackageField` / `loadPackageFieldSync` | `readPackageField` / `readPackageFieldSync` |
|
|
588
|
+
|
|
589
|
+
**Behavioral changes:**
|
|
590
|
+
|
|
591
|
+
```diff
|
|
592
|
+
// 1. User rules now override built-ins. Registering an extension a
|
|
593
|
+
// built-in claims (e.g. `.json`) previously had NO effect โ the
|
|
594
|
+
// built-in rule always matched first. It now takes precedence.
|
|
595
|
+
registerFormat({ test: ['.json'], reader: myJson5Reader }); // 4.x: overrides the built-in
|
|
596
|
+
|
|
597
|
+
// 2. Plugin-string loaders are removed, and rules use the object form
|
|
598
|
+
// with independent reader/writer slots. Import the implementation
|
|
599
|
+
// yourself and register it (optionally lazily, via a factory).
|
|
600
|
+
- registerLoader({ test: ['.toml'], loader: 'toml' }); // resolved to @locter/toml, sync require()
|
|
601
|
+
+ import { TomlReader, TomlWriter } from '@locter/toml';
|
|
602
|
+
+ registerFormat({ test: ['.toml'], reader: () => new TomlReader(), writer: () => new TomlWriter() });
|
|
603
|
+
|
|
604
|
+
// 3. The registry no longer implements the loader interface: execute() /
|
|
605
|
+
// executeSync() become read() / readSync() (accepting LocatorInfo |
|
|
606
|
+
// string), and findLoader() / resolve() are replaced by
|
|
607
|
+
// findReader() / findWriter() / builtInReader() / builtInWriter().
|
|
608
|
+
- const manager = new LoaderManager();
|
|
609
|
+
- await manager.execute('file.json');
|
|
610
|
+
+ const registry = new FormatRegistry();
|
|
611
|
+
+ await registry.read('file.json'); // mirrors the global read() helper
|
|
612
|
+
+ registry.findReader('file.json'); // IReader | undefined
|
|
613
|
+
+ registry.builtInReader('module'); // statically typed as ModuleReader
|
|
614
|
+
|
|
615
|
+
// 4. Every read() / readSync() result is now a module record (see
|
|
616
|
+
// "Module records" above). Module files already resolved this way โ
|
|
617
|
+
// json/yaml/conf and custom readers used to return the bare parsed
|
|
618
|
+
// value.
|
|
619
|
+
const pkg = await read('package.json');
|
|
620
|
+
pkg.version; // top-level keys still work
|
|
621
|
+
- pkg.default; // undefined
|
|
622
|
+
+ pkg.default; // the full parsed value
|
|
623
|
+
+ // The record is frozen and null-prototype: code that mutates the
|
|
624
|
+
+ // result, relies on Object.prototype, or enumerates/serializes it
|
|
625
|
+
+ // (now includes `default`) must use pkg.default instead.
|
|
626
|
+
|
|
627
|
+
// 5. ModuleReader.read() / readSync() return the raw module value;
|
|
628
|
+
// normalization happens exactly once, at the registry boundary. Only
|
|
629
|
+
// relevant when invoking the reader class directly.
|
|
630
|
+
- await new ModuleLoader().execute('file.ts'); // normalized record
|
|
631
|
+
+ await new ModuleReader().read('file.ts'); // raw module value
|
|
632
|
+
+ await registry.read('file.ts'); // normalized record
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Removed from the public surface** (internal plumbing that leaked through the old `export *` barrels; the package now exports a curated, snapshot-tested list):
|
|
636
|
+
|
|
637
|
+
- Locator internals: `buildLocatorPatterns`, `buildLocatorOptions`, `pathToLocatorInfo`, `buildFilePathWithoutExtension` (deleted โ it had no callers). `buildFilePath` and `isLocatorInfo` stay public.
|
|
638
|
+
- Record internals: `isESModule`, `toModuleRecord`, `createModuleRecord`. Use `read()` (which normalizes) and the public `getModuleExport` / `isModuleRecord`.
|
|
639
|
+
- Generic helpers: `isObject`, `isSafeObjectKey`, `hasOwnProperty`, `hasStringProperty`, `toArray`, `isFilePath`, `getFileNameExtension`, `removeFileNameExtension`, `isTypeScriptError`. The runtime detection helpers (`isJestRuntimeEnvironment`, `isVitestRuntimeEnvironment`, `isTsNodeRuntimeEnvironment`, `isTsxRuntimeEnvironment`) stay public.
|
|
640
|
+
|
|
641
|
+
**Locator option precedence** โ `{ onlyFiles: true, onlyDirectories: true }` used to resolve to *neither* restriction (everything matched); `onlyDirectories: true` now wins. `{ onlyFiles: false }` used to mean directories-only; it now means "no restriction" (files and directories).
|
|
642
|
+
|
|
643
|
+
**New in 4.0:**
|
|
644
|
+
|
|
645
|
+
- `write` / `writeSync` with per-format writers (`JSONWriter`, `YAMLWriter`,
|
|
646
|
+
`ConfWriter`), including comment-preserving YAML write-back โ see
|
|
647
|
+
[Writer](#writer).
|
|
648
|
+
- `writePackageField` / `writePackageFieldSync`.
|
|
649
|
+
- `LocterWriteError` + `wrapWriteError`.
|
|
650
|
+
- `isModuleRecord` โ brand-based detection of records produced by `read()`.
|
|
651
|
+
- The ports are exported as `IReader` / `IWriter`, alongside `Rule`,
|
|
652
|
+
`ReaderFactory`, `WriterFactory` โ so custom formats can be typed against
|
|
653
|
+
the package.
|
|
654
|
+
- The extensions handled by the module reader are exported as
|
|
655
|
+
`MODULE_FILE_EXTENSIONS`.
|
|
656
|
+
- `readPackageField` / `readPackageFieldSync` read `package.json` via the
|
|
657
|
+
built-in JSON reader directly: user-registered `.json` rules no longer
|
|
658
|
+
intercept the read, and synthetic record keys (`default`, `__esModule`) no
|
|
659
|
+
longer resolve as package fields.
|
|
660
|
+
|
|
661
|
+
### Migrating from 2.x
|
|
662
|
+
|
|
663
|
+
The 3.0 release reshapes a few public surfaces. The list below is exhaustive โ no other public APIs changed. (Symbols are shown under their 3.x names; see [Migrating from 3.x](#migrating-from-3x) for the current ones.)
|
|
664
|
+
|
|
665
|
+
```diff
|
|
666
|
+
- import { load, locate } from 'locter';
|
|
667
|
+
+ import { load, locate, LocterNotFoundError, LocterLoadError } from 'locter';
|
|
668
|
+
|
|
669
|
+
// 1. LocatorOptions.path โ cwd
|
|
670
|
+
- await locate('config.json', { path: process.cwd() });
|
|
671
|
+
+ await locate('config.json', { cwd: process.cwd() });
|
|
672
|
+
|
|
673
|
+
// 2. LocatorInfo.path now holds the FULL file path (it was the
|
|
674
|
+
// containing directory in 2.x). buildFilePath(info) still works in
|
|
675
|
+
// both versions but is now a near-identity for LocatorInfo inputs.
|
|
676
|
+
- const filePath = buildFilePath(info); // 2.x: composed from .path + .name + .extension
|
|
677
|
+
+ const filePath = info.path; // 3.x: just the path field
|
|
678
|
+
+ const dir = info.directory; // 3.x: containing directory (the old `info.path`)
|
|
679
|
+
|
|
680
|
+
// 3. load() / loadSync() (and every built-in loader) now throw
|
|
681
|
+
// LocterError subclasses instead of generic Error / ebec BaseError.
|
|
682
|
+
try {
|
|
683
|
+
await load('config.json');
|
|
684
|
+
- } catch (err) {
|
|
685
|
+
- if (err.message.includes('ENOENT')) { /* โฆ */ }
|
|
686
|
+
+ } catch (err) {
|
|
687
|
+
+ if (err instanceof LocterNotFoundError) { /* โฆ */ }
|
|
688
|
+
+ else if (err instanceof LocterLoadError) { /* parse error: err.cause */ }
|
|
689
|
+
+ else throw err;
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Additionally:
|
|
694
|
+
|
|
695
|
+
- The `ebec` runtime dependency is replaced by `@ebec/core`. If you used to import `BaseError` from `ebec`, switch to `@ebec/core`.
|
|
696
|
+
- The package is now ESM-only (no `require('locter')`). This change shipped in the 2.xโ3.x toolchain modernization commit; mentioned here for completeness.
|
|
697
|
+
|
|
343
698
|
|
|
344
699
|
## License
|
|
345
700
|
|