eslint-formatter-gitlab 4.0.0 → 5.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.
Files changed (4) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +21 -11
  3. package/index.js +132 -157
  4. package/package.json +12 -21
package/LICENSE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Copyright © 2018 Remco Haszing
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
- associated documentation files (the "Software"), to deal in the Software without restriction,
6
+ associated documentation files (the Software), to deal in the Software without restriction,
7
7
  including without limitation the rights to use, copy, modify, merge, publish, distribute,
8
8
  sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9
9
  furnished to do so, subject to the following conditions:
@@ -11,7 +11,7 @@ furnished to do so, subject to the following conditions:
11
11
  The above copyright notice and this permission notice shall be included in all copies or substantial
12
12
  portions of the Software.
13
13
 
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14
+ THE SOFTWARE IS PROVIDED AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15
15
  NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16
16
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
17
17
  OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
package/README.md CHANGED
@@ -2,11 +2,20 @@
2
2
 
3
3
  Show ESLint results directly in the
4
4
  [GitLab code quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html)
5
- results
5
+ results.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Requirements](#requirements)
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Example](#example)
13
+ - [Configuration](#configuration)
14
+ - [License](#license)
6
15
 
7
16
  ## Requirements
8
17
 
9
- This package requires at least Node.js 14 and ESLint 5.
18
+ This package requires at least Node.js 18 and ESLint 5.
10
19
 
11
20
  ## Installation
12
21
 
@@ -24,7 +33,7 @@ _.gitlab-ci.yml_:
24
33
 
25
34
  ```yaml
26
35
  eslint:
27
- image: node:18-alpine
36
+ image: node:20-alpine
28
37
  script:
29
38
  - npm ci
30
39
  - npx eslint --format gitlab .
@@ -33,8 +42,8 @@ eslint:
33
42
  codequality: gl-codequality.json
34
43
  ```
35
44
 
36
- The formatter will automatically detect a GitLab CI environment. It will detect where to output the
37
- code quality report based on the GitLab configuration file.
45
+ The formatter automatically detects a GitLab CI environment. It detects where to output the code
46
+ quality report based on the GitLab configuration file.
38
47
 
39
48
  ## Example
40
49
 
@@ -42,12 +51,13 @@ An example of the results can be seen in
42
51
  [Merge Request !1](https://gitlab.com/remcohaszing/eslint-formatter-gitlab/merge_requests/1) of
43
52
  `eslint-formatter-gitlab` itself.
44
53
 
45
- ## Configuration Options
54
+ ## Configuration
46
55
 
47
- ESLint formatters don’t take any configuration options. `eslint-formatter-gitlab` uses GitLab
48
- environment variables to configure the output. In addition, the environment variable
49
- `ESLINT_CODE_QUALITY_REPORT` is used to override the location to store the code quality report.
56
+ ESLint formatters don’t take any configuration options. `eslint-formatter-gitlab` uses GitLab’s
57
+ [predefined environment variables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)
58
+ to configure the output. In addition, the environment variable `ESLINT_CODE_QUALITY_REPORT` is used
59
+ to override the location to store the code quality report.
50
60
 
51
- # License
61
+ ## License
52
62
 
53
- [MIT](LICENSE.md) @ [Remco Haszing](https://gitlab.com/remcohaszing)
63
+ [MIT](LICENSE.md) © [Remco Haszing](https://gitlab.com/remcohaszing)
package/index.js CHANGED
@@ -2,75 +2,26 @@
2
2
  * @typedef {import('eslint').ESLint.LintResult} LintResult
3
3
  * @typedef {import('eslint').ESLint.LintResultData} LintResultData
4
4
  * @typedef {import('eslint').Linter.LintMessage} LintMessage
5
+ * @typedef {import('codeclimate-types').Issue} Issue
5
6
  */
6
7
 
7
- /**
8
- * @typedef {object} GitLabReports
9
- * @property {string} [codequality]
10
- */
11
-
12
- /**
13
- * @typedef {object} GitLabArtifacts
14
- * @property {GitLabReports} [reports]
15
- */
16
-
17
- /**
18
- * @typedef {object} GitLabJob
19
- * @property {GitLabArtifacts} [artifacts]
20
- */
8
+ const { createHash } = require('node:crypto')
9
+ const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs')
10
+ const { EOL } = require('node:os')
11
+ const { dirname, join, relative, resolve } = require('node:path')
21
12
 
22
- /**
23
- * @typedef {Record<string, GitLabJob>} GitLabCI
24
- */
25
-
26
- /**
27
- * @typedef {object} CodeClimateLines
28
- * @property {number} begin
29
- * @property {number} end
30
- */
31
-
32
- /**
33
- * @typedef {object} CodeClimateContents
34
- * @property {string} body
35
- */
36
-
37
- /**
38
- * @typedef {object} CodeClimateLocation
39
- * https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#locations
40
- * @property {string} path
41
- * @property {CodeClimateLines} lines
42
- */
43
-
44
- /**
45
- * @typedef {object} CodeClimateIssue
46
- * https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#issues
47
- * @property {'issue'} type
48
- * @property {string} check_name
49
- * @property {string} description
50
- * @property {CodeClimateContents} [contents]
51
- * @property {'info' | 'minor' | 'major' | 'critical' | 'blocker'} severity
52
- * @property {string} [fingerprint]
53
- * @property {CodeClimateLocation} location
54
- */
55
-
56
- const { createHash } = require('node:crypto');
57
- const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs');
58
- const { EOL } = require('node:os');
59
- const { dirname, join, relative, resolve } = require('node:path');
60
-
61
- const chalk = require('chalk');
62
- const yaml = require('yaml');
13
+ const chalk = require('chalk')
14
+ const yaml = require('yaml')
63
15
 
64
16
  const {
17
+ CI_COMMIT_SHORT_SHA,
65
18
  CI_CONFIG_PATH = '.gitlab-ci.yml',
66
19
  CI_JOB_NAME,
67
20
  CI_PROJECT_DIR = process.cwd(),
68
21
  CI_PROJECT_URL,
69
- CI_COMMIT_SHORT_SHA,
70
22
  ESLINT_CODE_QUALITY_REPORT,
71
- GITLAB_CI,
72
- NODE_ENV,
73
- } = process.env;
23
+ GITLAB_CI
24
+ } = process.env
74
25
 
75
26
  /**
76
27
  * @type {yaml.CollectionTag}
@@ -81,105 +32,123 @@ const reference = {
81
32
  default: false,
82
33
  resolve() {
83
34
  // We only allow the syntax. We don’t actually resolve the reference.
84
- },
85
- };
35
+ }
36
+ }
86
37
 
87
38
  /**
88
39
  * @returns {string} The output path of the code quality artifact.
89
40
  */
90
41
  function getOutputPath() {
91
- const configPath = join(CI_PROJECT_DIR, CI_CONFIG_PATH);
42
+ const configPath = join(CI_PROJECT_DIR, CI_CONFIG_PATH)
92
43
  // GitlabCI allows a custom configuration path which can be a URL or a path relative to another
93
44
  // project. In these cases CI_CONFIG_PATH is empty and we'll have to require the user provide
94
45
  // ESLINT_CODE_QUALITY_REPORT.
95
46
  if (!existsSync(configPath) || !lstatSync(configPath).isFile()) {
96
47
  throw new Error(
97
48
  'Could not resolve .gitlab-ci.yml to automatically detect report artifact path.' +
98
- ' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.',
99
- );
100
- }
101
- const jobs = /** @type {GitLabCI} */ (
102
- yaml.parse(readFileSync(configPath, 'utf8'), { version: '1.1', customTags: [reference] })
103
- );
104
- const { artifacts } = jobs[/** @type {string} */ (CI_JOB_NAME)];
105
- const location = artifacts?.reports?.codequality;
106
- const msg = `Expected ${CI_JOB_NAME}.artifacts.reports.codequality to be one exact path`;
107
- if (!location) {
108
- throw new Error(`${msg}, but no value was found.`);
49
+ ' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.'
50
+ )
109
51
  }
110
- if (Array.isArray(location)) {
111
- throw new TypeError(`${msg}, but found an array instead.`);
52
+ const doc = yaml.parseDocument(readFileSync(configPath, 'utf8'), {
53
+ version: '1.1',
54
+ customTags: [reference]
55
+ })
56
+ const path = [CI_JOB_NAME, 'artifacts', 'reports', 'codequality']
57
+ const location = doc.getIn(path)
58
+ if (typeof location !== 'string' || !location) {
59
+ throw new TypeError(
60
+ `Expected ${path.join('.')} to be one exact path, got: ${JSON.stringify(location)}`
61
+ )
112
62
  }
113
- return resolve(CI_PROJECT_DIR, location);
63
+ return resolve(CI_PROJECT_DIR, location)
114
64
  }
115
65
 
116
66
  /**
117
67
  * @param {string} filePath The path to the linted file.
118
68
  * @param {LintMessage} message The ESLint report message.
69
+ * @param {Set<string>} hashes Hashes already encountered. Used to avoid duplicate hashes
119
70
  * @returns {string} The fingerprint for the ESLint report message.
120
71
  */
121
- function createFingerprint(filePath, message) {
122
- const md5 = createHash('md5');
123
- md5.update(filePath);
72
+ function createFingerprint(filePath, message, hashes) {
73
+ const md5 = createHash('md5')
74
+ md5.update(filePath)
124
75
  if (message.ruleId) {
125
- md5.update(message.ruleId);
76
+ md5.update(message.ruleId)
126
77
  }
127
- md5.update(message.message);
128
- return md5.digest('hex');
78
+ md5.update(message.message)
79
+
80
+ // Create copy of hash since md5.digest() will finalize it, not allowing us to .update() again
81
+ let md5Tmp = md5.copy()
82
+ let hash = md5Tmp.digest('hex')
83
+
84
+ while (hashes.has(hash)) {
85
+ // Hash collision. This happens if we encounter the same ESLint message in one file
86
+ // multiple times. Keep generating new hashes until we get a unique one.
87
+ md5.update(hash)
88
+
89
+ md5Tmp = md5.copy()
90
+ hash = md5Tmp.digest('hex')
91
+ }
92
+
93
+ hashes.add(hash)
94
+ return hash
129
95
  }
130
96
 
131
97
  /**
132
98
  * @param {LintResult[]} results The ESLint report results.
133
99
  * @param {LintResultData} data The ESLint report result data.
134
- * @returns {CodeClimateIssue[]} The ESLint messages in the form of a GitLab code quality report.
100
+ * @returns {Issue[]} The ESLint messages in the form of a GitLab code quality report.
135
101
  */
136
102
  function convert(results, data) {
137
- /** @type {CodeClimateIssue[]} */
138
- const messages = [];
103
+ /** @type {Issue[]} */
104
+ const messages = []
105
+
106
+ /** @type {Set<string>} */
107
+ const hashes = new Set()
108
+
139
109
  for (const result of results) {
140
- for (const message of result.messages) {
141
- const relativePath = relative(CI_PROJECT_DIR, result.filePath);
110
+ const relativePath = relative(CI_PROJECT_DIR, result.filePath)
142
111
 
143
- /** @type {CodeClimateIssue} */
112
+ for (const message of result.messages) {
113
+ /** @type {Issue} */
144
114
  const issue = {
145
115
  type: 'issue',
116
+ categories: ['Style'],
146
117
  check_name: message.ruleId ?? '',
147
118
  description: message.message,
148
- severity: message.severity === 2 ? 'major' : 'minor',
149
- fingerprint: createFingerprint(relativePath, message),
119
+ severity: message.fatal ? 'critical' : message.severity === 2 ? 'major' : 'minor',
120
+ fingerprint: createFingerprint(relativePath, message, hashes),
150
121
  location: {
151
122
  path: relativePath,
152
123
  lines: {
153
124
  begin: message.line,
154
- end: message.endLine ?? message.line,
155
- },
156
- },
157
- };
158
- const docs = message.ruleId ? data.rulesMeta[message.ruleId]?.docs : undefined;
159
- if (docs) {
160
- let body = docs.description || '';
161
- if (docs.url) {
162
- if (body) {
163
- body += '\n\n';
125
+ end: message.endLine ?? message.line
164
126
  }
165
- body += `[${message.ruleId}](${docs.url})`;
166
127
  }
167
- if (body) {
168
- issue.contents = { body };
128
+ }
129
+ if (message.ruleId && message.ruleId in data.rulesMeta) {
130
+ const { docs, type } = data.rulesMeta[message.ruleId]
131
+ if (type === 'problem') {
132
+ issue.categories.unshift('Bug Risk')
133
+ }
134
+
135
+ if (docs) {
136
+ let body = docs.description || ''
137
+ if (docs.url) {
138
+ if (body) {
139
+ body += '\n\n'
140
+ }
141
+ body += `[${message.ruleId}](${docs.url})`
142
+ }
143
+ if (body) {
144
+ issue.content = { body }
145
+ }
169
146
  }
170
147
  }
171
- messages.push(issue);
148
+ messages.push(issue)
172
149
  }
173
150
  }
174
- return messages;
175
- }
176
-
177
- /**
178
- * @param {LintMessage} message The ESLint report message.
179
- * @returns {boolean} `true` if the message is at error level, `false` if it represents a warning
180
- */
181
- function messageIsLevelError(message) {
182
- return message.fatal || message.severity === 2;
151
+ return messages
183
152
  }
184
153
 
185
154
  /**
@@ -190,76 +159,80 @@ function messageIsLevelError(message) {
190
159
  * @returns {string} The formatted text.
191
160
  */
192
161
  function plural(count, text) {
193
- return `${count} ${text}${count === 1 ? '' : 's'}`;
162
+ return `${count} ${text}${count === 1 ? '' : 's'}`
194
163
  }
195
164
 
196
165
  /**
197
166
  * @param {LintResult[]} results The ESLint report results.
198
167
  * @returns {string} The ESLint messages converted to a format
199
- * suitable as output in GitLab CI job logs.
168
+ * suitable as output in GitLab CI job logs.
200
169
  */
201
170
  function gitlabConsoleFormatter(results) {
202
171
  // Severity labels manually padded to have equal lengths and end with spaces
203
- const labelError = `${chalk.red('error')} `;
204
- const labelWarn = `${chalk.yellow('warn')} `;
172
+ const labelFatal = `${chalk.magenta('fatal')} `
173
+ const labelError = `${chalk.red('error')} `
174
+ const labelWarn = `${chalk.yellow('warn')} `
205
175
 
206
- const lines = [''];
176
+ const lines = ['']
207
177
 
208
178
  /** @type {string | undefined} */
209
- let gitLabBaseURL;
179
+ let gitLabBaseURL
210
180
  if (CI_PROJECT_URL && CI_COMMIT_SHORT_SHA) {
211
- gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`;
181
+ gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`
212
182
  }
213
183
 
214
- let errors = 0;
215
- let warnings = 0;
216
- let maxRuleIdLength = 0;
217
- let maxMsgLength = 0;
184
+ let fatal = 0
185
+ let errors = 0
186
+ let warnings = 0
187
+ let maxRuleIdLength = 0
188
+ let maxMsgLength = 0
218
189
 
219
190
  for (const result of results) {
191
+ fatal += result.fatalErrorCount
192
+ errors += result.errorCount - result.fatalErrorCount
193
+ warnings += result.warningCount
220
194
  for (const message of result.messages) {
221
- const isError = messageIsLevelError(message);
222
- errors += isError ? 1 : 0;
223
- warnings += isError ? 0 : 1;
224
- maxRuleIdLength = message.ruleId
225
- ? Math.max(maxRuleIdLength, message.ruleId.length)
226
- : maxRuleIdLength;
227
- maxMsgLength = Math.max(maxMsgLength, message.message.length);
195
+ if (message.ruleId) {
196
+ maxRuleIdLength = Math.max(maxRuleIdLength, message.ruleId.length)
197
+ }
198
+ maxMsgLength = Math.max(maxMsgLength, message.message.length)
228
199
  }
229
200
  }
230
201
 
231
202
  for (const result of results) {
232
- const { filePath, messages } = result;
233
- const repoFilePath = relative(CI_PROJECT_DIR, filePath);
203
+ const { filePath, messages } = result
204
+ const repoFilePath = relative(CI_PROJECT_DIR, filePath)
234
205
 
235
206
  for (const message of messages) {
236
- let line;
237
- line = messageIsLevelError(message) ? labelError : labelWarn;
238
- line += String(message.ruleId || '').padEnd(maxRuleIdLength + 2);
239
- line += message.message.padEnd(maxMsgLength + 2);
207
+ let line = message.fatal ? labelFatal : message.severity === 1 ? labelWarn : labelError
208
+ line += String(message.ruleId || '').padEnd(maxRuleIdLength + 2)
209
+ line += message.message.padEnd(maxMsgLength + 2)
240
210
 
241
211
  if (gitLabBaseURL) {
242
212
  // Create link to referenced file in GitLab
243
- const anchor = message.line === undefined ? '' : `#L${message.line}`;
244
- line += chalk.blue(`${gitLabBaseURL}${repoFilePath}${anchor}`);
213
+ let anchor = `#L${message.line}`
214
+ if (message.endLine != null && message.endLine !== message.line) {
215
+ anchor += `-${message.endLine}`
216
+ }
217
+ line += chalk.blue(`${gitLabBaseURL}${repoFilePath}${anchor}`)
245
218
  } else {
246
- line += `${filePath}:${message.line || 0}:${message.column || 0}`;
219
+ line += `${filePath}:${message.line}:${message.column}`
247
220
  }
248
221
 
249
- lines.push(line);
222
+ lines.push(line)
250
223
  }
251
224
  }
252
225
 
253
- const total = warnings + errors;
226
+ const total = warnings + errors + fatal
254
227
  if (total > 0) {
255
- const details = `(${plural(errors, 'error')}, ${plural(warnings, 'warning')})`;
256
- lines.push('', `${chalk.red('✖')} ${plural(total, 'problem')} ${details}`);
228
+ const details = `(${fatal} fatal, ${plural(errors, 'error')}, ${plural(warnings, 'warning')})`
229
+ lines.push('', `${chalk.red('✖')} ${plural(total, 'problem')} ${details}`)
257
230
  } else {
258
- lines.push(`${chalk.green('✔')} No problems found`);
231
+ lines.push(`${chalk.green('✔')} No problems found`)
259
232
  }
260
233
 
261
- lines.push('');
262
- return lines.join(EOL);
234
+ lines.push('')
235
+ return lines.join(EOL)
263
236
  }
264
237
 
265
238
  /**
@@ -267,17 +240,19 @@ function gitlabConsoleFormatter(results) {
267
240
  * @param {LintResultData} data The ESLint report result data.
268
241
  */
269
242
  module.exports = (results, data) => {
270
- /* istanbul ignore next */
271
- if (GITLAB_CI === 'true' && NODE_ENV !== 'test') {
272
- chalk.level = 1;
243
+ /* c8 ignore start */
244
+ if (GITLAB_CI === 'true') {
245
+ chalk.level = 1
273
246
  }
247
+
248
+ /* c8 ignore stop */
274
249
  if (CI_JOB_NAME || ESLINT_CODE_QUALITY_REPORT) {
275
- const issues = convert(results, data);
276
- const outputPath = ESLINT_CODE_QUALITY_REPORT || getOutputPath();
277
- const dir = dirname(outputPath);
278
- mkdirSync(dir, { recursive: true });
279
- writeFileSync(outputPath, JSON.stringify(issues, null, 2));
250
+ const issues = convert(results, data)
251
+ const outputPath = ESLINT_CODE_QUALITY_REPORT || getOutputPath()
252
+ const dir = dirname(outputPath)
253
+ mkdirSync(dir, { recursive: true })
254
+ writeFileSync(outputPath, JSON.stringify(issues, null, 2))
280
255
  }
281
256
 
282
- return gitlabConsoleFormatter(results);
283
- };
257
+ return gitlabConsoleFormatter(results)
258
+ }
package/package.json CHANGED
@@ -1,24 +1,19 @@
1
1
  {
2
2
  "name": "eslint-formatter-gitlab",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Show ESLint results directly in the GitLab code quality results",
5
5
  "author": "Remco Haszing <remcohaszing@gmail.com>",
6
6
  "license": "MIT",
7
7
  "homepage": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab.git"
11
- },
8
+ "repository": "gitlab:remcohaszing/eslint-formatter-gitlab",
12
9
  "funding": "https://github.com/sponsors/remcohaszing",
13
- "bugs": {
14
- "url": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab.git/issues"
15
- },
10
+ "bugs": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab/-/issues",
16
11
  "exports": "./index.js",
17
12
  "files": [
18
13
  "index.js"
19
14
  ],
20
15
  "scripts": {
21
- "test": "jest --coverage"
16
+ "test": "c8 node --test --test-reporter @reporters/junit --test-reporter-destination=junit.xml --test-reporter spec --test-reporter-destination stdout"
22
17
  },
23
18
  "keywords": [
24
19
  "eslint",
@@ -27,9 +22,6 @@
27
22
  "gitlab",
28
23
  "gitlab-ci"
29
24
  ],
30
- "engines": {
31
- "node": ">=14.0.0"
32
- },
33
25
  "dependencies": {
34
26
  "chalk": "^4.0.0",
35
27
  "yaml": "^2.0.0"
@@ -38,17 +30,16 @@
38
30
  "eslint": "^5 || ^6 || ^7 || ^8"
39
31
  },
40
32
  "devDependencies": {
33
+ "@reporters/junit": "^1.0.0",
41
34
  "@types/eslint": "^8.0.0",
42
- "@types/jest": "^29.0.0",
43
- "@types/node": "^18.0.0",
35
+ "@types/node": "^20.0.0",
36
+ "c8": "^8.0.0",
37
+ "codeclimate-types": "^0.3.0",
44
38
  "eslint": "^8.0.0",
45
- "eslint-config-remcohaszing": "^7.0.0",
46
- "eslint-plugin-jest": "^27.0.0",
47
- "eslint-plugin-jest-formatting": "^3.0.0",
48
- "jest": "^29.0.0",
49
- "jest-junit": "^14.0.0",
50
- "memfs": "^3.0.0",
39
+ "eslint-config-remcohaszing": "^9.0.0",
51
40
  "prettier": "^2.0.0",
52
- "typescript": "^4.0.0"
41
+ "remark-cli": "^11.0.0",
42
+ "remark-preset-remcohaszing": "^1.0.0",
43
+ "typescript": "^5.0.0"
53
44
  }
54
45
  }