From a6270c14651e618a075fed312f75d2e2510f8dba Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Fri, 8 Nov 2024 10:47:59 +0100 Subject: [PATCH 01/16] chore: change markdownlint scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5db50bd..d3b6314 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "lint": "npm run eslint && npm run markdownlint", "eslint": "eslint .", "eslint:fix": "eslint . --fix", - "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#CHANGELOG.md\"", - "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\"", + "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#**/node_modules\" \"#CHANGELOG.md\"", + "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\" \"#**/node_modules\"", "markdownlint:fix-changelog": "markdownlint-cli2 --fix \"CHANGELOG.md\"", "test": "ava --verbose", "test:coverage": "rimraf coverage && mkdir -p coverage && c8 ava --tap | tap-xunit --package='dclint' > ./coverage/junit.xml", From 74097e4a7660319553ef159a03ef8e98ebfe7c77 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Fri, 8 Nov 2024 10:59:51 +0100 Subject: [PATCH 02/16] feat(cli): add `--max-warnings` option to enforce warning threshold BREAKING CHANGES: Process now exits with code 0 when only warnings are present. --- docs/cli.md | 10 ++++++++++ src/cli/cli.ts | 22 ++++++++++++++++++---- src/cli/cli.types.ts | 1 + 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 85cee01..a7ed7a3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -86,6 +86,16 @@ Below is a detailed explanation of each CLI option available in DCLint. - **Use Case**: This option is helpful when you only care about critical issues (errors) and want to suppress warnings from the output. +### `--max-warnings` + +- **Description**: Specifies the maximum number of allowed warnings before triggering a nonzero exit code. If the number + of warnings exceeds this limit, the command exits with a failure status. Note that any errors will also cause a + nonzero exit code, regardless of this setting. +- **Type**: `number` +- **Default**: `-1` (disables warning limit) +- **Use Case**: This option is useful for enforcing a stricter threshold on warnings. It can be applied when you want to + fail the command only if warnings reach a certain level, allowing for flexibility in handling non-critical issues. + ### `-o, --output-file` - **Description**: Specifies a file to write the linting report to. diff --git a/src/cli/cli.ts b/src/cli/cli.ts index ceacc66..fbe4f58 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -78,6 +78,11 @@ const { argv } = yargsLib(hideBin(process.argv)) description: 'Files or directories to exclude from the search', default: [], }) + .option('max-warnings', { + type: 'number', + description: 'Number of warnings to trigger nonzero exit code', + default: -1, + }) .help() .alias('version', 'v'); @@ -128,6 +133,10 @@ export default async function cli() { .filter((result) => result.messages.length > 0); } + // Count errors and warnings + const totalErrors = lintResults.reduce((count, result) => count + result.errorCount, 0); + const totalWarnings = lintResults.reduce((count, result) => count + result.warningCount, 0); + // Choose and apply the formatter const formatter = await loadFormatter(args.formatter); const formattedResults = formatter(lintResults); @@ -139,10 +148,15 @@ export default async function cli() { console.log(formattedResults); } - const isValid = lintResults.filter((result) => result.messages.length > 0).length === 0; - - if (!isValid) { - logger.debug(LOG_SOURCE.CLI, `${lintResults.length} errors found`); + // Determine exit code based on errors and warnings + if (totalErrors > 0) { + logger.debug(LOG_SOURCE.CLI, `${totalErrors} errors found`); + process.exit(1); + } else if (args.maxWarnings && args.maxWarnings >= 0 && totalWarnings > args.maxWarnings) { + logger.debug( + LOG_SOURCE.CLI, + `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${args.maxWarnings})`, + ); process.exit(1); } diff --git a/src/cli/cli.types.ts b/src/cli/cli.types.ts index 5da1b08..256050e 100644 --- a/src/cli/cli.types.ts +++ b/src/cli/cli.types.ts @@ -6,6 +6,7 @@ interface CLIConfig { formatter: string; config?: string; quiet: boolean; + maxWarnings?: number; outputFile?: string; color: boolean; debug: boolean; From 4b296fb78d461d94a955b096c921f54354405c6b Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Sun, 10 Nov 2024 14:49:55 +0100 Subject: [PATCH 03/16] feat(no-build-and-image-rule): add `checkPullPolicy` option to allow simultaneous usage of `build` and `image` --- docs/rules/no-build-and-image-rule.md | 19 +++++-- src/rules/no-build-and-image-rule.ts | 16 +++++- tests/rules/no-build-and-image-rule.spec.ts | 60 ++++++++++++++++++++- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/docs/rules/no-build-and-image-rule.md b/docs/rules/no-build-and-image-rule.md index bb7ad3f..2997e47 100644 --- a/docs/rules/no-build-and-image-rule.md +++ b/docs/rules/no-build-and-image-rule.md @@ -1,9 +1,9 @@ # No Build and Image Rule -Ensures that each service in a Docker Compose configuration uses either build or image, but not both. Using both +Ensures that each service in a Docker Compose configuration uses either `build` or `image`, but not both. Using both directives can cause ambiguity and unpredictable behavior during container creation. -This rule is not fixable, as it requires user intervention to decide whether to keep build or image. +This rule is not fixable, as it requires user intervention to decide whether to keep `build` or `image`. - **Rule Name:** no-build-and-image - **Type:** error @@ -11,6 +11,18 @@ This rule is not fixable, as it requires user intervention to decide whether to - **Severity:** major - **Fixable:** false +## Options + +- **checkPullPolicy** (boolean): Controls whether the rule should allow the simultaneous use of `build` and `image` if + the service also specifies `pull_policy` (Default `true`). + + **Behavior:** + + - If `checkPullPolicy` is `true`, the rule permits both `build` and `image` to be used together, but only if + `pull_policy` is present in the service definition. + - If `checkPullPolicy` is `false`, the rule enforces that each service uses either `build` or `image` exclusively, + regardless of the presence of `pull_policy`. + ## Problematic Code Example ```yaml @@ -50,7 +62,8 @@ If `pull_policy` is missing from the service definition, Compose attempts to pul source if the image isn't found in the registry or platform cache. Using both directives for the same service can lead to ambiguity and unexpected behavior during the build and deployment -process. Therefore, this rule enforces that each service should only use one of these directives. +process. Therefore, this rule enforces that each service should only use one of these directives unless +`checkPullPolicy` allows both. ## Version diff --git a/src/rules/no-build-and-image-rule.ts b/src/rules/no-build-and-image-rule.ts index bca63ac..27f4d51 100644 --- a/src/rules/no-build-and-image-rule.ts +++ b/src/rules/no-build-and-image-rule.ts @@ -10,6 +10,10 @@ import type { } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; +interface NoBuildAndImageRuleOptions { + checkPullPolicy?: boolean; +} + export default class NoBuildAndImageRule implements LintRule { public name = 'no-build-and-image'; @@ -27,6 +31,12 @@ export default class NoBuildAndImageRule implements LintRule { public fixable: boolean = false; + private readonly checkPullPolicy: boolean; + + constructor(options?: NoBuildAndImageRuleOptions) { + this.checkPullPolicy = options?.checkPullPolicy ?? true; + } + // eslint-disable-next-line class-methods-use-this public getMessage({ serviceName }: { serviceName: string }): string { return `Service "${serviceName}" is using both "build" and "image". Use either "build" or "image" but not both.`; @@ -47,7 +57,11 @@ export default class NoBuildAndImageRule implements LintRule { if (!isMap(service)) return; - if (service.has('build') && service.has('image')) { + const hasBuild = service.has('build'); + const hasImage = service.has('image'); + const hasPullPolicy = service.has('pull_policy'); + + if (hasBuild && hasImage && (!this.checkPullPolicy || !hasPullPolicy)) { const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'build'); errors.push({ rule: this.name, diff --git a/tests/rules/no-build-and-image-rule.spec.ts b/tests/rules/no-build-and-image-rule.spec.ts index 5e5d484..f6cd49b 100644 --- a/tests/rules/no-build-and-image-rule.spec.ts +++ b/tests/rules/no-build-and-image-rule.spec.ts @@ -14,6 +14,19 @@ services: image: postgres `; +// YAML with services using both build and image, including pull_policy +const yamlWithBuildImageAndPullPolicy = ` +services: + web: + build: . + image: nginx + pull_policy: always + db: + build: ./db + image: postgres + pull_policy: always +`; + // YAML with services using only build const yamlWithOnlyBuild = ` services: @@ -43,7 +56,36 @@ test('NoBuildAndImageRule: should return a warning when both "build" and "image" }; const errors = rule.check(context); - t.is(errors.length, 2, 'There should be two warnings when both "build" and "image" are used.'); + t.is( + errors.length, + 2, + 'There should be two warnings when both "build" and "image" are used and checkPullPolicy is false.', + ); + + const expectedMessages = [ + 'Service "web" is using both "build" and "image". Use either "build" or "image" but not both.', + 'Service "db" is using both "build" and "image". Use either "build" or "image" but not both.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); +}); + +test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service and checkPullPolicy is false', (t) => { + const rule = new NoBuildAndImageRule({ checkPullPolicy: false }); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithBuildImageAndPullPolicy).toJS() as Record, + sourceCode: yamlWithBuildImageAndPullPolicy, + }; + + const errors = rule.check(context); + t.is( + errors.length, + 2, + 'There should be two warnings when both "build" and "image" are used and checkPullPolicy is false.', + ); const expectedMessages = [ 'Service "web" is using both "build" and "image". Use either "build" or "image" but not both.', @@ -55,6 +97,22 @@ test('NoBuildAndImageRule: should return a warning when both "build" and "image" }); }); +test('NoBuildAndImageRule: should not return warnings when "build" and "image" are used with pull_policy and checkPullPolicy is true', (t) => { + const rule = new NoBuildAndImageRule({ checkPullPolicy: true }); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithBuildImageAndPullPolicy).toJS() as Record, + sourceCode: yamlWithBuildImageAndPullPolicy, + }; + + const errors = rule.check(context); + t.is( + errors.length, + 0, + 'There should be no warnings when "build" and "image" are used together with pull_policy and checkPullPolicy is true.', + ); +}); + test('NoBuildAndImageRule: should not return warnings when only "build" is used', (t) => { const rule = new NoBuildAndImageRule(); const context: LintContext = { From a02a4d35588f349eb8d44ae7a3a41dba05758ff4 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Tue, 1 Oct 2024 11:13:51 +0200 Subject: [PATCH 04/16] chore: move pull_request_template --- .github/{PULL_REQUEST_TEMPLATE => }/pull_request_template.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE => }/pull_request_template.md (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/pull_request_template.md From 2cf50c45c5ef26aeb962904730d1ff20210e8fbc Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Sun, 10 Nov 2024 14:57:38 +0100 Subject: [PATCH 05/16] chore: change indentation and fix linter warnings --- .eslintrc | 4 +- .markdownlint.cjs | 26 +- .prettierrc | 24 +- CHANGELOG.md | 101 ++++-- CODE_OF_CONDUCT.md | 117 +++---- CONTRIBUTING.md | 31 +- README.md | 76 +++-- SECURITY.md | 2 +- TODO.md | 113 +++---- ava.config.js | 16 +- docs/cli.md | 4 +- docs/errors.md | 4 +- docs/errors/invalid-schema.md | 10 +- docs/errors/invalid-yaml.md | 6 +- docs/formatters.md | 10 +- .../rules/no-duplicate-exported-ports-rule.md | 7 +- docs/rules/no-quotes-in-volumes-rule.md | 5 +- docs/rules/require-quotes-in-ports-rule.md | 8 +- .../service-container-name-regex-rule.md | 6 +- ...ce-dependencies-alphabetical-order-rule.md | 6 +- ...service-image-require-explicit-tag-rule.md | 4 +- docs/rules/service-keys-order-rule.md | 65 ++-- .../rules/services-alphabetical-order-rule.md | 4 +- docs/rules/top-level-properties-order-rule.md | 10 +- package.json | 7 +- release.config.js | 218 ++++++------- src/cli/cli.ts | 294 +++++++++--------- src/cli/cli.types.ts | 24 +- src/config/config.ts | 62 ++-- src/config/config.types.ts | 12 +- src/errors/compose-validation-error.ts | 30 +- src/errors/file-not-found-error.ts | 10 +- src/formatters/codeclimate.ts | 102 +++--- src/formatters/compact.ts | 16 +- src/formatters/json.ts | 2 +- src/formatters/junit.ts | 54 ++-- src/formatters/stylish.ts | 98 +++--- src/linter/linter.ts | 288 +++++++++-------- src/linter/linter.types.ts | 84 ++--- src/rules/no-build-and-image-rule.ts | 110 +++---- .../no-duplicate-container-names-rule.ts | 130 ++++---- src/rules/no-duplicate-exported-ports-rule.ts | 176 ++++++----- src/rules/no-quotes-in-volumes-rule.ts | 136 ++++---- src/rules/no-version-field-rule.ts | 90 +++--- src/rules/require-project-name-field-rule.ts | 76 ++--- src/rules/require-quotes-in-ports-rule.ts | 152 ++++----- .../service-container-name-regex-rule.ts | 102 +++--- ...ce-dependencies-alphabetical-order-rule.ts | 176 +++++------ ...service-image-require-explicit-tag-rule.ts | 172 +++++----- src/rules/service-keys-order-rule.ts | 282 +++++++++-------- .../service-ports-alphabetical-order-rule.ts | 136 ++++---- src/rules/services-alphabetical-order-rule.ts | 158 +++++----- src/rules/top-level-properties-order-rule.ts | 190 +++++------ src/util/compose-validation.ts | 56 ++-- src/util/files-finder.ts | 110 +++---- src/util/formatter-loader.ts | 78 ++--- src/util/line-finder.ts | 170 +++++----- src/util/load-schema.ts | 6 +- src/util/logger.ts | 114 +++---- src/util/rules-loader.ts | 84 ++--- src/util/service-ports-parser.ts | 68 ++-- tests/linter.spec.ts | 208 ++++++------- tests/rules/no-build-and-image-rule.spec.ts | 150 ++++----- .../no-duplicate-container-names-rule.spec.ts | 38 +-- .../no-duplicate-exported-ports-rule.spec.ts | 82 ++--- tests/rules/no-quotes-in-volumes-rule.spec.ts | 56 ++-- tests/rules/no-version-field-rule.spec.ts | 72 ++--- .../require-project-name-field-rule.spec.ts | 60 ++-- .../require-quotes-in-ports-rule.spec.ts | 92 +++--- .../service-container-name-regex-rule.spec.ts | 38 +-- ...pendencies-alphabetical-order-rule.spec.ts | 108 +++---- ...ce-image-require-explicit-tag-rule.spec.ts | 212 ++++++------- tests/rules/service-keys-order-rule.spec.ts | 64 ++-- ...vice-ports-alphabetical-order-rule.spec.ts | 40 +-- .../services-alphabetical-order-rule.spec.ts | 56 ++-- .../top-level-properties-order-rule.spec.ts | 140 ++++----- tests/util/files-finder.spec.ts | 122 ++++---- tests/util/line-finder.spec.ts | 36 +-- tests/util/service-ports-parser.spec.ts | 44 +-- 79 files changed, 3158 insertions(+), 3192 deletions(-) diff --git a/.eslintrc b/.eslintrc index db26f92..4d11206 100644 --- a/.eslintrc +++ b/.eslintrc @@ -53,8 +53,8 @@ } ], "prettier/prettier": 2, - "@stylistic/indent": ["error", 4], - "@stylistic/indent-binary-ops": ["error", 4], + "@stylistic/indent": ["error", 2], + "@stylistic/indent-binary-ops": ["error", 2], "arrow-body-style": 0, "prefer-arrow-callback": 0, "prefer-rest-params": 0, diff --git a/.markdownlint.cjs b/.markdownlint.cjs index 45519ce..4642f0c 100644 --- a/.markdownlint.cjs +++ b/.markdownlint.cjs @@ -1,15 +1,15 @@ module.exports = { - "default": true, - "MD004": { - "style": "dash" - }, - "MD013": { - "line_length": 120, - "ignore_code_blocks": true, - "ignore_urls": true, - "tables": false - }, - "MD024": { - "siblings_only": true - } + 'default': true, + 'MD004': { + 'style': 'dash', + }, + 'MD013': { + 'line_length': 120, + 'ignore_code_blocks': true, + 'ignore_urls': true, + 'tables': false, + }, + 'MD024': { + 'siblings_only': true, + }, }; diff --git a/.prettierrc b/.prettierrc index ec5b575..ac1ae92 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,12 +1,28 @@ { "printWidth": 120, - "tabWidth": 4, + "tabWidth": 2, "useTabs": false, "semi": true, "singleQuote": true, "quoteProps": "as-needed", "trailingComma": "all", "bracketSpacing": true, - "bracketSameLine": false, - "arrowParens": "always" -} + "arrowParens": "always", + "jsxSingleQuote": true, + "overrides": [ + { + "files": "*.md", + "options": { + "parser": "markdown", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": false, + "trailingComma": "none", + "proseWrap": "always", + "embeddedLanguageFormatting": "off" + } + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8ba16..e564ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,97 +6,134 @@ ### Others -- **deps-dev:** bump @semantic-release/github from 10.3.5 to 11.0.0 ([fcf7151](https://github.com/zavoloklom/docker-compose-linter/commit/fcf715159bdfcf7075a0c5efdbed0f8a9b518d5c)) -- **deps-dev:** bump @stylistic/eslint-plugin from 2.8.0 to 2.9.0 ([73e3f13](https://github.com/zavoloklom/docker-compose-linter/commit/73e3f13b8c4f655521c979bd3df76d51f075b16b)) -- **deps-dev:** bump @types/node from 20.16.5 to 20.16.10 ([2599b91](https://github.com/zavoloklom/docker-compose-linter/commit/2599b917c4c118d47ae0a79743e5717102a43316)) -- **deps-dev:** bump eslint-plugin-import from 2.30.0 to 2.31.0 ([dbcd009](https://github.com/zavoloklom/docker-compose-linter/commit/dbcd0092468e8e1c7857b36feedcbab53ebc64d1)) -- **deps-dev:** bump eslint-plugin-unicorn from 55.0.0 to 56.0.0 ([2a92689](https://github.com/zavoloklom/docker-compose-linter/commit/2a9268923c58dc8c65e2f852f6b18e241af417f2)) -- **deps-dev:** bump esmock from 2.6.7 to 2.6.9 ([98d2f92](https://github.com/zavoloklom/docker-compose-linter/commit/98d2f920caa086145c6493676fd3efff414c6d58)) -- **deps-dev:** bump semantic-release from 24.1.1 to 24.1.2 ([cdc1963](https://github.com/zavoloklom/docker-compose-linter/commit/cdc196300a0145f80ebcd1cf821b79b931b9ee34)) -- update dependabot config ([321fa32](https://github.com/zavoloklom/docker-compose-linter/commit/321fa328276ad68eb9575399bdc8d24310268f6b)) +- **deps-dev:** bump @semantic-release/github from 10.3.5 to 11.0.0 + ([fcf7151](https://github.com/zavoloklom/docker-compose-linter/commit/fcf715159bdfcf7075a0c5efdbed0f8a9b518d5c)) +- **deps-dev:** bump @stylistic/eslint-plugin from 2.8.0 to 2.9.0 + ([73e3f13](https://github.com/zavoloklom/docker-compose-linter/commit/73e3f13b8c4f655521c979bd3df76d51f075b16b)) +- **deps-dev:** bump @types/node from 20.16.5 to 20.16.10 + ([2599b91](https://github.com/zavoloklom/docker-compose-linter/commit/2599b917c4c118d47ae0a79743e5717102a43316)) +- **deps-dev:** bump eslint-plugin-import from 2.30.0 to 2.31.0 + ([dbcd009](https://github.com/zavoloklom/docker-compose-linter/commit/dbcd0092468e8e1c7857b36feedcbab53ebc64d1)) +- **deps-dev:** bump eslint-plugin-unicorn from 55.0.0 to 56.0.0 + ([2a92689](https://github.com/zavoloklom/docker-compose-linter/commit/2a9268923c58dc8c65e2f852f6b18e241af417f2)) +- **deps-dev:** bump esmock from 2.6.7 to 2.6.9 + ([98d2f92](https://github.com/zavoloklom/docker-compose-linter/commit/98d2f920caa086145c6493676fd3efff414c6d58)) +- **deps-dev:** bump semantic-release from 24.1.1 to 24.1.2 + ([cdc1963](https://github.com/zavoloklom/docker-compose-linter/commit/cdc196300a0145f80ebcd1cf821b79b931b9ee34)) +- update dependabot config + ([321fa32](https://github.com/zavoloklom/docker-compose-linter/commit/321fa328276ad68eb9575399bdc8d24310268f6b)) ### Documentation -- add yaml anchor handling section ([a7b61bb](https://github.com/zavoloklom/docker-compose-linter/commit/a7b61bb877ed2e0e67dedac1395d2a32113c57df)), closes [#39](https://github.com/zavoloklom/docker-compose-linter/issues/39) -- change GitLab CI Example ([c421f23](https://github.com/zavoloklom/docker-compose-linter/commit/c421f2315a584adcc6b2414c25fa968e6053ffd8)) +- add yaml anchor handling section + ([a7b61bb](https://github.com/zavoloklom/docker-compose-linter/commit/a7b61bb877ed2e0e67dedac1395d2a32113c57df)), + closes [#39](https://github.com/zavoloklom/docker-compose-linter/issues/39) +- change GitLab CI Example + ([c421f23](https://github.com/zavoloklom/docker-compose-linter/commit/c421f2315a584adcc6b2414c25fa968e6053ffd8)) ### Bug Fixes -- add yaml anchor/fragments support ([4d9826f](https://github.com/zavoloklom/docker-compose-linter/commit/4d9826f59831a583080d13fed2dbad6d3fab5f61)), closes [#39](https://github.com/zavoloklom/docker-compose-linter/issues/39) +- add yaml anchor/fragments support + ([4d9826f](https://github.com/zavoloklom/docker-compose-linter/commit/4d9826f59831a583080d13fed2dbad6d3fab5f61)), + closes [#39](https://github.com/zavoloklom/docker-compose-linter/issues/39) ## [1.0.6](https://github.com/zavoloklom/docker-compose-linter/compare/v1.0.5...v1.0.6) (2024-10-01) ### Bug Fixes -- run checks against any file provided by user and skip regex pattern ([0047590](https://github.com/zavoloklom/docker-compose-linter/commit/0047590e9459e7f13bfab81accd7fbac7c4139d9)), closes [#23](https://github.com/zavoloklom/docker-compose-linter/issues/23) +- run checks against any file provided by user and skip regex pattern + ([0047590](https://github.com/zavoloklom/docker-compose-linter/commit/0047590e9459e7f13bfab81accd7fbac7c4139d9)), + closes [#23](https://github.com/zavoloklom/docker-compose-linter/issues/23) ## [1.0.5](https://github.com/zavoloklom/docker-compose-linter/compare/v1.0.4...v1.0.5) (2024-10-01) ### Others -- **deps-dev:** bump @semantic-release/github from 10.3.4 to 10.3.5 ([53e65a8](https://github.com/zavoloklom/docker-compose-linter/commit/53e65a848c6ea1bc82cbb4977eebb7564478d748)) -- **deps-dev:** bump eslint from 8.57.0 to 8.57.1 ([2bbc6e7](https://github.com/zavoloklom/docker-compose-linter/commit/2bbc6e78179fa40fff5529caf0ff407f1449c8ed)) +- **deps-dev:** bump @semantic-release/github from 10.3.4 to 10.3.5 + ([53e65a8](https://github.com/zavoloklom/docker-compose-linter/commit/53e65a848c6ea1bc82cbb4977eebb7564478d748)) +- **deps-dev:** bump eslint from 8.57.0 to 8.57.1 + ([2bbc6e7](https://github.com/zavoloklom/docker-compose-linter/commit/2bbc6e78179fa40fff5529caf0ff407f1449c8ed)) ### Documentation -- add pull request template ([3770397](https://github.com/zavoloklom/docker-compose-linter/commit/3770397c3aebc829d8f8d1a8dae297303d3158b0)) -- update github issue templates ([a7ec994](https://github.com/zavoloklom/docker-compose-linter/commit/a7ec99412dcdda18f0405adfe10ed4f8e001a055)) +- add pull request template + ([3770397](https://github.com/zavoloklom/docker-compose-linter/commit/3770397c3aebc829d8f8d1a8dae297303d3158b0)) +- update github issue templates + ([a7ec994](https://github.com/zavoloklom/docker-compose-linter/commit/a7ec99412dcdda18f0405adfe10ed4f8e001a055)) ### Bug Fixes -- Search for compose.ya?ml ([0050953](https://github.com/zavoloklom/docker-compose-linter/commit/00509536eac9929613649b805ffbf392dc068598)) +- Search for compose.ya?ml + ([0050953](https://github.com/zavoloklom/docker-compose-linter/commit/00509536eac9929613649b805ffbf392dc068598)) ## [1.0.4](https://github.com/zavoloklom/docker-compose-linter/compare/v1.0.3...v1.0.4) (2024-09-20) ### Bug Fixes -- resolve error "key already set" in Service Keys Order Rule ([336723d](https://github.com/zavoloklom/docker-compose-linter/commit/336723d7ebcdf717f278896f7fbf0d39fce4f5e9)), closes [#9](https://github.com/zavoloklom/docker-compose-linter/issues/9) +- resolve error "key already set" in Service Keys Order Rule + ([336723d](https://github.com/zavoloklom/docker-compose-linter/commit/336723d7ebcdf717f278896f7fbf0d39fce4f5e9)), + closes [#9](https://github.com/zavoloklom/docker-compose-linter/issues/9) ## [1.0.3](https://github.com/zavoloklom/docker-compose-linter/compare/v1.0.2...v1.0.3) (2024-09-20) ### CI/CD -- update version for upload-artifact and download-artifact actions ([f3187a6](https://github.com/zavoloklom/docker-compose-linter/commit/f3187a63679c7cbaf1ec5a6f009a4a09a0d4f366)) +- update version for upload-artifact and download-artifact actions + ([f3187a6](https://github.com/zavoloklom/docker-compose-linter/commit/f3187a63679c7cbaf1ec5a6f009a4a09a0d4f366)) ### Bug Fixes -- handle port value provided with envs ([63c6176](https://github.com/zavoloklom/docker-compose-linter/commit/63c617671f0b55630a9bc36cfc65a734596e7c56)), closes [#8](https://github.com/zavoloklom/docker-compose-linter/issues/8) +- handle port value provided with envs + ([63c6176](https://github.com/zavoloklom/docker-compose-linter/commit/63c617671f0b55630a9bc36cfc65a734596e7c56)), + closes [#8](https://github.com/zavoloklom/docker-compose-linter/issues/8) ## [1.0.2](https://github.com/zavoloklom/docker-compose-linter/compare/v1.0.1...v1.0.2) (2024-09-20) ### Others -- add GitHub issue template for bugs ([4163c30](https://github.com/zavoloklom/docker-compose-linter/commit/4163c3084c3dae80d85bedfc7daba86b21f36318)) -- change order of semantic-release job ([e8d1831](https://github.com/zavoloklom/docker-compose-linter/commit/e8d1831a683e0d6428c30376b0a668b6138717a8)) -- **deps-dev:** bump @eslint-community/regexpp from 4.11.0 to 4.11.1 ([910d6ea](https://github.com/zavoloklom/docker-compose-linter/commit/910d6ea91a433021158073970283301d0909f153)) -- **deps-dev:** bump @semantic-release/github from 10.3.3 to 10.3.4 ([416f176](https://github.com/zavoloklom/docker-compose-linter/commit/416f176965b9e9fa894ee5d61e9b569b5d7f53a1)) -- set up a security policy ([8c220ac](https://github.com/zavoloklom/docker-compose-linter/commit/8c220ac824cceec1b0fb1066c0a11fa98eac1116)) +- add GitHub issue template for bugs + ([4163c30](https://github.com/zavoloklom/docker-compose-linter/commit/4163c3084c3dae80d85bedfc7daba86b21f36318)) +- change order of semantic-release job + ([e8d1831](https://github.com/zavoloklom/docker-compose-linter/commit/e8d1831a683e0d6428c30376b0a668b6138717a8)) +- **deps-dev:** bump @eslint-community/regexpp from 4.11.0 to 4.11.1 + ([910d6ea](https://github.com/zavoloklom/docker-compose-linter/commit/910d6ea91a433021158073970283301d0909f153)) +- **deps-dev:** bump @semantic-release/github from 10.3.3 to 10.3.4 + ([416f176](https://github.com/zavoloklom/docker-compose-linter/commit/416f176965b9e9fa894ee5d61e9b569b5d7f53a1)) +- set up a security policy + ([8c220ac](https://github.com/zavoloklom/docker-compose-linter/commit/8c220ac824cceec1b0fb1066c0a11fa98eac1116)) ### CI/CD -- add markdownlint command with @semantic-release/exec ([c6f8896](https://github.com/zavoloklom/docker-compose-linter/commit/c6f88964a174120041fff1b7744b3edde2f8c49e)) -- remove uploading reports to Codacy from PR ([f67cf3c](https://github.com/zavoloklom/docker-compose-linter/commit/f67cf3ce8005cbdd3e8504341437a6629cce563b)) +- add markdownlint command with @semantic-release/exec + ([c6f8896](https://github.com/zavoloklom/docker-compose-linter/commit/c6f88964a174120041fff1b7744b3edde2f8c49e)) +- remove uploading reports to Codacy from PR + ([f67cf3c](https://github.com/zavoloklom/docker-compose-linter/commit/f67cf3ce8005cbdd3e8504341437a6629cce563b)) ### Bug Fixes -- change cli-config JSON Schema ([a627504](https://github.com/zavoloklom/docker-compose-linter/commit/a627504f447e12d52d99617d8a1f9a7f99d0293f)) +- change cli-config JSON Schema + ([a627504](https://github.com/zavoloklom/docker-compose-linter/commit/a627504f447e12d52d99617d8a1f9a7f99d0293f)) ## [1.0.1](https://github.com/zavoloklom/docker-compose-linter/compare/v1.0.0...v1.0.1) (2024-09-14) ### Others -- update CHANGELOG.md generation to comply with linting rules ([43a7efa](https://github.com/zavoloklom/docker-compose-linter/commit/43a7efafb0fea05e50f81805758c8eec61f64153)) +- update CHANGELOG.md generation to comply with linting rules + ([43a7efa](https://github.com/zavoloklom/docker-compose-linter/commit/43a7efafb0fea05e50f81805758c8eec61f64153)) ### CI/CD -- add "Upload release artifacts" job ([2c12132](https://github.com/zavoloklom/docker-compose-linter/commit/2c12132e25c7b3de253f40c7f4bd2a0d50687315)) +- add "Upload release artifacts" job + ([2c12132](https://github.com/zavoloklom/docker-compose-linter/commit/2c12132e25c7b3de253f40c7f4bd2a0d50687315)) ### Bug Fixes -- correct npm release ci job ([267979d](https://github.com/zavoloklom/docker-compose-linter/commit/267979d635d695680f6f567df66ea47aa4203477)) +- correct npm release ci job + ([267979d](https://github.com/zavoloklom/docker-compose-linter/commit/267979d635d695680f6f567df66ea47aa4203477)) ## 1.0.0 (2024-09-14) ### Features -- initial release ([6969503](https://github.com/zavoloklom/docker-compose-linter/commit/69695032957556141669ea6a5daf213ba8479ffa)) +- initial release + ([6969503](https://github.com/zavoloklom/docker-compose-linter/commit/69695032957556141669ea6a5daf213ba8479ffa)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8355f0d..e5bbe98 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,135 +2,104 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for +everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity +and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, +color, religion, or sexual identity and orientation. -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to a positive environment for our -community include: +Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall - community +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -- The use of sexualized language or imagery, and sexual attention or advances of - any kind +- The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or email address, - without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. ## Scope -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing +the community in public spaces. Examples of representing our community include using an official email address, posting +via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -. -All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible +for enforcement at . All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. +All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem +in violation of this Code of Conduct: ### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the +community. -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation +and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning -**Community Impact**: A violation through a single incident or series of -actions. +**Community Impact**: A violation through a single incident or series of actions. -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding +interactions in community spaces as well as external channels like social media. Violating these terms may lead to a +temporary or permanent ban. ### 3. Temporary Ban -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified +period of time. No public or private interaction with the people involved, including unsolicited interaction with those +enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate +behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. -**Consequence**: A permanent ban from any sort of public interaction within the -community. +**Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code\_of\_conduct.html][v2.1]. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][mozilla-coc]. +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][mozilla-coc]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][faq]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org - [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html - [mozilla-coc]: https://github.com/mozilla/diversity - [faq]: https://www.contributor-covenant.org/faq - [translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1d8887..459ce9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,13 +17,12 @@ Before making contributions, ensure the following: 2. **Create a Branch**: Create a new branch in your local repository. This keeps your changes organized and separate from the main project. 3. **Development**: Make your changes in your branch. Here are a few things to keep in mind: - - **No Lint Errors**: Ensure your code changes adhere to the project's linting rules and do not introduce new lint - errors. - - **Testing**: All changes must be accompanied by passing tests. Add new tests if you are adding functionality or - fix existing tests if you are changing code. - - **Conventional Commits**: Commit your changes using - the [Conventional Commits](https://www.conventionalcommits.org) format. This standardization helps automate the - version management and changelog generation. + - **No Lint Errors**: Ensure your code changes adhere to the project's linting rules and do not introduce new lint + errors. + - **Testing**: All changes must be accompanied by passing tests. Add new tests if you are adding functionality or fix + existing tests if you are changing code. + - **Conventional Commits**: Commit your changes using the [Conventional Commits](https://www.conventionalcommits.org) + format. This standardization helps automate the version management and changelog generation. ## How to Add a New Rule @@ -33,15 +32,15 @@ Please follow the steps carefully to ensure consistency and maintainability of t ### Create a New Rule File 1. Navigate to the `src/rules/` directory. -2. Create a new `.ts` file with a descriptive name for your rule. Name it something like `new-check-rule.ts` - with `-rule` at the end. +2. Create a new `.ts` file with a descriptive name for your rule. Name it something like `new-check-rule.ts` with + `-rule` at the end. ### Implement the Rule and Write Your Logic Your rule should implement the `LintRule` interface from [linter.types.ts](./src/linter/linter.types.ts). -Implement the logic that validates if the Docker Compose file violates the rule in `check` method. Use the -`LintContext` to access the content, and return an array of `LintMessage` objects with information about any violations. +Implement the logic that validates if the Docker Compose file violates the rule in `check` method. Use the `LintContext` +to access the content, and return an array of `LintMessage` objects with information about any violations. If the rule is fixable, implement the logic to return the fixed content of the file in `fix` method. If the rule isn't fixable, this method can return the content unchanged. @@ -55,8 +54,8 @@ fixable, this method can return the content unchanged. ### Update Documentation 1. Go to the `docs/rules/` folder. -2. Create a markdown file describing your new rule (for example `new-check-rule.md`) based - on [template](./docs/rules/__TEMPLATE__.md) +2. Create a markdown file describing your new rule (for example `new-check-rule.md`) based on + [template](./docs/rules/__TEMPLATE__.md) ## Build Docker File Locally @@ -124,8 +123,8 @@ After you've made your changes: ## After Your Contribution -Once your contribution is merged, it will become part of the project. -I appreciate your hard work and contribution to making this tool better. -Also, I encourage you to continue participating in the project and joining in discussions and future enhancements. +Once your contribution is merged, it will become part of the project. I appreciate your hard work and contribution to +making this tool better. Also, I encourage you to continue participating in the project and joining in discussions and +future enhancements. **Thank you for contributing!** diff --git a/README.md b/README.md index b91b9fc..2772306 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=whit&style=flat-square)](https://conventionalcommits.org) > **Note**: Docker Compose configurations vary greatly between different projects and setups. While DCLint is stable, -> there may be edge cases or unique setups that cause issues. If you encounter any problems or have suggestions, -> please feel free to [open an issue](https://github.com/zavoloklom/docker-compose-linter/issues) -> or [submit a pull request](#contributing). Your feedback is highly appreciated! +> there may be edge cases or unique setups that cause issues. If you encounter any problems or have suggestions, please +> feel free to [open an issue](https://github.com/zavoloklom/docker-compose-linter/issues) or +> [submit a pull request](#contributing). Your feedback is highly appreciated! -Docker Compose Linter (**DCLint**) is a utility designed to analyze, validate and fix Docker Compose files. -It helps identify errors, style violations, and potential issues in Docker Compose files, ensuring your configurations -are robust, maintainable, and free from common pitfalls. +Docker Compose Linter (**DCLint**) is a utility designed to analyze, validate and fix Docker Compose files. It helps +identify errors, style violations, and potential issues in Docker Compose files, ensuring your configurations are +robust, maintainable, and free from common pitfalls. ## Features @@ -25,7 +25,8 @@ are robust, maintainable, and free from common pitfalls. issues in your files. - **Comments Support**: After automated sorting and fixing, comments remain in the correct place, ensuring no important information is lost during the formatting process. -- **Anchor Support:** Supports YAML anchors for shared configuration sections, with [some limitations](#anchor-handling). +- **Anchor Support:** Supports YAML anchors for shared configuration sections, with + [some limitations](#anchor-handling). ## Getting Started @@ -53,8 +54,8 @@ This command will lint your Docker Compose files in the current directory. ### Linting Specific Files and Directories -To lint a specific Docker Compose file or a directory containing such files, specify the path relative to your -project directory: +To lint a specific Docker Compose file or a directory containing such files, specify the path relative to your project +directory: ```shell npx dclint /path/to/docker-compose.yml @@ -66,8 +67,8 @@ To lint all Docker Compose files in a specific directory, use the path to the di npx dclint /path/to/directory ``` -In this case, `dclint` will search the specified directory for files matching the following -pattern `/^(docker-)?compose.*\.ya?ml$/`. +In this case, `dclint` will search the specified directory for files matching the following pattern +`/^(docker-)?compose.*\.ya?ml$/`. It will handle all matching files within the directory and, if [recursive search](./docs/cli.md#-r---recursive) is enabled, also in any subdirectories. @@ -82,8 +83,8 @@ To display help and see all available options: npx dclint -h ``` -For more details about available options and formatters, please refer to the [CLI Reference](./docs/cli.md) -and [Formatters Reference](./docs/formatters.md). +For more details about available options and formatters, please refer to the [CLI Reference](./docs/cli.md) and +[Formatters Reference](./docs/formatters.md). ## Usage with Docker @@ -106,8 +107,8 @@ docker run -t --rm -v ${PWD}:/app zavoloklom/dclint . ### Linting Specific Files and Directories in Docker -If you want to lint a specific Docker Compose file or a directory containing such files, specify the path relative -to your project directory: +If you want to lint a specific Docker Compose file or a directory containing such files, specify the path relative to +your project directory: ```shell docker run -t --rm -v ${PWD}:/app zavoloklom/dclint /app/path/to/docker-compose.yml @@ -125,8 +126,8 @@ To display help and see all available options: docker run -t --rm -v ${PWD}:/app zavoloklom/dclint -h ``` -For more information about available options and formatters, please refer to the [CLI Reference](./docs/cli.md) -and [Formatters Reference](./docs/formatters.md). +For more information about available options and formatters, please refer to the [CLI Reference](./docs/cli.md) and +[Formatters Reference](./docs/formatters.md). ## Rules and Errors @@ -136,10 +137,9 @@ documentation for each rule and the errors that can be detected by the linter is - [Rules Documentation](./docs/rules.md) - [Errors Documentation](./docs/errors.md) -DCLint uses the [yaml](https://github.com/eemeli/yaml) library for linting and formatting Docker Compose files. -This ensures that any configuration files you check are compliant with YAML standards. Before any rule -checks are applied, two important validations are performed, which cannot be -disabled - [YAML Validity Check](./docs/errors/invalid-yaml.md) +DCLint uses the [yaml](https://github.com/eemeli/yaml) library for linting and formatting Docker Compose files. This +ensures that any configuration files you check are compliant with YAML standards. Before any rule checks are applied, +two important validations are performed, which cannot be disabled - [YAML Validity Check](./docs/errors/invalid-yaml.md) and [Docker Compose Schema Validation](./docs/errors/invalid-schema.md). ### Anchor Handling @@ -147,8 +147,8 @@ and [Docker Compose Schema Validation](./docs/errors/invalid-schema.md). Docker Compose Linter provides support for YAML anchors specifically during schema validation, which enables the reuse of configuration sections across different services for cleaner and more maintainable files. -However, note that anchors are neither validated by individual linting rules nor automatically fixed when using -the `--fix` flag. +However, note that anchors are neither validated by individual linting rules nor automatically fixed when using the +`--fix` flag. When multiple anchors are required in a Docker Compose file, use the following syntax: @@ -168,22 +168,21 @@ services: This approach, which combines anchors in a single << line, is preferable to defining each anchor on separate lines ( e.g., `<< : *anchor1` followed by `<< : *anchor2`). -More information on YAML merge syntax is available in -the [official YAML documentation](https://yaml.org/type/merge.html) and -in [known issue with Docker Compose](https://github.com/docker/compose/issues/10411). +More information on YAML merge syntax is available in the +[official YAML documentation](https://yaml.org/type/merge.html) and in +[known issue with Docker Compose](https://github.com/docker/compose/issues/10411). For an example of anchor usage, refer to the sample Compose file in `tests/mocks/docker-compose.anchors.yml`. ## Configuration -DCLint allows you to customize the set of rules used during linting to fit your project's -specific needs. You can configure which rules are applied, their severity levels, and additional behavior settings -using a configuration file. +DCLint allows you to customize the set of rules used during linting to fit your project's specific needs. You can +configure which rules are applied, their severity levels, and additional behavior settings using a configuration file. ### Supported Configuration File Formats -DCLint supports flexible configuration options through the use -of [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). This means you can use various formats to configure the +DCLint supports flexible configuration options through the use of +[cosmiconfig](https://github.com/cosmiconfig/cosmiconfig). This means you can use various formats to configure the linter, including JSON, YAML, and JavaScript files. For example: @@ -220,8 +219,7 @@ Here is an example of a configuration file using JSON format: In addition to enabling or disabling rules, some rules may support custom parameters to tailor them to your specific needs. For example, the [require-quotes-in-ports](./docs/rules/require-quotes-in-ports-rule.md) rule allows you to -configure -whether single or double quotes should be used around port numbers. You can configure it like this: +configure whether single or double quotes should be used around port numbers. You can configure it like this: ```json { @@ -274,9 +272,9 @@ And this tools for Docker Compose formatting and fixing: ## Contributing -If you encounter any issues or have suggestions for improvements, feel free to open -an [issue](https://github.com/zavoloklom/docker-compose-linter/issues) or submit -a [pull request](https://github.com/zavoloklom/docker-compose-linter/pulls). +If you encounter any issues or have suggestions for improvements, feel free to open an +[issue](https://github.com/zavoloklom/docker-compose-linter/issues) or submit a +[pull request](https://github.com/zavoloklom/docker-compose-linter/pulls). If you'd like to contribute to this project, please read through the [CONTRIBUTING.md](./CONTRIBUTING.md) file. @@ -288,9 +286,9 @@ in this project, you agree to abide by its terms. ## Changelog -The changelog is automatically generated based -on [semantic-release](https://github.com/semantic-release/semantic-release) -and [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). +The changelog is automatically generated based on +[semantic-release](https://github.com/semantic-release/semantic-release) and +[conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). See the [CHANGELOG.md](./CHANGELOG.md) file for detailed lists of changes for each version. diff --git a/SECURITY.md b/SECURITY.md index 52ea138..6a4e1ea 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ We actively maintain and support the following versions of the project: | Version | Supported | -|-----------|--------------------| +| --------- | ------------------ | | `1.x.x` | :white_check_mark: | | `< 1.0.0` | :x: | diff --git a/TODO.md b/TODO.md index 4a7d5cc..c728895 100644 --- a/TODO.md +++ b/TODO.md @@ -7,17 +7,13 @@ ensure better code quality, readability, and maintainability. ### Alphabetical Sorting of Environment Variables -Category: style -Severity: info -Description: Environment variables within a service should be sorted alphabetically to improve readability. -Fixable: Yes +Category: style Severity: info Description: Environment variables within a service should be sorted alphabetically to +improve readability. Fixable: Yes ### Volumes Alphabetical Order Rule -Category: style -Severity: info -Description: Volumes in the volumes section should be sorted alphabetically to improve readability and maintainability. -Fixable: Yes +Category: style Severity: info Description: Volumes in the volumes section should be sorted alphabetically to improve +readability and maintainability. Fixable: Yes ```yaml # Wrong @@ -35,10 +31,8 @@ volumes: ### Alphabetical Sorting of Networks -Category: style -Severity: info -Description: Networks in the networks section should be alphabetically sorted for easier management and readability. -Fixable: Yes +Category: style Severity: info Description: Networks in the networks section should be alphabetically sorted for easier +management and readability. Fixable: Yes ```yaml # Wrong @@ -56,11 +50,8 @@ networks: ### Single Quotes for String Values -Category: best-practice -Severity: warning -Description: It is recommended to use single quotes (') for string values to maintain consistency and avoid errors when -processing YAML. -Fixable: Yes +Category: best-practice Severity: warning Description: It is recommended to use single quotes (') for string values to +maintain consistency and avoid errors when processing YAML. Fixable: Yes ```yaml # Wrong @@ -78,10 +69,8 @@ services: ### Consistent Key Style -Category: best-practice -Severity: warning -Description: All keys in the YAML file should use the same style—either with quotes or without. This helps avoid -inconsistencies and errors. +Category: best-practice Severity: warning Description: All keys in the YAML file should use the same style—either with +quotes or without. This helps avoid inconsistencies and errors. ```yaml # Wrong @@ -99,18 +88,13 @@ services: ### Empty Lines Between Services -Category: style -Severity: info -Description: It is recommended to leave empty lines between service definitions to improve readability. -Fixable: Yes +Category: style Severity: info Description: It is recommended to leave empty lines between service definitions to +improve readability. Fixable: Yes ### Empty Lines Between Configuration Sections -Category: style -Severity: info -Description: Leave an empty line between major configuration sections (e.g., services, networks, volumes) to improve -readability. -Fixable: Yes +Category: style Severity: info Description: Leave an empty line between major configuration sections (e.g., services, +networks, volumes) to improve readability. Fixable: Yes ```yaml # Wrong @@ -133,10 +117,8 @@ networks: ### Port Mapping Format -Category: best-practice -Severity: warning -Description: Ports should be specified in the host:container format to ensure clarity and prevent port mapping issues. -Fixable: yes +Category: best-practice Severity: warning Description: Ports should be specified in the host:container format to ensure +clarity and prevent port mapping issues. Fixable: yes ```yaml # Wrong @@ -158,11 +140,8 @@ services: ### Explicit Format for Environment Variables -Category: best-practice -Severity: warning -Description: It is recommended to use an explicit format for environment variables (e.g., KEY=value) to avoid ambiguity -and errors. -Fixable: Yes +Category: best-practice Severity: warning Description: It is recommended to use an explicit format for environment +variables (e.g., KEY=value) to avoid ambiguity and errors. Fixable: Yes ```yaml # Wrong @@ -184,10 +163,8 @@ services: ### Minimize Privileges -Category: security -Severity: error -Description: Services should not run with elevated privileges unless necessary. This improves container security. -Fixable: No +Category: security Severity: error Description: Services should not run with elevated privileges unless necessary. This +improves container security. Fixable: No ```yaml # Wrong @@ -207,17 +184,12 @@ services: ### Minimize the Number of Privileged Containers -Severity: error -Description: The number of privileged containers should be minimized to enhance security. -Fixable: No +Severity: error Description: The number of privileged containers should be minimized to enhance security. Fixable: No ### Use of Environment Variables -Category: best practice -Severity: warning -Description: It's preferable to use environment variables for sensitive data and configuration to avoid hardcoding them -in the configuration file. -Fixable: No +Category: best practice Severity: warning Description: It's preferable to use environment variables for sensitive data +and configuration to avoid hardcoding them in the configuration file. Fixable: No ```yaml # Wrong @@ -239,25 +211,20 @@ services: ### Limit Container Restarts -Category: performance -Severity: warning -Description: The container restart policy should be explicitly defined and align with the application's needs. +Category: performance Severity: warning Description: The container restart policy should be explicitly defined and align +with the application's needs. ### Ensure Each Service Uses a healthcheck -Category: performance -Severity: warning -Description: Using healthcheck ensures that services are running correctly and can trigger actions if problems are -detected. -Fixable: No +Category: performance Severity: warning Description: Using healthcheck ensures that services are running correctly and +can trigger actions if problems are detected. Fixable: No ### Specify Timeouts for healthcheck -Category: performance -Severity: warning -Description: It's recommended to set timeouts for container healthcheck to avoid hanging services in case of failures. +Category: performance Severity: warning Description: It's recommended to set timeouts for container healthcheck to avoid +hanging services in case of failures. ```yaml # Wrong @@ -280,9 +247,8 @@ services: ### Avoid Hardcoded Paths in volumes -Category: best practice -Severity: warning -Description: Avoid using hardcoded paths in volumes. Use environment variables or relative paths to improve portability. +Category: best practice Severity: warning Description: Avoid using hardcoded paths in volumes. Use environment variables +or relative paths to improve portability. ```yaml # Wrong @@ -304,10 +270,8 @@ services: ### Use Multi-Layered Secrets -Category: security -Severity: warning -Description: Use Docker's built-in secret management (e.g., secrets) to securely handle sensitive data within -containers. +Category: security Severity: warning Description: Use Docker's built-in secret management (e.g., secrets) to securely +handle sensitive data within containers. ```yaml # Wrong @@ -333,13 +297,10 @@ secrets: ### Empty Line at the End of the File (not sure) -Category: style -Severity: info -Description: Each Docker Compose file should end with an empty line for better compatibility with various tools and -version control systems. +Category: style Severity: info Description: Each Docker Compose file should end with an empty line for better +compatibility with various tools and version control systems. ### Indentation Should Be Set to 2 Spaces (not sure) -Category: style -Severity: info -Description: It is recommended to use 2-space indentation for better readability and consistency in the configuration. +Category: style Severity: info Description: It is recommended to use 2-space indentation for better readability and +consistency in the configuration. diff --git a/ava.config.js b/ava.config.js index 0b6d011..1acc21f 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,10 +1,10 @@ export default { - files: ['tests/**/*.spec.ts'], - extensions: { - ts: 'module', - }, - nodeArguments: ['--import=tsimp/import', '--no-warnings'], - timeout: '2m', - serial: true, - concurrency: 1, + files: ['tests/**/*.spec.ts'], + extensions: { + ts: 'module', + }, + nodeArguments: ['--import=tsimp/import', '--no-warnings'], + timeout: '2m', + serial: true, + concurrency: 1, }; diff --git a/docs/cli.md b/docs/cli.md index a7ed7a3..14d4f50 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -68,8 +68,8 @@ Below is a detailed explanation of each CLI option available in DCLint. - **Type**: `string` - **Default**: `"stylish"` - **Use Case**: Choose a different formatter for the output, such as `json`, to make the results easier to parse by - other tools or to fit specific formatting needs. For more information about available formatters please - read [Formatters Reference](./formatters.md). + other tools or to fit specific formatting needs. For more information about available formatters please read + [Formatters Reference](./formatters.md). ### `-c, --config` diff --git a/docs/errors.md b/docs/errors.md index d975ab1..8f31e69 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -1,7 +1,7 @@ # Errors -These checks are mandatory and are always performed before the linter applies any rules. -This helps prevent scenarios where the linter attempts to analyze an incorrect or improperly structured file. +These checks are mandatory and are always performed before the linter applies any rules. This helps prevent scenarios +where the linter attempts to analyze an incorrect or improperly structured file. - [Invalid YAML Rule](./errors/invalid-yaml.md) - [Invalid Schema Rule](./errors/invalid-schema.md) diff --git a/docs/errors/invalid-schema.md b/docs/errors/invalid-schema.md index 215b7ad..ee7c30e 100644 --- a/docs/errors/invalid-schema.md +++ b/docs/errors/invalid-schema.md @@ -11,9 +11,9 @@ your Docker Compose file against the defined schema, ensuring that it follows th ## Rule Details and Rationale -This rule validates your Docker Compose file against the JSON schema defined -in [compose.schema.json](../../schemas/compose.schema.json). This ensures that the structure and content of the file -adhere to the expected standards, reducing the likelihood of errors when running Docker Compose. +This rule validates your Docker Compose file against the JSON schema defined in +[compose.schema.json](../../schemas/compose.schema.json). This ensures that the structure and content of the file adhere +to the expected standards, reducing the likelihood of errors when running Docker Compose. By validating the file against a schema, you can catch issues early in the development process, leading to more stable and predictable deployments. @@ -24,8 +24,8 @@ This rule requires that the `compose.schema.json` schema is up-to-date with the However, the schema is not updated automatically. This means that if the schema is outdated, it may fail to recognize newer Docker Compose features, potentially causing false positives during validation. -To keep the schema current, you can manually update it and contribute to the project by following -the [instructions in the contribution guidelines](../../CONTRIBUTING.md#how-to-update-compose-schema). +To keep the schema current, you can manually update it and contribute to the project by following the +[instructions in the contribution guidelines](../../CONTRIBUTING.md#how-to-update-compose-schema). ## Version diff --git a/docs/errors/invalid-yaml.md b/docs/errors/invalid-yaml.md index 05e1ff4..dce707f 100644 --- a/docs/errors/invalid-yaml.md +++ b/docs/errors/invalid-yaml.md @@ -27,9 +27,9 @@ requirement for YAML files to function correctly. ## Known Limitations This rule only checks for the syntactical correctness of the YAML file. It does not verify the content against any -specific schema or enforce any specific structure beyond basic YAML syntax. -Additionally, the validation relies on the [yaml](https://github.com/eemeli/yaml) library, and if the library fails to -catch certain errors or inconsistencies in the YAML structure, those issues will not be flagged by this rule either. +specific schema or enforce any specific structure beyond basic YAML syntax. Additionally, the validation relies on the +[yaml](https://github.com/eemeli/yaml) library, and if the library fails to catch certain errors or inconsistencies in +the YAML structure, those issues will not be flagged by this rule either. ## Version diff --git a/docs/formatters.md b/docs/formatters.md index 8ceba6f..3887cf2 100644 --- a/docs/formatters.md +++ b/docs/formatters.md @@ -30,9 +30,9 @@ export default function jsonFormatter(results: LintResult[]): string { } ``` -To run ESLint with this formatter, you can use the `-f` (or `--format`) command line flag. -You must begin the path to a locally defined custom formatter with a period (`.`), such as `./my-awesome-formatter.js` -or `../formatters/my-awesome-formatter.ts`. +To run ESLint with this formatter, you can use the `-f` (or `--format`) command line flag. You must begin the path to a +locally defined custom formatter with a period (`.`), such as `./my-awesome-formatter.js` or +`../formatters/my-awesome-formatter.ts`. ```shell dclint -f ./my-awesome-formatter.js . @@ -41,8 +41,8 @@ dclint -f ./my-awesome-formatter.js . ### Packaging a Custom Formatter Custom formatters can be distributed through npm packages. To do so, create an npm package with a name in the format -`dclint-formatter-*`, where `*` is the name of your formatter (such as `dclint-formatter-awesome`). -Projects should then install the package and use the custom formatter with the `-f` (or `--format`) flag like this: +`dclint-formatter-*`, where `*` is the name of your formatter (such as `dclint-formatter-awesome`). Projects should then +install the package and use the custom formatter with the `-f` (or `--format`) flag like this: ```shell dclint -f dclint-formatter-awesome . diff --git a/docs/rules/no-duplicate-exported-ports-rule.md b/docs/rules/no-duplicate-exported-ports-rule.md index 4ec8e9d..2c4c1e6 100644 --- a/docs/rules/no-duplicate-exported-ports-rule.md +++ b/docs/rules/no-duplicate-exported-ports-rule.md @@ -44,10 +44,9 @@ services: ## Rule Details and Rationale The `ports` directive in Docker Compose defines which ports on the host machine are exposed and mapped to the -container's -internal ports. If two services attempt to export the same host port, Docker Compose will fail to start, as it cannot -bind multiple services to the same port on the host. This makes the configuration invalid, and the issue must be -resolved manually before containers can be started. +container's internal ports. If two services attempt to export the same host port, Docker Compose will fail to start, as +it cannot bind multiple services to the same port on the host. This makes the configuration invalid, and the issue must +be resolved manually before containers can be started. Duplicate `ports` can often be the result of simple typographical errors. By catching these issues early during linting, developers can avoid debugging complex port conflicts and ensure their Compose configurations are valid before diff --git a/docs/rules/no-quotes-in-volumes-rule.md b/docs/rules/no-quotes-in-volumes-rule.md index 2cc6878..39cfbd0 100644 --- a/docs/rules/no-quotes-in-volumes-rule.md +++ b/docs/rules/no-quotes-in-volumes-rule.md @@ -1,8 +1,7 @@ # No Quotes in Volumes Rule -Ensures that the values in the `volumes` section of services in the Docker Compose file are not enclosed in -quotes. Quoted paths can cause unexpected behavior in Docker, so this rule enforces that they are written without -quotes. +Ensures that the values in the `volumes` section of services in the Docker Compose file are not enclosed in quotes. +Quoted paths can cause unexpected behavior in Docker, so this rule enforces that they are written without quotes. This rule is fixable. The linter can automatically remove the quotes from volume paths without altering the paths themselves. diff --git a/docs/rules/require-quotes-in-ports-rule.md b/docs/rules/require-quotes-in-ports-rule.md index f9715d3..732ba3a 100644 --- a/docs/rules/require-quotes-in-ports-rule.md +++ b/docs/rules/require-quotes-in-ports-rule.md @@ -1,7 +1,7 @@ # Require Quotes in Ports Rule -Ensures that the port values in the `ports` section of services in the Docker Compose file are enclosed in quotes. -Using quotes around port numbers can prevent YAML parsing issues and ensure that the ports are interpreted correctly. +Ensures that the port values in the `ports` section of services in the Docker Compose file are enclosed in quotes. Using +quotes around port numbers can prevent YAML parsing issues and ensure that the ports are interpreted correctly. This rule is fixable. The linter can automatically add the required quotes around port numbers without altering the ports themselves. The type of quotes (single or double) can be configured via the `quoteType` option. @@ -48,8 +48,8 @@ services: ## Rule Details and Rationale This rule ensures that the port numbers specified in the `ports` section of services are enclosed in quotes. Quoting -ports helps avoid potential issues with YAML parsing, where unquoted numbers might be misinterpreted or cause -unexpected behavior. By enforcing this rule, we ensure that the configuration is more robust and consistent. +ports helps avoid potential issues with YAML parsing, where unquoted numbers might be misinterpreted or cause unexpected +behavior. By enforcing this rule, we ensure that the configuration is more robust and consistent. When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port lower than 60, because YAML parses numbers in the format `xx:yy` as a base-60 value. For this reason, we recommend diff --git a/docs/rules/service-container-name-regex-rule.md b/docs/rules/service-container-name-regex-rule.md index 05e031c..0be7ae1 100644 --- a/docs/rules/service-container-name-regex-rule.md +++ b/docs/rules/service-container-name-regex-rule.md @@ -29,9 +29,9 @@ services: ## Rule Details and Rationale -This rule ensures that container names in Docker Compose follow the required format defined by the regular -expression `[a-zA-Z0-9][a-zA-Z0-9_.-]+`. If a container name contains invalid characters, it can lead to errors when -Docker tries to run the services. This rule identifies invalid names and prevents configuration errors. +This rule ensures that container names in Docker Compose follow the required format defined by the regular expression +`[a-zA-Z0-9][a-zA-Z0-9_.-]+`. If a container name contains invalid characters, it can lead to errors when Docker tries +to run the services. This rule identifies invalid names and prevents configuration errors. Container names should only consist of letters, numbers, underscores (`_`), hyphens (`-`), and periods (`.`). Any other characters, such as `@`, make the configuration invalid. This rule prevents such issues by ensuring that all container diff --git a/docs/rules/service-dependencies-alphabetical-order-rule.md b/docs/rules/service-dependencies-alphabetical-order-rule.md index f53773f..033a856 100644 --- a/docs/rules/service-dependencies-alphabetical-order-rule.md +++ b/docs/rules/service-dependencies-alphabetical-order-rule.md @@ -60,9 +60,9 @@ services: ## Rule Details and Rationale -Sorting the list of services within `depends_on` alphabetically enhances readability and predictability, making it easier -to manage service dependencies. By maintaining a consistent order, developers can avoid confusion when working on large -configurations. +Sorting the list of services within `depends_on` alphabetically enhances readability and predictability, making it +easier to manage service dependencies. By maintaining a consistent order, developers can avoid confusion when working on +large configurations. ## Version diff --git a/docs/rules/service-image-require-explicit-tag-rule.md b/docs/rules/service-image-require-explicit-tag-rule.md index 1594b7b..a050715 100644 --- a/docs/rules/service-image-require-explicit-tag-rule.md +++ b/docs/rules/service-image-require-explicit-tag-rule.md @@ -60,8 +60,8 @@ customize which image tags should be flagged as problematic. If not provided, th ### Example Usage with Custom prohibitedTags -You can specify custom tags that should be prohibited **instead** of the default ones by passing them into -the rule constructor as follows: +You can specify custom tags that should be prohibited **instead** of the default ones by passing them into the rule +constructor as follows: ```json { diff --git a/docs/rules/service-keys-order-rule.md b/docs/rules/service-keys-order-rule.md index 5107176..7228106 100644 --- a/docs/rules/service-keys-order-rule.md +++ b/docs/rules/service-keys-order-rule.md @@ -47,78 +47,73 @@ services: The properties in the Docker Compose file are organized into logical groups. This structure enhances readability, maintainability, and clarity by following a logical flow from core service definitions to execution context. -This order and grouping of properties in the Docker Compose file create a structured, logical flow that enhances -the file’s readability and maintainability. By organizing properties from core definitions through to execution -context, the structure allows for quick comprehension and efficient management, adhering to best practices and -facilitating collaboration among developers and operations teams. +This order and grouping of properties in the Docker Compose file create a structured, logical flow that enhances the +file’s readability and maintainability. By organizing properties from core definitions through to execution context, the +structure allows for quick comprehension and efficient management, adhering to best practices and facilitating +collaboration among developers and operations teams. ### Core Definitions **Properties order:** `image`, `build`, `container_name` -These properties define what the service is, where it comes from, and how it is named. Placing them at the -top provides an immediate understanding of the service's fundamental characteristics, making it easier to identify -and manage. +These properties define what the service is, where it comes from, and how it is named. Placing them at the top provides +an immediate understanding of the service's fundamental characteristics, making it easier to identify and manage. ### Service Dependencies **Properties order:** `depends_on` -This property specifies the relationships between services and the order in which they should start. -Including it early helps clarify the service dependencies and overall architecture, which is crucial for ensuring -correct startup sequences and inter-service communication. +This property specifies the relationships between services and the order in which they should start. Including it early +helps clarify the service dependencies and overall architecture, which is crucial for ensuring correct startup sequences +and inter-service communication. ### Data Management and Configuration **Properties order:** `volumes`, `volumes_from`, `configs`, `secrets` -These properties handle data persistence, sharing, configuration management, and sensitive information -like secrets. Grouping them together provides a clear overview of how the service interacts with its data and -configuration resources, which is essential for ensuring data integrity and secure operations. +These properties handle data persistence, sharing, configuration management, and sensitive information like secrets. +Grouping them together provides a clear overview of how the service interacts with its data and configuration resources, +which is essential for ensuring data integrity and secure operations. ### Environment Configuration **Properties order:** `environment`, `env_file` -Environment variables and external files that define the service’s operating environment are crucial -for -configuring its behavior. Grouping these properties together ensures that all environment-related configurations -are -easily accessible, making it simpler to adjust the service’s runtime environment. +Environment variables and external files that define the service’s operating environment are crucial for configuring its +behavior. Grouping these properties together ensures that all environment-related configurations are easily accessible, +making it simpler to adjust the service’s runtime environment. ### Networking **Properties order:** `ports`, `networks`, `network_mode`, `extra_hosts` -These properties define how the service communicates within the Docker network and with the outside -world. -Grouping networking-related configurations together helps to clearly understand and manage the service’s -connectivity, ensuring proper interaction with other services and external clients. +These properties define how the service communicates within the Docker network and with the outside world. Grouping +networking-related configurations together helps to clearly understand and manage the service’s connectivity, ensuring +proper interaction with other services and external clients. ### Runtime Behavior **Properties order:** `command`, `entrypoint`, `working_dir`, `restart`, `healthcheck` -These properties dictate how the service runs, including the commands it executes, the working directory, -restart policies, and health checks. Placing these properties together creates a clear section focused on the -service’s runtime behavior, which is vital for ensuring that the service starts, runs, and maintains its operation -as expected. +These properties dictate how the service runs, including the commands it executes, the working directory, restart +policies, and health checks. Placing these properties together creates a clear section focused on the service’s runtime +behavior, which is vital for ensuring that the service starts, runs, and maintains its operation as expected. ### Operational Metadata **Properties:** `logging`, `labels` -Metadata and logging configurations are important for monitoring, categorizing, and managing the -service, but they are secondary to its core operation. By grouping them near the end, the focus remains on the -services functionality, while still keeping operational details easily accessible for management purposes. +Metadata and logging configurations are important for monitoring, categorizing, and managing the service, but they are +secondary to its core operation. By grouping them near the end, the focus remains on the services functionality, while +still keeping operational details easily accessible for management purposes. ### Security and Execution Context **Properties:** `user`, `isolation` -These properties define the security context and isolation levels under which the service runs. They -are crucial for maintaining security and proper resource management but are more specific details that logically -follow after the service’s general operation has been defined. +These properties define the security context and isolation levels under which the service runs. They are crucial for +maintaining security and proper resource management but are more specific details that logically follow after the +service’s general operation has been defined. ## Options @@ -126,11 +121,9 @@ The service-keys-order rule allows customization of how the keys within each ser key options to control the order of groups and the keys within those groups: - `groupOrder` (optional): Specifies the order of the logical groups within each service. If not provided, the default - group - order is used. + group order is used. - `groups` (optional): Allows specifying custom key sets for each group. If not provided, the default key sets are used - for - each group. + for each group. These options allow users to control both the order of the groups (e.g., ensuring networking configurations appear before environment variables) and the specific keys within those groups. diff --git a/docs/rules/services-alphabetical-order-rule.md b/docs/rules/services-alphabetical-order-rule.md index 2710635..cbb6ef0 100644 --- a/docs/rules/services-alphabetical-order-rule.md +++ b/docs/rules/services-alphabetical-order-rule.md @@ -42,8 +42,8 @@ services: ## Rule Details and Rationale -This rule ensures that services in the Docker Compose file are listed in alphabetical order. Consistent ordering -of services improves readability and maintainability, especially in larger projects. It allows team members to quickly +This rule ensures that services in the Docker Compose file are listed in alphabetical order. Consistent ordering of +services improves readability and maintainability, especially in larger projects. It allows team members to quickly locate and manage services within the configuration file. ## Version diff --git a/docs/rules/top-level-properties-order-rule.md b/docs/rules/top-level-properties-order-rule.md index c1e2599..06f208b 100644 --- a/docs/rules/top-level-properties-order-rule.md +++ b/docs/rules/top-level-properties-order-rule.md @@ -72,8 +72,8 @@ top-level properties: The `customOrder` option allows you to define a custom sequence for the top-level properties. If this option is not provided, the default order is used. -If you need to change the order, you can customize it using the `customOrder` option. For example, if you -want `services` to come first and `networks` to come after `version`, you can define a custom order like this: +If you need to change the order, you can customize it using the `customOrder` option. For example, if you want +`services` to come first and `networks` to come after `version`, you can define a custom order like this: ```json { @@ -102,9 +102,9 @@ the file. ## Known Limitations -Regardless of the `customOrder`, all properties that start with `x-` will always be sorted alphabetically. -If `x-properties` are not explicitly mentioned in the `customOrder`, they will still follow the -default behavior of being grouped together and sorted alphabetically. +Regardless of the `customOrder`, all properties that start with `x-` will always be sorted alphabetically. If +`x-properties` are not explicitly mentioned in the `customOrder`, they will still follow the default behavior of being +grouped together and sorted alphabetically. ## Version diff --git a/package.json b/package.json index d3b6314..e79868e 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,13 @@ "prebuild": "rimraf dist", "build": "tsc", "debug": "tsc && node --import=tsimp/import --no-warnings --inspect ./src/cli/cli.ts ./tests/mocks/docker-compose.yml -c ./tests/mocks/.dclintrc", - "lint": "npm run eslint && npm run markdownlint", "eslint": "eslint .", "eslint:fix": "eslint . --fix", + "lint": "npm run eslint && npm run markdownlint", "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#**/node_modules\" \"#CHANGELOG.md\"", "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\" \"#**/node_modules\"", - "markdownlint:fix-changelog": "markdownlint-cli2 --fix \"CHANGELOG.md\"", + "markdownlint:fix-changelog": "markdownlint-cli2 --fix \"CHANGELOG.md\" && prettier --write \"CHANGELOG.md\"", + "prettier": "prettier --write \"**/*.md\"", "test": "ava --verbose", "test:coverage": "rimraf coverage && mkdir -p coverage && c8 ava --tap | tap-xunit --package='dclint' > ./coverage/junit.xml", "update-compose-schema": "node ./scripts/download-compose-schema.cjs" @@ -52,7 +53,7 @@ "ajv": "^8.17.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", - "yaml": "^2.5.1", + "yaml": "^2.6.0", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/release.config.js b/release.config.js index 3fff4d3..8ceae62 100644 --- a/release.config.js +++ b/release.config.js @@ -1,113 +1,113 @@ export default { - branches: ['main'], - plugins: [ - '@semantic-release/commit-analyzer', - '@semantic-release/release-notes-generator', - [ - '@semantic-release/changelog', - { - changelogTitle: - '# Changelog' + - '\n\n> This file was generated automatically using [@semantic-release](https://github.com/semantic-release/semantic-release).', - }, - ], - [ - '@semantic-release/exec', - { - prepareCmd: 'npm run markdownlint:fix-changelog || true', - }, - ], - '@semantic-release/npm', - [ - '@semantic-release/git', - { - assets: ['package.json', 'package-lock.json', 'CHANGELOG.md'], - // eslint-disable-next-line no-template-curly-in-string - message: 'release: ${nextRelease.version} [skip ci]', - }, - ], - [ - '@semantic-release/github', - { - assets: [ - { - path: 'README.md', - label: 'Documentation', - }, - { - path: 'CHANGELOG.md', - label: 'Changelog', - }, - ], - }, - ], + branches: ['main'], + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + [ + '@semantic-release/changelog', + { + changelogTitle: + '# Changelog' + + '\n\n> This file was generated automatically using [@semantic-release](https://github.com/semantic-release/semantic-release).', + }, ], - preset: 'conventionalcommits', - presetConfig: { - types: [ - { - type: 'build', - section: 'Build System', - }, - { - type: 'chore', - section: 'Others', - }, - { - type: 'ci', - section: 'CI/CD', - }, - { - type: 'deps', - section: 'Dependencies', - }, - { - type: 'docs', - section: 'Documentation', - }, - { - type: 'feat', - section: 'Features', - }, - { - type: 'fix', - section: 'Bug Fixes', - }, - { - type: 'perf', - section: 'Performance Improvements', - }, - { - type: 'refactor', - section: 'Code Refactoring', - }, - { - type: 'revert', - section: 'Reverts', - }, - { - type: 'style', - section: 'Styling', - }, - { - type: 'test', - section: 'Tests', - }, - ], - releaseRules: [ - { - type: 'ci', - release: false, - }, - { - type: 'style', - release: false, - }, - { - type: 'test', - release: false, - }, + [ + '@semantic-release/exec', + { + prepareCmd: 'npm run markdownlint:fix-changelog || true', + }, + ], + '@semantic-release/npm', + [ + '@semantic-release/git', + { + assets: ['package.json', 'package-lock.json', 'CHANGELOG.md'], + // eslint-disable-next-line no-template-curly-in-string + message: 'release: ${nextRelease.version} [skip ci]', + }, + ], + [ + '@semantic-release/github', + { + assets: [ + { + path: 'README.md', + label: 'Documentation', + }, + { + path: 'CHANGELOG.md', + label: 'Changelog', + }, ], - userUrlFormat: 'https://github.com/{{user}}', - }, + }, + ], + ], + preset: 'conventionalcommits', + presetConfig: { + types: [ + { + type: 'build', + section: 'Build System', + }, + { + type: 'chore', + section: 'Others', + }, + { + type: 'ci', + section: 'CI/CD', + }, + { + type: 'deps', + section: 'Dependencies', + }, + { + type: 'docs', + section: 'Documentation', + }, + { + type: 'feat', + section: 'Features', + }, + { + type: 'fix', + section: 'Bug Fixes', + }, + { + type: 'perf', + section: 'Performance Improvements', + }, + { + type: 'refactor', + section: 'Code Refactoring', + }, + { + type: 'revert', + section: 'Reverts', + }, + { + type: 'style', + section: 'Styling', + }, + { + type: 'test', + section: 'Tests', + }, + ], + releaseRules: [ + { + type: 'ci', + release: false, + }, + { + type: 'style', + release: false, + }, + { + type: 'test', + release: false, + }, + ], + userUrlFormat: 'https://github.com/{{user}}', + }, }; diff --git a/src/cli/cli.ts b/src/cli/cli.ts index fbe4f58..879a8c6 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -10,158 +10,158 @@ import { Logger, LOG_SOURCE } from '../util/logger.js'; import { loadFormatter } from '../util/formatter-loader.js'; const packageJson = JSON.parse( - readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8'), + readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8'), ) as Record; const { argv } = yargsLib(hideBin(process.argv)) - .usage('Usage: $0 [options]') - .version((packageJson?.version as string) || 'unknown') - .command('$0 ', 'Check the files', (yargs) => { - yargs.positional('files', { - describe: 'Files to check', - type: 'string', - array: true, - demandOption: true, - }); - }) - .option('recursive', { - alias: 'r', - type: 'boolean', - description: 'Recursively search directories for Docker Compose files', - default: false, - }) - .option('fix', { - type: 'boolean', - description: 'Automatically fix problems', - default: false, - }) - .option('fix-dry-run', { - type: 'boolean', - description: 'Automatically fix problems without saving the changes to the file system', - default: false, - }) - .option('formatter', { - alias: 'f', - type: 'string', - description: 'Use a specific output format - default: stylish', - default: 'stylish', - }) - .option('config', { - alias: 'c', - type: 'string', - description: 'Path to config file', - }) - .option('quiet', { - alias: 'q', - type: 'boolean', - description: 'Report errors only', - default: false, - }) - .option('output-file', { - alias: 'o', - type: 'string', - description: 'Specify file to write report to', - }) - .option('color', { - type: 'boolean', - description: 'Force enabling/disabling of color', - default: true, - }) - .option('debug', { - type: 'boolean', - description: 'Output debugging information', - default: undefined, - }) - .option('exclude', { - alias: 'e', - type: 'array', - description: 'Files or directories to exclude from the search', - default: [], - }) - .option('max-warnings', { - type: 'number', - description: 'Number of warnings to trigger nonzero exit code', - default: -1, - }) - .help() - .alias('version', 'v'); + .usage('Usage: $0 [options]') + .version((packageJson?.version as string) || 'unknown') + .command('$0 ', 'Check the files', (yargs) => { + yargs.positional('files', { + describe: 'Files to check', + type: 'string', + array: true, + demandOption: true, + }); + }) + .option('recursive', { + alias: 'r', + type: 'boolean', + description: 'Recursively search directories for Docker Compose files', + default: false, + }) + .option('fix', { + type: 'boolean', + description: 'Automatically fix problems', + default: false, + }) + .option('fix-dry-run', { + type: 'boolean', + description: 'Automatically fix problems without saving the changes to the file system', + default: false, + }) + .option('formatter', { + alias: 'f', + type: 'string', + description: 'Use a specific output format - default: stylish', + default: 'stylish', + }) + .option('config', { + alias: 'c', + type: 'string', + description: 'Path to config file', + }) + .option('quiet', { + alias: 'q', + type: 'boolean', + description: 'Report errors only', + default: false, + }) + .option('output-file', { + alias: 'o', + type: 'string', + description: 'Specify file to write report to', + }) + .option('color', { + type: 'boolean', + description: 'Force enabling/disabling of color', + default: true, + }) + .option('debug', { + type: 'boolean', + description: 'Output debugging information', + default: undefined, + }) + .option('exclude', { + alias: 'e', + type: 'array', + description: 'Files or directories to exclude from the search', + default: [], + }) + .option('max-warnings', { + type: 'number', + description: 'Number of warnings to trigger nonzero exit code', + default: -1, + }) + .help() + .alias('version', 'v'); export default async function cli() { - const args = (await argv) as unknown as CLIConfig; - - // Initialize the logger with the final debug and color options - Logger.init(args.debug); - const logger = Logger.getInstance(); - - logger.debug(LOG_SOURCE.CLI, 'Debug mode is ON'); - logger.debug(LOG_SOURCE.CLI, 'Arguments:', args); - - const config = await loadConfig(args.config); - - // Override config values with CLI arguments if they are provided - if (args.quiet) { - config.quiet = args.quiet; - } - if (args.debug) { - config.debug = args.debug; - } - if (args.exclude.length > 0) { - config.exclude = args.exclude; - } - logger.debug(LOG_SOURCE.CLI, 'Final config:', config); - - const linter = new DCLinter(config); - - // Handle the `fix` and `fix-dry-run` flags - if (args.fix || args.fixDryRun) { - await linter.fixFiles(args.files, args.recursive, args.fixDryRun); - } - - // Always run the linter after attempting to fix issues - let lintResults = await linter.lintFiles(args.files, args.recursive); - - // Filter out warnings if `--quiet` is enabled - if (args.quiet) { - // Keep only files with errors - lintResults = lintResults - .map((result) => ({ - ...result, - messages: result.messages.filter((message) => message.type === 'error'), - errorCount: result.messages.filter((message) => message.type === 'error').length, - warningCount: 0, - })) - .filter((result) => result.messages.length > 0); - } - - // Count errors and warnings - const totalErrors = lintResults.reduce((count, result) => count + result.errorCount, 0); - const totalWarnings = lintResults.reduce((count, result) => count + result.warningCount, 0); - - // Choose and apply the formatter - const formatter = await loadFormatter(args.formatter); - const formattedResults = formatter(lintResults); - - // Output results - if (args.outputFile) { - writeFileSync(args.outputFile, formattedResults); - } else { - console.log(formattedResults); - } - - // Determine exit code based on errors and warnings - if (totalErrors > 0) { - logger.debug(LOG_SOURCE.CLI, `${totalErrors} errors found`); - process.exit(1); - } else if (args.maxWarnings && args.maxWarnings >= 0 && totalWarnings > args.maxWarnings) { - logger.debug( - LOG_SOURCE.CLI, - `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${args.maxWarnings})`, - ); - process.exit(1); - } - - logger.debug(LOG_SOURCE.CLI, 'All files are valid.'); - process.exit(0); + const args = (await argv) as unknown as CLIConfig; + + // Initialize the logger with the final debug and color options + Logger.init(args.debug); + const logger = Logger.getInstance(); + + logger.debug(LOG_SOURCE.CLI, 'Debug mode is ON'); + logger.debug(LOG_SOURCE.CLI, 'Arguments:', args); + + const config = await loadConfig(args.config); + + // Override config values with CLI arguments if they are provided + if (args.quiet) { + config.quiet = args.quiet; + } + if (args.debug) { + config.debug = args.debug; + } + if (args.exclude.length > 0) { + config.exclude = args.exclude; + } + logger.debug(LOG_SOURCE.CLI, 'Final config:', config); + + const linter = new DCLinter(config); + + // Handle the `fix` and `fix-dry-run` flags + if (args.fix || args.fixDryRun) { + await linter.fixFiles(args.files, args.recursive, args.fixDryRun); + } + + // Always run the linter after attempting to fix issues + let lintResults = await linter.lintFiles(args.files, args.recursive); + + // Filter out warnings if `--quiet` is enabled + if (args.quiet) { + // Keep only files with errors + lintResults = lintResults + .map((result) => ({ + ...result, + messages: result.messages.filter((message) => message.type === 'error'), + errorCount: result.messages.filter((message) => message.type === 'error').length, + warningCount: 0, + })) + .filter((result) => result.messages.length > 0); + } + + // Count errors and warnings + const totalErrors = lintResults.reduce((count, result) => count + result.errorCount, 0); + const totalWarnings = lintResults.reduce((count, result) => count + result.warningCount, 0); + + // Choose and apply the formatter + const formatter = await loadFormatter(args.formatter); + const formattedResults = formatter(lintResults); + + // Output results + if (args.outputFile) { + writeFileSync(args.outputFile, formattedResults); + } else { + console.log(formattedResults); + } + + // Determine exit code based on errors and warnings + if (totalErrors > 0) { + logger.debug(LOG_SOURCE.CLI, `${totalErrors} errors found`); + process.exit(1); + } else if (args.maxWarnings && args.maxWarnings >= 0 && totalWarnings > args.maxWarnings) { + logger.debug( + LOG_SOURCE.CLI, + `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${args.maxWarnings})`, + ); + process.exit(1); + } + + logger.debug(LOG_SOURCE.CLI, 'All files are valid.'); + process.exit(0); } await cli(); diff --git a/src/cli/cli.types.ts b/src/cli/cli.types.ts index 256050e..a098bea 100644 --- a/src/cli/cli.types.ts +++ b/src/cli/cli.types.ts @@ -1,16 +1,16 @@ interface CLIConfig { - files: string[]; - recursive: boolean; - fix: boolean; - fixDryRun: boolean; - formatter: string; - config?: string; - quiet: boolean; - maxWarnings?: number; - outputFile?: string; - color: boolean; - debug: boolean; - exclude: string[]; + files: string[]; + recursive: boolean; + fix: boolean; + fixDryRun: boolean; + formatter: string; + config?: string; + quiet: boolean; + maxWarnings?: number; + outputFile?: string; + color: boolean; + debug: boolean; + exclude: string[]; } export { CLIConfig }; diff --git a/src/config/config.ts b/src/config/config.ts index cfd52cf..7e0ab9d 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -5,43 +5,43 @@ import { Logger, LOG_SOURCE } from '../util/logger.js'; import { loadSchema } from '../util/load-schema.js'; function getDefaultConfig(): Config { - return { - rules: {}, - quiet: false, - debug: false, - exclude: [], - }; + return { + rules: {}, + quiet: false, + debug: false, + exclude: [], + }; } async function validateConfig(config: Config): Promise { - const logger = Logger.getInstance(); - logger.debug(LOG_SOURCE.CONFIG, 'Starting config validation'); - - const ajv = new Ajv(); - const schema = loadSchema('cli-config'); - const validate = ajv.compile(schema); - - if (!validate(config)) { - logger.error('Invalid configuration:', validate.errors); - process.exit(1); - } - logger.debug(LOG_SOURCE.CONFIG, 'Validation complete'); - return config; + const logger = Logger.getInstance(); + logger.debug(LOG_SOURCE.CONFIG, 'Starting config validation'); + + const ajv = new Ajv(); + const schema = loadSchema('cli-config'); + const validate = ajv.compile(schema); + + if (!validate(config)) { + logger.error('Invalid configuration:', validate.errors); + process.exit(1); + } + logger.debug(LOG_SOURCE.CONFIG, 'Validation complete'); + return config; } async function loadConfig(configPath?: string): Promise { - const logger = Logger.getInstance(); - const explorer = cosmiconfig('dclint'); - - const result = configPath ? await explorer.load(configPath) : await explorer.search(); - - if (result && result.config) { - logger.debug(LOG_SOURCE.CONFIG, `Configuration load from ${result.filepath}.`); - const config = result.config as unknown as Config; - return validateConfig(config); - } - logger.debug(LOG_SOURCE.CONFIG, 'Configuration file not found. Using default'); - return getDefaultConfig(); + const logger = Logger.getInstance(); + const explorer = cosmiconfig('dclint'); + + const result = configPath ? await explorer.load(configPath) : await explorer.search(); + + if (result && result.config) { + logger.debug(LOG_SOURCE.CONFIG, `Configuration load from ${result.filepath}.`); + const config = result.config as unknown as Config; + return validateConfig(config); + } + logger.debug(LOG_SOURCE.CONFIG, 'Configuration file not found. Using default'); + return getDefaultConfig(); } export { loadConfig }; diff --git a/src/config/config.types.ts b/src/config/config.types.ts index 9bf77ca..894b950 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -3,12 +3,12 @@ type ConfigRuleLevel = 0 | 1 | 2; type ConfigRule = ConfigRuleLevel | [ConfigRuleLevel, Record?]; interface Config { - rules: { - [ruleName: string]: ConfigRule; - }; - quiet: boolean; - debug: boolean; - exclude: string[]; + rules: { + [ruleName: string]: ConfigRule; + }; + quiet: boolean; + debug: boolean; + exclude: string[]; } export { Config, ConfigRule, ConfigRuleLevel }; diff --git a/src/errors/compose-validation-error.ts b/src/errors/compose-validation-error.ts index 260491b..e0e6cd2 100644 --- a/src/errors/compose-validation-error.ts +++ b/src/errors/compose-validation-error.ts @@ -1,26 +1,26 @@ import type { ErrorObject } from 'ajv'; class ComposeValidationError extends Error { - keyword: string; + keyword: string; - instancePath: string; + instancePath: string; - schemaPath: string; + schemaPath: string; - details: ErrorObject; + details: ErrorObject; - constructor(e: ErrorObject) { - super(`Validation error: ${e?.message}`); - this.name = 'ComposeValidationError'; - this.keyword = e.keyword; - this.instancePath = e.instancePath; - this.schemaPath = e.schemaPath; - this.details = e; - } + constructor(e: ErrorObject) { + super(`Validation error: ${e?.message}`); + this.name = 'ComposeValidationError'; + this.keyword = e.keyword; + this.instancePath = e.instancePath; + this.schemaPath = e.schemaPath; + this.details = e; + } - toString(): string { - return `ComposeValidationError: instancePath="${this.instancePath}", schemaPath="${this.schemaPath}", message="${this.message}".`; - } + toString(): string { + return `ComposeValidationError: instancePath="${this.instancePath}", schemaPath="${this.schemaPath}", message="${this.message}".`; + } } export { ComposeValidationError }; diff --git a/src/errors/file-not-found-error.ts b/src/errors/file-not-found-error.ts index cc3aa2e..4fc3051 100644 --- a/src/errors/file-not-found-error.ts +++ b/src/errors/file-not-found-error.ts @@ -1,9 +1,9 @@ class FileNotFoundError extends Error { - constructor(filePath?: string) { - super(); - this.message = `File or directory not found: ${filePath}`; - this.name = 'FileNotFoundError'; - } + constructor(filePath?: string) { + super(); + this.message = `File or directory not found: ${filePath}`; + this.name = 'FileNotFoundError'; + } } export { FileNotFoundError }; diff --git a/src/formatters/codeclimate.ts b/src/formatters/codeclimate.ts index e011ba0..840fb0e 100644 --- a/src/formatters/codeclimate.ts +++ b/src/formatters/codeclimate.ts @@ -2,62 +2,62 @@ import { createHash } from 'node:crypto'; import type { LintResult } from '../linter/linter.types.js'; const generateFingerprint = (data: (string | null)[], hashes: Set): string => { - const hash = createHash('md5'); + const hash = createHash('md5'); - // Filter out null values and update the hash - data.filter(Boolean).forEach((part) => { - hash.update(part!.toString()); // Using non-null assertion since filter removed null values - }); + // Filter out null values and update the hash + data.filter(Boolean).forEach((part) => { + hash.update(part!.toString()); // Using non-null assertion since filter removed null values + }); - // Hash collisions should not happen, but if they do, a random hash will be generated. - const hashCopy = hash.copy(); - let digest = hash.digest('hex'); - if (hashes.has(digest)) { - hashCopy.update(Math.random().toString()); - digest = hashCopy.digest('hex'); - } + // Hash collisions should not happen, but if they do, a random hash will be generated. + const hashCopy = hash.copy(); + let digest = hash.digest('hex'); + if (hashes.has(digest)) { + hashCopy.update(Math.random().toString()); + digest = hashCopy.digest('hex'); + } - hashes.add(digest); + hashes.add(digest); - return digest; + return digest; }; export default function codeclimateFormatter(results: LintResult[]): string { - const hashes = new Set(); - - const issues = results.flatMap((result) => { - return result.messages.map((message) => ({ - type: 'issue', - check_name: message.rule, - description: message.message, - content: { - body: `Error found in ${message.rule}`, - }, - categories: ['Style'], - location: { - path: result.filePath, - lines: { - begin: message.line, - end: message.endLine ?? message.line, - }, - positions: { - begin: { - line: message.line, - column: message.column, - }, - end: { - line: message.endLine ?? message.line, - column: message.endColumn ?? message.column, - }, - }, - }, - severity: message.severity, - fingerprint: generateFingerprint( - [result.filePath, message.rule, message.message, `${message.line}`, `${message.column}`], - hashes, - ), - })); - }); - - return JSON.stringify(issues); + const hashes = new Set(); + + const issues = results.flatMap((result) => { + return result.messages.map((message) => ({ + type: 'issue', + check_name: message.rule, + description: message.message, + content: { + body: `Error found in ${message.rule}`, + }, + categories: ['Style'], + location: { + path: result.filePath, + lines: { + begin: message.line, + end: message.endLine ?? message.line, + }, + positions: { + begin: { + line: message.line, + column: message.column, + }, + end: { + line: message.endLine ?? message.line, + column: message.endColumn ?? message.column, + }, + }, + }, + severity: message.severity, + fingerprint: generateFingerprint( + [result.filePath, message.rule, message.message, `${message.line}`, `${message.column}`], + hashes, + ), + })); + }); + + return JSON.stringify(issues); } diff --git a/src/formatters/compact.ts b/src/formatters/compact.ts index 91df8f4..58c8d97 100644 --- a/src/formatters/compact.ts +++ b/src/formatters/compact.ts @@ -1,13 +1,13 @@ import type { LintResult } from '../linter/linter.types.js'; export default function compactFormatter(results: LintResult[]): string { - return results - .map((result) => { - return result.messages - .map((error) => { - return `${result.filePath}:${error.line}:${error.column} ${error.message} [${error.rule}]`; - }) - .join('\n'); + return results + .map((result) => { + return result.messages + .map((error) => { + return `${result.filePath}:${error.line}:${error.column} ${error.message} [${error.rule}]`; }) - .join('\n\n'); + .join('\n'); + }) + .join('\n\n'); } diff --git a/src/formatters/json.ts b/src/formatters/json.ts index baa1073..9e492e4 100644 --- a/src/formatters/json.ts +++ b/src/formatters/json.ts @@ -1,5 +1,5 @@ import type { LintResult } from '../linter/linter.types.js'; export default function jsonFormatter(results: LintResult[]): string { - return JSON.stringify(results, null, 2); + return JSON.stringify(results, null, 2); } diff --git a/src/formatters/junit.ts b/src/formatters/junit.ts index c41419e..eed9259 100644 --- a/src/formatters/junit.ts +++ b/src/formatters/junit.ts @@ -1,46 +1,46 @@ import type { LintResult } from '../linter/linter.types.js'; function escapeXml(unsafe: string): string { - return unsafe.replace(/[<>&'"]/g, (c) => { - switch (c) { - case '<': - return '<'; - case '>': - return '>'; - case '&': - return '&'; - case '"': - return '"'; - case "'": - return '''; - default: - return c; - } - }); + return unsafe.replace(/[<>&'"]/g, (c) => { + switch (c) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case '"': + return '"'; + case "'": + return '''; + default: + return c; + } + }); } export default function junitFormatter(results: LintResult[]): string { - const testSuites = results - .map((result) => { - const testCases = result.messages - .map((error) => { - return ` + const testSuites = results + .map((result) => { + const testCases = result.messages + .map((error) => { + return ` ${escapeXml(result.filePath)}:${error.line}:${error.column} `; - }) - .join(''); + }) + .join(''); - return ` + return ` ${testCases} `; - }) - .join(''); + }) + .join(''); - return ` + return ` ${testSuites} `; diff --git a/src/formatters/stylish.ts b/src/formatters/stylish.ts index 1e9844c..117bfbd 100644 --- a/src/formatters/stylish.ts +++ b/src/formatters/stylish.ts @@ -3,63 +3,63 @@ import chalk from 'chalk'; import type { LintResult } from '../linter/linter.types.js'; export default function stylishFormatter(results: LintResult[]): string { - let output = ''; - let errorCount = 0; - let warningCount = 0; - let fixableErrorCount = 0; - let fixableWarningCount = 0; + let output = ''; + let errorCount = 0; + let warningCount = 0; + let fixableErrorCount = 0; + let fixableWarningCount = 0; - results.forEach((result) => { - if (result.messages.length === 0) { - return; - } + results.forEach((result) => { + if (result.messages.length === 0) { + return; + } - // Format the file path header without nested template literals - const filePath = chalk.underline(path.resolve(result.filePath)); - output += `\n${filePath}\n`; + // Format the file path header without nested template literals + const filePath = chalk.underline(path.resolve(result.filePath)); + output += `\n${filePath}\n`; - result.messages.forEach((msg) => { - const { type } = msg; - const color = type === 'error' ? chalk.red : chalk.yellow; - const line = msg.line.toString().padStart(4, ' '); - const column = msg.column.toString().padEnd(4, ' '); + result.messages.forEach((msg) => { + const { type } = msg; + const color = type === 'error' ? chalk.red : chalk.yellow; + const line = msg.line.toString().padStart(4, ' '); + const column = msg.column.toString().padEnd(4, ' '); - // Break down message formatting into separate parts - const position = chalk.dim(`${line}:${column}`); - const formattedType = color(type); - const ruleInfo = chalk.dim(msg.rule); + // Break down message formatting into separate parts + const position = chalk.dim(`${line}:${column}`); + const formattedType = color(type); + const ruleInfo = chalk.dim(msg.rule); - output += `${position} ${formattedType} ${msg.message} ${ruleInfo}\n`; + output += `${position} ${formattedType} ${msg.message} ${ruleInfo}\n`; - // Increment counts without using the ++ operator - if (type === 'error') { - errorCount += 1; - if (msg.fixable) { - fixableErrorCount += 1; - } - } else { - warningCount += 1; - if (msg.fixable) { - fixableWarningCount += 1; - } - } - }); + // Increment counts without using the ++ operator + if (type === 'error') { + errorCount += 1; + if (msg.fixable) { + fixableErrorCount += 1; + } + } else { + warningCount += 1; + if (msg.fixable) { + fixableWarningCount += 1; + } + } }); + }); - const totalProblems = errorCount + warningCount; - if (totalProblems > 0) { - const problemSummary = chalk.red.bold(`✖ ${totalProblems} problems`); - const errorSummary = chalk.red.bold(`${errorCount} errors`); - const warningSummary = chalk.yellow.bold(`${warningCount} warnings`); - output += `\n${problemSummary} (${errorSummary}, ${warningSummary})\n`; - } + const totalProblems = errorCount + warningCount; + if (totalProblems > 0) { + const problemSummary = chalk.red.bold(`✖ ${totalProblems} problems`); + const errorSummary = chalk.red.bold(`${errorCount} errors`); + const warningSummary = chalk.yellow.bold(`${warningCount} warnings`); + output += `\n${problemSummary} (${errorSummary}, ${warningSummary})\n`; + } - if (fixableErrorCount > 0 || fixableWarningCount > 0) { - const fixableSummary = chalk.green.bold( - `${fixableErrorCount} errors and ${fixableWarningCount} warnings potentially fixable with the \`--fix\` option.`, - ); - output += `${fixableSummary}\n`; - } + if (fixableErrorCount > 0 || fixableWarningCount > 0) { + const fixableSummary = chalk.green.bold( + `${fixableErrorCount} errors and ${fixableWarningCount} warnings potentially fixable with the \`--fix\` option.`, + ); + output += `${fixableSummary}\n`; + } - return output; + return output; } diff --git a/src/linter/linter.ts b/src/linter/linter.ts index f09a54f..ed18b55 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -9,165 +9,163 @@ import { validationComposeSchema } from '../util/compose-validation.js'; import { ComposeValidationError } from '../errors/compose-validation-error.js'; class DCLinter { - private readonly config: Config; + private readonly config: Config; - private rules: LintRule[]; + private rules: LintRule[]; - constructor(config: Config) { - this.config = config; - this.rules = []; - Logger.init(this.config.debug); - } + constructor(config: Config) { + this.config = config; + this.rules = []; + Logger.init(this.config.debug); + } - private async loadRules() { - if (this.rules.length === 0) { - this.rules = await loadLintRules(this.config); - } + private async loadRules() { + if (this.rules.length === 0) { + this.rules = await loadLintRules(this.config); } + } - private lintContent(context: LintContext): LintMessage[] { - const messages: LintMessage[] = []; + private lintContent(context: LintContext): LintMessage[] { + const messages: LintMessage[] = []; - this.rules.forEach((rule) => { - const ruleMessages = rule.check(context); - messages.push(...ruleMessages); - }); + this.rules.forEach((rule) => { + const ruleMessages = rule.check(context); + messages.push(...ruleMessages); + }); - return messages; - } + return messages; + } - private static validateFile(file: string): { context: LintContext | null; messages: LintMessage[] } { - const logger = Logger.getInstance(); - const messages: LintMessage[] = []; - const context: LintContext = { path: file, content: {}, sourceCode: '' }; - - try { - context.sourceCode = fs.readFileSync(file, 'utf8'); - const doc = parseDocument(context.sourceCode, { merge: true }); - - if (doc.errors && doc.errors.length > 0) { - doc.errors.forEach((error) => { - throw error; - }); - } - - // Use Record to type the parsed content safely - context.content = doc.toJS() as Record; - validationComposeSchema(context.content); - } catch (e: unknown) { - if (e instanceof YAMLError) { - const startPos: { line: number; col: number } | undefined = Array.isArray(e.linePos) - ? e.linePos[0] - : e.linePos; - messages.push({ - rule: 'invalid-yaml', - category: 'style', - severity: 'critical', - message: 'Invalid YAML format.', - line: startPos?.line || 1, - column: startPos?.col || 1, - type: 'error', - fixable: false, - }); - } else if (e instanceof ComposeValidationError) { - messages.push({ - rule: 'invalid-schema', - type: 'error', - category: 'style', - severity: 'critical', - message: e.toString(), - line: 1, - column: 1, - fixable: false, - }); - } else { - messages.push({ - rule: 'unknown-error', - category: 'style', - severity: 'critical', - message: 'unknown-error', - line: 1, - column: 1, - type: 'error', - fixable: false, - }); - logger.debug(LOG_SOURCE.LINTER, `Error while processing file ${file}`, e); - } - - return { context: null, messages }; - } + private static validateFile(file: string): { context: LintContext | null; messages: LintMessage[] } { + const logger = Logger.getInstance(); + const messages: LintMessage[] = []; + const context: LintContext = { path: file, content: {}, sourceCode: '' }; - return { context, messages }; - } + try { + context.sourceCode = fs.readFileSync(file, 'utf8'); + const doc = parseDocument(context.sourceCode, { merge: true }); - public async lintFiles(paths: string[], doRecursiveSearch: boolean): Promise { - const logger = Logger.getInstance(); - const lintResults: LintResult[] = []; - await this.loadRules(); - const files = findFilesForLinting(paths, doRecursiveSearch, this.config.exclude); - logger.debug(LOG_SOURCE.LINTER, `Compose files for linting: ${files.toString()}`); - - files.forEach((file) => { - logger.debug(LOG_SOURCE.LINTER, `Linting file: ${file}`); - - const { context, messages } = DCLinter.validateFile(file); - if (context) { - const fileMessages = this.lintContent(context); - messages.push(...fileMessages); - } - - const errorCount = messages.filter((msg) => msg.type === 'error').length; - const warningCount = messages.filter((msg) => msg.type === 'warning').length; - const fixableErrorCount = messages.filter((msg) => msg.fixable && msg.type === 'error').length; - const fixableWarningCount = messages.filter((msg) => msg.fixable && msg.type === 'warning').length; - - lintResults.push({ - filePath: file, - messages, - errorCount, - warningCount, - fixableErrorCount, - fixableWarningCount, - }); + if (doc.errors && doc.errors.length > 0) { + doc.errors.forEach((error) => { + throw error; }); + } + + // Use Record to type the parsed content safely + context.content = doc.toJS() as Record; + validationComposeSchema(context.content); + } catch (e: unknown) { + if (e instanceof YAMLError) { + const startPos: { line: number; col: number } | undefined = Array.isArray(e.linePos) ? e.linePos[0] : e.linePos; + messages.push({ + rule: 'invalid-yaml', + category: 'style', + severity: 'critical', + message: 'Invalid YAML format.', + line: startPos?.line || 1, + column: startPos?.col || 1, + type: 'error', + fixable: false, + }); + } else if (e instanceof ComposeValidationError) { + messages.push({ + rule: 'invalid-schema', + type: 'error', + category: 'style', + severity: 'critical', + message: e.toString(), + line: 1, + column: 1, + fixable: false, + }); + } else { + messages.push({ + rule: 'unknown-error', + category: 'style', + severity: 'critical', + message: 'unknown-error', + line: 1, + column: 1, + type: 'error', + fixable: false, + }); + logger.debug(LOG_SOURCE.LINTER, `Error while processing file ${file}`, e); + } - logger.debug(LOG_SOURCE.LINTER, 'Linting result:', JSON.stringify(lintResults)); - return lintResults; + return { context: null, messages }; } - public async fixFiles(paths: string[], doRecursiveSearch: boolean, dryRun: boolean = false): Promise { - const logger = Logger.getInstance(); - await this.loadRules(); - const files = findFilesForLinting(paths, doRecursiveSearch, this.config.exclude); - logger.debug(LOG_SOURCE.LINTER, `Compose files for fixing: ${files.toString()}`); - - files.forEach((file) => { - logger.debug(LOG_SOURCE.LINTER, `Fixing file: ${file}`); - - const { context, messages } = DCLinter.validateFile(file); - if (!context) { - logger.debug(LOG_SOURCE.LINTER, `Skipping file due to validation errors: ${file}`); - messages.forEach((message) => logger.debug(LOG_SOURCE.LINTER, JSON.stringify(message))); - return; - } - - let content = context.sourceCode; - - this.rules.forEach((rule) => { - if (typeof rule.fix === 'function') { - content = rule.fix(content); - } - }); - - if (dryRun) { - logger.info(`Dry run - changes for file: ${file}`); - logger.info('\n\n', content); - } else { - fs.writeFileSync(file, content, 'utf8'); - logger.debug(LOG_SOURCE.LINTER, `File fixed: ${file}`); - } - }); - } + return { context, messages }; + } + + public async lintFiles(paths: string[], doRecursiveSearch: boolean): Promise { + const logger = Logger.getInstance(); + const lintResults: LintResult[] = []; + await this.loadRules(); + const files = findFilesForLinting(paths, doRecursiveSearch, this.config.exclude); + logger.debug(LOG_SOURCE.LINTER, `Compose files for linting: ${files.toString()}`); + + files.forEach((file) => { + logger.debug(LOG_SOURCE.LINTER, `Linting file: ${file}`); + + const { context, messages } = DCLinter.validateFile(file); + if (context) { + const fileMessages = this.lintContent(context); + messages.push(...fileMessages); + } + + const errorCount = messages.filter((msg) => msg.type === 'error').length; + const warningCount = messages.filter((msg) => msg.type === 'warning').length; + const fixableErrorCount = messages.filter((msg) => msg.fixable && msg.type === 'error').length; + const fixableWarningCount = messages.filter((msg) => msg.fixable && msg.type === 'warning').length; + + lintResults.push({ + filePath: file, + messages, + errorCount, + warningCount, + fixableErrorCount, + fixableWarningCount, + }); + }); + + logger.debug(LOG_SOURCE.LINTER, 'Linting result:', JSON.stringify(lintResults)); + return lintResults; + } + + public async fixFiles(paths: string[], doRecursiveSearch: boolean, dryRun: boolean = false): Promise { + const logger = Logger.getInstance(); + await this.loadRules(); + const files = findFilesForLinting(paths, doRecursiveSearch, this.config.exclude); + logger.debug(LOG_SOURCE.LINTER, `Compose files for fixing: ${files.toString()}`); + + files.forEach((file) => { + logger.debug(LOG_SOURCE.LINTER, `Fixing file: ${file}`); + + const { context, messages } = DCLinter.validateFile(file); + if (!context) { + logger.debug(LOG_SOURCE.LINTER, `Skipping file due to validation errors: ${file}`); + messages.forEach((message) => logger.debug(LOG_SOURCE.LINTER, JSON.stringify(message))); + return; + } + + let content = context.sourceCode; + + this.rules.forEach((rule) => { + if (typeof rule.fix === 'function') { + content = rule.fix(content); + } + }); + + if (dryRun) { + logger.info(`Dry run - changes for file: ${file}`); + logger.info('\n\n', content); + } else { + fs.writeFileSync(file, content, 'utf8'); + logger.debug(LOG_SOURCE.LINTER, `File fixed: ${file}`); + } + }); + } } export { DCLinter }; diff --git a/src/linter/linter.types.ts b/src/linter/linter.types.ts index 9b3d57d..60cca86 100644 --- a/src/linter/linter.types.ts +++ b/src/linter/linter.types.ts @@ -1,53 +1,53 @@ interface LintContext { - path: string; - content: object; - sourceCode: string; + path: string; + content: object; + sourceCode: string; } interface LintMessage { - rule: string; // Rule name, e.g. 'no-quotes-in-volumes' - type: LintMessageType; // The type of the message, e.g. 'error' - severity: LintRuleSeverity; // The severity level of the issue - category: LintRuleCategory; // The category of the issue (style, security, etc.) - message: string; // Error message - line: number; // The line number where the issue is located - column: number; // The column number where the issue starts - endLine?: number; // The line number where the issue ends (optional) - endColumn?: number; // The column number where the issue ends (optional) - meta?: RuleMeta; // Metadata about the rule, including description and URL - fixable: boolean; // Is it possible to fix this issue + rule: string; // Rule name, e.g. 'no-quotes-in-volumes' + type: LintMessageType; // The type of the message, e.g. 'error' + severity: LintRuleSeverity; // The severity level of the issue + category: LintRuleCategory; // The category of the issue (style, security, etc.) + message: string; // Error message + line: number; // The line number where the issue is located + column: number; // The column number where the issue starts + endLine?: number; // The line number where the issue ends (optional) + endColumn?: number; // The column number where the issue ends (optional) + meta?: RuleMeta; // Metadata about the rule, including description and URL + fixable: boolean; // Is it possible to fix this issue } interface LintResult { - filePath: string; // Path to the file that was linted - messages: LintMessage[]; // Array of lint messages (errors and warnings) - errorCount: number; // Total number of errors found - warningCount: number; // Total number of warnings found - fixableErrorCount?: number; // Total number of fixable errors (optional) - fixableWarningCount?: number; // Total number of fixable warnings (optional) + filePath: string; // Path to the file that was linted + messages: LintMessage[]; // Array of lint messages (errors and warnings) + errorCount: number; // Total number of errors found + warningCount: number; // Total number of warnings found + fixableErrorCount?: number; // Total number of fixable errors (optional) + fixableWarningCount?: number; // Total number of fixable warnings (optional) } interface RuleMeta { - description: string; // A brief description of the rule - url: string; // A URL to documentation or resources related to the rule + description: string; // A brief description of the rule + url: string; // A URL to documentation or resources related to the rule } interface LintRule { - name: string; // The name of the rule, e.g., 'no-console' - type: LintMessageType; // The type of the message, e.g. 'error' - meta: RuleMeta; // Metadata about the rule, including description and URL - category: LintRuleCategory; // Category under which this rule falls - severity: LintRuleSeverity; // Default severity level for this rule - fixable: boolean; // Is it possible to fix this + name: string; // The name of the rule, e.g., 'no-console' + type: LintMessageType; // The type of the message, e.g. 'error' + meta: RuleMeta; // Metadata about the rule, including description and URL + category: LintRuleCategory; // Category under which this rule falls + severity: LintRuleSeverity; // Default severity level for this rule + fixable: boolean; // Is it possible to fix this - // Method for generating an error message if the rule is violated - getMessage(details?: object): string; + // Method for generating an error message if the rule is violated + getMessage(details?: object): string; - // Checks the file content and returns a list of lint messages - check(content: object, type?: LintMessageType): LintMessage[]; + // Checks the file content and returns a list of lint messages + check(content: object, type?: LintMessageType): LintMessage[]; - // Auto-fix that corrects errors in the file and returns the fixed content - fix?(content: string): string; + // Auto-fix that corrects errors in the file and returns the fixed content + fix?(content: string): string; } type LintMessageType = 'warning' | 'error'; @@ -57,12 +57,12 @@ type LintRuleSeverity = 'info' | 'minor' | 'major' | 'critical'; type LintRuleCategory = 'style' | 'security' | 'best-practice' | 'performance'; export { - LintContext, - LintMessage, - LintResult, - LintRule, - RuleMeta, - LintMessageType, - LintRuleSeverity, - LintRuleCategory, + LintContext, + LintMessage, + LintResult, + LintRule, + RuleMeta, + LintMessageType, + LintRuleSeverity, + LintRuleCategory, }; diff --git a/src/rules/no-build-and-image-rule.ts b/src/rules/no-build-and-image-rule.ts index 27f4d51..559abed 100644 --- a/src/rules/no-build-and-image-rule.ts +++ b/src/rules/no-build-and-image-rule.ts @@ -1,82 +1,82 @@ import { parseDocument, isMap, isScalar } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; interface NoBuildAndImageRuleOptions { - checkPullPolicy?: boolean; + checkPullPolicy?: boolean; } export default class NoBuildAndImageRule implements LintRule { - public name = 'no-build-and-image'; + public name = 'no-build-and-image'; - public type: LintMessageType = 'error'; + public type: LintMessageType = 'error'; - public category: LintRuleCategory = 'best-practice'; + public category: LintRuleCategory = 'best-practice'; - public severity: LintRuleSeverity = 'major'; + public severity: LintRuleSeverity = 'major'; - public meta: RuleMeta = { - description: - 'Ensure that each service uses either "build" or "image", but not both, to prevent ambiguity in Docker Compose configurations.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-build-and-image-rule.md', - }; + public meta: RuleMeta = { + description: + 'Ensure that each service uses either "build" or "image", but not both, to prevent ambiguity in Docker Compose configurations.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-build-and-image-rule.md', + }; - public fixable: boolean = false; + public fixable: boolean = false; - private readonly checkPullPolicy: boolean; + private readonly checkPullPolicy: boolean; - constructor(options?: NoBuildAndImageRuleOptions) { - this.checkPullPolicy = options?.checkPullPolicy ?? true; - } + constructor(options?: NoBuildAndImageRuleOptions) { + this.checkPullPolicy = options?.checkPullPolicy ?? true; + } - // eslint-disable-next-line class-methods-use-this - public getMessage({ serviceName }: { serviceName: string }): string { - return `Service "${serviceName}" is using both "build" and "image". Use either "build" or "image" but not both.`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ serviceName }: { serviceName: string }): string { + return `Service "${serviceName}" is using both "build" and "image". Use either "build" or "image" but not both.`; + } - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); - if (!isMap(services)) return []; + if (!isMap(services)) return []; - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; - if (!isMap(service)) return; + if (!isMap(service)) return; - const hasBuild = service.has('build'); - const hasImage = service.has('image'); - const hasPullPolicy = service.has('pull_policy'); + const hasBuild = service.has('build'); + const hasImage = service.has('image'); + const hasPullPolicy = service.has('pull_policy'); - if (hasBuild && hasImage && (!this.checkPullPolicy || !hasPullPolicy)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'build'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ serviceName }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + if (hasBuild && hasImage && (!this.checkPullPolicy || !hasPullPolicy)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'build'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ serviceName }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } } diff --git a/src/rules/no-duplicate-container-names-rule.ts b/src/rules/no-duplicate-container-names-rule.ts index 343cf3f..eb17309 100644 --- a/src/rules/no-duplicate-container-names-rule.ts +++ b/src/rules/no-duplicate-container-names-rule.ts @@ -1,86 +1,86 @@ import { parseDocument, isMap, isScalar } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; export default class NoDuplicateContainerNamesRule implements LintRule { - public name = 'no-duplicate-container-names'; + public name = 'no-duplicate-container-names'; - public type: LintMessageType = 'error'; + public type: LintMessageType = 'error'; - public category: LintRuleCategory = 'security'; + public category: LintRuleCategory = 'security'; - public severity: LintRuleSeverity = 'critical'; + public severity: LintRuleSeverity = 'critical'; - public meta: RuleMeta = { - description: - 'Ensure that container names in Docker Compose are unique to prevent name conflicts and ensure proper container management.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-container-names-rule.md', - }; + public meta: RuleMeta = { + description: + 'Ensure that container names in Docker Compose are unique to prevent name conflicts and ensure proper container management.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-container-names-rule.md', + }; - public fixable: boolean = false; + public fixable: boolean = false; - // eslint-disable-next-line class-methods-use-this - public getMessage({ - serviceName, - containerName, - anotherService, - }: { - serviceName: string; - containerName: string; - anotherService: string; - }): string { - return `Service "${serviceName}" has a duplicate container name "${containerName}" with service "${anotherService}". Container names MUST BE unique.`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ + serviceName, + containerName, + anotherService, + }: { + serviceName: string; + containerName: string; + anotherService: string; + }): string { + return `Service "${serviceName}" has a duplicate container name "${containerName}" with service "${anotherService}". Container names MUST BE unique.`; + } - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); - if (!isMap(services)) return []; + if (!isMap(services)) return []; - const containerNames: Map = new Map(); + const containerNames: Map = new Map(); - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; - if (isMap(service) && service.has('container_name')) { - const containerName = String(service.get('container_name')); + if (isMap(service) && service.has('container_name')) { + const containerName = String(service.get('container_name')); - if (containerNames.has(containerName)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'container_name'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ - serviceName, - containerName, - anotherService: String(containerNames.get(containerName)), - }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } else { - containerNames.set(containerName, serviceName); - } - } - }); + if (containerNames.has(containerName)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'container_name'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ + serviceName, + containerName, + anotherService: String(containerNames.get(containerName)), + }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, + }); + } else { + containerNames.set(containerName, serviceName); + } + } + }); - return errors; - } + return errors; + } } diff --git a/src/rules/no-duplicate-exported-ports-rule.ts b/src/rules/no-duplicate-exported-ports-rule.ts index f2ffd01..e1f1c5f 100644 --- a/src/rules/no-duplicate-exported-ports-rule.ts +++ b/src/rules/no-duplicate-exported-ports-rule.ts @@ -1,100 +1,98 @@ import { parseDocument, isMap, isSeq, isScalar } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; import { extractPublishedPortValue, parsePortsRange } from '../util/service-ports-parser.js'; export default class NoDuplicateExportedPortsRule implements LintRule { - public name = 'no-duplicate-exported-ports'; - - public type: LintMessageType = 'error'; - - public category: LintRuleCategory = 'security'; - - public severity: LintRuleSeverity = 'critical'; - - public meta: RuleMeta = { - description: - 'Ensure that exported ports in Docker Compose are unique to prevent port conflicts and ensure proper network behavior.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-exported-ports-rule.md', - }; - - public fixable: boolean = false; - - // eslint-disable-next-line class-methods-use-this - public getMessage({ - serviceName, - publishedPort, - anotherService, - }: { - serviceName: string; - publishedPort: string; - anotherService: string; - }): string { - return `Service "${serviceName}" is exporting port "${publishedPort}" which is already used by service "${anotherService}".`; - } - - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); - - if (!isMap(services)) return []; - - const exportedPortsMap: Map = new Map(); - - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; - - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; - - if (!isMap(service) || !service.has('ports')) return; - - const ports = service.get('ports'); - if (!isSeq(ports)) return; - - ports.items.forEach((portItem) => { - const publishedPort = extractPublishedPortValue(portItem); - const currentPortRange = parsePortsRange(publishedPort); - - currentPortRange.some((port) => { - if (exportedPortsMap.has(port)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'ports'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ - serviceName, - publishedPort, - anotherService: String(exportedPortsMap.get(port)), - }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - return true; - } - return false; - }); - - // Map ports to the service - currentPortRange.forEach( - (port) => !exportedPortsMap.has(port) && exportedPortsMap.set(port, serviceName), - ); + public name = 'no-duplicate-exported-ports'; + + public type: LintMessageType = 'error'; + + public category: LintRuleCategory = 'security'; + + public severity: LintRuleSeverity = 'critical'; + + public meta: RuleMeta = { + description: + 'Ensure that exported ports in Docker Compose are unique to prevent port conflicts and ensure proper network behavior.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-exported-ports-rule.md', + }; + + public fixable: boolean = false; + + // eslint-disable-next-line class-methods-use-this + public getMessage({ + serviceName, + publishedPort, + anotherService, + }: { + serviceName: string; + publishedPort: string; + anotherService: string; + }): string { + return `Service "${serviceName}" is exporting port "${publishedPort}" which is already used by service "${anotherService}".`; + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); + + if (!isMap(services)) return []; + + const exportedPortsMap: Map = new Map(); + + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; + + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; + + if (!isMap(service) || !service.has('ports')) return; + + const ports = service.get('ports'); + if (!isSeq(ports)) return; + + ports.items.forEach((portItem) => { + const publishedPort = extractPublishedPortValue(portItem); + const currentPortRange = parsePortsRange(publishedPort); + + currentPortRange.some((port) => { + if (exportedPortsMap.has(port)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'ports'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ + serviceName, + publishedPort, + anotherService: String(exportedPortsMap.get(port)), + }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + return true; + } + return false; }); - return errors; - } + // Map ports to the service + currentPortRange.forEach((port) => !exportedPortsMap.has(port) && exportedPortsMap.set(port, serviceName)); + }); + }); + + return errors; + } } diff --git a/src/rules/no-quotes-in-volumes-rule.ts b/src/rules/no-quotes-in-volumes-rule.ts index b01238d..98f232e 100644 --- a/src/rules/no-quotes-in-volumes-rule.ts +++ b/src/rules/no-quotes-in-volumes-rule.ts @@ -1,92 +1,92 @@ import { parseDocument, isMap, isSeq, isScalar, Scalar, ParsedNode } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberByValue } from '../util/line-finder.js'; export default class NoQuotesInVolumesRule implements LintRule { - public name = 'no-quotes-in-volumes'; + public name = 'no-quotes-in-volumes'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'style'; + public category: LintRuleCategory = 'style'; - public severity: LintRuleSeverity = 'info'; + public severity: LintRuleSeverity = 'info'; - public meta: RuleMeta = { - description: 'Ensure that quotes are not used in volume names in Docker Compose files.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-quotes-in-volumes-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that quotes are not used in volume names in Docker Compose files.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-quotes-in-volumes-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage(): string { - return 'Quotes should not be used in volume names.'; - } + // eslint-disable-next-line class-methods-use-this + public getMessage(): string { + return 'Quotes should not be used in volume names.'; + } - private static extractVolumes(doc: ParsedNode | null, callback: (volume: Scalar) => void) { - if (!doc || !isMap(doc)) return; + private static extractVolumes(doc: ParsedNode | null, callback: (volume: Scalar) => void) { + if (!doc || !isMap(doc)) return; - doc.items.forEach((item) => { - if (!isMap(item.value)) return; + doc.items.forEach((item) => { + if (!isMap(item.value)) return; - const serviceMap = item.value; - serviceMap.items.forEach((service) => { - if (!isMap(service.value)) return; + const serviceMap = item.value; + serviceMap.items.forEach((service) => { + if (!isMap(service.value)) return; - const volumes = service.value.items.find((i) => isScalar(i.key) && i.key.value === 'volumes'); - if (!volumes || !isSeq(volumes.value)) return; + const volumes = service.value.items.find((i) => isScalar(i.key) && i.key.value === 'volumes'); + if (!volumes || !isSeq(volumes.value)) return; - volumes.value.items.forEach((volume) => { - if (isScalar(volume)) { - callback(volume); - } - }); - }); + volumes.value.items.forEach((volume) => { + if (isScalar(volume)) { + callback(volume); + } }); - } - - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - - NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { - if (volume.type !== 'PLAIN') { - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage(), - line: findLineNumberByValue(context.sourceCode, String(volume.value)), - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + }); + }); + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + + NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { + if (volume.type !== 'PLAIN') { + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage(), + line: findLineNumberByValue(context.sourceCode, String(volume.value)), + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } - // eslint-disable-next-line class-methods-use-this - public fix(content: string): string { - const doc = parseDocument(content); + // eslint-disable-next-line class-methods-use-this + public fix(content: string): string { + const doc = parseDocument(content); - NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { - if (volume.type !== 'PLAIN') { - // eslint-disable-next-line no-param-reassign - volume.type = 'PLAIN'; - } - }); + NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { + if (volume.type !== 'PLAIN') { + // eslint-disable-next-line no-param-reassign + volume.type = 'PLAIN'; + } + }); - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/rules/no-version-field-rule.ts b/src/rules/no-version-field-rule.ts index d5afb6c..fd84644 100644 --- a/src/rules/no-version-field-rule.ts +++ b/src/rules/no-version-field-rule.ts @@ -1,64 +1,64 @@ import type { - LintRule, - LintMessage, - LintRuleCategory, - RuleMeta, - LintRuleSeverity, - LintMessageType, - LintContext, + LintRule, + LintMessage, + LintRuleCategory, + RuleMeta, + LintRuleSeverity, + LintMessageType, + LintContext, } from '../linter/linter.types.js'; import { findLineNumberByKey } from '../util/line-finder.js'; export default class NoVersionFieldRule implements LintRule { - public name = 'no-version-field'; + public name = 'no-version-field'; - public type: LintMessageType = 'error'; + public type: LintMessageType = 'error'; - public category: LintRuleCategory = 'best-practice'; + public category: LintRuleCategory = 'best-practice'; - public severity: LintRuleSeverity = 'minor'; + public severity: LintRuleSeverity = 'minor'; - public meta: RuleMeta = { - description: 'Ensure that the "version" field is not present in the Docker Compose file.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-version-field-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that the "version" field is not present in the Docker Compose file.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-version-field-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage(): string { - return 'The "version" field should not be present.'; - } - - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; + // eslint-disable-next-line class-methods-use-this + public getMessage(): string { + return 'The "version" field should not be present.'; + } - if (context.content && 'version' in context.content) { - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage(), - line: findLineNumberByKey(context.sourceCode, 'version'), - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; - return errors; + if (context.content && 'version' in context.content) { + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage(), + line: findLineNumberByKey(context.sourceCode, 'version'), + column: 1, + meta: this.meta, + fixable: this.fixable, + }); } - // eslint-disable-next-line class-methods-use-this - public fix(content: string): string { - const lines = content.split('\n'); - const versionLineIndex = lines.findIndex((line) => line.trim().startsWith('version:')); + return errors; + } - if (versionLineIndex !== -1) { - lines.splice(versionLineIndex, 1); // Remove the line with the version - } + // eslint-disable-next-line class-methods-use-this + public fix(content: string): string { + const lines = content.split('\n'); + const versionLineIndex = lines.findIndex((line) => line.trim().startsWith('version:')); - return lines.join('\n'); + if (versionLineIndex !== -1) { + lines.splice(versionLineIndex, 1); // Remove the line with the version } + + return lines.join('\n'); + } } diff --git a/src/rules/require-project-name-field-rule.ts b/src/rules/require-project-name-field-rule.ts index 1c6493e..b382752 100644 --- a/src/rules/require-project-name-field-rule.ts +++ b/src/rules/require-project-name-field-rule.ts @@ -1,51 +1,51 @@ import type { - LintRule, - LintMessage, - LintRuleCategory, - RuleMeta, - LintRuleSeverity, - LintMessageType, - LintContext, + LintRule, + LintMessage, + LintRuleCategory, + RuleMeta, + LintRuleSeverity, + LintMessageType, + LintContext, } from '../linter/linter.types.js'; export default class RequireProjectNameFieldRule implements LintRule { - public name = 'require-project-name-field'; + public name = 'require-project-name-field'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'best-practice'; + public category: LintRuleCategory = 'best-practice'; - public severity: LintRuleSeverity = 'minor'; + public severity: LintRuleSeverity = 'minor'; - public meta: RuleMeta = { - description: 'Ensure that the "name" field is present in the Docker Compose file.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-project-name-field-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that the "name" field is present in the Docker Compose file.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-project-name-field-rule.md', + }; - public fixable: boolean = false; + public fixable: boolean = false; - // eslint-disable-next-line class-methods-use-this - public getMessage(): string { - return 'The "name" field should be present.'; - } + // eslint-disable-next-line class-methods-use-this + public getMessage(): string { + return 'The "name" field should be present.'; + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - - if (context.content && !('name' in context.content)) { - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage(), - line: 1, // Default to the top of the file if the field is missing - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } - - return errors; + if (context.content && !('name' in context.content)) { + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage(), + line: 1, // Default to the top of the file if the field is missing + column: 1, + meta: this.meta, + fixable: this.fixable, + }); } + + return errors; + } } diff --git a/src/rules/require-quotes-in-ports-rule.ts b/src/rules/require-quotes-in-ports-rule.ts index b9d88ec..646584f 100644 --- a/src/rules/require-quotes-in-ports-rule.ts +++ b/src/rules/require-quotes-in-ports-rule.ts @@ -1,106 +1,106 @@ import { parseDocument, isMap, isSeq, isScalar, Scalar, ParsedNode } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberByValue } from '../util/line-finder.js'; interface RequireQuotesInPortsRuleOptions { - quoteType: 'single' | 'double'; + quoteType: 'single' | 'double'; } export default class RequireQuotesInPortsRule implements LintRule { - public name = 'require-quotes-in-ports'; + public name = 'require-quotes-in-ports'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'best-practice'; + public category: LintRuleCategory = 'best-practice'; - public severity: LintRuleSeverity = 'minor'; + public severity: LintRuleSeverity = 'minor'; - public meta: RuleMeta = { - description: 'Ensure that ports are enclosed in quotes in Docker Compose files.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-quotes-in-ports-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that ports are enclosed in quotes in Docker Compose files.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-quotes-in-ports-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage(): string { - return 'Ports should be enclosed in quotes in Docker Compose files.'; - } + // eslint-disable-next-line class-methods-use-this + public getMessage(): string { + return 'Ports should be enclosed in quotes in Docker Compose files.'; + } - private readonly quoteType: 'single' | 'double'; + private readonly quoteType: 'single' | 'double'; - constructor(options?: RequireQuotesInPortsRuleOptions) { - this.quoteType = options?.quoteType || 'single'; - } + constructor(options?: RequireQuotesInPortsRuleOptions) { + this.quoteType = options?.quoteType || 'single'; + } - private getQuoteType(): Scalar.Type { - return this.quoteType === 'single' ? 'QUOTE_SINGLE' : 'QUOTE_DOUBLE'; - } + private getQuoteType(): Scalar.Type { + return this.quoteType === 'single' ? 'QUOTE_SINGLE' : 'QUOTE_DOUBLE'; + } - // Static method to extract and process ports - private static extractPorts(doc: ParsedNode | null, callback: (port: Scalar) => void) { - if (!doc || !isMap(doc)) return; + // Static method to extract and process ports + private static extractPorts(doc: ParsedNode | null, callback: (port: Scalar) => void) { + if (!doc || !isMap(doc)) return; - doc.items.forEach((item) => { - if (!isMap(item.value)) return; + doc.items.forEach((item) => { + if (!isMap(item.value)) return; - const serviceMap = item.value; - serviceMap.items.forEach((service) => { - if (!isMap(service.value)) return; + const serviceMap = item.value; + serviceMap.items.forEach((service) => { + if (!isMap(service.value)) return; - const ports = service.value.items.find((i) => isScalar(i.key) && i.key.value === 'ports'); - if (!ports || !isSeq(ports.value)) return; + const ports = service.value.items.find((i) => isScalar(i.key) && i.key.value === 'ports'); + if (!ports || !isSeq(ports.value)) return; - ports.value.items.forEach((port) => { - if (isScalar(port)) { - callback(port); - } - }); - }); + ports.value.items.forEach((port) => { + if (isScalar(port)) { + callback(port); + } }); - } - - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - - RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { - if (port.type !== this.getQuoteType()) { - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage(), - line: findLineNumberByValue(context.sourceCode, String(port.value)), - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + }); + }); + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + + RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { + if (port.type !== this.getQuoteType()) { + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage(), + line: findLineNumberByValue(context.sourceCode, String(port.value)), + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } - public fix(content: string): string { - const doc = parseDocument(content); + public fix(content: string): string { + const doc = parseDocument(content); - RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { - if (port.type !== this.getQuoteType()) { - // eslint-disable-next-line no-param-reassign - port.type = this.getQuoteType(); - } - }); + RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { + if (port.type !== this.getQuoteType()) { + // eslint-disable-next-line no-param-reassign + port.type = this.getQuoteType(); + } + }); - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/rules/service-container-name-regex-rule.ts b/src/rules/service-container-name-regex-rule.ts index 85671d2..f2a1a44 100644 --- a/src/rules/service-container-name-regex-rule.ts +++ b/src/rules/service-container-name-regex-rule.ts @@ -1,75 +1,75 @@ import { parseDocument, isMap, isScalar } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; export default class ServiceContainerNameRegexRule implements LintRule { - public name = 'service-container-name-regex'; + public name = 'service-container-name-regex'; - public type: LintMessageType = 'error'; + public type: LintMessageType = 'error'; - public category: LintRuleCategory = 'security'; + public category: LintRuleCategory = 'security'; - public severity: LintRuleSeverity = 'critical'; + public severity: LintRuleSeverity = 'critical'; - public message = 'Container names must match the regex pattern [a-zA-Z0-9][a-zA-Z0-9_.-]+.'; + public message = 'Container names must match the regex pattern [a-zA-Z0-9][a-zA-Z0-9_.-]+.'; - public meta: RuleMeta = { - description: - 'Ensure that container names in Docker Compose match the required regex pattern to avoid invalid names.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-container-name-regex-rule.md', - }; + public meta: RuleMeta = { + description: + 'Ensure that container names in Docker Compose match the required regex pattern to avoid invalid names.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-container-name-regex-rule.md', + }; - public fixable: boolean = false; + public fixable: boolean = false; - // eslint-disable-next-line class-methods-use-this - public getMessage({ serviceName, containerName }: { serviceName: string; containerName: string }): string { - return `Service "${serviceName}" has an invalid container name "${containerName}". It must match the regex pattern ${ServiceContainerNameRegexRule.containerNameRegex}.`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ serviceName, containerName }: { serviceName: string; containerName: string }): string { + return `Service "${serviceName}" has an invalid container name "${containerName}". It must match the regex pattern ${ServiceContainerNameRegexRule.containerNameRegex}.`; + } - // see https://docs.docker.com/reference/compose-file/services/#container_name - private static readonly containerNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/; + // see https://docs.docker.com/reference/compose-file/services/#container_name + private static readonly containerNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/; - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); - if (!isMap(services)) return []; + if (!isMap(services)) return []; - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; - if (!isMap(service) || !service.has('container_name')) return; + if (!isMap(service) || !service.has('container_name')) return; - const containerName = String(service.get('container_name')); + const containerName = String(service.get('container_name')); - if (!ServiceContainerNameRegexRule.containerNameRegex.test(containerName)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'container_name'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ serviceName, containerName }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + if (!ServiceContainerNameRegexRule.containerNameRegex.test(containerName)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'container_name'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ serviceName, containerName }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } } diff --git a/src/rules/service-dependencies-alphabetical-order-rule.ts b/src/rules/service-dependencies-alphabetical-order-rule.ts index 3d2ae9f..af6cd2b 100644 --- a/src/rules/service-dependencies-alphabetical-order-rule.ts +++ b/src/rules/service-dependencies-alphabetical-order-rule.ts @@ -1,111 +1,111 @@ import { parseDocument, isSeq, isScalar, isMap, isPair } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; export default class ServiceDependenciesAlphabeticalOrderRule implements LintRule { - public name = 'service-dependencies-alphabetical-order'; + public name = 'service-dependencies-alphabetical-order'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'style'; + public category: LintRuleCategory = 'style'; - public severity: LintRuleSeverity = 'info'; + public severity: LintRuleSeverity = 'info'; - public meta: RuleMeta = { - description: 'Ensure that the services listed in the depends_on directive are sorted alphabetically.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-dependencies-alphabetical-order-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that the services listed in the depends_on directive are sorted alphabetically.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-dependencies-alphabetical-order-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage = ({ serviceName }: { serviceName: string }): string => { - return `Services in "depends_on" for service "${serviceName}" should be in alphabetical order.`; - }; + // eslint-disable-next-line class-methods-use-this + public getMessage = ({ serviceName }: { serviceName: string }): string => { + return `Services in "depends_on" for service "${serviceName}" should be in alphabetical order.`; + }; - private static extractServiceName(yamlNode: unknown): string { - // Short Syntax - if (isScalar(yamlNode)) { - return String(yamlNode.value); - } - // Long Syntax - if (isPair(yamlNode)) { - return String(yamlNode.key); - } - return ''; + private static extractServiceName(yamlNode: unknown): string { + // Short Syntax + if (isScalar(yamlNode)) { + return String(yamlNode.value); } - - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); - - if (!isMap(services)) return []; - - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; - - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; - - if (!service || !isMap(service)) return; - - const dependsOn = service.get('depends_on'); - if (!isSeq(dependsOn) && !isMap(dependsOn)) return; - - const extractedDependencies = dependsOn.items.map((item) => - ServiceDependenciesAlphabeticalOrderRule.extractServiceName(item), - ); - const sortedDependencies = [...extractedDependencies].sort((a, b) => a.localeCompare(b)); - - if (JSON.stringify(extractedDependencies) !== JSON.stringify(sortedDependencies)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'depends_on'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ serviceName }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + // Long Syntax + if (isPair(yamlNode)) { + return String(yamlNode.key); + } + return ''; + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); + + if (!isMap(services)) return []; + + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; + + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; + + if (!service || !isMap(service)) return; + + const dependsOn = service.get('depends_on'); + if (!isSeq(dependsOn) && !isMap(dependsOn)) return; + + const extractedDependencies = dependsOn.items.map((item) => + ServiceDependenciesAlphabeticalOrderRule.extractServiceName(item), + ); + const sortedDependencies = [...extractedDependencies].sort((a, b) => a.localeCompare(b)); + + if (JSON.stringify(extractedDependencies) !== JSON.stringify(sortedDependencies)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'depends_on'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ serviceName }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } - // eslint-disable-next-line class-methods-use-this - public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + // eslint-disable-next-line class-methods-use-this + public fix(content: string): string { + const doc = parseDocument(content); + const services = doc.get('services'); - if (!isMap(services)) return content; + if (!isMap(services)) return content; - services.items.forEach((serviceItem) => { - const service = serviceItem.value; - if (!service || !isMap(service)) return; + services.items.forEach((serviceItem) => { + const service = serviceItem.value; + if (!service || !isMap(service)) return; - const dependsOn = service.get('depends_on'); - if (!isSeq(dependsOn) && !isMap(dependsOn)) return; + const dependsOn = service.get('depends_on'); + if (!isSeq(dependsOn) && !isMap(dependsOn)) return; - dependsOn.items.sort((a, b) => { - const valueA = ServiceDependenciesAlphabeticalOrderRule.extractServiceName(a); - const valueB = ServiceDependenciesAlphabeticalOrderRule.extractServiceName(b); - return valueA.localeCompare(valueB); - }); - }); + dependsOn.items.sort((a, b) => { + const valueA = ServiceDependenciesAlphabeticalOrderRule.extractServiceName(a); + const valueB = ServiceDependenciesAlphabeticalOrderRule.extractServiceName(b); + return valueA.localeCompare(valueB); + }); + }); - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/rules/service-image-require-explicit-tag-rule.ts b/src/rules/service-image-require-explicit-tag-rule.ts index 8ccf0e6..f526237 100644 --- a/src/rules/service-image-require-explicit-tag-rule.ts +++ b/src/rules/service-image-require-explicit-tag-rule.ts @@ -1,98 +1,98 @@ import { parseDocument, isMap, isScalar } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; interface ServiceImageRequireExplicitTagRuleOptions { - prohibitedTags?: string[]; + prohibitedTags?: string[]; } export default class ServiceImageRequireExplicitTagRule implements LintRule { - public name = 'service-image-require-explicit-tag'; - - public type: LintMessageType = 'error'; - - public category: LintRuleCategory = 'security'; - - public severity: LintRuleSeverity = 'major'; - - public meta: RuleMeta = { - description: - 'Avoid using unspecific image tags like "latest" or "stable" in Docker Compose files to prevent unpredictable behavior. Specify a specific image version.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-image-require-explicit-tag-rule.md', - }; - - public fixable: boolean = false; - - // eslint-disable-next-line class-methods-use-this - public getMessage({ serviceName, image }: { serviceName: string; image: string }): string { - return `Service "${serviceName}" is using the image "${image}", which does not have a concrete version tag. Specify a concrete version tag.`; - } - - private readonly prohibitedTags: string[]; - - constructor(options?: ServiceImageRequireExplicitTagRuleOptions) { - // Default prohibited tags if not provided - this.prohibitedTags = options?.prohibitedTags || [ - 'latest', - 'stable', - 'edge', - 'test', - 'nightly', - 'dev', - 'beta', - 'canary', - ]; - } - - private isImageTagExplicit(image: string): boolean { - const lastPart = image.split('/').pop(); - if (!lastPart || !lastPart.includes(':')) return false; - - const [, tag] = lastPart.split(':'); - return !this.prohibitedTags.includes(tag); - } - - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); - - if (!isMap(services)) return []; - - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; - - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; - - if (!isMap(service) || !service.has('image')) return; - - const image = String(service.get('image')); - - if (!this.isImageTagExplicit(image)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'image'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ serviceName, image }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + public name = 'service-image-require-explicit-tag'; + + public type: LintMessageType = 'error'; + + public category: LintRuleCategory = 'security'; + + public severity: LintRuleSeverity = 'major'; + + public meta: RuleMeta = { + description: + 'Avoid using unspecific image tags like "latest" or "stable" in Docker Compose files to prevent unpredictable behavior. Specify a specific image version.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-image-require-explicit-tag-rule.md', + }; + + public fixable: boolean = false; + + // eslint-disable-next-line class-methods-use-this + public getMessage({ serviceName, image }: { serviceName: string; image: string }): string { + return `Service "${serviceName}" is using the image "${image}", which does not have a concrete version tag. Specify a concrete version tag.`; + } + + private readonly prohibitedTags: string[]; + + constructor(options?: ServiceImageRequireExplicitTagRuleOptions) { + // Default prohibited tags if not provided + this.prohibitedTags = options?.prohibitedTags || [ + 'latest', + 'stable', + 'edge', + 'test', + 'nightly', + 'dev', + 'beta', + 'canary', + ]; + } + + private isImageTagExplicit(image: string): boolean { + const lastPart = image.split('/').pop(); + if (!lastPart || !lastPart.includes(':')) return false; + + const [, tag] = lastPart.split(':'); + return !this.prohibitedTags.includes(tag); + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); + + if (!isMap(services)) return []; + + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; + + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; + + if (!isMap(service) || !service.has('image')) return; + + const image = String(service.get('image')); + + if (!this.isImageTagExplicit(image)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'image'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ serviceName, image }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } } diff --git a/src/rules/service-keys-order-rule.ts b/src/rules/service-keys-order-rule.ts index ad4fa75..c33575c 100644 --- a/src/rules/service-keys-order-rule.ts +++ b/src/rules/service-keys-order-rule.ts @@ -1,199 +1,197 @@ import { parseDocument, YAMLMap, isScalar, isMap } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; interface ServiceKeysOrderRuleOptions { - groupOrder?: GroupOrderEnum[]; - groups?: Partial>; + groupOrder?: GroupOrderEnum[]; + groups?: Partial>; } enum GroupOrderEnum { - CoreDefinitions = 'Core Definitions', - ServiceDependencies = 'Service Dependencies', - DataManagementAndConfiguration = 'Data Management and Configuration', - EnvironmentConfiguration = 'Environment Configuration', - Networking = 'Networking', - RuntimeBehavior = 'Runtime Behavior', - OperationalMetadata = 'Operational Metadata', - SecurityAndExecutionContext = 'Security and Execution Context', - Other = 'Other', + CoreDefinitions = 'Core Definitions', + ServiceDependencies = 'Service Dependencies', + DataManagementAndConfiguration = 'Data Management and Configuration', + EnvironmentConfiguration = 'Environment Configuration', + Networking = 'Networking', + RuntimeBehavior = 'Runtime Behavior', + OperationalMetadata = 'Operational Metadata', + SecurityAndExecutionContext = 'Security and Execution Context', + Other = 'Other', } // Default group order and groups const defaultGroupOrder: GroupOrderEnum[] = [ - GroupOrderEnum.CoreDefinitions, - GroupOrderEnum.ServiceDependencies, - GroupOrderEnum.DataManagementAndConfiguration, - GroupOrderEnum.EnvironmentConfiguration, - GroupOrderEnum.Networking, - GroupOrderEnum.RuntimeBehavior, - GroupOrderEnum.OperationalMetadata, - GroupOrderEnum.SecurityAndExecutionContext, - GroupOrderEnum.Other, + GroupOrderEnum.CoreDefinitions, + GroupOrderEnum.ServiceDependencies, + GroupOrderEnum.DataManagementAndConfiguration, + GroupOrderEnum.EnvironmentConfiguration, + GroupOrderEnum.Networking, + GroupOrderEnum.RuntimeBehavior, + GroupOrderEnum.OperationalMetadata, + GroupOrderEnum.SecurityAndExecutionContext, + GroupOrderEnum.Other, ]; const defaultGroups: Record = { - [GroupOrderEnum.CoreDefinitions]: ['image', 'build', 'container_name'], - [GroupOrderEnum.ServiceDependencies]: ['depends_on'], - [GroupOrderEnum.DataManagementAndConfiguration]: ['volumes', 'volumes_from', 'configs', 'secrets'], - [GroupOrderEnum.EnvironmentConfiguration]: ['environment', 'env_file'], - [GroupOrderEnum.Networking]: ['ports', 'networks', 'network_mode', 'extra_hosts'], - [GroupOrderEnum.RuntimeBehavior]: ['command', 'entrypoint', 'working_dir', 'restart', 'healthcheck'], - [GroupOrderEnum.OperationalMetadata]: ['logging', 'labels'], - [GroupOrderEnum.SecurityAndExecutionContext]: ['user', 'isolation'], - [GroupOrderEnum.Other]: [], + [GroupOrderEnum.CoreDefinitions]: ['image', 'build', 'container_name'], + [GroupOrderEnum.ServiceDependencies]: ['depends_on'], + [GroupOrderEnum.DataManagementAndConfiguration]: ['volumes', 'volumes_from', 'configs', 'secrets'], + [GroupOrderEnum.EnvironmentConfiguration]: ['environment', 'env_file'], + [GroupOrderEnum.Networking]: ['ports', 'networks', 'network_mode', 'extra_hosts'], + [GroupOrderEnum.RuntimeBehavior]: ['command', 'entrypoint', 'working_dir', 'restart', 'healthcheck'], + [GroupOrderEnum.OperationalMetadata]: ['logging', 'labels'], + [GroupOrderEnum.SecurityAndExecutionContext]: ['user', 'isolation'], + [GroupOrderEnum.Other]: [], }; export default class ServiceKeysOrderRule implements LintRule { - public name = 'service-keys-order'; + public name = 'service-keys-order'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'style'; + public category: LintRuleCategory = 'style'; - public severity: LintRuleSeverity = 'minor'; + public severity: LintRuleSeverity = 'minor'; - public message = 'Keys within each service should follow the predefined order.'; + public message = 'Keys within each service should follow the predefined order.'; - public meta: RuleMeta = { - description: 'Ensure that keys within each service in the Docker Compose file are ordered correctly.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-keys-order-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that keys within each service in the Docker Compose file are ordered correctly.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-keys-order-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage({ - serviceName, - key, - correctOrder, - }: { - serviceName: string; - key: string; - correctOrder: string[]; - }): string { - return `Key "${key}" in service "${serviceName}" is out of order. Expected order is: ${correctOrder.join(', ')}.`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ + serviceName, + key, + correctOrder, + }: { + serviceName: string; + key: string; + correctOrder: string[]; + }): string { + return `Key "${key}" in service "${serviceName}" is out of order. Expected order is: ${correctOrder.join(', ')}.`; + } - private readonly groupOrder: GroupOrderEnum[]; + private readonly groupOrder: GroupOrderEnum[]; - private readonly groups: Record; + private readonly groups: Record; - constructor(options?: ServiceKeysOrderRuleOptions) { - this.groupOrder = options?.groupOrder?.length ? options.groupOrder : defaultGroupOrder; + constructor(options?: ServiceKeysOrderRuleOptions) { + this.groupOrder = options?.groupOrder?.length ? options.groupOrder : defaultGroupOrder; - this.groups = { ...defaultGroups }; - if (options?.groups) { - Object.keys(options.groups).forEach((group) => { - const groupKey = group as GroupOrderEnum; - if (defaultGroups[groupKey] && options.groups) { - this.groups[groupKey] = options.groups[groupKey]!; - } - }); + this.groups = { ...defaultGroups }; + if (options?.groups) { + Object.keys(options.groups).forEach((group) => { + const groupKey = group as GroupOrderEnum; + if (defaultGroups[groupKey] && options.groups) { + this.groups[groupKey] = options.groups[groupKey]!; } + }); } + } - private getCorrectOrder(keys: string[]): string[] { - const otherKeys = keys.filter((key) => !Object.values(this.groups).flat().includes(key)).sort(); - - return this.groupOrder.flatMap((group) => this.groups[group]).concat(otherKeys); - } + private getCorrectOrder(keys: string[]): string[] { + const otherKeys = keys.filter((key) => !Object.values(this.groups).flat().includes(key)).sort(); - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + return this.groupOrder.flatMap((group) => this.groups[group]).concat(otherKeys); + } - if (!isMap(services)) return []; + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + if (!isMap(services)) return []; - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - if (!isMap(service)) return; + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; - const keys = service.items.map((key) => String(key.key)); + if (!isMap(service)) return; - const correctOrder = this.getCorrectOrder(keys); - let lastSeenIndex = -1; + const keys = service.items.map((key) => String(key.key)); - keys.forEach((key) => { - const expectedIndex = correctOrder.indexOf(key); + const correctOrder = this.getCorrectOrder(keys); + let lastSeenIndex = -1; - if (expectedIndex === -1) return; + keys.forEach((key) => { + const expectedIndex = correctOrder.indexOf(key); - if (expectedIndex < lastSeenIndex) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, key); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ serviceName, key, correctOrder }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + if (expectedIndex === -1) return; - lastSeenIndex = expectedIndex; - }); - }); + if (expectedIndex < lastSeenIndex) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, key); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ serviceName, key, correctOrder }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, + }); + } - return errors; - } + lastSeenIndex = expectedIndex; + }); + }); - public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + return errors; + } - if (!isMap(services)) return content; + public fix(content: string): string { + const doc = parseDocument(content); + const services = doc.get('services'); - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + if (!isMap(services)) return content; - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - if (!isMap(service)) return; + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; - const keys = service.items - .map((item) => (isScalar(item.key) ? String(item.key.value) : '')) - .filter(Boolean); + if (!isMap(service)) return; - const correctOrder = this.getCorrectOrder(keys); - const orderedService = new YAMLMap(); + const keys = service.items.map((item) => (isScalar(item.key) ? String(item.key.value) : '')).filter(Boolean); - correctOrder.forEach((key) => { - const item = service.items.find((i) => isScalar(i.key) && i.key.value === key); - if (item) { - orderedService.add(item); - } - }); + const correctOrder = this.getCorrectOrder(keys); + const orderedService = new YAMLMap(); - keys.forEach((key) => { - if (!correctOrder.includes(key)) { - const item = service.items.find((i) => isScalar(i.key) && i.key.value === key); - if (item) { - orderedService.add(item); - } - } - }); + correctOrder.forEach((key) => { + const item = service.items.find((i) => isScalar(i.key) && i.key.value === key); + if (item) { + orderedService.add(item); + } + }); + + keys.forEach((key) => { + if (!correctOrder.includes(key)) { + const item = service.items.find((i) => isScalar(i.key) && i.key.value === key); + if (item) { + orderedService.add(item); + } + } + }); - services.set(serviceName, orderedService); - }); + services.set(serviceName, orderedService); + }); - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/rules/service-ports-alphabetical-order-rule.ts b/src/rules/service-ports-alphabetical-order-rule.ts index a12de80..69f90f9 100644 --- a/src/rules/service-ports-alphabetical-order-rule.ts +++ b/src/rules/service-ports-alphabetical-order-rule.ts @@ -1,100 +1,100 @@ import { parseDocument, isSeq, isScalar, isMap } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; import { extractPublishedPortValue } from '../util/service-ports-parser.js'; export default class ServicePortsAlphabeticalOrderRule implements LintRule { - public name = 'service-ports-alphabetical-order'; + public name = 'service-ports-alphabetical-order'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'style'; + public category: LintRuleCategory = 'style'; - public severity: LintRuleSeverity = 'info'; + public severity: LintRuleSeverity = 'info'; - public meta: RuleMeta = { - description: 'Ensure that the list of ports in the Docker Compose service is alphabetically sorted.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-ports-alphabetical-order-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that the list of ports in the Docker Compose service is alphabetically sorted.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-ports-alphabetical-order-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage({ serviceName }: { serviceName: string }): string { - return `Ports in service "${serviceName}" should be in alphabetical order.`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ serviceName }: { serviceName: string }): string { + return `Ports in service "${serviceName}" should be in alphabetical order.`; + } - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); - if (!isMap(services)) return []; + if (!isMap(services)) return []; - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - const serviceName = String(serviceItem.key.value); - const service = serviceItem.value; + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; - if (!isMap(service) || !isSeq(service.get('ports'))) return; + if (!isMap(service) || !isSeq(service.get('ports'))) return; - const ports = service.get('ports'); - if (!isSeq(ports)) return; + const ports = service.get('ports'); + if (!isSeq(ports)) return; - const extractedPorts = ports.items.map((port) => extractPublishedPortValue(port)); - const sortedPorts = [...extractedPorts].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + const extractedPorts = ports.items.map((port) => extractPublishedPortValue(port)); + const sortedPorts = [...extractedPorts].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); - if (JSON.stringify(extractedPorts) !== JSON.stringify(sortedPorts)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'ports'); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: `Ports in service "${serviceName}" should be in alphabetical order.`, - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + if (JSON.stringify(extractedPorts) !== JSON.stringify(sortedPorts)) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'ports'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: `Ports in service "${serviceName}" should be in alphabetical order.`, + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } + }); - return errors; - } + return errors; + } - // eslint-disable-next-line class-methods-use-this - public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + // eslint-disable-next-line class-methods-use-this + public fix(content: string): string { + const doc = parseDocument(content); + const services = doc.get('services'); - if (!isMap(services)) return content; + if (!isMap(services)) return content; - services.items.forEach((serviceItem) => { - const service = serviceItem.value; + services.items.forEach((serviceItem) => { + const service = serviceItem.value; - if (!isMap(service) || !isSeq(service.get('ports'))) return; + if (!isMap(service) || !isSeq(service.get('ports'))) return; - const ports = service.get('ports'); - if (!isSeq(ports)) return; + const ports = service.get('ports'); + if (!isSeq(ports)) return; - ports.items.sort((a, b) => { - const valueA = extractPublishedPortValue(a); - const valueB = extractPublishedPortValue(b); + ports.items.sort((a, b) => { + const valueA = extractPublishedPortValue(a); + const valueB = extractPublishedPortValue(b); - return valueA.localeCompare(valueB, undefined, { numeric: true }); - }); - }); + return valueA.localeCompare(valueB, undefined, { numeric: true }); + }); + }); - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/rules/services-alphabetical-order-rule.ts b/src/rules/services-alphabetical-order-rule.ts index 9fbf203..a2b20b1 100644 --- a/src/rules/services-alphabetical-order-rule.ts +++ b/src/rules/services-alphabetical-order-rule.ts @@ -1,111 +1,111 @@ import { parseDocument, YAMLMap, isScalar, isMap } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; export default class ServicesAlphabeticalOrderRule implements LintRule { - public name = 'services-alphabetical-order'; + public name = 'services-alphabetical-order'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'style'; + public category: LintRuleCategory = 'style'; - public severity: LintRuleSeverity = 'minor'; + public severity: LintRuleSeverity = 'minor'; - public message = 'Services should be listed in alphabetical order.'; + public message = 'Services should be listed in alphabetical order.'; - public meta: RuleMeta = { - description: 'Ensure that services in the Docker Compose file are listed in alphabetical order.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/services-alphabetical-order-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that services in the Docker Compose file are listed in alphabetical order.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/services-alphabetical-order-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage({ serviceName, misplacedBefore }: { serviceName: string; misplacedBefore: string }): string { - return `Service "${serviceName}" should be before "${misplacedBefore}".`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ serviceName, misplacedBefore }: { serviceName: string; misplacedBefore: string }): string { + return `Service "${serviceName}" should be before "${misplacedBefore}".`; + } - private static findMisplacedService(processedServices: string[], currentService: string): string | null { - let misplacedBefore = ''; + private static findMisplacedService(processedServices: string[], currentService: string): string | null { + let misplacedBefore = ''; - processedServices.forEach((previousService) => { - if ( - previousService.localeCompare(currentService) > 0 && - (!misplacedBefore || previousService.localeCompare(misplacedBefore) < 0) - ) { - misplacedBefore = previousService; - } - }); - - return misplacedBefore; - } + processedServices.forEach((previousService) => { + if ( + previousService.localeCompare(currentService) > 0 && + (!misplacedBefore || previousService.localeCompare(misplacedBefore) < 0) + ) { + misplacedBefore = previousService; + } + }); - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + return misplacedBefore; + } - if (!isMap(services)) return []; + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const doc = parseDocument(context.sourceCode); + const services = doc.get('services'); - const processedServices: string[] = []; + if (!isMap(services)) return []; - services.items.forEach((serviceItem) => { - if (!isScalar(serviceItem.key)) return; + const processedServices: string[] = []; - const serviceName = String(serviceItem.key.value); - const misplacedBefore = ServicesAlphabeticalOrderRule.findMisplacedService(processedServices, serviceName); + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; - if (misplacedBefore) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName); + const serviceName = String(serviceItem.key.value); + const misplacedBefore = ServicesAlphabeticalOrderRule.findMisplacedService(processedServices, serviceName); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ serviceName, misplacedBefore }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + if (misplacedBefore) { + const line = findLineNumberForService(doc, context.sourceCode, serviceName); - processedServices.push(serviceName); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ serviceName, misplacedBefore }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } - return errors; - } + processedServices.push(serviceName); + }); - // eslint-disable-next-line class-methods-use-this - public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + return errors; + } - if (!isMap(services)) return content; + // eslint-disable-next-line class-methods-use-this + public fix(content: string): string { + const doc = parseDocument(content); + const services = doc.get('services'); - const sortedServices = new YAMLMap(); - const sortedItems = services.items.sort((a, b) => { - if (isScalar(a.key) && isScalar(b.key)) { - return String(a.key.value).localeCompare(String(b.key.value)); - } - return 0; - }); + if (!isMap(services)) return content; - sortedItems.forEach((item) => { - sortedServices.add(item); - }); + const sortedServices = new YAMLMap(); + const sortedItems = services.items.sort((a, b) => { + if (isScalar(a.key) && isScalar(b.key)) { + return String(a.key.value).localeCompare(String(b.key.value)); + } + return 0; + }); + + sortedItems.forEach((item) => { + sortedServices.add(item); + }); - doc.set('services', sortedServices); + doc.set('services', sortedServices); - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/rules/top-level-properties-order-rule.ts b/src/rules/top-level-properties-order-rule.ts index f819944..884f627 100644 --- a/src/rules/top-level-properties-order-rule.ts +++ b/src/rules/top-level-properties-order-rule.ts @@ -1,137 +1,137 @@ import { parseDocument, YAMLMap, isScalar, isMap } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberByKey } from '../util/line-finder.js'; interface TopLevelPropertiesOrderRuleOptions { - customOrder?: TopLevelKeys[]; + customOrder?: TopLevelKeys[]; } export enum TopLevelKeys { - XProperties = 'x-properties', - Version = 'version', - Name = 'name', - Include = 'include', - Services = 'services', - Networks = 'networks', - Volumes = 'volumes', - Secrets = 'secrets', - Configs = 'configs', + XProperties = 'x-properties', + Version = 'version', + Name = 'name', + Include = 'include', + Services = 'services', + Networks = 'networks', + Volumes = 'volumes', + Secrets = 'secrets', + Configs = 'configs', } export const DEFAULT_ORDER: TopLevelKeys[] = [ - TopLevelKeys.XProperties, - TopLevelKeys.Version, - TopLevelKeys.Name, - TopLevelKeys.Include, - TopLevelKeys.Services, - TopLevelKeys.Networks, - TopLevelKeys.Volumes, - TopLevelKeys.Secrets, - TopLevelKeys.Configs, + TopLevelKeys.XProperties, + TopLevelKeys.Version, + TopLevelKeys.Name, + TopLevelKeys.Include, + TopLevelKeys.Services, + TopLevelKeys.Networks, + TopLevelKeys.Volumes, + TopLevelKeys.Secrets, + TopLevelKeys.Configs, ]; export default class TopLevelPropertiesOrderRule implements LintRule { - public name = 'top-level-properties-order'; + public name = 'top-level-properties-order'; - public type: LintMessageType = 'warning'; + public type: LintMessageType = 'warning'; - public category: LintRuleCategory = 'style'; + public category: LintRuleCategory = 'style'; - public severity: LintRuleSeverity = 'major'; + public severity: LintRuleSeverity = 'major'; - public message = 'Top-level properties should follow the predefined order.'; + public message = 'Top-level properties should follow the predefined order.'; - public meta: RuleMeta = { - description: 'Ensure that top-level properties in the Docker Compose file are ordered correctly.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/top-level-properties-order-rule.md', - }; + public meta: RuleMeta = { + description: 'Ensure that top-level properties in the Docker Compose file are ordered correctly.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/top-level-properties-order-rule.md', + }; - public fixable: boolean = true; + public fixable: boolean = true; - // eslint-disable-next-line class-methods-use-this - public getMessage({ key, correctOrder }: { key: string; correctOrder: string[] }): string { - return `Property "${key}" is out of order. Expected order is: ${correctOrder.join(', ')}.`; - } + // eslint-disable-next-line class-methods-use-this + public getMessage({ key, correctOrder }: { key: string; correctOrder: string[] }): string { + return `Property "${key}" is out of order. Expected order is: ${correctOrder.join(', ')}.`; + } - private readonly expectedOrder: TopLevelKeys[]; + private readonly expectedOrder: TopLevelKeys[]; - constructor(options?: TopLevelPropertiesOrderRuleOptions) { - this.expectedOrder = options?.customOrder ?? DEFAULT_ORDER; - } + constructor(options?: TopLevelPropertiesOrderRuleOptions) { + this.expectedOrder = options?.customOrder ?? DEFAULT_ORDER; + } - public check(context: LintContext): LintMessage[] { - const errors: LintMessage[] = []; - const topLevelKeys = Object.keys(context.content); + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const topLevelKeys = Object.keys(context.content); - // Get and sort all 'x-' prefixed properties alphabetically - const sortedXProperties = topLevelKeys.filter((key) => key.startsWith('x-')).sort(); + // Get and sort all 'x-' prefixed properties alphabetically + const sortedXProperties = topLevelKeys.filter((key) => key.startsWith('x-')).sort(); - // Replace 'TopLevelKeys.XProperties' in the order with the actual sorted x-prefixed properties - const correctOrder = this.expectedOrder.flatMap((key) => - key === TopLevelKeys.XProperties ? sortedXProperties : [key], - ); + // Replace 'TopLevelKeys.XProperties' in the order with the actual sorted x-prefixed properties + const correctOrder = this.expectedOrder.flatMap((key) => + key === TopLevelKeys.XProperties ? sortedXProperties : [key], + ); - let lastSeenIndex = -1; + let lastSeenIndex = -1; - topLevelKeys.forEach((key) => { - const expectedIndex = correctOrder.indexOf(key); + topLevelKeys.forEach((key) => { + const expectedIndex = correctOrder.indexOf(key); - if (expectedIndex === -1 || expectedIndex < lastSeenIndex) { - const line = findLineNumberByKey(context.sourceCode, key); - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage({ key, correctOrder }), - line, - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } else { - lastSeenIndex = expectedIndex; - } + if (expectedIndex === -1 || expectedIndex < lastSeenIndex) { + const line = findLineNumberByKey(context.sourceCode, key); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage({ key, correctOrder }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, }); + } else { + lastSeenIndex = expectedIndex; + } + }); - return errors; - } + return errors; + } - public fix(content: string): string { - const doc = parseDocument(content); - const { contents } = doc; + public fix(content: string): string { + const doc = parseDocument(content); + const { contents } = doc; - if (!isMap(contents)) return content; + if (!isMap(contents)) return content; - const topLevelKeys = contents.items - .map((item) => (isScalar(item.key) ? String(item.key.value) : '')) - .filter(Boolean); + const topLevelKeys = contents.items + .map((item) => (isScalar(item.key) ? String(item.key.value) : '')) + .filter(Boolean); - const sortedXProperties = topLevelKeys.filter((key) => key.startsWith('x-')).sort(); + const sortedXProperties = topLevelKeys.filter((key) => key.startsWith('x-')).sort(); - const correctOrder = this.expectedOrder.flatMap((key) => - key === TopLevelKeys.XProperties ? sortedXProperties : [key], - ); + const correctOrder = this.expectedOrder.flatMap((key) => + key === TopLevelKeys.XProperties ? sortedXProperties : [key], + ); - const reorderedMap = new YAMLMap(); + const reorderedMap = new YAMLMap(); - correctOrder.forEach((key) => { - const item = contents.items.find((i) => isScalar(i.key) && String(i.key.value) === key); - if (item) { - reorderedMap.items.push(item); - } - }); + correctOrder.forEach((key) => { + const item = contents.items.find((i) => isScalar(i.key) && String(i.key.value) === key); + if (item) { + reorderedMap.items.push(item); + } + }); - doc.contents = reorderedMap as unknown as typeof doc.contents; + doc.contents = reorderedMap as unknown as typeof doc.contents; - return doc.toString(); - } + return doc.toString(); + } } diff --git a/src/util/compose-validation.ts b/src/util/compose-validation.ts index 6adf8f8..dad5a9d 100644 --- a/src/util/compose-validation.ts +++ b/src/util/compose-validation.ts @@ -6,41 +6,41 @@ import { loadSchema } from './load-schema.js'; type Schema = Record; function updateSchema(schema: Schema): Schema { - if (typeof schema !== 'object') return schema; + if (typeof schema !== 'object') return schema; - if ('id' in schema) { - // eslint-disable-next-line no-param-reassign - delete schema.id; - } + if ('id' in schema) { + // eslint-disable-next-line no-param-reassign + delete schema.id; + } - Object.entries(schema).forEach(([key, value]) => { - if (typeof value === 'object' && value !== null) { - // eslint-disable-next-line no-param-reassign - schema[key] = updateSchema(value as Schema); - } - }); + Object.entries(schema).forEach(([key, value]) => { + if (typeof value === 'object' && value !== null) { + // eslint-disable-next-line no-param-reassign + schema[key] = updateSchema(value as Schema); + } + }); - return schema; + return schema; } function validationComposeSchema(content: object) { - const ajv = new Ajv2019({ - allErrors: true, - strict: false, - strictSchema: false, - allowUnionTypes: true, - logger: false, + const ajv = new Ajv2019({ + allErrors: true, + strict: false, + strictSchema: false, + allowUnionTypes: true, + logger: false, + }); + + const composeSchema = loadSchema('compose'); + const validate = ajv.compile(updateSchema(composeSchema)); + const valid = validate(content); + + if (!valid && Array.isArray(validate.errors)) { + validate.errors.forEach((error: ErrorObject) => { + throw new ComposeValidationError(error); }); - - const composeSchema = loadSchema('compose'); - const validate = ajv.compile(updateSchema(composeSchema)); - const valid = validate(content); - - if (!valid && Array.isArray(validate.errors)) { - validate.errors.forEach((error: ErrorObject) => { - throw new ComposeValidationError(error); - }); - } + } } export { validationComposeSchema }; diff --git a/src/util/files-finder.ts b/src/util/files-finder.ts index efe8d88..8edbd61 100644 --- a/src/util/files-finder.ts +++ b/src/util/files-finder.ts @@ -4,73 +4,73 @@ import { Logger } from './logger.js'; import { FileNotFoundError } from '../errors/file-not-found-error.js'; export function findFilesForLinting(paths: string[], recursive: boolean, excludePaths: string[]): string[] { - const logger = Logger.getInstance(); - logger.debug('UTIL', `Looking for compose files in ${paths.toString()}`); + const logger = Logger.getInstance(); + logger.debug('UTIL', `Looking for compose files in ${paths.toString()}`); - let filesToCheck: string[] = []; + let filesToCheck: string[] = []; - // Default directories to exclude from the search - const defaultExcludes = ['node_modules', '.git', '.idea', '.tsimp']; + // Default directories to exclude from the search + const defaultExcludes = ['node_modules', '.git', '.idea', '.tsimp']; - // Combine default excludes with user-specified exclude paths - const excludeSet = new Set(defaultExcludes); - if (excludePaths && excludePaths.length > 0) { - excludePaths.forEach((p) => excludeSet.add(p)); - } - const exclude = Array.from(excludeSet); - logger.debug('UTIL', `Paths to exclude: ${exclude.toString()}`); + // Combine default excludes with user-specified exclude paths + const excludeSet = new Set(defaultExcludes); + if (excludePaths && excludePaths.length > 0) { + excludePaths.forEach((p) => excludeSet.add(p)); + } + const exclude = Array.from(excludeSet); + logger.debug('UTIL', `Paths to exclude: ${exclude.toString()}`); - // Regular expression to match [compose*.yml, compose*.yaml, docker-compose*.yml, docker-compose*.yaml] files - const dockerComposePattern = /^(docker-)?compose.*\.ya?ml$/; + // Regular expression to match [compose*.yml, compose*.yaml, docker-compose*.yml, docker-compose*.yaml] files + const dockerComposePattern = /^(docker-)?compose.*\.ya?ml$/; - paths.forEach((fileOrDir) => { - if (!fs.existsSync(fileOrDir)) { - logger.debug('UTIL', `File or directory not found: ${fileOrDir}`); - throw new FileNotFoundError(fileOrDir); - } + paths.forEach((fileOrDir) => { + if (!fs.existsSync(fileOrDir)) { + logger.debug('UTIL', `File or directory not found: ${fileOrDir}`); + throw new FileNotFoundError(fileOrDir); + } - let allPaths: string[] = []; + let allPaths: string[] = []; - const fileOrDirStats = fs.statSync(fileOrDir); + const fileOrDirStats = fs.statSync(fileOrDir); - if (fileOrDirStats.isDirectory()) { - try { - allPaths = fs.readdirSync(resolve(fileOrDir)).map((f) => join(fileOrDir, f)); - } catch (error) { - logger.debug('UTIL', `Error reading directory: ${fileOrDir}`, error); - allPaths = []; - } + if (fileOrDirStats.isDirectory()) { + try { + allPaths = fs.readdirSync(resolve(fileOrDir)).map((f) => join(fileOrDir, f)); + } catch (error) { + logger.debug('UTIL', `Error reading directory: ${fileOrDir}`, error); + allPaths = []; + } - allPaths.forEach((path) => { - // Skip files and directories listed in the exclude array - if (exclude.some((ex) => path.includes(ex))) { - logger.debug('UTIL', `Excluding ${path}`); - return; - } + allPaths.forEach((path) => { + // Skip files and directories listed in the exclude array + if (exclude.some((ex) => path.includes(ex))) { + logger.debug('UTIL', `Excluding ${path}`); + return; + } - const pathStats = fs.statSync(resolve(path)); + const pathStats = fs.statSync(resolve(path)); - if (pathStats.isDirectory()) { - if (recursive) { - // If recursive search is enabled, search within the directory - logger.debug('UTIL', `Recursive search is enabled, search within the directory: ${path}`); - const nestedFiles = findFilesForLinting([path], recursive, exclude); - filesToCheck = filesToCheck.concat(nestedFiles); - } - } else if (pathStats.isFile() && dockerComposePattern.test(basename(path))) { - // Add the file to the list if it matches the pattern - filesToCheck.push(path); - } - }); - } else if (fileOrDirStats.isFile()) { - filesToCheck.push(fileOrDir); + if (pathStats.isDirectory()) { + if (recursive) { + // If recursive search is enabled, search within the directory + logger.debug('UTIL', `Recursive search is enabled, search within the directory: ${path}`); + const nestedFiles = findFilesForLinting([path], recursive, exclude); + filesToCheck = filesToCheck.concat(nestedFiles); + } + } else if (pathStats.isFile() && dockerComposePattern.test(basename(path))) { + // Add the file to the list if it matches the pattern + filesToCheck.push(path); } - }); + }); + } else if (fileOrDirStats.isFile()) { + filesToCheck.push(fileOrDir); + } + }); - logger.debug( - 'UTIL', - `Found compose files in ${paths.toString()}: ${filesToCheck.length > 0 ? filesToCheck.join(', ') : 'None'}`, - ); + logger.debug( + 'UTIL', + `Found compose files in ${paths.toString()}: ${filesToCheck.length > 0 ? filesToCheck.join(', ') : 'None'}`, + ); - return filesToCheck; + return filesToCheck; } diff --git a/src/util/formatter-loader.ts b/src/util/formatter-loader.ts index e060859..032ec46 100644 --- a/src/util/formatter-loader.ts +++ b/src/util/formatter-loader.ts @@ -5,46 +5,46 @@ import { Logger } from './logger.js'; type FormatterFunction = (results: LintResult[]) => string; async function importFormatter(modulePath: string): Promise { - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const Formatter = (await import(modulePath)).default; - return Formatter as FormatterFunction; - } catch (error) { - throw new Error(`Module at ${modulePath} does not export a default formatter.`); - } + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const Formatter = (await import(modulePath)).default; + return Formatter as FormatterFunction; + } catch (error) { + throw new Error(`Module at ${modulePath} does not export a default formatter.`); + } } export async function loadFormatter(formatterName: string): Promise { - const logger = Logger.getInstance(); - - if (formatterName.startsWith('.')) { - const fullPath = path.resolve(formatterName); - const formatterModule = await importFormatter(fullPath); - logger.debug('UTIL', `Using formatter: ${fullPath}`); - return formatterModule; - } - - if (formatterName.includes('dclint-formatter-')) { - const formatterModule = await importFormatter(formatterName); - logger.debug('UTIL', `Using formatter: ${formatterName}`); - return formatterModule; - } - - const builtinFormatters: Record Promise> = { - json: async () => (await import('../formatters/json.js')).default as FormatterFunction, - compact: async () => (await import('../formatters/compact.js')).default as FormatterFunction, - stylish: async () => (await import('../formatters/stylish.js')).default as FormatterFunction, - junit: async () => (await import('../formatters/junit.js')).default as FormatterFunction, - codeclimate: async () => (await import('../formatters/codeclimate.js')).default as FormatterFunction, - }; - - let formatterLoader = builtinFormatters[formatterName]; - if (!formatterLoader) { - logger.warn(`Unknown formatter: ${formatterName}. Using default - stylish.`); - formatterLoader = builtinFormatters.stylish; - } - - logger.debug('UTIL', `Load formatter: ${formatterName}`); - - return formatterLoader(); + const logger = Logger.getInstance(); + + if (formatterName.startsWith('.')) { + const fullPath = path.resolve(formatterName); + const formatterModule = await importFormatter(fullPath); + logger.debug('UTIL', `Using formatter: ${fullPath}`); + return formatterModule; + } + + if (formatterName.includes('dclint-formatter-')) { + const formatterModule = await importFormatter(formatterName); + logger.debug('UTIL', `Using formatter: ${formatterName}`); + return formatterModule; + } + + const builtinFormatters: Record Promise> = { + json: async () => (await import('../formatters/json.js')).default as FormatterFunction, + compact: async () => (await import('../formatters/compact.js')).default as FormatterFunction, + stylish: async () => (await import('../formatters/stylish.js')).default as FormatterFunction, + junit: async () => (await import('../formatters/junit.js')).default as FormatterFunction, + codeclimate: async () => (await import('../formatters/codeclimate.js')).default as FormatterFunction, + }; + + let formatterLoader = builtinFormatters[formatterName]; + if (!formatterLoader) { + logger.warn(`Unknown formatter: ${formatterName}. Using default - stylish.`); + formatterLoader = builtinFormatters.stylish; + } + + logger.debug('UTIL', `Load formatter: ${formatterName}`); + + return formatterLoader(); } diff --git a/src/util/line-finder.ts b/src/util/line-finder.ts index e1fb891..0bfb7e0 100644 --- a/src/util/line-finder.ts +++ b/src/util/line-finder.ts @@ -8,15 +8,15 @@ import { isMap, isSeq, Node, isScalar } from 'yaml'; * @returns number The line number where the key is found, or 1 if not found. */ function findLineNumberByKey(content: string, key: string): number { - const lines = content.split('\n'); - const regex = new RegExp(`^\\s*${key}:`, 'i'); + const lines = content.split('\n'); + const regex = new RegExp(`^\\s*${key}:`, 'i'); - for (let i = 0; i < lines.length; i += 1) { - if (regex.test(lines[i])) { - return i + 1; // Lines start from 1, not 0 - } + for (let i = 0; i < lines.length; i += 1) { + if (regex.test(lines[i])) { + return i + 1; // Lines start from 1, not 0 } - return 1; // Default to 1 if the key is not found + } + return 1; // Default to 1 if the key is not found } /** @@ -26,8 +26,8 @@ function findLineNumberByKey(content: string, key: string): number { * @returns number The line number where the key is found, or 1 if not found. */ function findLineNumberByValue(content: string, value: string): number { - const lineIndex = content.split('\n').findIndex((line) => line.includes(value)); - return lineIndex === -1 ? 1 : lineIndex + 1; + const lineIndex = content.split('\n').findIndex((line) => line.includes(value)); + return lineIndex === -1 ? 1 : lineIndex + 1; } /** @@ -41,52 +41,52 @@ function findLineNumberByValue(content: string, value: string): number { * @returns number The line number where the key is found, or -1 if not found. */ function findLineNumberByKeyForService(doc: Document, content: string, serviceName: string, key: string): number { - const services = doc.get('services') as Node; + const services = doc.get('services') as Node; - if (!isMap(services)) { - return 1; - } + if (!isMap(services)) { + return 1; + } - const service = services.get(serviceName) as Node; + const service = services.get(serviceName) as Node; - if (!isMap(service)) { - return 1; - } + if (!isMap(service)) { + return 1; + } - let lineNumber = 1; - service.items.forEach((item) => { - const keyNode = item.key; + let lineNumber = 1; + service.items.forEach((item) => { + const keyNode = item.key; - if (isScalar(keyNode) && keyNode.value === key && keyNode.range) { - const [start] = keyNode.range; - lineNumber = content.slice(0, start).split('\n').length; - } - }); + if (isScalar(keyNode) && keyNode.value === key && keyNode.range) { + const [start] = keyNode.range; + lineNumber = content.slice(0, start).split('\n').length; + } + }); - return lineNumber; + return lineNumber; } /** * Refactored helper to get service block line number */ function getServiceStartLine(service: Node, content: string): number { - if (service.range) { - const [start] = service.range; - return content.slice(0, start).split('\n').length - 1; - } - return 1; + if (service.range) { + const [start] = service.range; + return content.slice(0, start).split('\n').length - 1; + } + return 1; } /** * Refactored helper to get key line number */ function getKeyLine(keyNode: Node, content: string): number { - if (keyNode.range) { - const [start] = keyNode.range; - const line = content.slice(0, start).split('\n').length; - return isScalar(keyNode) ? line : line - 1; - } - return 1; + if (keyNode.range) { + const [start] = keyNode.range; + const line = content.slice(0, start).split('\n').length; + return isScalar(keyNode) ? line : line - 1; + } + return 1; } /** @@ -103,65 +103,65 @@ function getKeyLine(keyNode: Node, content: string): number { * @returns number The line number where the service, key, or value is found, or 1 if not found. */ function findLineNumberForService( - doc: Document, - content: string, - serviceName: string, - key?: string, - value?: string, + doc: Document, + content: string, + serviceName: string, + key?: string, + value?: string, ): number { - const services = doc.get('services') as Node; - if (!isMap(services)) { - return 1; - } + const services = doc.get('services') as Node; + if (!isMap(services)) { + return 1; + } - // Locate Service - const service = services.get(serviceName) as Node; - if (!isMap(service)) { - return 1; - } + // Locate Service + const service = services.get(serviceName) as Node; + if (!isMap(service)) { + return 1; + } - // If the key is not provided, it returns the line number for the service block - if (!key) { - return getServiceStartLine(service, content); - } + // If the key is not provided, it returns the line number for the service block + if (!key) { + return getServiceStartLine(service, content); + } - // Locate Key in Service - const keyNode = service.get(key, true) as Node; - if (!keyNode) { - return 1; - } + // Locate Key in Service + const keyNode = service.get(key, true) as Node; + if (!keyNode) { + return 1; + } - // If value is not provided, return the line number of the key - if (!value) { - return getKeyLine(keyNode, content); - } + // If value is not provided, return the line number of the key + if (!value) { + return getKeyLine(keyNode, content); + } - if (isSeq(keyNode)) { - keyNode.items.forEach((item) => { - if (isScalar(item) && item.value === value && item.range) { - const [start] = item.range; - return content.slice(0, start).split('\n').length; - } + if (isSeq(keyNode)) { + keyNode.items.forEach((item) => { + if (isScalar(item) && item.value === value && item.range) { + const [start] = item.range; + return content.slice(0, start).split('\n').length; + } - return 1; - }); - } + return 1; + }); + } - if (isMap(keyNode)) { - keyNode.items.forEach((item) => { - const keyItem = item.key; - const valueItem = item.value; + if (isMap(keyNode)) { + keyNode.items.forEach((item) => { + const keyItem = item.key; + const valueItem = item.value; - if (isScalar(keyItem) && isScalar(valueItem) && valueItem.value === value && valueItem.range) { - const [start] = valueItem.range; - return content.slice(0, start).split('\n').length; - } + if (isScalar(keyItem) && isScalar(valueItem) && valueItem.value === value && valueItem.range) { + const [start] = valueItem.range; + return content.slice(0, start).split('\n').length; + } - return 1; - }); - } + return 1; + }); + } - return 1; // Default to 1 if the key or value is not found + return 1; // Default to 1 if the key or value is not found } export { findLineNumberByKey, findLineNumberByValue, findLineNumberByKeyForService, findLineNumberForService }; diff --git a/src/util/load-schema.ts b/src/util/load-schema.ts index 3b84629..0809c1b 100644 --- a/src/util/load-schema.ts +++ b/src/util/load-schema.ts @@ -3,9 +3,9 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; function loadSchema(name: string): Record { - return JSON.parse( - readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), `../../schemas/${name}.schema.json`), 'utf-8'), - ) as Record; + return JSON.parse( + readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), `../../schemas/${name}.schema.json`), 'utf-8'), + ) as Record; } export { loadSchema }; diff --git a/src/util/logger.ts b/src/util/logger.ts index 434d6c9..cb13a29 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -2,83 +2,83 @@ import chalk from 'chalk'; // Exported constants for log sources export const LOG_SOURCE = { - LINTER: 'LINTER', - CONFIG: 'CONFIG', - CLI: 'CLI', - UTIL: 'UTIL', - RULE: 'RULE', + LINTER: 'LINTER', + CONFIG: 'CONFIG', + CLI: 'CLI', + UTIL: 'UTIL', + RULE: 'RULE', } as const; type LogSource = (typeof LOG_SOURCE)[keyof typeof LOG_SOURCE]; class Logger { - private static instance: Logger; + private static instance: Logger; - private readonly debugMode: boolean = false; + private readonly debugMode: boolean = false; - private constructor(debug?: boolean) { - if (debug !== undefined) { - this.debugMode = debug; - } + private constructor(debug?: boolean) { + if (debug !== undefined) { + this.debugMode = debug; } + } - public static init(debug?: boolean): void { - if (!Logger.instance) { - Logger.instance = new Logger(debug); - } + public static init(debug?: boolean): void { + if (!Logger.instance) { + Logger.instance = new Logger(debug); } + } - public static getInstance(): Logger { - if (!Logger.instance) { - throw new Error('Logger is not initialized. Call Logger.init() first.'); - } - return Logger.instance; + public static getInstance(): Logger { + if (!Logger.instance) { + throw new Error('Logger is not initialized. Call Logger.init() first.'); } + return Logger.instance; + } - private static formatMessage(level: string, source?: LogSource): string { - const coloredLevel = Logger.getColoredLevel(level); - return source ? `${coloredLevel} [${source}]` : coloredLevel; - } + private static formatMessage(level: string, source?: LogSource): string { + const coloredLevel = Logger.getColoredLevel(level); + return source ? `${coloredLevel} [${source}]` : coloredLevel; + } - private static getColoredLevel(level: string): string { - switch (level) { - case 'DEBUG': - return chalk.blue('[DEBUG]'); - case 'INFO': - return chalk.green('[INFO]'); - case 'WARN': - return chalk.yellow('[WARN]'); - case 'ERROR': - return chalk.red('[ERROR]'); - default: - return `[${level}]`; - } + private static getColoredLevel(level: string): string { + switch (level) { + case 'DEBUG': + return chalk.blue('[DEBUG]'); + case 'INFO': + return chalk.green('[INFO]'); + case 'WARN': + return chalk.yellow('[WARN]'); + case 'ERROR': + return chalk.red('[ERROR]'); + default: + return `[${level}]`; } + } - public debug(source: LogSource, ...args: unknown[]): void { - if (this.debugMode) { - const message = Logger.formatMessage('DEBUG', source); - console.debug(message, ...args); - } + public debug(source: LogSource, ...args: unknown[]): void { + if (this.debugMode) { + const message = Logger.formatMessage('DEBUG', source); + console.debug(message, ...args); } + } - // eslint-disable-next-line class-methods-use-this - public info(...args: unknown[]): void { - const message = Logger.formatMessage('INFO'); - console.info(message, ...args); - } + // eslint-disable-next-line class-methods-use-this + public info(...args: unknown[]): void { + const message = Logger.formatMessage('INFO'); + console.info(message, ...args); + } - // eslint-disable-next-line class-methods-use-this - public warn(...args: unknown[]): void { - const message = Logger.formatMessage('WARN'); - console.warn(message, ...args); - } + // eslint-disable-next-line class-methods-use-this + public warn(...args: unknown[]): void { + const message = Logger.formatMessage('WARN'); + console.warn(message, ...args); + } - // eslint-disable-next-line class-methods-use-this - public error(...args: unknown[]): void { - const message = Logger.formatMessage('ERROR'); - console.error(message, ...args); - } + // eslint-disable-next-line class-methods-use-this + public error(...args: unknown[]): void { + const message = Logger.formatMessage('ERROR'); + console.error(message, ...args); + } } export { Logger }; diff --git a/src/util/rules-loader.ts b/src/util/rules-loader.ts index 6857080..1e6358d 100644 --- a/src/util/rules-loader.ts +++ b/src/util/rules-loader.ts @@ -6,65 +6,65 @@ import type { Config, ConfigRuleLevel, ConfigRule } from '../config/config.types import { Logger } from './logger.js'; async function importRule(file: string, rulesDir: string): Promise { - const logger = Logger.getInstance(); - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const RuleClass = (await import(path.join(rulesDir, file))).default; - - if (typeof RuleClass === 'function') { - return new (RuleClass as new () => LintRule)(); - } - return null; - } catch (error) { - logger.error(`Error importing rule from file: ${file}`, error); - return null; + const logger = Logger.getInstance(); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const RuleClass = (await import(path.join(rulesDir, file))).default; + + if (typeof RuleClass === 'function') { + return new (RuleClass as new () => LintRule)(); } + return null; + } catch (error) { + logger.error(`Error importing rule from file: ${file}`, error); + return null; + } } async function loadLintRules(config: Config): Promise { - const rulesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../rules'); + const rulesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../rules'); - const ruleFiles = fs - .readdirSync(rulesDir) - .filter((file) => file.endsWith('.js') || (file.endsWith('.ts') && !file.endsWith('d.ts'))); + const ruleFiles = fs + .readdirSync(rulesDir) + .filter((file) => file.endsWith('.js') || (file.endsWith('.ts') && !file.endsWith('d.ts'))); - // Parallel import with Promise.all - const ruleInstances: (LintRule | null)[] = await Promise.all( - ruleFiles.map(async (file) => importRule(file, rulesDir)), - ); + // Parallel import with Promise.all + const ruleInstances: (LintRule | null)[] = await Promise.all( + ruleFiles.map(async (file) => importRule(file, rulesDir)), + ); - const activeRules: LintRule[] = []; + const activeRules: LintRule[] = []; - ruleInstances.forEach((ruleInstance) => { - if (!ruleInstance) return; + ruleInstances.forEach((ruleInstance) => { + if (!ruleInstance) return; - const ruleConfig: ConfigRule = config.rules[ruleInstance.name]; + const ruleConfig: ConfigRule = config.rules[ruleInstance.name]; - let ruleLevel: ConfigRuleLevel; - let ruleOptions: Record | undefined; + let ruleLevel: ConfigRuleLevel; + let ruleOptions: Record | undefined; - if (Array.isArray(ruleConfig)) { - [ruleLevel, ruleOptions] = ruleConfig; - } else { - ruleLevel = ruleConfig; - } + if (Array.isArray(ruleConfig)) { + [ruleLevel, ruleOptions] = ruleConfig; + } else { + ruleLevel = ruleConfig; + } - if (ruleLevel === 0) return; + if (ruleLevel === 0) return; - const RuleClass = ruleInstance.constructor as new (options?: Record) => LintRule; - const instance = ruleOptions ? new RuleClass(ruleOptions) : new RuleClass(); + const RuleClass = ruleInstance.constructor as new (options?: Record) => LintRule; + const instance = ruleOptions ? new RuleClass(ruleOptions) : new RuleClass(); - const typeMap: { [key: number]: LintMessageType } = { - 1: 'warning', - 2: 'error', - }; + const typeMap: { [key: number]: LintMessageType } = { + 1: 'warning', + 2: 'error', + }; - instance.type = typeMap[ruleLevel] || instance.type; + instance.type = typeMap[ruleLevel] || instance.type; - activeRules.push(instance); - }); + activeRules.push(instance); + }); - return activeRules; + return activeRules; } export { loadLintRules }; diff --git a/src/util/service-ports-parser.ts b/src/util/service-ports-parser.ts index 91eed92..148fab4 100644 --- a/src/util/service-ports-parser.ts +++ b/src/util/service-ports-parser.ts @@ -2,47 +2,47 @@ import net from 'net'; import { isMap, isScalar } from 'yaml'; function extractPublishedPortValue(yamlNode: unknown): string { - if (isScalar(yamlNode)) { - const value = String(yamlNode.value); + if (isScalar(yamlNode)) { + const value = String(yamlNode.value); - // Check for host before ports - const parts = value.split(':'); - if (net.isIP(parts[0])) { - return String(parts[1]); - } - - return parts[0]; + // Check for host before ports + const parts = value.split(':'); + if (net.isIP(parts[0])) { + return String(parts[1]); } - if (isMap(yamlNode)) { - return String(yamlNode.get('published')) || ''; - } + return parts[0]; + } + + if (isMap(yamlNode)) { + return String(yamlNode.get('published')) || ''; + } - return ''; + return ''; } function parsePortsRange(port: string): string[] { - const [start, end] = port.split('-').map(Number); - - if (Number.isNaN(start) || Number.isNaN(end)) { - return []; - } - - if (!end) { - return [start.toString()]; - } - - if (start > end) { - // Invalid port range: start port is greater than end port - return []; - } - - const ports: string[] = []; - // eslint-disable-next-line no-plusplus - for (let i = start; i <= end; i++) { - ports.push(i.toString()); - } - return ports; + const [start, end] = port.split('-').map(Number); + + if (Number.isNaN(start) || Number.isNaN(end)) { + return []; + } + + if (!end) { + return [start.toString()]; + } + + if (start > end) { + // Invalid port range: start port is greater than end port + return []; + } + + const ports: string[] = []; + // eslint-disable-next-line no-plusplus + for (let i = start; i <= end; i++) { + ports.push(i.toString()); + } + return ports; } export { extractPublishedPortValue, parsePortsRange }; diff --git a/tests/linter.spec.ts b/tests/linter.spec.ts index f188453..db6dbb9 100644 --- a/tests/linter.spec.ts +++ b/tests/linter.spec.ts @@ -6,39 +6,39 @@ import type { LintResult, LintRule } from '../src/linter/linter.types.js'; // Sample configuration const config: Config = { - rules: {}, - quiet: false, - debug: false, - exclude: [], + rules: {}, + quiet: false, + debug: false, + exclude: [], }; // Sample lint rule for testing const mockRule: LintRule = { - name: 'mock-rule', - type: 'error', - category: 'style', - severity: 'major', - fixable: false, - meta: { - description: 'Mock rule description.', - url: 'https://example.com/mock-rule', - }, - check: (context) => [ - { - rule: 'mock-rule', - category: 'style', - severity: 'major', - message: 'Mock error detected.', - line: 1, - column: 1, - type: 'error', - fixable: false, - }, - ], - fix: (content: string) => content.replace('nginx', 'nginx:latest'), - getMessage(): string { - return 'Mock error.'; + name: 'mock-rule', + type: 'error', + category: 'style', + severity: 'major', + fixable: false, + meta: { + description: 'Mock rule description.', + url: 'https://example.com/mock-rule', + }, + check: (context) => [ + { + rule: 'mock-rule', + category: 'style', + severity: 'major', + message: 'Mock error detected.', + line: 1, + column: 1, + type: 'error', + fixable: false, }, + ], + fix: (content: string) => content.replace('nginx', 'nginx:latest'), + getMessage(): string { + return 'Mock error.'; + }, }; // Define constants to avoid duplication @@ -52,89 +52,89 @@ services: `; test.beforeEach(() => { - Logger.init(false); // Initialize logger + Logger.init(false); // Initialize logger }); test('DCLinter: should lint files correctly', async (t) => { - const mockFindFiles = (): string[] => [mockFilePath]; - const mockLoadLintRules = (): LintRule[] => [mockRule]; - const mockReadFileSync = (): string => mockFileContent; - - // Use esmock to mock both rules-loader and files-finder modules - // eslint-disable-next-line sonarjs/no-duplicate-string - const { DCLinter } = await esmock('../src/linter/linter.js', { - '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, - '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, - 'node:fs': { readFileSync: mockReadFileSync }, - }); - - const linter = new DCLinter(config); - - // Call lintFiles method - const result: LintResult[] = await linter.lintFiles([mockFilePath], false); - - // Assertions - t.is(result.length, 1, 'One file should be linted'); - t.is(result[0].filePath, mockFilePath, 'The linted file path should match the mock file path'); - t.is(result[0].messages.length, 1, 'There should be one lint message'); - t.is(result[0].messages[0].rule, 'mock-rule', 'The rule should be "mock-rule"'); - t.is(result[0].messages[0].message, 'Mock error detected.', 'The message should match the mock error'); - t.is(result[0].errorCount, 1, 'There should be one error'); - t.is(result[0].warningCount, 0, 'There should be no warnings'); + const mockFindFiles = (): string[] => [mockFilePath]; + const mockLoadLintRules = (): LintRule[] => [mockRule]; + const mockReadFileSync = (): string => mockFileContent; + + // Use esmock to mock both rules-loader and files-finder modules + // eslint-disable-next-line sonarjs/no-duplicate-string + const { DCLinter } = await esmock('../src/linter/linter.js', { + '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, + 'node:fs': { readFileSync: mockReadFileSync }, + }); + + const linter = new DCLinter(config); + + // Call lintFiles method + const result: LintResult[] = await linter.lintFiles([mockFilePath], false); + + // Assertions + t.is(result.length, 1, 'One file should be linted'); + t.is(result[0].filePath, mockFilePath, 'The linted file path should match the mock file path'); + t.is(result[0].messages.length, 1, 'There should be one lint message'); + t.is(result[0].messages[0].rule, 'mock-rule', 'The rule should be "mock-rule"'); + t.is(result[0].messages[0].message, 'Mock error detected.', 'The message should match the mock error'); + t.is(result[0].errorCount, 1, 'There should be one error'); + t.is(result[0].warningCount, 0, 'There should be no warnings'); }); test('DCLinter: should lint multiple files correctly', async (t) => { - const mockFindFiles = (): string[] => mockFilePaths; - const mockLoadLintRules = (): LintRule[] => [mockRule]; - const mockReadFileSync = (filePath: string): string => mockFileContent; - - // Use esmock to mock both rules-loader and files-finder modules - // eslint-disable-next-line sonarjs/no-duplicate-string - const { DCLinter } = await esmock('../src/linter/linter.js', { - '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, - '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, - 'node:fs': { readFileSync: mockReadFileSync }, - }); - - const linter = new DCLinter(config); - - // Call lintFiles method - const result: LintResult[] = await linter.lintFiles(mockFilePaths, false); - - // Assertions - t.is(result.length, 2, 'Two files should be linted'); - t.is(result[0].filePath, mockFilePaths[0], 'The linted file path should match the first mock file path'); - t.is(result[1].filePath, mockFilePaths[1], 'The linted file path should match the second mock file path'); - t.is(result[0].messages.length, 1, 'There should be one lint message for the first file'); - t.is(result[1].messages.length, 1, 'There should be one lint message for the second file'); + const mockFindFiles = (): string[] => mockFilePaths; + const mockLoadLintRules = (): LintRule[] => [mockRule]; + const mockReadFileSync = (filePath: string): string => mockFileContent; + + // Use esmock to mock both rules-loader and files-finder modules + // eslint-disable-next-line sonarjs/no-duplicate-string + const { DCLinter } = await esmock('../src/linter/linter.js', { + '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, + 'node:fs': { readFileSync: mockReadFileSync }, + }); + + const linter = new DCLinter(config); + + // Call lintFiles method + const result: LintResult[] = await linter.lintFiles(mockFilePaths, false); + + // Assertions + t.is(result.length, 2, 'Two files should be linted'); + t.is(result[0].filePath, mockFilePaths[0], 'The linted file path should match the first mock file path'); + t.is(result[1].filePath, mockFilePaths[1], 'The linted file path should match the second mock file path'); + t.is(result[0].messages.length, 1, 'There should be one lint message for the first file'); + t.is(result[1].messages.length, 1, 'There should be one lint message for the second file'); }); test('DCLinter: should fix files', async (t) => { - const mockFindFiles = (): string[] => [mockFilePath]; - const mockLoadLintRules = (): LintRule[] => [mockRule]; - const mockReadFileSync = (): string => mockFileContent; - const mockWriteFileSync = (): void => {}; - - // Use esmock to mock both rules-loader and files-finder modules - // eslint-disable-next-line sonarjs/no-duplicate-string - const { DCLinter } = await esmock('../src/linter/linter.js', { - '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, - '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, - 'node:fs': { readFileSync: mockReadFileSync, writeFileSync: mockWriteFileSync }, - }); - - const linter = new DCLinter(config); - - // Mock logger to capture dry-run output - let loggedOutput = ''; - Logger.getInstance().info = (...messages: string[]): void => { - loggedOutput += messages.join(' '); - }; - - // Call fixFiles method in dry-run mode - await linter.fixFiles([mockFilePath], false, true); - - // Assertions - t.regex(loggedOutput, /Dry run - changes for file/, 'Dry run should output changes'); - t.regex(loggedOutput, /nginx:latest/, 'Dry run output should contain "nginx:latest"'); + const mockFindFiles = (): string[] => [mockFilePath]; + const mockLoadLintRules = (): LintRule[] => [mockRule]; + const mockReadFileSync = (): string => mockFileContent; + const mockWriteFileSync = (): void => {}; + + // Use esmock to mock both rules-loader and files-finder modules + // eslint-disable-next-line sonarjs/no-duplicate-string + const { DCLinter } = await esmock('../src/linter/linter.js', { + '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, + 'node:fs': { readFileSync: mockReadFileSync, writeFileSync: mockWriteFileSync }, + }); + + const linter = new DCLinter(config); + + // Mock logger to capture dry-run output + let loggedOutput = ''; + Logger.getInstance().info = (...messages: string[]): void => { + loggedOutput += messages.join(' '); + }; + + // Call fixFiles method in dry-run mode + await linter.fixFiles([mockFilePath], false, true); + + // Assertions + t.regex(loggedOutput, /Dry run - changes for file/, 'Dry run should output changes'); + t.regex(loggedOutput, /nginx:latest/, 'Dry run output should contain "nginx:latest"'); }); diff --git a/tests/rules/no-build-and-image-rule.spec.ts b/tests/rules/no-build-and-image-rule.spec.ts index f6cd49b..26d2e26 100644 --- a/tests/rules/no-build-and-image-rule.spec.ts +++ b/tests/rules/no-build-and-image-rule.spec.ts @@ -48,91 +48,91 @@ services: const filePath = '/docker-compose.yml'; test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service', (t) => { - const rule = new NoBuildAndImageRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithBuildAndImage).toJS() as Record, - sourceCode: yamlWithBuildAndImage, - }; - - const errors = rule.check(context); - t.is( - errors.length, - 2, - 'There should be two warnings when both "build" and "image" are used and checkPullPolicy is false.', - ); - - const expectedMessages = [ - 'Service "web" is using both "build" and "image". Use either "build" or "image" but not both.', - 'Service "db" is using both "build" and "image". Use either "build" or "image" but not both.', - ]; - - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + const rule = new NoBuildAndImageRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithBuildAndImage).toJS() as Record, + sourceCode: yamlWithBuildAndImage, + }; + + const errors = rule.check(context); + t.is( + errors.length, + 2, + 'There should be two warnings when both "build" and "image" are used and checkPullPolicy is false.', + ); + + const expectedMessages = [ + 'Service "web" is using both "build" and "image". Use either "build" or "image" but not both.', + 'Service "db" is using both "build" and "image". Use either "build" or "image" but not both.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service and checkPullPolicy is false', (t) => { - const rule = new NoBuildAndImageRule({ checkPullPolicy: false }); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithBuildImageAndPullPolicy).toJS() as Record, - sourceCode: yamlWithBuildImageAndPullPolicy, - }; - - const errors = rule.check(context); - t.is( - errors.length, - 2, - 'There should be two warnings when both "build" and "image" are used and checkPullPolicy is false.', - ); - - const expectedMessages = [ - 'Service "web" is using both "build" and "image". Use either "build" or "image" but not both.', - 'Service "db" is using both "build" and "image". Use either "build" or "image" but not both.', - ]; - - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + const rule = new NoBuildAndImageRule({ checkPullPolicy: false }); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithBuildImageAndPullPolicy).toJS() as Record, + sourceCode: yamlWithBuildImageAndPullPolicy, + }; + + const errors = rule.check(context); + t.is( + errors.length, + 2, + 'There should be two warnings when both "build" and "image" are used and checkPullPolicy is false.', + ); + + const expectedMessages = [ + 'Service "web" is using both "build" and "image". Use either "build" or "image" but not both.', + 'Service "db" is using both "build" and "image". Use either "build" or "image" but not both.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('NoBuildAndImageRule: should not return warnings when "build" and "image" are used with pull_policy and checkPullPolicy is true', (t) => { - const rule = new NoBuildAndImageRule({ checkPullPolicy: true }); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithBuildImageAndPullPolicy).toJS() as Record, - sourceCode: yamlWithBuildImageAndPullPolicy, - }; - - const errors = rule.check(context); - t.is( - errors.length, - 0, - 'There should be no warnings when "build" and "image" are used together with pull_policy and checkPullPolicy is true.', - ); + const rule = new NoBuildAndImageRule({ checkPullPolicy: true }); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithBuildImageAndPullPolicy).toJS() as Record, + sourceCode: yamlWithBuildImageAndPullPolicy, + }; + + const errors = rule.check(context); + t.is( + errors.length, + 0, + 'There should be no warnings when "build" and "image" are used together with pull_policy and checkPullPolicy is true.', + ); }); test('NoBuildAndImageRule: should not return warnings when only "build" is used', (t) => { - const rule = new NoBuildAndImageRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithOnlyBuild).toJS() as Record, - sourceCode: yamlWithOnlyBuild, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when only "build" is used.'); + const rule = new NoBuildAndImageRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithOnlyBuild).toJS() as Record, + sourceCode: yamlWithOnlyBuild, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when only "build" is used.'); }); test('NoBuildAndImageRule: should not return warnings when only "image" is used', (t) => { - const rule = new NoBuildAndImageRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithOnlyImage).toJS() as Record, - sourceCode: yamlWithOnlyImage, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when only "image" is used.'); + const rule = new NoBuildAndImageRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithOnlyImage).toJS() as Record, + sourceCode: yamlWithOnlyImage, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when only "image" is used.'); }); diff --git a/tests/rules/no-duplicate-container-names-rule.spec.ts b/tests/rules/no-duplicate-container-names-rule.spec.ts index 0978878..f5f2683 100644 --- a/tests/rules/no-duplicate-container-names-rule.spec.ts +++ b/tests/rules/no-duplicate-container-names-rule.spec.ts @@ -26,29 +26,29 @@ services: `; test('NoDuplicateContainerNamesRule: should return an error when duplicate container names are found', (t) => { - const rule = new NoDuplicateContainerNamesRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithDuplicateContainerNames).toJS() as Record, - sourceCode: yamlWithDuplicateContainerNames, - }; + const rule = new NoDuplicateContainerNamesRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithDuplicateContainerNames).toJS() as Record, + sourceCode: yamlWithDuplicateContainerNames, + }; - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one error when duplicate container names are found.'); + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one error when duplicate container names are found.'); - const expectedMessage = - 'Service "db" has a duplicate container name "my_container" with service "web". Container names MUST BE unique.'; - t.true(errors[0].message.includes(expectedMessage)); + const expectedMessage = + 'Service "db" has a duplicate container name "my_container" with service "web". Container names MUST BE unique.'; + t.true(errors[0].message.includes(expectedMessage)); }); test('NoDuplicateContainerNamesRule: should not return errors when container names are unique', (t) => { - const rule = new NoDuplicateContainerNamesRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithUniqueContainerNames).toJS() as Record, - sourceCode: yamlWithUniqueContainerNames, - }; + const rule = new NoDuplicateContainerNamesRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithUniqueContainerNames).toJS() as Record, + sourceCode: yamlWithUniqueContainerNames, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no errors when container names are unique.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no errors when container names are unique.'); }); diff --git a/tests/rules/no-duplicate-exported-ports-rule.spec.ts b/tests/rules/no-duplicate-exported-ports-rule.spec.ts index ff3ffd4..45dacd5 100644 --- a/tests/rules/no-duplicate-exported-ports-rule.spec.ts +++ b/tests/rules/no-duplicate-exported-ports-rule.spec.ts @@ -102,58 +102,58 @@ services: const filePath = '/docker-compose.yml'; test('NoDuplicateExportedPortsRule: should return multiple errors when duplicate exported ports are found', (t) => { - const rule = new NoDuplicateExportedPortsRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithDuplicatePorts).toJS() as Record, - sourceCode: yamlWithDuplicatePorts, - }; + const rule = new NoDuplicateExportedPortsRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithDuplicatePorts).toJS() as Record, + sourceCode: yamlWithDuplicatePorts, + }; - const errors = rule.check(context); - t.is(errors.length, 5, 'There should be five errors when duplicate exported ports are found.'); + const errors = rule.check(context); + t.is(errors.length, 5, 'There should be five errors when duplicate exported ports are found.'); - const expectedMessages = [ - 'Service "b-service" is exporting port "8080" which is already used by service "a-service".', - 'Service "c-service" is exporting port "8080" which is already used by service "a-service".', - 'Service "d-service" is exporting port "8080" which is already used by service "a-service".', - 'Service "e-service" is exporting port "8080" which is already used by service "a-service".', - 'Service "f-service" is exporting port "8080" which is already used by service "a-service".', - ]; + const expectedMessages = [ + 'Service "b-service" is exporting port "8080" which is already used by service "a-service".', + 'Service "c-service" is exporting port "8080" which is already used by service "a-service".', + 'Service "d-service" is exporting port "8080" which is already used by service "a-service".', + 'Service "e-service" is exporting port "8080" which is already used by service "a-service".', + 'Service "f-service" is exporting port "8080" which is already used by service "a-service".', + ]; - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('NoDuplicateExportedPortsRule: should not return errors when exported ports are unique', (t) => { - const rule = new NoDuplicateExportedPortsRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithUniquePorts).toJS() as Record, - sourceCode: yamlWithUniquePorts, - }; + const rule = new NoDuplicateExportedPortsRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithUniquePorts).toJS() as Record, + sourceCode: yamlWithUniquePorts, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no errors when exported ports are unique.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no errors when exported ports are unique.'); }); test('NoDuplicateExportedPortsRule: should return an error when range overlap is detected', (t) => { - const rule = new NoDuplicateExportedPortsRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithRangeOverlap).toJS() as Record, - sourceCode: yamlWithRangeOverlap, - }; + const rule = new NoDuplicateExportedPortsRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithRangeOverlap).toJS() as Record, + sourceCode: yamlWithRangeOverlap, + }; - const errors = rule.check(context); - t.is(errors.length, 2, 'There should be two errors when range overlap is detected.'); + const errors = rule.check(context); + t.is(errors.length, 2, 'There should be two errors when range overlap is detected.'); - const expectedMessages = [ - 'Service "b-service" is exporting port "8094" which is already used by service "a-service".', - 'Service "d-service" is exporting port "8000-8085" which is already used by service "c-service".', - ]; + const expectedMessages = [ + 'Service "b-service" is exporting port "8094" which is already used by service "a-service".', + 'Service "d-service" is exporting port "8000-8085" which is already used by service "c-service".', + ]; - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); diff --git a/tests/rules/no-quotes-in-volumes-rule.spec.ts b/tests/rules/no-quotes-in-volumes-rule.spec.ts index c9b703e..e02e111 100644 --- a/tests/rules/no-quotes-in-volumes-rule.spec.ts +++ b/tests/rules/no-quotes-in-volumes-rule.spec.ts @@ -18,43 +18,43 @@ services: `; test('NoQuotesInVolumesRule: should not return errors for YAML without quotes in volumes', (t) => { - const rule = new NoQuotesInVolumesRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: {}, // You can mock content if necessary for your logic - sourceCode: correctYAML, - }; - - const errors = rule.check(context); - t.deepEqual(errors.length, 0, 'There should be no errors for correct YAML.'); + const rule = new NoQuotesInVolumesRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: {}, // You can mock content if necessary for your logic + sourceCode: correctYAML, + }; + + const errors = rule.check(context); + t.deepEqual(errors.length, 0, 'There should be no errors for correct YAML.'); }); test('NoQuotesInVolumesRule: should return errors for YAML with quotes in volumes', (t) => { - const rule = new NoQuotesInVolumesRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: {}, // Mock content as needed - sourceCode: incorrectYAML, - }; - - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one error for YAML with quoted volume name.'); - t.is(errors[0].message, 'Quotes should not be used in volume names.'); - t.is(errors[0].rule, 'no-quotes-in-volumes'); - t.is(errors[0].severity, 'info'); + const rule = new NoQuotesInVolumesRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: {}, // Mock content as needed + sourceCode: incorrectYAML, + }; + + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one error for YAML with quoted volume name.'); + t.is(errors[0].message, 'Quotes should not be used in volume names.'); + t.is(errors[0].rule, 'no-quotes-in-volumes'); + t.is(errors[0].severity, 'info'); }); test('NoQuotesInVolumesRule: should fix YAML with quotes in volumes', (t) => { - const rule = new NoQuotesInVolumesRule(); - const fixedYAML = rule.fix(incorrectYAML); + const rule = new NoQuotesInVolumesRule(); + const fixedYAML = rule.fix(incorrectYAML); - t.true(fixedYAML.includes('- data'), 'The quotes around volume name should be removed.'); - t.false(fixedYAML.includes('"data"'), 'The volume name should no longer have quotes.'); + t.true(fixedYAML.includes('- data'), 'The quotes around volume name should be removed.'); + t.false(fixedYAML.includes('"data"'), 'The volume name should no longer have quotes.'); }); test('NoQuotesInVolumesRule: should not modify YAML without quotes in volumes', (t) => { - const rule = new NoQuotesInVolumesRule(); - const fixedYAML = rule.fix(correctYAML); + const rule = new NoQuotesInVolumesRule(); + const fixedYAML = rule.fix(correctYAML); - t.is(fixedYAML.trim(), correctYAML.trim(), 'YAML without quotes should remain unchanged.'); + t.is(fixedYAML.trim(), correctYAML.trim(), 'YAML without quotes should remain unchanged.'); }); diff --git a/tests/rules/no-version-field-rule.spec.ts b/tests/rules/no-version-field-rule.spec.ts index c6e501d..7aae094 100644 --- a/tests/rules/no-version-field-rule.spec.ts +++ b/tests/rules/no-version-field-rule.spec.ts @@ -17,55 +17,55 @@ services: `; test('NoVersionFieldRule: should return an error when "version" field is present', (t) => { - const rule = new NoVersionFieldRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: { - version: '3', - services: { - web: { - image: 'nginx', - }, - }, + const rule = new NoVersionFieldRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: { + version: '3', + services: { + web: { + image: 'nginx', }, - sourceCode: yamlWithVersion, - }; + }, + }, + sourceCode: yamlWithVersion, + }; - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one error when the "version" field is present.'); - t.is(errors[0].message, 'The "version" field should not be present.'); - t.is(errors[0].rule, 'no-version-field'); - t.is(errors[0].severity, 'minor'); + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one error when the "version" field is present.'); + t.is(errors[0].message, 'The "version" field should not be present.'); + t.is(errors[0].rule, 'no-version-field'); + t.is(errors[0].severity, 'minor'); }); test('NoVersionFieldRule: should not return errors when "version" field is not present', (t) => { - const rule = new NoVersionFieldRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: { - services: { - web: { - image: 'nginx', - }, - }, + const rule = new NoVersionFieldRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: { + services: { + web: { + image: 'nginx', }, - sourceCode: yamlWithoutVersion, - }; + }, + }, + sourceCode: yamlWithoutVersion, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no errors when the "version" field is absent.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no errors when the "version" field is absent.'); }); test('NoVersionFieldRule: should fix by removing the "version" field', (t) => { - const rule = new NoVersionFieldRule(); - const fixedYAML = rule.fix(yamlWithVersion); + const rule = new NoVersionFieldRule(); + const fixedYAML = rule.fix(yamlWithVersion); - t.false(fixedYAML.includes('version:'), 'The "version" field should be removed.'); + t.false(fixedYAML.includes('version:'), 'The "version" field should be removed.'); }); test('NoVersionFieldRule: should not modify YAML without "version" field', (t) => { - const rule = new NoVersionFieldRule(); - const fixedYAML = rule.fix(yamlWithoutVersion); + const rule = new NoVersionFieldRule(); + const fixedYAML = rule.fix(yamlWithoutVersion); - t.is(fixedYAML.trim(), yamlWithoutVersion.trim(), 'YAML without "version" should remain unchanged.'); + t.is(fixedYAML.trim(), yamlWithoutVersion.trim(), 'YAML without "version" should remain unchanged.'); }); diff --git a/tests/rules/require-project-name-field-rule.spec.ts b/tests/rules/require-project-name-field-rule.spec.ts index 60ce39b..fbea128 100644 --- a/tests/rules/require-project-name-field-rule.spec.ts +++ b/tests/rules/require-project-name-field-rule.spec.ts @@ -17,41 +17,41 @@ services: `; test('RequiredProjectNameFieldRule: should return a warning when "name" field is missing', (t) => { - const rule = new RequireProjectNameFieldRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: { - services: { - web: { - image: 'nginx', - }, - }, + const rule = new RequireProjectNameFieldRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: { + services: { + web: { + image: 'nginx', }, - sourceCode: yamlWithoutName, - }; + }, + }, + sourceCode: yamlWithoutName, + }; - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one warning when the "name" field is missing.'); - t.is(errors[0].message, 'The "name" field should be present.'); - t.is(errors[0].rule, 'require-project-name-field'); - t.is(errors[0].severity, 'minor'); + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one warning when the "name" field is missing.'); + t.is(errors[0].message, 'The "name" field should be present.'); + t.is(errors[0].rule, 'require-project-name-field'); + t.is(errors[0].severity, 'minor'); }); test('RequiredProjectNameFieldRule: should not return warnings when "name" field is present', (t) => { - const rule = new RequireProjectNameFieldRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: { - name: 'my-project', - services: { - web: { - image: 'nginx', - }, - }, + const rule = new RequireProjectNameFieldRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: { + name: 'my-project', + services: { + web: { + image: 'nginx', }, - sourceCode: yamlWithName, - }; + }, + }, + sourceCode: yamlWithName, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when the "name" field is present.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when the "name" field is present.'); }); diff --git a/tests/rules/require-quotes-in-ports-rule.spec.ts b/tests/rules/require-quotes-in-ports-rule.spec.ts index 4b589bd..bd36dd3 100644 --- a/tests/rules/require-quotes-in-ports-rule.spec.ts +++ b/tests/rules/require-quotes-in-ports-rule.spec.ts @@ -27,72 +27,72 @@ services: const pathToFile = '/docker-compose.yml'; test('RequireQuotesInPortsRule: should return a warning when ports are not quoted', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); - const context: LintContext = { - path: pathToFile, - content: {}, - sourceCode: yamlWithoutQuotes, - }; - - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one warning when ports are not quoted.'); - t.is(errors[0].message, 'Ports should be enclosed in quotes in Docker Compose files.'); - t.is(errors[0].rule, 'require-quotes-in-ports'); - t.is(errors[0].severity, 'minor'); + const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); + const context: LintContext = { + path: pathToFile, + content: {}, + sourceCode: yamlWithoutQuotes, + }; + + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one warning when ports are not quoted.'); + t.is(errors[0].message, 'Ports should be enclosed in quotes in Docker Compose files.'); + t.is(errors[0].rule, 'require-quotes-in-ports'); + t.is(errors[0].severity, 'minor'); }); test('RequireQuotesInPortsRule: should not return warnings when ports are quoted with single quotes', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); - const context: LintContext = { - path: pathToFile, - content: {}, - sourceCode: yamlWithSingleQuotes, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when ports are quoted with single quotes.'); + const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); + const context: LintContext = { + path: pathToFile, + content: {}, + sourceCode: yamlWithSingleQuotes, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when ports are quoted with single quotes.'); }); test('RequireQuotesInPortsRule: should not return warnings when ports are quoted with double quotes', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); - const context: LintContext = { - path: pathToFile, - content: {}, - sourceCode: yamlWithDoubleQuotes, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when ports are quoted with double quotes.'); + const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); + const context: LintContext = { + path: pathToFile, + content: {}, + sourceCode: yamlWithDoubleQuotes, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when ports are quoted with double quotes.'); }); test('RequireQuotesInPortsRule: should fix unquoted ports by adding single quotes and not modify already quoted ports', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); + const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); - const fixedYAML = rule.fix(yamlWithoutQuotes); - t.true(fixedYAML.includes(`'8080:80'`), 'The ports should be quoted with single quotes.'); - t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); + const fixedYAML = rule.fix(yamlWithoutQuotes); + t.true(fixedYAML.includes(`'8080:80'`), 'The ports should be quoted with single quotes.'); + t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); test('RequireQuotesInPortsRule: should fix double quotes ports by changing them to single quotes', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); + const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); - const fixedYAML = rule.fix(yamlWithSingleQuotes); - t.true(fixedYAML.includes(`'8080:80'`), 'The ports should be quoted with single quotes.'); - t.false(fixedYAML.includes(`"8080:80"`), 'The ports should not have double quotes.'); + const fixedYAML = rule.fix(yamlWithSingleQuotes); + t.true(fixedYAML.includes(`'8080:80'`), 'The ports should be quoted with single quotes.'); + t.false(fixedYAML.includes(`"8080:80"`), 'The ports should not have double quotes.'); }); test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quotes and not modify already quoted ports', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); + const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); - const fixedYAML = rule.fix(yamlWithoutQuotes); - t.true(fixedYAML.includes(`"8080:80"`), 'The ports should be quoted with double quotes.'); - t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); + const fixedYAML = rule.fix(yamlWithoutQuotes); + t.true(fixedYAML.includes(`"8080:80"`), 'The ports should be quoted with double quotes.'); + t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); test('RequireQuotesInPortsRule: should fix single quotes ports by changing them to double quotes', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); + const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); - const fixedYAML = rule.fix(yamlWithSingleQuotes); - t.true(fixedYAML.includes(`"8080:80"`), 'The ports should be quoted with double quotes.'); - t.false(fixedYAML.includes(`'8080:80'`), 'The ports should not have single quotes.'); + const fixedYAML = rule.fix(yamlWithSingleQuotes); + t.true(fixedYAML.includes(`"8080:80"`), 'The ports should be quoted with double quotes.'); + t.false(fixedYAML.includes(`'8080:80'`), 'The ports should not have single quotes.'); }); diff --git a/tests/rules/service-container-name-regex-rule.spec.ts b/tests/rules/service-container-name-regex-rule.spec.ts index 653989a..c06e6e4 100644 --- a/tests/rules/service-container-name-regex-rule.spec.ts +++ b/tests/rules/service-container-name-regex-rule.spec.ts @@ -20,29 +20,29 @@ services: `; test('ServiceContainerNameRegexRule: should return an error for invalid container name', (t) => { - const rule = new ServiceContainerNameRegexRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithInvalidContainerName).toJS() as Record, - sourceCode: yamlWithInvalidContainerName, - }; + const rule = new ServiceContainerNameRegexRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithInvalidContainerName).toJS() as Record, + sourceCode: yamlWithInvalidContainerName, + }; - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one error when the container name is invalid.'); + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one error when the container name is invalid.'); - const expectedMessage = - 'Service "web" has an invalid container name "my-app@123". It must match the regex pattern /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.'; - t.true(errors[0].message.includes(expectedMessage)); + const expectedMessage = + 'Service "web" has an invalid container name "my-app@123". It must match the regex pattern /^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/.'; + t.true(errors[0].message.includes(expectedMessage)); }); test('ServiceContainerNameRegexRule: should not return an error for valid container name', (t) => { - const rule = new ServiceContainerNameRegexRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithValidContainerName).toJS() as Record, - sourceCode: yamlWithValidContainerName, - }; + const rule = new ServiceContainerNameRegexRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithValidContainerName).toJS() as Record, + sourceCode: yamlWithValidContainerName, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no errors when the container name is valid.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no errors when the container name is valid.'); }); diff --git a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts index a5dd420..77fdcba 100644 --- a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts @@ -54,74 +54,74 @@ const filePath = '/docker-compose.yml'; // Short syntax tests test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when short syntax services are not in alphabetical order', (t) => { - const rule = new ServiceDependenciesAlphabeticalOrderRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithIncorrectShortSyntax).toJS() as Record, - sourceCode: yamlWithIncorrectShortSyntax, - }; - - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one warning when short syntax services are out of order.'); - t.true(errors[0].message.includes(`Services in "depends_on" for service "web" should be in alphabetical order.`)); + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithIncorrectShortSyntax).toJS() as Record, + sourceCode: yamlWithIncorrectShortSyntax, + }; + + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one warning when short syntax services are out of order.'); + t.true(errors[0].message.includes(`Services in "depends_on" for service "web" should be in alphabetical order.`)); }); test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when short syntax services are in alphabetical order', (t) => { - const rule = new ServiceDependenciesAlphabeticalOrderRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithCorrectShortSyntax).toJS() as Record, - sourceCode: yamlWithCorrectShortSyntax, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when short syntax services are in alphabetical order.'); + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithCorrectShortSyntax).toJS() as Record, + sourceCode: yamlWithCorrectShortSyntax, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when short syntax services are in alphabetical order.'); }); test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of short syntax services', (t) => { - const rule = new ServiceDependenciesAlphabeticalOrderRule(); - const fixedYAML = rule.fix(yamlWithIncorrectShortSyntax); - - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectShortSyntax), - 'The short syntax services should be reordered alphabetically.', - ); + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectShortSyntax); + + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCorrectShortSyntax), + 'The short syntax services should be reordered alphabetically.', + ); }); // Long syntax tests test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when long syntax services are not in alphabetical order', (t) => { - const rule = new ServiceDependenciesAlphabeticalOrderRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithIncorrectLongSyntax).toJS() as Record, - sourceCode: yamlWithIncorrectLongSyntax, - }; - - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one warning when long syntax services are out of order.'); - t.true(errors[0].message.includes(`Services in "depends_on" for service "web" should be in alphabetical order.`)); + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithIncorrectLongSyntax).toJS() as Record, + sourceCode: yamlWithIncorrectLongSyntax, + }; + + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one warning when long syntax services are out of order.'); + t.true(errors[0].message.includes(`Services in "depends_on" for service "web" should be in alphabetical order.`)); }); test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when long syntax services are in alphabetical order', (t) => { - const rule = new ServiceDependenciesAlphabeticalOrderRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithCorrectLongSyntax).toJS() as Record, - sourceCode: yamlWithCorrectLongSyntax, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when long syntax services are in alphabetical order.'); + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithCorrectLongSyntax).toJS() as Record, + sourceCode: yamlWithCorrectLongSyntax, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when long syntax services are in alphabetical order.'); }); test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of long syntax services', (t) => { - const rule = new ServiceDependenciesAlphabeticalOrderRule(); - const fixedYAML = rule.fix(yamlWithIncorrectLongSyntax); - - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectLongSyntax), - 'The long syntax services should be reordered alphabetically.', - ); + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectLongSyntax); + + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCorrectLongSyntax), + 'The long syntax services should be reordered alphabetically.', + ); }); diff --git a/tests/rules/service-image-require-explicit-tag-rule.spec.ts b/tests/rules/service-image-require-explicit-tag-rule.spec.ts index 0173cd8..6c9d4a4 100644 --- a/tests/rules/service-image-require-explicit-tag-rule.spec.ts +++ b/tests/rules/service-image-require-explicit-tag-rule.spec.ts @@ -94,129 +94,129 @@ services: const filePath = '/docker-compose.yml'; test('ServiceImageRequireExplicitTagRule: should return a warning when no tag is specified', (t) => { - const rule = new ServiceImageRequireExplicitTagRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithoutTag).toJS() as Record, - sourceCode: yamlWithoutTag, - }; - - const errors = rule.check(context); - t.is(errors.length, 4, 'There should be four warnings when no image tag is specified.'); - - const expectedMessages = [ - 'Service "a-service" is using the image "nginx", which does not have a concrete version tag.', - 'Service "b-service" is using the image "library/nginx", which does not have a concrete version tag.', - 'Service "c-service" is using the image "docker.io/library/nginx", which does not have a concrete version tag.', - 'Service "d-service" is using the image "my_private.registry:5000/nginx", which does not have a concrete version tag.', - ]; - - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + const rule = new ServiceImageRequireExplicitTagRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithoutTag).toJS() as Record, + sourceCode: yamlWithoutTag, + }; + + const errors = rule.check(context); + t.is(errors.length, 4, 'There should be four warnings when no image tag is specified.'); + + const expectedMessages = [ + 'Service "a-service" is using the image "nginx", which does not have a concrete version tag.', + 'Service "b-service" is using the image "library/nginx", which does not have a concrete version tag.', + 'Service "c-service" is using the image "docker.io/library/nginx", which does not have a concrete version tag.', + 'Service "d-service" is using the image "my_private.registry:5000/nginx", which does not have a concrete version tag.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('ServiceImageRequireExplicitTagRule: should return a warning when using latest tag', (t) => { - const rule = new ServiceImageRequireExplicitTagRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithLatestTag).toJS() as Record, - sourceCode: yamlWithLatestTag, - }; - - const errors = rule.check(context); - t.is(errors.length, 4, 'There should be four warnings when the latest tag is used.'); - - const expectedMessages = [ - 'Service "a-service" is using the image "nginx:latest", which does not have a concrete version tag.', - 'Service "b-service" is using the image "library/nginx:latest", which does not have a concrete version tag.', - 'Service "c-service" is using the image "docker.io/library/nginx:latest", which does not have a concrete version tag.', - 'Service "d-service" is using the image "my_private.registry:5000/nginx:latest", which does not have a concrete version tag.', - ]; - - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + const rule = new ServiceImageRequireExplicitTagRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithLatestTag).toJS() as Record, + sourceCode: yamlWithLatestTag, + }; + + const errors = rule.check(context); + t.is(errors.length, 4, 'There should be four warnings when the latest tag is used.'); + + const expectedMessages = [ + 'Service "a-service" is using the image "nginx:latest", which does not have a concrete version tag.', + 'Service "b-service" is using the image "library/nginx:latest", which does not have a concrete version tag.', + 'Service "c-service" is using the image "docker.io/library/nginx:latest", which does not have a concrete version tag.', + 'Service "d-service" is using the image "my_private.registry:5000/nginx:latest", which does not have a concrete version tag.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('ServiceImageRequireExplicitTagRule: should return a warning when using stable tag', (t) => { - const rule = new ServiceImageRequireExplicitTagRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithStableTag).toJS() as Record, - sourceCode: yamlWithStableTag, - }; - - const errors = rule.check(context); - t.is(errors.length, 4, 'There should be four warnings when the stable tag is used.'); - - const expectedMessages = [ - 'Service "a-service" is using the image "nginx:stable", which does not have a concrete version tag.', - 'Service "b-service" is using the image "library/nginx:stable", which does not have a concrete version tag.', - 'Service "c-service" is using the image "docker.io/library/nginx:stable", which does not have a concrete version tag.', - 'Service "d-service" is using the image "my_private.registry:5000/nginx:stable", which does not have a concrete version tag.', - ]; - - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + const rule = new ServiceImageRequireExplicitTagRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithStableTag).toJS() as Record, + sourceCode: yamlWithStableTag, + }; + + const errors = rule.check(context); + t.is(errors.length, 4, 'There should be four warnings when the stable tag is used.'); + + const expectedMessages = [ + 'Service "a-service" is using the image "nginx:stable", which does not have a concrete version tag.', + 'Service "b-service" is using the image "library/nginx:stable", which does not have a concrete version tag.', + 'Service "c-service" is using the image "docker.io/library/nginx:stable", which does not have a concrete version tag.', + 'Service "d-service" is using the image "my_private.registry:5000/nginx:stable", which does not have a concrete version tag.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('ServiceImageRequireExplicitTagRule: should return a warning when using prohibited tags', (t) => { - const rule = new ServiceImageRequireExplicitTagRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithProhibitedTags).toJS() as Record, - sourceCode: yamlWithProhibitedTags, - }; - - const errors = rule.check(context); - t.is(errors.length, 6, 'There should be six warnings when prohibited tags are used.'); - - const expectedMessages = [ - 'Service "a-service" is using the image "nginx:edge", which does not have a concrete version tag.', - 'Service "b-service" is using the image "library/nginx:test", which does not have a concrete version tag.', - 'Service "c-service" is using the image "docker.io/library/nginx:nightly", which does not have a concrete version tag.', - 'Service "d-service" is using the image "my_private.registry:5000/nginx:dev", which does not have a concrete version tag.', - 'Service "e-service" is using the image "library/nginx:beta", which does not have a concrete version tag.', - 'Service "f-service" is using the image "library/nginx:canary", which does not have a concrete version tag.', - ]; - - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + const rule = new ServiceImageRequireExplicitTagRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithProhibitedTags).toJS() as Record, + sourceCode: yamlWithProhibitedTags, + }; + + const errors = rule.check(context); + t.is(errors.length, 6, 'There should be six warnings when prohibited tags are used.'); + + const expectedMessages = [ + 'Service "a-service" is using the image "nginx:edge", which does not have a concrete version tag.', + 'Service "b-service" is using the image "library/nginx:test", which does not have a concrete version tag.', + 'Service "c-service" is using the image "docker.io/library/nginx:nightly", which does not have a concrete version tag.', + 'Service "d-service" is using the image "my_private.registry:5000/nginx:dev", which does not have a concrete version tag.', + 'Service "e-service" is using the image "library/nginx:beta", which does not have a concrete version tag.', + 'Service "f-service" is using the image "library/nginx:canary", which does not have a concrete version tag.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('ServiceImageRequireExplicitTagRule: should use custom prohibitedTags when provided in the constructor', (t) => { - const rule = new ServiceImageRequireExplicitTagRule({ prohibitedTags: ['unstable', 'preview'] }); + const rule = new ServiceImageRequireExplicitTagRule({ prohibitedTags: ['unstable', 'preview'] }); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithCustomTags).toJS() as Record, - sourceCode: yamlWithCustomTags, - }; + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithCustomTags).toJS() as Record, + sourceCode: yamlWithCustomTags, + }; - const errors = rule.check(context); - t.is(errors.length, 2, 'There should be two warnings for custom prohibited tags "unstable" and "preview".'); + const errors = rule.check(context); + t.is(errors.length, 2, 'There should be two warnings for custom prohibited tags "unstable" and "preview".'); - const expectedMessages = [ - 'Service "a-service" is using the image "nginx:unstable", which does not have a concrete version tag.', - 'Service "b-service" is using the image "library/nginx:preview", which does not have a concrete version tag.', - ]; + const expectedMessages = [ + 'Service "a-service" is using the image "nginx:unstable", which does not have a concrete version tag.', + 'Service "b-service" is using the image "library/nginx:preview", which does not have a concrete version tag.', + ]; - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('ServiceImageRequireExplicitTagRule: should not return warnings when a specific version tag or digest is used', (t) => { - const rule = new ServiceImageRequireExplicitTagRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithSpecificVersion).toJS() as Record, - sourceCode: yamlWithSpecificVersion, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when a specific version tag or digest is used.'); + const rule = new ServiceImageRequireExplicitTagRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithSpecificVersion).toJS() as Record, + sourceCode: yamlWithSpecificVersion, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when a specific version tag or digest is used.'); }); diff --git a/tests/rules/service-keys-order-rule.spec.ts b/tests/rules/service-keys-order-rule.spec.ts index 509edbe..84508f2 100644 --- a/tests/rules/service-keys-order-rule.spec.ts +++ b/tests/rules/service-keys-order-rule.spec.ts @@ -40,47 +40,47 @@ services: const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); test('ServiceKeysOrderRule: should return a warning when service keys are in the wrong order', (t) => { - const rule = new ServiceKeysOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, - sourceCode: yamlWithIncorrectOrder, - }; + const rule = new ServiceKeysOrderRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, + sourceCode: yamlWithIncorrectOrder, + }; - const errors = rule.check(context); - t.is(errors.length, 4, 'There should be two warnings when service keys are out of order.'); + const errors = rule.check(context); + t.is(errors.length, 4, 'There should be two warnings when service keys are out of order.'); - const expectedMessages = [ - 'Key "ports" in service "web" is out of order.', - 'Key "environment" in service "web" is out of order.', - 'Key "volumes" in service "web" is out of order.', - 'Key "cpu_rt_period" in service "web" is out of order.', - ]; + const expectedMessages = [ + 'Key "ports" in service "web" is out of order.', + 'Key "environment" in service "web" is out of order.', + 'Key "volumes" in service "web" is out of order.', + 'Key "cpu_rt_period" in service "web" is out of order.', + ]; - errors.forEach((error, index) => { - t.true(error.message.includes(expectedMessages[index])); - }); + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); }); test('ServiceKeysOrderRule: should not return warnings when service keys are in the correct order', (t) => { - const rule = new ServiceKeysOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithCorrectOrder).toJS() as Record, - sourceCode: yamlWithCorrectOrder, - }; + const rule = new ServiceKeysOrderRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithCorrectOrder).toJS() as Record, + sourceCode: yamlWithCorrectOrder, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when service keys are in the correct order.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when service keys are in the correct order.'); }); test('ServiceKeysOrderRule: should fix the order of service keys', (t) => { - const rule = new ServiceKeysOrderRule(); - const fixedYAML = rule.fix(yamlWithIncorrectOrder); + const rule = new ServiceKeysOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectOrder); - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectOrder), - 'The service keys should be reordered correctly.', - ); + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCorrectOrder), + 'The service keys should be reordered correctly.', + ); }); diff --git a/tests/rules/service-ports-alphabetical-order-rule.spec.ts b/tests/rules/service-ports-alphabetical-order-rule.spec.ts index 572e2f2..912f3f6 100644 --- a/tests/rules/service-ports-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-ports-alphabetical-order-rule.spec.ts @@ -50,34 +50,34 @@ services: const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); test('ServicePortsAlphabeticalOrderRule: should return a warning when ports are not alphabetically ordered', (t) => { - const rule = new ServicePortsAlphabeticalOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithIncorrectPortOrder).toJS() as Record, - sourceCode: yamlWithIncorrectPortOrder, - }; + const rule = new ServicePortsAlphabeticalOrderRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithIncorrectPortOrder).toJS() as Record, + sourceCode: yamlWithIncorrectPortOrder, + }; - const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one warning when ports are out of order.'); + const errors = rule.check(context); + t.is(errors.length, 1, 'There should be one warning when ports are out of order.'); - t.true(errors[0].message.includes(`Ports in service "web" should be in alphabetical order.`)); + t.true(errors[0].message.includes(`Ports in service "web" should be in alphabetical order.`)); }); test('ServicePortsAlphabeticalOrderRule: should not return warnings when ports are alphabetically ordered', (t) => { - const rule = new ServicePortsAlphabeticalOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithCorrectPortOrder).toJS() as Record, - sourceCode: yamlWithCorrectPortOrder, - }; + const rule = new ServicePortsAlphabeticalOrderRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithCorrectPortOrder).toJS() as Record, + sourceCode: yamlWithCorrectPortOrder, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when ports are in alphabetical order.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when ports are in alphabetical order.'); }); test('ServicePortsAlphabeticalOrderRule: should fix the order of ports', (t) => { - const rule = new ServicePortsAlphabeticalOrderRule(); - const fixedYAML = rule.fix(yamlWithIncorrectPortOrder); + const rule = new ServicePortsAlphabeticalOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectPortOrder); - t.is(normalizeYAML(fixedYAML), normalizeYAML(yamlWithCorrectPortOrder), 'The ports should be reordered correctly.'); + t.is(normalizeYAML(fixedYAML), normalizeYAML(yamlWithCorrectPortOrder), 'The ports should be reordered correctly.'); }); diff --git a/tests/rules/services-alphabetical-order-rule.spec.ts b/tests/rules/services-alphabetical-order-rule.spec.ts index 8046947..83ace3c 100644 --- a/tests/rules/services-alphabetical-order-rule.spec.ts +++ b/tests/rules/services-alphabetical-order-rule.spec.ts @@ -36,42 +36,42 @@ services: const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); test('ServicesAlphabeticalOrderRule: should return a warning when services are out of order', (t) => { - const rule = new ServicesAlphabeticalOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, - sourceCode: yamlWithIncorrectOrder, - }; + const rule = new ServicesAlphabeticalOrderRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, + sourceCode: yamlWithIncorrectOrder, + }; - const errors = rule.check(context); - t.is(errors.length, 3, 'There should be 3 warnings when services are out of order.'); + const errors = rule.check(context); + t.is(errors.length, 3, 'There should be 3 warnings when services are out of order.'); - // Check error messages - t.true(errors[0].message.includes('Service "b-service" should be before "database".')); - t.true(errors[1].message.includes('Service "app" should be before "b-service".')); - t.true(errors[2].message.includes('Service "cache" should be before "database".')); + // Check error messages + t.true(errors[0].message.includes('Service "b-service" should be before "database".')); + t.true(errors[1].message.includes('Service "app" should be before "b-service".')); + t.true(errors[2].message.includes('Service "cache" should be before "database".')); }); test('ServicesAlphabeticalOrderRule: should not return warnings when services are in alphabetical order', (t) => { - const rule = new ServicesAlphabeticalOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithCorrectOrder).toJS() as Record, - sourceCode: yamlWithCorrectOrder, - }; + const rule = new ServicesAlphabeticalOrderRule(); + const context: LintContext = { + path: '/docker-compose.yml', + content: parseDocument(yamlWithCorrectOrder).toJS() as Record, + sourceCode: yamlWithCorrectOrder, + }; - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when services are in alphabetical order.'); + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when services are in alphabetical order.'); }); test('ServicesAlphabeticalOrderRule: should fix the order of services', (t) => { - const rule = new ServicesAlphabeticalOrderRule(); - const fixedYAML = rule.fix(yamlWithIncorrectOrder); + const rule = new ServicesAlphabeticalOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectOrder); - // Normalize both YAML strings for comparison - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectOrder), - 'The services should be reordered alphabetically.', - ); + // Normalize both YAML strings for comparison + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCorrectOrder), + 'The services should be reordered alphabetically.', + ); }); diff --git a/tests/rules/top-level-properties-order-rule.spec.ts b/tests/rules/top-level-properties-order-rule.spec.ts index 35ba220..96f0754 100644 --- a/tests/rules/top-level-properties-order-rule.spec.ts +++ b/tests/rules/top-level-properties-order-rule.spec.ts @@ -44,43 +44,43 @@ const filePath = '/docker-compose.yml'; const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); test('TopLevelPropertiesOrderRule: should return a warning when top-level properties are out of order', (t) => { - const rule = new TopLevelPropertiesOrderRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, - sourceCode: yamlWithIncorrectOrder, - }; - - const errors = rule.check(context); - t.is(errors.length, 3, 'There should be 3 warnings for out-of-order properties.'); - - // Check error messages - t.true(errors[0].message.includes('Property "x-b" is out of order.')); - t.true(errors[1].message.includes('Property "x-a" is out of order.')); - t.true(errors[2].message.includes('Property "networks" is out of order.')); + const rule = new TopLevelPropertiesOrderRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, + sourceCode: yamlWithIncorrectOrder, + }; + + const errors = rule.check(context); + t.is(errors.length, 3, 'There should be 3 warnings for out-of-order properties.'); + + // Check error messages + t.true(errors[0].message.includes('Property "x-b" is out of order.')); + t.true(errors[1].message.includes('Property "x-a" is out of order.')); + t.true(errors[2].message.includes('Property "networks" is out of order.')); }); test('TopLevelPropertiesOrderRule: should not return warnings when top-level properties are in the correct order', (t) => { - const rule = new TopLevelPropertiesOrderRule(); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithCorrectOrder).toJS() as Record, - sourceCode: yamlWithCorrectOrder, - }; - - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when top-level properties are in the correct order.'); + const rule = new TopLevelPropertiesOrderRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithCorrectOrder).toJS() as Record, + sourceCode: yamlWithCorrectOrder, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should be no warnings when top-level properties are in the correct order.'); }); test('TopLevelPropertiesOrderRule: should fix the order of top-level properties', (t) => { - const rule = new TopLevelPropertiesOrderRule(); - const fixedYAML = rule.fix(yamlWithIncorrectOrder); - - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectOrder), - 'The top-level properties should be reordered correctly.', - ); + const rule = new TopLevelPropertiesOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectOrder); + + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCorrectOrder), + 'The top-level properties should be reordered correctly.', + ); }); // Custom order tests @@ -119,46 +119,46 @@ x-b: `; test('TopLevelPropertiesOrderRule: should return warnings based on custom order', (t) => { - const customOrder = [ - TopLevelKeys.Version, - TopLevelKeys.Services, - TopLevelKeys.Volumes, - TopLevelKeys.Networks, - TopLevelKeys.XProperties, - ]; - - const rule = new TopLevelPropertiesOrderRule({ customOrder }); - const context: LintContext = { - path: filePath, - content: parseDocument(yamlWithCustomOrder).toJS() as Record, - sourceCode: yamlWithCustomOrder, - }; - - const errors = rule.check(context); - t.is(errors.length, 4, 'There should be 4 warnings for out-of-order properties based on the custom order.'); - - // Check error messages - t.true(errors[0].message.includes('Property "version" is out of order.')); - t.true(errors[1].message.includes('Property "volumes" is out of order.')); - t.true(errors[2].message.includes('Property "x-a" is out of order.')); - t.true(errors[3].message.includes('Property "networks" is out of order.')); + const customOrder = [ + TopLevelKeys.Version, + TopLevelKeys.Services, + TopLevelKeys.Volumes, + TopLevelKeys.Networks, + TopLevelKeys.XProperties, + ]; + + const rule = new TopLevelPropertiesOrderRule({ customOrder }); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithCustomOrder).toJS() as Record, + sourceCode: yamlWithCustomOrder, + }; + + const errors = rule.check(context); + t.is(errors.length, 4, 'There should be 4 warnings for out-of-order properties based on the custom order.'); + + // Check error messages + t.true(errors[0].message.includes('Property "version" is out of order.')); + t.true(errors[1].message.includes('Property "volumes" is out of order.')); + t.true(errors[2].message.includes('Property "x-a" is out of order.')); + t.true(errors[3].message.includes('Property "networks" is out of order.')); }); test('TopLevelPropertiesOrderRule: should fix the order of top-level properties based on custom order', (t) => { - const customOrder = [ - TopLevelKeys.Version, - TopLevelKeys.Services, - TopLevelKeys.Volumes, - TopLevelKeys.Networks, - TopLevelKeys.XProperties, - ]; - - const rule = new TopLevelPropertiesOrderRule({ customOrder }); - const fixedYAML = rule.fix(yamlWithCustomOrder); - - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCustomOrderCorrected), - 'The top-level properties should be reordered correctly based on custom order.', - ); + const customOrder = [ + TopLevelKeys.Version, + TopLevelKeys.Services, + TopLevelKeys.Volumes, + TopLevelKeys.Networks, + TopLevelKeys.XProperties, + ]; + + const rule = new TopLevelPropertiesOrderRule({ customOrder }); + const fixedYAML = rule.fix(yamlWithCustomOrder); + + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCustomOrderCorrected), + 'The top-level properties should be reordered correctly based on custom order.', + ); }); diff --git a/tests/util/files-finder.spec.ts b/tests/util/files-finder.spec.ts index f76e4eb..93542b5 100644 --- a/tests/util/files-finder.spec.ts +++ b/tests/util/files-finder.spec.ts @@ -20,89 +20,89 @@ const mockDirectoriesInDirectory = ['another_dir', 'node_modules']; const mockFilesInSubDirectory = ['docker-compose.yml', 'another-file.yaml', 'example.txt']; test.beforeEach(() => { - Logger.init(false); // Initialize logger + Logger.init(false); // Initialize logger }); const mockReaddirSync = (dir: string): string[] => { - if (dir === mockDirectory) { - return [...mockFilesInDirectory, ...mockDirectoriesInDirectory]; - } - if (dir === mockNodeModulesDirectory || dir === mockFolderDirectory) { - return mockFilesInSubDirectory; - } - return []; + if (dir === mockDirectory) { + return [...mockFilesInDirectory, ...mockDirectoriesInDirectory]; + } + if (dir === mockNodeModulesDirectory || dir === mockFolderDirectory) { + return mockFilesInSubDirectory; + } + return []; }; const mockStatSync = (filePath: string) => { - const isDirectory = - filePath === mockDirectory || filePath === mockNodeModulesDirectory || filePath === mockFolderDirectory; - return { - isDirectory: () => isDirectory, - isFile: () => !isDirectory, - }; + const isDirectory = + filePath === mockDirectory || filePath === mockNodeModulesDirectory || filePath === mockFolderDirectory; + return { + isDirectory: () => isDirectory, + isFile: () => !isDirectory, + }; }; const mockExistsSync = () => true; test('findFilesForLinting: should handle recursive search and find only compose files in directory and exclude node_modules', async (t) => { - // Use esmock to mock fs module - const { findFilesForLinting } = await esmock( - '../../src/util/files-finder.js', - { - 'node:fs': { existsSync: mockExistsSync, readdirSync: mockReaddirSync, statSync: mockStatSync }, - }, - ); + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: mockExistsSync, readdirSync: mockReaddirSync, statSync: mockStatSync }, + }, + ); - const result = findFilesForLinting([mockDirectory], false, []); + const result = findFilesForLinting([mockDirectory], false, []); - t.deepEqual(result, [mockDockerComposeFile, mockComposeFile], 'Should return only compose files on higher level'); + t.deepEqual(result, [mockDockerComposeFile, mockComposeFile], 'Should return only compose files on higher level'); - const resultRecursive = findFilesForLinting([mockDirectory], true, []); + const resultRecursive = findFilesForLinting([mockDirectory], true, []); - t.deepEqual( - resultRecursive, - [mockDockerComposeFile, mockComposeFile, mockSubDirectoryFile], - 'Should should handle recursive search and return only compose files and exclude files in node_modules subdirectory', - ); + t.deepEqual( + resultRecursive, + [mockDockerComposeFile, mockComposeFile, mockSubDirectoryFile], + 'Should should handle recursive search and return only compose files and exclude files in node_modules subdirectory', + ); }); test('findFilesForLinting: should return file directly if file is passed and search only compose in directory', async (t) => { - // Use esmock to mock fs module - const { findFilesForLinting } = await esmock( - '../../src/util/files-finder.js', - { - 'node:fs': { existsSync: mockExistsSync, statSync: mockStatSync, readdirSync: mockReaddirSync }, - }, - ); + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: mockExistsSync, statSync: mockStatSync, readdirSync: mockReaddirSync }, + }, + ); - const result = findFilesForLinting([mockAnotherFile], false, []); + const result = findFilesForLinting([mockAnotherFile], false, []); - t.deepEqual(result, [mockAnotherFile], 'Should return the another file directly when passed'); + t.deepEqual(result, [mockAnotherFile], 'Should return the another file directly when passed'); - const resultWithDirectory = findFilesForLinting([mockAnotherFile, mockFolderDirectory], false, []); + const resultWithDirectory = findFilesForLinting([mockAnotherFile, mockFolderDirectory], false, []); - t.deepEqual( - resultWithDirectory, - [mockAnotherFile, mockSubDirectoryFile], - 'Should return the another file directly when passed', - ); + t.deepEqual( + resultWithDirectory, + [mockAnotherFile, mockSubDirectoryFile], + 'Should return the another file directly when passed', + ); }); test('findFilesForLinting: should throw error if path does not exist', async (t) => { - // Use esmock to mock fs module - const { findFilesForLinting } = await esmock( - '../../src/util/files-finder.js', - { - 'node:fs': { existsSync: () => false }, - }, - ); - - const error = t.throws(() => findFilesForLinting([mockNonExistentPath], false, []), { - instanceOf: FileNotFoundError, - }); - - t.is( - error.message, - `File or directory not found: ${mockNonExistentPath}`, - 'Should throw FileNotFoundError if path does not exist', - ); + // Use esmock to mock fs module + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder.js', + { + 'node:fs': { existsSync: () => false }, + }, + ); + + const error = t.throws(() => findFilesForLinting([mockNonExistentPath], false, []), { + instanceOf: FileNotFoundError, + }); + + t.is( + error.message, + `File or directory not found: ${mockNonExistentPath}`, + 'Should throw FileNotFoundError if path does not exist', + ); }); diff --git a/tests/util/line-finder.spec.ts b/tests/util/line-finder.spec.ts index c07d77e..d52731d 100644 --- a/tests/util/line-finder.spec.ts +++ b/tests/util/line-finder.spec.ts @@ -2,7 +2,7 @@ import test from 'ava'; import { findLineNumberByKey, findLineNumberByValue } from '../../src/util/line-finder.js'; test('findLineNumberByKey: should return the correct line number when the key exists', (t) => { - const yamlContent = ` + const yamlContent = ` version: '3' services: web: @@ -11,12 +11,12 @@ services: image: postgres `; - const line = findLineNumberByKey(yamlContent, 'image'); - t.is(line, 5, 'Should return the correct line number for the key "image"'); + const line = findLineNumberByKey(yamlContent, 'image'); + t.is(line, 5, 'Should return the correct line number for the key "image"'); }); test('findLineNumberByKey: should return 1 when the key does not exist', (t) => { - const yamlContent = ` + const yamlContent = ` version: '3' services: web: @@ -25,12 +25,12 @@ services: image: postgres `; - const line = findLineNumberByKey(yamlContent, 'nonexistentKey'); - t.is(line, 1, 'Should return 1 when the key does not exist'); + const line = findLineNumberByKey(yamlContent, 'nonexistentKey'); + t.is(line, 1, 'Should return 1 when the key does not exist'); }); test('findLineNumberByKey: should work for nested keys', (t) => { - const yamlContent = ` + const yamlContent = ` version: '3' services: web: @@ -41,12 +41,12 @@ services: image: postgres `; - const line = findLineNumberByKey(yamlContent, 'ports'); - t.is(line, 6, 'Should return the correct line number for the nested key "ports"'); + const line = findLineNumberByKey(yamlContent, 'ports'); + t.is(line, 6, 'Should return the correct line number for the nested key "ports"'); }); test('findLineNumberByValue: should return the correct line number when the value exists', (t) => { - const yamlContent = ` + const yamlContent = ` version: '3' services: web: @@ -55,12 +55,12 @@ services: image: postgres `; - const line = findLineNumberByValue(yamlContent, 'nginx'); - t.is(line, 5, 'Should return the correct line number for the value "nginx"'); + const line = findLineNumberByValue(yamlContent, 'nginx'); + t.is(line, 5, 'Should return the correct line number for the value "nginx"'); }); test('findLineNumberByValue: should return 0 when the value does not exist', (t) => { - const yamlContent = ` + const yamlContent = ` version: '3' services: web: @@ -69,12 +69,12 @@ services: image: postgres `; - const line = findLineNumberByValue(yamlContent, 'nonexistentValue'); - t.is(line, 1, 'Should return 1 when the value does not exist'); + const line = findLineNumberByValue(yamlContent, 'nonexistentValue'); + t.is(line, 1, 'Should return 1 when the value does not exist'); }); test('findLineNumberByValue: should return the correct line number for a value inside an array', (t) => { - const yamlContent = ` + const yamlContent = ` version: '3' services: web: @@ -83,6 +83,6 @@ services: - "80:80" `; - const line = findLineNumberByValue(yamlContent, '80:80'); - t.is(line, 7, 'Should return the correct line number for the value "80:80"'); + const line = findLineNumberByValue(yamlContent, '80:80'); + t.is(line, 7, 'Should return the correct line number for the value "80:80"'); }); diff --git a/tests/util/service-ports-parser.spec.ts b/tests/util/service-ports-parser.spec.ts index db00671..2d65f60 100644 --- a/tests/util/service-ports-parser.spec.ts +++ b/tests/util/service-ports-parser.spec.ts @@ -3,47 +3,47 @@ import { Scalar, YAMLMap } from 'yaml'; import { extractPublishedPortValue, parsePortsRange } from '../../src/util/service-ports-parser.js'; test('extractPublishedPortValue should return port from scalar value with no IP', (t) => { - const scalarNode = new Scalar('8080:9000'); - const result = extractPublishedPortValue(scalarNode); - t.is(result, '8080'); + const scalarNode = new Scalar('8080:9000'); + const result = extractPublishedPortValue(scalarNode); + t.is(result, '8080'); }); test('extractPublishedPortValue should return correct port from scalar value with IP', (t) => { - const scalarNode = new Scalar('127.0.0.1:3000'); - const result = extractPublishedPortValue(scalarNode); - t.is(result, '3000'); + const scalarNode = new Scalar('127.0.0.1:3000'); + const result = extractPublishedPortValue(scalarNode); + t.is(result, '3000'); }); test('extractPublishedPortValue should return published port from map node', (t) => { - const mapNode = new YAMLMap(); - mapNode.set('published', '8080'); - const result = extractPublishedPortValue(mapNode); - t.is(result, '8080'); + const mapNode = new YAMLMap(); + mapNode.set('published', '8080'); + const result = extractPublishedPortValue(mapNode); + t.is(result, '8080'); }); test('extractPublishedPortValue should return empty string for unknown node type', (t) => { - const result = extractPublishedPortValue({}); - t.is(result, ''); + const result = extractPublishedPortValue({}); + t.is(result, ''); }); test('parsePortsRange should return array of ports for a range', (t) => { - t.deepEqual(parsePortsRange('3000-3002'), ['3000', '3001', '3002']); - t.deepEqual(parsePortsRange('3000-3000'), ['3000']); + t.deepEqual(parsePortsRange('3000-3002'), ['3000', '3001', '3002']); + t.deepEqual(parsePortsRange('3000-3000'), ['3000']); }); test('parsePortsRange should return single port when no range is specified', (t) => { - const result = parsePortsRange('8080'); - t.deepEqual(result, ['8080']); + const result = parsePortsRange('8080'); + t.deepEqual(result, ['8080']); }); test('parsePortsRange should return empty array for invalid range', (t) => { - t.deepEqual(parsePortsRange('$TEST'), []); - t.deepEqual(parsePortsRange('$TEST-3002'), []); - t.deepEqual(parsePortsRange('3000-$TEST'), []); - t.deepEqual(parsePortsRange('$TEST-$TEST'), []); - t.deepEqual(parsePortsRange('3000-$TEST-$TEST-5000'), []); + t.deepEqual(parsePortsRange('$TEST'), []); + t.deepEqual(parsePortsRange('$TEST-3002'), []); + t.deepEqual(parsePortsRange('3000-$TEST'), []); + t.deepEqual(parsePortsRange('$TEST-$TEST'), []); + t.deepEqual(parsePortsRange('3000-$TEST-$TEST-5000'), []); }); test('parsePortsRange should return empty array when start port is greater than end port', (t) => { - t.deepEqual(parsePortsRange('3005-3002'), []); + t.deepEqual(parsePortsRange('3005-3002'), []); }); From eb5d4b3d961a9a1bfe16c261aaf13069a6e54735 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Sun, 10 Nov 2024 14:58:16 +0100 Subject: [PATCH 06/16] feat(schema): update docker compose schema from source --- schemas/compose.schema.json | 73 ++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/schemas/compose.schema.json b/schemas/compose.schema.json index 335cbe0..95326f3 100644 --- a/schemas/compose.schema.json +++ b/schemas/compose.schema.json @@ -19,7 +19,6 @@ "include": { "type": "array", "items": { - "type": "object", "$ref": "#/definitions/include" }, "description": "compose sub-projects to be included." @@ -115,7 +114,7 @@ "pull": {"type": ["boolean", "string"]}, "target": {"type": "string"}, "shm_size": {"type": ["integer", "string"]}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "extra_hosts": {"$ref": "#/definitions/extra_hosts"}, "isolation": {"type": "string"}, "privileged": {"type": ["boolean", "string"]}, "secrets": {"$ref": "#/definitions/service_config_or_secret"}, @@ -216,7 +215,25 @@ ] }, "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "devices": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["source"], + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "permissions": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + } + }, "dns": {"$ref": "#/definitions/string_or_list"}, "dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true}, "dns_search": {"$ref": "#/definitions/string_or_list"}, @@ -249,7 +266,7 @@ ] }, "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "extra_hosts": {"$ref": "#/definitions/extra_hosts"}, "group_add": { "type": "array", "items": { @@ -353,6 +370,8 @@ }, "uniqueItems": true }, + "post_start": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}}, + "pre_stop": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}}, "privileged": {"type": ["boolean", "string"]}, "profiles": {"$ref": "#/definitions/list_of_strings"}, "pull_policy": {"type": "string", "enum": [ @@ -483,11 +502,11 @@ }, "additionalProperties": false, "patternProperties": {"^x-": {}} - }, - "additionalProperties": false, - "patternProperties": {"^x-": {}} + } } - } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} }, "deployment": { "id": "#/definitions/deployment", @@ -625,7 +644,10 @@ "options":{"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": {"^x-": {}}, + "required": [ + "capabilities" + ] } }, @@ -796,6 +818,20 @@ ] }, + "service_hook": { + "id": "#/definitions/service_hook", + "type": "object", + "properties": { + "command": {"$ref": "#/definitions/command"}, + "user": {"type": "string"}, + "privileged": {"type": ["boolean", "string"]}, + "working_dir": {"type": "string"}, + "environment": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "env_file": { "oneOf": [ {"type": "string"}, @@ -811,6 +847,9 @@ "path": { "type": "string" }, + "format": { + "type": "string" + }, "required": { "type": ["boolean", "string"], "default": true @@ -854,6 +893,22 @@ ] }, + "extra_hosts": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "array"] + }, + "uniqueItems": false + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + "blkio_limit": { "type": "object", "properties": { From 1df7c6dd8f92ee715a273fe162152797590db15a Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Sun, 10 Nov 2024 15:42:45 +0100 Subject: [PATCH 07/16] chore: add new linters and fix eslint warnings --- .eslintrc | 86 ----------- .eslintrc.cjs | 96 ++++++++++++ ava.config.js | 3 + package-lock.json | 142 +++++++++++++++++- package.json | 5 +- src/cli/cli.ts | 44 +++--- src/config/config.ts | 3 +- src/errors/compose-validation-error.ts | 12 +- src/errors/config-validation-error.ts | 13 ++ src/formatters/codeclimate.ts | 9 +- src/formatters/json.ts | 1 + src/formatters/junit.ts | 2 +- src/formatters/stylish.ts | 20 +-- src/index.ts | 4 +- src/linter/linter.ts | 30 ++-- src/rules/no-build-and-image-rule.ts | 6 +- .../no-duplicate-container-names-rule.ts | 6 +- src/rules/no-duplicate-exported-ports-rule.ts | 6 +- src/rules/no-quotes-in-volumes-rule.ts | 18 +-- src/rules/require-quotes-in-ports-rule.ts | 18 +-- .../service-container-name-regex-rule.ts | 6 +- ...ce-dependencies-alphabetical-order-rule.ts | 12 +- ...service-image-require-explicit-tag-rule.ts | 6 +- src/rules/service-keys-order-rule.ts | 18 +-- .../service-ports-alphabetical-order-rule.ts | 12 +- src/rules/services-alphabetical-order-rule.ts | 14 +- src/rules/top-level-properties-order-rule.ts | 10 +- src/util/check-for-updates.ts | 25 --- src/util/files-finder.ts | 24 +-- src/util/formatter-loader.ts | 6 +- src/util/line-finder.ts | 20 +-- src/util/load-schema.ts | 2 +- src/util/logger.ts | 16 +- src/util/rules-loader.ts | 12 +- src/util/service-ports-parser.ts | 4 +- tests/linter.spec.ts | 11 +- tests/rules/no-build-and-image-rule.spec.ts | 16 +- .../no-duplicate-container-names-rule.spec.ts | 7 +- .../no-duplicate-exported-ports-rule.spec.ts | 10 +- tests/rules/no-quotes-in-volumes-rule.spec.ts | 15 +- tests/rules/no-version-field-rule.spec.ts | 13 +- .../require-project-name-field-rule.spec.ts | 7 +- .../require-quotes-in-ports-rule.spec.ts | 22 ++- .../service-container-name-regex-rule.spec.ts | 7 +- ...pendencies-alphabetical-order-rule.spec.ts | 21 ++- ...ce-image-require-explicit-tag-rule.spec.ts | 19 ++- tests/rules/service-keys-order-rule.spec.ts | 12 +- ...vice-ports-alphabetical-order-rule.spec.ts | 12 +- .../services-alphabetical-order-rule.spec.ts | 12 +- .../top-level-properties-order-rule.spec.ts | 18 ++- tests/util/files-finder.spec.ts | 17 ++- tests/util/line-finder.spec.ts | 19 ++- tests/util/service-ports-parser.spec.ts | 25 ++- 53 files changed, 610 insertions(+), 364 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.cjs create mode 100644 src/errors/config-validation-error.ts delete mode 100644 src/util/check-for-updates.ts diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 4d11206..0000000 --- a/.eslintrc +++ /dev/null @@ -1,86 +0,0 @@ -{ - "root": true, - "env": { - "node": true, - "es2024": true - }, - "globals": { - "process": true, - "import": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.eslint.json", - "sourceType": "module", - "ecmaVersion": "latest" - }, - "extends": [ - "eslint:recommended", - "plugin:import/recommended", - "plugin:@typescript-eslint/recommended-type-checked", - "plugin:sonarjs/recommended-legacy", - "airbnb-base", - "airbnb-typescript/base", - "plugin:import/typescript", - "plugin:prettier/recommended" - ], - "plugins": [ - "@typescript-eslint", - "sonarjs", - "import", - "@stylistic", - "prettier" - ], - "settings": { - "import/resolver": { - "typescript": { - "alwaysTryTypes": true, - "project": "./tsconfig.eslint.json", - "extensions": [".ts", ".tsx", ".d.ts"] - }, - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx", ".d.ts"] - } - } - }, - "rules": { - "no-unused-vars": 0, - "no-console": 0, - "@typescript-eslint/no-unused-vars": [ - 2, - { - "args": "none" - } - ], - "prettier/prettier": 2, - "@stylistic/indent": ["error", 2], - "@stylistic/indent-binary-ops": ["error", 2], - "arrow-body-style": 0, - "prefer-arrow-callback": 0, - "prefer-rest-params": 0, - "sonarjs/cognitive-complexity": 1, - "@typescript-eslint/triple-slash-reference": [ - 2, - { - "path": "never", - "types": "always", - "lib": "always" - } - ], - "import/prefer-default-export": 0, - "import/no-default-export": 0, - "import/no-unresolved": [2, { "commonjs": true }], - "import/extensions": 0, - "import/order": [ - 2, - { - "groups": [ - "builtin", - "external", - "internal" - ] - } - ] - }, - "ignorePatterns": ["node_modules", "dist", ".tsimp", "coverage", "bin"] -} diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..802886b --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,96 @@ +module.exports = { + 'root': true, + 'env': { + 'node': true, + 'es2024': true, + }, + 'globals': { + 'process': true, + 'import': true, + }, + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.eslint.json', + 'sourceType': 'module', + 'ecmaVersion': 'latest', + }, + 'extends': [ + 'eslint:recommended', + 'plugin:import/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:sonarjs/recommended-legacy', + 'airbnb-base', + 'airbnb-typescript/base', + 'plugin:import/typescript', + 'plugin:ava/recommended', + 'plugin:unicorn/recommended', + 'plugin:prettier/recommended', + ], + 'plugins': [ + '@typescript-eslint', + 'sonarjs', + 'import', + '@stylistic', + 'prettier', + ], + 'settings': { + 'import/resolver': { + 'typescript': { + 'alwaysTryTypes': true, + 'project': './tsconfig.eslint.json', + 'extensions': ['.ts', '.tsx', '.d.ts'], + }, + 'node': { + 'extensions': ['.js', '.jsx', '.ts', '.tsx', '.d.ts'], + }, + }, + }, + 'rules': { + 'no-unused-vars': 0, + 'no-console': 0, + '@typescript-eslint/no-unused-vars': [ + 2, + { + 'args': 'none', + }, + ], + 'prettier/prettier': 2, + '@stylistic/indent': ['error', 2], + '@stylistic/indent-binary-ops': ['error', 2], + 'arrow-body-style': 0, + 'prefer-arrow-callback': 0, + 'prefer-rest-params': 0, + 'sonarjs/cognitive-complexity': 1, + '@typescript-eslint/triple-slash-reference': [ + 2, + { + 'path': 'never', + 'types': 'always', + 'lib': 'always', + }, + ], + '@typescript-eslint/ban-ts-comment': 0, + 'import/prefer-default-export': 0, + 'import/no-default-export': 0, + 'import/no-unresolved': [2, { 'commonjs': true }], + 'import/extensions': 0, + 'import/order': [ + 2, + { + 'groups': [ + 'builtin', + 'external', + 'internal', + ], + }, + ], + 'no-restricted-syntax': 0, + 'unicorn/no-array-for-each': 0, + 'unicorn/consistent-function-scoping': 0, + 'unicorn/no-null': 0, + 'unicorn/no-await-expression-member': 0, + 'unicorn/switch-case-braces': [2, 'avoid'], + 'unicorn/import-style': [2, {"styles": {"node:path": {"named": true, "default": false}}}] + }, + 'ignorePatterns': ['node_modules', 'dist', '.tsimp', 'coverage', 'bin'], +}; diff --git a/ava.config.js b/ava.config.js index 1acc21f..cf20b29 100644 --- a/ava.config.js +++ b/ava.config.js @@ -2,6 +2,9 @@ export default { files: ['tests/**/*.spec.ts'], extensions: { ts: 'module', + mjs: true, + cjs: true, + js: true, }, nodeArguments: ['--import=tsimp/import', '--no-warnings'], timeout: '2m', diff --git a/package-lock.json b/package-lock.json index 9eafaf8..36d52a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "ajv": "^8.17.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", - "yaml": "^2.5.1", + "yaml": "^2.6.0", "yargs": "^17.7.2" }, "bin": { @@ -40,12 +40,13 @@ "eslint-config-airbnb-typescript": "18.0.0", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "3.6.3", + "eslint-plugin-ava": "14.0.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-prettier": "5.2.1", "eslint-plugin-sonarjs": "1.0.3", "eslint-plugin-unicorn": "56.0.0", "esmock": "2.6.9", - "markdownlint-cli2": "^0.15.0", + "markdownlint-cli2": "0.15.0", "semantic-release": "24.2.0", "tap-xunit": "2.4.1", "tsimp": "2.0.12", @@ -3232,6 +3233,19 @@ "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "dev": true }, + "node_modules/enhance-visitors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/enhance-visitors/-/enhance-visitors-1.0.0.tgz", + "integrity": "sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.13.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -3775,6 +3789,60 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-ava": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-ava/-/eslint-plugin-ava-14.0.0.tgz", + "integrity": "sha512-XmKT6hppaipwwnLVwwvQliSU6AF1QMHiNoLD5JQfzhUhf0jY7CO0O624fQrE+Y/fTb9vbW8r77nKf7M/oHulxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhance-visitors": "^1.0.0", + "eslint-utils": "^3.0.0", + "espree": "^9.0.0", + "espurify": "^2.1.1", + "import-modules": "^2.1.0", + "micro-spelling-correcter": "^1.1.1", + "pkg-dir": "^5.0.0", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=14.17 <15 || >=16.4" + }, + "peerDependencies": { + "eslint": ">=8.26.0" + } + }, + "node_modules/eslint-plugin-ava/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-ava/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint-plugin-import": { "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", @@ -3907,6 +3975,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.0.tgz", "integrity": "sha512-aXpddVz/PQMmd69uxO98PA4iidiVNvA0xOtbpUoz1WhBd4RxOQQYqN618v68drY0hmy5uU2jy1bheKEVWBjlPw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "@eslint-community/eslint-utils": "^4.4.0", @@ -3963,6 +4032,35 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", @@ -4157,6 +4255,13 @@ "node": ">=4" } }, + "node_modules/espurify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/espurify/-/espurify-2.1.1.tgz", + "integrity": "sha512-zttWvnkhcDyGOhSH4vO2qCBILpdCMv/MX8lp4cqgRkQoDRGK2oZxi2GfWhlP2dIXmk7BaKeOTuzbHhyC68o8XQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -5112,6 +5217,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/import-modules": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.1.0.tgz", + "integrity": "sha512-8HEWcnkbGpovH9yInoisxaSoIg9Brbul+Ju3Kqe2UsYDUBJD/iQjSgEj0zPcTDPKfPp2fs5xlv1i+JSye/m1/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6098,6 +6216,13 @@ "node": ">= 8" } }, + "node_modules/micro-spelling-correcter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/micro-spelling-correcter/-/micro-spelling-correcter-1.1.1.tgz", + "integrity": "sha512-lkJ3Rj/mtjlRcHk6YyCbvZhyWTOzdBvTHsxMmZSk5jxN1YyVSQ+JETAom55mdzfcyDrY/49Z7UCW760BK30crg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9472,6 +9597,19 @@ "node": ">=4" } }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/plur": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", diff --git a/package.json b/package.json index e79868e..265be0d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "eslint": "eslint .", "eslint:fix": "eslint . --fix", "lint": "npm run eslint && npm run markdownlint", - "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#**/node_modules\" \"#CHANGELOG.md\"", + "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#**/node_modules\"", "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\" \"#**/node_modules\"", "markdownlint:fix-changelog": "markdownlint-cli2 --fix \"CHANGELOG.md\" && prettier --write \"CHANGELOG.md\"", "prettier": "prettier --write \"**/*.md\"", @@ -77,12 +77,13 @@ "eslint-config-airbnb-typescript": "18.0.0", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-typescript": "3.6.3", + "eslint-plugin-ava": "14.0.0", "eslint-plugin-import": "2.31.0", "eslint-plugin-prettier": "5.2.1", "eslint-plugin-sonarjs": "1.0.3", "eslint-plugin-unicorn": "56.0.0", "esmock": "2.6.9", - "markdownlint-cli2": "^0.15.0", + "markdownlint-cli2": "0.15.0", "semantic-release": "24.2.0", "tap-xunit": "2.4.1", "tsimp": "2.0.12", diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 879a8c6..99925e2 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,3 +1,5 @@ +#!/usr/bin/env node + import { readFileSync, writeFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -10,7 +12,7 @@ import { Logger, LOG_SOURCE } from '../util/logger.js'; import { loadFormatter } from '../util/formatter-loader.js'; const packageJson = JSON.parse( - readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8'), + readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf8'), ) as Record; const { argv } = yargsLib(hideBin(process.argv)) @@ -87,41 +89,43 @@ const { argv } = yargsLib(hideBin(process.argv)) .alias('version', 'v'); export default async function cli() { - const args = (await argv) as unknown as CLIConfig; + const cliArguments = (await argv) as unknown as CLIConfig; // Initialize the logger with the final debug and color options - Logger.init(args.debug); + Logger.init(cliArguments.debug); const logger = Logger.getInstance(); logger.debug(LOG_SOURCE.CLI, 'Debug mode is ON'); - logger.debug(LOG_SOURCE.CLI, 'Arguments:', args); + logger.debug(LOG_SOURCE.CLI, 'Arguments:', cliArguments); - const config = await loadConfig(args.config); + const config = await loadConfig(cliArguments.config).catch((error) => { + process.exit(1); + }); // Override config values with CLI arguments if they are provided - if (args.quiet) { - config.quiet = args.quiet; + if (cliArguments.quiet) { + config.quiet = cliArguments.quiet; } - if (args.debug) { - config.debug = args.debug; + if (cliArguments.debug) { + config.debug = cliArguments.debug; } - if (args.exclude.length > 0) { - config.exclude = args.exclude; + if (cliArguments.exclude.length > 0) { + config.exclude = cliArguments.exclude; } logger.debug(LOG_SOURCE.CLI, 'Final config:', config); const linter = new DCLinter(config); // Handle the `fix` and `fix-dry-run` flags - if (args.fix || args.fixDryRun) { - await linter.fixFiles(args.files, args.recursive, args.fixDryRun); + if (cliArguments.fix || cliArguments.fixDryRun) { + await linter.fixFiles(cliArguments.files, cliArguments.recursive, cliArguments.fixDryRun); } // Always run the linter after attempting to fix issues - let lintResults = await linter.lintFiles(args.files, args.recursive); + let lintResults = await linter.lintFiles(cliArguments.files, cliArguments.recursive); // Filter out warnings if `--quiet` is enabled - if (args.quiet) { + if (cliArguments.quiet) { // Keep only files with errors lintResults = lintResults .map((result) => ({ @@ -138,12 +142,12 @@ export default async function cli() { const totalWarnings = lintResults.reduce((count, result) => count + result.warningCount, 0); // Choose and apply the formatter - const formatter = await loadFormatter(args.formatter); + const formatter = await loadFormatter(cliArguments.formatter); const formattedResults = formatter(lintResults); // Output results - if (args.outputFile) { - writeFileSync(args.outputFile, formattedResults); + if (cliArguments.outputFile) { + writeFileSync(cliArguments.outputFile, formattedResults); } else { console.log(formattedResults); } @@ -152,10 +156,10 @@ export default async function cli() { if (totalErrors > 0) { logger.debug(LOG_SOURCE.CLI, `${totalErrors} errors found`); process.exit(1); - } else if (args.maxWarnings && args.maxWarnings >= 0 && totalWarnings > args.maxWarnings) { + } else if (cliArguments.maxWarnings && cliArguments.maxWarnings >= 0 && totalWarnings > cliArguments.maxWarnings) { logger.debug( LOG_SOURCE.CLI, - `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${args.maxWarnings})`, + `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${cliArguments.maxWarnings})`, ); process.exit(1); } diff --git a/src/config/config.ts b/src/config/config.ts index 7e0ab9d..ec3d241 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -3,6 +3,7 @@ import { Ajv } from 'ajv'; import type { Config } from './config.types.js'; import { Logger, LOG_SOURCE } from '../util/logger.js'; import { loadSchema } from '../util/load-schema.js'; +import { ConfigValidationError } from '../errors/config-validation-error.js'; function getDefaultConfig(): Config { return { @@ -23,7 +24,7 @@ async function validateConfig(config: Config): Promise { if (!validate(config)) { logger.error('Invalid configuration:', validate.errors); - process.exit(1); + throw new ConfigValidationError(validate.errors); } logger.debug(LOG_SOURCE.CONFIG, 'Validation complete'); return config; diff --git a/src/errors/compose-validation-error.ts b/src/errors/compose-validation-error.ts index e0e6cd2..8e6adc3 100644 --- a/src/errors/compose-validation-error.ts +++ b/src/errors/compose-validation-error.ts @@ -9,13 +9,13 @@ class ComposeValidationError extends Error { details: ErrorObject; - constructor(e: ErrorObject) { - super(`Validation error: ${e?.message}`); + constructor(error: ErrorObject) { + super(`Validation error: ${error?.message}`); this.name = 'ComposeValidationError'; - this.keyword = e.keyword; - this.instancePath = e.instancePath; - this.schemaPath = e.schemaPath; - this.details = e; + this.keyword = error.keyword; + this.instancePath = error.instancePath; + this.schemaPath = error.schemaPath; + this.details = error; } toString(): string { diff --git a/src/errors/config-validation-error.ts b/src/errors/config-validation-error.ts new file mode 100644 index 0000000..ff87111 --- /dev/null +++ b/src/errors/config-validation-error.ts @@ -0,0 +1,13 @@ +import { ErrorObject } from 'ajv'; + +class ConfigValidationError extends Error { + constructor(validationErrors?: ErrorObject[] | null | undefined) { + super(); + this.message = `Invalid configuration: ${ + validationErrors?.map((error) => error.message).join(', ') || 'No details' + }`; + this.name = 'ConfigValidationError'; + } +} + +export { ConfigValidationError }; diff --git a/src/formatters/codeclimate.ts b/src/formatters/codeclimate.ts index 840fb0e..8ae67ce 100644 --- a/src/formatters/codeclimate.ts +++ b/src/formatters/codeclimate.ts @@ -5,9 +5,12 @@ const generateFingerprint = (data: (string | null)[], hashes: Set): stri const hash = createHash('md5'); // Filter out null values and update the hash - data.filter(Boolean).forEach((part) => { - hash.update(part!.toString()); // Using non-null assertion since filter removed null values - }); + for (const part of data.filter(Boolean)) { + hash.update(part!.toString()); + } + // data.filter(Boolean).forEach((part) => { + // hash.update(part!.toString()); // Using non-null assertion since filter removed null values + // }); // Hash collisions should not happen, but if they do, a random hash will be generated. const hashCopy = hash.copy(); diff --git a/src/formatters/json.ts b/src/formatters/json.ts index 9e492e4..d24f319 100644 --- a/src/formatters/json.ts +++ b/src/formatters/json.ts @@ -1,5 +1,6 @@ import type { LintResult } from '../linter/linter.types.js'; export default function jsonFormatter(results: LintResult[]): string { + // eslint-disable-next-line unicorn/no-null return JSON.stringify(results, null, 2); } diff --git a/src/formatters/junit.ts b/src/formatters/junit.ts index eed9259..c1d1593 100644 --- a/src/formatters/junit.ts +++ b/src/formatters/junit.ts @@ -1,7 +1,7 @@ import type { LintResult } from '../linter/linter.types.js'; function escapeXml(unsafe: string): string { - return unsafe.replace(/[<>&'"]/g, (c) => { + return unsafe.replaceAll(/[<>&'"]/g, (c) => { switch (c) { case '<': return '<'; diff --git a/src/formatters/stylish.ts b/src/formatters/stylish.ts index 117bfbd..0723313 100644 --- a/src/formatters/stylish.ts +++ b/src/formatters/stylish.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import chalk from 'chalk'; import type { LintResult } from '../linter/linter.types.js'; @@ -15,31 +15,31 @@ export default function stylishFormatter(results: LintResult[]): string { } // Format the file path header without nested template literals - const filePath = chalk.underline(path.resolve(result.filePath)); + const filePath = chalk.underline(resolve(result.filePath)); output += `\n${filePath}\n`; - result.messages.forEach((msg) => { - const { type } = msg; + result.messages.forEach((message) => { + const { type } = message; const color = type === 'error' ? chalk.red : chalk.yellow; - const line = msg.line.toString().padStart(4, ' '); - const column = msg.column.toString().padEnd(4, ' '); + const line = message.line.toString().padStart(4, ' '); + const column = message.column.toString().padEnd(4, ' '); // Break down message formatting into separate parts const position = chalk.dim(`${line}:${column}`); const formattedType = color(type); - const ruleInfo = chalk.dim(msg.rule); + const ruleInfo = chalk.dim(message.rule); - output += `${position} ${formattedType} ${msg.message} ${ruleInfo}\n`; + output += `${position} ${formattedType} ${message.message} ${ruleInfo}\n`; // Increment counts without using the ++ operator if (type === 'error') { errorCount += 1; - if (msg.fixable) { + if (message.fixable) { fixableErrorCount += 1; } } else { warningCount += 1; - if (msg.fixable) { + if (message.fixable) { fixableWarningCount += 1; } } diff --git a/src/index.ts b/src/index.ts index 39c8f5b..5062148 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1 @@ -import { DCLinter } from './linter/linter.js'; - -export { DCLinter }; +export { DCLinter } from './linter/linter.js'; diff --git a/src/linter/linter.ts b/src/linter/linter.ts index ed18b55..100e4a2 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -43,20 +43,22 @@ class DCLinter { try { context.sourceCode = fs.readFileSync(file, 'utf8'); - const doc = parseDocument(context.sourceCode, { merge: true }); + const parsedDocument = parseDocument(context.sourceCode, { merge: true }); - if (doc.errors && doc.errors.length > 0) { - doc.errors.forEach((error) => { + if (parsedDocument.errors && parsedDocument.errors.length > 0) { + parsedDocument.errors.forEach((error) => { throw error; }); } // Use Record to type the parsed content safely - context.content = doc.toJS() as Record; + context.content = parsedDocument.toJS() as Record; validationComposeSchema(context.content); - } catch (e: unknown) { - if (e instanceof YAMLError) { - const startPos: { line: number; col: number } | undefined = Array.isArray(e.linePos) ? e.linePos[0] : e.linePos; + } catch (error: unknown) { + if (error instanceof YAMLError) { + const startPos: { line: number; col: number } | undefined = Array.isArray(error.linePos) + ? error.linePos[0] + : error.linePos; messages.push({ rule: 'invalid-yaml', category: 'style', @@ -67,13 +69,13 @@ class DCLinter { type: 'error', fixable: false, }); - } else if (e instanceof ComposeValidationError) { + } else if (error instanceof ComposeValidationError) { messages.push({ rule: 'invalid-schema', type: 'error', category: 'style', severity: 'critical', - message: e.toString(), + message: error.toString(), line: 1, column: 1, fixable: false, @@ -89,7 +91,7 @@ class DCLinter { type: 'error', fixable: false, }); - logger.debug(LOG_SOURCE.LINTER, `Error while processing file ${file}`, e); + logger.debug(LOG_SOURCE.LINTER, `Error while processing file ${file}`, error); } return { context: null, messages }; @@ -114,10 +116,10 @@ class DCLinter { messages.push(...fileMessages); } - const errorCount = messages.filter((msg) => msg.type === 'error').length; - const warningCount = messages.filter((msg) => msg.type === 'warning').length; - const fixableErrorCount = messages.filter((msg) => msg.fixable && msg.type === 'error').length; - const fixableWarningCount = messages.filter((msg) => msg.fixable && msg.type === 'warning').length; + const errorCount = messages.filter((message) => message.type === 'error').length; + const warningCount = messages.filter((message) => message.type === 'warning').length; + const fixableErrorCount = messages.filter((message) => message.fixable && message.type === 'error').length; + const fixableWarningCount = messages.filter((message) => message.fixable && message.type === 'warning').length; lintResults.push({ filePath: file, diff --git a/src/rules/no-build-and-image-rule.ts b/src/rules/no-build-and-image-rule.ts index 559abed..07cad8a 100644 --- a/src/rules/no-build-and-image-rule.ts +++ b/src/rules/no-build-and-image-rule.ts @@ -44,8 +44,8 @@ export default class NoBuildAndImageRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -62,7 +62,7 @@ export default class NoBuildAndImageRule implements LintRule { const hasPullPolicy = service.has('pull_policy'); if (hasBuild && hasImage && (!this.checkPullPolicy || !hasPullPolicy)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'build'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'build'); errors.push({ rule: this.name, type: this.type, diff --git a/src/rules/no-duplicate-container-names-rule.ts b/src/rules/no-duplicate-container-names-rule.ts index eb17309..d589f6b 100644 --- a/src/rules/no-duplicate-container-names-rule.ts +++ b/src/rules/no-duplicate-container-names-rule.ts @@ -42,8 +42,8 @@ export default class NoDuplicateContainerNamesRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -59,7 +59,7 @@ export default class NoDuplicateContainerNamesRule implements LintRule { const containerName = String(service.get('container_name')); if (containerNames.has(containerName)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'container_name'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'container_name'); errors.push({ rule: this.name, type: this.type, diff --git a/src/rules/no-duplicate-exported-ports-rule.ts b/src/rules/no-duplicate-exported-ports-rule.ts index e1f1c5f..447cfa0 100644 --- a/src/rules/no-duplicate-exported-ports-rule.ts +++ b/src/rules/no-duplicate-exported-ports-rule.ts @@ -43,8 +43,8 @@ export default class NoDuplicateExportedPortsRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -67,7 +67,7 @@ export default class NoDuplicateExportedPortsRule implements LintRule { currentPortRange.some((port) => { if (exportedPortsMap.has(port)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'ports'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'ports'); errors.push({ rule: this.name, type: this.type, diff --git a/src/rules/no-quotes-in-volumes-rule.ts b/src/rules/no-quotes-in-volumes-rule.ts index 98f232e..dcd614b 100644 --- a/src/rules/no-quotes-in-volumes-rule.ts +++ b/src/rules/no-quotes-in-volumes-rule.ts @@ -31,17 +31,17 @@ export default class NoQuotesInVolumesRule implements LintRule { return 'Quotes should not be used in volume names.'; } - private static extractVolumes(doc: ParsedNode | null, callback: (volume: Scalar) => void) { - if (!doc || !isMap(doc)) return; + private static extractVolumes(document: ParsedNode | null, callback: (volume: Scalar) => void) { + if (!document || !isMap(document)) return; - doc.items.forEach((item) => { + document.items.forEach((item) => { if (!isMap(item.value)) return; const serviceMap = item.value; serviceMap.items.forEach((service) => { if (!isMap(service.value)) return; - const volumes = service.value.items.find((i) => isScalar(i.key) && i.key.value === 'volumes'); + const volumes = service.value.items.find((node) => isScalar(node.key) && node.key.value === 'volumes'); if (!volumes || !isSeq(volumes.value)) return; volumes.value.items.forEach((volume) => { @@ -55,9 +55,9 @@ export default class NoQuotesInVolumesRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); + const parsedDocument = parseDocument(context.sourceCode); - NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { + NoQuotesInVolumesRule.extractVolumes(parsedDocument.contents, (volume) => { if (volume.type !== 'PLAIN') { errors.push({ rule: this.name, @@ -78,15 +78,15 @@ export default class NoQuotesInVolumesRule implements LintRule { // eslint-disable-next-line class-methods-use-this public fix(content: string): string { - const doc = parseDocument(content); + const parsedDocument = parseDocument(content); - NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { + NoQuotesInVolumesRule.extractVolumes(parsedDocument.contents, (volume) => { if (volume.type !== 'PLAIN') { // eslint-disable-next-line no-param-reassign volume.type = 'PLAIN'; } }); - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/rules/require-quotes-in-ports-rule.ts b/src/rules/require-quotes-in-ports-rule.ts index 646584f..f31deeb 100644 --- a/src/rules/require-quotes-in-ports-rule.ts +++ b/src/rules/require-quotes-in-ports-rule.ts @@ -46,17 +46,17 @@ export default class RequireQuotesInPortsRule implements LintRule { } // Static method to extract and process ports - private static extractPorts(doc: ParsedNode | null, callback: (port: Scalar) => void) { - if (!doc || !isMap(doc)) return; + private static extractPorts(document: ParsedNode | null, callback: (port: Scalar) => void) { + if (!document || !isMap(document)) return; - doc.items.forEach((item) => { + document.items.forEach((item) => { if (!isMap(item.value)) return; const serviceMap = item.value; serviceMap.items.forEach((service) => { if (!isMap(service.value)) return; - const ports = service.value.items.find((i) => isScalar(i.key) && i.key.value === 'ports'); + const ports = service.value.items.find((node) => isScalar(node.key) && node.key.value === 'ports'); if (!ports || !isSeq(ports.value)) return; ports.value.items.forEach((port) => { @@ -70,9 +70,9 @@ export default class RequireQuotesInPortsRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); + const parsedDocument = parseDocument(context.sourceCode); - RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { + RequireQuotesInPortsRule.extractPorts(parsedDocument.contents, (port) => { if (port.type !== this.getQuoteType()) { errors.push({ rule: this.name, @@ -92,15 +92,15 @@ export default class RequireQuotesInPortsRule implements LintRule { } public fix(content: string): string { - const doc = parseDocument(content); + const parsedDocument = parseDocument(content); - RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { + RequireQuotesInPortsRule.extractPorts(parsedDocument.contents, (port) => { if (port.type !== this.getQuoteType()) { // eslint-disable-next-line no-param-reassign port.type = this.getQuoteType(); } }); - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/rules/service-container-name-regex-rule.ts b/src/rules/service-container-name-regex-rule.ts index f2a1a44..ac2c273 100644 --- a/src/rules/service-container-name-regex-rule.ts +++ b/src/rules/service-container-name-regex-rule.ts @@ -39,8 +39,8 @@ export default class ServiceContainerNameRegexRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -55,7 +55,7 @@ export default class ServiceContainerNameRegexRule implements LintRule { const containerName = String(service.get('container_name')); if (!ServiceContainerNameRegexRule.containerNameRegex.test(containerName)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'container_name'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'container_name'); errors.push({ rule: this.name, type: this.type, diff --git a/src/rules/service-dependencies-alphabetical-order-rule.ts b/src/rules/service-dependencies-alphabetical-order-rule.ts index af6cd2b..1cfe09b 100644 --- a/src/rules/service-dependencies-alphabetical-order-rule.ts +++ b/src/rules/service-dependencies-alphabetical-order-rule.ts @@ -45,8 +45,8 @@ export default class ServiceDependenciesAlphabeticalOrderRule implements LintRul public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -67,7 +67,7 @@ export default class ServiceDependenciesAlphabeticalOrderRule implements LintRul const sortedDependencies = [...extractedDependencies].sort((a, b) => a.localeCompare(b)); if (JSON.stringify(extractedDependencies) !== JSON.stringify(sortedDependencies)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'depends_on'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'depends_on'); errors.push({ rule: this.name, type: this.type, @@ -87,8 +87,8 @@ export default class ServiceDependenciesAlphabeticalOrderRule implements LintRul // eslint-disable-next-line class-methods-use-this public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + const parsedDocument = parseDocument(content); + const services = parsedDocument.get('services'); if (!isMap(services)) return content; @@ -106,6 +106,6 @@ export default class ServiceDependenciesAlphabeticalOrderRule implements LintRul }); }); - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/rules/service-image-require-explicit-tag-rule.ts b/src/rules/service-image-require-explicit-tag-rule.ts index f526237..7d7ff9a 100644 --- a/src/rules/service-image-require-explicit-tag-rule.ts +++ b/src/rules/service-image-require-explicit-tag-rule.ts @@ -62,8 +62,8 @@ export default class ServiceImageRequireExplicitTagRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -78,7 +78,7 @@ export default class ServiceImageRequireExplicitTagRule implements LintRule { const image = String(service.get('image')); if (!this.isImageTagExplicit(image)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'image'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'image'); errors.push({ rule: this.name, type: this.type, diff --git a/src/rules/service-keys-order-rule.ts b/src/rules/service-keys-order-rule.ts index c33575c..d614736 100644 --- a/src/rules/service-keys-order-rule.ts +++ b/src/rules/service-keys-order-rule.ts @@ -104,13 +104,13 @@ export default class ServiceKeysOrderRule implements LintRule { private getCorrectOrder(keys: string[]): string[] { const otherKeys = keys.filter((key) => !Object.values(this.groups).flat().includes(key)).sort(); - return this.groupOrder.flatMap((group) => this.groups[group]).concat(otherKeys); + return [...this.groupOrder.flatMap((group) => this.groups[group]), ...otherKeys]; } public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -133,7 +133,7 @@ export default class ServiceKeysOrderRule implements LintRule { if (expectedIndex === -1) return; if (expectedIndex < lastSeenIndex) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, key); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, key); errors.push({ rule: this.name, type: this.type, @@ -155,8 +155,8 @@ export default class ServiceKeysOrderRule implements LintRule { } public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + const parsedDocument = parseDocument(content); + const services = parsedDocument.get('services'); if (!isMap(services)) return content; @@ -174,7 +174,7 @@ export default class ServiceKeysOrderRule implements LintRule { const orderedService = new YAMLMap(); correctOrder.forEach((key) => { - const item = service.items.find((i) => isScalar(i.key) && i.key.value === key); + const item = service.items.find((node) => isScalar(node.key) && node.key.value === key); if (item) { orderedService.add(item); } @@ -182,7 +182,7 @@ export default class ServiceKeysOrderRule implements LintRule { keys.forEach((key) => { if (!correctOrder.includes(key)) { - const item = service.items.find((i) => isScalar(i.key) && i.key.value === key); + const item = service.items.find((node) => isScalar(node.key) && node.key.value === key); if (item) { orderedService.add(item); } @@ -192,6 +192,6 @@ export default class ServiceKeysOrderRule implements LintRule { services.set(serviceName, orderedService); }); - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/rules/service-ports-alphabetical-order-rule.ts b/src/rules/service-ports-alphabetical-order-rule.ts index 69f90f9..a62b4d0 100644 --- a/src/rules/service-ports-alphabetical-order-rule.ts +++ b/src/rules/service-ports-alphabetical-order-rule.ts @@ -34,8 +34,8 @@ export default class ServicePortsAlphabeticalOrderRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -54,7 +54,7 @@ export default class ServicePortsAlphabeticalOrderRule implements LintRule { const sortedPorts = [...extractedPorts].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); if (JSON.stringify(extractedPorts) !== JSON.stringify(sortedPorts)) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName, 'ports'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'ports'); errors.push({ rule: this.name, type: this.type, @@ -74,8 +74,8 @@ export default class ServicePortsAlphabeticalOrderRule implements LintRule { // eslint-disable-next-line class-methods-use-this public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + const parsedDocument = parseDocument(content); + const services = parsedDocument.get('services'); if (!isMap(services)) return content; @@ -95,6 +95,6 @@ export default class ServicePortsAlphabeticalOrderRule implements LintRule { }); }); - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/rules/services-alphabetical-order-rule.ts b/src/rules/services-alphabetical-order-rule.ts index a2b20b1..f4abe38 100644 --- a/src/rules/services-alphabetical-order-rule.ts +++ b/src/rules/services-alphabetical-order-rule.ts @@ -50,8 +50,8 @@ export default class ServicesAlphabeticalOrderRule implements LintRule { public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const doc = parseDocument(context.sourceCode); - const services = doc.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -64,7 +64,7 @@ export default class ServicesAlphabeticalOrderRule implements LintRule { const misplacedBefore = ServicesAlphabeticalOrderRule.findMisplacedService(processedServices, serviceName); if (misplacedBefore) { - const line = findLineNumberForService(doc, context.sourceCode, serviceName); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName); errors.push({ rule: this.name, @@ -87,8 +87,8 @@ export default class ServicesAlphabeticalOrderRule implements LintRule { // eslint-disable-next-line class-methods-use-this public fix(content: string): string { - const doc = parseDocument(content); - const services = doc.get('services'); + const parsedDocument = parseDocument(content); + const services = parsedDocument.get('services'); if (!isMap(services)) return content; @@ -104,8 +104,8 @@ export default class ServicesAlphabeticalOrderRule implements LintRule { sortedServices.add(item); }); - doc.set('services', sortedServices); + parsedDocument.set('services', sortedServices); - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/rules/top-level-properties-order-rule.ts b/src/rules/top-level-properties-order-rule.ts index 884f627..f628cbb 100644 --- a/src/rules/top-level-properties-order-rule.ts +++ b/src/rules/top-level-properties-order-rule.ts @@ -106,8 +106,8 @@ export default class TopLevelPropertiesOrderRule implements LintRule { } public fix(content: string): string { - const doc = parseDocument(content); - const { contents } = doc; + const parsedDocument = parseDocument(content); + const { contents } = parsedDocument; if (!isMap(contents)) return content; @@ -124,14 +124,14 @@ export default class TopLevelPropertiesOrderRule implements LintRule { const reorderedMap = new YAMLMap(); correctOrder.forEach((key) => { - const item = contents.items.find((i) => isScalar(i.key) && String(i.key.value) === key); + const item = contents.items.find((node) => isScalar(node.key) && String(node.key.value) === key); if (item) { reorderedMap.items.push(item); } }); - doc.contents = reorderedMap as unknown as typeof doc.contents; + parsedDocument.contents = reorderedMap as unknown as typeof parsedDocument.contents; - return doc.toString(); + return parsedDocument.toString(); } } diff --git a/src/util/check-for-updates.ts b/src/util/check-for-updates.ts deleted file mode 100644 index e94aabe..0000000 --- a/src/util/check-for-updates.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* -import { version as currentVersion } from '../../package.json'; -import { LOG_SOURCE, Logger } from './logger.js'; - -async function checkForUpdates() { - const logger = Logger.getInstance(); - try { - const response = await fetch('https://registry.npmjs.org/docker-compose-linter'); - const data = await response.json(); - - const latestVersion = data['dist-tags'].latest; - - if (currentVersion !== latestVersion) { - logger.info(`A new release of docker-compose-linter is available: v${currentVersion} -> v${latestVersion}`); - logger.info(`Update it by running: npm install -g docker-compose-linter`); - } else { - logger.debug(LOG_SOURCE.UTIL, 'You are using the latest version of docker-compose-linter.'); - } - } catch (error) { - logger.debug(LOG_SOURCE.UTIL, 'Failed to check for updates:', error); - } -} - -export { checkForUpdates } -*/ diff --git a/src/util/files-finder.ts b/src/util/files-finder.ts index 8edbd61..cc199f5 100644 --- a/src/util/files-finder.ts +++ b/src/util/files-finder.ts @@ -17,27 +17,27 @@ export function findFilesForLinting(paths: string[], recursive: boolean, exclude if (excludePaths && excludePaths.length > 0) { excludePaths.forEach((p) => excludeSet.add(p)); } - const exclude = Array.from(excludeSet); + const exclude = [...excludeSet]; logger.debug('UTIL', `Paths to exclude: ${exclude.toString()}`); // Regular expression to match [compose*.yml, compose*.yaml, docker-compose*.yml, docker-compose*.yaml] files const dockerComposePattern = /^(docker-)?compose.*\.ya?ml$/; - paths.forEach((fileOrDir) => { - if (!fs.existsSync(fileOrDir)) { - logger.debug('UTIL', `File or directory not found: ${fileOrDir}`); - throw new FileNotFoundError(fileOrDir); + paths.forEach((fileOrDirectory) => { + if (!fs.existsSync(fileOrDirectory)) { + logger.debug('UTIL', `File or directory not found: ${fileOrDirectory}`); + throw new FileNotFoundError(fileOrDirectory); } let allPaths: string[] = []; - const fileOrDirStats = fs.statSync(fileOrDir); + const fileOrDirectoryStats = fs.statSync(fileOrDirectory); - if (fileOrDirStats.isDirectory()) { + if (fileOrDirectoryStats.isDirectory()) { try { - allPaths = fs.readdirSync(resolve(fileOrDir)).map((f) => join(fileOrDir, f)); + allPaths = fs.readdirSync(resolve(fileOrDirectory)).map((f) => join(fileOrDirectory, f)); } catch (error) { - logger.debug('UTIL', `Error reading directory: ${fileOrDir}`, error); + logger.debug('UTIL', `Error reading directory: ${fileOrDirectory}`, error); allPaths = []; } @@ -55,15 +55,15 @@ export function findFilesForLinting(paths: string[], recursive: boolean, exclude // If recursive search is enabled, search within the directory logger.debug('UTIL', `Recursive search is enabled, search within the directory: ${path}`); const nestedFiles = findFilesForLinting([path], recursive, exclude); - filesToCheck = filesToCheck.concat(nestedFiles); + filesToCheck = [...filesToCheck, ...nestedFiles]; } } else if (pathStats.isFile() && dockerComposePattern.test(basename(path))) { // Add the file to the list if it matches the pattern filesToCheck.push(path); } }); - } else if (fileOrDirStats.isFile()) { - filesToCheck.push(fileOrDir); + } else if (fileOrDirectoryStats.isFile()) { + filesToCheck.push(fileOrDirectory); } }); diff --git a/src/util/formatter-loader.ts b/src/util/formatter-loader.ts index 032ec46..036b465 100644 --- a/src/util/formatter-loader.ts +++ b/src/util/formatter-loader.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import type { LintResult } from '../linter/linter.types.js'; import { Logger } from './logger.js'; @@ -9,7 +9,7 @@ async function importFormatter(modulePath: string): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const Formatter = (await import(modulePath)).default; return Formatter as FormatterFunction; - } catch (error) { + } catch { throw new Error(`Module at ${modulePath} does not export a default formatter.`); } } @@ -18,7 +18,7 @@ export async function loadFormatter(formatterName: string): Promise { return JSON.parse( - readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), `../../schemas/${name}.schema.json`), 'utf-8'), + readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), `../../schemas/${name}.schema.json`), 'utf8'), ) as Record; } diff --git a/src/util/logger.ts b/src/util/logger.ts index cb13a29..e00472f 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -55,29 +55,29 @@ class Logger { } } - public debug(source: LogSource, ...args: unknown[]): void { + public debug(source: LogSource, ...options: unknown[]): void { if (this.debugMode) { const message = Logger.formatMessage('DEBUG', source); - console.debug(message, ...args); + console.debug(message, ...options); } } // eslint-disable-next-line class-methods-use-this - public info(...args: unknown[]): void { + public info(...options: unknown[]): void { const message = Logger.formatMessage('INFO'); - console.info(message, ...args); + console.info(message, ...options); } // eslint-disable-next-line class-methods-use-this - public warn(...args: unknown[]): void { + public warn(...options: unknown[]): void { const message = Logger.formatMessage('WARN'); - console.warn(message, ...args); + console.warn(message, ...options); } // eslint-disable-next-line class-methods-use-this - public error(...args: unknown[]): void { + public error(...options: unknown[]): void { const message = Logger.formatMessage('ERROR'); - console.error(message, ...args); + console.error(message, ...options); } } diff --git a/src/util/rules-loader.ts b/src/util/rules-loader.ts index 1e6358d..73ecf1c 100644 --- a/src/util/rules-loader.ts +++ b/src/util/rules-loader.ts @@ -1,15 +1,15 @@ -import path from 'node:path'; +import { join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import fs from 'node:fs'; import type { LintRule, LintMessageType } from '../linter/linter.types.js'; import type { Config, ConfigRuleLevel, ConfigRule } from '../config/config.types.js'; import { Logger } from './logger.js'; -async function importRule(file: string, rulesDir: string): Promise { +async function importRule(file: string, rulesDirectory: string): Promise { const logger = Logger.getInstance(); try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const RuleClass = (await import(path.join(rulesDir, file))).default; + const RuleClass = (await import(join(rulesDirectory, file))).default; if (typeof RuleClass === 'function') { return new (RuleClass as new () => LintRule)(); @@ -22,15 +22,15 @@ async function importRule(file: string, rulesDir: string): Promise { - const rulesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../rules'); + const rulesDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../rules'); const ruleFiles = fs - .readdirSync(rulesDir) + .readdirSync(rulesDirectory) .filter((file) => file.endsWith('.js') || (file.endsWith('.ts') && !file.endsWith('d.ts'))); // Parallel import with Promise.all const ruleInstances: (LintRule | null)[] = await Promise.all( - ruleFiles.map(async (file) => importRule(file, rulesDir)), + ruleFiles.map(async (file) => importRule(file, rulesDirectory)), ); const activeRules: LintRule[] = []; diff --git a/src/util/service-ports-parser.ts b/src/util/service-ports-parser.ts index 148fab4..682a29a 100644 --- a/src/util/service-ports-parser.ts +++ b/src/util/service-ports-parser.ts @@ -1,4 +1,4 @@ -import net from 'net'; +import net from 'node:net'; import { isMap, isScalar } from 'yaml'; function extractPublishedPortValue(yamlNode: unknown): string { @@ -38,7 +38,7 @@ function parsePortsRange(port: string): string[] { } const ports: string[] = []; - // eslint-disable-next-line no-plusplus + // eslint-disable-next-line no-plusplus,unicorn/prevent-abbreviations for (let i = start; i <= end; i++) { ports.push(i.toString()); } diff --git a/tests/linter.spec.ts b/tests/linter.spec.ts index db6dbb9..9a40e93 100644 --- a/tests/linter.spec.ts +++ b/tests/linter.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import esmock from 'esmock'; import { Logger } from '../src/util/logger.js'; import type { Config } from '../src/config/config.types.js'; @@ -51,11 +52,13 @@ services: image: nginx `; +// @ts-ignore TS2339 test.beforeEach(() => { Logger.init(false); // Initialize logger }); -test('DCLinter: should lint files correctly', async (t) => { +// @ts-ignore TS2349 +test('DCLinter: should lint files correctly', async (t: ExecutionContext) => { const mockFindFiles = (): string[] => [mockFilePath]; const mockLoadLintRules = (): LintRule[] => [mockRule]; const mockReadFileSync = (): string => mockFileContent; @@ -83,7 +86,8 @@ test('DCLinter: should lint files correctly', async (t) => { t.is(result[0].warningCount, 0, 'There should be no warnings'); }); -test('DCLinter: should lint multiple files correctly', async (t) => { +// @ts-ignore TS2349 +test('DCLinter: should lint multiple files correctly', async (t: ExecutionContext) => { const mockFindFiles = (): string[] => mockFilePaths; const mockLoadLintRules = (): LintRule[] => [mockRule]; const mockReadFileSync = (filePath: string): string => mockFileContent; @@ -109,7 +113,8 @@ test('DCLinter: should lint multiple files correctly', async (t) => { t.is(result[1].messages.length, 1, 'There should be one lint message for the second file'); }); -test('DCLinter: should fix files', async (t) => { +// @ts-ignore TS2349 +test('DCLinter: should fix files', async (t: ExecutionContext) => { const mockFindFiles = (): string[] => [mockFilePath]; const mockLoadLintRules = (): LintRule[] => [mockRule]; const mockReadFileSync = (): string => mockFileContent; diff --git a/tests/rules/no-build-and-image-rule.spec.ts b/tests/rules/no-build-and-image-rule.spec.ts index 26d2e26..73606d6 100644 --- a/tests/rules/no-build-and-image-rule.spec.ts +++ b/tests/rules/no-build-and-image-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import NoBuildAndImageRule from '../../src/rules/no-build-and-image-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -47,7 +48,8 @@ services: const filePath = '/docker-compose.yml'; -test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service', (t) => { +// @ts-ignore TS2349 +test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service', (t: ExecutionContext) => { const rule = new NoBuildAndImageRule(); const context: LintContext = { path: filePath, @@ -72,7 +74,8 @@ test('NoBuildAndImageRule: should return a warning when both "build" and "image" }); }); -test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service and checkPullPolicy is false', (t) => { +// @ts-ignore TS2349 +test('NoBuildAndImageRule: should return a warning when both "build" and "image" are used in a service and checkPullPolicy is false', (t: ExecutionContext) => { const rule = new NoBuildAndImageRule({ checkPullPolicy: false }); const context: LintContext = { path: filePath, @@ -97,7 +100,8 @@ test('NoBuildAndImageRule: should return a warning when both "build" and "image" }); }); -test('NoBuildAndImageRule: should not return warnings when "build" and "image" are used with pull_policy and checkPullPolicy is true', (t) => { +// @ts-ignore TS2349 +test('NoBuildAndImageRule: should not return warnings when "build" and "image" are used with pull_policy and checkPullPolicy is true', (t: ExecutionContext) => { const rule = new NoBuildAndImageRule({ checkPullPolicy: true }); const context: LintContext = { path: filePath, @@ -113,7 +117,8 @@ test('NoBuildAndImageRule: should not return warnings when "build" and "image" a ); }); -test('NoBuildAndImageRule: should not return warnings when only "build" is used', (t) => { +// @ts-ignore TS2349 +test('NoBuildAndImageRule: should not return warnings when only "build" is used', (t: ExecutionContext) => { const rule = new NoBuildAndImageRule(); const context: LintContext = { path: filePath, @@ -125,7 +130,8 @@ test('NoBuildAndImageRule: should not return warnings when only "build" is used' t.is(errors.length, 0, 'There should be no warnings when only "build" is used.'); }); -test('NoBuildAndImageRule: should not return warnings when only "image" is used', (t) => { +// @ts-ignore TS2349 +test('NoBuildAndImageRule: should not return warnings when only "image" is used', (t: ExecutionContext) => { const rule = new NoBuildAndImageRule(); const context: LintContext = { path: filePath, diff --git a/tests/rules/no-duplicate-container-names-rule.spec.ts b/tests/rules/no-duplicate-container-names-rule.spec.ts index f5f2683..9c7c382 100644 --- a/tests/rules/no-duplicate-container-names-rule.spec.ts +++ b/tests/rules/no-duplicate-container-names-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import NoDuplicateContainerNamesRule from '../../src/rules/no-duplicate-container-names-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -25,7 +26,8 @@ services: container_name: db_container `; -test('NoDuplicateContainerNamesRule: should return an error when duplicate container names are found', (t) => { +// @ts-ignore TS2349 +test('NoDuplicateContainerNamesRule: should return an error when duplicate container names are found', (t: ExecutionContext) => { const rule = new NoDuplicateContainerNamesRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -41,7 +43,8 @@ test('NoDuplicateContainerNamesRule: should return an error when duplicate conta t.true(errors[0].message.includes(expectedMessage)); }); -test('NoDuplicateContainerNamesRule: should not return errors when container names are unique', (t) => { +// @ts-ignore TS2349 +test('NoDuplicateContainerNamesRule: should not return errors when container names are unique', (t: ExecutionContext) => { const rule = new NoDuplicateContainerNamesRule(); const context: LintContext = { path: '/docker-compose.yml', diff --git a/tests/rules/no-duplicate-exported-ports-rule.spec.ts b/tests/rules/no-duplicate-exported-ports-rule.spec.ts index 45dacd5..4b01ce3 100644 --- a/tests/rules/no-duplicate-exported-ports-rule.spec.ts +++ b/tests/rules/no-duplicate-exported-ports-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import NoDuplicateExportedPortsRule from '../../src/rules/no-duplicate-exported-ports-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -101,7 +102,8 @@ services: const filePath = '/docker-compose.yml'; -test('NoDuplicateExportedPortsRule: should return multiple errors when duplicate exported ports are found', (t) => { +// @ts-ignore TS2349 +test('NoDuplicateExportedPortsRule: should return multiple errors when duplicate exported ports are found', (t: ExecutionContext) => { const rule = new NoDuplicateExportedPortsRule(); const context: LintContext = { path: filePath, @@ -125,7 +127,8 @@ test('NoDuplicateExportedPortsRule: should return multiple errors when duplicate }); }); -test('NoDuplicateExportedPortsRule: should not return errors when exported ports are unique', (t) => { +// @ts-ignore TS2349 +test('NoDuplicateExportedPortsRule: should not return errors when exported ports are unique', (t: ExecutionContext) => { const rule = new NoDuplicateExportedPortsRule(); const context: LintContext = { path: filePath, @@ -137,7 +140,8 @@ test('NoDuplicateExportedPortsRule: should not return errors when exported ports t.is(errors.length, 0, 'There should be no errors when exported ports are unique.'); }); -test('NoDuplicateExportedPortsRule: should return an error when range overlap is detected', (t) => { +// @ts-ignore TS2349 +test('NoDuplicateExportedPortsRule: should return an error when range overlap is detected', (t: ExecutionContext) => { const rule = new NoDuplicateExportedPortsRule(); const context: LintContext = { path: filePath, diff --git a/tests/rules/no-quotes-in-volumes-rule.spec.ts b/tests/rules/no-quotes-in-volumes-rule.spec.ts index e02e111..6b42030 100644 --- a/tests/rules/no-quotes-in-volumes-rule.spec.ts +++ b/tests/rules/no-quotes-in-volumes-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import NoQuotesInVolumesRule from '../../src/rules/no-quotes-in-volumes-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -17,7 +18,8 @@ services: - "data" `; -test('NoQuotesInVolumesRule: should not return errors for YAML without quotes in volumes', (t) => { +// @ts-ignore TS2349 +test('NoQuotesInVolumesRule: should not return errors for YAML without quotes in volumes', (t: ExecutionContext) => { const rule = new NoQuotesInVolumesRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -26,10 +28,11 @@ test('NoQuotesInVolumesRule: should not return errors for YAML without quotes in }; const errors = rule.check(context); - t.deepEqual(errors.length, 0, 'There should be no errors for correct YAML.'); + t.is(errors.length, 0, 'There should be no errors for correct YAML.'); }); -test('NoQuotesInVolumesRule: should return errors for YAML with quotes in volumes', (t) => { +// @ts-ignore TS2349 +test('NoQuotesInVolumesRule: should return errors for YAML with quotes in volumes', (t: ExecutionContext) => { const rule = new NoQuotesInVolumesRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -44,7 +47,8 @@ test('NoQuotesInVolumesRule: should return errors for YAML with quotes in volume t.is(errors[0].severity, 'info'); }); -test('NoQuotesInVolumesRule: should fix YAML with quotes in volumes', (t) => { +// @ts-ignore TS2349 +test('NoQuotesInVolumesRule: should fix YAML with quotes in volumes', (t: ExecutionContext) => { const rule = new NoQuotesInVolumesRule(); const fixedYAML = rule.fix(incorrectYAML); @@ -52,7 +56,8 @@ test('NoQuotesInVolumesRule: should fix YAML with quotes in volumes', (t) => { t.false(fixedYAML.includes('"data"'), 'The volume name should no longer have quotes.'); }); -test('NoQuotesInVolumesRule: should not modify YAML without quotes in volumes', (t) => { +// @ts-ignore TS2349 +test('NoQuotesInVolumesRule: should not modify YAML without quotes in volumes', (t: ExecutionContext) => { const rule = new NoQuotesInVolumesRule(); const fixedYAML = rule.fix(correctYAML); diff --git a/tests/rules/no-version-field-rule.spec.ts b/tests/rules/no-version-field-rule.spec.ts index 7aae094..e5c9537 100644 --- a/tests/rules/no-version-field-rule.spec.ts +++ b/tests/rules/no-version-field-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import NoVersionFieldRule from '../../src/rules/no-version-field-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -16,7 +17,8 @@ services: image: nginx `; -test('NoVersionFieldRule: should return an error when "version" field is present', (t) => { +// @ts-ignore TS2349 +test('NoVersionFieldRule: should return an error when "version" field is present', (t: ExecutionContext) => { const rule = new NoVersionFieldRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -38,7 +40,8 @@ test('NoVersionFieldRule: should return an error when "version" field is present t.is(errors[0].severity, 'minor'); }); -test('NoVersionFieldRule: should not return errors when "version" field is not present', (t) => { +// @ts-ignore TS2349 +test('NoVersionFieldRule: should not return errors when "version" field is not present', (t: ExecutionContext) => { const rule = new NoVersionFieldRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -56,14 +59,16 @@ test('NoVersionFieldRule: should not return errors when "version" field is not p t.is(errors.length, 0, 'There should be no errors when the "version" field is absent.'); }); -test('NoVersionFieldRule: should fix by removing the "version" field', (t) => { +// @ts-ignore TS2349 +test('NoVersionFieldRule: should fix by removing the "version" field', (t: ExecutionContext) => { const rule = new NoVersionFieldRule(); const fixedYAML = rule.fix(yamlWithVersion); t.false(fixedYAML.includes('version:'), 'The "version" field should be removed.'); }); -test('NoVersionFieldRule: should not modify YAML without "version" field', (t) => { +// @ts-ignore TS2349 +test('NoVersionFieldRule: should not modify YAML without "version" field', (t: ExecutionContext) => { const rule = new NoVersionFieldRule(); const fixedYAML = rule.fix(yamlWithoutVersion); diff --git a/tests/rules/require-project-name-field-rule.spec.ts b/tests/rules/require-project-name-field-rule.spec.ts index fbea128..7b8e725 100644 --- a/tests/rules/require-project-name-field-rule.spec.ts +++ b/tests/rules/require-project-name-field-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import RequireProjectNameFieldRule from '../../src/rules/require-project-name-field-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -16,7 +17,8 @@ services: image: nginx `; -test('RequiredProjectNameFieldRule: should return a warning when "name" field is missing', (t) => { +// @ts-ignore TS2349 +test('RequiredProjectNameFieldRule: should return a warning when "name" field is missing', (t: ExecutionContext) => { const rule = new RequireProjectNameFieldRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -37,7 +39,8 @@ test('RequiredProjectNameFieldRule: should return a warning when "name" field is t.is(errors[0].severity, 'minor'); }); -test('RequiredProjectNameFieldRule: should not return warnings when "name" field is present', (t) => { +// @ts-ignore TS2349 +test('RequiredProjectNameFieldRule: should not return warnings when "name" field is present', (t: ExecutionContext) => { const rule = new RequireProjectNameFieldRule(); const context: LintContext = { path: '/docker-compose.yml', diff --git a/tests/rules/require-quotes-in-ports-rule.spec.ts b/tests/rules/require-quotes-in-ports-rule.spec.ts index bd36dd3..490fddc 100644 --- a/tests/rules/require-quotes-in-ports-rule.spec.ts +++ b/tests/rules/require-quotes-in-ports-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import RequireQuotesInPortsRule from '../../src/rules/require-quotes-in-ports-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -26,7 +27,8 @@ services: const pathToFile = '/docker-compose.yml'; -test('RequireQuotesInPortsRule: should return a warning when ports are not quoted', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should return a warning when ports are not quoted', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const context: LintContext = { path: pathToFile, @@ -41,7 +43,8 @@ test('RequireQuotesInPortsRule: should return a warning when ports are not quote t.is(errors[0].severity, 'minor'); }); -test('RequireQuotesInPortsRule: should not return warnings when ports are quoted with single quotes', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should not return warnings when ports are quoted with single quotes', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const context: LintContext = { path: pathToFile, @@ -53,7 +56,8 @@ test('RequireQuotesInPortsRule: should not return warnings when ports are quoted t.is(errors.length, 0, 'There should be no warnings when ports are quoted with single quotes.'); }); -test('RequireQuotesInPortsRule: should not return warnings when ports are quoted with double quotes', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should not return warnings when ports are quoted with double quotes', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const context: LintContext = { path: pathToFile, @@ -65,7 +69,8 @@ test('RequireQuotesInPortsRule: should not return warnings when ports are quoted t.is(errors.length, 0, 'There should be no warnings when ports are quoted with double quotes.'); }); -test('RequireQuotesInPortsRule: should fix unquoted ports by adding single quotes and not modify already quoted ports', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should fix unquoted ports by adding single quotes and not modify already quoted ports', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const fixedYAML = rule.fix(yamlWithoutQuotes); @@ -73,7 +78,8 @@ test('RequireQuotesInPortsRule: should fix unquoted ports by adding single quote t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); -test('RequireQuotesInPortsRule: should fix double quotes ports by changing them to single quotes', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should fix double quotes ports by changing them to single quotes', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const fixedYAML = rule.fix(yamlWithSingleQuotes); @@ -81,7 +87,8 @@ test('RequireQuotesInPortsRule: should fix double quotes ports by changing them t.false(fixedYAML.includes(`"8080:80"`), 'The ports should not have double quotes.'); }); -test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quotes and not modify already quoted ports', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quotes and not modify already quoted ports', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const fixedYAML = rule.fix(yamlWithoutQuotes); @@ -89,7 +96,8 @@ test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quote t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); -test('RequireQuotesInPortsRule: should fix single quotes ports by changing them to double quotes', (t) => { +// @ts-ignore TS2349 +test('RequireQuotesInPortsRule: should fix single quotes ports by changing them to double quotes', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const fixedYAML = rule.fix(yamlWithSingleQuotes); diff --git a/tests/rules/service-container-name-regex-rule.spec.ts b/tests/rules/service-container-name-regex-rule.spec.ts index c06e6e4..2668d31 100644 --- a/tests/rules/service-container-name-regex-rule.spec.ts +++ b/tests/rules/service-container-name-regex-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import ServiceContainerNameRegexRule from '../../src/rules/service-container-name-regex-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -19,7 +20,8 @@ services: container_name: "my-app-123" `; -test('ServiceContainerNameRegexRule: should return an error for invalid container name', (t) => { +// @ts-ignore TS2349 +test('ServiceContainerNameRegexRule: should return an error for invalid container name', (t: ExecutionContext) => { const rule = new ServiceContainerNameRegexRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -35,7 +37,8 @@ test('ServiceContainerNameRegexRule: should return an error for invalid containe t.true(errors[0].message.includes(expectedMessage)); }); -test('ServiceContainerNameRegexRule: should not return an error for valid container name', (t) => { +// @ts-ignore TS2349 +test('ServiceContainerNameRegexRule: should not return an error for valid container name', (t: ExecutionContext) => { const rule = new ServiceContainerNameRegexRule(); const context: LintContext = { path: '/docker-compose.yml', diff --git a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts index 77fdcba..da75ebd 100644 --- a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import ServiceDependenciesAlphabeticalOrderRule from '../../src/rules/service-dependencies-alphabetical-order-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -48,12 +49,13 @@ services: `; // Helper function to normalize YAML -const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); const filePath = '/docker-compose.yml'; // Short syntax tests -test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when short syntax services are not in alphabetical order', (t) => { +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when short syntax services are not in alphabetical order', (t: ExecutionContext) => { const rule = new ServiceDependenciesAlphabeticalOrderRule(); const context: LintContext = { path: filePath, @@ -66,7 +68,8 @@ test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when sho t.true(errors[0].message.includes(`Services in "depends_on" for service "web" should be in alphabetical order.`)); }); -test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when short syntax services are in alphabetical order', (t) => { +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when short syntax services are in alphabetical order', (t: ExecutionContext) => { const rule = new ServiceDependenciesAlphabeticalOrderRule(); const context: LintContext = { path: filePath, @@ -78,7 +81,8 @@ test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when t.is(errors.length, 0, 'There should be no warnings when short syntax services are in alphabetical order.'); }); -test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of short syntax services', (t) => { +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of short syntax services', (t: ExecutionContext) => { const rule = new ServiceDependenciesAlphabeticalOrderRule(); const fixedYAML = rule.fix(yamlWithIncorrectShortSyntax); @@ -90,7 +94,8 @@ test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of short sy }); // Long syntax tests -test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when long syntax services are not in alphabetical order', (t) => { +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when long syntax services are not in alphabetical order', (t: ExecutionContext) => { const rule = new ServiceDependenciesAlphabeticalOrderRule(); const context: LintContext = { path: filePath, @@ -103,7 +108,8 @@ test('ServiceDependenciesAlphabeticalOrderRule: should return a warning when lon t.true(errors[0].message.includes(`Services in "depends_on" for service "web" should be in alphabetical order.`)); }); -test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when long syntax services are in alphabetical order', (t) => { +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when long syntax services are in alphabetical order', (t: ExecutionContext) => { const rule = new ServiceDependenciesAlphabeticalOrderRule(); const context: LintContext = { path: filePath, @@ -115,7 +121,8 @@ test('ServiceDependenciesAlphabeticalOrderRule: should not return warnings when t.is(errors.length, 0, 'There should be no warnings when long syntax services are in alphabetical order.'); }); -test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of long syntax services', (t) => { +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of long syntax services', (t: ExecutionContext) => { const rule = new ServiceDependenciesAlphabeticalOrderRule(); const fixedYAML = rule.fix(yamlWithIncorrectLongSyntax); diff --git a/tests/rules/service-image-require-explicit-tag-rule.spec.ts b/tests/rules/service-image-require-explicit-tag-rule.spec.ts index 6c9d4a4..3155c48 100644 --- a/tests/rules/service-image-require-explicit-tag-rule.spec.ts +++ b/tests/rules/service-image-require-explicit-tag-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import ServiceImageRequireExplicitTagRule from '../../src/rules/service-image-require-explicit-tag-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -93,7 +94,8 @@ services: const filePath = '/docker-compose.yml'; -test('ServiceImageRequireExplicitTagRule: should return a warning when no tag is specified', (t) => { +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when no tag is specified', (t: ExecutionContext) => { const rule = new ServiceImageRequireExplicitTagRule(); const context: LintContext = { path: filePath, @@ -116,7 +118,8 @@ test('ServiceImageRequireExplicitTagRule: should return a warning when no tag is }); }); -test('ServiceImageRequireExplicitTagRule: should return a warning when using latest tag', (t) => { +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when using latest tag', (t: ExecutionContext) => { const rule = new ServiceImageRequireExplicitTagRule(); const context: LintContext = { path: filePath, @@ -139,7 +142,8 @@ test('ServiceImageRequireExplicitTagRule: should return a warning when using lat }); }); -test('ServiceImageRequireExplicitTagRule: should return a warning when using stable tag', (t) => { +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when using stable tag', (t: ExecutionContext) => { const rule = new ServiceImageRequireExplicitTagRule(); const context: LintContext = { path: filePath, @@ -162,7 +166,8 @@ test('ServiceImageRequireExplicitTagRule: should return a warning when using sta }); }); -test('ServiceImageRequireExplicitTagRule: should return a warning when using prohibited tags', (t) => { +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when using prohibited tags', (t: ExecutionContext) => { const rule = new ServiceImageRequireExplicitTagRule(); const context: LintContext = { path: filePath, @@ -187,7 +192,8 @@ test('ServiceImageRequireExplicitTagRule: should return a warning when using pro }); }); -test('ServiceImageRequireExplicitTagRule: should use custom prohibitedTags when provided in the constructor', (t) => { +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should use custom prohibitedTags when provided in the constructor', (t: ExecutionContext) => { const rule = new ServiceImageRequireExplicitTagRule({ prohibitedTags: ['unstable', 'preview'] }); const context: LintContext = { @@ -209,7 +215,8 @@ test('ServiceImageRequireExplicitTagRule: should use custom prohibitedTags when }); }); -test('ServiceImageRequireExplicitTagRule: should not return warnings when a specific version tag or digest is used', (t) => { +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should not return warnings when a specific version tag or digest is used', (t: ExecutionContext) => { const rule = new ServiceImageRequireExplicitTagRule(); const context: LintContext = { path: filePath, diff --git a/tests/rules/service-keys-order-rule.spec.ts b/tests/rules/service-keys-order-rule.spec.ts index 84508f2..54f2860 100644 --- a/tests/rules/service-keys-order-rule.spec.ts +++ b/tests/rules/service-keys-order-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import ServiceKeysOrderRule from '../../src/rules/service-keys-order-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -37,9 +38,10 @@ services: `; // Helper function to strip spaces and normalize strings for comparison -const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); -test('ServiceKeysOrderRule: should return a warning when service keys are in the wrong order', (t) => { +// @ts-ignore TS2349 +test('ServiceKeysOrderRule: should return a warning when service keys are in the wrong order', (t: ExecutionContext) => { const rule = new ServiceKeysOrderRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -62,7 +64,8 @@ test('ServiceKeysOrderRule: should return a warning when service keys are in the }); }); -test('ServiceKeysOrderRule: should not return warnings when service keys are in the correct order', (t) => { +// @ts-ignore TS2349 +test('ServiceKeysOrderRule: should not return warnings when service keys are in the correct order', (t: ExecutionContext) => { const rule = new ServiceKeysOrderRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -74,7 +77,8 @@ test('ServiceKeysOrderRule: should not return warnings when service keys are in t.is(errors.length, 0, 'There should be no warnings when service keys are in the correct order.'); }); -test('ServiceKeysOrderRule: should fix the order of service keys', (t) => { +// @ts-ignore TS2349 +test('ServiceKeysOrderRule: should fix the order of service keys', (t: ExecutionContext) => { const rule = new ServiceKeysOrderRule(); const fixedYAML = rule.fix(yamlWithIncorrectOrder); diff --git a/tests/rules/service-ports-alphabetical-order-rule.spec.ts b/tests/rules/service-ports-alphabetical-order-rule.spec.ts index 912f3f6..e91804f 100644 --- a/tests/rules/service-ports-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-ports-alphabetical-order-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import ServicePortsAlphabeticalOrderRule from '../../src/rules/service-ports-alphabetical-order-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -47,9 +48,10 @@ services: `; // Helper function to strip spaces and normalize strings for comparison -const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); -test('ServicePortsAlphabeticalOrderRule: should return a warning when ports are not alphabetically ordered', (t) => { +// @ts-ignore TS2349 +test('ServicePortsAlphabeticalOrderRule: should return a warning when ports are not alphabetically ordered', (t: ExecutionContext) => { const rule = new ServicePortsAlphabeticalOrderRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -63,7 +65,8 @@ test('ServicePortsAlphabeticalOrderRule: should return a warning when ports are t.true(errors[0].message.includes(`Ports in service "web" should be in alphabetical order.`)); }); -test('ServicePortsAlphabeticalOrderRule: should not return warnings when ports are alphabetically ordered', (t) => { +// @ts-ignore TS2349 +test('ServicePortsAlphabeticalOrderRule: should not return warnings when ports are alphabetically ordered', (t: ExecutionContext) => { const rule = new ServicePortsAlphabeticalOrderRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -75,7 +78,8 @@ test('ServicePortsAlphabeticalOrderRule: should not return warnings when ports a t.is(errors.length, 0, 'There should be no warnings when ports are in alphabetical order.'); }); -test('ServicePortsAlphabeticalOrderRule: should fix the order of ports', (t) => { +// @ts-ignore TS2349 +test('ServicePortsAlphabeticalOrderRule: should fix the order of ports', (t: ExecutionContext) => { const rule = new ServicePortsAlphabeticalOrderRule(); const fixedYAML = rule.fix(yamlWithIncorrectPortOrder); diff --git a/tests/rules/services-alphabetical-order-rule.spec.ts b/tests/rules/services-alphabetical-order-rule.spec.ts index 83ace3c..3298cdc 100644 --- a/tests/rules/services-alphabetical-order-rule.spec.ts +++ b/tests/rules/services-alphabetical-order-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import ServicesAlphabeticalOrderRule from '../../src/rules/services-alphabetical-order-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -33,9 +34,10 @@ services: `; // Helper function to normalize strings for comparison -const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); -test('ServicesAlphabeticalOrderRule: should return a warning when services are out of order', (t) => { +// @ts-ignore TS2349 +test('ServicesAlphabeticalOrderRule: should return a warning when services are out of order', (t: ExecutionContext) => { const rule = new ServicesAlphabeticalOrderRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -52,7 +54,8 @@ test('ServicesAlphabeticalOrderRule: should return a warning when services are o t.true(errors[2].message.includes('Service "cache" should be before "database".')); }); -test('ServicesAlphabeticalOrderRule: should not return warnings when services are in alphabetical order', (t) => { +// @ts-ignore TS2349 +test('ServicesAlphabeticalOrderRule: should not return warnings when services are in alphabetical order', (t: ExecutionContext) => { const rule = new ServicesAlphabeticalOrderRule(); const context: LintContext = { path: '/docker-compose.yml', @@ -64,7 +67,8 @@ test('ServicesAlphabeticalOrderRule: should not return warnings when services ar t.is(errors.length, 0, 'There should be no warnings when services are in alphabetical order.'); }); -test('ServicesAlphabeticalOrderRule: should fix the order of services', (t) => { +// @ts-ignore TS2349 +test('ServicesAlphabeticalOrderRule: should fix the order of services', (t: ExecutionContext) => { const rule = new ServicesAlphabeticalOrderRule(); const fixedYAML = rule.fix(yamlWithIncorrectOrder); diff --git a/tests/rules/top-level-properties-order-rule.spec.ts b/tests/rules/top-level-properties-order-rule.spec.ts index 96f0754..cef34dd 100644 --- a/tests/rules/top-level-properties-order-rule.spec.ts +++ b/tests/rules/top-level-properties-order-rule.spec.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; import TopLevelPropertiesOrderRule, { TopLevelKeys } from '../../src/rules/top-level-properties-order-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; @@ -41,9 +42,10 @@ volumes: const filePath = '/docker-compose.yml'; // Helper function to normalize YAML strings for comparison -const normalizeYAML = (yaml: string) => yaml.replace(/\s+/g, ' ').trim(); +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); -test('TopLevelPropertiesOrderRule: should return a warning when top-level properties are out of order', (t) => { +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should return a warning when top-level properties are out of order', (t: ExecutionContext) => { const rule = new TopLevelPropertiesOrderRule(); const context: LintContext = { path: filePath, @@ -60,7 +62,8 @@ test('TopLevelPropertiesOrderRule: should return a warning when top-level proper t.true(errors[2].message.includes('Property "networks" is out of order.')); }); -test('TopLevelPropertiesOrderRule: should not return warnings when top-level properties are in the correct order', (t) => { +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should not return warnings when top-level properties are in the correct order', (t: ExecutionContext) => { const rule = new TopLevelPropertiesOrderRule(); const context: LintContext = { path: filePath, @@ -72,7 +75,8 @@ test('TopLevelPropertiesOrderRule: should not return warnings when top-level pro t.is(errors.length, 0, 'There should be no warnings when top-level properties are in the correct order.'); }); -test('TopLevelPropertiesOrderRule: should fix the order of top-level properties', (t) => { +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should fix the order of top-level properties', (t: ExecutionContext) => { const rule = new TopLevelPropertiesOrderRule(); const fixedYAML = rule.fix(yamlWithIncorrectOrder); @@ -118,7 +122,8 @@ x-b: some-key: some-value `; -test('TopLevelPropertiesOrderRule: should return warnings based on custom order', (t) => { +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should return warnings based on custom order', (t: ExecutionContext) => { const customOrder = [ TopLevelKeys.Version, TopLevelKeys.Services, @@ -144,7 +149,8 @@ test('TopLevelPropertiesOrderRule: should return warnings based on custom order' t.true(errors[3].message.includes('Property "networks" is out of order.')); }); -test('TopLevelPropertiesOrderRule: should fix the order of top-level properties based on custom order', (t) => { +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should fix the order of top-level properties based on custom order', (t: ExecutionContext) => { const customOrder = [ TopLevelKeys.Version, TopLevelKeys.Services, diff --git a/tests/util/files-finder.spec.ts b/tests/util/files-finder.spec.ts index 93542b5..d92f0e1 100644 --- a/tests/util/files-finder.spec.ts +++ b/tests/util/files-finder.spec.ts @@ -1,6 +1,7 @@ /* eslint-disable sonarjs/no-duplicate-string, @stylistic/indent */ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import esmock from 'esmock'; import { Logger } from '../../src/util/logger.js'; import { FileNotFoundError } from '../../src/errors/file-not-found-error.js'; @@ -19,15 +20,16 @@ const mockFilesInDirectory = ['docker-compose.yml', 'compose.yaml', 'another-fil const mockDirectoriesInDirectory = ['another_dir', 'node_modules']; const mockFilesInSubDirectory = ['docker-compose.yml', 'another-file.yaml', 'example.txt']; +// @ts-ignore TS2339 test.beforeEach(() => { Logger.init(false); // Initialize logger }); -const mockReaddirSync = (dir: string): string[] => { - if (dir === mockDirectory) { +const mockReaddirSync = (directory: string): string[] => { + if (directory === mockDirectory) { return [...mockFilesInDirectory, ...mockDirectoriesInDirectory]; } - if (dir === mockNodeModulesDirectory || dir === mockFolderDirectory) { + if (directory === mockNodeModulesDirectory || directory === mockFolderDirectory) { return mockFilesInSubDirectory; } return []; @@ -43,7 +45,8 @@ const mockStatSync = (filePath: string) => { }; const mockExistsSync = () => true; -test('findFilesForLinting: should handle recursive search and find only compose files in directory and exclude node_modules', async (t) => { +// @ts-ignore TS2349 +test('findFilesForLinting: should handle recursive search and find only compose files in directory and exclude node_modules', async (t: ExecutionContext) => { // Use esmock to mock fs module const { findFilesForLinting } = await esmock( '../../src/util/files-finder.js', @@ -65,7 +68,8 @@ test('findFilesForLinting: should handle recursive search and find only compose ); }); -test('findFilesForLinting: should return file directly if file is passed and search only compose in directory', async (t) => { +// @ts-ignore TS2349 +test('findFilesForLinting: should return file directly if file is passed and search only compose in directory', async (t: ExecutionContext) => { // Use esmock to mock fs module const { findFilesForLinting } = await esmock( '../../src/util/files-finder.js', @@ -87,7 +91,8 @@ test('findFilesForLinting: should return file directly if file is passed and sea ); }); -test('findFilesForLinting: should throw error if path does not exist', async (t) => { +// @ts-ignore TS2349 +test('findFilesForLinting: should throw error if path does not exist', async (t: ExecutionContext) => { // Use esmock to mock fs module const { findFilesForLinting } = await esmock( '../../src/util/files-finder.js', diff --git a/tests/util/line-finder.spec.ts b/tests/util/line-finder.spec.ts index d52731d..2cfc1ad 100644 --- a/tests/util/line-finder.spec.ts +++ b/tests/util/line-finder.spec.ts @@ -1,7 +1,9 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { findLineNumberByKey, findLineNumberByValue } from '../../src/util/line-finder.js'; -test('findLineNumberByKey: should return the correct line number when the key exists', (t) => { +// @ts-ignore TS2349 +test('findLineNumberByKey: should return the correct line number when the key exists', (t: ExecutionContext) => { const yamlContent = ` version: '3' services: @@ -15,7 +17,8 @@ services: t.is(line, 5, 'Should return the correct line number for the key "image"'); }); -test('findLineNumberByKey: should return 1 when the key does not exist', (t) => { +// @ts-ignore TS2349 +test('findLineNumberByKey: should return 1 when the key does not exist', (t: ExecutionContext) => { const yamlContent = ` version: '3' services: @@ -29,7 +32,8 @@ services: t.is(line, 1, 'Should return 1 when the key does not exist'); }); -test('findLineNumberByKey: should work for nested keys', (t) => { +// @ts-ignore TS2349 +test('findLineNumberByKey: should work for nested keys', (t: ExecutionContext) => { const yamlContent = ` version: '3' services: @@ -45,7 +49,8 @@ services: t.is(line, 6, 'Should return the correct line number for the nested key "ports"'); }); -test('findLineNumberByValue: should return the correct line number when the value exists', (t) => { +// @ts-ignore TS2349 +test('findLineNumberByValue: should return the correct line number when the value exists', (t: ExecutionContext) => { const yamlContent = ` version: '3' services: @@ -59,7 +64,8 @@ services: t.is(line, 5, 'Should return the correct line number for the value "nginx"'); }); -test('findLineNumberByValue: should return 0 when the value does not exist', (t) => { +// @ts-ignore TS2349 +test('findLineNumberByValue: should return 0 when the value does not exist', (t: ExecutionContext) => { const yamlContent = ` version: '3' services: @@ -73,7 +79,8 @@ services: t.is(line, 1, 'Should return 1 when the value does not exist'); }); -test('findLineNumberByValue: should return the correct line number for a value inside an array', (t) => { +// @ts-ignore TS2349 +test('findLineNumberByValue: should return the correct line number for a value inside an array', (t: ExecutionContext) => { const yamlContent = ` version: '3' services: diff --git a/tests/util/service-ports-parser.spec.ts b/tests/util/service-ports-parser.spec.ts index 2d65f60..32d3885 100644 --- a/tests/util/service-ports-parser.spec.ts +++ b/tests/util/service-ports-parser.spec.ts @@ -1,42 +1,50 @@ import test from 'ava'; +import type { ExecutionContext } from 'ava'; import { Scalar, YAMLMap } from 'yaml'; import { extractPublishedPortValue, parsePortsRange } from '../../src/util/service-ports-parser.js'; -test('extractPublishedPortValue should return port from scalar value with no IP', (t) => { +// @ts-ignore TS2349 +test('extractPublishedPortValue should return port from scalar value with no IP', (t: ExecutionContext) => { const scalarNode = new Scalar('8080:9000'); const result = extractPublishedPortValue(scalarNode); t.is(result, '8080'); }); -test('extractPublishedPortValue should return correct port from scalar value with IP', (t) => { +// @ts-ignore TS2349 +test('extractPublishedPortValue should return correct port from scalar value with IP', (t: ExecutionContext) => { const scalarNode = new Scalar('127.0.0.1:3000'); const result = extractPublishedPortValue(scalarNode); t.is(result, '3000'); }); -test('extractPublishedPortValue should return published port from map node', (t) => { +// @ts-ignore TS2349 +test('extractPublishedPortValue should return published port from map node', (t: ExecutionContext) => { const mapNode = new YAMLMap(); mapNode.set('published', '8080'); const result = extractPublishedPortValue(mapNode); t.is(result, '8080'); }); -test('extractPublishedPortValue should return empty string for unknown node type', (t) => { +// @ts-ignore TS2349 +test('extractPublishedPortValue should return empty string for unknown node type', (t: ExecutionContext) => { const result = extractPublishedPortValue({}); t.is(result, ''); }); -test('parsePortsRange should return array of ports for a range', (t) => { +// @ts-ignore TS2349 +test('parsePortsRange should return array of ports for a range', (t: ExecutionContext) => { t.deepEqual(parsePortsRange('3000-3002'), ['3000', '3001', '3002']); t.deepEqual(parsePortsRange('3000-3000'), ['3000']); }); -test('parsePortsRange should return single port when no range is specified', (t) => { +// @ts-ignore TS2349 +test('parsePortsRange should return single port when no range is specified', (t: ExecutionContext) => { const result = parsePortsRange('8080'); t.deepEqual(result, ['8080']); }); -test('parsePortsRange should return empty array for invalid range', (t) => { +// @ts-ignore TS2349 +test('parsePortsRange should return empty array for invalid range', (t: ExecutionContext) => { t.deepEqual(parsePortsRange('$TEST'), []); t.deepEqual(parsePortsRange('$TEST-3002'), []); t.deepEqual(parsePortsRange('3000-$TEST'), []); @@ -44,6 +52,7 @@ test('parsePortsRange should return empty array for invalid range', (t) => { t.deepEqual(parsePortsRange('3000-$TEST-$TEST-5000'), []); }); -test('parsePortsRange should return empty array when start port is greater than end port', (t) => { +// @ts-ignore TS2349 +test('parsePortsRange should return empty array when start port is greater than end port', (t: ExecutionContext) => { t.deepEqual(parsePortsRange('3005-3002'), []); }); From 4c681de0b0ef3491d98acd03c075671c07de9f78 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Sun, 10 Nov 2024 21:59:00 +0100 Subject: [PATCH 08/16] chore: rename linter config schema --- schemas/{cli-config.schema.json => linter-config.schema.json} | 0 src/config/config.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename schemas/{cli-config.schema.json => linter-config.schema.json} (100%) diff --git a/schemas/cli-config.schema.json b/schemas/linter-config.schema.json similarity index 100% rename from schemas/cli-config.schema.json rename to schemas/linter-config.schema.json diff --git a/src/config/config.ts b/src/config/config.ts index ec3d241..7ba3167 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -19,7 +19,7 @@ async function validateConfig(config: Config): Promise { logger.debug(LOG_SOURCE.CONFIG, 'Starting config validation'); const ajv = new Ajv(); - const schema = loadSchema('cli-config'); + const schema = loadSchema('linter-config'); const validate = ajv.compile(schema); if (!validate(config)) { From 7afc2586ff043c628cd74d915232873da756ffbe Mon Sep 17 00:00:00 2001 From: Ben Rexin Date: Sun, 13 Oct 2024 19:12:03 +0200 Subject: [PATCH 09/16] feat(new-rule): require port bindings to include inferface ip --- ...o-implicit-listen-everywhere-ports-rule.ts | 80 +++++++++++++++++++ src/util/service-ports-parser.ts | 34 +++++++- ...licit-listen-everywhere-ports-rule.spec.ts | 68 ++++++++++++++++ tests/util/service-ports-parser.spec.ts | 55 ++++++++++++- 4 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 src/rules/no-implicit-listen-everywhere-ports-rule.ts create mode 100644 tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts diff --git a/src/rules/no-implicit-listen-everywhere-ports-rule.ts b/src/rules/no-implicit-listen-everywhere-ports-rule.ts new file mode 100644 index 0000000..fe74497 --- /dev/null +++ b/src/rules/no-implicit-listen-everywhere-ports-rule.ts @@ -0,0 +1,80 @@ +import { parseDocument, isMap, isSeq, isScalar } from 'yaml'; +import type { + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types.js'; +import { findLineNumberForService } from '../util/line-finder.js'; +import { extractPublishedPortValue, extractPublishedPortInferfaceValue } from '../util/service-ports-parser.js'; + +export default class NoDuplicateExportedPortsRule implements LintRule { + public name = 'no-duplicate-exported-ports'; + + public type: LintMessageType = 'error'; + + public category: LintRuleCategory = 'security'; + + public severity: LintRuleSeverity = 'critical'; + + public meta: RuleMeta = { + description: + 'Ensure that exported ports in Docker Compose are bound to specific Interfaces to prevent accidential exposure of containers.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-exported-ports-rule.md', + }; + + public fixable = false; + + // eslint-disable-next-line class-methods-use-this + public getMessage({ serviceName, publishedPort }: { serviceName: string; publishedPort: string }): string { + return `Service "${serviceName}" is exporting port "${publishedPort}" without specifing the interface to listen on.`; + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const document = parseDocument(context.sourceCode); + const services = document.get('services'); + + if (!isMap(services)) return []; + + services.items.forEach((serviceItem) => { + if (!isScalar(serviceItem.key)) return; + + const serviceName = String(serviceItem.key.value); + const service = serviceItem.value; + + if (!isMap(service) || !service.has('ports')) return; + + const ports = service.get('ports'); + if (!isSeq(ports)) return; + + ports.items.forEach((portItem) => { + const publishedInterface = extractPublishedPortInferfaceValue(portItem); + const publishedPort = extractPublishedPortValue(portItem); + + if (publishedInterface === '') { + const line = findLineNumberForService(document, context.sourceCode, serviceName, 'ports'); + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: + this.getMessage({ + serviceName, + publishedPort, + }) + String(ports), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, + }); + } + }); + }); + return errors; + } +} diff --git a/src/util/service-ports-parser.ts b/src/util/service-ports-parser.ts index 682a29a..aaafe49 100644 --- a/src/util/service-ports-parser.ts +++ b/src/util/service-ports-parser.ts @@ -6,7 +6,12 @@ function extractPublishedPortValue(yamlNode: unknown): string { const value = String(yamlNode.value); // Check for host before ports - const parts = value.split(':'); + const parts = value.split(/:(?![^[]*\])/); + + if (parts[0].startsWith('[') && parts[0].endsWith(']')) { + parts[0] = parts[0].slice(1, -1); + } + if (net.isIP(parts[0])) { return String(parts[1]); } @@ -21,6 +26,31 @@ function extractPublishedPortValue(yamlNode: unknown): string { return ''; } +function extractPublishedPortInferfaceValue(yamlNode: unknown): string { + if (isScalar(yamlNode)) { + const value = String(yamlNode.value); + + // Split on single colon + const parts = value.split(/:(?![^[]*\])/); + + if (parts[0].startsWith('[') && parts[0].endsWith(']')) { + parts[0] = parts[0].slice(1, -1); + } + + if (net.isIP(parts[0])) { + return String(parts[0]); + } + + return ''; + } + + if (isMap(yamlNode)) { + return String(yamlNode.get('host_ip')) || ''; + } + + return ''; +} + function parsePortsRange(port: string): string[] { const [start, end] = port.split('-').map(Number); @@ -45,4 +75,4 @@ function parsePortsRange(port: string): string[] { return ports; } -export { extractPublishedPortValue, parsePortsRange }; +export { extractPublishedPortValue, extractPublishedPortInferfaceValue, parsePortsRange }; diff --git a/tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts b/tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts new file mode 100644 index 0000000..0d55c78 --- /dev/null +++ b/tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts @@ -0,0 +1,68 @@ +import test from 'ava'; +import { parseDocument } from 'yaml'; +import NoImplicitListenEverywherePortsRule from '../../src/rules/no-implicit-listen-everywhere-ports-rule.js'; +import type { LintContext } from '../../src/linter/linter.types.js'; + +// YAML with multiple duplicate exported ports +const yamlWithImplicitListenEverywherePorts = ` +services: + a-service: + image: nginx + ports: + - 8080:80 + b-service: + image: nginx + ports: + - 8080 +`; + +// YAML with unique exported ports using different syntax +const yamlWithExplicitListenIPPorts = ` +services: + a-service: + image: nginx + ports: + - 0.0.0.0:8080:8080 + b-service: + image: nginx + ports: + - 127.0.0.1:8081:8081 + - '[::1]:8082:8082' +`; + +const filePath = '/docker-compose.yml'; + +// @ts-ignore TS2349 +test('NoImplicitListenEverywherePortsRule: should return multiple errors when duplicate exported ports are found', (t) => { + const rule = new NoImplicitListenEverywherePortsRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithImplicitListenEverywherePorts).toJS() as Record, + sourceCode: yamlWithImplicitListenEverywherePorts, + }; + + const errors = rule.check(context); + t.is(errors.length, 2, 'There should be two errors when ports without host_ip are found.'); + + const expectedMessages = [ + 'Service "a-service" is exporting port "8080" without specifing the interface to listen on.', + 'Service "b-service" is exporting port "8080" without specifing the interface to listen on.', + ]; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessages[index])); + }); +}); + +// @ts-ignore TS2349 +test('NoImplicitListenEverywherePortsRule: should not return errors when exported ports have host_ip configured', (t) => { + const rule = new NoImplicitListenEverywherePortsRule(); + const context: LintContext = { + path: filePath, + content: parseDocument(yamlWithExplicitListenIPPorts).toJS() as Record, + sourceCode: yamlWithExplicitListenIPPorts, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should not be any errors.'); +}); diff --git a/tests/util/service-ports-parser.spec.ts b/tests/util/service-ports-parser.spec.ts index 32d3885..b66e098 100644 --- a/tests/util/service-ports-parser.spec.ts +++ b/tests/util/service-ports-parser.spec.ts @@ -1,7 +1,11 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { Scalar, YAMLMap } from 'yaml'; -import { extractPublishedPortValue, parsePortsRange } from '../../src/util/service-ports-parser.js'; +import { + extractPublishedPortValue, + extractPublishedPortInferfaceValue, + parsePortsRange, +} from '../../src/util/service-ports-parser.js'; // @ts-ignore TS2349 test('extractPublishedPortValue should return port from scalar value with no IP', (t: ExecutionContext) => { @@ -17,6 +21,13 @@ test('extractPublishedPortValue should return correct port from scalar value wit t.is(result, '3000'); }); +// @ts-ignore TS2349 +test('extractPublishedPortValue should return correct port from scalar value with IPv6', (t: ExecutionContext) => { + const scalarNode = new Scalar('[::1]:3000'); + const result = extractPublishedPortValue(scalarNode); + t.is(result, '3000'); +}); + // @ts-ignore TS2349 test('extractPublishedPortValue should return published port from map node', (t: ExecutionContext) => { const mapNode = new YAMLMap(); @@ -37,6 +48,48 @@ test('parsePortsRange should return array of ports for a range', (t: ExecutionCo t.deepEqual(parsePortsRange('3000-3000'), ['3000']); }); +// @ts-ignore TS2349 +test('extractPublishedPortInferfaceValue should return listen ip string for scalar without IP', (t: ExecutionContext) => { + const scalarNode = new Scalar('8080:8080'); + const result = extractPublishedPortInferfaceValue(scalarNode); + t.is(result, ''); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInferfaceValue should return listen ip string for scalar without IP and automapped port', (t: ExecutionContext) => { + const scalarNode = new Scalar('8080'); + const result = extractPublishedPortInferfaceValue(scalarNode); + t.is(result, ''); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInferfaceValue should return listen ip string for scalar on 127.0.0.1 with automapped port', (t: ExecutionContext) => { + const scalarNode = new Scalar('127.0.0.1:8080'); + const result = extractPublishedPortInferfaceValue(scalarNode); + t.is(result, '127.0.0.1'); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInferfaceValue should return listen ip string for scalar on 0.0.0.0 with automapped port', (t: ExecutionContext) => { + const scalarNode = new Scalar('0.0.0.0:8080'); + const result = extractPublishedPortInferfaceValue(scalarNode); + t.is(result, '0.0.0.0'); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInferfaceValue should return listen ip string for scalar on ::1 with automapped port', (t: ExecutionContext) => { + const scalarNode = new Scalar('[::1]:8080'); + const result = extractPublishedPortInferfaceValue(scalarNode); + t.is(result, '::1'); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInferfaceValue should return listen ip string for scalar on ::1 without automated port', (t: ExecutionContext) => { + const scalarNode = new Scalar('[::1]:8080:8080'); + const result = extractPublishedPortInferfaceValue(scalarNode); + t.is(result, '::1'); +}); + // @ts-ignore TS2349 test('parsePortsRange should return single port when no range is specified', (t: ExecutionContext) => { const result = parsePortsRange('8080'); From 1bcfa50150fa831e88f959eee5af741ae8468f92 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Mon, 11 Nov 2024 00:27:27 +0100 Subject: [PATCH 10/16] fix: change handling ports when it is a yamlMap --- src/util/service-ports-parser.ts | 12 +++---- tests/util/service-ports-parser.spec.ts | 48 ++++++++++++++++++------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/util/service-ports-parser.ts b/src/util/service-ports-parser.ts index aaafe49..d138dff 100644 --- a/src/util/service-ports-parser.ts +++ b/src/util/service-ports-parser.ts @@ -6,7 +6,7 @@ function extractPublishedPortValue(yamlNode: unknown): string { const value = String(yamlNode.value); // Check for host before ports - const parts = value.split(/:(?![^[]*\])/); + const parts = value.split(/:(?![^[]*])/); if (parts[0].startsWith('[') && parts[0].endsWith(']')) { parts[0] = parts[0].slice(1, -1); @@ -20,18 +20,18 @@ function extractPublishedPortValue(yamlNode: unknown): string { } if (isMap(yamlNode)) { - return String(yamlNode.get('published')) || ''; + return yamlNode.get('published')?.toString() || ''; } return ''; } -function extractPublishedPortInferfaceValue(yamlNode: unknown): string { +function extractPublishedPortInterfaceValue(yamlNode: unknown): string { if (isScalar(yamlNode)) { const value = String(yamlNode.value); // Split on single colon - const parts = value.split(/:(?![^[]*\])/); + const parts = value.split(/:(?![^[]*])/); if (parts[0].startsWith('[') && parts[0].endsWith(']')) { parts[0] = parts[0].slice(1, -1); @@ -45,7 +45,7 @@ function extractPublishedPortInferfaceValue(yamlNode: unknown): string { } if (isMap(yamlNode)) { - return String(yamlNode.get('host_ip')) || ''; + return yamlNode.get('host_ip')?.toString() || ''; } return ''; @@ -75,4 +75,4 @@ function parsePortsRange(port: string): string[] { return ports; } -export { extractPublishedPortValue, extractPublishedPortInferfaceValue, parsePortsRange }; +export { extractPublishedPortValue, extractPublishedPortInterfaceValue, parsePortsRange }; diff --git a/tests/util/service-ports-parser.spec.ts b/tests/util/service-ports-parser.spec.ts index b66e098..4926c7d 100644 --- a/tests/util/service-ports-parser.spec.ts +++ b/tests/util/service-ports-parser.spec.ts @@ -3,7 +3,7 @@ import type { ExecutionContext } from 'ava'; import { Scalar, YAMLMap } from 'yaml'; import { extractPublishedPortValue, - extractPublishedPortInferfaceValue, + extractPublishedPortInterfaceValue, parsePortsRange, } from '../../src/util/service-ports-parser.js'; @@ -49,47 +49,69 @@ test('parsePortsRange should return array of ports for a range', (t: ExecutionCo }); // @ts-ignore TS2349 -test('extractPublishedPortInferfaceValue should return listen ip string for scalar without IP', (t: ExecutionContext) => { +test('parsePortsRange should return empty string from map node', (t: ExecutionContext) => { + const mapNode = new YAMLMap(); + const result = extractPublishedPortValue(mapNode); + t.is(result, ''); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInterfaceValue should return listen ip string for scalar without IP', (t: ExecutionContext) => { const scalarNode = new Scalar('8080:8080'); - const result = extractPublishedPortInferfaceValue(scalarNode); + const result = extractPublishedPortInterfaceValue(scalarNode); t.is(result, ''); }); // @ts-ignore TS2349 -test('extractPublishedPortInferfaceValue should return listen ip string for scalar without IP and automapped port', (t: ExecutionContext) => { +test('extractPublishedPortInterfaceValue should return listen ip string for scalar without IP and automapped port', (t: ExecutionContext) => { const scalarNode = new Scalar('8080'); - const result = extractPublishedPortInferfaceValue(scalarNode); + const result = extractPublishedPortInterfaceValue(scalarNode); t.is(result, ''); }); // @ts-ignore TS2349 -test('extractPublishedPortInferfaceValue should return listen ip string for scalar on 127.0.0.1 with automapped port', (t: ExecutionContext) => { +test('extractPublishedPortInterfaceValue should return listen ip string for scalar on 127.0.0.1 with automapped port', (t: ExecutionContext) => { const scalarNode = new Scalar('127.0.0.1:8080'); - const result = extractPublishedPortInferfaceValue(scalarNode); + const result = extractPublishedPortInterfaceValue(scalarNode); t.is(result, '127.0.0.1'); }); // @ts-ignore TS2349 -test('extractPublishedPortInferfaceValue should return listen ip string for scalar on 0.0.0.0 with automapped port', (t: ExecutionContext) => { +test('extractPublishedPortInterfaceValue should return listen ip string for scalar on 0.0.0.0 with automapped port', (t: ExecutionContext) => { const scalarNode = new Scalar('0.0.0.0:8080'); - const result = extractPublishedPortInferfaceValue(scalarNode); + const result = extractPublishedPortInterfaceValue(scalarNode); t.is(result, '0.0.0.0'); }); // @ts-ignore TS2349 -test('extractPublishedPortInferfaceValue should return listen ip string for scalar on ::1 with automapped port', (t: ExecutionContext) => { +test('extractPublishedPortInterfaceValue should return listen ip string for scalar on ::1 with automapped port', (t: ExecutionContext) => { const scalarNode = new Scalar('[::1]:8080'); - const result = extractPublishedPortInferfaceValue(scalarNode); + const result = extractPublishedPortInterfaceValue(scalarNode); t.is(result, '::1'); }); // @ts-ignore TS2349 -test('extractPublishedPortInferfaceValue should return listen ip string for scalar on ::1 without automated port', (t: ExecutionContext) => { +test('extractPublishedPortInterfaceValue should return listen ip string for scalar on ::1 without automated port', (t: ExecutionContext) => { const scalarNode = new Scalar('[::1]:8080:8080'); - const result = extractPublishedPortInferfaceValue(scalarNode); + const result = extractPublishedPortInterfaceValue(scalarNode); t.is(result, '::1'); }); +// @ts-ignore TS2349 +test('extractPublishedPortValue should return host_ip from map node', (t: ExecutionContext) => { + const mapNode = new YAMLMap(); + mapNode.set('host_ip', '0.0.0.0'); + const result = extractPublishedPortInterfaceValue(mapNode); + t.is(result, '0.0.0.0'); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortValue should return empty string from map node', (t: ExecutionContext) => { + const mapNode = new YAMLMap(); + const result = extractPublishedPortInterfaceValue(mapNode); + t.is(result, ''); +}); + // @ts-ignore TS2349 test('parsePortsRange should return single port when no range is specified', (t: ExecutionContext) => { const result = parsePortsRange('8080'); From 9b04d2c8caeb9f4cf94d223442c3e5b033b6057f Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Mon, 11 Nov 2024 00:29:23 +0100 Subject: [PATCH 11/16] docs(no-unbound-port-interfaces-rule): rename rule and add documentation --- docs/rules/no-unbound-port-interfaces-rule.md | 95 +++++++++++++++++++ ....ts => no-unbound-port-interfaces-rule.ts} | 34 ++++--- tests/mocks/docker-compose.yml | 2 +- ...> no-unbound-port-interfaces-rule.spec.ts} | 23 +++-- 4 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 docs/rules/no-unbound-port-interfaces-rule.md rename src/rules/{no-implicit-listen-everywhere-ports-rule.ts => no-unbound-port-interfaces-rule.ts} (57%) rename tests/rules/{no-implicit-listen-everywhere-ports-rule.spec.ts => no-unbound-port-interfaces-rule.spec.ts} (63%) diff --git a/docs/rules/no-unbound-port-interfaces-rule.md b/docs/rules/no-unbound-port-interfaces-rule.md new file mode 100644 index 0000000..f570d7b --- /dev/null +++ b/docs/rules/no-unbound-port-interfaces-rule.md @@ -0,0 +1,95 @@ +# No Unbound Port Interfaces Rule + +When specifying ports for services in Docker Compose, it is recommended to explicitly set the host interface or IP +address to prevent accidental exposure to the network. + +- **Rule Name**: no-unbound-port-interfaces +- **Type**: error +- **Category**: security +- **Severity**: major +- **Fixable**: no + +## Problematic Code Example + +```yaml +services: + database: + image: postgres + ports: + - "5432:5432" # Exposed on all interfaces (0.0.0.0) by default +``` + +## Correct Code Example + +```yaml +services: + database: + image: postgres + ports: + - "127.0.0.1:5432:5432" +``` + +## Rule Details and Rationale + +This rule helps prevent accidental exposure of container ports to the local network. Without specifying a host interface +or IP address, services may unintentionally become accessible from outside, posing a security risk. It is recommended to +always set a `host_ip` to appropriately limit container access. + +When Docker Compose ports are specified without a host IP address, the ports are bound to all network interfaces by +default (i.e., `0.0.0.0`). This means that any service running in a container is accessible from all IP addresses that +can reach the host machine. If the Docker setup is running on a server connected to the internet, any exposed ports +become open to the world, potentially exposing sensitive services or data. + +Consider a Docker Compose setup for a development environment with the following configuration: + +```yaml +services: + database: + image: postgres + ports: + - "5432:5432" # Exposed on all interfaces (0.0.0.0) by default + app: + image: myapp + depends_on: + - database + ports: + - "80:80" # Exposed on all interfaces (0.0.0.0) by default + +``` + +Because both services are exposed on `0.0.0.0` (all network interfaces), any client with access to the network, +including the internet if this is a cloud-hosted server, can connect directly to these services without restriction. + +**Unauthorized Access:** Attackers could scan the IP of the host machine and discover open ports (80 and 5432). They +might attempt brute-force attacks on the database or probe the application for vulnerabilities. + +**Data Leakage:** If the PostgreSQL database doesn’t have proper authentication or is configured with weak credentials, +an attacker could gain direct access to the database, leading to data exfiltration or corruption. + +**Service Disruption:** By connecting to open ports, attackers could flood services with requests, potentially causing a +denial-of-service (DoS) or exhausting resources for legitimate users. + +If you need to keep services accessible only within Docker’s internal network for inter-container communication, +consider using [`expose`](https://docs.docker.com/reference/compose-file/services/#expose) instead of `ports` in your +Docker Compose configuration. + +### Usage in Local Environments + +If Docker Compose is used exclusively in a local environment (e.g., for development), explicitly specifying IP addresses +may seem redundant, as containers are isolated by default in a virtual network. However, for strict security adherence, +specifying an interface even in local networks can help avoid accidental network exposure. In these cases, consider +configuring the rule as a recommendation (a warning) to highlight potential risks but not enforce strict compliance. + +If additional complexity hinders readability, you can add an exception to this rule for strictly local configurations to +skip IP interface checks in such cases. + +## Version + +This rule was introduced in Docker-Compose-Linter [1.1.0](https://github.com/zavoloklom/docker-compose-linter/releases). + +## References + +- [Docker Compose Ports Reference](https://docs.docker.com/compose/compose-file/#ports) +- [Networking in Compose](https://docs.docker.com/compose/how-tos/networking/) +- [Docker Networks Explained](https://accesto.com/blog/docker-networks-explained-part-2/) +- [OWASP Docker Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) diff --git a/src/rules/no-implicit-listen-everywhere-ports-rule.ts b/src/rules/no-unbound-port-interfaces-rule.ts similarity index 57% rename from src/rules/no-implicit-listen-everywhere-ports-rule.ts rename to src/rules/no-unbound-port-interfaces-rule.ts index fe74497..247ffce 100644 --- a/src/rules/no-implicit-listen-everywhere-ports-rule.ts +++ b/src/rules/no-unbound-port-interfaces-rule.ts @@ -9,34 +9,34 @@ import type { RuleMeta, } from '../linter/linter.types.js'; import { findLineNumberForService } from '../util/line-finder.js'; -import { extractPublishedPortValue, extractPublishedPortInferfaceValue } from '../util/service-ports-parser.js'; +import { extractPublishedPortInterfaceValue } from '../util/service-ports-parser.js'; -export default class NoDuplicateExportedPortsRule implements LintRule { - public name = 'no-duplicate-exported-ports'; +export default class NoUnboundPortInterfacesRule implements LintRule { + public name = 'no-unbound-port-interfaces'; public type: LintMessageType = 'error'; public category: LintRuleCategory = 'security'; - public severity: LintRuleSeverity = 'critical'; + public severity: LintRuleSeverity = 'major'; public meta: RuleMeta = { description: - 'Ensure that exported ports in Docker Compose are bound to specific Interfaces to prevent accidential exposure of containers.', - url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-exported-ports-rule.md', + 'Ensure that exported ports in Docker Compose are bound to specific Interfaces to prevent unintentional exposing services to the network.', + url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-unbound-port-interfaces-rule.md', }; public fixable = false; // eslint-disable-next-line class-methods-use-this - public getMessage({ serviceName, publishedPort }: { serviceName: string; publishedPort: string }): string { - return `Service "${serviceName}" is exporting port "${publishedPort}" without specifing the interface to listen on.`; + public getMessage({ serviceName, port }: { serviceName: string; port: string }): string { + return `Service "${serviceName}" is exporting port "${port}" without specifying the interface to listen on.`; } public check(context: LintContext): LintMessage[] { const errors: LintMessage[] = []; - const document = parseDocument(context.sourceCode); - const services = document.get('services'); + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.get('services'); if (!isMap(services)) return []; @@ -52,21 +52,19 @@ export default class NoDuplicateExportedPortsRule implements LintRule { if (!isSeq(ports)) return; ports.items.forEach((portItem) => { - const publishedInterface = extractPublishedPortInferfaceValue(portItem); - const publishedPort = extractPublishedPortValue(portItem); + const publishedInterface = extractPublishedPortInterfaceValue(portItem); if (publishedInterface === '') { - const line = findLineNumberForService(document, context.sourceCode, serviceName, 'ports'); + const line = findLineNumberForService(parsedDocument, context.sourceCode, serviceName, 'ports'); errors.push({ rule: this.name, type: this.type, category: this.category, severity: this.severity, - message: - this.getMessage({ - serviceName, - publishedPort, - }) + String(ports), + message: this.getMessage({ + serviceName, + port: isSeq(portItem) ? portItem.toString() : String(portItem), + }), line, column: 1, meta: this.meta, diff --git a/tests/mocks/docker-compose.yml b/tests/mocks/docker-compose.yml index d43b7e9..7811d9d 100644 --- a/tests/mocks/docker-compose.yml +++ b/tests/mocks/docker-compose.yml @@ -35,7 +35,7 @@ services: command: sh -c "tail -f /dev/null" ports: - "11150:3000" - - "11032:3000" + - "127.0.0.1:11032:3000" b-service: build: context: ../../app/b-service diff --git a/tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts b/tests/rules/no-unbound-port-interfaces-rule.spec.ts similarity index 63% rename from tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts rename to tests/rules/no-unbound-port-interfaces-rule.spec.ts index 0d55c78..e29841b 100644 --- a/tests/rules/no-implicit-listen-everywhere-ports-rule.spec.ts +++ b/tests/rules/no-unbound-port-interfaces-rule.spec.ts @@ -1,6 +1,6 @@ import test from 'ava'; import { parseDocument } from 'yaml'; -import NoImplicitListenEverywherePortsRule from '../../src/rules/no-implicit-listen-everywhere-ports-rule.js'; +import NoUnboundPortInterfacesRule from '../../src/rules/no-unbound-port-interfaces-rule.js'; import type { LintContext } from '../../src/linter/linter.types.js'; // YAML with multiple duplicate exported ports @@ -10,10 +10,14 @@ services: image: nginx ports: - 8080:80 + - 8080 b-service: image: nginx ports: - - 8080 + - target: 1000 + published: 8081 + protocol: tcp + mode: host `; // YAML with unique exported ports using different syntax @@ -33,8 +37,8 @@ services: const filePath = '/docker-compose.yml'; // @ts-ignore TS2349 -test('NoImplicitListenEverywherePortsRule: should return multiple errors when duplicate exported ports are found', (t) => { - const rule = new NoImplicitListenEverywherePortsRule(); +test('NoUnboundPortInterfacesRule: should return multiple errors when duplicate exported ports are found', (t) => { + const rule = new NoUnboundPortInterfacesRule(); const context: LintContext = { path: filePath, content: parseDocument(yamlWithImplicitListenEverywherePorts).toJS() as Record, @@ -42,11 +46,12 @@ test('NoImplicitListenEverywherePortsRule: should return multiple errors when du }; const errors = rule.check(context); - t.is(errors.length, 2, 'There should be two errors when ports without host_ip are found.'); + t.is(errors.length, 3, 'There should be two errors when ports without host_ip are found.'); const expectedMessages = [ - 'Service "a-service" is exporting port "8080" without specifing the interface to listen on.', - 'Service "b-service" is exporting port "8080" without specifing the interface to listen on.', + 'Service "a-service" is exporting port "8080:80" without specifying the interface to listen on.', + 'Service "a-service" is exporting port "8080" without specifying the interface to listen on.', + 'Service "b-service" is exporting port "{"target":1000,"published":8081,"protocol":"tcp","mode":"host"}" without specifying the interface to listen on.', ]; errors.forEach((error, index) => { @@ -55,8 +60,8 @@ test('NoImplicitListenEverywherePortsRule: should return multiple errors when du }); // @ts-ignore TS2349 -test('NoImplicitListenEverywherePortsRule: should not return errors when exported ports have host_ip configured', (t) => { - const rule = new NoImplicitListenEverywherePortsRule(); +test('NoUnboundPortInterfacesRule: should not return errors when exported ports have host_ip configured', (t) => { + const rule = new NoUnboundPortInterfacesRule(); const context: LintContext = { path: filePath, content: parseDocument(yamlWithExplicitListenIPPorts).toJS() as Record, From cafb4d0fbc49989af19af19e07a29b2e16d9dff0 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Mon, 11 Nov 2024 01:40:47 +0100 Subject: [PATCH 12/16] feat(require-quotes-in-ports-rule): handle "expose" section similar to "ports" --- docs/rules/require-quotes-in-ports-rule.md | 30 +++++-- src/rules/require-quotes-in-ports-rule.ts | 83 +++++++++++-------- src/util/line-finder.ts | 54 ++---------- tests/mocks/docker-compose.yml | 3 + .../require-quotes-in-ports-rule.spec.ts | 25 ++++-- 5 files changed, 104 insertions(+), 91 deletions(-) diff --git a/docs/rules/require-quotes-in-ports-rule.md b/docs/rules/require-quotes-in-ports-rule.md index 732ba3a..737b3ab 100644 --- a/docs/rules/require-quotes-in-ports-rule.md +++ b/docs/rules/require-quotes-in-ports-rule.md @@ -1,7 +1,8 @@ # Require Quotes in Ports Rule -Ensures that the port values in the `ports` section of services in the Docker Compose file are enclosed in quotes. Using -quotes around port numbers can prevent YAML parsing issues and ensure that the ports are interpreted correctly. +Ensures that the port values in the `ports` and `expose` sections of services in the Docker Compose file are enclosed in +quotes. Using quotes around port numbers can prevent YAML parsing issues and ensure that the ports are interpreted +correctly. This rule is fixable. The linter can automatically add the required quotes around port numbers without altering the ports themselves. The type of quotes (single or double) can be configured via the `quoteType` option. @@ -21,6 +22,8 @@ services: ports: - 80:80 - 443:443 + expose: + - 3000 ``` ## Correct code example (Single Quotes) @@ -32,6 +35,8 @@ services: ports: - '80:80' - '443:443' + expose: + - '3000' ``` ## Correct code example (Double Quotes) @@ -43,17 +48,23 @@ services: ports: - "80:80" - "443:443" + expose: + - "3000" ``` ## Rule Details and Rationale -This rule ensures that the port numbers specified in the `ports` section of services are enclosed in quotes. Quoting -ports helps avoid potential issues with YAML parsing, where unquoted numbers might be misinterpreted or cause unexpected -behavior. By enforcing this rule, we ensure that the configuration is more robust and consistent. +This rule ensures that the port numbers specified in the `ports` and `expose` sections of services are enclosed in +quotes. Quoting ports helps avoid potential issues with YAML parsing, where unquoted numbers might be misinterpreted or +cause unexpected behavior. By enforcing this rule, we ensure that the configuration is more robust and consistent. When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port -lower than 60, because YAML parses numbers in the format `xx:yy` as a base-60 value. For this reason, we recommend -always explicitly specifying your port mappings as strings. +lower than 60, because YAML parses numbers in the format `xx:yy` as a [base-60 value](https://yaml.org/type/float.html). +For this reason, we recommend always explicitly specifying your port mappings as strings. + +Although the expose section in Docker Compose does not suffer from the same YAML parsing vulnerabilities as the ports +section, it is still recommended to enclose exposed ports in quotes. Consistently using quotes across both `ports` and +`expose` sections creates a uniform configuration style, making the file easier to read and maintain. ## Options @@ -85,8 +96,11 @@ If you want to enforce the use of double quotes around port mappings you can do This rule was introduced in Docker-Compose-Linter [1.0.0](https://github.com/zavoloklom/docker-compose-linter/releases). +Handling `expose` section is added in [1.1.0](https://github.com/zavoloklom/docker-compose-linter/releases) + ## References - [Stackoverflow Discussion: Quotes on docker-compose.yml ports](https://stackoverflow.com/questions/58810789/quotes-on-docker-compose-yml-ports-make-any-difference) -- [Compose file reference](https://docker-docs.uclv.cu/compose/compose-file/#ports) +- [Compose File Reference: Ports](https://docker-docs.uclv.cu/compose/compose-file/#ports) +- [Compose File Reference: Expose](https://docs.docker.com/reference/compose-file/services/#expose) - [Awesome Compose Examples](https://github.com/docker/awesome-compose) diff --git a/src/rules/require-quotes-in-ports-rule.ts b/src/rules/require-quotes-in-ports-rule.ts index f31deeb..941a1ee 100644 --- a/src/rules/require-quotes-in-ports-rule.ts +++ b/src/rules/require-quotes-in-ports-rule.ts @@ -1,4 +1,4 @@ -import { parseDocument, isMap, isSeq, isScalar, Scalar, ParsedNode } from 'yaml'; +import { parseDocument, isMap, isSeq, isScalar, Scalar, ParsedNode, Pair } from 'yaml'; import type { LintContext, LintMessage, @@ -8,7 +8,7 @@ import type { LintRuleSeverity, RuleMeta, } from '../linter/linter.types.js'; -import { findLineNumberByValue } from '../util/line-finder.js'; +import { findLineNumberForService } from '../util/line-finder.js'; interface RequireQuotesInPortsRuleOptions { quoteType: 'single' | 'double'; @@ -24,7 +24,7 @@ export default class RequireQuotesInPortsRule implements LintRule { public severity: LintRuleSeverity = 'minor'; public meta: RuleMeta = { - description: 'Ensure that ports are enclosed in quotes in Docker Compose files.', + description: 'Ensure that ports (in `ports` and `expose` sections) are enclosed in quotes in Docker Compose files.', url: 'https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/require-quotes-in-ports-rule.md', }; @@ -32,21 +32,28 @@ export default class RequireQuotesInPortsRule implements LintRule { // eslint-disable-next-line class-methods-use-this public getMessage(): string { - return 'Ports should be enclosed in quotes in Docker Compose files.'; + return 'Ports in `ports` and `expose` sections should be enclosed in quotes.'; } private readonly quoteType: 'single' | 'double'; + private readonly portsSections: string[]; + constructor(options?: RequireQuotesInPortsRuleOptions) { this.quoteType = options?.quoteType || 'single'; + this.portsSections = ['ports', 'expose']; } private getQuoteType(): Scalar.Type { return this.quoteType === 'single' ? 'QUOTE_SINGLE' : 'QUOTE_DOUBLE'; } - // Static method to extract and process ports - private static extractPorts(document: ParsedNode | null, callback: (port: Scalar) => void) { + // Static method to extract and process values + private static extractValues( + document: ParsedNode | null, + section: string, + callback: (service: Pair, port: Scalar) => void, + ) { if (!document || !isMap(document)) return; document.items.forEach((item) => { @@ -56,14 +63,14 @@ export default class RequireQuotesInPortsRule implements LintRule { serviceMap.items.forEach((service) => { if (!isMap(service.value)) return; - const ports = service.value.items.find((node) => isScalar(node.key) && node.key.value === 'ports'); - if (!ports || !isSeq(ports.value)) return; - - ports.value.items.forEach((port) => { - if (isScalar(port)) { - callback(port); - } - }); + const nodes = service.value.items.find((node) => isScalar(node.key) && node.key.value === section); + if (nodes && isSeq(nodes.value)) { + nodes.value.items.forEach((node) => { + if (isScalar(node)) { + callback(service, node); + } + }); + } }); }); } @@ -72,20 +79,28 @@ export default class RequireQuotesInPortsRule implements LintRule { const errors: LintMessage[] = []; const parsedDocument = parseDocument(context.sourceCode); - RequireQuotesInPortsRule.extractPorts(parsedDocument.contents, (port) => { - if (port.type !== this.getQuoteType()) { - errors.push({ - rule: this.name, - type: this.type, - category: this.category, - severity: this.severity, - message: this.getMessage(), - line: findLineNumberByValue(context.sourceCode, String(port.value)), - column: 1, - meta: this.meta, - fixable: this.fixable, - }); - } + this.portsSections.forEach((section) => { + RequireQuotesInPortsRule.extractValues(parsedDocument.contents, section, (service, port) => { + if (port.type !== this.getQuoteType()) { + errors.push({ + rule: this.name, + type: this.type, + category: this.category, + severity: this.severity, + message: this.getMessage(), + line: findLineNumberForService( + parsedDocument, + context.sourceCode, + String(service.key), + section, + String(port.value), + ), + column: 1, + meta: this.meta, + fixable: this.fixable, + }); + } + }); }); return errors; @@ -94,11 +109,13 @@ export default class RequireQuotesInPortsRule implements LintRule { public fix(content: string): string { const parsedDocument = parseDocument(content); - RequireQuotesInPortsRule.extractPorts(parsedDocument.contents, (port) => { - if (port.type !== this.getQuoteType()) { - // eslint-disable-next-line no-param-reassign - port.type = this.getQuoteType(); - } + this.portsSections.forEach((section) => { + RequireQuotesInPortsRule.extractValues(parsedDocument.contents, section, (service, port) => { + if (port.type !== this.getQuoteType()) { + // eslint-disable-next-line no-param-reassign + port.type = this.getQuoteType(); + } + }); }); return parsedDocument.toString(); diff --git a/src/util/line-finder.ts b/src/util/line-finder.ts index 56cbcc7..dd13204 100644 --- a/src/util/line-finder.ts +++ b/src/util/line-finder.ts @@ -32,42 +32,6 @@ function findLineNumberByValue(content: string, value: string): number { return lineIndex === -1 ? 1 : lineIndex + 1; } -/** - * Finds the line number where the key is located in the YAML content for a specific service. - * Searches only within the service block, ensuring boundaries are respected. - * - * @param document The YAML content parsed with lib yaml. - * @param content The parsed YAML content as a string. - * @param serviceName The name of the service in which to search for the key. - * @param key The key to search for in the content. - * @returns number The line number where the key is found, or -1 if not found. - */ -function findLineNumberByKeyForService(document: Document, content: string, serviceName: string, key: string): number { - const services = document.get('services') as Node; - - if (!isMap(services)) { - return 1; - } - - const service = services.get(serviceName) as Node; - - if (!isMap(service)) { - return 1; - } - - let lineNumber = 1; - service.items.forEach((item) => { - const keyNode = item.key; - - if (isScalar(keyNode) && keyNode.value === key && keyNode.range) { - const [start] = keyNode.range; - lineNumber = content.slice(0, start).split('\n').length; - } - }); - - return lineNumber; -} - /** * Refactored helper to get service block line number */ @@ -139,31 +103,31 @@ function findLineNumberForService( } if (isSeq(keyNode)) { + let line = 1; keyNode.items.forEach((item) => { - if (isScalar(item) && item.value === value && item.range) { + if (isScalar(item) && String(item.value) === String(value) && item.range) { const [start] = item.range; - return content.slice(0, start).split('\n').length; + line = content.slice(0, start).split('\n').length; } - - return 1; }); + return line; } if (isMap(keyNode)) { + let line = 1; keyNode.items.forEach((item) => { const keyItem = item.key; const valueItem = item.value; - if (isScalar(keyItem) && isScalar(valueItem) && valueItem.value === value && valueItem.range) { + if (isScalar(keyItem) && isScalar(valueItem) && String(valueItem.value) === String(value) && valueItem.range) { const [start] = valueItem.range; - return content.slice(0, start).split('\n').length; + line = content.slice(0, start).split('\n').length; } - - return 1; }); + return line; } return 1; // Default to 1 if the key or value is not found } -export { findLineNumberByKey, findLineNumberByValue, findLineNumberByKeyForService, findLineNumberForService }; +export { findLineNumberByKey, findLineNumberByValue, findLineNumberForService }; diff --git a/tests/mocks/docker-compose.yml b/tests/mocks/docker-compose.yml index 7811d9d..1de7071 100644 --- a/tests/mocks/docker-compose.yml +++ b/tests/mocks/docker-compose.yml @@ -36,6 +36,9 @@ services: ports: - "11150:3000" - "127.0.0.1:11032:3000" + - 3000 + expose: + - 3000 b-service: build: context: ../../app/b-service diff --git a/tests/rules/require-quotes-in-ports-rule.spec.ts b/tests/rules/require-quotes-in-ports-rule.spec.ts index 490fddc..24f6b18 100644 --- a/tests/rules/require-quotes-in-ports-rule.spec.ts +++ b/tests/rules/require-quotes-in-ports-rule.spec.ts @@ -38,7 +38,10 @@ test('RequireQuotesInPortsRule: should return a warning when ports are not quote const errors = rule.check(context); t.is(errors.length, 1, 'There should be one warning when ports are not quoted.'); - t.is(errors[0].message, 'Ports should be enclosed in quotes in Docker Compose files.'); + t.is( + errors[0].message, + 'Ports in `ports` and `expose` sections should be enclosed in quotes.', + ); t.is(errors[0].rule, 'require-quotes-in-ports'); t.is(errors[0].severity, 'minor'); }); @@ -74,7 +77,10 @@ test('RequireQuotesInPortsRule: should fix unquoted ports by adding single quote const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const fixedYAML = rule.fix(yamlWithoutQuotes); - t.true(fixedYAML.includes(`'8080:80'`), 'The ports should be quoted with single quotes.'); + t.true( + fixedYAML.includes(`'8080:80'`), + 'The Ports in `ports` and `expose` sections should be quoted with single quotes.', + ); t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); @@ -83,7 +89,10 @@ test('RequireQuotesInPortsRule: should fix double quotes ports by changing them const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const fixedYAML = rule.fix(yamlWithSingleQuotes); - t.true(fixedYAML.includes(`'8080:80'`), 'The ports should be quoted with single quotes.'); + t.true( + fixedYAML.includes(`'8080:80'`), + 'The Ports in `ports` and `expose` sections should be quoted with single quotes.', + ); t.false(fixedYAML.includes(`"8080:80"`), 'The ports should not have double quotes.'); }); @@ -92,7 +101,10 @@ test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quote const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const fixedYAML = rule.fix(yamlWithoutQuotes); - t.true(fixedYAML.includes(`"8080:80"`), 'The ports should be quoted with double quotes.'); + t.true( + fixedYAML.includes(`"8080:80"`), + 'The Ports in `ports` and `expose` sections should be quoted with double quotes.', + ); t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); @@ -101,6 +113,9 @@ test('RequireQuotesInPortsRule: should fix single quotes ports by changing them const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const fixedYAML = rule.fix(yamlWithSingleQuotes); - t.true(fixedYAML.includes(`"8080:80"`), 'The ports should be quoted with double quotes.'); + t.true( + fixedYAML.includes(`"8080:80"`), + 'The Ports in `ports` and `expose` sections should be quoted with double quotes.', + ); t.false(fixedYAML.includes(`'8080:80'`), 'The ports should not have single quotes.'); }); From c03a917d6ce0b0dbab8576983e34d2ea038cd681 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Mon, 11 Nov 2024 13:34:55 +0100 Subject: [PATCH 13/16] feat: change building system to rollup, compiling linter into a binary BREAKING CHANGE: - Renamed `bin/dclint.js` to `bin/dclint.cjs`. - Docker entrypoint changed to use the compiled binary over the Node.js implementation. --- .dockerignore | 50 + .eslintrc.cjs | 2 +- .../actions/install-dependencies/action.yml | 27 + .github/workflows/main.yml | 206 +- .gitignore | 8 + .markdownlint.cjs | 11 +- CONTRIBUTING.md | 158 +- Dockerfile | 51 +- Makefile | 50 + README.md | 36 +- SECURITY.md | 9 +- bin/dclint.js | 2 - docs/rules/no-unbound-port-interfaces-rule.md | 2 +- docs/rules/require-quotes-in-ports-rule.md | 2 +- package-lock.json | 3551 ++++++++++++++--- package.json | 27 +- release.config.js | 25 +- rollup.base.config.js | 41 + rollup.config.cli.js | 18 + rollup.config.lib.js | 15 + rollup.config.pkg.js | 13 + scripts/generate-sea.sh | 16 + sea-config.json | 7 + src/cli/cli.ts | 204 +- src/cli/cli.types.ts | 2 +- src/config/config.ts | 10 +- src/formatters/codeclimate.ts | 2 +- src/formatters/compact.ts | 2 +- src/formatters/index.ts | 13 + src/formatters/json.ts | 2 +- src/formatters/junit.ts | 2 +- src/formatters/stylish.ts | 27 +- src/index.ts | 2 +- src/linter/linter.ts | 34 +- src/rules/index.ts | 33 + src/rules/no-build-and-image-rule.ts | 4 +- .../no-duplicate-container-names-rule.ts | 4 +- src/rules/no-duplicate-exported-ports-rule.ts | 6 +- src/rules/no-quotes-in-volumes-rule.ts | 4 +- src/rules/no-unbound-port-interfaces-rule.ts | 6 +- src/rules/no-version-field-rule.ts | 4 +- src/rules/require-project-name-field-rule.ts | 2 +- src/rules/require-quotes-in-ports-rule.ts | 4 +- .../service-container-name-regex-rule.ts | 4 +- ...ce-dependencies-alphabetical-order-rule.ts | 4 +- ...service-image-require-explicit-tag-rule.ts | 4 +- src/rules/service-keys-order-rule.ts | 4 +- .../service-ports-alphabetical-order-rule.ts | 6 +- src/rules/services-alphabetical-order-rule.ts | 4 +- src/rules/top-level-properties-order-rule.ts | 4 +- src/util/compose-validation.ts | 6 +- src/util/files-finder.ts | 4 +- src/util/formatter-loader.ts | 26 +- src/util/load-schema.ts | 11 - src/util/logger.ts | 10 +- src/util/rules-loader.ts | 95 +- src/util/schema-loader.ts | 15 + tests/linter.spec.ts | 24 +- tests/mocks/docker-compose.correct.yml | 63 + tests/rules/no-build-and-image-rule.spec.ts | 4 +- .../no-duplicate-container-names-rule.spec.ts | 4 +- .../no-duplicate-exported-ports-rule.spec.ts | 4 +- tests/rules/no-quotes-in-volumes-rule.spec.ts | 4 +- .../no-unbound-port-interfaces-rule.spec.ts | 4 +- tests/rules/no-version-field-rule.spec.ts | 4 +- .../require-project-name-field-rule.spec.ts | 4 +- .../require-quotes-in-ports-rule.spec.ts | 9 +- .../service-container-name-regex-rule.spec.ts | 4 +- ...pendencies-alphabetical-order-rule.spec.ts | 4 +- ...ce-image-require-explicit-tag-rule.spec.ts | 4 +- tests/rules/service-keys-order-rule.spec.ts | 4 +- ...vice-ports-alphabetical-order-rule.spec.ts | 4 +- .../services-alphabetical-order-rule.spec.ts | 4 +- .../top-level-properties-order-rule.spec.ts | 4 +- tests/util/files-finder.spec.ts | 16 +- tests/util/line-finder.spec.ts | 2 +- tests/util/service-ports-parser.spec.ts | 2 +- tsconfig.json | 4 +- 78 files changed, 4179 insertions(+), 888 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/actions/install-dependencies/action.yml create mode 100644 Makefile delete mode 100755 bin/dclint.js create mode 100644 rollup.base.config.js create mode 100644 rollup.config.cli.js create mode 100644 rollup.config.lib.js create mode 100644 rollup.config.pkg.js create mode 100755 scripts/generate-sea.sh create mode 100644 sea-config.json create mode 100644 src/formatters/index.ts create mode 100644 src/rules/index.ts delete mode 100644 src/util/load-schema.ts create mode 100644 src/util/schema-loader.ts create mode 100644 tests/mocks/docker-compose.correct.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..67c9790 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# OS +.DS_Store + +# Git +.git/ +.gitignore + +# Github +.github/ + +# Docker +Dockerfile + +# Documentation +/documentation +/docs + +# Dependencies +/node_modules + +# Compiled output and runtime +/dist +/bin +/pkg +/sea +/.tsimp +/sea-prep.blob +/dclint + +# Tests +/coverage + +# Other +Makefile diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 802886b..2aba164 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -92,5 +92,5 @@ module.exports = { 'unicorn/switch-case-braces': [2, 'avoid'], 'unicorn/import-style': [2, {"styles": {"node:path": {"named": true, "default": false}}}] }, - 'ignorePatterns': ['node_modules', 'dist', '.tsimp', 'coverage', 'bin'], + 'ignorePatterns': ['node_modules', 'dist', '.tsimp', 'coverage', 'bin', 'rollup*config*js'], }; diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 0000000..c972d77 --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,27 @@ +name: "Install Node.js dependencies with Cache" +description: "Sets up Node.js, caches dependencies, and installs them" +inputs: + node-version: + description: "Node.js version" + required: true + default: "20.18.0" + +runs: + using: "composite" + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Cache Node.js modules + uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci + shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7118c8..c79a5ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,28 +4,22 @@ on: push: branches: - main + - beta pull_request: branches: - '**' jobs: - build: + tests: runs-on: ubuntu-latest - steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v3 + - name: Set up Node.js with Cache and Install + uses: ./.github/actions/install-dependencies with: - node-version: '20.17.0' - - - name: Install dependencies - run: npm ci - - - name: Build the project - run: npm run build + node-version: '20.18.0' - name: Run linter run: npm run lint @@ -39,35 +33,147 @@ jobs: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: wget -qO - https://coverage.codacy.com/get.sh | bash -s -- report -r ./coverage/cobertura-coverage.xml + - name: Build the project + run: npm run build:cli + + - name: Upload tests artifacts + uses: actions/upload-artifact@v4 + with: + name: tests-artifacts + path: | + ./dist + ./bin + retention-days: 1 + + debug: + runs-on: ubuntu-latest +# if: github.event_name == 'pull_request' || github.ref != 'refs/heads/main' + needs: tests + strategy: + matrix: + node-version: [18, 20, 22, 23] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: tests-artifacts + path: ./ + + - name: Set up Node.js with Cache and Install + uses: ./.github/actions/install-dependencies + with: + node-version: '20.18.0' + + - name: Run debug:bin + run: npm run debug:bin + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js with Cache and Install + uses: ./.github/actions/install-dependencies + with: + node-version: '20.18.0' + + - name: Generate new version + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release --dry-run --no-ci + + - name: Build the project + run: | + export VERSION=$(cat .VERSION) + npm run build + - name: Upload build artifacts uses: actions/upload-artifact@v4 - if: github.ref == 'refs/heads/main' with: name: build-artifacts - path: ./dist + path: | + ./dist + ./bin + ./pkg + retention-days: 7 - release: + build_sea: runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' - + needs: + - build + strategy: + matrix: + arch: [amd64, arm64] + os: [alpine, bullseye] steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download build artifacts uses: actions/download-artifact@v4 with: name: build-artifacts - path: ./dist + path: ./ + + - name: Set up QEMU for multi-arch + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/arm64 - - name: Set up Node.js - uses: actions/setup-node@v3 + - name: Build SEA + run: | + docker run --rm --platform linux/${{ matrix.arch }} -v "$PWD":/app -w /app node:20.18.0-${{ matrix.os }} ./scripts/generate-sea.sh ./sea/dclint-${{ matrix.os }}-${{ matrix.arch }} + + - name: Upload build SEA artifacts + uses: actions/upload-artifact@v4 with: - node-version: '20.17.0' + name: build-sea-artifacts-${{ matrix.os }}-${{ matrix.arch }} + path: | + ./sea + retention-days: 7 + + release: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + needs: + - tests + - debug + - build + - build_sea + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ---------- + # Download and Organize Artifacts + # ---------- + + - name: Download Artifacts + uses: actions/download-artifact@v4 + + - name: Organize downloaded artifacts + run: | + mv build-artifacts/* . + + mkdir -p ./sea + for dir in build-sea-artifacts-*; do + mv "$dir/"* ./sea + done + + # ---------- + # Create npm release, tag, github release + # ---------- - - name: Install dependencies - run: npm ci + - name: Set up Node.js with Cache and Install + uses: ./.github/actions/install-dependencies + with: + node-version: '20.18.0' - name: Run semantic-release env: @@ -83,22 +189,11 @@ jobs: ./package.json ./package-lock.json ./CHANGELOG.md + retention-days: 1 - docker: - runs-on: ubuntu-latest - needs: release - if: github.ref == 'refs/heads/main' - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Download release artifacts - uses: actions/download-artifact@v4 - with: - name: release-artifacts - path: ./ - overwrite: true + # ---------- + # Publishing Docker images + # ---------- - name: Get build arguments id: vars @@ -111,18 +206,38 @@ jobs: echo "BUILD_REVISION=$BUILD_REVISION" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: install: true - name: Log in to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Docker image - uses: docker/build-push-action@v5 + # Build and push the Alpine version + - name: Build and push Alpine version + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/dclint:alpine + ${{ secrets.DOCKERHUB_USERNAME }}/dclint:latest-alpine + ${{ secrets.DOCKERHUB_USERNAME }}/dclint:${{ env.BUILD_VERSION }}-alpine + build-args: | + BUILD_DATE=${{ env.BUILD_DATE }} + BUILD_VERSION=${{ env.BUILD_VERSION }} + BUILD_REVISION=${{ env.BUILD_REVISION }} + target: alpine-version + cache-from: type=gha + cache-to: type=gha,mode=max + + # Build and push the Scratch version + - name: Build and push Scratch version + uses: docker/build-push-action@v6 with: context: . push: true @@ -134,3 +249,6 @@ jobs: BUILD_DATE=${{ env.BUILD_DATE }} BUILD_VERSION=${{ env.BUILD_VERSION }} BUILD_REVISION=${{ env.BUILD_REVISION }} + target: scratch-version + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 04e47b7..2bf0781 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,14 @@ # Build /dist +/bin +/pkg # Runtime /.tsimp + +# Generated files +/sea-prep.blob +/sea +/.VERSION +/*-artifacts* diff --git a/.markdownlint.cjs b/.markdownlint.cjs index 4642f0c..caf4955 100644 --- a/.markdownlint.cjs +++ b/.markdownlint.cjs @@ -1,12 +1,19 @@ module.exports = { 'default': true, + 'no-hard-tabs': false, + 'whitespace': false, + 'MD003': { + 'style': 'atx', + }, 'MD004': { 'style': 'dash', }, + 'MD007': { + 'indent': 2, + }, 'MD013': { 'line_length': 120, - 'ignore_code_blocks': true, - 'ignore_urls': true, + 'code_blocks': false, 'tables': false, }, 'MD024': { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 459ce9c..5524885 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,160 @@ fixable, this method can return the content unchanged. 2. Create a markdown file describing your new rule (for example `new-check-rule.md`) based on [template](./docs/rules/__TEMPLATE__.md) +## How to Build the Project + +The project has several Rollup configurations designed for specific build outputs. You can use the provided npm scripts +to build each configuration individually or all at once. + +1. **CLI Build (`rollup.config.cli.js`)** + + This configuration builds the CLI, producing a minified CommonJS file (`bin/dclint.cjs`) for command-line use. The + minification keeps the output compact for efficient distribution. + + **How to Run:** + + ```shell + npm run build:cli + ``` + +2. **PKG Build for SEA (`rollup.config.pkg.js`)** + + This configuration bundles the entire project, including dependencies, into a single file (`pkg/dclint.cjs`). It is + useful for creating a Single Executable Application (SEA) with Node.js, as all dependencies are embedded in the + output. + + **How to Run:** + + ```shell + npm run build:pkg + ``` + +3. **Library Build (`rollup.config.lib.js`)** + + This configuration generates the main library with outputs in both CommonJS and ESM formats, along with TypeScript + declaration files. This is ideal for distributing the library to be used in various module systems. + + **How to Run:** + + ```shell + npm run build:lib + ``` + +Each configuration has its specific purpose, helping you generate optimized builds for different parts of the project. +Use the `pkg` build when you need to create a single executable, the `cli` build for compact CLI usage, and the `lib` +build for general library distribution. To run all builds at once, use: + +```shell +npm run build +``` + +## How to Build SEA + +Single Executable Applications (SEA) allow you to bundle your Node.js application into a single executable file. This +approach is especially useful for distributing CLI tools, as it removes the need for users to install Node.js or other +dependencies. In this project, the `pkg` build configuration is designed specifically for SEA, bundling all dependencies +into a single file. + +### MacOS + +The following commands are specific to macOS for building a SEA. + +```shell +# Create package build +npm run build:pkg + +# Clean previous build artifacts +rm -rf dclint sea-prep.blob + +# Generate SEA Blob using the Node.js SEA configuration file +node --experimental-sea-config sea-config.json + +# Copy the Node.js binary to create the executable +cp $(command -v node) dclint + +# Remove signature to run on macOS +codesign --remove-signature dclint + +# Inject SEA Blob into the executable using postject +sudo npx postject dclint NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA + +# Sign the executable to allow it to run on macOS +codesign --sign - dclint +``` + +### Linux + +```shell +# Create package build +npm run build:pkg + +# Clean previous build artifacts +rm -rf dclint sea-prep.blob + +# Generate SEA Blob using the Node.js SEA configuration file +node --experimental-sea-config sea-config.json + +# Copy the Node.js binary to create the executable +cp $(command -v node) dclint + +# Inject SEA Blob into the executable using postject +npx postject dclint NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA +``` + +Also, you can use Docker to compile it: + +```bash +docker run --rm -v "$PWD":/app -w /app node:20.18.0-alpine ./scripts/generate-sea.sh ./pkg/dclint-alpine +docker run --rm -v "$PWD":/app -w /app node:20.18.0-bullseye ./scripts/generate-sea.sh ./pkg/dclint-bullseye +``` + +After running these commands, you will have a standalone `dclint` executable, ready for distribution and use. This SEA +version simplifies deployment, allowing users to run the CLI tool without any external dependencies. + +To verify that everything is working correctly, run the following command: + +```shell +./dclint ./tests/mocks/docker-compose.yml -c ./tests/mocks/.dclintrc +./dclint -v +``` + +To suppress the experimental feature warning: + +```text +(node:99747) ExperimentalWarning: Single executable application is an experimental feature and might change at any time +(Use `dclint --trace-warnings ...` to show where the warning was created) +``` + +set the environment variable `NODE_NO_WARNINGS=1`: + +```bash +NODE_NO_WARNINGS=1 ./dclint ./tests/mocks/docker-compose.yml -c ./tests/mocks/.dclintrc +``` + +Note that this SEA still need some dependencies to run: + +```text +For Ubuntu +ldd /bin/dclint + linux-vdso.so.1 + libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 + libstdc++.so.6 => /usr/lib/aarch64-linux-gnu/libstdc++.so.6 + libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 + libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 + libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 + libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 + /lib/ld-linux-aarch64.so.1 +``` + +```text +For Alpine +ldd /bin/dclint + /lib/ld-musl-aarch64.so.1 + libstdc++.so.6 => /usr/lib/libstdc++.so.6 + libc.musl-aarch64.so.1 => /lib/ld-musl-aarch64.so.1 + libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 +``` + ## Build Docker File Locally ```shell @@ -98,8 +252,8 @@ You can do this by running the linter on a Docker Compose file: npm run debug ``` -If no errors occur, and the linter correctly identifies new fields or deprecated ones, then the schema has been updated -successfully. +If no errors occur during execution, and the linter correctly identifies all errors and warnings, then the schema has +been updated successfully. After updating the schema, it's also important to run the project's unit tests to confirm that nothing was broken by the schema update: diff --git a/Dockerfile b/Dockerfile index 48d8cfe..d0ad253 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.17.0-alpine3.19 +FROM node:20.18.0-alpine3.19 AS builder WORKDIR /dclint @@ -6,11 +6,56 @@ COPY package*.json ./ RUN npm ci COPY . . -RUN npm run build + +# SEA Builder +RUN npm run build:pkg && ./scripts/generate-sea.sh /bin/dclint + +FROM alpine:3.19 AS alpine-version + +ENV NODE_NO_WARNINGS=1 + +RUN apk update && apk upgrade && \ + apk add --no-cache \ + libstdc++=~13.2 \ + && rm -rf /tmp/* /var/cache/apk/* + +COPY --from=builder /bin/dclint /bin/dclint + +WORKDIR /app + +ENTRYPOINT ["/bin/dclint"] + +ARG BUILD_DATE="0000-00-00T00:00:00+0000" +ARG BUILD_VERSION="0.0.0" +ARG BUILD_REVISION="0000000" + +LABEL \ + org.opencontainers.image.title="Docker Compose Linter (Alpine)" \ + org.opencontainers.image.description="A command-line tool for validating and enforcing best practices in Docker Compose files." \ + org.opencontainers.image.created="${BUILD_DATE}" \ + org.opencontainers.image.authors="Sergey Kupletsky " \ + org.opencontainers.image.url="https://github.com/zavoloklom/docker-compose-linter" \ + org.opencontainers.image.documentation="https://github.com/zavoloklom/docker-compose-linter" \ + org.opencontainers.image.source="https://github.com/zavoloklom/docker-compose-linter.git" \ + org.opencontainers.image.version="${BUILD_VERSION}" \ + org.opencontainers.image.revision="${BUILD_REVISION}" \ + org.opencontainers.image.vendor="Sergey Kupletsky" \ + org.opencontainers.image.licenses="MIT" + +FROM scratch AS scratch-version + +ENV NODE_NO_WARNINGS=1 + +COPY --from=builder "/usr/lib/libstdc++.so.6" "/usr/lib/libstdc++.so.6" +COPY --from=builder /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=builder /lib/ld-musl-aarch64.so.1 /lib/ld-musl-aarch64.so.1 +COPY --from=builder /lib/libc.musl-aarch64.so.1 /lib/libc.musl-aarch64.so.1 + +COPY --from=builder /bin/dclint /bin/dclint WORKDIR /app -ENTRYPOINT ["node", "--no-warnings", "/dclint/bin/dclint.js"] +ENTRYPOINT ["/bin/dclint"] ARG BUILD_DATE="0000-00-00T00:00:00+0000" ARG BUILD_VERSION="0.0.0" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..46a1c0f --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +.DEFAULT_GOAL := help +.PHONY: help docker dev dev-sync check-version + +%: + @true + +help: ## Show this help. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +###### +# CONFIGURATION +###### + +MSYS_NO_PATHCONV:=1 # Fix for Windows +ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +IMAGE_NAME=zavoloklom/dclint +IMAGE_TAG=dev + +BUILD_VERSION=$(shell awk -F\" '/"version":/ {print $$4}' package.json) +BUILD_DATE=$(shell date +%Y-%m-%dT%T%z) +BUILD_REVISION=$(shell git rev-parse --short HEAD) + +###### +# MAIN SCRIPTS +###### + +docker: ## Build docker image. + docker build --file Dockerfile . --tag ${IMAGE_NAME}:${IMAGE_TAG} \ + --pull \ + --build-arg BUILD_DATE=${BUILD_DATE} \ + --build-arg BUILD_VERSION=${BUILD_VERSION} \ + --build-arg BUILD_REVISION=${BUILD_REVISION} + +dev: docker ## Start development inside container. Note: node_modules, bin, pkg, dist and sea are not synced. + docker run --rm -it --ipc=host \ + -v ${PWD}:/app \ + --mount type=volume,dst=/var/www/app/node_modules \ + --mount type=volume,dst=/var/www/app/dist \ + --mount type=volume,dst=/var/www/app/bin \ + --mount type=volume,dst=/var/www/app/pkg \ + --mount type=volume,dst=/var/www/app/sea \ + --entrypoint /bin/sh \ + ${IMAGE_NAME}:${IMAGE_TAG} + +dev-sync: docker ## Start development inside container. Note: all files are synced. + docker run --rm -it -v ${PWD}:/app --entrypoint /bin/sh ${IMAGE_NAME}:${IMAGE_TAG} + +check-version: docker ## Check version in docker container. Note: all files are not synced. + docker run --rm -it ${IMAGE_NAME}:${IMAGE_TAG} -v diff --git a/README.md b/README.md index 2772306..e9e2b49 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,40 @@ docker run -t --rm -v ${PWD}:/app zavoloklom/dclint -h For more information about available options and formatters, please refer to the [CLI Reference](./docs/cli.md) and [Formatters Reference](./docs/formatters.md). +## Usage as a Library + +The `dclint` library can be integrated directly into your code, allowing you to run linting checks programmatically and +format the results as desired. Below are examples of how to use `dclint` as a library in both CommonJS and ES module +formats. + +### Example with CommonJS + +```javascript +const { DCLinter } = require('dclint'); + +(async () => { + const linter = new DCLinter(); + + const lintResults = await linter.lintFiles(['.'], true); + const formattedResults = await linter.formatResults(lintResults, 'stylish'); + + console.log(formattedResults); +})(); +``` + +### Example with ES Module + +```javascript +import { DCLinter } from 'dclint'; + +const linter = new DCLinter(); + +const lintResults = await linter.lintFiles(['.'], true); +const formattedResults = await linter.formatResults(lintResults, 'stylish'); + +console.log(formattedResults); +``` + ## Rules and Errors Docker Compose Linter includes set of rules to ensure your Docker Compose files adhere to best practices. Detailed @@ -250,7 +284,7 @@ lint-docker-compose: name: zavoloklom/dclint entrypoint: [ "" ] script: - - node --no-warnings /dclint/bin/dclint.js . -r -f codeclimate -o gl-codequality.json + - /bin/dclint . -r -f codeclimate -o gl-codequality.json artifacts: reports: codequality: gl-codequality.json diff --git a/SECURITY.md b/SECURITY.md index 6a4e1ea..45c4550 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,11 @@ We actively maintain and support the following versions of the project: -| Version | Supported | -| --------- | ------------------ | -| `1.x.x` | :white_check_mark: | -| `< 1.0.0` | :x: | +| Version | Supported | End of Support | +| --------- | ------------------ | -------------- | +| `2.x.x` | :white_check_mark: | - | +| `1.x.x` | :x: | 01.12.2024 | +| `< 1.0.0` | :x: | 15.09.2024 | Please make sure to update to the latest version to ensure you're using the most secure version of our software. diff --git a/bin/dclint.js b/bin/dclint.js deleted file mode 100755 index 221b6e0..0000000 --- a/bin/dclint.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import cli from '../dist/cli/cli.js'; diff --git a/docs/rules/no-unbound-port-interfaces-rule.md b/docs/rules/no-unbound-port-interfaces-rule.md index f570d7b..3dd5804 100644 --- a/docs/rules/no-unbound-port-interfaces-rule.md +++ b/docs/rules/no-unbound-port-interfaces-rule.md @@ -85,7 +85,7 @@ skip IP interface checks in such cases. ## Version -This rule was introduced in Docker-Compose-Linter [1.1.0](https://github.com/zavoloklom/docker-compose-linter/releases). +This rule was introduced in Docker-Compose-Linter [2.0.0](https://github.com/zavoloklom/docker-compose-linter/releases). ## References diff --git a/docs/rules/require-quotes-in-ports-rule.md b/docs/rules/require-quotes-in-ports-rule.md index 737b3ab..aca66df 100644 --- a/docs/rules/require-quotes-in-ports-rule.md +++ b/docs/rules/require-quotes-in-ports-rule.md @@ -96,7 +96,7 @@ If you want to enforce the use of double quotes around port mappings you can do This rule was introduced in Docker-Compose-Linter [1.0.0](https://github.com/zavoloklom/docker-compose-linter/releases). -Handling `expose` section is added in [1.1.0](https://github.com/zavoloklom/docker-compose-linter/releases) +Handling `expose` section is added in [2.0.0](https://github.com/zavoloklom/docker-compose-linter/releases) ## References diff --git a/package-lock.json b/package-lock.json index 36d52a6..8fa6d18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,23 @@ "license": "MIT", "dependencies": { "ajv": "^8.17.1", - "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", + "picocolors": "^1.1.1", "yaml": "^2.6.0", "yargs": "^17.7.2" }, "bin": { - "dclint": "bin/dclint.js", - "docker-compose-linter": "bin/dclint.js" + "dclint": "bin/dclint.cjs" }, "devDependencies": { + "@babel/preset-env": "7.26.0", + "@rollup/plugin-babel": "6.0.4", + "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-node-resolve": "15.3.0", + "@rollup/plugin-replace": "6.0.1", + "@rollup/plugin-terser": "0.4.4", + "@rollup/plugin-typescript": "12.1.1", "@semantic-release/changelog": "6.0.3", "@semantic-release/commit-analyzer": "13.0.0", "@semantic-release/exec": "6.0.3", @@ -61,731 +68,2906 @@ "url": "https://github.com/zavoloklom#how-to-support" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, "engines": { - "node": ">=0.8.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", + "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", + "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/types": "^7.25.9" }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", "dev": true, + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/helper-simple-access": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", + "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dev": true, - "engines": { - "node": ">=12.22" + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@isaacs/cached": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/cached/-/cached-1.0.1.tgz", - "integrity": "sha512-7kGcJ9Hc1f4qpTApWz3swxbF9Qv1NF/GxuPtXeTptbsgvJIoufSd0h854Nq/2bw80F5C1onsFgEI05l+q0e4vw==", + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, - "dependencies": { - "@isaacs/catcher": "^1.0.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@isaacs/catcher": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@isaacs/catcher/-/catcher-1.0.4.tgz", - "integrity": "sha512-g2klMwbnguClWNnCeQ1zYaDJsvPbIbnjdJPDE0z09MqoejJDZSLK5vIKiClq2Bkg5ubuI8vaN6wfIUi5GYzMVA==", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", "dev": true, + "license": "MIT", "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" + "@babel/helper-plugin-utils": "^7.25.9" }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "4" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "6", - "debug": "4" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "engines": { - "node": ">= 6" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", "dev": true, + "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=12.4.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/auth-token": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", - "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@octokit/core": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", - "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.0.0", - "@octokit/request": "^9.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/graphql": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", - "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/request": "^9.0.0", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.0" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", + "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", - "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^13.5.0" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" }, "peerDependencies": { - "@octokit/core": ">=6" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/plugin-retry": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.1.1.tgz", - "integrity": "sha512-G9Ue+x2odcb8E1XIPhaFBnTTIrrUDfXN05iFXiqhR+SeeeDMMILcAnysOsxUpEWcQp2e5Ft397FCXTcPkiPkLw==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/request-error": "^6.0.0", - "@octokit/types": "^13.0.0", - "bottleneck": "^2.15.3" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" }, "peerDependencies": { - "@octokit/core": ">=6" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/plugin-throttling": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-9.3.1.tgz", - "integrity": "sha512-Qd91H4liUBhwLB2h6jZ99bsxoQdhgPk6TdwnClPyTBSDAdviGPceViEgUwj+pcQDmB/rfAXAXK7MTochpHM3yQ==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^13.0.0", - "bottleneck": "^2.15.3" + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" }, "peerDependencies": { - "@octokit/core": "^6.0.0" + "@babel/core": "^7.12.0" } }, - "node_modules/@octokit/request": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", - "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^10.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^7.0.2" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/request-error": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", - "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^13.0.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" }, "engines": { - "node": ">= 18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", "dev": true, - "optional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=14" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/unts" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, "engines": { - "node": ">=12.22.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": "4.2.10" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=12.22.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", + "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@pnpm/npm-conf": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", "dev": true, + "license": "MIT", "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", + "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-simple-access": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", + "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", + "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.25.9", + "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/cached": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/cached/-/cached-1.0.1.tgz", + "integrity": "sha512-7kGcJ9Hc1f4qpTApWz3swxbF9Qv1NF/GxuPtXeTptbsgvJIoufSd0h854Nq/2bw80F5C1onsFgEI05l+q0e4vw==", + "dev": true, + "dependencies": { + "@isaacs/catcher": "^1.0.0" + } + }, + "node_modules/@isaacs/catcher": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@isaacs/catcher/-/catcher-1.0.4.tgz", + "integrity": "sha512-g2klMwbnguClWNnCeQ1zYaDJsvPbIbnjdJPDE0z09MqoejJDZSLK5vIKiClq2Bkg5ubuI8vaN6wfIUi5GYzMVA==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "dev": true, + "dependencies": { + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", + "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.1.1.tgz", + "integrity": "sha512-G9Ue+x2odcb8E1XIPhaFBnTTIrrUDfXN05iFXiqhR+SeeeDMMILcAnysOsxUpEWcQp2e5Ft397FCXTcPkiPkLw==", + "dev": true, + "dependencies": { + "@octokit/request-error": "^6.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-9.3.1.tgz", + "integrity": "sha512-Qd91H4liUBhwLB2h6jZ99bsxoQdhgPk6TdwnClPyTBSDAdviGPceViEgUwj+pcQDmB/rfAXAXK7MTochpHM3yQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^6.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", + "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", + "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-babel/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", + "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript/node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", + "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", + "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", + "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", + "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", + "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", + "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", + "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", + "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", + "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", + "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", + "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", + "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", + "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", + "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", + "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", + "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", + "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", + "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", + "cpu": [ + "x64" + ], "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -1268,6 +3450,13 @@ "eslint": ">=8.40.0" } }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1295,6 +3484,13 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -2137,6 +4333,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2192,9 +4440,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -2210,11 +4458,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -2223,6 +4472,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -2300,9 +4556,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001660", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", - "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true, "funding": [ { @@ -2317,7 +4573,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/cbor": { "version": "9.0.2", @@ -2335,6 +4592,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -2730,6 +4988,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -2737,7 +4996,8 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -2748,12 +5008,26 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -3097,6 +5371,16 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3204,10 +5488,11 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.23", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.23.tgz", - "integrity": "sha512-mBhODedOXg4v5QWwl21DjM5amzjmI1zw9EPrPK/5Wx7C8jt33bpZNrC7OhHUG3pxRtbLpr3W2dXT+Ph1SsfRZA==", - "dev": true + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "dev": true, + "license": "ISC" }, "node_modules/emittery": { "version": "1.0.3", @@ -4416,6 +6701,21 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -4616,6 +6916,22 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4735,6 +7051,17 @@ "node": ">=8" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5485,6 +7812,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -5566,6 +7900,16 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -5793,7 +8137,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5957,6 +8302,13 @@ "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -5993,6 +8345,16 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -9464,9 +11826,10 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.2", @@ -9745,6 +12108,16 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -9999,6 +12372,43 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -10026,6 +12436,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core/node_modules/regjsparser": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", + "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, "node_modules/registry-auth-token": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", @@ -10038,6 +12479,13 @@ "node": ">=14" } }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/regjsparser": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", @@ -10148,6 +12596,46 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", + "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.25.0", + "@rollup/rollup-android-arm64": "4.25.0", + "@rollup/rollup-darwin-arm64": "4.25.0", + "@rollup/rollup-darwin-x64": "4.25.0", + "@rollup/rollup-freebsd-arm64": "4.25.0", + "@rollup/rollup-freebsd-x64": "4.25.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", + "@rollup/rollup-linux-arm-musleabihf": "4.25.0", + "@rollup/rollup-linux-arm64-gnu": "4.25.0", + "@rollup/rollup-linux-arm64-musl": "4.25.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", + "@rollup/rollup-linux-riscv64-gnu": "4.25.0", + "@rollup/rollup-linux-s390x-gnu": "4.25.0", + "@rollup/rollup-linux-x64-gnu": "4.25.0", + "@rollup/rollup-linux-x64-musl": "4.25.0", + "@rollup/rollup-win32-arm64-msvc": "4.25.0", + "@rollup/rollup-win32-ia32-msvc": "4.25.0", + "@rollup/rollup-win32-x64-msvc": "4.25.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10553,6 +13041,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -10758,6 +13256,13 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, "node_modules/sock-daemon": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/sock-daemon/-/sock-daemon-1.4.2.tgz", @@ -10843,6 +13348,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/spawn-error-forwarder": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", @@ -11387,6 +13903,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -11812,6 +14347,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11860,6 +14396,16 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unicode-emoji-modifier-base": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", @@ -11869,6 +14415,40 @@ "node": ">=4" } }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unicorn-magic": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", @@ -11912,9 +14492,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -11930,9 +14510,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index 265be0d..82b0919 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "keywords": [ "docker", "docker-compose", + "compose", "linter", "lint", "best practices" @@ -20,11 +21,11 @@ "license": "MIT", "author": "Sergey Kupletsky (https://github.com/zavoloklom)", "type": "module", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/index.cjs", + "module": "dist/index.esm.js", + "types": "dist/types/src/index.d.ts", "bin": { - "dclint": "./bin/dclint.js", - "docker-compose-linter": "./bin/dclint.js" + "dclint": "./bin/dclint.cjs" }, "directories": { "test": "tests" @@ -35,9 +36,12 @@ "schemas" ], "scripts": { - "prebuild": "rimraf dist", - "build": "tsc", + "build": "npm run build:lib & npm run build:cli & npm run build:pkg", + "build:cli": "rimraf bin && rollup -c rollup.config.cli.js", + "build:lib": "rimraf dist && rollup -c rollup.config.lib.js", + "build:pkg": "rimraf pkg && rollup -c rollup.config.pkg.js", "debug": "tsc && node --import=tsimp/import --no-warnings --inspect ./src/cli/cli.ts ./tests/mocks/docker-compose.yml -c ./tests/mocks/.dclintrc", + "debug:bin": "node ./bin/dclint.cjs ./tests/mocks/docker-compose.correct.yml --fix", "eslint": "eslint .", "eslint:fix": "eslint . --fix", "lint": "npm run eslint && npm run markdownlint", @@ -47,16 +51,25 @@ "prettier": "prettier --write \"**/*.md\"", "test": "ava --verbose", "test:coverage": "rimraf coverage && mkdir -p coverage && c8 ava --tap | tap-xunit --package='dclint' > ./coverage/junit.xml", + "tsc": "tsc", "update-compose-schema": "node ./scripts/download-compose-schema.cjs" }, "dependencies": { "ajv": "^8.17.1", - "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", + "picocolors": "^1.1.1", "yaml": "^2.6.0", "yargs": "^17.7.2" }, "devDependencies": { + "@babel/preset-env": "7.26.0", + "@rollup/plugin-babel": "6.0.4", + "@rollup/plugin-commonjs": "28.0.1", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-node-resolve": "15.3.0", + "@rollup/plugin-replace": "6.0.1", + "@rollup/plugin-terser": "0.4.4", + "@rollup/plugin-typescript": "12.1.1", "@semantic-release/changelog": "6.0.3", "@semantic-release/commit-analyzer": "13.0.0", "@semantic-release/exec": "6.0.3", diff --git a/release.config.js b/release.config.js index 8ceae62..8f79347 100644 --- a/release.config.js +++ b/release.config.js @@ -1,5 +1,5 @@ export default { - branches: ['main'], + branches: ['main', { name: 'beta', prerelease: true, channel: 'beta' }], plugins: [ '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', @@ -17,6 +17,13 @@ export default { prepareCmd: 'npm run markdownlint:fix-changelog || true', }, ], + [ + '@semantic-release/exec', + { + // eslint-disable-next-line no-template-curly-in-string + verifyReleaseCmd: 'echo ${nextRelease.version} > .VERSION', + }, + ], '@semantic-release/npm', [ '@semantic-release/git', @@ -38,6 +45,22 @@ export default { path: 'CHANGELOG.md', label: 'Changelog', }, + { + path: 'sea/dclint-alpine-amd64', + label: 'DClint Alpine Linux Binary (amd64)', + }, + { + path: 'sea/dclint-bullseye-amd64', + label: 'DClint Bullseye Linux Binary (amd64)', + }, + { + path: 'sea/dclint-alpine-arm64', + label: 'DClint Alpine Linux Binary (arm64)', + }, + { + path: 'sea/dclint-bullseye-arm64', + label: 'DClint Bullseye Linux Binary (arm64)', + }, ], }, ], diff --git a/rollup.base.config.js b/rollup.base.config.js new file mode 100644 index 0000000..1b92629 --- /dev/null +++ b/rollup.base.config.js @@ -0,0 +1,41 @@ +import fs from 'node:fs'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import typescript from '@rollup/plugin-typescript'; +import { babel } from '@rollup/plugin-babel'; +import replace from '@rollup/plugin-replace'; +import terser from '@rollup/plugin-terser'; + +const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +const version = process.env.VERSION ?? packageJson?.version; + +export default (outDir, declaration = false, minify = false) => { + const plugins = [ + json(), + commonjs(), + nodeResolve({ preferBuiltins: true }), + typescript({ + tsconfig: './tsconfig.json', + outDir, + declaration, + declarationDir: declaration ? `${outDir}/types` : null, + include: ['src/**/*.ts', 'schemas/*.json'], + }), + babel({ + babelHelpers: 'bundled', + presets: ['@babel/preset-env'], + exclude: 'node_modules/**', + }), + replace({ + preventAssignment: true, + 'process.env.VERSION': JSON.stringify(version), + }), + ]; + + if (minify) { + plugins.push(terser()); + } + + return { plugins }; +}; diff --git a/rollup.config.cli.js b/rollup.config.cli.js new file mode 100644 index 0000000..45df5de --- /dev/null +++ b/rollup.config.cli.js @@ -0,0 +1,18 @@ +import baseConfig from './rollup.base.config.js'; +import fs from 'node:fs'; + +const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +const dependencies = Object.keys(packageJson.dependencies || {}); + +export default { + ...baseConfig('bin', false, true), + input: 'src/cli/cli.ts', + output: { + file: 'bin/dclint.cjs', + format: 'cjs', + inlineDynamicImports: true, + exports: 'auto', + }, + context: 'globalThis', + external: dependencies, +}; diff --git a/rollup.config.lib.js b/rollup.config.lib.js new file mode 100644 index 0000000..739b81b --- /dev/null +++ b/rollup.config.lib.js @@ -0,0 +1,15 @@ +import baseConfig from './rollup.base.config.js'; +import fs from 'node:fs'; + +const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +const dependencies = Object.keys(packageJson.dependencies || {}); + +export default { + ...baseConfig('dist', true, true), + input: 'src/index.ts', + output: [ + { dir: 'dist', format: 'cjs', entryFileNames: '[name].cjs', exports: 'auto' }, + { dir: 'dist', format: 'esm', entryFileNames: '[name].esm.js', inlineDynamicImports: true }, + ], + external: dependencies, +}; diff --git a/rollup.config.pkg.js b/rollup.config.pkg.js new file mode 100644 index 0000000..fb9dabe --- /dev/null +++ b/rollup.config.pkg.js @@ -0,0 +1,13 @@ +import baseConfig from './rollup.base.config.js'; + +export default { + ...baseConfig('pkg', false, false), + input: 'src/cli/cli.ts', + output: { + file: 'pkg/dclint.cjs', + format: 'cjs', + inlineDynamicImports: true, + exports: 'auto', + }, + context: 'globalThis', +}; diff --git a/scripts/generate-sea.sh b/scripts/generate-sea.sh new file mode 100755 index 0000000..eeb35b9 --- /dev/null +++ b/scripts/generate-sea.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Проверка, что путь к файлу генерации передан как аргумент +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +GENERATION_PATH="$1" + +# Выполнение команд +rm -rf "$GENERATION_PATH" && rm -rf sea-prep.blob && \ +mkdir -p "$(dirname "$GENERATION_PATH")" && \ +node --experimental-sea-config sea-config.json && \ +cp "$(command -v node)" "$GENERATION_PATH" && \ +npx -y postject "$GENERATION_PATH" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 diff --git a/sea-config.json b/sea-config.json new file mode 100644 index 0000000..10b7560 --- /dev/null +++ b/sea-config.json @@ -0,0 +1,7 @@ +{ + "main": "./pkg/dclint.cjs", + "output": "sea-prep.blob", + "files": [ + "./pkg/dclint.cjs" + ] +} \ No newline at end of file diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 99925e2..aefebbd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,97 +1,90 @@ #!/usr/bin/env node -import { readFileSync, writeFileSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { writeFileSync } from 'node:fs'; import yargsLib from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { loadConfig } from '../config/config.js'; -import { DCLinter } from '../linter/linter.js'; -import type { CLIConfig } from './cli.types.js'; -import { Logger, LOG_SOURCE } from '../util/logger.js'; -import { loadFormatter } from '../util/formatter-loader.js'; - -const packageJson = JSON.parse( - readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf8'), -) as Record; - -const { argv } = yargsLib(hideBin(process.argv)) - .usage('Usage: $0 [options]') - .version((packageJson?.version as string) || 'unknown') - .command('$0 ', 'Check the files', (yargs) => { - yargs.positional('files', { - describe: 'Files to check', +import { loadConfig } from '../config/config'; +import { DCLinter } from '../linter/linter'; +import type { CLIConfig } from './cli.types'; +import { Logger, LOG_SOURCE } from '../util/logger'; + +async function main() { + process.env.NODE_NO_WARNINGS = '1'; + const { argv } = yargsLib(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .version(process.env.VERSION ?? 'unknown') + .command('$0 ', 'Check the files', (yargs) => { + yargs.positional('files', { + describe: 'Files to check', + type: 'string', + array: true, + demandOption: true, + }); + }) + .option('recursive', { + alias: 'r', + type: 'boolean', + description: 'Recursively search directories for Docker Compose files', + default: false, + }) + .option('fix', { + type: 'boolean', + description: 'Automatically fix problems', + default: false, + }) + .option('fix-dry-run', { + type: 'boolean', + description: 'Automatically fix problems without saving the changes to the file system', + default: false, + }) + .option('formatter', { + alias: 'f', type: 'string', - array: true, - demandOption: true, - }); - }) - .option('recursive', { - alias: 'r', - type: 'boolean', - description: 'Recursively search directories for Docker Compose files', - default: false, - }) - .option('fix', { - type: 'boolean', - description: 'Automatically fix problems', - default: false, - }) - .option('fix-dry-run', { - type: 'boolean', - description: 'Automatically fix problems without saving the changes to the file system', - default: false, - }) - .option('formatter', { - alias: 'f', - type: 'string', - description: 'Use a specific output format - default: stylish', - default: 'stylish', - }) - .option('config', { - alias: 'c', - type: 'string', - description: 'Path to config file', - }) - .option('quiet', { - alias: 'q', - type: 'boolean', - description: 'Report errors only', - default: false, - }) - .option('output-file', { - alias: 'o', - type: 'string', - description: 'Specify file to write report to', - }) - .option('color', { - type: 'boolean', - description: 'Force enabling/disabling of color', - default: true, - }) - .option('debug', { - type: 'boolean', - description: 'Output debugging information', - default: undefined, - }) - .option('exclude', { - alias: 'e', - type: 'array', - description: 'Files or directories to exclude from the search', - default: [], - }) - .option('max-warnings', { - type: 'number', - description: 'Number of warnings to trigger nonzero exit code', - default: -1, - }) - .help() - .alias('version', 'v'); - -export default async function cli() { - const cliArguments = (await argv) as unknown as CLIConfig; - - // Initialize the logger with the final debug and color options + description: 'Use a specific output format - default: stylish', + default: 'stylish', + }) + .option('config', { + alias: 'c', + type: 'string', + description: 'Path to config file', + }) + .option('quiet', { + alias: 'q', + type: 'boolean', + description: 'Report errors only', + default: false, + }) + .option('output-file', { + alias: 'o', + type: 'string', + description: 'Specify file to write report to', + }) + .option('color', { + type: 'boolean', + description: 'Force enabling/disabling of color', + default: true, + }) + .option('debug', { + type: 'boolean', + description: 'Output debugging information', + default: false, + }) + .option('exclude', { + alias: 'e', + type: 'array', + description: 'Files or directories to exclude from the search', + default: [], + }) + .option('max-warnings', { + type: 'number', + description: 'Number of warnings to trigger nonzero exit code', + default: -1, + }) + .help() + .alias('version', 'v'); + + const cliArguments = argv as unknown as CLIConfig; + Logger.init(cliArguments.debug); const logger = Logger.getInstance(); @@ -102,31 +95,21 @@ export default async function cli() { process.exit(1); }); - // Override config values with CLI arguments if they are provided - if (cliArguments.quiet) { - config.quiet = cliArguments.quiet; - } - if (cliArguments.debug) { - config.debug = cliArguments.debug; - } - if (cliArguments.exclude.length > 0) { - config.exclude = cliArguments.exclude; - } + if (cliArguments.quiet) config.quiet = cliArguments.quiet; + if (cliArguments.debug) config.debug = cliArguments.debug; + if (cliArguments.exclude.length > 0) config.exclude = cliArguments.exclude; + logger.debug(LOG_SOURCE.CLI, 'Final config:', config); const linter = new DCLinter(config); - // Handle the `fix` and `fix-dry-run` flags if (cliArguments.fix || cliArguments.fixDryRun) { await linter.fixFiles(cliArguments.files, cliArguments.recursive, cliArguments.fixDryRun); } - // Always run the linter after attempting to fix issues let lintResults = await linter.lintFiles(cliArguments.files, cliArguments.recursive); - // Filter out warnings if `--quiet` is enabled if (cliArguments.quiet) { - // Keep only files with errors lintResults = lintResults .map((result) => ({ ...result, @@ -137,26 +120,21 @@ export default async function cli() { .filter((result) => result.messages.length > 0); } - // Count errors and warnings const totalErrors = lintResults.reduce((count, result) => count + result.errorCount, 0); const totalWarnings = lintResults.reduce((count, result) => count + result.warningCount, 0); - // Choose and apply the formatter - const formatter = await loadFormatter(cliArguments.formatter); - const formattedResults = formatter(lintResults); + const formattedResults = await linter.formatResults(lintResults, cliArguments.formatter); - // Output results if (cliArguments.outputFile) { writeFileSync(cliArguments.outputFile, formattedResults); } else { console.log(formattedResults); } - // Determine exit code based on errors and warnings if (totalErrors > 0) { logger.debug(LOG_SOURCE.CLI, `${totalErrors} errors found`); process.exit(1); - } else if (cliArguments.maxWarnings && cliArguments.maxWarnings >= 0 && totalWarnings > cliArguments.maxWarnings) { + } else if (cliArguments.maxWarnings >= 0 && totalWarnings > cliArguments.maxWarnings) { logger.debug( LOG_SOURCE.CLI, `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${cliArguments.maxWarnings})`, @@ -168,4 +146,8 @@ export default async function cli() { process.exit(0); } -await cli(); +// eslint-disable-next-line unicorn/prefer-top-level-await +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/src/cli/cli.types.ts b/src/cli/cli.types.ts index a098bea..a965758 100644 --- a/src/cli/cli.types.ts +++ b/src/cli/cli.types.ts @@ -6,7 +6,7 @@ interface CLIConfig { formatter: string; config?: string; quiet: boolean; - maxWarnings?: number; + maxWarnings: number; outputFile?: string; color: boolean; debug: boolean; diff --git a/src/config/config.ts b/src/config/config.ts index 7ba3167..0aa1d24 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,9 +1,9 @@ import { cosmiconfig } from 'cosmiconfig'; import { Ajv } from 'ajv'; -import type { Config } from './config.types.js'; -import { Logger, LOG_SOURCE } from '../util/logger.js'; -import { loadSchema } from '../util/load-schema.js'; -import { ConfigValidationError } from '../errors/config-validation-error.js'; +import type { Config } from './config.types'; +import { Logger, LOG_SOURCE } from '../util/logger'; +import { schemaLoader } from '../util/schema-loader'; +import { ConfigValidationError } from '../errors/config-validation-error'; function getDefaultConfig(): Config { return { @@ -19,7 +19,7 @@ async function validateConfig(config: Config): Promise { logger.debug(LOG_SOURCE.CONFIG, 'Starting config validation'); const ajv = new Ajv(); - const schema = loadSchema('linter-config'); + const schema = schemaLoader('linter-config'); const validate = ajv.compile(schema); if (!validate(config)) { diff --git a/src/formatters/codeclimate.ts b/src/formatters/codeclimate.ts index 8ae67ce..24dd836 100644 --- a/src/formatters/codeclimate.ts +++ b/src/formatters/codeclimate.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; const generateFingerprint = (data: (string | null)[], hashes: Set): string => { const hash = createHash('md5'); diff --git a/src/formatters/compact.ts b/src/formatters/compact.ts index 58c8d97..d2af3cb 100644 --- a/src/formatters/compact.ts +++ b/src/formatters/compact.ts @@ -1,4 +1,4 @@ -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; export default function compactFormatter(results: LintResult[]): string { return results diff --git a/src/formatters/index.ts b/src/formatters/index.ts new file mode 100644 index 0000000..63154fe --- /dev/null +++ b/src/formatters/index.ts @@ -0,0 +1,13 @@ +import codeclimateFormatter from './codeclimate'; +import compactFormatter from './compact'; +import jsonFormatter from './json'; +import junitFormatter from './junit'; +import stylishFormatter from './stylish'; + +export default { + codeclimateFormatter, + compactFormatter, + jsonFormatter, + junitFormatter, + stylishFormatter, +}; diff --git a/src/formatters/json.ts b/src/formatters/json.ts index d24f319..2c6d83d 100644 --- a/src/formatters/json.ts +++ b/src/formatters/json.ts @@ -1,4 +1,4 @@ -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; export default function jsonFormatter(results: LintResult[]): string { // eslint-disable-next-line unicorn/no-null diff --git a/src/formatters/junit.ts b/src/formatters/junit.ts index c1d1593..70b2e58 100644 --- a/src/formatters/junit.ts +++ b/src/formatters/junit.ts @@ -1,4 +1,4 @@ -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; function escapeXml(unsafe: string): string { return unsafe.replaceAll(/[<>&'"]/g, (c) => { diff --git a/src/formatters/stylish.ts b/src/formatters/stylish.ts index 0723313..7d63e37 100644 --- a/src/formatters/stylish.ts +++ b/src/formatters/stylish.ts @@ -1,6 +1,6 @@ import { resolve } from 'node:path'; -import chalk from 'chalk'; -import type { LintResult } from '../linter/linter.types.js'; +import pc from 'picocolors'; +import type { LintResult } from '../linter/linter.types'; export default function stylishFormatter(results: LintResult[]): string { let output = ''; @@ -14,24 +14,21 @@ export default function stylishFormatter(results: LintResult[]): string { return; } - // Format the file path header without nested template literals - const filePath = chalk.underline(resolve(result.filePath)); + const filePath = pc.underline(resolve(result.filePath)); output += `\n${filePath}\n`; result.messages.forEach((message) => { const { type } = message; - const color = type === 'error' ? chalk.red : chalk.yellow; + const color = type === 'error' ? pc.red : pc.yellow; const line = message.line.toString().padStart(4, ' '); const column = message.column.toString().padEnd(4, ' '); - // Break down message formatting into separate parts - const position = chalk.dim(`${line}:${column}`); + const position = pc.dim(`${line}:${column}`); const formattedType = color(type); - const ruleInfo = chalk.dim(message.rule); + const ruleInfo = pc.dim(message.rule); output += `${position} ${formattedType} ${message.message} ${ruleInfo}\n`; - // Increment counts without using the ++ operator if (type === 'error') { errorCount += 1; if (message.fixable) { @@ -48,15 +45,17 @@ export default function stylishFormatter(results: LintResult[]): string { const totalProblems = errorCount + warningCount; if (totalProblems > 0) { - const problemSummary = chalk.red.bold(`✖ ${totalProblems} problems`); - const errorSummary = chalk.red.bold(`${errorCount} errors`); - const warningSummary = chalk.yellow.bold(`${warningCount} warnings`); + const problemSummary = pc.red(pc.bold(`✖ ${totalProblems} problems`)); + const errorSummary = pc.red(pc.bold(`${errorCount} errors`)); + const warningSummary = pc.yellow(pc.bold(`${warningCount} warnings`)); output += `\n${problemSummary} (${errorSummary}, ${warningSummary})\n`; } if (fixableErrorCount > 0 || fixableWarningCount > 0) { - const fixableSummary = chalk.green.bold( - `${fixableErrorCount} errors and ${fixableWarningCount} warnings potentially fixable with the \`--fix\` option.`, + const fixableSummary = pc.green( + pc.bold( + `${fixableErrorCount} errors and ${fixableWarningCount} warnings potentially fixable with the \`--fix\` option.`, + ), ); output += `${fixableSummary}\n`; } diff --git a/src/index.ts b/src/index.ts index 5062148..ddd9767 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export { DCLinter } from './linter/linter.js'; +export { DCLinter } from './linter/linter'; diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 100e4a2..cb6f234 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -1,22 +1,30 @@ import fs from 'node:fs'; import { parseDocument, YAMLError } from 'yaml'; -import type { Config } from '../config/config.types.js'; -import type { LintRule, LintMessage, LintResult, LintContext } from './linter.types.js'; -import { findFilesForLinting } from '../util/files-finder.js'; -import { loadLintRules } from '../util/rules-loader.js'; -import { Logger, LOG_SOURCE } from '../util/logger.js'; -import { validationComposeSchema } from '../util/compose-validation.js'; -import { ComposeValidationError } from '../errors/compose-validation-error.js'; +import type { Config } from '../config/config.types'; +import type { LintRule, LintMessage, LintResult, LintContext } from './linter.types'; +import { findFilesForLinting } from '../util/files-finder'; +import { loadLintRules } from '../util/rules-loader'; +import { Logger, LOG_SOURCE } from '../util/logger'; +import { validationComposeSchema } from '../util/compose-validation'; +import { ComposeValidationError } from '../errors/compose-validation-error'; +import { loadFormatter } from '../util/formatter-loader'; + +const DEFAULT_CONFIG: Config = { + debug: false, + exclude: [], + rules: {}, + quiet: false, +}; class DCLinter { private readonly config: Config; private rules: LintRule[]; - constructor(config: Config) { - this.config = config; + constructor(config?: Config) { + this.config = { ...DEFAULT_CONFIG, ...config }; this.rules = []; - Logger.init(this.config.debug); + Logger.init(this.config?.debug); } private async loadRules() { @@ -168,6 +176,12 @@ class DCLinter { } }); } + + // eslint-disable-next-line class-methods-use-this + public async formatResults(lintResults: LintResult[], formatterName: string) { + const formatter = await loadFormatter(formatterName); + return formatter(lintResults); + } } export { DCLinter }; diff --git a/src/rules/index.ts b/src/rules/index.ts new file mode 100644 index 0000000..765a827 --- /dev/null +++ b/src/rules/index.ts @@ -0,0 +1,33 @@ +import NoBuildAndImageRule from './no-build-and-image-rule'; +import NoDuplicateContainerNamesRule from './no-duplicate-container-names-rule'; +import NoDuplicateExportedPortsRule from './no-duplicate-exported-ports-rule'; +import NoQuotesInVolumesRule from './no-quotes-in-volumes-rule'; +import NoUnboundPortInterfacesRule from './no-unbound-port-interfaces-rule'; +import NoVersionFieldRule from './no-version-field-rule'; +import RequireProjectNameFieldRule from './require-project-name-field-rule'; +import RequireQuotesInPortsRule from './require-quotes-in-ports-rule'; +import ServiceContainerNameRegexRule from './service-container-name-regex-rule'; +import ServiceDependenciesAlphabeticalOrderRule from './service-dependencies-alphabetical-order-rule'; +import ServiceImageRequireExplicitTagRule from './service-image-require-explicit-tag-rule'; +import ServiceKeysOrderRule from './service-keys-order-rule'; +import ServicePortsAlphabeticalOrderRule from './service-ports-alphabetical-order-rule'; +import ServicesAlphabeticalOrderRule from './services-alphabetical-order-rule'; +import TopLevelPropertiesOrderRule from './top-level-properties-order-rule'; + +export default { + NoBuildAndImageRule, + NoDuplicateContainerNamesRule, + NoDuplicateExportedPortsRule, + NoQuotesInVolumesRule, + NoUnboundPortInterfacesRule, + NoVersionFieldRule, + RequireProjectNameFieldRule, + RequireQuotesInPortsRule, + ServiceContainerNameRegexRule, + ServiceDependenciesAlphabeticalOrderRule, + ServiceImageRequireExplicitTagRule, + ServiceKeysOrderRule, + ServicePortsAlphabeticalOrderRule, + ServicesAlphabeticalOrderRule, + TopLevelPropertiesOrderRule, +}; diff --git a/src/rules/no-build-and-image-rule.ts b/src/rules/no-build-and-image-rule.ts index 07cad8a..8d20774 100644 --- a/src/rules/no-build-and-image-rule.ts +++ b/src/rules/no-build-and-image-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; interface NoBuildAndImageRuleOptions { checkPullPolicy?: boolean; diff --git a/src/rules/no-duplicate-container-names-rule.ts b/src/rules/no-duplicate-container-names-rule.ts index d589f6b..a00e148 100644 --- a/src/rules/no-duplicate-container-names-rule.ts +++ b/src/rules/no-duplicate-container-names-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; export default class NoDuplicateContainerNamesRule implements LintRule { public name = 'no-duplicate-container-names'; diff --git a/src/rules/no-duplicate-exported-ports-rule.ts b/src/rules/no-duplicate-exported-ports-rule.ts index 447cfa0..f2b59e7 100644 --- a/src/rules/no-duplicate-exported-ports-rule.ts +++ b/src/rules/no-duplicate-exported-ports-rule.ts @@ -7,9 +7,9 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; -import { extractPublishedPortValue, parsePortsRange } from '../util/service-ports-parser.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; +import { extractPublishedPortValue, parsePortsRange } from '../util/service-ports-parser'; export default class NoDuplicateExportedPortsRule implements LintRule { public name = 'no-duplicate-exported-ports'; diff --git a/src/rules/no-quotes-in-volumes-rule.ts b/src/rules/no-quotes-in-volumes-rule.ts index dcd614b..ae8277c 100644 --- a/src/rules/no-quotes-in-volumes-rule.ts +++ b/src/rules/no-quotes-in-volumes-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberByValue } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberByValue } from '../util/line-finder'; export default class NoQuotesInVolumesRule implements LintRule { public name = 'no-quotes-in-volumes'; diff --git a/src/rules/no-unbound-port-interfaces-rule.ts b/src/rules/no-unbound-port-interfaces-rule.ts index 247ffce..099ff00 100644 --- a/src/rules/no-unbound-port-interfaces-rule.ts +++ b/src/rules/no-unbound-port-interfaces-rule.ts @@ -7,9 +7,9 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; -import { extractPublishedPortInterfaceValue } from '../util/service-ports-parser.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; +import { extractPublishedPortInterfaceValue } from '../util/service-ports-parser'; export default class NoUnboundPortInterfacesRule implements LintRule { public name = 'no-unbound-port-interfaces'; diff --git a/src/rules/no-version-field-rule.ts b/src/rules/no-version-field-rule.ts index fd84644..c4e73af 100644 --- a/src/rules/no-version-field-rule.ts +++ b/src/rules/no-version-field-rule.ts @@ -6,8 +6,8 @@ import type { LintRuleSeverity, LintMessageType, LintContext, -} from '../linter/linter.types.js'; -import { findLineNumberByKey } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberByKey } from '../util/line-finder'; export default class NoVersionFieldRule implements LintRule { public name = 'no-version-field'; diff --git a/src/rules/require-project-name-field-rule.ts b/src/rules/require-project-name-field-rule.ts index b382752..a997dd7 100644 --- a/src/rules/require-project-name-field-rule.ts +++ b/src/rules/require-project-name-field-rule.ts @@ -6,7 +6,7 @@ import type { LintRuleSeverity, LintMessageType, LintContext, -} from '../linter/linter.types.js'; +} from '../linter/linter.types'; export default class RequireProjectNameFieldRule implements LintRule { public name = 'require-project-name-field'; diff --git a/src/rules/require-quotes-in-ports-rule.ts b/src/rules/require-quotes-in-ports-rule.ts index 941a1ee..8877d51 100644 --- a/src/rules/require-quotes-in-ports-rule.ts +++ b/src/rules/require-quotes-in-ports-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; interface RequireQuotesInPortsRuleOptions { quoteType: 'single' | 'double'; diff --git a/src/rules/service-container-name-regex-rule.ts b/src/rules/service-container-name-regex-rule.ts index ac2c273..b245986 100644 --- a/src/rules/service-container-name-regex-rule.ts +++ b/src/rules/service-container-name-regex-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; export default class ServiceContainerNameRegexRule implements LintRule { public name = 'service-container-name-regex'; diff --git a/src/rules/service-dependencies-alphabetical-order-rule.ts b/src/rules/service-dependencies-alphabetical-order-rule.ts index 1cfe09b..03b5f11 100644 --- a/src/rules/service-dependencies-alphabetical-order-rule.ts +++ b/src/rules/service-dependencies-alphabetical-order-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; export default class ServiceDependenciesAlphabeticalOrderRule implements LintRule { public name = 'service-dependencies-alphabetical-order'; diff --git a/src/rules/service-image-require-explicit-tag-rule.ts b/src/rules/service-image-require-explicit-tag-rule.ts index 7d7ff9a..9a3a8de 100644 --- a/src/rules/service-image-require-explicit-tag-rule.ts +++ b/src/rules/service-image-require-explicit-tag-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; interface ServiceImageRequireExplicitTagRuleOptions { prohibitedTags?: string[]; diff --git a/src/rules/service-keys-order-rule.ts b/src/rules/service-keys-order-rule.ts index d614736..99df89e 100644 --- a/src/rules/service-keys-order-rule.ts +++ b/src/rules/service-keys-order-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; interface ServiceKeysOrderRuleOptions { groupOrder?: GroupOrderEnum[]; diff --git a/src/rules/service-ports-alphabetical-order-rule.ts b/src/rules/service-ports-alphabetical-order-rule.ts index a62b4d0..bf67a5b 100644 --- a/src/rules/service-ports-alphabetical-order-rule.ts +++ b/src/rules/service-ports-alphabetical-order-rule.ts @@ -7,9 +7,9 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; -import { extractPublishedPortValue } from '../util/service-ports-parser.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; +import { extractPublishedPortValue } from '../util/service-ports-parser'; export default class ServicePortsAlphabeticalOrderRule implements LintRule { public name = 'service-ports-alphabetical-order'; diff --git a/src/rules/services-alphabetical-order-rule.ts b/src/rules/services-alphabetical-order-rule.ts index f4abe38..5c2508c 100644 --- a/src/rules/services-alphabetical-order-rule.ts +++ b/src/rules/services-alphabetical-order-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; export default class ServicesAlphabeticalOrderRule implements LintRule { public name = 'services-alphabetical-order'; diff --git a/src/rules/top-level-properties-order-rule.ts b/src/rules/top-level-properties-order-rule.ts index f628cbb..24cb5c4 100644 --- a/src/rules/top-level-properties-order-rule.ts +++ b/src/rules/top-level-properties-order-rule.ts @@ -7,8 +7,8 @@ import type { LintRuleCategory, LintRuleSeverity, RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberByKey } from '../util/line-finder.js'; +} from '../linter/linter.types'; +import { findLineNumberByKey } from '../util/line-finder'; interface TopLevelPropertiesOrderRuleOptions { customOrder?: TopLevelKeys[]; diff --git a/src/util/compose-validation.ts b/src/util/compose-validation.ts index dad5a9d..fe4987e 100644 --- a/src/util/compose-validation.ts +++ b/src/util/compose-validation.ts @@ -1,7 +1,7 @@ import { Ajv2019 } from 'ajv/dist/2019.js'; import { ErrorObject } from 'ajv'; -import { ComposeValidationError } from '../errors/compose-validation-error.js'; -import { loadSchema } from './load-schema.js'; +import { ComposeValidationError } from '../errors/compose-validation-error'; +import { schemaLoader } from './schema-loader'; type Schema = Record; @@ -32,7 +32,7 @@ function validationComposeSchema(content: object) { logger: false, }); - const composeSchema = loadSchema('compose'); + const composeSchema = schemaLoader('compose'); const validate = ajv.compile(updateSchema(composeSchema)); const valid = validate(content); diff --git a/src/util/files-finder.ts b/src/util/files-finder.ts index cc199f5..7cb50f6 100644 --- a/src/util/files-finder.ts +++ b/src/util/files-finder.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import { basename, join, resolve } from 'node:path'; -import { Logger } from './logger.js'; -import { FileNotFoundError } from '../errors/file-not-found-error.js'; +import { Logger } from './logger'; +import { FileNotFoundError } from '../errors/file-not-found-error'; export function findFilesForLinting(paths: string[], recursive: boolean, excludePaths: string[]): string[] { const logger = Logger.getInstance(); diff --git a/src/util/formatter-loader.ts b/src/util/formatter-loader.ts index 036b465..7a5de91 100644 --- a/src/util/formatter-loader.ts +++ b/src/util/formatter-loader.ts @@ -1,6 +1,7 @@ import { resolve } from 'node:path'; -import type { LintResult } from '../linter/linter.types.js'; -import { Logger } from './logger.js'; +import type { LintResult } from '../linter/linter.types'; +import { Logger } from './logger'; +import Formatters from '../formatters/index'; type FormatterFunction = (results: LintResult[]) => string; @@ -30,21 +31,12 @@ export async function loadFormatter(formatterName: string): Promise Promise> = { - json: async () => (await import('../formatters/json.js')).default as FormatterFunction, - compact: async () => (await import('../formatters/compact.js')).default as FormatterFunction, - stylish: async () => (await import('../formatters/stylish.js')).default as FormatterFunction, - junit: async () => (await import('../formatters/junit.js')).default as FormatterFunction, - codeclimate: async () => (await import('../formatters/codeclimate.js')).default as FormatterFunction, - }; - - let formatterLoader = builtinFormatters[formatterName]; - if (!formatterLoader) { - logger.warn(`Unknown formatter: ${formatterName}. Using default - stylish.`); - formatterLoader = builtinFormatters.stylish; + const formatterFunction = Formatters[`${formatterName}Formatter` as keyof typeof Formatters]; + if (formatterFunction) { + logger.debug('UTIL', `Using built-in formatter: ${formatterName}`); + return formatterFunction as FormatterFunction; } - logger.debug('UTIL', `Load formatter: ${formatterName}`); - - return formatterLoader(); + logger.warn(`Unknown formatter: ${formatterName}. Using default - stylish.`); + return Formatters.stylishFormatter; } diff --git a/src/util/load-schema.ts b/src/util/load-schema.ts deleted file mode 100644 index 96fc4cf..0000000 --- a/src/util/load-schema.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -function loadSchema(name: string): Record { - return JSON.parse( - readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), `../../schemas/${name}.schema.json`), 'utf8'), - ) as Record; -} - -export { loadSchema }; diff --git a/src/util/logger.ts b/src/util/logger.ts index e00472f..7211a9c 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -1,4 +1,4 @@ -import chalk from 'chalk'; +import pc from 'picocolors'; // Exported constants for log sources export const LOG_SOURCE = { @@ -43,13 +43,13 @@ class Logger { private static getColoredLevel(level: string): string { switch (level) { case 'DEBUG': - return chalk.blue('[DEBUG]'); + return pc.blue('[DEBUG]'); case 'INFO': - return chalk.green('[INFO]'); + return pc.green('[INFO]'); case 'WARN': - return chalk.yellow('[WARN]'); + return pc.yellow('[WARN]'); case 'ERROR': - return chalk.red('[ERROR]'); + return pc.red('[ERROR]'); default: return `[${level}]`; } diff --git a/src/util/rules-loader.ts b/src/util/rules-loader.ts index 73ecf1c..e9b59c9 100644 --- a/src/util/rules-loader.ts +++ b/src/util/rules-loader.ts @@ -1,68 +1,43 @@ -import { join, resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import fs from 'node:fs'; -import type { LintRule, LintMessageType } from '../linter/linter.types.js'; -import type { Config, ConfigRuleLevel, ConfigRule } from '../config/config.types.js'; -import { Logger } from './logger.js'; - -async function importRule(file: string, rulesDirectory: string): Promise { - const logger = Logger.getInstance(); - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - const RuleClass = (await import(join(rulesDirectory, file))).default; - - if (typeof RuleClass === 'function') { - return new (RuleClass as new () => LintRule)(); - } - return null; - } catch (error) { - logger.error(`Error importing rule from file: ${file}`, error); - return null; - } -} +import type { LintRule, LintMessageType } from '../linter/linter.types'; +import type { Config, ConfigRuleLevel, ConfigRule } from '../config/config.types'; +import { Logger } from './logger'; +import Rules from '../rules/index'; async function loadLintRules(config: Config): Promise { - const rulesDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '../rules'); - - const ruleFiles = fs - .readdirSync(rulesDirectory) - .filter((file) => file.endsWith('.js') || (file.endsWith('.ts') && !file.endsWith('d.ts'))); - - // Parallel import with Promise.all - const ruleInstances: (LintRule | null)[] = await Promise.all( - ruleFiles.map(async (file) => importRule(file, rulesDirectory)), - ); - + const logger = Logger.getInstance(); const activeRules: LintRule[] = []; - ruleInstances.forEach((ruleInstance) => { - if (!ruleInstance) return; - - const ruleConfig: ConfigRule = config.rules[ruleInstance.name]; - - let ruleLevel: ConfigRuleLevel; - let ruleOptions: Record | undefined; - - if (Array.isArray(ruleConfig)) { - [ruleLevel, ruleOptions] = ruleConfig; - } else { - ruleLevel = ruleConfig; + for (const RuleClass of Object.values(Rules)) { + try { + const ruleInstance = new (RuleClass as new () => LintRule)(); + const ruleConfig: ConfigRule = config.rules[ruleInstance.name]; + + let ruleLevel: ConfigRuleLevel; + let ruleOptions: Record | undefined; + + if (Array.isArray(ruleConfig)) { + [ruleLevel, ruleOptions] = ruleConfig; + } else { + ruleLevel = ruleConfig; + } + + if (ruleLevel !== 0) { + const instance = ruleOptions + ? new (RuleClass as new (options?: Record) => LintRule)(ruleOptions) + : ruleInstance; + + const typeMap: { [key: number]: LintMessageType } = { + 1: 'warning', + 2: 'error', + }; + + instance.type = typeMap[ruleLevel] || instance.type; + activeRules.push(instance); + } + } catch (error) { + logger.error(`Error loading rule: ${RuleClass?.name}`, error); } - - if (ruleLevel === 0) return; - - const RuleClass = ruleInstance.constructor as new (options?: Record) => LintRule; - const instance = ruleOptions ? new RuleClass(ruleOptions) : new RuleClass(); - - const typeMap: { [key: number]: LintMessageType } = { - 1: 'warning', - 2: 'error', - }; - - instance.type = typeMap[ruleLevel] || instance.type; - - activeRules.push(instance); - }); + } return activeRules; } diff --git a/src/util/schema-loader.ts b/src/util/schema-loader.ts new file mode 100644 index 0000000..89c8dfe --- /dev/null +++ b/src/util/schema-loader.ts @@ -0,0 +1,15 @@ +import composeSchema from '../../schemas/compose.schema.json' with { type: 'json' }; +import linterConfigSchema from '../../schemas/linter-config.schema.json' with { type: 'json' }; + +function schemaLoader(schemaName: string): Record { + switch (schemaName) { + case 'compose': + return composeSchema; + case 'linter-config': + return linterConfigSchema; + default: + return {}; + } +} + +export { schemaLoader }; diff --git a/tests/linter.spec.ts b/tests/linter.spec.ts index 9a40e93..3517923 100644 --- a/tests/linter.spec.ts +++ b/tests/linter.spec.ts @@ -1,9 +1,9 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import esmock from 'esmock'; -import { Logger } from '../src/util/logger.js'; -import type { Config } from '../src/config/config.types.js'; -import type { LintResult, LintRule } from '../src/linter/linter.types.js'; +import { Logger } from '../src/util/logger'; +import type { Config } from '../src/config/config.types'; +import type { LintResult, LintRule } from '../src/linter/linter.types'; // Sample configuration const config: Config = { @@ -65,9 +65,9 @@ test('DCLinter: should lint files correctly', async (t: ExecutionContext) => { // Use esmock to mock both rules-loader and files-finder modules // eslint-disable-next-line sonarjs/no-duplicate-string - const { DCLinter } = await esmock('../src/linter/linter.js', { - '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, - '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, + const { DCLinter } = await esmock('../src/linter/linter', { + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { findFilesForLinting: mockFindFiles }, 'node:fs': { readFileSync: mockReadFileSync }, }); @@ -94,9 +94,9 @@ test('DCLinter: should lint multiple files correctly', async (t: ExecutionContex // Use esmock to mock both rules-loader and files-finder modules // eslint-disable-next-line sonarjs/no-duplicate-string - const { DCLinter } = await esmock('../src/linter/linter.js', { - '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, - '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, + const { DCLinter } = await esmock('../src/linter/linter', { + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { findFilesForLinting: mockFindFiles }, 'node:fs': { readFileSync: mockReadFileSync }, }); @@ -122,9 +122,9 @@ test('DCLinter: should fix files', async (t: ExecutionContext) => { // Use esmock to mock both rules-loader and files-finder modules // eslint-disable-next-line sonarjs/no-duplicate-string - const { DCLinter } = await esmock('../src/linter/linter.js', { - '../src/util/rules-loader.js': { loadLintRules: mockLoadLintRules }, - '../src/util/files-finder.js': { findFilesForLinting: mockFindFiles }, + const { DCLinter } = await esmock('../src/linter/linter', { + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { findFilesForLinting: mockFindFiles }, 'node:fs': { readFileSync: mockReadFileSync, writeFileSync: mockWriteFileSync }, }); diff --git a/tests/mocks/docker-compose.correct.yml b/tests/mocks/docker-compose.correct.yml new file mode 100644 index 0000000..766a7a6 --- /dev/null +++ b/tests/mocks/docker-compose.correct.yml @@ -0,0 +1,63 @@ +name: "test" +services: + a-service: + build: + context: ../../tests/a-service + dockerfile: Dockerfile + args: + - TEST=${TEST} + container_name: a-service + depends_on: + b-service: + condition: service_healthy + c-service: + condition: service_started + volumes: + - ../../app/a-service/:/var/www/app + - /var/www/app/node_modules + environment: + - TEST=${TEST} + env_file: ./envs/.env.a-service + ports: + - '127.0.0.1:3001' + - '127.0.0.1:11032:3000' + - '127.0.0.1:11150:3000' + # command: sh -c "npm run start" + command: sh -c "tail -f /dev/null" + expose: + - '3000' + b-service: + build: + context: ../../app/b-service + dockerfile: Dockerfile + target: builder + args: + - TEST1=${TEST} + - TEST2=${TEST} + container_name: b-service + depends_on: + - c-service + - kafka + volumes: + - ../../app/flexible-forms-client/:/var/www/app + - data:/var/www/app/node_modules + env_file: ./envs/.env.b-service + ports: + - '127.0.0.1:11131:3000' + command: sh -c "npm run start" + c-service: + build: + context: ../../tests/c-service + dockerfile: Dockerfile + args: + - TEST=${TEST} + environment: + - TEST='HQTb_=d.4*FPN@^;w2)UZ%' + test: + image: node:20 + build: "" + container_name: a2-service + pull_policy: always +volumes: + data: + driver: local diff --git a/tests/rules/no-build-and-image-rule.spec.ts b/tests/rules/no-build-and-image-rule.spec.ts index 73606d6..3c07397 100644 --- a/tests/rules/no-build-and-image-rule.spec.ts +++ b/tests/rules/no-build-and-image-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import NoBuildAndImageRule from '../../src/rules/no-build-and-image-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import NoBuildAndImageRule from '../../src/rules/no-build-and-image-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML with services using both build and image const yamlWithBuildAndImage = ` diff --git a/tests/rules/no-duplicate-container-names-rule.spec.ts b/tests/rules/no-duplicate-container-names-rule.spec.ts index 9c7c382..e634983 100644 --- a/tests/rules/no-duplicate-container-names-rule.spec.ts +++ b/tests/rules/no-duplicate-container-names-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import NoDuplicateContainerNamesRule from '../../src/rules/no-duplicate-container-names-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import NoDuplicateContainerNamesRule from '../../src/rules/no-duplicate-container-names-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML с дублирующимися именами контейнеров const yamlWithDuplicateContainerNames = ` diff --git a/tests/rules/no-duplicate-exported-ports-rule.spec.ts b/tests/rules/no-duplicate-exported-ports-rule.spec.ts index 4b01ce3..336f5d1 100644 --- a/tests/rules/no-duplicate-exported-ports-rule.spec.ts +++ b/tests/rules/no-duplicate-exported-ports-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import NoDuplicateExportedPortsRule from '../../src/rules/no-duplicate-exported-ports-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import NoDuplicateExportedPortsRule from '../../src/rules/no-duplicate-exported-ports-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML with multiple duplicate exported ports const yamlWithDuplicatePorts = ` diff --git a/tests/rules/no-quotes-in-volumes-rule.spec.ts b/tests/rules/no-quotes-in-volumes-rule.spec.ts index 6b42030..9c77660 100644 --- a/tests/rules/no-quotes-in-volumes-rule.spec.ts +++ b/tests/rules/no-quotes-in-volumes-rule.spec.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; -import NoQuotesInVolumesRule from '../../src/rules/no-quotes-in-volumes-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import NoQuotesInVolumesRule from '../../src/rules/no-quotes-in-volumes-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const correctYAML = ` diff --git a/tests/rules/no-unbound-port-interfaces-rule.spec.ts b/tests/rules/no-unbound-port-interfaces-rule.spec.ts index e29841b..f89e194 100644 --- a/tests/rules/no-unbound-port-interfaces-rule.spec.ts +++ b/tests/rules/no-unbound-port-interfaces-rule.spec.ts @@ -1,7 +1,7 @@ import test from 'ava'; import { parseDocument } from 'yaml'; -import NoUnboundPortInterfacesRule from '../../src/rules/no-unbound-port-interfaces-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import NoUnboundPortInterfacesRule from '../../src/rules/no-unbound-port-interfaces-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML with multiple duplicate exported ports const yamlWithImplicitListenEverywherePorts = ` diff --git a/tests/rules/no-version-field-rule.spec.ts b/tests/rules/no-version-field-rule.spec.ts index e5c9537..6172fb8 100644 --- a/tests/rules/no-version-field-rule.spec.ts +++ b/tests/rules/no-version-field-rule.spec.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; -import NoVersionFieldRule from '../../src/rules/no-version-field-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import NoVersionFieldRule from '../../src/rules/no-version-field-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithVersion = ` diff --git a/tests/rules/require-project-name-field-rule.spec.ts b/tests/rules/require-project-name-field-rule.spec.ts index 7b8e725..b4bcf61 100644 --- a/tests/rules/require-project-name-field-rule.spec.ts +++ b/tests/rules/require-project-name-field-rule.spec.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; -import RequireProjectNameFieldRule from '../../src/rules/require-project-name-field-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import RequireProjectNameFieldRule from '../../src/rules/require-project-name-field-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithName = ` diff --git a/tests/rules/require-quotes-in-ports-rule.spec.ts b/tests/rules/require-quotes-in-ports-rule.spec.ts index 24f6b18..95d5ae5 100644 --- a/tests/rules/require-quotes-in-ports-rule.spec.ts +++ b/tests/rules/require-quotes-in-ports-rule.spec.ts @@ -1,7 +1,7 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; -import RequireQuotesInPortsRule from '../../src/rules/require-quotes-in-ports-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import RequireQuotesInPortsRule from '../../src/rules/require-quotes-in-ports-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithoutQuotes = ` @@ -38,10 +38,7 @@ test('RequireQuotesInPortsRule: should return a warning when ports are not quote const errors = rule.check(context); t.is(errors.length, 1, 'There should be one warning when ports are not quoted.'); - t.is( - errors[0].message, - 'Ports in `ports` and `expose` sections should be enclosed in quotes.', - ); + t.is(errors[0].message, 'Ports in `ports` and `expose` sections should be enclosed in quotes.'); t.is(errors[0].rule, 'require-quotes-in-ports'); t.is(errors[0].severity, 'minor'); }); diff --git a/tests/rules/service-container-name-regex-rule.spec.ts b/tests/rules/service-container-name-regex-rule.spec.ts index 2668d31..5f840dc 100644 --- a/tests/rules/service-container-name-regex-rule.spec.ts +++ b/tests/rules/service-container-name-regex-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import ServiceContainerNameRegexRule from '../../src/rules/service-container-name-regex-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import ServiceContainerNameRegexRule from '../../src/rules/service-container-name-regex-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML with incorrect syntax const yamlWithInvalidContainerName = ` diff --git a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts index da75ebd..b7b9440 100644 --- a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import ServiceDependenciesAlphabeticalOrderRule from '../../src/rules/service-dependencies-alphabetical-order-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import ServiceDependenciesAlphabeticalOrderRule from '../../src/rules/service-dependencies-alphabetical-order-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML with short syntax (incorrect order) const yamlWithIncorrectShortSyntax = ` diff --git a/tests/rules/service-image-require-explicit-tag-rule.spec.ts b/tests/rules/service-image-require-explicit-tag-rule.spec.ts index 3155c48..d427526 100644 --- a/tests/rules/service-image-require-explicit-tag-rule.spec.ts +++ b/tests/rules/service-image-require-explicit-tag-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import ServiceImageRequireExplicitTagRule from '../../src/rules/service-image-require-explicit-tag-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import ServiceImageRequireExplicitTagRule from '../../src/rules/service-image-require-explicit-tag-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // YAML with services using no tag, but valid image formats const yamlWithoutTag = ` diff --git a/tests/rules/service-keys-order-rule.spec.ts b/tests/rules/service-keys-order-rule.spec.ts index 54f2860..b6da044 100644 --- a/tests/rules/service-keys-order-rule.spec.ts +++ b/tests/rules/service-keys-order-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import ServiceKeysOrderRule from '../../src/rules/service-keys-order-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import ServiceKeysOrderRule from '../../src/rules/service-keys-order-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithIncorrectOrder = ` diff --git a/tests/rules/service-ports-alphabetical-order-rule.spec.ts b/tests/rules/service-ports-alphabetical-order-rule.spec.ts index e91804f..657c979 100644 --- a/tests/rules/service-ports-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-ports-alphabetical-order-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import ServicePortsAlphabeticalOrderRule from '../../src/rules/service-ports-alphabetical-order-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import ServicePortsAlphabeticalOrderRule from '../../src/rules/service-ports-alphabetical-order-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithIncorrectPortOrder = ` diff --git a/tests/rules/services-alphabetical-order-rule.spec.ts b/tests/rules/services-alphabetical-order-rule.spec.ts index 3298cdc..c8892df 100644 --- a/tests/rules/services-alphabetical-order-rule.spec.ts +++ b/tests/rules/services-alphabetical-order-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import ServicesAlphabeticalOrderRule from '../../src/rules/services-alphabetical-order-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import ServicesAlphabeticalOrderRule from '../../src/rules/services-alphabetical-order-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithIncorrectOrder = ` diff --git a/tests/rules/top-level-properties-order-rule.spec.ts b/tests/rules/top-level-properties-order-rule.spec.ts index cef34dd..e44b3dc 100644 --- a/tests/rules/top-level-properties-order-rule.spec.ts +++ b/tests/rules/top-level-properties-order-rule.spec.ts @@ -1,8 +1,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import { parseDocument } from 'yaml'; -import TopLevelPropertiesOrderRule, { TopLevelKeys } from '../../src/rules/top-level-properties-order-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import TopLevelPropertiesOrderRule, { TopLevelKeys } from '../../src/rules/top-level-properties-order-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML content with incorrect order of top-level properties const yamlWithIncorrectOrder = ` diff --git a/tests/util/files-finder.spec.ts b/tests/util/files-finder.spec.ts index d92f0e1..b1a9a86 100644 --- a/tests/util/files-finder.spec.ts +++ b/tests/util/files-finder.spec.ts @@ -3,8 +3,8 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; import esmock from 'esmock'; -import { Logger } from '../../src/util/logger.js'; -import { FileNotFoundError } from '../../src/errors/file-not-found-error.js'; +import { Logger } from '../../src/util/logger'; +import { FileNotFoundError } from '../../src/errors/file-not-found-error'; const mockDirectory = '/path/to/directory'; const mockNodeModulesDirectory = '/path/to/directory/node_modules'; @@ -48,8 +48,8 @@ const mockExistsSync = () => true; // @ts-ignore TS2349 test('findFilesForLinting: should handle recursive search and find only compose files in directory and exclude node_modules', async (t: ExecutionContext) => { // Use esmock to mock fs module - const { findFilesForLinting } = await esmock( - '../../src/util/files-finder.js', + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder', { 'node:fs': { existsSync: mockExistsSync, readdirSync: mockReaddirSync, statSync: mockStatSync }, }, @@ -71,8 +71,8 @@ test('findFilesForLinting: should handle recursive search and find only compose // @ts-ignore TS2349 test('findFilesForLinting: should return file directly if file is passed and search only compose in directory', async (t: ExecutionContext) => { // Use esmock to mock fs module - const { findFilesForLinting } = await esmock( - '../../src/util/files-finder.js', + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder', { 'node:fs': { existsSync: mockExistsSync, statSync: mockStatSync, readdirSync: mockReaddirSync }, }, @@ -94,8 +94,8 @@ test('findFilesForLinting: should return file directly if file is passed and sea // @ts-ignore TS2349 test('findFilesForLinting: should throw error if path does not exist', async (t: ExecutionContext) => { // Use esmock to mock fs module - const { findFilesForLinting } = await esmock( - '../../src/util/files-finder.js', + const { findFilesForLinting } = await esmock( + '../../src/util/files-finder', { 'node:fs': { existsSync: () => false }, }, diff --git a/tests/util/line-finder.spec.ts b/tests/util/line-finder.spec.ts index 2cfc1ad..f2750f2 100644 --- a/tests/util/line-finder.spec.ts +++ b/tests/util/line-finder.spec.ts @@ -1,6 +1,6 @@ import test from 'ava'; import type { ExecutionContext } from 'ava'; -import { findLineNumberByKey, findLineNumberByValue } from '../../src/util/line-finder.js'; +import { findLineNumberByKey, findLineNumberByValue } from '../../src/util/line-finder'; // @ts-ignore TS2349 test('findLineNumberByKey: should return the correct line number when the key exists', (t: ExecutionContext) => { diff --git a/tests/util/service-ports-parser.spec.ts b/tests/util/service-ports-parser.spec.ts index 4926c7d..7ed6111 100644 --- a/tests/util/service-ports-parser.spec.ts +++ b/tests/util/service-ports-parser.spec.ts @@ -5,7 +5,7 @@ import { extractPublishedPortValue, extractPublishedPortInterfaceValue, parsePortsRange, -} from '../../src/util/service-ports-parser.js'; +} from '../../src/util/service-ports-parser'; // @ts-ignore TS2349 test('extractPublishedPortValue should return port from scalar value with no IP', (t: ExecutionContext) => { diff --git a/tsconfig.json b/tsconfig.json index 3bb58fa..915b078 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,14 +6,14 @@ "declaration": true, "outDir": "./dist", "strict": true, - "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "importHelpers": true, "noEmitHelpers": true, + "esModuleInterop": true, "resolveJsonModule": true, }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "schemas/*.json"], "exclude": [ "node_modules", "dist" From 3306d93882490c913362f8fc876fc4e4abb01569 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Tue, 12 Nov 2024 15:32:52 +0100 Subject: [PATCH 14/16] fix(require-quotes-in-ports-rule): ensure single port values are wrapped in quotes --- src/rules/require-quotes-in-ports-rule.ts | 5 +- .../require-quotes-in-ports-rule.spec.ts | 49 ++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/rules/require-quotes-in-ports-rule.ts b/src/rules/require-quotes-in-ports-rule.ts index 8877d51..09304c6 100644 --- a/src/rules/require-quotes-in-ports-rule.ts +++ b/src/rules/require-quotes-in-ports-rule.ts @@ -112,8 +112,9 @@ export default class RequireQuotesInPortsRule implements LintRule { this.portsSections.forEach((section) => { RequireQuotesInPortsRule.extractValues(parsedDocument.contents, section, (service, port) => { if (port.type !== this.getQuoteType()) { - // eslint-disable-next-line no-param-reassign - port.type = this.getQuoteType(); + const newPort = new Scalar(String(port.value)); + newPort.type = this.getQuoteType(); + Object.assign(port, newPort); } }); }); diff --git a/tests/rules/require-quotes-in-ports-rule.spec.ts b/tests/rules/require-quotes-in-ports-rule.spec.ts index 95d5ae5..1ebb9c2 100644 --- a/tests/rules/require-quotes-in-ports-rule.spec.ts +++ b/tests/rules/require-quotes-in-ports-rule.spec.ts @@ -8,23 +8,35 @@ const yamlWithoutQuotes = ` services: web: ports: + - 80 - 8080:80 + expose: + - 3000 `; const yamlWithSingleQuotes = ` services: web: ports: + - '80' - '8080:80' + expose: + - '3000' `; const yamlWithDoubleQuotes = ` services: web: ports: + - "80" - "8080:80" + expose: + - "3000" `; +// Helper function to normalize YAML +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); + const pathToFile = '/docker-compose.yml'; // @ts-ignore TS2349 @@ -37,10 +49,13 @@ test('RequireQuotesInPortsRule: should return a warning when ports are not quote }; const errors = rule.check(context); - t.is(errors.length, 1, 'There should be one warning when ports are not quoted.'); - t.is(errors[0].message, 'Ports in `ports` and `expose` sections should be enclosed in quotes.'); - t.is(errors[0].rule, 'require-quotes-in-ports'); - t.is(errors[0].severity, 'minor'); + t.is(errors.length, 3, 'There should be one warning when ports are not quoted.'); + + const expectedMessage = 'Ports in `ports` and `expose` sections should be enclosed in quotes.'; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessage)); + }); }); // @ts-ignore TS2349 @@ -74,23 +89,23 @@ test('RequireQuotesInPortsRule: should fix unquoted ports by adding single quote const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); const fixedYAML = rule.fix(yamlWithoutQuotes); - t.true( - fixedYAML.includes(`'8080:80'`), + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithSingleQuotes), 'The Ports in `ports` and `expose` sections should be quoted with single quotes.', ); - t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); // @ts-ignore TS2349 test('RequireQuotesInPortsRule: should fix double quotes ports by changing them to single quotes', (t: ExecutionContext) => { const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); - const fixedYAML = rule.fix(yamlWithSingleQuotes); - t.true( - fixedYAML.includes(`'8080:80'`), + const fixedYAML = rule.fix(yamlWithDoubleQuotes); + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithSingleQuotes), 'The Ports in `ports` and `expose` sections should be quoted with single quotes.', ); - t.false(fixedYAML.includes(`"8080:80"`), 'The ports should not have double quotes.'); }); // @ts-ignore TS2349 @@ -98,11 +113,11 @@ test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quote const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const fixedYAML = rule.fix(yamlWithoutQuotes); - t.true( - fixedYAML.includes(`"8080:80"`), + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithDoubleQuotes), 'The Ports in `ports` and `expose` sections should be quoted with double quotes.', ); - t.false(fixedYAML.includes('ports:\n - 8080:80'), 'The unquoted ports should no longer exist.'); }); // @ts-ignore TS2349 @@ -110,9 +125,9 @@ test('RequireQuotesInPortsRule: should fix single quotes ports by changing them const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); const fixedYAML = rule.fix(yamlWithSingleQuotes); - t.true( - fixedYAML.includes(`"8080:80"`), + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithDoubleQuotes), 'The Ports in `ports` and `expose` sections should be quoted with double quotes.', ); - t.false(fixedYAML.includes(`'8080:80'`), 'The ports should not have single quotes.'); }); From c3e23670cbced9621b04fc9ee2e638c88b429005 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Thu, 14 Nov 2024 15:39:57 +0100 Subject: [PATCH 15/16] ci: add Hadolint check --- .github/workflows/main.yml | 8 ++++++-- .hadolint.yaml | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .hadolint.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c79a5ec..85d28e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,11 @@ jobs: - name: Run linter run: npm run lint + - name: Run Hadolint on Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ./Dockerfile + - name: Run tests run: npm run test:coverage @@ -47,7 +52,7 @@ jobs: debug: runs-on: ubuntu-latest -# if: github.event_name == 'pull_request' || github.ref != 'refs/heads/main' + if: github.event_name == 'pull_request' needs: tests strategy: matrix: @@ -142,7 +147,6 @@ jobs: if: github.ref == 'refs/heads/main' needs: - tests - - debug - build - build_sea diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..be27648 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,14 @@ +strict-labels: true + +label-schema: + org.opencontainers.image.title: text + org.opencontainers.image.description: text + org.opencontainers.image.created: text # Originally rfc3339, replace if passed not with ARG + org.opencontainers.image.authors: text + org.opencontainers.image.url: url + org.opencontainers.image.documentation: url + org.opencontainers.image.source: url + org.opencontainers.image.version: text # Originally semver, replace if passed not with ARG + org.opencontainers.image.revision: text # Originally hash, replace if passed not with ARG + org.opencontainers.image.vendor: text + org.opencontainers.image.licenses: text From d826cb4aa6b46db4187cdef2565aa26b997169d8 Mon Sep 17 00:00:00 2001 From: Sergey Kupletsky Date: Thu, 14 Nov 2024 16:16:06 +0100 Subject: [PATCH 16/16] feat: add Disabling Rules via Comments feature --- README.md | 6 + docs/configuration-comments.md | 103 +++++++++++++++ src/linter/linter.ts | 51 ++++++-- src/util/comments-handler.ts | 57 +++++++++ tests/linter.spec.ts | 87 +++++++++++++ tests/mocks/docker-compose.yml | 1 + tests/util/comments-handler.spec.ts | 187 ++++++++++++++++++++++++++++ 7 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 docs/configuration-comments.md create mode 100644 src/util/comments-handler.ts create mode 100644 tests/util/comments-handler.spec.ts diff --git a/README.md b/README.md index e9e2b49..3cda818 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,12 @@ ensures that any configuration files you check are compliant with YAML standards two important validations are performed, which cannot be disabled - [YAML Validity Check](./docs/errors/invalid-yaml.md) and [Docker Compose Schema Validation](./docs/errors/invalid-schema.md). +### Disabling Rules via Comments + +You can disable specific linting rules or all rules in your Docker Compose files using comments. These comments can be +used either to disable rules for the entire file or for individual lines. For detailed instructions on how to use these +comments, check out the full documentation here: [Using Configuration Comments](./docs/configuration-comments.md). + ### Anchor Handling Docker Compose Linter provides support for YAML anchors specifically during schema validation, which enables the reuse diff --git a/docs/configuration-comments.md b/docs/configuration-comments.md new file mode 100644 index 0000000..a40569e --- /dev/null +++ b/docs/configuration-comments.md @@ -0,0 +1,103 @@ +# Using Configuration Comments + +Disabling linting rules via comments should be done cautiously and only when there is a clear, valid reason. It should +**not** be the default approach for resolving linting issues, as overuse can lead to poor code quality. Whenever a rule +is disabled, it's important to provide a comment explaining why this is being done in that specific situation. If a +disable comment is added as a **temporary measure**, make sure the underlying issue is addressed later and a follow-up +task is created. + +**Configuration files** should be preferred over inline disables whenever possible, as they allow for consistent rule +management across the project. + +## Disabling Rules + +### Disabling All Rules for the Entire File + +To **disable all linting rules** for the entire file, add the following comment at the **top** of the file: + +```yaml +# dclint disable-file +``` + +This will disable **all rules** for the entire file. Use this only when it is absolutely necessary. + +### Disabling Specific Rules for the Entire File + +To **disable specific rules** for the entire file, add the following comment at the **top** of the file: + +```yaml +# dclint disable +``` + +This will disable **all rules** in the file. Alternatively, you can specify specific rules like this: + +```yaml +# dclint disable rule-name +``` + +You can also disable multiple specific rules: + +```yaml +# dclint disable rule-name another-rule-name +``` + +### Disabling Rules for Specific Lines + +**Note**: `disable-line` **only affects linting**, and **does not work with auto-fix**. Auto-fix is not applied to lines +where `disable-line` is used. + +To **disable linting rules for a specific line**, use the `disable-line` comment. This can be added in two ways - on the +same or previous line: + +- **Disable all rules for a line**: + + ```yaml + services: + service-a: + image: nginx # dclint disable-line + service-b: + # dclint disable-line + image: nginx + ``` + +- **Disable a specific rule for a line**: + + ```yaml + services: + service-a: + image: nginx # dclint disable-line rule-name + service-b: + # dclint disable-line rule-name + image: nginx + ``` + +- **Disable multiple specific rules for a line**: + + ```yaml + services: + service-a: + image: nginx # dclint disable-line rule-name another-rule-name + service-b: + # dclint disable-line rule-name another-rule-name + image: nginx + ``` + +### **Important Notes** + +- **Auto-Fix Limitation**: The `# dclint disable-line` comment will **not** affect auto-fix operations. It only disables + linting for the specified line but does not prevent automatic fixes from being applied. +- **`# dclint disable-file` vs. `# dclint disable`**: The `# dclint disable-file` comment disables **all rules** for the + entire file, and it works **faster** than `# dclint disable`, which disables rules one by one. Use + `# dclint disable-file` when you need to quickly disable all rules across the file. + +### **Summary of Commands** + +| Command | Description | Affects Linting | Affects Auto-Fix | +| ------------------------------------- | ------------------------------------------------------- | ------------------ | ------------------ | +| `# dclint disable-file` | Disables all linting rules for the entire file. | :white_check_mark: | :white_check_mark: | +| `# dclint disable` | Disables all linting rules for the entire file. | :white_check_mark: | :white_check_mark: | +| `# dclint disable rule-name` | Disables a specific rule for the entire file. | :white_check_mark: | :white_check_mark: | +| `# dclint disable rule-name ...` | Disables multiple specific rules for the entire file. | :white_check_mark: | :white_check_mark: | +| `# dclint disable-line` | Disables all linting rules for the specific line. | :white_check_mark: | :x: | +| `# dclint disable-line rule-name` | Disables a specific rule for the specific line. | :white_check_mark: | :x: | +| `# dclint disable-line rule-name ...` | Disables multiple specific rules for the specific line. | :white_check_mark: | :x: | diff --git a/src/linter/linter.ts b/src/linter/linter.ts index cb6f234..0aa969a 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -8,6 +8,11 @@ import { Logger, LOG_SOURCE } from '../util/logger'; import { validationComposeSchema } from '../util/compose-validation'; import { ComposeValidationError } from '../errors/compose-validation-error'; import { loadFormatter } from '../util/formatter-loader'; +import { + extractDisableLineRules, + extractGlobalDisableRules, + startsWithDisableFileComment, +} from '../util/comments-handler'; const DEFAULT_CONFIG: Config = { debug: false, @@ -36,14 +41,45 @@ class DCLinter { private lintContent(context: LintContext): LintMessage[] { const messages: LintMessage[] = []; + // Get globally disabled rules (from the first line) + const globalDisableRules = extractGlobalDisableRules(context.sourceCode); + const disableLineRules = extractDisableLineRules(context.sourceCode); + this.rules.forEach((rule) => { - const ruleMessages = rule.check(context); + // Ignore rule from comments + if (globalDisableRules.has('*') || globalDisableRules.has(rule.name)) return; + + const ruleMessages = rule.check(context).filter((message) => { + const disableRulesForLine = disableLineRules.get(message.line); + + if (disableRulesForLine && disableRulesForLine.has('*')) { + return false; + } + + return !disableRulesForLine || !disableRulesForLine.has(rule.name); + }); messages.push(...ruleMessages); }); return messages; } + private fixContent(content: string): string { + let fixedContent = content; + const globalDisableRules = extractGlobalDisableRules(fixedContent); + + this.rules.forEach((rule) => { + // Ignore rule from comments + if (globalDisableRules.has('*') || globalDisableRules.has(rule.name)) return; + + if (typeof rule.fix === 'function') { + fixedContent = rule.fix(fixedContent); + } + }); + + return fixedContent; + } + private static validateFile(file: string): { context: LintContext | null; messages: LintMessage[] } { const logger = Logger.getInstance(); const messages: LintMessage[] = []; @@ -105,6 +141,11 @@ class DCLinter { return { context: null, messages }; } + if (startsWithDisableFileComment(context.sourceCode)) { + logger.debug(LOG_SOURCE.LINTER, `Linter disabled for file: ${file}`); + return { context: null, messages }; + } + return { context, messages }; } @@ -159,13 +200,7 @@ class DCLinter { return; } - let content = context.sourceCode; - - this.rules.forEach((rule) => { - if (typeof rule.fix === 'function') { - content = rule.fix(content); - } - }); + const content = this.fixContent(context.sourceCode); if (dryRun) { logger.info(`Dry run - changes for file: ${file}`); diff --git a/src/util/comments-handler.ts b/src/util/comments-handler.ts new file mode 100644 index 0000000..19b1eeb --- /dev/null +++ b/src/util/comments-handler.ts @@ -0,0 +1,57 @@ +function startsWithDisableFileComment(content: string): boolean { + return content.startsWith('# dclint disable-file'); +} + +function extractGlobalDisableRules(content: string): Set { + const disableRules = new Set(); + + // Get the first line and trim whitespace + const firstLine = content.trim().split('\n')[0].trim(); + + // Check if the first line contains "dclint disable" + const disableMatch = firstLine.match(/#\s*dclint\s+disable\s*(.*)/); + if (disableMatch) { + const rules = disableMatch[1].trim(); + + // If no specific rules are provided, disable all rules + if (rules === '') { + disableRules.add('*'); + } else { + // Otherwise, disable specific rules mentioned + rules.split(/\s+/).forEach((rule) => disableRules.add(rule)); + } + } + + return disableRules; +} + +function extractDisableLineRules(content: string): Map> { + const disableRulesPerLine = new Map>(); + const lines = content.split('\n'); + + lines.forEach((line, index) => { + // Check if the line is a comment + const isCommentLine = line.trim().startsWith('#'); + const lineNumber = isCommentLine ? index + 2 : index + 1; + + const disableMatch = line.match(/#\s*dclint\s+disable-line\s*(.*)/); + if (!disableMatch) return; + + const rules = disableMatch[1].trim(); + + if (!disableRulesPerLine.has(lineNumber)) { + disableRulesPerLine.set(lineNumber, new Set()); + } + + // If no specific rule is provided, disable all rules + if (rules === '') { + disableRulesPerLine.get(lineNumber)?.add('*'); + } else { + rules.split(/\s+/).forEach((rule) => disableRulesPerLine.get(lineNumber)?.add(rule)); + } + }); + + return disableRulesPerLine; +} + +export { startsWithDisableFileComment, extractGlobalDisableRules, extractDisableLineRules }; diff --git a/tests/linter.spec.ts b/tests/linter.spec.ts index 3517923..f41bd3f 100644 --- a/tests/linter.spec.ts +++ b/tests/linter.spec.ts @@ -52,6 +52,8 @@ services: image: nginx `; +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); + // @ts-ignore TS2339 test.beforeEach(() => { Logger.init(false); // Initialize logger @@ -86,6 +88,56 @@ test('DCLinter: should lint files correctly', async (t: ExecutionContext) => { t.is(result[0].warningCount, 0, 'There should be no warnings'); }); +// @ts-ignore TS2349 +test('DCLinter: should disable linter for a file', async (t: ExecutionContext) => { + const mockReadFileSync = (): string => `# dclint disable-file + version: '3' + services: + web: + build: . + image: nginx + `; + const mockFindFiles = (): string[] => mockFilePaths; + + const mockWriteFileSync = (filePath: string, content: string): void => { + // Normalize the content by trimming leading/trailing whitespace + // and remove all excess newlines to compare correctly + const originalContent = normalizeYAML(mockReadFileSync()); + const actualContent = normalizeYAML(content); + + t.is(actualContent, originalContent, 'The content should remain unchanged as the rule is disabled'); + }; + + const { DCLinter } = await esmock('../src/linter/linter', { + 'node:fs': { readFileSync: mockReadFileSync, writeFileSync: mockWriteFileSync }, + '../src/util/files-finder': { findFilesForLinting: mockFindFiles }, + }); + const linter = new DCLinter(config); + const result = await linter.lintFiles([mockFilePath], false); + t.is(result[0].messages.length, 0, 'No messages should be present when rule is disabled for part of file'); + + // Call fixFiles method to apply fixes + await linter.fixFiles([mockFilePath], false, false); // Dry run is set to false +}); + +test('DCLinter: should disable specific rule for part of the file', async (t: ExecutionContext) => { + const mockReadFileSync = (): string => `--- # dclint disable require-project-name-field + services: + web: + image: nginx:1.0.0 + build: . # dclint disable-line no-build-and-image + `; + const mockFindFiles = (): string[] => mockFilePaths; + + const { DCLinter } = await esmock('../src/linter/linter', { + 'node:fs': { readFileSync: mockReadFileSync }, + '../src/util/files-finder': { findFilesForLinting: mockFindFiles }, + }); + const linter = new DCLinter(config); + const result = await linter.lintFiles([mockFilePath], false); + t.is(result[0].messages.length, 0, 'No messages should be present when rule is disabled for part of file'); +}); + // @ts-ignore TS2349 test('DCLinter: should lint multiple files correctly', async (t: ExecutionContext) => { const mockFindFiles = (): string[] => mockFilePaths; @@ -143,3 +195,38 @@ test('DCLinter: should fix files', async (t: ExecutionContext) => { t.regex(loggedOutput, /Dry run - changes for file/, 'Dry run should output changes'); t.regex(loggedOutput, /nginx:latest/, 'Dry run output should contain "nginx:latest"'); }); + +// @ts-ignore TS2349 +test('DCLinter: should apply fixes correctly while ignoring disabled rules', async (t: ExecutionContext) => { + const mockReadFileSync = (): string => ` + # dclint disable mock-rule + version: '3' + services: + web: + image: nginx + `; + + const mockFindFiles = (): string[] => mockFilePaths; + const mockLoadLintRules = (): LintRule[] => [mockRule]; + + // eslint-disable-next-line sonarjs/no-identical-functions + const mockWriteFileSync = (filePath: string, content: string): void => { + const originalContent = normalizeYAML(mockReadFileSync()); + const actualContent = normalizeYAML(content); + t.is(actualContent, originalContent, 'The content should remain unchanged as the rule is disabled'); + }; + + const { DCLinter } = await esmock('../src/linter/linter', { + 'node:fs': { readFileSync: mockReadFileSync, writeFileSync: mockWriteFileSync }, + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { findFilesForLinting: mockFindFiles }, + }); + + const linter = new DCLinter(config); + + // Call fixFiles method to apply fixes + await linter.fixFiles([mockFilePath], false, false); // Dry run is set to false + + // Check that the "nginx:latest" is added + // The "mock-rule" rule should be ignored as it was disabled globally in the first line +}); diff --git a/tests/mocks/docker-compose.yml b/tests/mocks/docker-compose.yml index 1de7071..d249a42 100644 --- a/tests/mocks/docker-compose.yml +++ b/tests/mocks/docker-compose.yml @@ -10,6 +10,7 @@ services: environment: - TEST='HQTb_=d.4*FPN@^;w2)UZ%' test: + # dclint disable-line no-build-and-image build: "" image: node container_name: a-service diff --git a/tests/util/comments-handler.spec.ts b/tests/util/comments-handler.spec.ts new file mode 100644 index 0000000..30f4b2c --- /dev/null +++ b/tests/util/comments-handler.spec.ts @@ -0,0 +1,187 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import test from 'ava'; +import { + startsWithDisableFileComment, + extractDisableLineRules, + extractGlobalDisableRules, +} from '../../src/util/comments-handler'; + +// @ts-ignore TS2349 +test('startsWithDisableFileComment should return true when content starts with "# dclint disable-file"', (t) => { + const content = '# dclint disable-file\nversion: "3"'; + t.true( + startsWithDisableFileComment(content), + 'Function should return true if content starts with "# dclint disable-file"', + ); +}); + +// @ts-ignore TS2349 +test('startsWithDisableFileComment should return false when content does not start with "# dclint disable-file"', (t) => { + const content = 'version: "3"\n# dclint disable-file'; + t.false( + startsWithDisableFileComment(content), + 'Function should return false if content does not start with "# dclint disable-file"', + ); +}); + +// @ts-ignore TS2349 +test('startsWithDisableFileComment should return false when content is empty', (t) => { + const content = ''; + t.false(startsWithDisableFileComment(content), 'Function should return false if content is empty'); +}); + +// @ts-ignore TS2349 +test('startsWithDisableFileComment should return false when content starts with different comment', (t) => { + const content = '# some other comment\nversion: "3"'; + t.false( + startsWithDisableFileComment(content), + 'Function should return false if content starts with a different comment', + ); +}); + +// @ts-ignore TS2349 +test('extractGlobalDisableRules should disable all rules if # dclint disable is used without specific rules', (t) => { + const content = ` + # dclint disable + key: value 1 + key: value 2 + `; + const result = extractGlobalDisableRules(content); + t.deepEqual( + [...result], + ['*'], // '*' means all rules disabled + 'Should disable all rules for the entire file (comment in the first line)', + ); +}); + +// @ts-ignore TS2349 +test('extractGlobalDisableRules should disable specific rule if # dclint disable rule-name is used', (t) => { + const content = ` + # dclint disable rule-name + key: value 1 + key: value 2 + `; + const result = extractGlobalDisableRules(content); + t.deepEqual( + [...result], + ['rule-name'], // Only "rule-name" should be disabled + 'Should disable only the "rule-name" rule for the entire file (comment in the first line)', + ); +}); + +// @ts-ignore TS2349 +test('extractGlobalDisableRules should disable multiple specific rules if # dclint disable rule-name another-rule-name is used', (t) => { + const content = ` + # dclint disable rule-name another-rule-name + key: value 1 + key: value 2 + `; + const result = extractGlobalDisableRules(content); + t.deepEqual( + [...result], + ['rule-name', 'another-rule-name'], // Both rules should be disabled + 'Should disable "rule-name" and "another-rule-name" for the entire file (comment in the first line)', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should correctly extract rules for disabling from a comment on the same line', (t) => { + const content = ` + key: value 1 + key: value 2 # dclint disable-line no-quotes-in-volumes + key: value 3 + `; + const result = extractDisableLineRules(content); + t.deepEqual( + [...(result.get(3) || [])], + ['no-quotes-in-volumes'], + 'Should extract the correct rule ("no-quotes-in-volumes") for the second line', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should correctly handle multiple rules for a line on the same line', (t) => { + const content = ` + key: value 1 + key: value 2 # dclint disable-line no-quotes-in-volumes no-unbound-port-interfaces + key: value 3 + `; + const result = extractDisableLineRules(content); + t.deepEqual( + [...(result.get(3) || [])], + ['no-quotes-in-volumes', 'no-unbound-port-interfaces'], + 'Should extract multiple rules ("no-quotes-in-volumes", "no-unbound-port-interfaces") for the second line', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should correctly handle rules from a comment on the previous line', (t) => { + const content = ` + key: value 1 + # dclint disable-line no-quotes-in-volumes + key: value 2 + `; + const result = extractDisableLineRules(content); + + t.deepEqual( + [...(result.get(4) || [])], + ['no-quotes-in-volumes'], + 'Should extract the correct rule ("no-quotes-in-volumes") for the third line (previous comment)', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should correctly handle multiple rules from a comment on the previous line', (t) => { + const content = ` + key: value 1 + # dclint disable-line no-quotes-in-volumes no-unbound-port-interfaces + key: value 2 + `; + const result = extractDisableLineRules(content); + t.deepEqual( + [...(result.get(4) || [])], + ['no-quotes-in-volumes', 'no-unbound-port-interfaces'], + 'Should extract multiple rules ("no-quotes-in-volumes", "no-unbound-port-interfaces") for the third line (previous comment)', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should return an empty set if no disable-line comment is present', (t) => { + const content = ` + key: value 1 + key: value 2 + key: value 3 + `; + const result = extractDisableLineRules(content); + t.deepEqual( + [...(result.get(2) || [])], + [], + 'Should return an empty set if no disable-line comment is present on that line', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should disable all rules for a line with empty disable-line comment', (t) => { + const content = ` + key: value 1 + key: value 2 # dclint disable-line + key: value 3 + `; + const result = extractDisableLineRules(content); + t.deepEqual( + [...(result.get(3) || [])], + ['*'], + 'Should disable all rules when there is no specific rule in the comment', + ); +}); + +// @ts-ignore TS2349 +test('extractDisableLineRules should disable all rules for the next line if the comment is before', (t) => { + const content = ` + key: value 1 + # dclint disable-line + key: value 2 + `; + const result = extractDisableLineRules(content); + t.deepEqual([...(result.get(4) || [])], ['*'], 'Should disable all rules for the line after the comment'); +});