load-oxfmt-config 0.4.0 → 0.5.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 +126 -29
- package/dist/index.d.mts +130 -12
- package/dist/index.mjs +368 -108
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- 🔍 **Auto-discovery** - Automatically searches for config files in current and parent directories
|
|
13
13
|
- 📦 **Multiple formats** - Supports `.oxfmtrc.json`, `.oxfmtrc.jsonc`, and `oxfmt.config.ts`
|
|
14
14
|
- 🧩 **EditorConfig fallback** - Merges supported `.editorconfig` fields into the returned oxfmt config result
|
|
15
|
+
- 🚫 **Ignore resolution** - Resolves ignore status with oxfmt CLI-like global + config-scoped semantics
|
|
15
16
|
- ⚡ **Built-in caching** - Caches both file resolution and parsed configs for optimal performance
|
|
16
17
|
- 🎯 **TypeScript support** - Fully typed with comprehensive type definitions
|
|
17
18
|
- 🛠️ **Flexible API** - Support explicit config paths or automatic discovery
|
|
@@ -37,22 +38,22 @@ pnpm add load-oxfmt-config
|
|
|
37
38
|
Load config from current directory or parent directories:
|
|
38
39
|
|
|
39
40
|
```ts
|
|
40
|
-
import {
|
|
41
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
41
42
|
|
|
42
43
|
// Automatically searches for oxfmt config files and the nearest .editorconfig
|
|
43
|
-
const
|
|
44
|
-
console.log(config) // { printWidth: 80, ... }
|
|
44
|
+
const result = await loadOxfmtConfigResult()
|
|
45
|
+
console.log(result.config) // { printWidth: 80, ... }
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
### Merge With `.editorconfig`
|
|
48
49
|
|
|
49
50
|
```ts
|
|
50
|
-
import {
|
|
51
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
+
const result = await loadOxfmtConfigResult({ cwd: '/path/to/project' })
|
|
53
54
|
|
|
54
55
|
// Returns one merged static config object
|
|
55
|
-
console.log(config)
|
|
56
|
+
console.log(result.config)
|
|
56
57
|
// {
|
|
57
58
|
// tabWidth: 2,
|
|
58
59
|
// printWidth: 100,
|
|
@@ -65,26 +66,52 @@ console.log(config)
|
|
|
65
66
|
### Specify Working Directory
|
|
66
67
|
|
|
67
68
|
```ts
|
|
68
|
-
import {
|
|
69
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
69
70
|
|
|
70
|
-
const
|
|
71
|
+
const result = await loadOxfmtConfigResult({
|
|
71
72
|
cwd: '/path/to/project',
|
|
72
73
|
})
|
|
73
74
|
```
|
|
74
75
|
|
|
76
|
+
### Get Config Metadata
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
80
|
+
|
|
81
|
+
const result = await loadOxfmtConfigResult({ cwd: '/path/to/project' })
|
|
82
|
+
|
|
83
|
+
console.log(result.config) // merged oxfmt options
|
|
84
|
+
console.log(result.filepath) // /path/to/project/.oxfmtrc.json (or undefined)
|
|
85
|
+
console.log(result.dirname) // /path/to/project (or undefined)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Resolve Ignore Status
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { isOxfmtIgnored } from 'load-oxfmt-config'
|
|
92
|
+
|
|
93
|
+
const result = await isOxfmtIgnored({
|
|
94
|
+
cwd: '/path/to/project',
|
|
95
|
+
filepath: '/path/to/project/src/generated/foo.ts',
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
console.log(result)
|
|
99
|
+
// { ignored: true, reason: 'config-ignore-patterns' }
|
|
100
|
+
```
|
|
101
|
+
|
|
75
102
|
### Explicit Config Path
|
|
76
103
|
|
|
77
104
|
```ts
|
|
78
|
-
import {
|
|
105
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
79
106
|
|
|
80
107
|
// Relative path (resolved relative to cwd)
|
|
81
|
-
const
|
|
108
|
+
const result = await loadOxfmtConfigResult({
|
|
82
109
|
configPath: 'configs/.oxfmtrc.json',
|
|
83
110
|
cwd: '/path/to/project',
|
|
84
111
|
})
|
|
85
112
|
|
|
86
113
|
// Absolute path
|
|
87
|
-
const
|
|
114
|
+
const absoluteResult = await loadOxfmtConfigResult({
|
|
88
115
|
configPath: '/absolute/path/to/.oxfmtrc.json',
|
|
89
116
|
})
|
|
90
117
|
```
|
|
@@ -92,10 +119,10 @@ const config = await loadOxfmtConfig({
|
|
|
92
119
|
### Disable `.editorconfig`
|
|
93
120
|
|
|
94
121
|
```ts
|
|
95
|
-
import {
|
|
122
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
96
123
|
|
|
97
124
|
// Skip .editorconfig reading entirely
|
|
98
|
-
const
|
|
125
|
+
const result = await loadOxfmtConfigResult({
|
|
99
126
|
editorconfig: false,
|
|
100
127
|
})
|
|
101
128
|
```
|
|
@@ -103,10 +130,10 @@ const config = await loadOxfmtConfig({
|
|
|
103
130
|
### Limit `.editorconfig` to `cwd`
|
|
104
131
|
|
|
105
132
|
```ts
|
|
106
|
-
import {
|
|
133
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
107
134
|
|
|
108
135
|
// Only look in the cwd directory itself, no upward traversal
|
|
109
|
-
const
|
|
136
|
+
const result = await loadOxfmtConfigResult({
|
|
110
137
|
editorconfig: { onlyCwd: true },
|
|
111
138
|
})
|
|
112
139
|
```
|
|
@@ -114,10 +141,10 @@ const config = await loadOxfmtConfig({
|
|
|
114
141
|
### Override `.editorconfig` Search Directory
|
|
115
142
|
|
|
116
143
|
```ts
|
|
117
|
-
import {
|
|
144
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
118
145
|
|
|
119
146
|
// Search .editorconfig from a custom directory instead of the config file's directory
|
|
120
|
-
const
|
|
147
|
+
const result = await loadOxfmtConfigResult({
|
|
121
148
|
editorconfig: {
|
|
122
149
|
cwd: '/path/to/editorconfig-dir',
|
|
123
150
|
},
|
|
@@ -127,14 +154,23 @@ const config = await loadOxfmtConfig({
|
|
|
127
154
|
### Disable Caching
|
|
128
155
|
|
|
129
156
|
```ts
|
|
130
|
-
import {
|
|
157
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
131
158
|
|
|
132
159
|
// Force reload from disk, bypassing cache
|
|
133
|
-
const
|
|
160
|
+
const result = await loadOxfmtConfigResult({
|
|
134
161
|
useCache: false,
|
|
135
162
|
})
|
|
136
163
|
```
|
|
137
164
|
|
|
165
|
+
### Legacy API (Deprecated)
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
169
|
+
|
|
170
|
+
// Deprecated: prefer loadOxfmtConfigResult
|
|
171
|
+
const config = await loadOxfmtConfig({ cwd: '/path/to/project' })
|
|
172
|
+
```
|
|
173
|
+
|
|
138
174
|
### Path Resolution Only
|
|
139
175
|
|
|
140
176
|
```ts
|
|
@@ -149,13 +185,29 @@ console.log(configPath) // '/path/to/.oxfmtrc.json' or undefined
|
|
|
149
185
|
|
|
150
186
|
### `loadOxfmtConfig(options?)`
|
|
151
187
|
|
|
188
|
+
> Deprecated. Prefer `loadOxfmtConfigResult(options?)`.
|
|
189
|
+
|
|
152
190
|
Load and parse oxfmt configuration files, then merge supported `.editorconfig` fields into the returned result.
|
|
153
191
|
|
|
154
192
|
**Parameters:**
|
|
155
193
|
|
|
156
194
|
- `options` - Optional configuration object
|
|
157
195
|
|
|
158
|
-
**Returns:** `Promise<
|
|
196
|
+
**Returns:** `Promise<OxfmtOptions>` - Parsed and merged oxfmt configuration object. Returns empty object `{}` if no supported config file is found.
|
|
197
|
+
|
|
198
|
+
### `loadOxfmtConfigResult(options?)`
|
|
199
|
+
|
|
200
|
+
Load and parse oxfmt configuration files, merge supported `.editorconfig` fields, and return metadata for the resolved config file.
|
|
201
|
+
|
|
202
|
+
**Parameters:**
|
|
203
|
+
|
|
204
|
+
- `options` - Optional configuration object (same as `loadOxfmtConfig`)
|
|
205
|
+
|
|
206
|
+
**Returns:** `Promise<LoadOxfmtConfigResult>`
|
|
207
|
+
|
|
208
|
+
- `config` - Parsed and merged oxfmt configuration object
|
|
209
|
+
- `filepath` - Resolved config absolute path (undefined when not found)
|
|
210
|
+
- `dirname` - Config directory (undefined when not found)
|
|
159
211
|
|
|
160
212
|
**Throws:** Error if config file exists but cannot be parsed.
|
|
161
213
|
|
|
@@ -170,6 +222,23 @@ Resolve the absolute path to oxfmt config file.
|
|
|
170
222
|
|
|
171
223
|
**Returns:** `Promise<string | undefined>` - Absolute path to config file, or `undefined` if not found.
|
|
172
224
|
|
|
225
|
+
### `isOxfmtIgnored(options)`
|
|
226
|
+
|
|
227
|
+
Resolve whether a file should be ignored with oxfmt CLI-like semantics.
|
|
228
|
+
|
|
229
|
+
**Returns:** `Promise<IsOxfmtIgnoredResult>`
|
|
230
|
+
|
|
231
|
+
`ignorePath` accepts either a single path (`string`) or multiple paths (`string[]`).
|
|
232
|
+
|
|
233
|
+
- `ignored` - Whether the file is ignored
|
|
234
|
+
- `reason` - One of:
|
|
235
|
+
- `default-dir`
|
|
236
|
+
- `lockfile`
|
|
237
|
+
- `gitignore`
|
|
238
|
+
- `prettierignore`
|
|
239
|
+
- `ignore-path`
|
|
240
|
+
- `config-ignore-patterns`
|
|
241
|
+
|
|
173
242
|
## Options
|
|
174
243
|
|
|
175
244
|
All options are optional.
|
|
@@ -229,9 +298,7 @@ When `configPath` is not provided, the loader automatically searches for config
|
|
|
229
298
|
2. **Supported filenames:**
|
|
230
299
|
- `.oxfmtrc.json`
|
|
231
300
|
- `.oxfmtrc.jsonc`
|
|
232
|
-
|
|
233
|
-
- `oxfmt.config.ts`
|
|
234
|
-
|
|
301
|
+
- `oxfmt.config.ts`
|
|
235
302
|
3. **Stops when:**
|
|
236
303
|
- A valid config file is found
|
|
237
304
|
- Reaches the filesystem root
|
|
@@ -278,18 +345,47 @@ export default {
|
|
|
278
345
|
}
|
|
279
346
|
```
|
|
280
347
|
|
|
348
|
+
When `configPath` is passed explicitly, extensions `.json`, `.jsonc`, `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, and `.cjs` are supported. Extensionless paths are also accepted and parsed as JSON.
|
|
349
|
+
|
|
281
350
|
## `.editorconfig` Support
|
|
282
351
|
|
|
283
352
|
The loader reads the nearest `.editorconfig` file and maps the subset of fields that oxfmt supports:
|
|
284
353
|
|
|
285
354
|
- `end_of_line` → `endOfLine`
|
|
286
355
|
- `indent_style` → `useTabs`
|
|
287
|
-
- `indent_size`
|
|
356
|
+
- `indent_size` → `tabWidth` when `indent_style = space`
|
|
357
|
+
- `tab_width` → `tabWidth`
|
|
288
358
|
- `max_line_length` → `printWidth`
|
|
289
359
|
- `insert_final_newline` → `insertFinalNewline`
|
|
290
360
|
- `quote_type` → `singleQuote`
|
|
291
361
|
|
|
292
|
-
|
|
362
|
+
Only `[*]` is treated as a global section to match oxfmt. Other sections such as `[**]` and `[*.ts]` are converted into returned `overrides` entries.
|
|
363
|
+
|
|
364
|
+
## Ignore Strategy
|
|
365
|
+
|
|
366
|
+
`isOxfmtIgnored()` applies two layers:
|
|
367
|
+
|
|
368
|
+
1. Global ignore
|
|
369
|
+
2. Ignore patterns from the resolved oxfmt config (`ignorePatterns`)
|
|
370
|
+
|
|
371
|
+
Global ignore includes:
|
|
372
|
+
|
|
373
|
+
- Default ignored directories: `.git`, `.svn`, `.jj`, `node_modules`
|
|
374
|
+
- Default lockfiles: `package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`
|
|
375
|
+
- Ignore files:
|
|
376
|
+
- If `ignorePath` is provided: use those files only (multiple supported)
|
|
377
|
+
- If `ignorePath` is not provided: use `.gitignore` and `.prettierignore` from `cwd`
|
|
378
|
+
|
|
379
|
+
Notes:
|
|
380
|
+
|
|
381
|
+
- `node_modules` can be included by passing `withNodeModules: true`.
|
|
382
|
+
- This package does not read parent `.gitignore` files or global gitignore settings.
|
|
383
|
+
- The default lockfile list mirrors oxfmt documentation intent (`package-lock.json`, `pnpm-lock.yaml`, etc.) and common ecosystem lockfiles. It is not guaranteed to be a complete internal oxfmt list.
|
|
384
|
+
- `ignorePatterns` are always interpreted relative to the resolved oxfmt config directory.
|
|
385
|
+
- Nested config behavior follows oxfmt semantics:
|
|
386
|
+
- default: nearest config from target file directory upward
|
|
387
|
+
- `disableNestedConfig: true`: resolve from `cwd` only
|
|
388
|
+
- `configPath`: also disables nested lookup (same intent as CLI `-c`)
|
|
293
389
|
|
|
294
390
|
## Precedence
|
|
295
391
|
|
|
@@ -304,17 +400,18 @@ This means explicit oxfmt config values always win over `.editorconfig` fallback
|
|
|
304
400
|
|
|
305
401
|
## Limitations
|
|
306
402
|
|
|
307
|
-
`loadOxfmtConfig()` returns a static `OxfmtOptions`
|
|
403
|
+
`loadOxfmtConfigResult()` (and the deprecated `loadOxfmtConfig()`) returns a static merged `OxfmtOptions` shape. That means `.editorconfig` support is represented as merged root + overrides config data, not as per-file runtime evaluation. In practice this works well for common root settings and section-based overrides, but it is not a full replacement for oxfmt's own file-by-file config resolution.
|
|
308
404
|
|
|
309
405
|
## Error Handling
|
|
310
406
|
|
|
311
407
|
```ts
|
|
312
|
-
import {
|
|
408
|
+
import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
313
409
|
|
|
314
410
|
try {
|
|
315
|
-
const
|
|
411
|
+
const result = await loadOxfmtConfigResult()
|
|
412
|
+
console.log(result.config)
|
|
316
413
|
} catch (error) {
|
|
317
|
-
// Thrown when config file exists but contains invalid JSON
|
|
414
|
+
// Thrown when config file exists but contains invalid JSON/JSONC
|
|
318
415
|
console.error('Failed to parse oxfmt config:', error.message)
|
|
319
416
|
}
|
|
320
417
|
```
|
package/dist/index.d.mts
CHANGED
|
@@ -77,35 +77,153 @@ interface OxfmtOptions extends FormatConfig {
|
|
|
77
77
|
*/
|
|
78
78
|
overrides?: OxfmtConfigOverride[];
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Result object with metadata about resolved oxfmt config.
|
|
82
|
+
*/
|
|
83
|
+
interface LoadOxfmtConfigResult {
|
|
84
|
+
/**
|
|
85
|
+
* Final merged config (oxfmt + optional editorconfig mapping)
|
|
86
|
+
*/
|
|
87
|
+
config: OxfmtOptions;
|
|
88
|
+
/**
|
|
89
|
+
* Absolute path of resolved config file
|
|
90
|
+
*/
|
|
91
|
+
filepath?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Directory of resolved config file
|
|
94
|
+
*/
|
|
95
|
+
dirname?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Options for resolving whether a single file should be ignored.
|
|
99
|
+
*/
|
|
100
|
+
interface IsOxfmtIgnoredOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Current working directory.
|
|
103
|
+
* Also the base directory for default .gitignore/.prettierignore lookup.
|
|
104
|
+
*/
|
|
105
|
+
cwd?: string;
|
|
106
|
+
/**
|
|
107
|
+
* File path to test.
|
|
108
|
+
*/
|
|
109
|
+
filepath: string;
|
|
110
|
+
/**
|
|
111
|
+
* Explicit oxfmt config path.
|
|
112
|
+
* When provided, nested config lookup is disabled (same as oxfmt CLI -c).
|
|
113
|
+
*/
|
|
114
|
+
configPath?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Ignore files to use instead of cwd .gitignore/.prettierignore.
|
|
117
|
+
* Can be passed multiple times in CLI style.
|
|
118
|
+
*/
|
|
119
|
+
ignorePath?: string | string[];
|
|
120
|
+
/**
|
|
121
|
+
* Whether node_modules should be included.
|
|
122
|
+
*/
|
|
123
|
+
withNodeModules?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Disable nested config lookup.
|
|
126
|
+
*/
|
|
127
|
+
disableNestedConfig?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Whether to use in-memory cache.
|
|
130
|
+
*/
|
|
131
|
+
useCache?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Ignore resolution result.
|
|
135
|
+
*/
|
|
136
|
+
interface IsOxfmtIgnoredResult {
|
|
137
|
+
/**
|
|
138
|
+
* Whether the file should be ignored.
|
|
139
|
+
*/
|
|
140
|
+
ignored: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Matched ignore source.
|
|
143
|
+
*/
|
|
144
|
+
reason?: 'default-dir' | 'lockfile' | 'gitignore' | 'prettierignore' | 'ignore-path' | 'config-ignore-patterns';
|
|
145
|
+
}
|
|
80
146
|
/**
|
|
81
147
|
* @deprecated Use `OxfmtConfigOverride` instead
|
|
82
148
|
*/
|
|
83
149
|
type FormatOptionOverride = OxfmtConfigOverride;
|
|
84
150
|
//#endregion
|
|
85
|
-
//#region src/
|
|
151
|
+
//#region src/config-file.d.ts
|
|
86
152
|
/**
|
|
87
153
|
* Resolve the oxfmt config file path.
|
|
88
|
-
*
|
|
89
|
-
* -
|
|
154
|
+
*
|
|
155
|
+
* - If `configPath` is provided, absolute paths are returned as-is;
|
|
156
|
+
* relative paths are joined to `cwd`.
|
|
157
|
+
* - Otherwise, walk upward from `cwd` to find known filenames.
|
|
90
158
|
*
|
|
91
159
|
* @param cwd - Starting directory for resolution.
|
|
92
|
-
* @param configPath - Optional explicit path
|
|
93
|
-
* @returns Absolute path to
|
|
160
|
+
* @param configPath - Optional explicit path.
|
|
161
|
+
* @returns Absolute path to config file, or undefined when not found.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* import { resolveOxfmtrcPath } from 'load-oxfmt-config'
|
|
166
|
+
*
|
|
167
|
+
* const path = await resolveOxfmtrcPath(process.cwd())
|
|
168
|
+
* ```
|
|
94
169
|
*/
|
|
95
170
|
declare function resolveOxfmtrcPath(cwd: string, configPath?: string): Promise<string | undefined>;
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/core.d.ts
|
|
96
173
|
/**
|
|
97
|
-
*
|
|
98
|
-
* Caching is enabled by default; pass `useCache: false` to force a re-read.
|
|
174
|
+
* Resolve config + editorconfig and return merged config with metadata.
|
|
99
175
|
*
|
|
100
|
-
* @param options -
|
|
101
|
-
* @returns
|
|
102
|
-
* @throws {Error} when the config file exists but cannot be parsed.
|
|
176
|
+
* @param options - Loader settings.
|
|
177
|
+
* @returns Merged config and optional resolved config metadata.
|
|
103
178
|
*
|
|
104
179
|
* @example
|
|
105
180
|
* ```ts
|
|
106
|
-
*
|
|
181
|
+
* import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
182
|
+
*
|
|
183
|
+
* const result = await loadOxfmtConfigResult({ cwd: process.cwd() })
|
|
184
|
+
* console.log(result.config)
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
declare function loadOxfmtConfigResult(options?: Options): Promise<LoadOxfmtConfigResult>;
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/ignore.d.ts
|
|
190
|
+
/**
|
|
191
|
+
* Resolve whether a file should be ignored using oxfmt-like CLI semantics.
|
|
192
|
+
*
|
|
193
|
+
* @param options - Ignore resolution options.
|
|
194
|
+
* @returns Ignore status with optional reason.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { isOxfmtIgnored } from 'load-oxfmt-config'
|
|
199
|
+
*
|
|
200
|
+
* const result = await isOxfmtIgnored({
|
|
201
|
+
* cwd: process.cwd(),
|
|
202
|
+
* filepath: 'src/generated/file.ts',
|
|
203
|
+
* })
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
declare function isOxfmtIgnored(options: IsOxfmtIgnoredOptions): Promise<IsOxfmtIgnoredResult>;
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/legacy.d.ts
|
|
209
|
+
/**
|
|
210
|
+
* Legacy API that returns only the merged config object.
|
|
211
|
+
*
|
|
212
|
+
* Prefer using `loadOxfmtConfigResult` when you also need resolved config metadata.
|
|
213
|
+
*
|
|
214
|
+
* @deprecated Prefer `loadOxfmtConfigResult`.
|
|
215
|
+
*
|
|
216
|
+
* @param options - Loader options.
|
|
217
|
+
* @returns Parsed and merged oxfmt options.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
222
|
+
*
|
|
223
|
+
* // Deprecated: prefer loadOxfmtConfigResult.
|
|
224
|
+
* const config = await loadOxfmtConfig({ cwd: process.cwd() })
|
|
107
225
|
* ```
|
|
108
226
|
*/
|
|
109
227
|
declare function loadOxfmtConfig(options?: Options): Promise<OxfmtOptions>;
|
|
110
228
|
//#endregion
|
|
111
|
-
export { EditorconfigOption, FormatOptionOverride, Options, OxfmtConfigOverride, OxfmtOptions, loadOxfmtConfig, resolveOxfmtrcPath };
|
|
229
|
+
export { EditorconfigOption, FormatOptionOverride, IsOxfmtIgnoredOptions, IsOxfmtIgnoredResult, LoadOxfmtConfigResult, Options, OxfmtConfigOverride, OxfmtOptions, isOxfmtIgnored, loadOxfmtConfig, loadOxfmtConfigResult, resolveOxfmtrcPath };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
1
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
2
|
import process from "node:process";
|
|
4
3
|
import { interopDefault, isBoolean, isNumber, isObject } from "@ntnyq/utils";
|
|
5
|
-
import {
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
6
6
|
import { parseBuffer } from "editorconfig";
|
|
7
|
+
import ignore from "ignore";
|
|
8
|
+
import picomatch from "picomatch";
|
|
7
9
|
//#region src/constants.ts
|
|
8
10
|
/**
|
|
9
11
|
* Supported configuration files for oxfmt.
|
|
@@ -15,13 +17,178 @@ const OXFMT_CONFIG_FILES = [
|
|
|
15
17
|
"oxfmt.config.ts"
|
|
16
18
|
];
|
|
17
19
|
/**
|
|
20
|
+
* Supported extensions for explicit config paths.
|
|
21
|
+
*/
|
|
22
|
+
const OXFMT_EXPLICIT_CONFIG_EXTENSIONS = [
|
|
23
|
+
".json",
|
|
24
|
+
".jsonc",
|
|
25
|
+
".ts",
|
|
26
|
+
".mts",
|
|
27
|
+
".cts",
|
|
28
|
+
".js",
|
|
29
|
+
".mjs",
|
|
30
|
+
".cjs"
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Default ignored directories used by oxfmt CLI semantics.
|
|
34
|
+
*/
|
|
35
|
+
const DEFAULT_IGNORED_DIRS = [
|
|
36
|
+
".git",
|
|
37
|
+
".svn",
|
|
38
|
+
".jj",
|
|
39
|
+
"node_modules"
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Common lockfiles ignored by default.
|
|
43
|
+
*
|
|
44
|
+
* Note: oxfmt docs only list package-lock.json and pnpm-lock.yaml explicitly,
|
|
45
|
+
* then mention "etc.". This list mirrors common ecosystem lockfiles.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_IGNORED_LOCKFILES = [
|
|
48
|
+
"package-lock.json",
|
|
49
|
+
"npm-shrinkwrap.json",
|
|
50
|
+
"pnpm-lock.yaml",
|
|
51
|
+
"yarn.lock",
|
|
52
|
+
"bun.lock",
|
|
53
|
+
"bun.lockb"
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Default ignore files loaded from cwd when --ignore-path is not provided.
|
|
57
|
+
*/
|
|
58
|
+
const DEFAULT_IGNORE_FILES = [".gitignore", ".prettierignore"];
|
|
59
|
+
/**
|
|
18
60
|
* Supported EditorConfig filename.
|
|
19
61
|
*/
|
|
20
62
|
const EDITORCONFIG_FILE = ".editorconfig";
|
|
21
63
|
/**
|
|
22
64
|
* Sections that apply globally and can be merged into root-level oxfmt options.
|
|
23
65
|
*/
|
|
24
|
-
const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"
|
|
66
|
+
const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*"];
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/config-file.ts
|
|
69
|
+
/**
|
|
70
|
+
* Build a cache key for path resolution (cwd + configPath).
|
|
71
|
+
*
|
|
72
|
+
* @param cwd - Current working directory.
|
|
73
|
+
* @param configPath - Optional config path.
|
|
74
|
+
* @returns Cache key for resolve cache.
|
|
75
|
+
*/
|
|
76
|
+
function getResolveCacheKey(cwd, configPath) {
|
|
77
|
+
return `${cwd}::${configPath || ""}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build a cache key for config content; prefixes missing entries.
|
|
81
|
+
*
|
|
82
|
+
* @param resolvedPath - Resolved oxfmt config path.
|
|
83
|
+
* @param editorconfigPath - Resolved editorconfig path.
|
|
84
|
+
* @param resolveKey - Resolution cache key.
|
|
85
|
+
* @returns Cache key for config content cache.
|
|
86
|
+
*/
|
|
87
|
+
function getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey) {
|
|
88
|
+
return `${resolvedPath || `missing-oxfmt:${resolveKey}`}::${editorconfigPath || `missing-editorconfig:${resolveKey}`}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the oxfmt config file path.
|
|
92
|
+
*
|
|
93
|
+
* - If `configPath` is provided, absolute paths are returned as-is;
|
|
94
|
+
* relative paths are joined to `cwd`.
|
|
95
|
+
* - Otherwise, walk upward from `cwd` to find known filenames.
|
|
96
|
+
*
|
|
97
|
+
* @param cwd - Starting directory for resolution.
|
|
98
|
+
* @param configPath - Optional explicit path.
|
|
99
|
+
* @returns Absolute path to config file, or undefined when not found.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* import { resolveOxfmtrcPath } from 'load-oxfmt-config'
|
|
104
|
+
*
|
|
105
|
+
* const path = await resolveOxfmtrcPath(process.cwd())
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
async function resolveOxfmtrcPath(cwd, configPath) {
|
|
109
|
+
const resolvedCwd = resolve(cwd);
|
|
110
|
+
if (configPath) return isAbsolute(configPath) ? configPath : join(resolvedCwd, configPath);
|
|
111
|
+
let currentDir = resolvedCwd;
|
|
112
|
+
while (true) {
|
|
113
|
+
for (const filename of OXFMT_CONFIG_FILES) {
|
|
114
|
+
const configFilePath = join(currentDir, filename);
|
|
115
|
+
try {
|
|
116
|
+
if ((await stat(configFilePath)).isFile()) return configFilePath;
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
const parentDir = join(currentDir, "..");
|
|
120
|
+
if (parentDir === currentDir) break;
|
|
121
|
+
currentDir = parentDir;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Read and parse an oxfmt config file.
|
|
126
|
+
*
|
|
127
|
+
* @param resolvedPath - Absolute path to config file.
|
|
128
|
+
* @returns Parsed config object.
|
|
129
|
+
*/
|
|
130
|
+
async function readConfigFromFile(resolvedPath) {
|
|
131
|
+
const extension = extname(resolvedPath);
|
|
132
|
+
if (extension === ".ts" || extension === ".mts" || extension === ".cts" || extension === ".js" || extension === ".mjs" || extension === ".cjs") {
|
|
133
|
+
const mod = await (await interopDefault(import("jiti")))(import.meta.url).import(resolvedPath);
|
|
134
|
+
return mod["default"] ?? mod;
|
|
135
|
+
}
|
|
136
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
137
|
+
if (extension === ".jsonc") {
|
|
138
|
+
const parseErrors = [];
|
|
139
|
+
const parsed = parse(content, parseErrors);
|
|
140
|
+
if (parseErrors.length > 0) {
|
|
141
|
+
const firstError = parseErrors[0];
|
|
142
|
+
const errorCode = firstError === void 0 ? "Unknown" : printParseErrorCode(firstError.error);
|
|
143
|
+
throw new Error(`Invalid JSONC syntax: ${errorCode}`);
|
|
144
|
+
}
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
if (extension === ".json") return JSON.parse(content);
|
|
148
|
+
if (!extension) return JSON.parse(content);
|
|
149
|
+
if (!OXFMT_EXPLICIT_CONFIG_EXTENSIONS.includes(extension)) throw new Error(`Unsupported oxfmt config extension "${extension}" at ${resolvedPath}`);
|
|
150
|
+
return JSON.parse(content);
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/utils.ts
|
|
154
|
+
/**
|
|
155
|
+
* Return a cached promise by key, creating and storing it on miss.
|
|
156
|
+
*
|
|
157
|
+
* If the promise rejects, the cache entry is removed so future calls can retry.
|
|
158
|
+
*
|
|
159
|
+
* @param cache - Map used to store inflight/resolved promises.
|
|
160
|
+
* @param key - Cache key.
|
|
161
|
+
* @param factory - Factory to create the promise when missing.
|
|
162
|
+
* @returns Cached or newly created promise.
|
|
163
|
+
*/
|
|
164
|
+
function cachePromise(cache, key, factory) {
|
|
165
|
+
const cached = cache.get(key);
|
|
166
|
+
if (cached) return cached;
|
|
167
|
+
const task = factory().catch((error) => {
|
|
168
|
+
cache.delete(key);
|
|
169
|
+
throw error;
|
|
170
|
+
});
|
|
171
|
+
cache.set(key, task);
|
|
172
|
+
return task;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Normalize a filesystem path to POSIX-style separators.
|
|
176
|
+
*
|
|
177
|
+
* @param path - Original path.
|
|
178
|
+
* @returns Path using `/` as separator.
|
|
179
|
+
*/
|
|
180
|
+
function toPosixPath(path) {
|
|
181
|
+
return path.replaceAll("\\", "/");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Split a path into non-empty segments.
|
|
185
|
+
*
|
|
186
|
+
* @param path - Original path.
|
|
187
|
+
* @returns Path segments.
|
|
188
|
+
*/
|
|
189
|
+
function splitPathSegments(path) {
|
|
190
|
+
return path.split(/[\\/]+/u).filter(Boolean);
|
|
191
|
+
}
|
|
25
192
|
//#endregion
|
|
26
193
|
//#region src/editorconfig.ts
|
|
27
194
|
/**
|
|
@@ -89,15 +256,6 @@ async function resolveEditorconfigPath(startDir, onlyCwd = false) {
|
|
|
89
256
|
}
|
|
90
257
|
}
|
|
91
258
|
/**
|
|
92
|
-
* Normalizes a file path to POSIX separators for glob compatibility.
|
|
93
|
-
*
|
|
94
|
-
* @param path - The path to normalize.
|
|
95
|
-
* @returns The path with forward slashes.
|
|
96
|
-
*/
|
|
97
|
-
function toPosixPath(path) {
|
|
98
|
-
return path.replaceAll("\\", "/");
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
259
|
* Checks whether an EditorConfig section should be treated as a global section.
|
|
102
260
|
*
|
|
103
261
|
* @param sectionName - The section name parsed from .editorconfig.
|
|
@@ -139,12 +297,13 @@ function parseEditorconfigQuoteType(value) {
|
|
|
139
297
|
* Resolves the effective tab width from an EditorConfig section.
|
|
140
298
|
*
|
|
141
299
|
* @param section - The parsed EditorConfig section body.
|
|
300
|
+
* @param useTabs - The resolved indent style from the same section, when available.
|
|
142
301
|
* @returns The resolved tab width, or undefined when no numeric value is available.
|
|
143
302
|
*/
|
|
144
|
-
function parseEditorconfigTabWidth(section) {
|
|
303
|
+
function parseEditorconfigTabWidth(section, useTabs) {
|
|
145
304
|
const indentSize = section["indent_size"];
|
|
146
305
|
const tabWidth = section["tab_width"];
|
|
147
|
-
if (indentSize && indentSize !== "unset" && indentSize !== "tab") {
|
|
306
|
+
if (useTabs === false && indentSize && indentSize !== "unset" && indentSize !== "tab") {
|
|
148
307
|
const parsedIndentSize = Number(indentSize);
|
|
149
308
|
if (Number.isFinite(parsedIndentSize)) return parsedIndentSize;
|
|
150
309
|
}
|
|
@@ -167,7 +326,7 @@ function mapEditorconfigSectionToOptions(section) {
|
|
|
167
326
|
else if (section["indent_style"] === "space") options.useTabs = false;
|
|
168
327
|
const singleQuote = parseEditorconfigQuoteType(section["quote_type"]);
|
|
169
328
|
if (isBoolean(singleQuote)) options.singleQuote = singleQuote;
|
|
170
|
-
const tabWidth = parseEditorconfigTabWidth(section);
|
|
329
|
+
const tabWidth = parseEditorconfigTabWidth(section, options.useTabs);
|
|
171
330
|
if (isNumber(tabWidth)) options.tabWidth = tabWidth;
|
|
172
331
|
if (section["max_line_length"] && section["max_line_length"] !== "unset") {
|
|
173
332
|
const parsedPrintWidth = Number(section["max_line_length"]);
|
|
@@ -188,7 +347,7 @@ function mapEditorconfigSectionToOptions(section) {
|
|
|
188
347
|
function rebaseEditorconfigPattern(pattern, editorconfigDir, anchorDir) {
|
|
189
348
|
const relativePrefix = toPosixPath(relative(anchorDir, editorconfigDir));
|
|
190
349
|
if (!relativePrefix || relativePrefix === ".") return pattern;
|
|
191
|
-
return `${relativePrefix}/${pattern.replace(
|
|
350
|
+
return `${relativePrefix}/${pattern.replace(/^\//u, "")}`;
|
|
192
351
|
}
|
|
193
352
|
/**
|
|
194
353
|
* Reads a .editorconfig file and converts it into root options and override entries.
|
|
@@ -225,129 +384,230 @@ async function readEditorconfigFromFile(editorconfigPath, anchorDir) {
|
|
|
225
384
|
const resolveCache = /* @__PURE__ */ new Map();
|
|
226
385
|
const configCache = /* @__PURE__ */ new Map();
|
|
227
386
|
/**
|
|
228
|
-
*
|
|
387
|
+
* Resolve config + editorconfig and return merged config with metadata.
|
|
229
388
|
*
|
|
230
|
-
* @param
|
|
231
|
-
* @
|
|
232
|
-
*
|
|
233
|
-
* @
|
|
389
|
+
* @param options - Loader settings.
|
|
390
|
+
* @returns Merged config and optional resolved config metadata.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* import { loadOxfmtConfigResult } from 'load-oxfmt-config'
|
|
395
|
+
*
|
|
396
|
+
* const result = await loadOxfmtConfigResult({ cwd: process.cwd() })
|
|
397
|
+
* console.log(result.config)
|
|
398
|
+
* ```
|
|
234
399
|
*/
|
|
235
|
-
function
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
400
|
+
async function loadOxfmtConfigResult(options = {}) {
|
|
401
|
+
const useCache = options.useCache !== false;
|
|
402
|
+
const cwd = resolve(options.cwd || process.cwd());
|
|
403
|
+
const editorconfig = options.editorconfig ?? true;
|
|
404
|
+
const useEditorconfig = editorconfig !== false;
|
|
405
|
+
const isEditorconfigOptionsObject = useEditorconfig && isObject(editorconfig);
|
|
406
|
+
const onlyCwd = isEditorconfigOptionsObject ? editorconfig.onlyCwd ?? false : false;
|
|
407
|
+
const editorconfigCwd = isEditorconfigOptionsObject && editorconfig.cwd ? resolve(editorconfig.cwd) : void 0;
|
|
408
|
+
const resolveKey = getResolveCacheKey(cwd, options.configPath);
|
|
409
|
+
const editorconfigSearchDir = editorconfigCwd || getEditorconfigSearchDir(cwd, options.configPath);
|
|
410
|
+
const editorconfigResolveKey = editorconfigCwd ? getEditorconfigResolveCacheKey(`${editorconfigCwd}::${options.configPath || ""}`) : getEditorconfigResolveCacheKey(resolveKey);
|
|
411
|
+
const resolvedPath = useCache ? await cachePromise(resolveCache, resolveKey, () => resolveOxfmtrcPath(cwd, options.configPath)) : await resolveOxfmtrcPath(cwd, options.configPath);
|
|
412
|
+
const editorconfigPath = useEditorconfig ? await (useCache ? cachePromise(resolveCache, editorconfigResolveKey, () => resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : void 0;
|
|
413
|
+
const anchorDir = dirname(resolvedPath || editorconfigPath || cwd);
|
|
414
|
+
const loadTask = async () => {
|
|
415
|
+
const oxfmtConfig = resolvedPath ? await readConfigFromFile(resolvedPath).catch((error) => {
|
|
416
|
+
throw new Error(`Failed to parse oxfmt configuration file at ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
417
|
+
}) : {};
|
|
418
|
+
if (!editorconfigPath) return oxfmtConfig;
|
|
419
|
+
const editorconfigData = await readEditorconfigFromFile(editorconfigPath, anchorDir);
|
|
420
|
+
const mergedConfig = mergeRootOptions(oxfmtConfig, editorconfigData.rootOptions);
|
|
421
|
+
const mergedOverrides = mergeOverrides(oxfmtConfig.overrides, editorconfigData.overrides);
|
|
422
|
+
if (!mergedOverrides) return mergedConfig;
|
|
423
|
+
return {
|
|
424
|
+
...mergedConfig,
|
|
425
|
+
overrides: mergedOverrides
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
const hasNoConfigSources = !resolvedPath && !editorconfigPath;
|
|
429
|
+
return {
|
|
430
|
+
config: await (async () => {
|
|
431
|
+
if (hasNoConfigSources) return useCache ? await cachePromise(configCache, getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey), () => Promise.resolve({})) : {};
|
|
432
|
+
return useCache ? await cachePromise(configCache, getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey), loadTask) : await loadTask();
|
|
433
|
+
})(),
|
|
434
|
+
...resolvedPath ? {
|
|
435
|
+
filepath: resolvedPath,
|
|
436
|
+
dirname: dirname(resolvedPath)
|
|
437
|
+
} : {}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/ignore.ts
|
|
442
|
+
const ignoreMatcherCache = /* @__PURE__ */ new Map();
|
|
443
|
+
/**
|
|
444
|
+
* Check whether a file is under oxfmt's default ignored directories.
|
|
445
|
+
*
|
|
446
|
+
* @param filepath - Absolute file path to test.
|
|
447
|
+
* @param options - Matching options.
|
|
448
|
+
* @returns True when the path should be ignored by default directory rules.
|
|
449
|
+
*/
|
|
450
|
+
function isDefaultIgnoredDir(filepath, options) {
|
|
451
|
+
const directories = options.withNodeModules ? DEFAULT_IGNORED_DIRS.filter((dir) => dir !== "node_modules") : DEFAULT_IGNORED_DIRS;
|
|
452
|
+
const segments = splitPathSegments(dirname(filepath));
|
|
453
|
+
return directories.some((dir) => segments.includes(dir));
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Check whether a file is a default ignored lockfile.
|
|
457
|
+
*
|
|
458
|
+
* @param filepath - Absolute file path.
|
|
459
|
+
* @returns True when the basename matches a default lockfile name.
|
|
460
|
+
*/
|
|
461
|
+
function isLockfile(filepath) {
|
|
462
|
+
return DEFAULT_IGNORED_LOCKFILES.includes(basename(filepath));
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Read and parse an ignore file into an `ignore` matcher.
|
|
466
|
+
*
|
|
467
|
+
* @param ignoreFilePath - Ignore file path.
|
|
468
|
+
* @param useCache - Whether to cache matcher instances.
|
|
469
|
+
* @returns Parsed matcher, or undefined when file does not exist.
|
|
470
|
+
*/
|
|
471
|
+
function loadIgnoreMatcher(ignoreFilePath, useCache) {
|
|
472
|
+
const loadTask = () => readFile(ignoreFilePath, "utf8").then((content) => {
|
|
473
|
+
const ig = ignore();
|
|
474
|
+
ig.add(content);
|
|
475
|
+
return ig;
|
|
476
|
+
}).catch((error) => {
|
|
477
|
+
const code = error.code;
|
|
478
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
240
479
|
throw error;
|
|
241
480
|
});
|
|
242
|
-
|
|
243
|
-
return
|
|
481
|
+
if (!useCache) return loadTask();
|
|
482
|
+
return cachePromise(ignoreMatcherCache, ignoreFilePath, loadTask);
|
|
244
483
|
}
|
|
245
484
|
/**
|
|
246
|
-
* Build a
|
|
485
|
+
* Build a POSIX relative path.
|
|
247
486
|
*
|
|
248
|
-
* @param
|
|
249
|
-
* @param
|
|
250
|
-
* @
|
|
251
|
-
* @returns Cache key for config content.
|
|
487
|
+
* @param from - Base directory.
|
|
488
|
+
* @param to - Target path.
|
|
489
|
+
* @returns POSIX relative path.
|
|
252
490
|
*/
|
|
253
|
-
function
|
|
254
|
-
return
|
|
491
|
+
function relativeSafe(from, to) {
|
|
492
|
+
return toPosixPath(relative(from, to));
|
|
255
493
|
}
|
|
256
494
|
/**
|
|
257
|
-
*
|
|
258
|
-
* - If `configPath` is provided, absolute paths are returned as-is; relative paths are joined to cwd.
|
|
259
|
-
* - Otherwise, walk upward from cwd to find known filenames.
|
|
495
|
+
* Match file path against one ignore file.
|
|
260
496
|
*
|
|
261
|
-
* @param
|
|
262
|
-
* @param
|
|
263
|
-
* @
|
|
497
|
+
* @param filepath - Absolute file path.
|
|
498
|
+
* @param ignoreFilePath - Ignore file path.
|
|
499
|
+
* @param useCache - Whether to use matcher cache.
|
|
500
|
+
* @returns True when ignored by this file.
|
|
264
501
|
*/
|
|
265
|
-
async function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
if ((await stat(configFilePath)).isFile()) return configFilePath;
|
|
273
|
-
} catch {}
|
|
274
|
-
}
|
|
275
|
-
const parentDir = join(currentDir, "..");
|
|
276
|
-
if (parentDir === currentDir) break;
|
|
277
|
-
currentDir = parentDir;
|
|
278
|
-
}
|
|
502
|
+
async function matchIgnoreFile(filepath, ignoreFilePath, useCache) {
|
|
503
|
+
const matcher = await loadIgnoreMatcher(ignoreFilePath, useCache);
|
|
504
|
+
if (!matcher) return false;
|
|
505
|
+
const relativeToIgnore = relativeSafe(dirname(ignoreFilePath), filepath);
|
|
506
|
+
if (relativeToIgnore === ".." || relativeToIgnore.startsWith("../")) return false;
|
|
507
|
+
return matcher.ignores(relativeToIgnore);
|
|
279
508
|
}
|
|
280
509
|
/**
|
|
281
|
-
*
|
|
510
|
+
* Resolve an ignore file path against cwd when needed.
|
|
282
511
|
*
|
|
512
|
+
* @param path - Absolute or relative path.
|
|
283
513
|
* @param cwd - Current working directory.
|
|
284
|
-
* @
|
|
285
|
-
* @returns Cache key for resolve cache.
|
|
514
|
+
* @returns Absolute ignore file path.
|
|
286
515
|
*/
|
|
287
|
-
function
|
|
288
|
-
return
|
|
516
|
+
function resolveIgnoreFilePath(path, cwd) {
|
|
517
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
289
518
|
}
|
|
290
519
|
/**
|
|
291
|
-
*
|
|
520
|
+
* Match `ignorePatterns` from config with support for negated patterns.
|
|
292
521
|
*
|
|
293
|
-
* @param
|
|
294
|
-
* @
|
|
522
|
+
* @param filepath - Absolute file path.
|
|
523
|
+
* @param configDir - Resolved config directory.
|
|
524
|
+
* @param patterns - Config ignore patterns.
|
|
525
|
+
* @returns True when patterns mark the file as ignored.
|
|
295
526
|
*/
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
527
|
+
function matchConfigIgnorePatterns(filepath, configDir, patterns) {
|
|
528
|
+
const relativeFile = relativeSafe(configDir, filepath);
|
|
529
|
+
let ignored = false;
|
|
530
|
+
for (const rawPattern of patterns) {
|
|
531
|
+
if (!rawPattern) continue;
|
|
532
|
+
const isNegative = rawPattern.startsWith("!");
|
|
533
|
+
const pattern = isNegative ? rawPattern.slice(1) : rawPattern;
|
|
534
|
+
if (!pattern) continue;
|
|
535
|
+
if (picomatch(pattern, { dot: true })(relativeFile)) ignored = !isNegative;
|
|
300
536
|
}
|
|
301
|
-
|
|
302
|
-
if (resolvedPath.endsWith(".jsonc")) return parse(content);
|
|
303
|
-
return JSON.parse(content);
|
|
537
|
+
return ignored;
|
|
304
538
|
}
|
|
305
539
|
/**
|
|
306
|
-
*
|
|
307
|
-
* Caching is enabled by default; pass `useCache: false` to force a re-read.
|
|
540
|
+
* Resolve whether a file should be ignored using oxfmt-like CLI semantics.
|
|
308
541
|
*
|
|
309
|
-
* @param options -
|
|
310
|
-
* @returns
|
|
311
|
-
* @throws {Error} when the config file exists but cannot be parsed.
|
|
542
|
+
* @param options - Ignore resolution options.
|
|
543
|
+
* @returns Ignore status with optional reason.
|
|
312
544
|
*
|
|
313
545
|
* @example
|
|
314
546
|
* ```ts
|
|
315
|
-
*
|
|
547
|
+
* import { isOxfmtIgnored } from 'load-oxfmt-config'
|
|
548
|
+
*
|
|
549
|
+
* const result = await isOxfmtIgnored({
|
|
550
|
+
* cwd: process.cwd(),
|
|
551
|
+
* filepath: 'src/generated/file.ts',
|
|
552
|
+
* })
|
|
316
553
|
* ```
|
|
317
554
|
*/
|
|
318
|
-
async function
|
|
555
|
+
async function isOxfmtIgnored(options) {
|
|
319
556
|
const useCache = options.useCache !== false;
|
|
320
|
-
const cwd = options.cwd
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (!editorconfigPath) return oxfmtConfig;
|
|
336
|
-
const editorconfigData = await readEditorconfigFromFile(editorconfigPath, anchorDir);
|
|
337
|
-
const mergedConfig = mergeRootOptions(oxfmtConfig, editorconfigData.rootOptions);
|
|
338
|
-
const mergedOverrides = mergeOverrides(oxfmtConfig.overrides, editorconfigData.overrides);
|
|
339
|
-
if (!mergedOverrides) return mergedConfig;
|
|
340
|
-
return {
|
|
341
|
-
...mergedConfig,
|
|
342
|
-
overrides: mergedOverrides
|
|
557
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
558
|
+
const filepath = isAbsolute(options.filepath) ? resolve(options.filepath) : resolve(cwd, options.filepath);
|
|
559
|
+
if (isDefaultIgnoredDir(filepath, options.withNodeModules ? { withNodeModules: true } : {})) return {
|
|
560
|
+
ignored: true,
|
|
561
|
+
reason: "default-dir"
|
|
562
|
+
};
|
|
563
|
+
if (isLockfile(filepath)) return {
|
|
564
|
+
ignored: true,
|
|
565
|
+
reason: "lockfile"
|
|
566
|
+
};
|
|
567
|
+
const explicitIgnorePaths = (typeof options.ignorePath === "string" ? [options.ignorePath] : options.ignorePath)?.map((path) => resolveIgnoreFilePath(path, cwd));
|
|
568
|
+
if (explicitIgnorePaths && explicitIgnorePaths.length > 0) {
|
|
569
|
+
for (const ignoreFilePath of explicitIgnorePaths) if (await matchIgnoreFile(filepath, ignoreFilePath, useCache)) return {
|
|
570
|
+
ignored: true,
|
|
571
|
+
reason: "ignore-path"
|
|
343
572
|
};
|
|
573
|
+
} else for (const ignoreFile of DEFAULT_IGNORE_FILES) if (await matchIgnoreFile(filepath, resolve(cwd, ignoreFile), useCache)) return {
|
|
574
|
+
ignored: true,
|
|
575
|
+
reason: ignoreFile === ".gitignore" ? "gitignore" : "prettierignore"
|
|
344
576
|
};
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
577
|
+
const configResult = await loadOxfmtConfigResult({
|
|
578
|
+
cwd: options.configPath || options.disableNestedConfig ? cwd : dirname(filepath),
|
|
579
|
+
...options.configPath ? { configPath: options.configPath } : {},
|
|
580
|
+
editorconfig: false,
|
|
581
|
+
useCache
|
|
582
|
+
});
|
|
583
|
+
if (configResult.dirname && configResult.config.ignorePatterns && configResult.config.ignorePatterns.length > 0 && matchConfigIgnorePatterns(filepath, configResult.dirname, configResult.config.ignorePatterns)) return {
|
|
584
|
+
ignored: true,
|
|
585
|
+
reason: "config-ignore-patterns"
|
|
586
|
+
};
|
|
587
|
+
return { ignored: false };
|
|
588
|
+
}
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/legacy.ts
|
|
591
|
+
/**
|
|
592
|
+
* Legacy API that returns only the merged config object.
|
|
593
|
+
*
|
|
594
|
+
* Prefer using `loadOxfmtConfigResult` when you also need resolved config metadata.
|
|
595
|
+
*
|
|
596
|
+
* @deprecated Prefer `loadOxfmtConfigResult`.
|
|
597
|
+
*
|
|
598
|
+
* @param options - Loader options.
|
|
599
|
+
* @returns Parsed and merged oxfmt options.
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
604
|
+
*
|
|
605
|
+
* // Deprecated: prefer loadOxfmtConfigResult.
|
|
606
|
+
* const config = await loadOxfmtConfig({ cwd: process.cwd() })
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
async function loadOxfmtConfig(options = {}) {
|
|
610
|
+
return (await loadOxfmtConfigResult(options)).config;
|
|
351
611
|
}
|
|
352
612
|
//#endregion
|
|
353
|
-
export { loadOxfmtConfig, resolveOxfmtrcPath };
|
|
613
|
+
export { isOxfmtIgnored, loadOxfmtConfig, loadOxfmtConfigResult, resolveOxfmtrcPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "load-oxfmt-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Load oxfmt config files and merge supported .editorconfig options.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"editorconfig",
|
|
@@ -39,23 +39,26 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@ntnyq/utils": "^0.
|
|
42
|
+
"@ntnyq/utils": "^0.13.2",
|
|
43
43
|
"editorconfig": "^3.0.2",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
44
|
+
"ignore": "^7.0.5",
|
|
45
|
+
"jiti": "^2.7.0",
|
|
46
|
+
"jsonc-parser": "^3.3.1",
|
|
47
|
+
"picomatch": "^4.0.4"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@ntnyq/tsconfig": "^3.1.0",
|
|
49
51
|
"@types/node": "^25.6.0",
|
|
50
|
-
"@
|
|
52
|
+
"@types/picomatch": "^4.0.3",
|
|
53
|
+
"@typescript/native-preview": "^7.0.0-dev.20260506.1",
|
|
51
54
|
"bumpp": "^11.0.1",
|
|
52
55
|
"husky": "^9.1.7",
|
|
53
56
|
"nano-staged": "^1.0.2",
|
|
54
57
|
"npm-run-all2": "^8.0.4",
|
|
55
|
-
"oxfmt": "^0.
|
|
56
|
-
"oxlint": "^1.
|
|
57
|
-
"tsdown": "^0.21.
|
|
58
|
-
"vitest": "^4.1.
|
|
58
|
+
"oxfmt": "^0.48.0",
|
|
59
|
+
"oxlint": "^1.63.0",
|
|
60
|
+
"tsdown": "^0.21.10",
|
|
61
|
+
"vitest": "^4.1.5"
|
|
59
62
|
},
|
|
60
63
|
"peerDependencies": {
|
|
61
64
|
"oxfmt": ">=0.41.0"
|