editorconfig 2.0.1 → 3.0.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/lib/cli.test.d.ts +1 -0
- package/lib/cli.test.js +50 -0
- package/lib/index.d.ts +53 -8
- package/lib/index.js +4 -3
- package/lib/index.test.d.ts +1 -0
- package/lib/index.test.js +224 -0
- package/package.json +10 -5
- package/eslint.config.mjs +0 -13
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'chai/register-should.js';
|
package/lib/cli.test.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("chai/register-should.js");
|
|
7
|
+
const cli_js_1 = __importDefault(require("./cli.js"));
|
|
8
|
+
async function exec(...args) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const err = [];
|
|
11
|
+
const res = {
|
|
12
|
+
stdout: '',
|
|
13
|
+
stderr: '',
|
|
14
|
+
};
|
|
15
|
+
try {
|
|
16
|
+
res.props = await (0, cli_js_1.default)(['node', 'editorconfig', ...args], {
|
|
17
|
+
writeOut(s) {
|
|
18
|
+
out.push(s);
|
|
19
|
+
},
|
|
20
|
+
writeErr(s) {
|
|
21
|
+
err.push(s);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (er) {
|
|
26
|
+
res.err = er;
|
|
27
|
+
}
|
|
28
|
+
if (out.length) {
|
|
29
|
+
res.stdout = out.join('');
|
|
30
|
+
}
|
|
31
|
+
if (err.length) {
|
|
32
|
+
res.stderr = err.join('');
|
|
33
|
+
}
|
|
34
|
+
return res;
|
|
35
|
+
}
|
|
36
|
+
describe('Command line interface', () => {
|
|
37
|
+
it('helps', async () => {
|
|
38
|
+
const res = await exec('--help');
|
|
39
|
+
res.stdout.should.match(/^Usage:/);
|
|
40
|
+
});
|
|
41
|
+
it('Lists files', async () => {
|
|
42
|
+
const res = await exec('foo.md', '--files');
|
|
43
|
+
res.stdout.trim().should.match(/\.editorconfig \[\*\.md\]$/);
|
|
44
|
+
});
|
|
45
|
+
it('Lists multiple files', async () => {
|
|
46
|
+
const res = await exec('foo.md', 'bar.js', '--files');
|
|
47
|
+
res.stdout.should.match(/^\[foo\.md\]/);
|
|
48
|
+
res.stdout.trim().should.match(/\.editorconfig \[\*\]$/);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,24 +1,70 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { Minimatch } from 'minimatch';
|
|
3
3
|
export interface KnownProps {
|
|
4
|
+
/**
|
|
5
|
+
* Specifies the character set. Use of `utf-8-bom` is discouraged.
|
|
6
|
+
*/
|
|
7
|
+
charset?: 'latin1' | 'utf-8' | 'utf-8-bom' | 'utf-16be' | 'utf-16le' | 'unset';
|
|
8
|
+
/**
|
|
9
|
+
* Specifies how line breaks are represented.
|
|
10
|
+
*/
|
|
4
11
|
end_of_line?: 'lf' | 'crlf' | 'unset';
|
|
5
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Specifies the number of columns used for each indentation level
|
|
14
|
+
* and the width of soft tabs (when supported).
|
|
15
|
+
*
|
|
16
|
+
* If `indent_size` in the config is set to `tab`,
|
|
17
|
+
* the value of this property will be:
|
|
18
|
+
* - the same as the value of the {@link KnownProps.tab_width tab_width}
|
|
19
|
+
* if it is specified;
|
|
20
|
+
* - `tab` if it is not.
|
|
21
|
+
*/
|
|
6
22
|
indent_size?: number | 'tab' | 'unset';
|
|
23
|
+
/**
|
|
24
|
+
* Specifies whether tabs or spaces should be used for indentation.
|
|
25
|
+
* - `tab`: Use hard tabs for indentation,
|
|
26
|
+
* filling the remainder with spaces if needed.
|
|
27
|
+
* - `space`: Use spaces for indentation.
|
|
28
|
+
*/
|
|
29
|
+
indent_style?: 'tab' | 'space' | 'unset';
|
|
30
|
+
/**
|
|
31
|
+
* Specifies whether a file should end with a newline character when saved.
|
|
32
|
+
* - `true`: Ensure the file ends with a newline.
|
|
33
|
+
* - `false`: Ensure the file does not end with a newline.
|
|
34
|
+
*
|
|
35
|
+
* Editors must not insert newlines in empty files
|
|
36
|
+
* when saving those files, even if `insert_final_newline` = true.
|
|
37
|
+
*/
|
|
7
38
|
insert_final_newline?: true | false | 'unset';
|
|
39
|
+
/**
|
|
40
|
+
* Specifies the number of columns used to represent a tab character.
|
|
41
|
+
*
|
|
42
|
+
* This defaults to the value of {@link KnownProps.indent_size indent_size}
|
|
43
|
+
* and should not usually need to be specified.
|
|
44
|
+
*/
|
|
8
45
|
tab_width?: number | 'unset';
|
|
46
|
+
/**
|
|
47
|
+
* Specifies whether all whitespace characters
|
|
48
|
+
* preceding newline characters in the file should be removed.
|
|
49
|
+
* - `true`: Remove all trailing whitespace before newlines.
|
|
50
|
+
* - `false`: Preserve trailing whitespace.
|
|
51
|
+
*/
|
|
9
52
|
trim_trailing_whitespace?: true | false | 'unset';
|
|
10
|
-
charset?: string | 'unset';
|
|
11
53
|
}
|
|
12
|
-
interface UnknownMap {
|
|
54
|
+
export interface UnknownMap {
|
|
13
55
|
[index: string]: unknown;
|
|
14
56
|
}
|
|
15
|
-
export type
|
|
57
|
+
export type PossibleValue = boolean | number | (string & {});
|
|
58
|
+
export type AddPossibleValues<T extends object> = {
|
|
59
|
+
[K in keyof T]: T[K] | PossibleValue;
|
|
60
|
+
};
|
|
61
|
+
export type Props = AddPossibleValues<KnownProps> & UnknownMap;
|
|
16
62
|
export interface ECFile {
|
|
17
63
|
name: string;
|
|
18
64
|
contents?: Buffer;
|
|
19
65
|
}
|
|
20
|
-
type SectionGlob = Minimatch | null;
|
|
21
|
-
type GlobbedProps = [SectionName, Props, SectionGlob][];
|
|
66
|
+
export type SectionGlob = Minimatch | null;
|
|
67
|
+
export type GlobbedProps = [SectionName, Props, SectionGlob][];
|
|
22
68
|
export interface ProcessedFileConfig {
|
|
23
69
|
root: boolean;
|
|
24
70
|
name: string;
|
|
@@ -60,7 +106,7 @@ export declare function parseBuffer(data: Buffer): ParseStringResult;
|
|
|
60
106
|
*
|
|
61
107
|
* @param data String to parse.
|
|
62
108
|
* @returns Parsed contents. Will be truncated if there was a parse error.
|
|
63
|
-
* @deprecated Use {@link
|
|
109
|
+
* @deprecated Use {@link parseBuffer} instead.
|
|
64
110
|
*/
|
|
65
111
|
export declare function parseString(data: string): ParseStringResult;
|
|
66
112
|
/**
|
|
@@ -125,4 +171,3 @@ export declare function parseSync(filepath: string, options?: ParseOptions): Pro
|
|
|
125
171
|
* @private
|
|
126
172
|
*/
|
|
127
173
|
export declare function matcher(options: ParseOptions, ...buffers: Buffer[]): (filepath: string) => Props;
|
|
128
|
-
export {};
|
package/lib/index.js
CHANGED
|
@@ -54,12 +54,13 @@ const package_json_1 = __importDefault(require("../package.json"));
|
|
|
54
54
|
const escapedSep = new RegExp(path.sep.replace(/\\/g, '\\\\'), 'g');
|
|
55
55
|
const matchOptions = { matchBase: true, dot: true };
|
|
56
56
|
const knownPropNames = [
|
|
57
|
+
'charset',
|
|
57
58
|
'end_of_line',
|
|
58
|
-
'indent_style',
|
|
59
59
|
'indent_size',
|
|
60
|
+
'indent_style',
|
|
60
61
|
'insert_final_newline',
|
|
62
|
+
// 'tab_width' // This should be an integer, not a string.
|
|
61
63
|
'trim_trailing_whitespace',
|
|
62
|
-
'charset',
|
|
63
64
|
];
|
|
64
65
|
const knownProps = new Set(knownPropNames);
|
|
65
66
|
/**
|
|
@@ -103,7 +104,7 @@ function parseBuffer(data) {
|
|
|
103
104
|
*
|
|
104
105
|
* @param data String to parse.
|
|
105
106
|
* @returns Parsed contents. Will be truncated if there was a parse error.
|
|
106
|
-
* @deprecated Use {@link
|
|
107
|
+
* @deprecated Use {@link parseBuffer} instead.
|
|
107
108
|
*/
|
|
108
109
|
function parseString(data) {
|
|
109
110
|
return parseBuffer(node_buffer_1.Buffer.from(data));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'chai/register-should.js';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
37
|
+
require("chai/register-should.js");
|
|
38
|
+
const editorconfig = __importStar(require("./index.js"));
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const node_buffer_1 = require("node:buffer");
|
|
42
|
+
describe('parse', () => {
|
|
43
|
+
const expected = {
|
|
44
|
+
indent_style: 'space',
|
|
45
|
+
indent_size: 2,
|
|
46
|
+
end_of_line: 'lf',
|
|
47
|
+
charset: 'utf-8',
|
|
48
|
+
trim_trailing_whitespace: true,
|
|
49
|
+
insert_final_newline: true,
|
|
50
|
+
tab_width: 2,
|
|
51
|
+
block_comment: '*',
|
|
52
|
+
block_comment_end: '*/',
|
|
53
|
+
block_comment_start: '/**',
|
|
54
|
+
};
|
|
55
|
+
const target = path.join(__dirname, '/app.js');
|
|
56
|
+
it('async', async () => {
|
|
57
|
+
const cfg = await editorconfig.parse(target);
|
|
58
|
+
cfg.should.eql(expected);
|
|
59
|
+
});
|
|
60
|
+
it('sync', () => {
|
|
61
|
+
const visited = [];
|
|
62
|
+
const cfg = editorconfig.parseSync(target, { files: visited });
|
|
63
|
+
cfg.should.eql(expected);
|
|
64
|
+
visited.should.have.lengthOf(1);
|
|
65
|
+
visited[0].glob.should.eql('*');
|
|
66
|
+
visited[0].fileName.should.match(/\.editorconfig$/);
|
|
67
|
+
});
|
|
68
|
+
it('caches', async () => {
|
|
69
|
+
const cache = new Map();
|
|
70
|
+
const cfg = await editorconfig.parse(target, { cache });
|
|
71
|
+
cfg.should.eql(expected);
|
|
72
|
+
cache.size.should.be.eql(2);
|
|
73
|
+
await editorconfig.parse(target, { cache });
|
|
74
|
+
cache.size.should.be.eql(2);
|
|
75
|
+
});
|
|
76
|
+
it('caches sync', () => {
|
|
77
|
+
const cache = new Map();
|
|
78
|
+
const cfg = editorconfig.parseSync(target, { cache });
|
|
79
|
+
cfg.should.eql(expected);
|
|
80
|
+
cache.size.should.be.eql(2);
|
|
81
|
+
editorconfig.parseSync(target, { cache });
|
|
82
|
+
cache.size.should.be.eql(2);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('parseFromFiles', () => {
|
|
86
|
+
const expected = {
|
|
87
|
+
block_comment_end: '*/',
|
|
88
|
+
block_comment_start: '/**',
|
|
89
|
+
block_comment: '*',
|
|
90
|
+
charset: 'utf-8',
|
|
91
|
+
end_of_line: 'lf',
|
|
92
|
+
indent_size: 2,
|
|
93
|
+
indent_style: 'space',
|
|
94
|
+
insert_final_newline: true,
|
|
95
|
+
tab_width: 2,
|
|
96
|
+
trim_trailing_whitespace: true,
|
|
97
|
+
};
|
|
98
|
+
const configs = [];
|
|
99
|
+
const configPath = path.resolve(__dirname, '../.editorconfig');
|
|
100
|
+
configs.push({
|
|
101
|
+
name: configPath,
|
|
102
|
+
contents: fs.readFileSync(configPath),
|
|
103
|
+
});
|
|
104
|
+
const target = path.join(__dirname, '/app.js');
|
|
105
|
+
const configs2 = [
|
|
106
|
+
{ name: 'early', contents: node_buffer_1.Buffer.alloc(0) },
|
|
107
|
+
configs[0],
|
|
108
|
+
];
|
|
109
|
+
it('async', async () => {
|
|
110
|
+
const cfg = await editorconfig.parseFromFiles(target, Promise.resolve(configs));
|
|
111
|
+
cfg.should.eql(expected);
|
|
112
|
+
});
|
|
113
|
+
it('sync', () => {
|
|
114
|
+
const cfg = editorconfig.parseFromFilesSync(target, configs);
|
|
115
|
+
cfg.should.eql(expected);
|
|
116
|
+
});
|
|
117
|
+
it('handles null', () => {
|
|
118
|
+
const cfg = editorconfig.parseFromFilesSync(target, [{
|
|
119
|
+
name: configPath,
|
|
120
|
+
contents: node_buffer_1.Buffer.from('[*]\nfoo = null\n'),
|
|
121
|
+
}]);
|
|
122
|
+
cfg.should.eql({ foo: 'null' });
|
|
123
|
+
});
|
|
124
|
+
it('caches async', async () => {
|
|
125
|
+
const cache = new Map();
|
|
126
|
+
const cfg = await editorconfig.parseFromFiles(target, Promise.resolve(configs2), { cache });
|
|
127
|
+
cfg.should.eql(expected);
|
|
128
|
+
cache.size.should.be.eql(2);
|
|
129
|
+
const cfg2 = await editorconfig.parseFromFiles(target, Promise.resolve(configs2), { cache });
|
|
130
|
+
cfg2.should.eql(expected);
|
|
131
|
+
cache.size.should.be.eql(2);
|
|
132
|
+
});
|
|
133
|
+
it('caches sync', () => {
|
|
134
|
+
const cache = new Map();
|
|
135
|
+
const cfg = editorconfig.parseFromFilesSync(target, configs2, { cache });
|
|
136
|
+
cfg.should.eql(expected);
|
|
137
|
+
cache.size.should.be.eql(2);
|
|
138
|
+
const cfg2 = editorconfig.parseFromFilesSync(target, configs2, { cache });
|
|
139
|
+
cfg2.should.eql(expected);
|
|
140
|
+
cache.size.should.be.eql(2);
|
|
141
|
+
});
|
|
142
|
+
it('handles minimatch escapables', () => {
|
|
143
|
+
// Note that this `#` does not actually test the /^#/ escaping logic,
|
|
144
|
+
// because this path will go through a `path.dirname` before that happens.
|
|
145
|
+
// It's here to catch what would happen if minimatch started to treat #
|
|
146
|
+
// differently inside a pattern.
|
|
147
|
+
const bogusPath = path.resolve(__dirname, '#?*+@!()|[]{}');
|
|
148
|
+
const escConfigs = [
|
|
149
|
+
{
|
|
150
|
+
name: `${bogusPath}/.editorconfig`,
|
|
151
|
+
contents: configs[0].contents,
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
const escTarget = `${bogusPath}/app.js`;
|
|
155
|
+
const cfg = editorconfig.parseFromFilesSync(escTarget, escConfigs);
|
|
156
|
+
cfg.should.eql(expected);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('parseString', () => {
|
|
160
|
+
const expected = [
|
|
161
|
+
[null, { root: 'true' }],
|
|
162
|
+
['*', {
|
|
163
|
+
block_comment_end: '*/',
|
|
164
|
+
block_comment_start: '/**',
|
|
165
|
+
block_comment: '*',
|
|
166
|
+
charset: 'utf-8',
|
|
167
|
+
end_of_line: 'lf',
|
|
168
|
+
indent_size: '2',
|
|
169
|
+
indent_style: 'space',
|
|
170
|
+
insert_final_newline: 'true',
|
|
171
|
+
trim_trailing_whitespace: 'true',
|
|
172
|
+
}],
|
|
173
|
+
['*.md', { indent_size: '4' }],
|
|
174
|
+
];
|
|
175
|
+
const configPath = path.resolve(__dirname, '../.editorconfig');
|
|
176
|
+
const contents = fs.readFileSync(configPath, 'utf8');
|
|
177
|
+
it('sync', () => {
|
|
178
|
+
const cfg = editorconfig.parseString(contents);
|
|
179
|
+
cfg.should.eql(expected);
|
|
180
|
+
});
|
|
181
|
+
it('handles errors', () => {
|
|
182
|
+
const cfg = editorconfig.parseString('root: ');
|
|
183
|
+
cfg.should.eql([[null, {}]]);
|
|
184
|
+
});
|
|
185
|
+
it('handles backslashes in glob', () => {
|
|
186
|
+
const cfg = editorconfig.parseString('[a\\\\b]');
|
|
187
|
+
cfg.should.eql([[null, {}], ['a\\\\b', {}]]);
|
|
188
|
+
});
|
|
189
|
+
it('handles blank comments', () => {
|
|
190
|
+
const cfg = editorconfig.parseString('#');
|
|
191
|
+
cfg.should.eql([[null, {}]]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('extra behavior', () => {
|
|
195
|
+
it('handles extended globs', () => {
|
|
196
|
+
// These failed when we had noext: true in matchOptions
|
|
197
|
+
const matcher = editorconfig.matcher({
|
|
198
|
+
root: __dirname,
|
|
199
|
+
}, node_buffer_1.Buffer.from(`\
|
|
200
|
+
[*]
|
|
201
|
+
indent_size = 4
|
|
202
|
+
|
|
203
|
+
[!(package).json]
|
|
204
|
+
indent_size = 3`));
|
|
205
|
+
matcher(path.join(__dirname, 'package.json')).should.include({ indent_size: 4 });
|
|
206
|
+
matcher(path.join(__dirname, 'foo.json')).should.include({ indent_size: 3 });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('unset', () => {
|
|
210
|
+
it('pair witht the value `unset`', () => {
|
|
211
|
+
const matcher = editorconfig.matcher({
|
|
212
|
+
root: __dirname,
|
|
213
|
+
unset: true,
|
|
214
|
+
}, node_buffer_1.Buffer.from(`\
|
|
215
|
+
[*]
|
|
216
|
+
indent_size = 4
|
|
217
|
+
|
|
218
|
+
[*.json]
|
|
219
|
+
indent_size = unset
|
|
220
|
+
`));
|
|
221
|
+
matcher(path.join(__dirname, 'index.js')).should.include({ indent_size: 4 });
|
|
222
|
+
matcher(path.join(__dirname, 'index.json')).should.be.eql({});
|
|
223
|
+
});
|
|
224
|
+
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editorconfig",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "EditorConfig File Locator and Interpreter for Node.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"editorconfig",
|
|
7
7
|
"core"
|
|
8
8
|
],
|
|
9
9
|
"main": "./lib/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/*",
|
|
12
|
+
"bin/*"
|
|
13
|
+
],
|
|
10
14
|
"contributors": [
|
|
11
15
|
"Hong Xu (topbug.net)",
|
|
12
16
|
"Jed Mao (https://github.com/jedmao/)",
|
|
13
17
|
"Trey Hunner (http://treyhunner.com)",
|
|
14
|
-
"Joe Hildebrand (https://github.com/hildjj/)"
|
|
18
|
+
"Joe Hildebrand (https://github.com/hildjj/)",
|
|
19
|
+
"SunsetTechuila (https://github.com/SunsetTechuila/)"
|
|
15
20
|
],
|
|
16
21
|
"directories": {
|
|
17
22
|
"bin": "./bin",
|
|
@@ -26,11 +31,11 @@
|
|
|
26
31
|
"license": "MIT",
|
|
27
32
|
"dependencies": {
|
|
28
33
|
"@one-ini/wasm": "0.2.0",
|
|
29
|
-
"commander": "^
|
|
34
|
+
"commander": "^14.0.0",
|
|
30
35
|
"minimatch": "10.0.1",
|
|
31
|
-
"semver": "^7.7.
|
|
36
|
+
"semver": "^7.7.2"
|
|
32
37
|
},
|
|
33
38
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
39
|
+
"node": ">=20"
|
|
35
40
|
}
|
|
36
41
|
}
|