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