load-oxfmt-config 0.3.0 → 0.4.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 CHANGED
@@ -111,6 +111,19 @@ const config = await loadOxfmtConfig({
111
111
  })
112
112
  ```
113
113
 
114
+ ### Override `.editorconfig` Search Directory
115
+
116
+ ```ts
117
+ import { loadOxfmtConfig } from 'load-oxfmt-config'
118
+
119
+ // Search .editorconfig from a custom directory instead of the config file's directory
120
+ const config = await loadOxfmtConfig({
121
+ editorconfig: {
122
+ cwd: '/path/to/editorconfig-dir',
123
+ },
124
+ })
125
+ ```
126
+
114
127
  ### Disable Caching
115
128
 
116
129
  ```ts
@@ -203,9 +216,10 @@ Control how `.editorconfig` files are read and merged:
203
216
  - **`false`** — Disable `.editorconfig` reading entirely.
204
217
  - **`EditorconfigOption`** — Enable with additional settings:
205
218
 
206
- | Property | Type | Default | Description |
207
- | --------- | --------- | ------- | --------------------------------------------------------------------------------- |
208
- | `onlyCwd` | `boolean` | `false` | When `true`, only look for `.editorconfig` in `cwd` itself — no upward traversal. |
219
+ | Property | Type | Default | Description |
220
+ | --------- | --------- | ----------- | --------------------------------------------------------------------------------------------------------------------- |
221
+ | `onlyCwd` | `boolean` | `false` | When `true`, only look for `.editorconfig` in `cwd` itself — no upward traversal. |
222
+ | `cwd` | `string` | `undefined` | Override the directory from which `.editorconfig` resolution starts, instead of the config file's directory or `cwd`. |
209
223
 
210
224
  ## Config File Discovery
211
225
 
@@ -273,6 +287,7 @@ The loader reads the nearest `.editorconfig` file and maps the subset of fields
273
287
  - `indent_size` / `tab_width` → `tabWidth`
274
288
  - `max_line_length` → `printWidth`
275
289
  - `insert_final_newline` → `insertFinalNewline`
290
+ - `quote_type` → `singleQuote`
276
291
 
277
292
  Glob sections such as `[*.ts]` are converted into returned `overrides` entries.
278
293
 
package/dist/index.d.mts CHANGED
@@ -12,6 +12,15 @@ interface EditorconfigOption {
12
12
  * @default false
13
13
  */
14
14
  onlyCwd?: boolean;
15
+ /**
16
+ * Override the directory from which `.editorconfig` resolution starts.
17
+ * When set, editorconfig is searched from this directory instead of from
18
+ * the config file's directory (or the top-level `cwd`).
19
+ *
20
+ * This is useful when the oxfmt config path is pre-resolved and you still
21
+ * want editorconfig to be resolved relative to each file's directory.
22
+ */
23
+ cwd?: string;
15
24
  }
16
25
  /**
17
26
  * Format option override for a single matching rule
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, relative } from "node:path";
3
3
  import process from "node:process";
4
- import { interopDefault } from "@ntnyq/utils";
4
+ import { interopDefault, isBoolean, isNumber, isObject } from "@ntnyq/utils";
5
5
  import { parse } from "jsonc-parser";
6
6
  import { parseBuffer } from "editorconfig";
7
7
  //#region src/constants.ts
@@ -24,23 +24,57 @@ const EDITORCONFIG_FILE = ".editorconfig";
24
24
  const EDITORCONFIG_GLOBAL_SECTION_NAMES = ["*", "**"];
25
25
  //#endregion
26
26
  //#region src/editorconfig.ts
27
+ /**
28
+ * Builds the cache key used for resolved EditorConfig lookups.
29
+ *
30
+ * @param resolveKey - The base resolution key for the current lookup.
31
+ * @returns The namespaced cache key for EditorConfig resolution.
32
+ */
27
33
  function getEditorconfigResolveCacheKey(resolveKey) {
28
34
  return `editorconfig::${resolveKey}`;
29
35
  }
36
+ /**
37
+ * Determines the directory that should be used to search for a nearby .editorconfig file.
38
+ *
39
+ * @param cwd - The current working directory for the lookup.
40
+ * @param configPath - An optional config path used to anchor the search.
41
+ * @returns The directory where EditorConfig discovery should begin.
42
+ */
30
43
  function getEditorconfigSearchDir(cwd, configPath) {
31
44
  if (!configPath) return cwd;
32
45
  return dirname(isAbsolute(configPath) ? configPath : join(cwd, configPath));
33
46
  }
47
+ /**
48
+ * Merges root-level EditorConfig options with oxfmt config options.
49
+ *
50
+ * @param oxfmtConfig - The explicit oxfmt options.
51
+ * @param editorconfigRootOptions - Root options derived from .editorconfig.
52
+ * @returns A single root options object where oxfmt takes precedence.
53
+ */
34
54
  function mergeRootOptions(oxfmtConfig, editorconfigRootOptions) {
35
55
  return {
36
56
  ...editorconfigRootOptions,
37
57
  ...oxfmtConfig
38
58
  };
39
59
  }
60
+ /**
61
+ * Merges EditorConfig-derived overrides with explicit oxfmt overrides.
62
+ *
63
+ * @param oxfmtOverrides - Overrides declared in the oxfmt config.
64
+ * @param editorconfigOverrides - Overrides derived from .editorconfig sections.
65
+ * @returns The merged overrides array, or undefined when no overrides exist.
66
+ */
40
67
  function mergeOverrides(oxfmtOverrides, editorconfigOverrides) {
41
68
  const mergedOverrides = [...editorconfigOverrides, ...oxfmtOverrides || []];
42
69
  return mergedOverrides.length > 0 ? mergedOverrides : void 0;
43
70
  }
71
+ /**
72
+ * Resolves the nearest .editorconfig file starting from a directory and optionally walking upward.
73
+ *
74
+ * @param startDir - The directory where the search starts.
75
+ * @param onlyCwd - When true, only checks the starting directory.
76
+ * @returns The resolved .editorconfig path, or undefined when none is found.
77
+ */
44
78
  async function resolveEditorconfigPath(startDir, onlyCwd = false) {
45
79
  let currentDir = startDir;
46
80
  while (true) {
@@ -54,19 +88,59 @@ async function resolveEditorconfigPath(startDir, onlyCwd = false) {
54
88
  currentDir = parentDir;
55
89
  }
56
90
  }
91
+ /**
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
+ */
57
97
  function toPosixPath(path) {
58
98
  return path.replaceAll("\\", "/");
59
99
  }
100
+ /**
101
+ * Checks whether an EditorConfig section should be treated as a global section.
102
+ *
103
+ * @param sectionName - The section name parsed from .editorconfig.
104
+ * @returns True when the section applies globally.
105
+ */
60
106
  function isEditorconfigGlobalSection(sectionName) {
61
107
  return Boolean(sectionName && EDITORCONFIG_GLOBAL_SECTION_NAMES.includes(sectionName));
62
108
  }
109
+ /**
110
+ * Parses an EditorConfig boolean string.
111
+ *
112
+ * @param value - The raw EditorConfig value.
113
+ * @returns The parsed boolean, or undefined when the value is unsupported.
114
+ */
63
115
  function parseEditorconfigBoolean(value) {
64
116
  if (value === "true") return true;
65
117
  if (value === "false") return false;
66
118
  }
119
+ /**
120
+ * Parses the end-of-line style supported by oxfmt.
121
+ *
122
+ * @param value - The raw EditorConfig end_of_line value.
123
+ * @returns The normalized line ending value, or undefined when unsupported.
124
+ */
67
125
  function parseEditorconfigEndOfLine(value) {
68
126
  if (value === "lf" || value === "crlf" || value === "cr") return value;
69
127
  }
128
+ /**
129
+ * Parses the single quote preference from an EditorConfig section.
130
+ *
131
+ * @param value - The raw EditorConfig value for single quote preference.
132
+ * @returns True if single quotes are preferred, false if double quotes are preferred, or undefined when auto.
133
+ */
134
+ function parseEditorconfigQuoteType(value) {
135
+ if (value === "single") return true;
136
+ if (value === "double") return false;
137
+ }
138
+ /**
139
+ * Resolves the effective tab width from an EditorConfig section.
140
+ *
141
+ * @param section - The parsed EditorConfig section body.
142
+ * @returns The resolved tab width, or undefined when no numeric value is available.
143
+ */
70
144
  function parseEditorconfigTabWidth(section) {
71
145
  const indentSize = section["indent_size"];
72
146
  const tabWidth = section["tab_width"];
@@ -79,27 +153,50 @@ function parseEditorconfigTabWidth(section) {
79
153
  if (Number.isFinite(parsedTabWidth)) return parsedTabWidth;
80
154
  }
81
155
  }
156
+ /**
157
+ * Maps a parsed EditorConfig section to the subset of oxfmt formatter options it supports.
158
+ *
159
+ * @param section - The parsed EditorConfig section body.
160
+ * @returns Formatter options derived from the section.
161
+ */
82
162
  function mapEditorconfigSectionToOptions(section) {
83
163
  const options = {};
84
164
  const endOfLine = parseEditorconfigEndOfLine(section["end_of_line"]);
85
165
  if (endOfLine) options.endOfLine = endOfLine;
86
166
  if (section["indent_style"] === "tab") options.useTabs = true;
87
167
  else if (section["indent_style"] === "space") options.useTabs = false;
168
+ const singleQuote = parseEditorconfigQuoteType(section["quote_type"]);
169
+ if (isBoolean(singleQuote)) options.singleQuote = singleQuote;
88
170
  const tabWidth = parseEditorconfigTabWidth(section);
89
- if (typeof tabWidth === "number") options.tabWidth = tabWidth;
171
+ if (isNumber(tabWidth)) options.tabWidth = tabWidth;
90
172
  if (section["max_line_length"] && section["max_line_length"] !== "unset") {
91
173
  const parsedPrintWidth = Number(section["max_line_length"]);
92
174
  if (Number.isFinite(parsedPrintWidth)) options.printWidth = parsedPrintWidth;
93
175
  }
94
176
  const insertFinalNewline = parseEditorconfigBoolean(section["insert_final_newline"]);
95
- if (typeof insertFinalNewline === "boolean") options.insertFinalNewline = insertFinalNewline;
177
+ if (isBoolean(insertFinalNewline)) options.insertFinalNewline = insertFinalNewline;
96
178
  return options;
97
179
  }
180
+ /**
181
+ * Rebases an EditorConfig section pattern from the config directory to the target anchor directory.
182
+ *
183
+ * @param pattern - The original EditorConfig section pattern.
184
+ * @param editorconfigDir - The directory containing the .editorconfig file.
185
+ * @param anchorDir - The directory used as the override pattern base.
186
+ * @returns The rebased glob pattern.
187
+ */
98
188
  function rebaseEditorconfigPattern(pattern, editorconfigDir, anchorDir) {
99
189
  const relativePrefix = toPosixPath(relative(anchorDir, editorconfigDir));
100
190
  if (!relativePrefix || relativePrefix === ".") return pattern;
101
191
  return `${relativePrefix}/${pattern.replace(/^\//, "")}`;
102
192
  }
193
+ /**
194
+ * Reads a .editorconfig file and converts it into root options and override entries.
195
+ *
196
+ * @param editorconfigPath - The absolute path to the .editorconfig file.
197
+ * @param anchorDir - The directory used to rebase section patterns.
198
+ * @returns Parsed EditorConfig data ready to merge into oxfmt config.
199
+ */
103
200
  async function readEditorconfigFromFile(editorconfigPath, anchorDir) {
104
201
  const parsedSections = parseBuffer(await readFile(editorconfigPath));
105
202
  const editorconfigDir = dirname(editorconfigPath);
@@ -223,10 +320,11 @@ async function loadOxfmtConfig(options = {}) {
223
320
  const cwd = options.cwd || process.cwd();
224
321
  const editorconfig = options.editorconfig ?? true;
225
322
  const useEditorconfig = editorconfig !== false;
226
- const onlyCwd = useEditorconfig && typeof editorconfig === "object" ? editorconfig.onlyCwd ?? false : false;
323
+ const onlyCwd = useEditorconfig && isObject(editorconfig) ? editorconfig.onlyCwd ?? false : false;
324
+ const editorconfigCwd = useEditorconfig && isObject(editorconfig) ? editorconfig.cwd : void 0;
227
325
  const resolveKey = getResolveCacheKey(cwd, options.configPath);
228
- const editorconfigSearchDir = getEditorconfigSearchDir(cwd, options.configPath);
229
- const editorconfigResolveKey = getEditorconfigResolveCacheKey(resolveKey);
326
+ const editorconfigSearchDir = editorconfigCwd || getEditorconfigSearchDir(cwd, options.configPath);
327
+ const editorconfigResolveKey = editorconfigCwd ? getEditorconfigResolveCacheKey(`${editorconfigCwd}::${options.configPath || ""}`) : getEditorconfigResolveCacheKey(resolveKey);
230
328
  const resolvedPath = useCache ? await cachePromise(resolveCache, resolveKey, () => resolveOxfmtrcPath(cwd, options.configPath)) : await resolveOxfmtrcPath(cwd, options.configPath);
231
329
  const editorconfigPath = useEditorconfig ? await (useCache ? cachePromise(resolveCache, editorconfigResolveKey, () => resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : resolveEditorconfigPath(editorconfigSearchDir, onlyCwd)) : void 0;
232
330
  const anchorDir = dirname(resolvedPath || editorconfigPath || cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "load-oxfmt-config",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Load oxfmt config files and merge supported .editorconfig options.",
5
5
  "keywords": [
6
6
  "editorconfig",
@@ -39,23 +39,23 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@ntnyq/utils": "^0.11.2",
42
+ "@ntnyq/utils": "^0.12.0",
43
43
  "editorconfig": "^3.0.2",
44
44
  "jiti": "^2.6.1",
45
45
  "jsonc-parser": "^3.3.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@ntnyq/tsconfig": "^3.1.0",
49
- "@types/node": "^25.5.0",
50
- "@typescript/native-preview": "^7.0.0-dev.20260326.1",
49
+ "@types/node": "^25.6.0",
50
+ "@typescript/native-preview": "^7.0.0-dev.20260413.1",
51
51
  "bumpp": "^11.0.1",
52
52
  "husky": "^9.1.7",
53
- "nano-staged": "^0.9.0",
53
+ "nano-staged": "^1.0.2",
54
54
  "npm-run-all2": "^8.0.4",
55
- "oxfmt": "^0.42.0",
56
- "oxlint": "^1.57.0",
57
- "tsdown": "^0.21.5",
58
- "vitest": "^4.1.2"
55
+ "oxfmt": "^0.45.0",
56
+ "oxlint": "^1.60.0",
57
+ "tsdown": "^0.21.8",
58
+ "vitest": "^4.1.4"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "oxfmt": ">=0.41.0"