locter 2.2.1 โ 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 +581 -44
- package/dist/index.d.mts +563 -0
- package/dist/index.mjs +1117 -671
- package/dist/index.mjs.map +1 -1
- package/package.json +36 -40
- package/dist/index.cjs +0 -743
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/loader/built-in/conf/index.d.ts +0 -1
- package/dist/loader/built-in/conf/module.d.ts +0 -6
- package/dist/loader/built-in/index.d.ts +0 -3
- package/dist/loader/built-in/json/index.d.ts +0 -1
- package/dist/loader/built-in/json/module.d.ts +0 -5
- package/dist/loader/built-in/module/index.d.ts +0 -3
- package/dist/loader/built-in/module/module.d.ts +0 -15
- package/dist/loader/built-in/module/type.d.ts +0 -8
- package/dist/loader/built-in/module/utils.d.ts +0 -9
- package/dist/loader/built-in/yaml/index.d.ts +0 -1
- package/dist/loader/built-in/yaml/module.d.ts +0 -5
- package/dist/loader/constants.d.ts +0 -6
- package/dist/loader/helpers.d.ts +0 -6
- package/dist/loader/index.d.ts +0 -3
- package/dist/loader/module.d.ts +0 -18
- package/dist/loader/singleton.d.ts +0 -2
- package/dist/loader/type.d.ts +0 -8
- package/dist/locator/async.d.ts +0 -3
- package/dist/locator/index.d.ts +0 -4
- package/dist/locator/sync.d.ts +0 -3
- package/dist/locator/types.d.ts +0 -15
- package/dist/locator/utils.d.ts +0 -7
- package/dist/utils/error.d.ts +0 -1
- package/dist/utils/file-name.d.ts +0 -2
- package/dist/utils/file-path.d.ts +0 -1
- package/dist/utils/has-property.d.ts +0 -2
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/object.d.ts +0 -2
- package/dist/utils/runtime.d.ts +0 -2
- package/dist/utils/to-array.d.ts +0 -1
- package/dist/utils/typescript.d.ts +0 -1
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
|
|
@@ -50,8 +90,8 @@ import { locateMany } from 'locter';
|
|
|
50
90
|
console.log(files);
|
|
51
91
|
/*
|
|
52
92
|
[
|
|
53
|
-
{
|
|
54
|
-
{
|
|
93
|
+
{ directory: '/cwd/files', name: 'example', extension: '.js', path: '/cwd/files/example.js' },
|
|
94
|
+
{ directory: '/cwd/files', name: 'example', extension: '.ts', path: '/cwd/files/example.ts' }
|
|
55
95
|
]
|
|
56
96
|
*/
|
|
57
97
|
|
|
@@ -62,9 +102,9 @@ import { locateMany } from 'locter';
|
|
|
62
102
|
console.log(files);
|
|
63
103
|
/*
|
|
64
104
|
[
|
|
65
|
-
{
|
|
66
|
-
{
|
|
67
|
-
{
|
|
105
|
+
{ directory: '/cwd/files', name: 'example', extension: '.js', path: '/cwd/files/example.js' },
|
|
106
|
+
{ directory: '/cwd/files', name: 'example', extension: '.ts', path: '/cwd/files/example.ts' },
|
|
107
|
+
{ directory: '/cwd/files', name: 'example-long', extension: '.ts', path: '/cwd/files/example-long.ts' }
|
|
68
108
|
]
|
|
69
109
|
*/
|
|
70
110
|
})
|
|
@@ -86,78 +126,575 @@ import { locate } from 'locter';
|
|
|
86
126
|
|
|
87
127
|
console.log(file);
|
|
88
128
|
/*
|
|
89
|
-
{
|
|
129
|
+
{ directory: '/cwd/files', name: 'example', extension: '.js', path: '/cwd/files/example.js' }
|
|
90
130
|
*/
|
|
91
131
|
})
|
|
92
132
|
```
|
|
93
133
|
|
|
94
134
|
A synchronous variant is also available: `locateSync`
|
|
95
135
|
|
|
96
|
-
|
|
136
|
+
**Walking up**
|
|
137
|
+
|
|
138
|
+
`locateUp` walks from a starting directory toward the filesystem root and returns the first match (useful for discovering config files at the repo root from any sub-directory):
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { locateUp } from 'locter';
|
|
142
|
+
|
|
143
|
+
(async () => {
|
|
144
|
+
const info = await locateUp('trapi.config.{ts,mts,cts,mjs,cjs,js,json}', {
|
|
145
|
+
cwd: process.cwd(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log(info);
|
|
149
|
+
/*
|
|
150
|
+
{ directory: '/repo', name: 'trapi.config', extension: '.ts', path: '/repo/trapi.config.ts' }
|
|
151
|
+
or `undefined` if nothing matched
|
|
152
|
+
*/
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Pass `stopAt: '/repo'` to cap the walk at a known ceiling โ inclusive, so the ceiling directory itself is still searched.
|
|
157
|
+
|
|
158
|
+
A synchronous variant `locateUpSync` is also available.
|
|
159
|
+
|
|
160
|
+
**Options**
|
|
161
|
+
|
|
162
|
+
`locate` / `locateMany` (and their sync variants) accept an options object:
|
|
163
|
+
|
|
164
|
+
| Option | Type | Default | Notes |
|
|
165
|
+
|-------------------|-------------------------|-----------------|--------------------------------------------------------|
|
|
166
|
+
| `cwd` | `string \| string[]` | `process.cwd()` | Working directory (or directories) the pattern is resolved against |
|
|
167
|
+
| `ignore` | `string \| string[]` | `[]` | Patterns to exclude |
|
|
168
|
+
| `onlyFiles` | `boolean` | `true` | Match files only |
|
|
169
|
+
| `onlyDirectories` | `boolean` | `false` | Match directories only |
|
|
170
|
+
| `dot` | `boolean` | `false` | Match dotfiles (e.g. `.env`, `.npmrc`) for wildcard patterns |
|
|
97
171
|
|
|
98
|
-
|
|
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
|
+
|
|
174
|
+
`locateUp` / `locateUpSync` take the same options except `cwd` must be a single `string`, and additionally accept `stopAt?: string` (inclusive ceiling for the walk).
|
|
175
|
+
|
|
176
|
+
### Reader
|
|
177
|
+
|
|
178
|
+
The `read` method loads a file/module in an asynchronous fashion.
|
|
99
179
|
Either a string or the output of the locate/locateSync method can be passed as argument.
|
|
100
180
|
|
|
101
181
|
```typescript
|
|
102
|
-
import {
|
|
182
|
+
import { read, locate } from 'locter';
|
|
103
183
|
|
|
104
184
|
(async () => {
|
|
105
185
|
const file = await locate(
|
|
106
186
|
'files/example.{js,.ts}'
|
|
107
187
|
);
|
|
108
188
|
|
|
109
|
-
let content = await
|
|
189
|
+
let content = await read(file);
|
|
110
190
|
console.log(content);
|
|
111
191
|
// ...
|
|
112
192
|
|
|
113
|
-
content = await
|
|
193
|
+
content = await read('...');
|
|
114
194
|
console.log(content);
|
|
115
195
|
// ...
|
|
116
196
|
})
|
|
117
197
|
```
|
|
118
198
|
|
|
119
|
-
There is also a synchronous method called `
|
|
199
|
+
There is also a synchronous method called `readSync`.
|
|
120
200
|
|
|
121
201
|
```typescript
|
|
122
|
-
import {
|
|
202
|
+
import { readSync, locateSync } from 'locter';
|
|
203
|
+
|
|
204
|
+
const file = locateSync(
|
|
205
|
+
'files/example.{js,.ts}'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const content = readSync(file);
|
|
209
|
+
```
|
|
210
|
+
|
|
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';
|
|
123
224
|
|
|
124
225
|
(async () => {
|
|
125
|
-
const
|
|
126
|
-
'files/example.{js,.ts}'
|
|
127
|
-
);
|
|
226
|
+
const pkg = await read('package.json');
|
|
128
227
|
|
|
129
|
-
|
|
130
|
-
console.log(
|
|
131
|
-
|
|
228
|
+
console.log(pkg.default); // the full parsed JSON value
|
|
229
|
+
console.log(pkg.version); // top-level keys are named exports
|
|
230
|
+
})
|
|
231
|
+
```
|
|
132
232
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
```
|
|
256
|
+
|
|
257
|
+
**Reading a `package.json` field**
|
|
258
|
+
|
|
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.):
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { readPackageField } from 'locter';
|
|
263
|
+
|
|
264
|
+
(async () => {
|
|
265
|
+
const cwd = process.cwd();
|
|
266
|
+
|
|
267
|
+
const value = await readPackageField<{ entry: string }>('myapp', { cwd });
|
|
268
|
+
// โ the value of the `myapp` field, or undefined if package.json
|
|
269
|
+
// or the field is absent.
|
|
270
|
+
|
|
271
|
+
// Walk parent directories until a package.json is found:
|
|
272
|
+
const parentValue = await readPackageField('myapp', {
|
|
273
|
+
cwd,
|
|
274
|
+
walkUp: true,
|
|
275
|
+
stopAt: '/repo', // optional ceiling, inclusive
|
|
276
|
+
});
|
|
136
277
|
})
|
|
137
278
|
```
|
|
138
279
|
|
|
139
|
-
|
|
140
|
-
- **ConfLoader**: This loader allows to load `.conf` files.
|
|
141
|
-
- **JSONLoader**: This loader allows to load `.json` files.
|
|
142
|
-
- **YAMLLoader**: This loader allows to load `.yml` files.
|
|
143
|
-
- **ModuleLoader**: This loader allows to load modules with
|
|
144
|
-
`.js`, `.mjs`, `.mts`, `.cjs`, `.cts`, `.ts` file extensions independent of the environment (cjs or esm).
|
|
280
|
+
Throws `LocterLoadError` if the resolved `package.json` is malformed. A sync variant `readPackageFieldSync` is also available.
|
|
145
281
|
|
|
146
|
-
|
|
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:
|
|
147
287
|
|
|
148
288
|
```typescript
|
|
149
|
-
import {
|
|
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
|
+
```
|
|
334
|
+
|
|
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**
|
|
150
346
|
|
|
151
|
-
|
|
152
|
-
|
|
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**.
|
|
153
351
|
|
|
352
|
+
```typescript
|
|
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:
|
|
370
|
+
|
|
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).
|
|
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
|
+
},
|
|
154
395
|
},
|
|
155
|
-
|
|
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.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { TextFileReader, TextFileWriter, registerFormat } from 'locter';
|
|
416
|
+
import { parse, stringify } from 'smol-toml';
|
|
156
417
|
|
|
418
|
+
class TomlReader extends TextFileReader {
|
|
419
|
+
parse(content: string) {
|
|
420
|
+
return parse(content);
|
|
157
421
|
}
|
|
158
|
-
}
|
|
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
|
+
});
|
|
159
435
|
```
|
|
160
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
|
|
458
|
+
```
|
|
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
|
+
|
|
464
|
+
### Errors
|
|
465
|
+
|
|
466
|
+
`read` / `write` (and every built-in format) throw a typed subclass of
|
|
467
|
+
`LocterError`, so callers can distinguish failure modes without substring
|
|
468
|
+
matching on `.message`:
|
|
469
|
+
|
|
470
|
+
| Class | When it's thrown |
|
|
471
|
+
|--------------------------------|---------------------------------------------------------|
|
|
472
|
+
| `LocterNotFoundError` | File or module does not exist (`ENOENT`, `MODULE_NOT_FOUND`, `ERR_MODULE_NOT_FOUND`) |
|
|
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 |
|
|
475
|
+
| `LocterUnknownExtensionError` | No rule matched the file's extension |
|
|
476
|
+
| `LocterError` | Base class; matches all of the above |
|
|
477
|
+
|
|
478
|
+
Each error exposes the offending `path` and preserves the underlying error
|
|
479
|
+
on `cause`:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import { read, LocterNotFoundError, LocterLoadError } from 'locter';
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await read('config.json');
|
|
486
|
+
} catch (err) {
|
|
487
|
+
if (err instanceof LocterNotFoundError) {
|
|
488
|
+
console.error(`Config file not found: ${err.path}`);
|
|
489
|
+
} else if (err instanceof LocterLoadError) {
|
|
490
|
+
console.error(`Failed to parse config: ${err.cause}`);
|
|
491
|
+
} else {
|
|
492
|
+
throw err;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
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).
|
|
500
|
+
|
|
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.
|
|
502
|
+
|
|
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.
|
|
504
|
+
|
|
505
|
+
### Runtime environments
|
|
506
|
+
|
|
507
|
+
Locter ships with runtime detection helpers:
|
|
508
|
+
|
|
509
|
+
- `isJestRuntimeEnvironment()` โ true when running under Jest. Built-in: the
|
|
510
|
+
`ModuleReader` falls back to `require()` under Jest to avoid a known
|
|
511
|
+
segmentation fault ([nodejs/node#35889](https://github.com/nodejs/node/issues/35889)).
|
|
512
|
+
- `isVitestRuntimeEnvironment()` โ true when running under Vitest (checks
|
|
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.
|
|
521
|
+
|
|
522
|
+
#### Using locter with Vitest
|
|
523
|
+
|
|
524
|
+
Locter's built-in `await import(id)` runs inside `node_modules/locter` and
|
|
525
|
+
therefore bypasses Vitest's `vite-node` module graph by default. For most use
|
|
526
|
+
cases this is fine, but it produces **duplicate module instances** when test
|
|
527
|
+
code statically imports a module and locter dynamically loads the same module
|
|
528
|
+
(or a transitive dependency of it). This breaks class identity for libraries
|
|
529
|
+
that rely on it (e.g. TypeORM entity metadata).
|
|
530
|
+
|
|
531
|
+
Use `setModuleReader` to inject an `import` call from user space so Vitest can
|
|
532
|
+
rewrite it:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// vitest setup file
|
|
536
|
+
import { setModuleReader } from 'locter';
|
|
537
|
+
|
|
538
|
+
setModuleReader({
|
|
539
|
+
load: (id) => import(id),
|
|
540
|
+
});
|
|
541
|
+
```
|
|
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
|
+
|
|
552
|
+
Alternatively, the same effect can be achieved by inlining locter in
|
|
553
|
+
`vitest.config.ts`:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
export default defineConfig({
|
|
557
|
+
test: {
|
|
558
|
+
server: {
|
|
559
|
+
deps: {
|
|
560
|
+
inline: [/locter/],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
```
|
|
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
|
+
|
|
161
698
|
|
|
162
699
|
## License
|
|
163
700
|
|