|
1 | 1 | 'use strict'; |
2 | 2 |
|
3 | | -const fs = require('fs'); |
4 | | -const path = require('path'); |
| 3 | +const editorconfig = require('editorconfig'); |
5 | 4 |
|
6 | 5 | /** |
7 | | - * Lightweight .editorconfig parser. |
8 | | - * |
9 | | - * Walks up from `filePath` collecting .editorconfig files, parses their |
10 | | - * INI-like sections, and returns the merged properties that apply. |
11 | | - * |
12 | | - * Only simple glob patterns are supported: |
13 | | - * * – matches everything |
14 | | - * *.ext – matches files with that extension |
15 | | - * *.{a,b} – matches files with extension a or b |
16 | | - * [section] – literal pattern |
17 | | - * |
18 | | - * This intentionally does NOT replicate the full editorconfig-core spec |
19 | | - * (no ** path globs, no numeric ranges, etc.) because for indent_size |
20 | | - * resolution the simple patterns cover virtually all real configs. |
21 | | - */ |
22 | | - |
23 | | -const COMMENT_RE = /^\s*[#;]/; |
24 | | -const SECTION_RE = /^\s*\[(.+?)]\s*$/; |
25 | | -const PROPERTY_RE = /^\s*([\w.-]+)\s*=\s*(.*?)\s*$/; |
26 | | - |
27 | | -function parseEditorConfig(contents) { |
28 | | - const sections = []; |
29 | | - let current = { glob: null, props: {} }; // preamble (root, etc.) |
30 | | - sections.push(current); |
31 | | - |
32 | | - for (const rawLine of contents.split(/\r?\n/)) { |
33 | | - const line = rawLine.trim(); |
34 | | - if (!line || COMMENT_RE.test(line)) { |
35 | | - continue; |
36 | | - } |
37 | | - const sectionMatch = SECTION_RE.exec(line); |
38 | | - if (sectionMatch) { |
39 | | - current = { glob: sectionMatch[1], props: {} }; |
40 | | - sections.push(current); |
41 | | - continue; |
42 | | - } |
43 | | - const propMatch = PROPERTY_RE.exec(line); |
44 | | - if (propMatch) { |
45 | | - const key = propMatch[1].toLowerCase(); |
46 | | - let value = propMatch[2].toLowerCase(); |
47 | | - // Parse numbers and booleans |
48 | | - if (/^\d+$/.test(value)) { |
49 | | - value = Number(value); |
50 | | - } else if (value === 'true') { |
51 | | - value = true; |
52 | | - } else if (value === 'false') { |
53 | | - value = false; |
54 | | - } |
55 | | - current.props[key] = value; |
56 | | - } |
57 | | - } |
58 | | - return sections; |
59 | | -} |
60 | | - |
61 | | -/** |
62 | | - * Tests whether a simple editorconfig glob matches a filename (basename only). |
63 | | - */ |
64 | | -function globMatchesFilename(glob, filename) { |
65 | | - // Strip leading path separators (editorconfig globs without / apply to basename) |
66 | | - if (glob.includes('/')) { |
67 | | - // Path-style globs are not supported in this lightweight impl |
68 | | - return false; |
69 | | - } |
70 | | - if (glob === '*') { |
71 | | - return true; |
72 | | - } |
73 | | - // Handle *.{ext1,ext2} and *.ext |
74 | | - const braceMatch = /^\*\.{([^}]+)}$/.exec(glob); |
75 | | - if (braceMatch) { |
76 | | - const extensions = braceMatch[1].split(',').map((e) => e.trim()); |
77 | | - return extensions.some((ext) => filename.endsWith(`.${ext}`)); |
78 | | - } |
79 | | - if (glob.startsWith('*.')) { |
80 | | - const ext = glob.slice(1); // e.g. ".hbs" |
81 | | - return filename.endsWith(ext); |
82 | | - } |
83 | | - // Literal match |
84 | | - return filename === glob; |
85 | | -} |
86 | | - |
87 | | -/** |
88 | | - * Collect .editorconfig files from `dir` up to the filesystem root, |
89 | | - * stopping at a file that declares `root = true`. |
90 | | - */ |
91 | | -function collectConfigFiles(dir) { |
92 | | - const files = []; |
93 | | - let current = dir; |
94 | | - // eslint-disable-next-line no-constant-condition |
95 | | - while (true) { |
96 | | - const configPath = path.join(current, '.editorconfig'); |
97 | | - try { |
98 | | - const contents = fs.readFileSync(configPath, 'utf8'); |
99 | | - const sections = parseEditorConfig(contents); |
100 | | - const isRoot = sections[0] && sections[0].glob === null && sections[0].props.root === true; |
101 | | - files.push({ dir: current, sections }); |
102 | | - if (isRoot) { |
103 | | - break; |
104 | | - } |
105 | | - } catch { |
106 | | - // No .editorconfig at this level, keep going |
107 | | - } |
108 | | - const parent = path.dirname(current); |
109 | | - if (parent === current) { |
110 | | - break; |
111 | | - } |
112 | | - current = parent; |
113 | | - } |
114 | | - return files; |
115 | | -} |
116 | | - |
117 | | -/** |
118 | | - * Resolve editorconfig properties for a given file path. |
| 6 | + * Resolve editorconfig properties for a given file path using the official |
| 7 | + * editorconfig library. |
119 | 8 | * |
120 | 9 | * Returns an object like `{ indent_size: 4, indent_style: 'space', ... }` |
121 | 10 | * with only the properties that matched. Returns an empty object if no |
122 | 11 | * .editorconfig is found or no sections match. |
123 | 12 | */ |
124 | 13 | function resolveEditorConfig(filePath) { |
125 | | - const dir = path.dirname(filePath); |
126 | | - const filename = path.basename(filePath); |
127 | | - const configFiles = collectConfigFiles(dir); |
128 | | - |
129 | | - // Merge: outermost first, innermost wins (same as editorconfig spec) |
130 | | - const merged = {}; |
131 | | - for (let i = configFiles.length - 1; i >= 0; i--) { |
132 | | - for (const section of configFiles[i].sections) { |
133 | | - if (section.glob === null) { |
134 | | - continue; // preamble section (root = true, etc.) |
135 | | - } |
136 | | - if (globMatchesFilename(section.glob, filename)) { |
137 | | - Object.assign(merged, section.props); |
138 | | - } |
139 | | - } |
140 | | - } |
141 | | - |
142 | | - // Apply editorconfig post-processing rules |
143 | | - if (merged.indent_style === 'tab' && merged.indent_size === undefined) { |
144 | | - merged.indent_size = 'tab'; |
145 | | - } |
146 | | - if ( |
147 | | - merged.indent_size !== undefined && |
148 | | - merged.tab_width === undefined && |
149 | | - merged.indent_size !== 'tab' |
150 | | - ) { |
151 | | - merged.tab_width = merged.indent_size; |
152 | | - } |
153 | | - if (merged.indent_size === 'tab' && merged.tab_width !== undefined) { |
154 | | - merged.indent_size = merged.tab_width; |
155 | | - } |
156 | | - |
157 | | - return merged; |
| 14 | + return editorconfig.parseSync(filePath); |
158 | 15 | } |
159 | 16 |
|
160 | 17 | module.exports = { resolveEditorConfig }; |
0 commit comments