load-oxfmt-config 0.1.1 → 0.2.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 +104 -8
- package/dist/index.d.mts +47 -16
- package/dist/index.mjs +193 -58
- package/package.json +29 -27
package/README.md
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/load-oxfmt-config)
|
|
6
6
|
[](https://github.com/ntnyq/load-oxfmt-config/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
-
> Load and resolve
|
|
8
|
+
> Load and resolve oxfmt configuration files and merge supported `.editorconfig` settings for [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html).
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
12
|
- 🔍 **Auto-discovery** - Automatically searches for config files in current and parent directories
|
|
13
|
-
- 📦 **Multiple formats** - Supports
|
|
13
|
+
- 📦 **Multiple formats** - Supports `.oxfmtrc.json`, `.oxfmtrc.jsonc`, and `oxfmt.config.ts`
|
|
14
|
+
- 🧩 **EditorConfig fallback** - Merges supported `.editorconfig` fields into the returned oxfmt config result
|
|
14
15
|
- ⚡ **Built-in caching** - Caches both file resolution and parsed configs for optimal performance
|
|
15
16
|
- 🎯 **TypeScript support** - Fully typed with comprehensive type definitions
|
|
16
17
|
- 🛠️ **Flexible API** - Support explicit config paths or automatic discovery
|
|
@@ -38,11 +39,29 @@ Load config from current directory or parent directories:
|
|
|
38
39
|
```ts
|
|
39
40
|
import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
40
41
|
|
|
41
|
-
// Automatically searches for
|
|
42
|
+
// Automatically searches for oxfmt config files and the nearest .editorconfig
|
|
42
43
|
const config = await loadOxfmtConfig()
|
|
43
44
|
console.log(config) // { printWidth: 80, ... }
|
|
44
45
|
```
|
|
45
46
|
|
|
47
|
+
### Merge With `.editorconfig`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
51
|
+
|
|
52
|
+
const config = await loadOxfmtConfig({ cwd: '/path/to/project' })
|
|
53
|
+
|
|
54
|
+
// Returns one merged static config object
|
|
55
|
+
console.log(config)
|
|
56
|
+
// {
|
|
57
|
+
// tabWidth: 2,
|
|
58
|
+
// printWidth: 100,
|
|
59
|
+
// overrides: [
|
|
60
|
+
// { files: ['src/**/*.ts'], options: { printWidth: 120 } }
|
|
61
|
+
// ]
|
|
62
|
+
// }
|
|
63
|
+
```
|
|
64
|
+
|
|
46
65
|
### Specify Working Directory
|
|
47
66
|
|
|
48
67
|
```ts
|
|
@@ -70,6 +89,28 @@ const config = await loadOxfmtConfig({
|
|
|
70
89
|
})
|
|
71
90
|
```
|
|
72
91
|
|
|
92
|
+
### Disable `.editorconfig`
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
96
|
+
|
|
97
|
+
// Skip .editorconfig reading entirely
|
|
98
|
+
const config = await loadOxfmtConfig({
|
|
99
|
+
editorconfig: false,
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Limit `.editorconfig` to `cwd`
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { loadOxfmtConfig } from 'load-oxfmt-config'
|
|
107
|
+
|
|
108
|
+
// Only look in the cwd directory itself, no upward traversal
|
|
109
|
+
const config = await loadOxfmtConfig({
|
|
110
|
+
editorconfig: { onlyCwd: true },
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
73
114
|
### Disable Caching
|
|
74
115
|
|
|
75
116
|
```ts
|
|
@@ -95,13 +136,13 @@ console.log(configPath) // '/path/to/.oxfmtrc.json' or undefined
|
|
|
95
136
|
|
|
96
137
|
### `loadOxfmtConfig(options?)`
|
|
97
138
|
|
|
98
|
-
Load and parse oxfmt configuration
|
|
139
|
+
Load and parse oxfmt configuration files, then merge supported `.editorconfig` fields into the returned result.
|
|
99
140
|
|
|
100
141
|
**Parameters:**
|
|
101
142
|
|
|
102
143
|
- `options` - Optional configuration object
|
|
103
144
|
|
|
104
|
-
**Returns:** `Promise<FormatOptions>` - Parsed oxfmt configuration object. Returns empty object `{}` if no config file is found.
|
|
145
|
+
**Returns:** `Promise<FormatOptions>` - Parsed and merged oxfmt configuration object. Returns empty object `{}` if no supported config file is found.
|
|
105
146
|
|
|
106
147
|
**Throws:** Error if config file exists but cannot be parsed.
|
|
107
148
|
|
|
@@ -151,6 +192,21 @@ Enable in-memory caching for both path resolution and parsed config contents. Wh
|
|
|
151
192
|
|
|
152
193
|
Set to `false` to force reload from disk on every call.
|
|
153
194
|
|
|
195
|
+
### `editorconfig`
|
|
196
|
+
|
|
197
|
+
- **Type:** `boolean | EditorconfigOption`
|
|
198
|
+
- **Default:** `true`
|
|
199
|
+
|
|
200
|
+
Control how `.editorconfig` files are read and merged:
|
|
201
|
+
|
|
202
|
+
- **`true`** — Read and merge the nearest `.editorconfig`, walking up from the config file's directory (or `cwd` when no config path is given).
|
|
203
|
+
- **`false`** — Disable `.editorconfig` reading entirely.
|
|
204
|
+
- **`EditorconfigOption`** — Enable with additional settings:
|
|
205
|
+
|
|
206
|
+
| Property | Type | Default | Description |
|
|
207
|
+
| --------- | --------- | ------- | --------------------------------------------------------------------------------- |
|
|
208
|
+
| `onlyCwd` | `boolean` | `false` | When `true`, only look for `.editorconfig` in `cwd` itself — no upward traversal. |
|
|
209
|
+
|
|
154
210
|
## Config File Discovery
|
|
155
211
|
|
|
156
212
|
When `configPath` is not provided, the loader automatically searches for config files:
|
|
@@ -159,10 +215,14 @@ When `configPath` is not provided, the loader automatically searches for config
|
|
|
159
215
|
2. **Supported filenames:**
|
|
160
216
|
- `.oxfmtrc.json`
|
|
161
217
|
- `.oxfmtrc.jsonc`
|
|
218
|
+
|
|
219
|
+
- `oxfmt.config.ts`
|
|
220
|
+
|
|
162
221
|
3. **Stops when:**
|
|
163
222
|
- A valid config file is found
|
|
164
223
|
- Reaches the filesystem root
|
|
165
|
-
4. **
|
|
224
|
+
4. **EditorConfig:** The nearest `.editorconfig` is also resolved and merged into the returned result
|
|
225
|
+
5. **Returns:** Empty object `{}` if no config file is found
|
|
166
226
|
|
|
167
227
|
## Supported Config Formats
|
|
168
228
|
|
|
@@ -191,10 +251,46 @@ JSON with comments support:
|
|
|
191
251
|
/* Code style preferences */
|
|
192
252
|
"useTabs": false,
|
|
193
253
|
"semi": true,
|
|
194
|
-
"singleQuote": true
|
|
254
|
+
"singleQuote": true,
|
|
195
255
|
}
|
|
196
256
|
```
|
|
197
257
|
|
|
258
|
+
### TypeScript (`oxfmt.config.ts`)
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
export default {
|
|
262
|
+
printWidth: 100,
|
|
263
|
+
tabWidth: 2,
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## `.editorconfig` Support
|
|
268
|
+
|
|
269
|
+
The loader reads the nearest `.editorconfig` file and maps the subset of fields that oxfmt supports:
|
|
270
|
+
|
|
271
|
+
- `end_of_line` → `endOfLine`
|
|
272
|
+
- `indent_style` → `useTabs`
|
|
273
|
+
- `indent_size` / `tab_width` → `tabWidth`
|
|
274
|
+
- `max_line_length` → `printWidth`
|
|
275
|
+
- `insert_final_newline` → `insertFinalNewline`
|
|
276
|
+
|
|
277
|
+
Glob sections such as `[*.ts]` are converted into returned `overrides` entries.
|
|
278
|
+
|
|
279
|
+
## Precedence
|
|
280
|
+
|
|
281
|
+
The merged result follows this order:
|
|
282
|
+
|
|
283
|
+
1. Root-level values from the nearest `.editorconfig`
|
|
284
|
+
2. Root-level values from `.oxfmtrc.json`, `.oxfmtrc.jsonc`, or `oxfmt.config.ts`
|
|
285
|
+
3. Overrides generated from `.editorconfig` sections
|
|
286
|
+
4. Overrides declared directly in the oxfmt config file
|
|
287
|
+
|
|
288
|
+
This means explicit oxfmt config values always win over `.editorconfig` fallback values.
|
|
289
|
+
|
|
290
|
+
## Limitations
|
|
291
|
+
|
|
292
|
+
`loadOxfmtConfig()` returns a static `OxfmtOptions` object. That means `.editorconfig` support is represented as a merged config shape, 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.
|
|
293
|
+
|
|
198
294
|
## Error Handling
|
|
199
295
|
|
|
200
296
|
```ts
|
|
@@ -218,7 +314,7 @@ The caching system maintains two separate caches:
|
|
|
218
314
|
**Cache keys are based on:**
|
|
219
315
|
|
|
220
316
|
- `cwd` + `configPath` for path resolution
|
|
221
|
-
- Resolved
|
|
317
|
+
- Resolved oxfmt path and resolved `.editorconfig` path for config content
|
|
222
318
|
|
|
223
319
|
**Cache invalidation:**
|
|
224
320
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FormatConfig } from "oxfmt";
|
|
2
2
|
|
|
3
3
|
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Object-form editorconfig option, enabling fine-grained control.
|
|
6
|
+
*/
|
|
7
|
+
interface EditorconfigOption {
|
|
8
|
+
/**
|
|
9
|
+
* When `true`, only look for `.editorconfig` in the `cwd` directory itself
|
|
10
|
+
* (no upward traversal).
|
|
11
|
+
*
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
onlyCwd?: boolean;
|
|
15
|
+
}
|
|
4
16
|
/**
|
|
5
17
|
* Format option override for a single matching rule
|
|
6
18
|
*/
|
|
7
|
-
interface
|
|
19
|
+
interface OxfmtConfigOverride {
|
|
8
20
|
/**
|
|
9
21
|
* Glob patterns to match files
|
|
10
22
|
*/
|
|
@@ -16,7 +28,7 @@ interface FormatOptionOverride {
|
|
|
16
28
|
/**
|
|
17
29
|
* Format options to apply
|
|
18
30
|
*/
|
|
19
|
-
options?:
|
|
31
|
+
options?: FormatConfig;
|
|
20
32
|
}
|
|
21
33
|
interface Options {
|
|
22
34
|
/**
|
|
@@ -31,18 +43,47 @@ interface Options {
|
|
|
31
43
|
* Whether to use cache
|
|
32
44
|
*/
|
|
33
45
|
useCache?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Control `.editorconfig` reading.
|
|
48
|
+
* - `true` (default): read and merge `.editorconfig`, walking up from the config
|
|
49
|
+
* file's directory (or `cwd` when no config path is given).
|
|
50
|
+
* - `false`: disable `.editorconfig` reading entirely.
|
|
51
|
+
* - `EditorconfigOption`: enable with additional settings (e.g. `onlyCwd`).
|
|
52
|
+
*
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
editorconfig?: boolean | EditorconfigOption;
|
|
34
56
|
}
|
|
35
57
|
/**
|
|
36
58
|
* Final oxfmt options (including overrides)
|
|
37
59
|
*/
|
|
38
|
-
interface OxfmtOptions extends
|
|
60
|
+
interface OxfmtOptions extends FormatConfig {
|
|
61
|
+
/**
|
|
62
|
+
* Ignore files matching these glob patterns
|
|
63
|
+
* Patterns are based on the location of the Oxfmt configuration file
|
|
64
|
+
*/
|
|
65
|
+
ignorePatterns?: string[];
|
|
39
66
|
/**
|
|
40
67
|
* Array of format option overrides
|
|
41
68
|
*/
|
|
42
|
-
overrides?:
|
|
69
|
+
overrides?: OxfmtConfigOverride[];
|
|
43
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* @deprecated Use `OxfmtConfigOverride` instead
|
|
73
|
+
*/
|
|
74
|
+
type FormatOptionOverride = OxfmtConfigOverride;
|
|
44
75
|
//#endregion
|
|
45
76
|
//#region src/core.d.ts
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the oxfmt config file path.
|
|
79
|
+
* - If `configPath` is provided, absolute paths are returned as-is; relative paths are joined to cwd.
|
|
80
|
+
* - Otherwise, walk upward from cwd to find known filenames.
|
|
81
|
+
*
|
|
82
|
+
* @param cwd - Starting directory for resolution.
|
|
83
|
+
* @param configPath - Optional explicit path (absolute or relative to cwd).
|
|
84
|
+
* @returns Absolute path to the config file, or undefined when not found.
|
|
85
|
+
*/
|
|
86
|
+
declare function resolveOxfmtrcPath(cwd: string, configPath?: string): Promise<string | undefined>;
|
|
46
87
|
/**
|
|
47
88
|
* Load oxfmt configuration: resolve the file path, then read and parse it.
|
|
48
89
|
* Caching is enabled by default; pass `useCache: false` to force a re-read.
|
|
@@ -57,15 +98,5 @@ interface OxfmtOptions extends FormatOptions {
|
|
|
57
98
|
* ```
|
|
58
99
|
*/
|
|
59
100
|
declare function loadOxfmtConfig(options?: Options): Promise<OxfmtOptions>;
|
|
60
|
-
/**
|
|
61
|
-
* Resolve the oxfmt config file path.
|
|
62
|
-
* - If `configPath` is provided, absolute paths are returned as-is; relative paths are joined to cwd.
|
|
63
|
-
* - Otherwise, walk upward from cwd to find known filenames.
|
|
64
|
-
*
|
|
65
|
-
* @param cwd - Starting directory for resolution.
|
|
66
|
-
* @param configPath - Optional explicit path (absolute or relative to cwd).
|
|
67
|
-
* @returns Absolute path to the config file, or undefined when not found.
|
|
68
|
-
*/
|
|
69
|
-
declare function resolveOxfmtrcPath(cwd: string, configPath?: string): Promise<string | undefined>;
|
|
70
101
|
//#endregion
|
|
71
|
-
export { FormatOptionOverride, Options, OxfmtOptions, loadOxfmtConfig, resolveOxfmtrcPath };
|
|
102
|
+
export { EditorconfigOption, FormatOptionOverride, Options, OxfmtConfigOverride, OxfmtOptions, loadOxfmtConfig, resolveOxfmtrcPath };
|
package/dist/index.mjs
CHANGED
|
@@ -1,71 +1,132 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { interopDefault } from "@ntnyq/utils";
|
|
4
5
|
import { parse } from "jsonc-parser";
|
|
5
|
-
|
|
6
|
+
import { parseBuffer } from "editorconfig";
|
|
6
7
|
//#region src/constants.ts
|
|
7
8
|
/**
|
|
8
9
|
* Supported configuration files for oxfmt.
|
|
9
10
|
* @see https://oxc.rs/docs/guide/usage/formatter/config.html#oxfmtrc-json-c
|
|
10
11
|
*/
|
|
11
|
-
const OXFMT_CONFIG_FILES = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const configCache = /* @__PURE__ */ new Map();
|
|
12
|
+
const OXFMT_CONFIG_FILES = [
|
|
13
|
+
".oxfmtrc.json",
|
|
14
|
+
".oxfmtrc.jsonc",
|
|
15
|
+
"oxfmt.config.ts"
|
|
16
|
+
];
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* Caching is enabled by default; pass `useCache: false` to force a re-read.
|
|
20
|
-
*
|
|
21
|
-
* @param options - Optional loader settings (cwd, configPath, useCache).
|
|
22
|
-
* @returns Parsed oxfmt OxfmtOptions or an empty object when missing.
|
|
23
|
-
* @throws {Error} when the config file exists but cannot be parsed.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* const config = await loadOxfmtConfig({ cwd: '/project' })
|
|
28
|
-
* ```
|
|
18
|
+
* Supported EditorConfig filename.
|
|
29
19
|
*/
|
|
30
|
-
|
|
31
|
-
const useCache = options.useCache !== false;
|
|
32
|
-
const cwd = options.cwd || process.cwd();
|
|
33
|
-
const resolveKey = getResolveCacheKey(cwd, options.configPath);
|
|
34
|
-
const resolvedPath = useCache ? await cachePromise(resolveCache, resolveKey, () => resolveOxfmtrcPath(cwd, options.configPath)) : await resolveOxfmtrcPath(cwd, options.configPath);
|
|
35
|
-
if (!resolvedPath) {
|
|
36
|
-
if (!useCache) return {};
|
|
37
|
-
return cachePromise(configCache, getConfigCacheKey(resolvedPath, resolveKey), () => Promise.resolve({}));
|
|
38
|
-
}
|
|
39
|
-
const loadTask = () => readConfigFromFile(resolvedPath).catch((err) => {
|
|
40
|
-
throw new Error(`Failed to parse oxfmt configuration file at ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
41
|
-
});
|
|
42
|
-
if (!useCache) return loadTask();
|
|
43
|
-
return cachePromise(configCache, getConfigCacheKey(resolvedPath, resolveKey), loadTask);
|
|
44
|
-
}
|
|
20
|
+
const EDITORCONFIG_FILE = ".editorconfig";
|
|
45
21
|
/**
|
|
46
|
-
*
|
|
47
|
-
* - If `configPath` is provided, absolute paths are returned as-is; relative paths are joined to cwd.
|
|
48
|
-
* - Otherwise, walk upward from cwd to find known filenames.
|
|
49
|
-
*
|
|
50
|
-
* @param cwd - Starting directory for resolution.
|
|
51
|
-
* @param configPath - Optional explicit path (absolute or relative to cwd).
|
|
52
|
-
* @returns Absolute path to the config file, or undefined when not found.
|
|
22
|
+
* Sections that apply globally and can be merged into root-level oxfmt options.
|
|
53
23
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
24
|
+
const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*", "**"];
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/editorconfig.ts
|
|
27
|
+
function getEditorconfigResolveCacheKey(resolveKey) {
|
|
28
|
+
return `editorconfig::${resolveKey}`;
|
|
29
|
+
}
|
|
30
|
+
function getEditorconfigSearchDir(cwd, configPath) {
|
|
31
|
+
if (!configPath) return cwd;
|
|
32
|
+
return dirname(isAbsolute(configPath) ? configPath : join(cwd, configPath));
|
|
33
|
+
}
|
|
34
|
+
function mergeRootOptions(oxfmtConfig, editorconfigRootOptions) {
|
|
35
|
+
return {
|
|
36
|
+
...editorconfigRootOptions,
|
|
37
|
+
...oxfmtConfig
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function mergeOverrides(oxfmtOverrides, editorconfigOverrides) {
|
|
41
|
+
const mergedOverrides = [...editorconfigOverrides, ...oxfmtOverrides || []];
|
|
42
|
+
return mergedOverrides.length > 0 ? mergedOverrides : void 0;
|
|
43
|
+
}
|
|
44
|
+
async function resolveEditorconfigPath(startDir, onlyCwd = false) {
|
|
45
|
+
let currentDir = startDir;
|
|
57
46
|
while (true) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
47
|
+
const editorconfigPath = join(currentDir, EDITORCONFIG_FILE);
|
|
48
|
+
try {
|
|
49
|
+
if ((await stat(editorconfigPath)).isFile()) return editorconfigPath;
|
|
50
|
+
} catch {}
|
|
51
|
+
if (onlyCwd) break;
|
|
64
52
|
const parentDir = join(currentDir, "..");
|
|
65
53
|
if (parentDir === currentDir) break;
|
|
66
54
|
currentDir = parentDir;
|
|
67
55
|
}
|
|
68
56
|
}
|
|
57
|
+
function toPosixPath(path) {
|
|
58
|
+
return path.replaceAll("\\", "/");
|
|
59
|
+
}
|
|
60
|
+
function isEditorconfigGlobalSection(sectionName) {
|
|
61
|
+
return Boolean(sectionName && EDITORCONFIG_GLOBAL_SECTION_NAMES.includes(sectionName));
|
|
62
|
+
}
|
|
63
|
+
function parseEditorconfigBoolean(value) {
|
|
64
|
+
if (value === "true") return true;
|
|
65
|
+
if (value === "false") return false;
|
|
66
|
+
}
|
|
67
|
+
function parseEditorconfigEndOfLine(value) {
|
|
68
|
+
if (value === "lf" || value === "crlf" || value === "cr") return value;
|
|
69
|
+
}
|
|
70
|
+
function parseEditorconfigTabWidth(section) {
|
|
71
|
+
const indentSize = section["indent_size"];
|
|
72
|
+
const tabWidth = section["tab_width"];
|
|
73
|
+
if (indentSize && indentSize !== "unset" && indentSize !== "tab") {
|
|
74
|
+
const parsedIndentSize = Number(indentSize);
|
|
75
|
+
if (Number.isFinite(parsedIndentSize)) return parsedIndentSize;
|
|
76
|
+
}
|
|
77
|
+
if (tabWidth && tabWidth !== "unset") {
|
|
78
|
+
const parsedTabWidth = Number(tabWidth);
|
|
79
|
+
if (Number.isFinite(parsedTabWidth)) return parsedTabWidth;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function mapEditorconfigSectionToOptions(section) {
|
|
83
|
+
const options = {};
|
|
84
|
+
const endOfLine = parseEditorconfigEndOfLine(section["end_of_line"]);
|
|
85
|
+
if (endOfLine) options.endOfLine = endOfLine;
|
|
86
|
+
if (section["indent_style"] === "tab") options.useTabs = true;
|
|
87
|
+
else if (section["indent_style"] === "space") options.useTabs = false;
|
|
88
|
+
const tabWidth = parseEditorconfigTabWidth(section);
|
|
89
|
+
if (typeof tabWidth === "number") options.tabWidth = tabWidth;
|
|
90
|
+
if (section["max_line_length"] && section["max_line_length"] !== "unset") {
|
|
91
|
+
const parsedPrintWidth = Number(section["max_line_length"]);
|
|
92
|
+
if (Number.isFinite(parsedPrintWidth)) options.printWidth = parsedPrintWidth;
|
|
93
|
+
}
|
|
94
|
+
const insertFinalNewline = parseEditorconfigBoolean(section["insert_final_newline"]);
|
|
95
|
+
if (typeof insertFinalNewline === "boolean") options.insertFinalNewline = insertFinalNewline;
|
|
96
|
+
return options;
|
|
97
|
+
}
|
|
98
|
+
function rebaseEditorconfigPattern(pattern, editorconfigDir, anchorDir) {
|
|
99
|
+
const relativePrefix = toPosixPath(relative(anchorDir, editorconfigDir));
|
|
100
|
+
if (!relativePrefix || relativePrefix === ".") return pattern;
|
|
101
|
+
return `${relativePrefix}/${pattern.replace(/^\//, "")}`;
|
|
102
|
+
}
|
|
103
|
+
async function readEditorconfigFromFile(editorconfigPath, anchorDir) {
|
|
104
|
+
const parsedSections = parseBuffer(await readFile(editorconfigPath));
|
|
105
|
+
const editorconfigDir = dirname(editorconfigPath);
|
|
106
|
+
const rootOptions = {};
|
|
107
|
+
const overrides = [];
|
|
108
|
+
for (const [sectionName, sectionBody] of parsedSections) {
|
|
109
|
+
if (!sectionName) continue;
|
|
110
|
+
const mappedOptions = mapEditorconfigSectionToOptions(sectionBody);
|
|
111
|
+
if (Object.keys(mappedOptions).length === 0) continue;
|
|
112
|
+
if (isEditorconfigGlobalSection(sectionName)) {
|
|
113
|
+
Object.assign(rootOptions, mappedOptions);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
overrides.push({
|
|
117
|
+
files: [rebaseEditorconfigPattern(sectionName, editorconfigDir, anchorDir)],
|
|
118
|
+
options: mappedOptions
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
overrides,
|
|
123
|
+
rootOptions
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/core.ts
|
|
128
|
+
const resolveCache = /* @__PURE__ */ new Map();
|
|
129
|
+
const configCache = /* @__PURE__ */ new Map();
|
|
69
130
|
/**
|
|
70
131
|
* Return a cached promise by key, creating and storing it on miss; failures clear the entry.
|
|
71
132
|
*
|
|
@@ -77,9 +138,9 @@ async function resolveOxfmtrcPath(cwd, configPath) {
|
|
|
77
138
|
function cachePromise(cache, key, factory) {
|
|
78
139
|
const cached = cache.get(key);
|
|
79
140
|
if (cached) return cached;
|
|
80
|
-
const task = factory().catch((
|
|
141
|
+
const task = factory().catch((error) => {
|
|
81
142
|
cache.delete(key);
|
|
82
|
-
throw
|
|
143
|
+
throw error;
|
|
83
144
|
});
|
|
84
145
|
cache.set(key, task);
|
|
85
146
|
return task;
|
|
@@ -88,11 +149,36 @@ function cachePromise(cache, key, factory) {
|
|
|
88
149
|
* Build a cache key for config content; prefixes missing entries with `missing:`.
|
|
89
150
|
*
|
|
90
151
|
* @param resolvedPath - Resolved config path or undefined when missing.
|
|
152
|
+
* @param editorconfigPath - Resolved editorconfig path or undefined when missing.
|
|
91
153
|
* @param resolveKey - Key used for path resolution caching.
|
|
92
154
|
* @returns Cache key for config content.
|
|
93
155
|
*/
|
|
94
|
-
function getConfigCacheKey(resolvedPath, resolveKey) {
|
|
95
|
-
return resolvedPath || `missing:${resolveKey}`;
|
|
156
|
+
function getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey) {
|
|
157
|
+
return `${resolvedPath || `missing-oxfmt:${resolveKey}`}::${editorconfigPath || `missing-editorconfig:${resolveKey}`}`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the oxfmt config file path.
|
|
161
|
+
* - If `configPath` is provided, absolute paths are returned as-is; relative paths are joined to cwd.
|
|
162
|
+
* - Otherwise, walk upward from cwd to find known filenames.
|
|
163
|
+
*
|
|
164
|
+
* @param cwd - Starting directory for resolution.
|
|
165
|
+
* @param configPath - Optional explicit path (absolute or relative to cwd).
|
|
166
|
+
* @returns Absolute path to the config file, or undefined when not found.
|
|
167
|
+
*/
|
|
168
|
+
async function resolveOxfmtrcPath(cwd, configPath) {
|
|
169
|
+
if (configPath) return isAbsolute(configPath) ? configPath : join(cwd, configPath);
|
|
170
|
+
let currentDir = cwd;
|
|
171
|
+
while (true) {
|
|
172
|
+
for (const filename of OXFMT_CONFIG_FILES) {
|
|
173
|
+
const configFilePath = join(currentDir, filename);
|
|
174
|
+
try {
|
|
175
|
+
if ((await stat(configFilePath)).isFile()) return configFilePath;
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
const parentDir = join(currentDir, "..");
|
|
179
|
+
if (parentDir === currentDir) break;
|
|
180
|
+
currentDir = parentDir;
|
|
181
|
+
}
|
|
96
182
|
}
|
|
97
183
|
/**
|
|
98
184
|
* Build a cache key for path resolution (cwd + configPath).
|
|
@@ -105,16 +191,65 @@ function getResolveCacheKey(cwd, configPath) {
|
|
|
105
191
|
return `${cwd}::${configPath || ""}`;
|
|
106
192
|
}
|
|
107
193
|
/**
|
|
108
|
-
* Read and parse config file, supporting JSON and
|
|
194
|
+
* Read and parse config file, supporting JSON, JSONC, and TypeScript/JavaScript.
|
|
109
195
|
*
|
|
110
196
|
* @param resolvedPath - Absolute path to the config file.
|
|
111
197
|
* @returns Parsed OxfmtOptions object.
|
|
112
198
|
*/
|
|
113
199
|
async function readConfigFromFile(resolvedPath) {
|
|
114
|
-
|
|
200
|
+
if (resolvedPath.endsWith(".ts")) {
|
|
201
|
+
const mod = await (await interopDefault(import("jiti")))(import.meta.url).import(resolvedPath);
|
|
202
|
+
return mod["default"] ?? mod;
|
|
203
|
+
}
|
|
204
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
115
205
|
if (resolvedPath.endsWith(".jsonc")) return parse(content);
|
|
116
206
|
return JSON.parse(content);
|
|
117
207
|
}
|
|
118
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Load oxfmt configuration: resolve the file path, then read and parse it.
|
|
210
|
+
* Caching is enabled by default; pass `useCache: false` to force a re-read.
|
|
211
|
+
*
|
|
212
|
+
* @param options - Optional loader settings (cwd, configPath, useCache).
|
|
213
|
+
* @returns Parsed oxfmt OxfmtOptions or an empty object when missing.
|
|
214
|
+
* @throws {Error} when the config file exists but cannot be parsed.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const config = await loadOxfmtConfig({ cwd: '/project' })
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
async function loadOxfmtConfig(options = {}) {
|
|
222
|
+
const useCache = options.useCache !== false;
|
|
223
|
+
const cwd = options.cwd || process.cwd();
|
|
224
|
+
const editorconfig = options.editorconfig ?? true;
|
|
225
|
+
const useEditorconfig = editorconfig !== false;
|
|
226
|
+
const onlyCwd = useEditorconfig && typeof editorconfig === "object" ? editorconfig.onlyCwd ?? false : false;
|
|
227
|
+
const resolveKey = getResolveCacheKey(cwd, options.configPath);
|
|
228
|
+
const editorconfigSearchDir = getEditorconfigSearchDir(cwd, options.configPath);
|
|
229
|
+
const editorconfigResolveKey = getEditorconfigResolveCacheKey(resolveKey);
|
|
230
|
+
const resolvedPath = useCache ? await cachePromise(resolveCache, resolveKey, () => resolveOxfmtrcPath(cwd, options.configPath)) : await resolveOxfmtrcPath(cwd, options.configPath);
|
|
231
|
+
const editorconfigPath = useEditorconfig ? await (useCache ? cachePromise(resolveCache, editorconfigResolveKey, () => resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : void 0;
|
|
232
|
+
const anchorDir = dirname(resolvedPath || editorconfigPath || cwd);
|
|
233
|
+
const loadTask = async () => {
|
|
234
|
+
const oxfmtConfig = resolvedPath ? await readConfigFromFile(resolvedPath).catch((error) => {
|
|
235
|
+
throw new Error(`Failed to parse oxfmt configuration file at ${resolvedPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
236
|
+
}) : {};
|
|
237
|
+
if (!editorconfigPath) return oxfmtConfig;
|
|
238
|
+
const editorconfigData = await readEditorconfigFromFile(editorconfigPath, anchorDir);
|
|
239
|
+
const mergedConfig = mergeRootOptions(oxfmtConfig, editorconfigData.rootOptions);
|
|
240
|
+
const mergedOverrides = mergeOverrides(oxfmtConfig.overrides, editorconfigData.overrides);
|
|
241
|
+
if (!mergedOverrides) return mergedConfig;
|
|
242
|
+
return {
|
|
243
|
+
...mergedConfig,
|
|
244
|
+
overrides: mergedOverrides
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
if (!resolvedPath && !editorconfigPath) {
|
|
248
|
+
if (!useCache) return {};
|
|
249
|
+
return cachePromise(configCache, getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey), () => Promise.resolve({}));
|
|
250
|
+
}
|
|
251
|
+
if (!useCache) return loadTask();
|
|
252
|
+
return cachePromise(configCache, getConfigCacheKey(resolvedPath, editorconfigPath, resolveKey), loadTask);
|
|
253
|
+
}
|
|
119
254
|
//#endregion
|
|
120
|
-
export { loadOxfmtConfig, resolveOxfmtrcPath };
|
|
255
|
+
export { loadOxfmtConfig, resolveOxfmtrcPath };
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "load-oxfmt-config",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "Load .oxfmtrc.json(c) for oxfmt.",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Load oxfmt config files and merge supported .editorconfig options.",
|
|
6
5
|
"keywords": [
|
|
6
|
+
"editorconfig",
|
|
7
7
|
"jsonc-parser",
|
|
8
8
|
"load-config",
|
|
9
9
|
"load-oxfmt-config",
|
|
@@ -11,16 +11,23 @@
|
|
|
11
11
|
"oxfmt",
|
|
12
12
|
"oxfmtrc"
|
|
13
13
|
],
|
|
14
|
+
"homepage": "https://github.com/ntnyq/load-oxfmt-config#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ntnyq/load-oxfmt-config/issues"
|
|
17
|
+
},
|
|
14
18
|
"license": "MIT",
|
|
15
19
|
"author": {
|
|
16
20
|
"name": "ntnyq",
|
|
17
21
|
"email": "ntnyq13@gmail.com"
|
|
18
22
|
},
|
|
19
|
-
"homepage": "https://github.com/ntnyq/load-oxfmt-config#readme",
|
|
20
23
|
"repository": "ntnyq/load-oxfmt-config",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./dist/index.mjs",
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
24
31
|
"exports": {
|
|
25
32
|
"./package.json": "./package.json",
|
|
26
33
|
".": {
|
|
@@ -28,46 +35,41 @@
|
|
|
28
35
|
"default": "./dist/index.mjs"
|
|
29
36
|
}
|
|
30
37
|
},
|
|
31
|
-
"main": "./dist/index.mjs",
|
|
32
|
-
"types": "./dist/index.d.mts",
|
|
33
|
-
"files": [
|
|
34
|
-
"dist"
|
|
35
|
-
],
|
|
36
38
|
"publishConfig": {
|
|
37
39
|
"access": "public"
|
|
38
40
|
},
|
|
39
|
-
"sideEffects": false,
|
|
40
|
-
"peerDependencies": {
|
|
41
|
-
"oxfmt": "0.x"
|
|
42
|
-
},
|
|
43
41
|
"dependencies": {
|
|
42
|
+
"@ntnyq/utils": "^0.11.2",
|
|
43
|
+
"editorconfig": "^3.0.2",
|
|
44
|
+
"jiti": "^2.6.1",
|
|
44
45
|
"jsonc-parser": "^3.3.1"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@ntnyq/eslint-config": "^6.0.0-beta.7",
|
|
48
48
|
"@ntnyq/tsconfig": "^3.1.0",
|
|
49
|
-
"@types/node": "^25.
|
|
50
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
51
|
-
"bumpp": "^
|
|
52
|
-
"eslint": "^10.0.0",
|
|
49
|
+
"@types/node": "^25.5.0",
|
|
50
|
+
"@typescript/native-preview": "^7.0.0-dev.20260326.1",
|
|
51
|
+
"bumpp": "^11.0.1",
|
|
53
52
|
"husky": "^9.1.7",
|
|
54
53
|
"nano-staged": "^0.9.0",
|
|
55
54
|
"npm-run-all2": "^8.0.4",
|
|
56
|
-
"oxfmt": "^0.
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"vitest": "^4.
|
|
55
|
+
"oxfmt": "^0.42.0",
|
|
56
|
+
"oxlint": "^1.57.0",
|
|
57
|
+
"tsdown": "^0.21.5",
|
|
58
|
+
"vitest": "^4.1.2"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"oxfmt": ">=0.41.0"
|
|
60
62
|
},
|
|
61
63
|
"nano-staged": {
|
|
62
|
-
"*.{js,ts,mjs,
|
|
64
|
+
"*.{js,ts,mjs,tsx}": "oxlint --fix",
|
|
63
65
|
"*": "oxfmt --no-error-on-unmatched-pattern"
|
|
64
66
|
},
|
|
65
67
|
"scripts": {
|
|
66
68
|
"build": "tsdown",
|
|
67
69
|
"dev": "tsdown --watch",
|
|
68
|
-
"lint": "eslint",
|
|
69
70
|
"format": "oxfmt",
|
|
70
71
|
"format:check": "oxfmt --check",
|
|
72
|
+
"lint": "oxlint",
|
|
71
73
|
"release": "run-s release:check release:version",
|
|
72
74
|
"release:check": "run-s format:check lint typecheck test",
|
|
73
75
|
"release:version": "bumpp",
|