eslint-formatter-gitlab 3.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 +28 -21
  3. package/index.js +179 -110
  4. package/package.json +18 -27
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
@@ -1,10 +1,21 @@
1
1
  # ESLint Formatter for GitLab
2
2
 
3
- > Show ESLint results directly in the [GitLab code quality] results
3
+ Show ESLint results directly in the
4
+ [GitLab code quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html)
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)
4
15
 
5
16
  ## Requirements
6
17
 
7
- This requires at least GitLab Bronze or Starter 11.5 and at least ESLint 5.
18
+ This package requires at least Node.js 18 and ESLint 5.
8
19
 
9
20
  ## Installation
10
21
 
@@ -14,13 +25,15 @@ Install `eslint` and `eslint-formatter-gitlab` using your package manager.
14
25
  npm install --save-dev eslint eslint-formatter-gitlab
15
26
  ```
16
27
 
28
+ ## Usage
29
+
17
30
  Define a GitLab job to run `eslint`.
18
31
 
19
32
  _.gitlab-ci.yml_:
20
33
 
21
34
  ```yaml
22
35
  eslint:
23
- image: node:14-alpine
36
+ image: node:20-alpine
24
37
  script:
25
38
  - npm ci
26
39
  - npx eslint --format gitlab .
@@ -29,28 +42,22 @@ eslint:
29
42
  codequality: gl-codequality.json
30
43
  ```
31
44
 
32
- The formatter will automatically detect a GitLab CI environment. It will detect where to output the
33
- 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.
34
47
 
35
48
  ## Example
36
49
 
37
- An example of the results can be seen in [Merge Request !1] of `eslint-formatter-gitlab` itself.
38
-
39
- ## Configuration Options
40
-
41
- ESLint formatters don’t take any configuration options. In order to still allow some way of
42
- configuration, options are passed using environment variables.
43
-
44
- | Environment Variable | Description |
45
- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
- | `ESLINT_CODE_QUALITY_REPORT` | The location to store the code quality report. By default it will detect the location of the codequality artifact defined in the GitLab CI configuration file. |
50
+ An example of the results can be seen in
51
+ [Merge Request !1](https://gitlab.com/remcohaszing/eslint-formatter-gitlab/merge_requests/1) of
52
+ `eslint-formatter-gitlab` itself.
47
53
 
48
- ## Upgrading
54
+ ## Configuration
49
55
 
50
- ### to v3
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.
51
60
 
52
- - Support for the environment variable `ESLINT_FORMATTER` has been removed, console output now
53
- always uses a builtin formatter.
61
+ ## License
54
62
 
55
- [gitlab code quality]: https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html
56
- [merge request !1]: https://gitlab.com/remcohaszing/eslint-formatter-gitlab/merge_requests/1
63
+ [MIT](LICENSE.md) © [Remco Haszing](https://gitlab.com/remcohaszing)
package/index.js CHANGED
@@ -1,189 +1,258 @@
1
- const { createHash } = require('crypto');
2
- const { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
3
- const { EOL } = require('os');
4
- const { dirname, join, relative, resolve } = require('path');
1
+ /**
2
+ * @typedef {import('eslint').ESLint.LintResult} LintResult
3
+ * @typedef {import('eslint').ESLint.LintResultData} LintResultData
4
+ * @typedef {import('eslint').Linter.LintMessage} LintMessage
5
+ * @typedef {import('codeclimate-types').Issue} Issue
6
+ */
7
+
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')
5
12
 
6
- // eslint-disable-next-line unicorn/import-style
7
- const chalk = require('chalk');
8
- const yaml = require('js-yaml');
13
+ const chalk = require('chalk')
14
+ const yaml = require('yaml')
9
15
 
10
16
  const {
17
+ CI_COMMIT_SHORT_SHA,
11
18
  CI_CONFIG_PATH = '.gitlab-ci.yml',
12
19
  CI_JOB_NAME,
13
20
  CI_PROJECT_DIR = process.cwd(),
14
21
  CI_PROJECT_URL,
15
- CI_COMMIT_SHORT_SHA,
16
22
  ESLINT_CODE_QUALITY_REPORT,
17
- GITLAB_CI,
18
- NODE_ENV,
19
- } = process.env;
23
+ GITLAB_CI
24
+ } = process.env
25
+
26
+ /**
27
+ * @type {yaml.CollectionTag}
28
+ */
29
+ const reference = {
30
+ tag: '!reference',
31
+ collection: 'seq',
32
+ default: false,
33
+ resolve() {
34
+ // We only allow the syntax. We don’t actually resolve the reference.
35
+ }
36
+ }
20
37
 
21
38
  /**
22
39
  * @returns {string} The output path of the code quality artifact.
23
40
  */
24
41
  function getOutputPath() {
25
- const configPath = join(CI_PROJECT_DIR, CI_CONFIG_PATH);
42
+ const configPath = join(CI_PROJECT_DIR, CI_CONFIG_PATH)
26
43
  // GitlabCI allows a custom configuration path which can be a URL or a path relative to another
27
44
  // project. In these cases CI_CONFIG_PATH is empty and we'll have to require the user provide
28
45
  // ESLINT_CODE_QUALITY_REPORT.
29
46
  if (!existsSync(configPath) || !lstatSync(configPath).isFile()) {
30
47
  throw new Error(
31
48
  'Could not resolve .gitlab-ci.yml to automatically detect report artifact path.' +
32
- ' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.',
33
- );
34
- }
35
- const jobs = yaml.load(readFileSync(configPath, 'utf-8'));
36
- const { artifacts } = jobs[CI_JOB_NAME];
37
- const location = artifacts && artifacts.reports && artifacts.reports.codequality;
38
- const msg = `Expected ${CI_JOB_NAME}.artifacts.reports.codequality to be one exact path`;
39
- if (!location) {
40
- throw new Error(`${msg}, but no value was found.`);
49
+ ' Please manually provide a path via the ESLINT_CODE_QUALITY_REPORT variable.'
50
+ )
41
51
  }
42
- if (Array.isArray(location)) {
43
- 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
+ )
44
62
  }
45
- return resolve(CI_PROJECT_DIR, location);
63
+ return resolve(CI_PROJECT_DIR, location)
46
64
  }
47
65
 
48
66
  /**
49
- * @param {string} filePath - The path to the linted file.
50
- * @param {object} message - The ESLint report message.
67
+ * @param {string} filePath The path to the linted file.
68
+ * @param {LintMessage} message The ESLint report message.
69
+ * @param {Set<string>} hashes Hashes already encountered. Used to avoid duplicate hashes
51
70
  * @returns {string} The fingerprint for the ESLint report message.
52
71
  */
53
- function createFingerprint(filePath, message) {
54
- const md5 = createHash('md5');
55
- md5.update(filePath);
72
+ function createFingerprint(filePath, message, hashes) {
73
+ const md5 = createHash('md5')
74
+ md5.update(filePath)
56
75
  if (message.ruleId) {
57
- md5.update(message.ruleId);
76
+ md5.update(message.ruleId)
58
77
  }
59
- md5.update(message.message);
60
- 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
61
95
  }
62
96
 
63
97
  /**
64
- * @param {object[]} results - The ESLint report results.
65
- * @returns {object[]} The ESLint messages in the form of a GitLab code quality report.
98
+ * @param {LintResult[]} results The ESLint report results.
99
+ * @param {LintResultData} data The ESLint report result data.
100
+ * @returns {Issue[]} The ESLint messages in the form of a GitLab code quality report.
66
101
  */
67
- function convert(results) {
68
- const messages = [];
102
+ function convert(results, data) {
103
+ /** @type {Issue[]} */
104
+ const messages = []
105
+
106
+ /** @type {Set<string>} */
107
+ const hashes = new Set()
108
+
69
109
  for (const result of results) {
110
+ const relativePath = relative(CI_PROJECT_DIR, result.filePath)
111
+
70
112
  for (const message of result.messages) {
71
- const relativePath = relative(CI_PROJECT_DIR, result.filePath);
72
- // https://github.com/codeclimate/spec/blob/master/SPEC.md#data-types
73
- messages.push({
113
+ /** @type {Issue} */
114
+ const issue = {
115
+ type: 'issue',
116
+ categories: ['Style'],
117
+ check_name: message.ruleId ?? '',
74
118
  description: message.message,
75
- severity: message.severity === 2 ? 'major' : 'minor',
76
- fingerprint: createFingerprint(relativePath, message),
119
+ severity: message.fatal ? 'critical' : message.severity === 2 ? 'major' : 'minor',
120
+ fingerprint: createFingerprint(relativePath, message, hashes),
77
121
  location: {
78
122
  path: relativePath,
79
123
  lines: {
80
124
  begin: message.line,
81
- },
82
- },
83
- });
125
+ end: message.endLine ?? message.line
126
+ }
127
+ }
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
+ }
146
+ }
147
+ }
148
+ messages.push(issue)
84
149
  }
85
150
  }
86
- return messages;
151
+ return messages
87
152
  }
88
153
 
89
154
  /**
90
- * @param {object} message - The ESLint report message.
91
- * @returns {boolean} `true` if the message is at error level, `false` if it represents a warning
155
+ * Make a text singular or plural based on the count.
156
+ *
157
+ * @param {number} count The count of the data.
158
+ * @param {string} text The text to make singular or plural.
159
+ * @returns {string} The formatted text.
92
160
  */
93
- function messageIsLevelError(message) {
94
- return message.fatal || message.severity === 2;
161
+ function plural(count, text) {
162
+ return `${count} ${text}${count === 1 ? '' : 's'}`
95
163
  }
96
164
 
97
165
  /**
98
- * @param {object[]} results - The ESLint report results.
99
- * @returns {object} Statistics about the number of problems at various levels
100
- * and length of contained description strings.
101
- */
102
- function calculateResultsStats(results) {
103
- const stats = { total: 0, errors: 0, warnings: 0, maxRuleIdLength: 0, maxMsgLength: 0 };
104
-
105
- for (const result of results) {
106
- for (const message of result.messages) {
107
- const isError = messageIsLevelError(message);
108
- stats.errors += isError ? 1 : 0;
109
- stats.warnings += isError ? 0 : 1;
110
- stats.maxRuleIdLength = message.ruleId
111
- ? Math.max(stats.maxRuleIdLength, message.ruleId.length)
112
- : stats.maxRuleIdLength;
113
- stats.maxMsgLength = Math.max(stats.maxMsgLength, message.message.length);
114
- }
115
- }
116
-
117
- stats.total = stats.warnings + stats.errors;
118
-
119
- return stats;
120
- }
121
-
122
- const plural = (count, text) => `${count} ${text}${count === 1 ? '' : 's'}`;
123
-
124
- /**
125
- * @param {object[]} results - The ESLint report results.
166
+ * @param {LintResult[]} results The ESLint report results.
126
167
  * @returns {string} The ESLint messages converted to a format
127
- * suitable as output in GitLab CI job logs.
168
+ * suitable as output in GitLab CI job logs.
128
169
  */
129
170
  function gitlabConsoleFormatter(results) {
130
171
  // Severity labels manually padded to have equal lengths and end with spaces
131
- const labelError = `${chalk.red('error')} `;
132
- const labelWarn = `${chalk.yellow('warn')} `;
172
+ const labelFatal = `${chalk.magenta('fatal')} `
173
+ const labelError = `${chalk.red('error')} `
174
+ const labelWarn = `${chalk.yellow('warn')} `
133
175
 
134
- const lines = [''];
176
+ const lines = ['']
135
177
 
136
- let gitLabBaseURL;
178
+ /** @type {string | undefined} */
179
+ let gitLabBaseURL
137
180
  if (CI_PROJECT_URL && CI_COMMIT_SHORT_SHA) {
138
- gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`;
181
+ gitLabBaseURL = `${CI_PROJECT_URL}/-/blob/${CI_COMMIT_SHORT_SHA}/`
139
182
  }
140
183
 
141
- const stats = calculateResultsStats(results);
184
+ let fatal = 0
185
+ let errors = 0
186
+ let warnings = 0
187
+ let maxRuleIdLength = 0
188
+ let maxMsgLength = 0
142
189
 
143
190
  for (const result of results) {
144
- const { filePath, messages } = result;
145
- const repoFilePath = relative(CI_PROJECT_DIR, filePath);
191
+ fatal += result.fatalErrorCount
192
+ errors += result.errorCount - result.fatalErrorCount
193
+ warnings += result.warningCount
194
+ for (const message of result.messages) {
195
+ if (message.ruleId) {
196
+ maxRuleIdLength = Math.max(maxRuleIdLength, message.ruleId.length)
197
+ }
198
+ maxMsgLength = Math.max(maxMsgLength, message.message.length)
199
+ }
200
+ }
201
+
202
+ for (const result of results) {
203
+ const { filePath, messages } = result
204
+ const repoFilePath = relative(CI_PROJECT_DIR, filePath)
146
205
 
147
206
  for (const message of messages) {
148
- let line;
149
- line = messageIsLevelError(message) ? labelError : labelWarn;
150
- line += String(message.ruleId ? message.ruleId : '').padEnd(stats.maxRuleIdLength + 2);
151
- line += message.message.padEnd(stats.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)
152
210
 
153
211
  if (gitLabBaseURL) {
154
212
  // Create link to referenced file in GitLab
155
- const anchor = message.line === undefined ? '' : `#L${message.line}`;
156
- 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}`)
157
218
  } else {
158
- line += `${filePath}:${message.line || 0}:${message.column || 0}`;
219
+ line += `${filePath}:${message.line}:${message.column}`
159
220
  }
160
221
 
161
- lines.push(line);
222
+ lines.push(line)
162
223
  }
163
224
  }
164
225
 
165
- if (stats.total > 0) {
166
- const details = `(${plural(stats.errors, 'error')}, ${plural(stats.warnings, 'warning')})`;
167
- lines.push('', `${chalk.red('')} ${plural(stats.total, 'problem')} ${details}`);
226
+ const total = warnings + errors + fatal
227
+ if (total > 0) {
228
+ const details = `(${fatal} fatal, ${plural(errors, 'error')}, ${plural(warnings, 'warning')})`
229
+ lines.push('', `${chalk.red('✖')} ${plural(total, 'problem')} ${details}`)
168
230
  } else {
169
- lines.push(`${chalk.green('✔')} No problems found`);
231
+ lines.push(`${chalk.green('✔')} No problems found`)
170
232
  }
171
233
 
172
- lines.push('');
173
- return lines.join(EOL);
234
+ lines.push('')
235
+ return lines.join(EOL)
174
236
  }
175
237
 
176
- module.exports = (results) => {
177
- if (GITLAB_CI === 'true' && NODE_ENV !== 'test') {
178
- chalk.level = 1;
238
+ /**
239
+ * @param {LintResult[]} results The ESLint report results.
240
+ * @param {LintResultData} data The ESLint report result data.
241
+ */
242
+ module.exports = (results, data) => {
243
+ /* c8 ignore start */
244
+ if (GITLAB_CI === 'true') {
245
+ chalk.level = 1
179
246
  }
247
+
248
+ /* c8 ignore stop */
180
249
  if (CI_JOB_NAME || ESLINT_CODE_QUALITY_REPORT) {
181
- const data = convert(results);
182
- const outputPath = ESLINT_CODE_QUALITY_REPORT || getOutputPath();
183
- const dir = dirname(outputPath);
184
- mkdirSync(dir, { recursive: true });
185
- writeFileSync(outputPath, JSON.stringify(data, 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))
186
255
  }
187
256
 
188
- return gitlabConsoleFormatter(results);
189
- };
257
+ return gitlabConsoleFormatter(results)
258
+ }
package/package.json CHANGED
@@ -1,22 +1,19 @@
1
1
  {
2
2
  "name": "eslint-formatter-gitlab",
3
- "version": "3.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
- },
12
- "bugs": {
13
- "url": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab.git/issues"
14
- },
8
+ "repository": "gitlab:remcohaszing/eslint-formatter-gitlab",
9
+ "funding": "https://github.com/sponsors/remcohaszing",
10
+ "bugs": "https://gitlab.com/remcohaszing/eslint-formatter-gitlab/-/issues",
11
+ "exports": "./index.js",
15
12
  "files": [
16
13
  "index.js"
17
14
  ],
18
15
  "scripts": {
19
- "test": "jest"
16
+ "test": "c8 node --test --test-reporter @reporters/junit --test-reporter-destination=junit.xml --test-reporter spec --test-reporter-destination stdout"
20
17
  },
21
18
  "keywords": [
22
19
  "eslint",
@@ -27,28 +24,22 @@
27
24
  ],
28
25
  "dependencies": {
29
26
  "chalk": "^4.0.0",
30
- "js-yaml": "^4.0.0"
27
+ "yaml": "^2.0.0"
31
28
  },
32
29
  "peerDependencies": {
33
30
  "eslint": "^5 || ^6 || ^7 || ^8"
34
31
  },
35
32
  "devDependencies": {
36
- "codecov": "^3.0.0",
37
- "eslint": "^7.0.0",
38
- "eslint-config-remcohaszing": "^3.0.0",
39
- "eslint-plugin-eslint-comments": "^3.0.0",
40
- "eslint-plugin-import": "^2.0.0",
41
- "eslint-plugin-jest": "^24.0.0",
42
- "eslint-plugin-jest-formatting": "^3.0.0",
43
- "eslint-plugin-jsdoc": "^36.0.0",
44
- "eslint-plugin-markdown": "^2.0.0",
45
- "eslint-plugin-node": "^11.0.0",
46
- "eslint-plugin-prettier": "^4.0.0",
47
- "eslint-plugin-sort-destructure-keys": "^1.0.0",
48
- "eslint-plugin-unicorn": "^36.0.0",
49
- "jest": "^27.0.0",
50
- "jest-junit": "^13.0.0",
51
- "memfs": "^3.0.0",
52
- "prettier": "^2.0.0"
33
+ "@reporters/junit": "^1.0.0",
34
+ "@types/eslint": "^8.0.0",
35
+ "@types/node": "^20.0.0",
36
+ "c8": "^8.0.0",
37
+ "codeclimate-types": "^0.3.0",
38
+ "eslint": "^8.0.0",
39
+ "eslint-config-remcohaszing": "^9.0.0",
40
+ "prettier": "^2.0.0",
41
+ "remark-cli": "^11.0.0",
42
+ "remark-preset-remcohaszing": "^1.0.0",
43
+ "typescript": "^5.0.0"
53
44
  }
54
45
  }