1 | import * as fs from 'fs'
|
2 | import * as path from 'path'
|
3 | import * as semver from 'semver'
|
4 |
|
5 | import minimatch from './lib/fnmatch'
|
6 | import { parseString, ParseStringResult } from './lib/ini'
|
7 |
|
8 | export { parseString }
|
9 |
|
10 | import pkg from '../package.json'
|
11 |
|
12 | export interface KnownProps {
|
13 | end_of_line?: 'lf' | 'crlf' | 'unset'
|
14 | indent_style?: 'tab' | 'space' | 'unset'
|
15 | indent_size?: number | 'tab' | 'unset'
|
16 | insert_final_newline?: true | false | 'unset'
|
17 | tab_width?: number | 'unset'
|
18 | trim_trailing_whitespace?: true | false | 'unset'
|
19 | charset?: string | 'unset'
|
20 | }
|
21 |
|
22 | export interface ECFile {
|
23 | name: string
|
24 | contents: string | Buffer
|
25 | }
|
26 |
|
27 | export interface FileConfig {
|
28 | name: string
|
29 | contents: ParseStringResult
|
30 | }
|
31 |
|
32 | export interface ParseOptions {
|
33 | config?: string
|
34 | version?: string
|
35 | root?: string
|
36 | }
|
37 |
|
38 | const knownProps = {
|
39 | end_of_line: true,
|
40 | indent_style: true,
|
41 | indent_size: true,
|
42 | insert_final_newline: true,
|
43 | trim_trailing_whitespace: true,
|
44 | charset: true,
|
45 | }
|
46 |
|
47 | function fnmatch(filepath: string, glob: string) {
|
48 | const matchOptions = { matchBase: true, dot: true, noext: true }
|
49 | glob = glob.replace(/\*\*/g, '{*,**/**/**}')
|
50 | return minimatch(filepath, glob, matchOptions)
|
51 | }
|
52 |
|
53 | function getConfigFileNames(filepath: string, options: ParseOptions) {
|
54 | const paths = []
|
55 | do {
|
56 | filepath = path.dirname(filepath)
|
57 | paths.push(path.join(filepath, options.config as string))
|
58 | } while (filepath !== options.root)
|
59 | return paths
|
60 | }
|
61 |
|
62 | function processMatches(matches: KnownProps, version: string) {
|
63 |
|
64 |
|
65 | if (
|
66 | 'indent_style' in matches
|
67 | && matches.indent_style === 'tab'
|
68 | && !('indent_size' in matches)
|
69 | && semver.gte(version, '0.10.0')
|
70 | ) {
|
71 | matches.indent_size = 'tab'
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 | if (
|
77 | 'indent_size' in matches
|
78 | && !('tab_width' in matches)
|
79 | && matches.indent_size !== 'tab'
|
80 | ) {
|
81 | matches.tab_width = matches.indent_size
|
82 | }
|
83 |
|
84 |
|
85 | if (
|
86 | 'indent_size' in matches
|
87 | && 'tab_width' in matches
|
88 | && matches.indent_size === 'tab'
|
89 | ) {
|
90 | matches.indent_size = matches.tab_width
|
91 | }
|
92 |
|
93 | return matches
|
94 | }
|
95 |
|
96 | function processOptions(options: ParseOptions = {}, filepath: string) {
|
97 | return {
|
98 | config: options.config || '.editorconfig',
|
99 | version: options.version || pkg.version,
|
100 | root: path.resolve(options.root || path.parse(filepath).root),
|
101 | }
|
102 | }
|
103 |
|
104 | function buildFullGlob(pathPrefix: string, glob: string) {
|
105 | switch (glob.indexOf('/')) {
|
106 | case -1:
|
107 | glob = '**/' + glob
|
108 | break
|
109 | case 0:
|
110 | glob = glob.substring(1)
|
111 | break
|
112 | default:
|
113 | break
|
114 | }
|
115 | return path.join(pathPrefix, glob)
|
116 | }
|
117 |
|
118 | function extendProps(props: {} = {}, options: {} = {}) {
|
119 | for (const key in options) {
|
120 | if (options.hasOwnProperty(key)) {
|
121 | const value = options[key]
|
122 | const key2 = key.toLowerCase()
|
123 | let value2 = value
|
124 | if (knownProps[key2]) {
|
125 | value2 = value.toLowerCase()
|
126 | }
|
127 | try {
|
128 | value2 = JSON.parse(value)
|
129 | } catch (e) {}
|
130 | if (typeof value === 'undefined' || value === null) {
|
131 |
|
132 |
|
133 | value2 = String(value)
|
134 | }
|
135 | props[key2] = value2
|
136 | }
|
137 | }
|
138 | return props
|
139 | }
|
140 |
|
141 | function parseFromConfigs(
|
142 | configs: FileConfig[],
|
143 | filepath: string,
|
144 | options: ParseOptions,
|
145 | ) {
|
146 | return processMatches(
|
147 | configs
|
148 | .reverse()
|
149 | .reduce(
|
150 | (matches: KnownProps, file) => {
|
151 | const pathPrefix = path.dirname(file.name)
|
152 | file.contents.forEach((section) => {
|
153 | const glob = section[0]
|
154 | const options2 = section[1]
|
155 | if (!glob) {
|
156 | return
|
157 | }
|
158 | const fullGlob = buildFullGlob(pathPrefix, glob)
|
159 | if (!fnmatch(filepath, fullGlob)) {
|
160 | return
|
161 | }
|
162 | matches = extendProps(matches, options2)
|
163 | })
|
164 | return matches
|
165 | },
|
166 | {},
|
167 | ),
|
168 | options.version as string,
|
169 | )
|
170 | }
|
171 |
|
172 | function getConfigsForFiles(files: ECFile[]) {
|
173 | const configs = []
|
174 | for (const i in files) {
|
175 | if (files.hasOwnProperty(i)) {
|
176 | const file = files[i]
|
177 | const contents = parseString(file.contents as string)
|
178 | configs.push({
|
179 | name: file.name,
|
180 | contents,
|
181 | })
|
182 | if ((contents[0][1].root || '').toLowerCase() === 'true') {
|
183 | break
|
184 | }
|
185 | }
|
186 | }
|
187 | return configs
|
188 | }
|
189 |
|
190 | async function readConfigFiles(filepaths: string[]) {
|
191 | return Promise.all(
|
192 | filepaths.map((name) => new Promise((resolve) => {
|
193 | fs.readFile(name, 'utf8', (err, data) => {
|
194 | resolve({
|
195 | name,
|
196 | contents: err ? '' : data,
|
197 | })
|
198 | })
|
199 | })),
|
200 | )
|
201 | }
|
202 |
|
203 | function readConfigFilesSync(filepaths: string[]) {
|
204 | const files: ECFile[] = []
|
205 | let file: string | number | Buffer
|
206 | filepaths.forEach((filepath) => {
|
207 | try {
|
208 | file = fs.readFileSync(filepath, 'utf8')
|
209 | } catch (e) {
|
210 | file = ''
|
211 | }
|
212 | files.push({
|
213 | name: filepath,
|
214 | contents: file,
|
215 | })
|
216 | })
|
217 | return files
|
218 | }
|
219 |
|
220 | function opts(filepath: string, options: ParseOptions = {}): [
|
221 | string,
|
222 | ParseOptions
|
223 | ] {
|
224 | const resolvedFilePath = path.resolve(filepath)
|
225 | return [
|
226 | resolvedFilePath,
|
227 | processOptions(options, resolvedFilePath),
|
228 | ]
|
229 | }
|
230 |
|
231 | export async function parseFromFiles(
|
232 | filepath: string,
|
233 | files: Promise<ECFile[]>,
|
234 | options: ParseOptions = {},
|
235 | ) {
|
236 | const [resolvedFilePath, processedOptions] = opts(filepath, options)
|
237 | return files.then(getConfigsForFiles)
|
238 | .then((configs) => parseFromConfigs(
|
239 | configs,
|
240 | resolvedFilePath,
|
241 | processedOptions,
|
242 | ))
|
243 | }
|
244 |
|
245 | export function parseFromFilesSync(
|
246 | filepath: string,
|
247 | files: ECFile[],
|
248 | options: ParseOptions = {},
|
249 | ) {
|
250 | const [resolvedFilePath, processedOptions] = opts(filepath, options)
|
251 | return parseFromConfigs(
|
252 | getConfigsForFiles(files),
|
253 | resolvedFilePath,
|
254 | processedOptions,
|
255 | )
|
256 | }
|
257 |
|
258 | export async function parse(_filepath: string, _options: ParseOptions = {}) {
|
259 | const [resolvedFilePath, processedOptions] = opts(_filepath, _options)
|
260 | const filepaths = getConfigFileNames(resolvedFilePath, processedOptions)
|
261 | return readConfigFiles(filepaths)
|
262 | .then(getConfigsForFiles)
|
263 | .then((configs) => parseFromConfigs(
|
264 | configs,
|
265 | resolvedFilePath,
|
266 | processedOptions,
|
267 | ))
|
268 | }
|
269 |
|
270 | export function parseSync(_filepath: string, _options: ParseOptions = {}) {
|
271 | const [resolvedFilePath, processedOptions] = opts(_filepath, _options)
|
272 | const filepaths = getConfigFileNames(resolvedFilePath, processedOptions)
|
273 | const files = readConfigFilesSync(filepaths)
|
274 | return parseFromConfigs(
|
275 | getConfigsForFiles(files),
|
276 | resolvedFilePath,
|
277 | processedOptions,
|
278 | )
|
279 | }
|