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 b/.eslintrc deleted file mode 100644 index db26f92..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", 4], - "@stylistic/indent-binary-ops": ["error", 4], - "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..2aba164 --- /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', '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/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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7118c8..85d28e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,32 +4,31 @@ 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 + - name: Run Hadolint on Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ./Dockerfile + - name: Run tests run: npm run test:coverage @@ -39,35 +38,146 @@ 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' + 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 Node.js - uses: actions/setup-node@v3 + - name: Set up QEMU for multi-arch + uses: docker/setup-qemu-action@v3 with: - node-version: '20.17.0' + platforms: linux/arm64 + + - 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: + 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 + - build + - build_sea + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # ---------- + # Download and Organize Artifacts + # ---------- - - name: Install dependencies - run: npm ci + - 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: 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 +193,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 +210,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 +253,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/.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 diff --git a/.markdownlint.cjs b/.markdownlint.cjs index 45519ce..caf4955 100644 --- a/.markdownlint.cjs +++ b/.markdownlint.cjs @@ -1,15 +1,22 @@ 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, + 'no-hard-tabs': false, + 'whitespace': false, + 'MD003': { + 'style': 'atx', + }, + 'MD004': { + 'style': 'dash', + }, + 'MD007': { + 'indent': 2, + }, + 'MD013': { + 'line_length': 120, + 'code_blocks': false, + '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..5524885 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,162 @@ 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) + +## 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 @@ -99,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: @@ -124,8 +277,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/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 b91b9fc..3cda818 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,42 @@ 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). + +## 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 @@ -136,19 +171,24 @@ 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). +### 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 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 +208,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 +259,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 { @@ -252,7 +290,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 @@ -274,9 +312,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 +326,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..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/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..cf20b29 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,10 +1,13 @@ 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', + mjs: true, + cjs: true, + js: true, + }, + nodeArguments: ['--import=tsimp/import', '--no-warnings'], + timeout: '2m', + serial: true, + concurrency: 1, }; 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/cli.md b/docs/cli.md index 85cee01..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` @@ -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/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/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-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/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/no-unbound-port-interfaces-rule.md b/docs/rules/no-unbound-port-interfaces-rule.md new file mode 100644 index 0000000..3dd5804 --- /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 [2.0.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/docs/rules/require-quotes-in-ports-rule.md b/docs/rules/require-quotes-in-ports-rule.md index f9715d3..aca66df 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 [2.0.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/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-lock.json b/package-lock.json index 9eafaf8..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", - "yaml": "^2.5.1", + "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", @@ -40,12 +47,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", @@ -60,89 +68,1648 @@ "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/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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/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": { + "@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/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": ">=6" + } + }, + "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": { + "@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": ">=6.9.0" + } + }, + "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": ">=6.9.0" + } + }, + "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": ">=6.9.0" + } + }, + "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": { + "@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": ">=6.9.0" + } + }, + "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/@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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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": { + "@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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "@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" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "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": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/helper-module-imports": "^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" + } + }, + "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": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": ">=6.9.0" + } + }, + "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": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "@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/@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": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.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": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": ">=6.9.0" + } + }, + "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/@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": { + "@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/@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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "@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.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "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": { + "@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/@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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^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-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": { + "@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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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/@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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@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-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": { + "@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.12.0" + } + }, + "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": { + "@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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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, + "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-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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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/@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": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@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/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==", - "engines": { - "node": ">=6.9.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/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/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-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "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==", + "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": { - "color-convert": "^1.9.0" + "regenerator-runtime": "^0.14.0" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "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/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": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "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/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": ">=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/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/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/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": { - "has-flag": "^3.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, "node_modules/@bcoe/v8-coverage": { @@ -440,6 +2007,21 @@ "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", @@ -449,6 +2031,27 @@ "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", @@ -689,76 +2292,386 @@ "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==", + "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": { - "@octokit/openapi-types": "^22.2.0" + "@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/@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/@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, - "optional": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, "engines": { - "node": ">=14" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "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/@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": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">=14.0.0" }, - "funding": { - "url": "https://opencollective.com/unts" + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "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/@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": ">=12.22.0" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "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/@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": { - "graceful-fs": "4.2.10" + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" }, "engines": { - "node": ">=12.22.0" + "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/@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==", + "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": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=12" + "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": { @@ -786,6 +2699,276 @@ "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, + "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, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1267,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", @@ -1294,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", @@ -2136,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", @@ -2191,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": [ { @@ -2209,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" @@ -2222,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", @@ -2299,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": [ { @@ -2316,7 +4573,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/cbor": { "version": "9.0.2", @@ -2334,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" }, @@ -2729,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" } @@ -2736,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", @@ -2747,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", @@ -3096,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", @@ -3203,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", @@ -3232,6 +5518,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 +6074,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 +6260,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 +6317,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 +6540,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", @@ -4311,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", @@ -4511,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", @@ -4630,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", @@ -5112,6 +7544,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", @@ -5367,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", @@ -5448,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", @@ -5675,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", @@ -5839,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", @@ -5875,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", @@ -6098,6 +8578,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", @@ -9339,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", @@ -9472,6 +11960,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", @@ -9607,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", @@ -9861,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", @@ -9888,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", @@ -9900,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", @@ -10010,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", @@ -10415,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", @@ -10620,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", @@ -10705,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", @@ -11249,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", @@ -11674,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" @@ -11722,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", @@ -11731,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", @@ -11774,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": [ { @@ -11792,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 5db50bd..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,27 +36,40 @@ "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", - "lint": "npm run eslint && npm run markdownlint", + "debug:bin": "node ./bin/dclint.cjs ./tests/mocks/docker-compose.correct.yml --fix", "eslint": "eslint .", "eslint:fix": "eslint . --fix", - "markdownlint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#CHANGELOG.md\"", - "markdownlint:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\"", - "markdownlint:fix-changelog": "markdownlint-cli2 --fix \"CHANGELOG.md\"", + "lint": "npm run eslint && npm run markdownlint", + "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\"", "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", - "yaml": "^2.5.1", + "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", @@ -76,12 +90,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/release.config.js b/release.config.js index 3fff4d3..8f79347 100644 --- a/release.config.js +++ b/release.config.js @@ -1,113 +1,136 @@ 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', { name: 'beta', prerelease: true, channel: 'beta' }], + 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/exec', + { + // eslint-disable-next-line no-template-curly-in-string + verifyReleaseCmd: 'echo ${nextRelease.version} > .VERSION', + }, + ], + '@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', + }, + { + 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)', + }, ], - 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/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/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": { 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/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 ceacc66..aefebbd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,153 +1,153 @@ -import { readFileSync, writeFileSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +#!/usr/bin/env node + +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'), 'utf-8'), -) as Record; - -const { argv } = yargsLib(hideBin(process.argv)) +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((packageJson?.version as string) || 'unknown') + .version(process.env.VERSION ?? 'unknown') .command('$0 ', 'Check the files', (yargs) => { - yargs.positional('files', { - describe: 'Files to check', - type: 'string', - array: true, - demandOption: true, - }); + 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, + alias: 'r', + type: 'boolean', + description: 'Recursively search directories for Docker Compose files', + default: false, }) .option('fix', { - type: 'boolean', - description: 'Automatically fix problems', - default: false, + 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, + 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', + 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', + alias: 'c', + type: 'string', + description: 'Path to config file', }) .option('quiet', { - alias: 'q', - type: 'boolean', - description: 'Report errors only', - default: false, + alias: 'q', + type: 'boolean', + description: 'Report errors only', + default: false, }) .option('output-file', { - alias: 'o', - type: 'string', - description: 'Specify file to write report to', + alias: 'o', + type: 'string', + description: 'Specify file to write report to', }) .option('color', { - type: 'boolean', - description: 'Force enabling/disabling of color', - default: true, + type: 'boolean', + description: 'Force enabling/disabling of color', + default: true, }) .option('debug', { - type: 'boolean', - description: 'Output debugging information', - default: undefined, + type: 'boolean', + description: 'Output debugging information', + default: false, }) .option('exclude', { - alias: 'e', - type: 'array', - description: 'Files or directories to exclude from the search', - default: [], + 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); - } - - // 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); - } - - const isValid = lintResults.filter((result) => result.messages.length > 0).length === 0; - - if (!isValid) { - logger.debug(LOG_SOURCE.CLI, `${lintResults.length} errors found`); - process.exit(1); - } - - logger.debug(LOG_SOURCE.CLI, 'All files are valid.'); - process.exit(0); + const cliArguments = argv as unknown as CLIConfig; + + Logger.init(cliArguments.debug); + const logger = Logger.getInstance(); + + logger.debug(LOG_SOURCE.CLI, 'Debug mode is ON'); + logger.debug(LOG_SOURCE.CLI, 'Arguments:', cliArguments); + + const config = await loadConfig(cliArguments.config).catch((error) => { + process.exit(1); + }); + + 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); + + if (cliArguments.fix || cliArguments.fixDryRun) { + await linter.fixFiles(cliArguments.files, cliArguments.recursive, cliArguments.fixDryRun); + } + + let lintResults = await linter.lintFiles(cliArguments.files, cliArguments.recursive); + + if (cliArguments.quiet) { + 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); + } + + const totalErrors = lintResults.reduce((count, result) => count + result.errorCount, 0); + const totalWarnings = lintResults.reduce((count, result) => count + result.warningCount, 0); + + const formattedResults = await linter.formatResults(lintResults, cliArguments.formatter); + + if (cliArguments.outputFile) { + writeFileSync(cliArguments.outputFile, formattedResults); + } else { + console.log(formattedResults); + } + + if (totalErrors > 0) { + logger.debug(LOG_SOURCE.CLI, `${totalErrors} errors found`); + process.exit(1); + } else if (cliArguments.maxWarnings >= 0 && totalWarnings > cliArguments.maxWarnings) { + logger.debug( + LOG_SOURCE.CLI, + `Warning threshold exceeded: ${totalWarnings} warnings (max allowed: ${cliArguments.maxWarnings})`, + ); + process.exit(1); + } + + logger.debug(LOG_SOURCE.CLI, 'All files are valid.'); + 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 5da1b08..a965758 100644 --- a/src/cli/cli.types.ts +++ b/src/cli/cli.types.ts @@ -1,15 +1,16 @@ interface CLIConfig { - files: string[]; - recursive: boolean; - fix: boolean; - fixDryRun: boolean; - formatter: string; - config?: string; - quiet: boolean; - 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..0aa1d24 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,47 +1,48 @@ 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 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 { - 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 = schemaLoader('linter-config'); + const validate = ajv.compile(schema); + + if (!validate(config)) { + logger.error('Invalid configuration:', validate.errors); + throw new ConfigValidationError(validate.errors); + } + 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..8e6adc3 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(error: ErrorObject) { + super(`Validation error: ${error?.message}`); + this.name = 'ComposeValidationError'; + this.keyword = error.keyword; + this.instancePath = error.instancePath; + this.schemaPath = error.schemaPath; + this.details = error; + } - 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/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/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..24dd836 100644 --- a/src/formatters/codeclimate.ts +++ b/src/formatters/codeclimate.ts @@ -1,63 +1,66 @@ 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'); + 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 + 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(); - 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..d2af3cb 100644 --- a/src/formatters/compact.ts +++ b/src/formatters/compact.ts @@ -1,13 +1,13 @@ -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; 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/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 baa1073..2c6d83d 100644 --- a/src/formatters/json.ts +++ b/src/formatters/json.ts @@ -1,5 +1,6 @@ -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; export default function jsonFormatter(results: LintResult[]): string { - return JSON.stringify(results, null, 2); + // 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 c41419e..70b2e58 100644 --- a/src/formatters/junit.ts +++ b/src/formatters/junit.ts @@ -1,46 +1,46 @@ -import type { LintResult } from '../linter/linter.types.js'; +import type { LintResult } from '../linter/linter.types'; 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.replaceAll(/[<>&'"]/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..7d63e37 100644 --- a/src/formatters/stylish.ts +++ b/src/formatters/stylish.ts @@ -1,65 +1,64 @@ -import path from 'node:path'; -import chalk from 'chalk'; -import type { LintResult } from '../linter/linter.types.js'; +import { resolve } from 'node:path'; +import pc from 'picocolors'; +import type { LintResult } from '../linter/linter.types'; 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`; + const filePath = pc.underline(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((message) => { + const { type } = message; + 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 formattedType = color(type); - const ruleInfo = chalk.dim(msg.rule); + const position = pc.dim(`${line}:${column}`); + const formattedType = color(type); + const ruleInfo = pc.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) { - fixableErrorCount += 1; - } - } else { - warningCount += 1; - if (msg.fixable) { - fixableWarningCount += 1; - } - } - }); + if (type === 'error') { + errorCount += 1; + if (message.fixable) { + fixableErrorCount += 1; + } + } else { + warningCount += 1; + if (message.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 = 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.`, - ); - output += `${fixableSummary}\n`; - } + if (fixableErrorCount > 0 || fixableWarningCount > 0) { + const fixableSummary = pc.green( + pc.bold( + `${fixableErrorCount} errors and ${fixableWarningCount} warnings potentially fixable with the \`--fix\` option.`, + ), + ); + output += `${fixableSummary}\n`; + } - return output; + return output; } diff --git a/src/index.ts b/src/index.ts index 39c8f5b..ddd9767 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'; diff --git a/src/linter/linter.ts b/src/linter/linter.ts index f09a54f..0aa969a 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -1,173 +1,222 @@ 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'; +import { + extractDisableLineRules, + extractGlobalDisableRules, + startsWithDisableFileComment, +} from '../util/comments-handler'; + +const DEFAULT_CONFIG: Config = { + debug: false, + exclude: [], + rules: {}, + quiet: false, +}; 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 = { ...DEFAULT_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); - }); + // Get globally disabled rules (from the first line) + const globalDisableRules = extractGlobalDisableRules(context.sourceCode); + const disableLineRules = extractDisableLineRules(context.sourceCode); - return messages; - } + this.rules.forEach((rule) => { + // 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); - 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 }; + if (disableRulesForLine && disableRulesForLine.has('*')) { + return false; } - return { context, messages }; - } + 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; - 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 (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[] = []; + const context: LintContext = { path: file, content: {}, sourceCode: '' }; + + try { + context.sourceCode = fs.readFileSync(file, 'utf8'); + const parsedDocument = parseDocument(context.sourceCode, { merge: true }); + + if (parsedDocument.errors && parsedDocument.errors.length > 0) { + parsedDocument.errors.forEach((error) => { + throw error; + }); + } + + // Use Record to type the parsed content safely + context.content = parsedDocument.toJS() as Record; + validationComposeSchema(context.content); + } 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', + severity: 'critical', + message: 'Invalid YAML format.', + line: startPos?.line || 1, + column: startPos?.col || 1, + type: 'error', + fixable: false, }); + } else if (error instanceof ComposeValidationError) { + messages.push({ + rule: 'invalid-schema', + type: 'error', + category: 'style', + severity: 'critical', + message: error.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}`, error); + } - 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}`); - } - }); + if (startsWithDisableFileComment(context.sourceCode)) { + logger.debug(LOG_SOURCE.LINTER, `Linter disabled for file: ${file}`); + return { context: null, messages }; } + + 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((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, + 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; + } + + const content = this.fixContent(context.sourceCode); + + 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}`); + } + }); + } + + // 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/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/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 bca63ac..8d20774 100644 --- a/src/rules/no-build-and-image-rule.ts +++ b/src/rules/no-build-and-image-rule.ts @@ -1,68 +1,82 @@ import { parseDocument, isMap, isScalar } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; + +interface NoBuildAndImageRuleOptions { + 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 category: LintRuleCategory = 'best-practice'; + + public severity: LintRuleSeverity = 'major'; - public type: LintMessageType = 'error'; + 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 category: LintRuleCategory = 'best-practice'; + public fixable: boolean = false; - public severity: LintRuleSeverity = 'major'; + private readonly checkPullPolicy: boolean; - 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', - }; + constructor(options?: NoBuildAndImageRuleOptions) { + this.checkPullPolicy = options?.checkPullPolicy ?? true; + } - public fixable: boolean = false; + // 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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'); - if (service.has('build') && service.has('image')) { - 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(parsedDocument, 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..a00e148 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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..f2b59e7 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; -import { extractPublishedPortValue, parsePortsRange } from '../util/service-ports-parser.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} 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'; - - 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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..ae8277c 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, -} from '../linter/linter.types.js'; -import { findLineNumberByValue } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberByValue } from '../util/line-finder'; 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(document: ParsedNode | null, callback: (volume: Scalar) => void) { + if (!document || !isMap(document)) return; - doc.items.forEach((item) => { - if (!isMap(item.value)) return; + document.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((node) => isScalar(node.key) && node.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 parsedDocument = parseDocument(context.sourceCode); + + NoQuotesInVolumesRule.extractVolumes(parsedDocument.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 parsedDocument = parseDocument(content); - NoQuotesInVolumesRule.extractVolumes(doc.contents, (volume) => { - if (volume.type !== 'PLAIN') { - // eslint-disable-next-line no-param-reassign - volume.type = 'PLAIN'; - } - }); + 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/no-unbound-port-interfaces-rule.ts b/src/rules/no-unbound-port-interfaces-rule.ts new file mode 100644 index 0000000..099ff00 --- /dev/null +++ b/src/rules/no-unbound-port-interfaces-rule.ts @@ -0,0 +1,78 @@ +import { parseDocument, isMap, isSeq, isScalar } from 'yaml'; +import type { + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} 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'; + + public type: LintMessageType = 'error'; + + public category: LintRuleCategory = 'security'; + + public severity: LintRuleSeverity = 'major'; + + public meta: RuleMeta = { + description: + '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, 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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 = extractPublishedPortInterfaceValue(portItem); + + if (publishedInterface === '') { + 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, + port: isSeq(portItem) ? portItem.toString() : String(portItem), + }), + line, + column: 1, + meta: this.meta, + fixable: this.fixable, + }); + } + }); + }); + return errors; + } +} diff --git a/src/rules/no-version-field-rule.ts b/src/rules/no-version-field-rule.ts index d5afb6c..c4e73af 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, -} from '../linter/linter.types.js'; -import { findLineNumberByKey } from '../util/line-finder.js'; + LintRule, + LintMessage, + LintRuleCategory, + RuleMeta, + LintRuleSeverity, + LintMessageType, + LintContext, +} from '../linter/linter.types'; +import { findLineNumberByKey } from '../util/line-finder'; 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..a997dd7 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, -} from '../linter/linter.types.js'; + LintRule, + LintMessage, + LintRuleCategory, + RuleMeta, + LintRuleSeverity, + LintMessageType, + LintContext, +} from '../linter/linter.types'; 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..09304c6 100644 --- a/src/rules/require-quotes-in-ports-rule.ts +++ b/src/rules/require-quotes-in-ports-rule.ts @@ -1,106 +1,124 @@ -import { parseDocument, isMap, isSeq, isScalar, Scalar, ParsedNode } from 'yaml'; +import { parseDocument, isMap, isSeq, isScalar, Scalar, ParsedNode, Pair } from 'yaml'; import type { - LintContext, - LintMessage, - LintMessageType, - LintRule, - LintRuleCategory, - LintRuleSeverity, - RuleMeta, -} from '../linter/linter.types.js'; -import { findLineNumberByValue } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; interface RequireQuotesInPortsRuleOptions { - quoteType: 'single' | 'double'; + quoteType: 'single' | 'double'; } export default class RequireQuotesInPortsRule implements LintRule { - public name = 'require-quotes-in-ports'; - - public type: LintMessageType = 'warning'; - - public category: LintRuleCategory = 'best-practice'; - - 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 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.'; - } - - private readonly quoteType: 'single' | 'double'; - - constructor(options?: RequireQuotesInPortsRuleOptions) { - this.quoteType = options?.quoteType || 'single'; - } - - 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; - - doc.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'); - if (!ports || !isSeq(ports.value)) return; - - 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 name = 'require-quotes-in-ports'; - return errors; - } + public type: LintMessageType = 'warning'; - public fix(content: string): string { - const doc = parseDocument(content); + public category: LintRuleCategory = 'best-practice'; - RequireQuotesInPortsRule.extractPorts(doc.contents, (port) => { - if (port.type !== this.getQuoteType()) { - // eslint-disable-next-line no-param-reassign - port.type = this.getQuoteType(); - } - }); + public severity: LintRuleSeverity = 'minor'; + + public meta: RuleMeta = { + 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', + }; + + public fixable: boolean = true; + + // eslint-disable-next-line class-methods-use-this + public getMessage(): string { + return 'Ports in `ports` and `expose` sections should be enclosed in quotes.'; + } + + private readonly quoteType: 'single' | 'double'; + + private readonly portsSections: string[]; - return doc.toString(); - } + 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 values + private static extractValues( + document: ParsedNode | null, + section: string, + callback: (service: Pair, port: Scalar) => void, + ) { + if (!document || !isMap(document)) return; + + document.items.forEach((item) => { + if (!isMap(item.value)) return; + + const serviceMap = item.value; + serviceMap.items.forEach((service) => { + if (!isMap(service.value)) return; + + 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); + } + }); + } + }); + }); + } + + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const parsedDocument = parseDocument(context.sourceCode); + + 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; + } + + public fix(content: string): string { + const parsedDocument = parseDocument(content); + + this.portsSections.forEach((section) => { + RequireQuotesInPortsRule.extractValues(parsedDocument.contents, section, (service, port) => { + if (port.type !== this.getQuoteType()) { + const newPort = new Scalar(String(port.value)); + newPort.type = this.getQuoteType(); + Object.assign(port, newPort); + } + }); + }); + + return parsedDocument.toString(); + } } diff --git a/src/rules/service-container-name-regex-rule.ts b/src/rules/service-container-name-regex-rule.ts index 85671d2..b245986 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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..03b5f11 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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 parsedDocument = parseDocument(content); + const services = parsedDocument.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 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 8ccf0e6..9a3a8de 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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..99df89e 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; 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]), ...otherKeys]; + } - if (!isMap(services)) return []; + public check(context: LintContext): LintMessage[] { + const errors: LintMessage[] = []; + const parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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 parsedDocument = parseDocument(content); + const services = parsedDocument.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((node) => isScalar(node.key) && node.key.value === key); + if (item) { + orderedService.add(item); + } + }); + + keys.forEach((key) => { + if (!correctOrder.includes(key)) { + const item = service.items.find((node) => isScalar(node.key) && node.key.value === key); + if (item) { + orderedService.add(item); + } + } + }); - services.set(serviceName, orderedService); - }); + 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 a12de80..bf67a5b 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; -import { extractPublishedPortValue } from '../util/service-ports-parser.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} 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'; + 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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 parsedDocument = parseDocument(content); + const services = parsedDocument.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 parsedDocument.toString(); + } } diff --git a/src/rules/services-alphabetical-order-rule.ts b/src/rules/services-alphabetical-order-rule.ts index 9fbf203..5c2508c 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, -} from '../linter/linter.types.js'; -import { findLineNumberForService } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberForService } from '../util/line-finder'; 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 parsedDocument = parseDocument(context.sourceCode); + const services = parsedDocument.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(parsedDocument, 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 parsedDocument = parseDocument(content); + const services = parsedDocument.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); + 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 f819944..24cb5c4 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, -} from '../linter/linter.types.js'; -import { findLineNumberByKey } from '../util/line-finder.js'; + LintContext, + LintMessage, + LintMessageType, + LintRule, + LintRuleCategory, + LintRuleSeverity, + RuleMeta, +} from '../linter/linter.types'; +import { findLineNumberByKey } from '../util/line-finder'; 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 parsedDocument = parseDocument(content); + const { contents } = parsedDocument; - 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((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/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/src/util/compose-validation.ts b/src/util/compose-validation.ts index 6adf8f8..fe4987e 100644 --- a/src/util/compose-validation.ts +++ b/src/util/compose-validation.ts @@ -1,46 +1,46 @@ 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; 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 = schemaLoader('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..7cb50f6 100644 --- a/src/util/files-finder.ts +++ b/src/util/files-finder.ts @@ -1,76 +1,76 @@ 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(); - 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 = [...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((fileOrDirectory) => { + if (!fs.existsSync(fileOrDirectory)) { + logger.debug('UTIL', `File or directory not found: ${fileOrDirectory}`); + throw new FileNotFoundError(fileOrDirectory); + } - let allPaths: string[] = []; + let allPaths: string[] = []; - const fileOrDirStats = fs.statSync(fileOrDir); + const fileOrDirectoryStats = fs.statSync(fileOrDirectory); - 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 (fileOrDirectoryStats.isDirectory()) { + try { + allPaths = fs.readdirSync(resolve(fileOrDirectory)).map((f) => join(fileOrDirectory, f)); + } catch (error) { + logger.debug('UTIL', `Error reading directory: ${fileOrDirectory}`, 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, ...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 (fileOrDirectoryStats.isFile()) { + filesToCheck.push(fileOrDirectory); + } + }); - 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..7a5de91 100644 --- a/src/util/formatter-loader.ts +++ b/src/util/formatter-loader.ts @@ -1,50 +1,42 @@ -import path from 'node:path'; -import type { LintResult } from '../linter/linter.types.js'; -import { Logger } from './logger.js'; +import { resolve } from 'node:path'; +import type { LintResult } from '../linter/linter.types'; +import { Logger } from './logger'; +import Formatters from '../formatters/index'; 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 { + 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 = 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 formatterFunction = Formatters[`${formatterName}Formatter` as keyof typeof Formatters]; + if (formatterFunction) { + logger.debug('UTIL', `Using built-in formatter: ${formatterName}`); + return formatterFunction as FormatterFunction; + } + + logger.warn(`Unknown formatter: ${formatterName}. Using default - stylish.`); + return Formatters.stylishFormatter; } diff --git a/src/util/line-finder.ts b/src/util/line-finder.ts index e1fb891..dd13204 100644 --- a/src/util/line-finder.ts +++ b/src/util/line-finder.ts @@ -8,15 +8,17 @@ 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 - } + let lineNumber = 1; // Lines start from 1, not 0 + for (const line of lines) { + if (regex.test(line)) { + return lineNumber; } - return 1; // Default to 1 if the key is not found + lineNumber += 1; + } + return 1; // Default to 1 if the key is not found } /** @@ -26,67 +28,31 @@ 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; -} - -/** - * 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 doc 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(doc: Document, content: string, serviceName: string, key: string): number { - const services = doc.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; + const lineIndex = content.split('\n').findIndex((line) => line.includes(value)); + return lineIndex === -1 ? 1 : lineIndex + 1; } /** * 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; } /** @@ -95,7 +61,7 @@ function getKeyLine(keyNode: Node, content: string): number { * If the key is provided without a value, it returns the line number for the key. * If both key and value are provided, it searches for the specific key-value pair within the service. * - * @param doc The YAML content parsed with lib yaml. + * @param document * @param content The parsed YAML content as a string. * @param serviceName The name of the service to search for. * @param key The optional key to search for in the service. @@ -103,65 +69,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, + document: Document, + content: string, + serviceName: string, + key?: string, + value?: string, ): number { - 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; - } - - // 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; - } - - // 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; - } - - return 1; - }); - } + const services = document.get('services') as Node; + if (!isMap(services)) { + return 1; + } - if (isMap(keyNode)) { - keyNode.items.forEach((item) => { - const keyItem = item.key; - const valueItem = item.value; + // Locate Service + const service = services.get(serviceName) as Node; + if (!isMap(service)) { + return 1; + } - if (isScalar(keyItem) && isScalar(valueItem) && valueItem.value === value && valueItem.range) { - const [start] = valueItem.range; - return content.slice(0, start).split('\n').length; - } + // If the key is not provided, it returns the line number for the service block + if (!key) { + return getServiceStartLine(service, content); + } - 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 (isSeq(keyNode)) { + let line = 1; + keyNode.items.forEach((item) => { + if (isScalar(item) && String(item.value) === String(value) && item.range) { + const [start] = item.range; + line = content.slice(0, start).split('\n').length; + } + }); + 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) && String(valueItem.value) === String(value) && valueItem.range) { + const [start] = valueItem.range; + line = content.slice(0, start).split('\n').length; + } + }); + return line; + } - 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 }; +export { findLineNumberByKey, findLineNumberByValue, findLineNumberForService }; diff --git a/src/util/load-schema.ts b/src/util/load-schema.ts deleted file mode 100644 index 3b84629..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`), 'utf-8'), - ) as Record; -} - -export { loadSchema }; diff --git a/src/util/logger.ts b/src/util/logger.ts index 434d6c9..7211a9c 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -1,84 +1,84 @@ -import chalk from 'chalk'; +import pc from 'picocolors'; // 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 pc.blue('[DEBUG]'); + case 'INFO': + return pc.green('[INFO]'); + case 'WARN': + return pc.yellow('[WARN]'); + case 'ERROR': + return pc.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, ...options: unknown[]): void { + if (this.debugMode) { + const message = Logger.formatMessage('DEBUG', source); + console.debug(message, ...options); } + } - // 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(...options: unknown[]): void { + const message = Logger.formatMessage('INFO'); + console.info(message, ...options); + } - // 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(...options: unknown[]): void { + const message = Logger.formatMessage('WARN'); + console.warn(message, ...options); + } - // 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(...options: unknown[]): void { + const message = Logger.formatMessage('ERROR'); + console.error(message, ...options); + } } export { Logger }; diff --git a/src/util/rules-loader.ts b/src/util/rules-loader.ts index 6857080..e9b59c9 100644 --- a/src/util/rules-loader.ts +++ b/src/util/rules-loader.ts @@ -1,70 +1,45 @@ -import path 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 { - 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; - } -} +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 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'))); - - // Parallel import with Promise.all - const ruleInstances: (LintRule | null)[] = await Promise.all( - ruleFiles.map(async (file) => importRule(file, rulesDir)), - ); + const logger = Logger.getInstance(); + const activeRules: LintRule[] = []; - const activeRules: LintRule[] = []; - - ruleInstances.forEach((ruleInstance) => { - if (!ruleInstance) return; - - const ruleConfig: ConfigRule = config.rules[ruleInstance.name]; - - let ruleLevel: ConfigRuleLevel; - let ruleOptions: Record | undefined; + for (const RuleClass of Object.values(Rules)) { + try { + const ruleInstance = new (RuleClass as new () => LintRule)(); + const ruleConfig: ConfigRule = config.rules[ruleInstance.name]; - if (Array.isArray(ruleConfig)) { - [ruleLevel, ruleOptions] = ruleConfig; - } else { - ruleLevel = ruleConfig; - } + let ruleLevel: ConfigRuleLevel; + let ruleOptions: Record | undefined; - if (ruleLevel === 0) return; + if (Array.isArray(ruleConfig)) { + [ruleLevel, ruleOptions] = ruleConfig; + } else { + ruleLevel = ruleConfig; + } - const RuleClass = ruleInstance.constructor as new (options?: Record) => LintRule; - const instance = ruleOptions ? new RuleClass(ruleOptions) : new RuleClass(); + if (ruleLevel !== 0) { + const instance = ruleOptions + ? new (RuleClass as new (options?: Record) => LintRule)(ruleOptions) + : ruleInstance; const typeMap: { [key: number]: LintMessageType } = { - 1: 'warning', - 2: 'error', + 1: 'warning', + 2: 'error', }; instance.type = typeMap[ruleLevel] || instance.type; - activeRules.push(instance); - }); + } + } catch (error) { + logger.error(`Error loading rule: ${RuleClass?.name}`, error); + } + } - return activeRules; + return activeRules; } export { loadLintRules }; 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/src/util/service-ports-parser.ts b/src/util/service-ports-parser.ts index 91eed92..d138dff 100644 --- a/src/util/service-ports-parser.ts +++ b/src/util/service-ports-parser.ts @@ -1,48 +1,78 @@ -import net from 'net'; +import net from 'node: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]); - } + // Check for host before ports + const parts = value.split(/:(?![^[]*])/); - return parts[0]; + if (parts[0].startsWith('[') && parts[0].endsWith(']')) { + parts[0] = parts[0].slice(1, -1); } - if (isMap(yamlNode)) { - return String(yamlNode.get('published')) || ''; + if (net.isIP(parts[0])) { + return String(parts[1]); } - return ''; + return parts[0]; + } + + if (isMap(yamlNode)) { + return yamlNode.get('published')?.toString() || ''; + } + + return ''; } -function parsePortsRange(port: string): string[] { - const [start, end] = port.split('-').map(Number); +function extractPublishedPortInterfaceValue(yamlNode: unknown): string { + if (isScalar(yamlNode)) { + const value = String(yamlNode.value); - if (Number.isNaN(start) || Number.isNaN(end)) { - return []; - } + // Split on single colon + const parts = value.split(/:(?![^[]*])/); - if (!end) { - return [start.toString()]; + if (parts[0].startsWith('[') && parts[0].endsWith(']')) { + parts[0] = parts[0].slice(1, -1); } - if (start > end) { - // Invalid port range: start port is greater than end port - return []; + if (net.isIP(parts[0])) { + return String(parts[0]); } - const ports: string[] = []; - // eslint-disable-next-line no-plusplus - for (let i = start; i <= end; i++) { - ports.push(i.toString()); - } - return ports; + return ''; + } + + if (isMap(yamlNode)) { + return yamlNode.get('host_ip')?.toString() || ''; + } + + 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,unicorn/prevent-abbreviations + for (let i = start; i <= end; i++) { + ports.push(i.toString()); + } + return ports; } -export { extractPublishedPortValue, parsePortsRange }; +export { extractPublishedPortValue, extractPublishedPortInterfaceValue, parsePortsRange }; diff --git a/tests/linter.spec.ts b/tests/linter.spec.ts index f188453..f41bd3f 100644 --- a/tests/linter.spec.ts +++ b/tests/linter.spec.ts @@ -1,44 +1,45 @@ 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 = { - 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 @@ -51,90 +52,181 @@ services: image: nginx `; +const normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); + +// @ts-ignore TS2339 test.beforeEach(() => { - Logger.init(false); // Initialize logger + Logger.init(false); // Initialize logger +}); + +// @ts-ignore TS2349 +test('DCLinter: should lint files correctly', async (t: ExecutionContext) => { + 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', { + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { 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'); +}); + +// @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'); }); -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'); +// @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; + + // 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', { + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { 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 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'); +// @ts-ignore TS2349 +test('DCLinter: should fix files', async (t: ExecutionContext) => { + 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', { + '../src/util/rules-loader': { loadLintRules: mockLoadLintRules }, + '../src/util/files-finder': { 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"'); }); -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"'); +// @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.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/mocks/docker-compose.yml b/tests/mocks/docker-compose.yml index d43b7e9..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 @@ -35,7 +36,10 @@ services: command: sh -c "tail -f /dev/null" ports: - "11150:3000" - - "11032:3000" + - "127.0.0.1:11032:3000" + - 3000 + expose: + - 3000 b-service: build: context: ../../app/b-service diff --git a/tests/rules/no-build-and-image-rule.spec.ts b/tests/rules/no-build-and-image-rule.spec.ts index 5e5d484..3c07397 100644 --- a/tests/rules/no-build-and-image-rule.spec.ts +++ b/tests/rules/no-build-and-image-rule.spec.ts @@ -1,7 +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 = ` @@ -14,6 +15,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: @@ -34,47 +48,97 @@ 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.'); - - 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])); - }); +// @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, + 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])); + }); +}); + +// @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, + 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])); + }); +}); + +// @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, + 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, - }; +// @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, + 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 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, - }; +// @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, + 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 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..e634983 100644 --- a/tests/rules/no-duplicate-container-names-rule.spec.ts +++ b/tests/rules/no-duplicate-container-names-rule.spec.ts @@ -1,7 +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 = ` @@ -25,30 +26,32 @@ services: container_name: db_container `; -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, - }; +// @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', + 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, - }; +// @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', + 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..336f5d1 100644 --- a/tests/rules/no-duplicate-exported-ports-rule.spec.ts +++ b/tests/rules/no-duplicate-exported-ports-rule.spec.ts @@ -1,7 +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 = ` @@ -101,59 +102,62 @@ 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, - }; +// @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, + 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, - }; +// @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, + 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, - }; +// @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, + 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..9c77660 100644 --- a/tests/rules/no-quotes-in-volumes-rule.spec.ts +++ b/tests/rules/no-quotes-in-volumes-rule.spec.ts @@ -1,6 +1,7 @@ import test from 'ava'; -import NoQuotesInVolumesRule from '../../src/rules/no-quotes-in-volumes-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import type { ExecutionContext } from 'ava'; +import NoQuotesInVolumesRule from '../../src/rules/no-quotes-in-volumes-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const correctYAML = ` @@ -17,44 +18,48 @@ services: - "data" `; -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, - }; +// @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', + 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 errors = rule.check(context); + 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) => { - 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'); +// @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', + 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); +// @ts-ignore TS2349 +test('NoQuotesInVolumesRule: should fix YAML with quotes in volumes', (t: ExecutionContext) => { + 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); +// @ts-ignore TS2349 +test('NoQuotesInVolumesRule: should not modify YAML without quotes in volumes', (t: ExecutionContext) => { + 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-unbound-port-interfaces-rule.spec.ts b/tests/rules/no-unbound-port-interfaces-rule.spec.ts new file mode 100644 index 0000000..f89e194 --- /dev/null +++ b/tests/rules/no-unbound-port-interfaces-rule.spec.ts @@ -0,0 +1,73 @@ +import test from 'ava'; +import { parseDocument } from 'yaml'; +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 = ` +services: + a-service: + image: nginx + ports: + - 8080:80 + - 8080 + b-service: + image: nginx + ports: + - target: 1000 + published: 8081 + protocol: tcp + mode: host +`; + +// 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('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, + sourceCode: yamlWithImplicitListenEverywherePorts, + }; + + const errors = rule.check(context); + 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: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) => { + t.true(error.message.includes(expectedMessages[index])); + }); +}); + +// @ts-ignore TS2349 +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, + sourceCode: yamlWithExplicitListenIPPorts, + }; + + const errors = rule.check(context); + t.is(errors.length, 0, 'There should not be any errors.'); +}); diff --git a/tests/rules/no-version-field-rule.spec.ts b/tests/rules/no-version-field-rule.spec.ts index c6e501d..6172fb8 100644 --- a/tests/rules/no-version-field-rule.spec.ts +++ b/tests/rules/no-version-field-rule.spec.ts @@ -1,6 +1,7 @@ import test from 'ava'; -import NoVersionFieldRule from '../../src/rules/no-version-field-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import type { ExecutionContext } from 'ava'; +import NoVersionFieldRule from '../../src/rules/no-version-field-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithVersion = ` @@ -16,56 +17,60 @@ services: image: nginx `; -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', - }, - }, +// @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', + 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', - }, - }, +// @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', + 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); +// @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.'); + 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); +// @ts-ignore TS2349 +test('NoVersionFieldRule: should not modify YAML without "version" field', (t: ExecutionContext) => { + 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..b4bcf61 100644 --- a/tests/rules/require-project-name-field-rule.spec.ts +++ b/tests/rules/require-project-name-field-rule.spec.ts @@ -1,6 +1,7 @@ import test from 'ava'; -import RequireProjectNameFieldRule from '../../src/rules/require-project-name-field-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import type { ExecutionContext } from 'ava'; +import RequireProjectNameFieldRule from '../../src/rules/require-project-name-field-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests const yamlWithName = ` @@ -16,42 +17,44 @@ services: image: nginx `; -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', - }, - }, +// @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', + 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', - }, - }, +// @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', + 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..1ebb9c2 100644 --- a/tests/rules/require-quotes-in-ports-rule.spec.ts +++ b/tests/rules/require-quotes-in-ports-rule.spec.ts @@ -1,98 +1,133 @@ import test from 'ava'; -import RequireQuotesInPortsRule from '../../src/rules/require-quotes-in-ports-rule.js'; -import type { LintContext } from '../../src/linter/linter.types.js'; +import type { ExecutionContext } from 'ava'; +import RequireQuotesInPortsRule from '../../src/rules/require-quotes-in-ports-rule'; +import type { LintContext } from '../../src/linter/linter.types'; // Sample YAML for tests 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'; -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'); -}); +// @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, + content: {}, + sourceCode: yamlWithoutQuotes, + }; -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, 3, 'There should be one warning when ports are not quoted.'); - const errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when ports are quoted with single quotes.'); + const expectedMessage = 'Ports in `ports` and `expose` sections should be enclosed in quotes.'; + + errors.forEach((error, index) => { + t.true(error.message.includes(expectedMessage)); + }); }); -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, - }; +// @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, + 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 errors = rule.check(context); - t.is(errors.length, 0, 'There should be no warnings when ports are quoted with double quotes.'); +// @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, + 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' }); +// @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); - 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.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithSingleQuotes), + 'The Ports in `ports` and `expose` sections should be quoted with single quotes.', + ); }); -test('RequireQuotesInPortsRule: should fix double quotes ports by changing them to single quotes', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'single' }); +// @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'`), '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(yamlWithDoubleQuotes); + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithSingleQuotes), + 'The Ports in `ports` and `expose` sections should be quoted with single quotes.', + ); }); -test('RequireQuotesInPortsRule: should fix unquoted ports by adding double quotes and not modify already quoted ports', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); +// @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); - 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.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithDoubleQuotes), + 'The Ports in `ports` and `expose` sections should be quoted with double quotes.', + ); }); -test('RequireQuotesInPortsRule: should fix single quotes ports by changing them to double quotes', (t) => { - const rule = new RequireQuotesInPortsRule({ quoteType: 'double' }); +// @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); - 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.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithDoubleQuotes), + 'The Ports in `ports` and `expose` sections should be quoted with double 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..5f840dc 100644 --- a/tests/rules/service-container-name-regex-rule.spec.ts +++ b/tests/rules/service-container-name-regex-rule.spec.ts @@ -1,7 +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 = ` @@ -19,30 +20,32 @@ services: container_name: "my-app-123" `; -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, - }; +// @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', + 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, - }; +// @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', + 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..b7b9440 100644 --- a/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-dependencies-alphabetical-order-rule.spec.ts @@ -1,7 +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 = ` @@ -48,80 +49,86 @@ 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) => { - 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.`)); +// @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, + 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.'); +// @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, + 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); +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of short syntax services', (t: ExecutionContext) => { + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectShortSyntax); - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectShortSyntax), - 'The short syntax services should be reordered alphabetically.', - ); + 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.`)); +// @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, + 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.'); +// @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, + 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); +// @ts-ignore TS2349 +test('ServiceDependenciesAlphabeticalOrderRule: should fix the order of long syntax services', (t: ExecutionContext) => { + const rule = new ServiceDependenciesAlphabeticalOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectLongSyntax); - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectLongSyntax), - 'The long syntax services should be reordered alphabetically.', - ); + 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..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,7 +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 = ` @@ -93,130 +94,136 @@ 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])); - }); +// @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, + 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])); - }); +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when using latest tag', (t: ExecutionContext) => { + 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])); - }); +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when using stable tag', (t: ExecutionContext) => { + 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])); - }); +// @ts-ignore TS2349 +test('ServiceImageRequireExplicitTagRule: should return a warning when using prohibited tags', (t: ExecutionContext) => { + 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'] }); +// @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 = { - 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.'); +// @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, + 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..b6da044 100644 --- a/tests/rules/service-keys-order-rule.spec.ts +++ b/tests/rules/service-keys-order-rule.spec.ts @@ -1,7 +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 = ` @@ -37,50 +38,53 @@ 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) => { - const rule = new ServiceKeysOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, - sourceCode: yamlWithIncorrectOrder, - }; +// @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', + 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, - }; +// @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', + 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); +// @ts-ignore TS2349 +test('ServiceKeysOrderRule: should fix the order of service keys', (t: ExecutionContext) => { + 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..657c979 100644 --- a/tests/rules/service-ports-alphabetical-order-rule.spec.ts +++ b/tests/rules/service-ports-alphabetical-order-rule.spec.ts @@ -1,7 +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 = ` @@ -47,37 +48,40 @@ 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) => { - const rule = new ServicePortsAlphabeticalOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithIncorrectPortOrder).toJS() as Record, - sourceCode: yamlWithIncorrectPortOrder, - }; +// @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', + 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, - }; +// @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', + 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); +// @ts-ignore TS2349 +test('ServicePortsAlphabeticalOrderRule: should fix the order of ports', (t: ExecutionContext) => { + 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..c8892df 100644 --- a/tests/rules/services-alphabetical-order-rule.spec.ts +++ b/tests/rules/services-alphabetical-order-rule.spec.ts @@ -1,7 +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 = ` @@ -33,45 +34,48 @@ 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) => { - const rule = new ServicesAlphabeticalOrderRule(); - const context: LintContext = { - path: '/docker-compose.yml', - content: parseDocument(yamlWithIncorrectOrder).toJS() as Record, - sourceCode: yamlWithIncorrectOrder, - }; +// @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', + 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, - }; +// @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', + 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); +// @ts-ignore TS2349 +test('ServicesAlphabeticalOrderRule: should fix the order of services', (t: ExecutionContext) => { + 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..e44b3dc 100644 --- a/tests/rules/top-level-properties-order-rule.spec.ts +++ b/tests/rules/top-level-properties-order-rule.spec.ts @@ -1,7 +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 = ` @@ -41,46 +42,49 @@ volumes: const filePath = '/docker-compose.yml'; // Helper function to normalize YAML strings for comparison -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 normalizeYAML = (yaml: string) => yaml.replaceAll(/\s+/g, ' ').trim(); + +// @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, + 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.'); +// @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, + 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); +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should fix the order of top-level properties', (t: ExecutionContext) => { + const rule = new TopLevelPropertiesOrderRule(); + const fixedYAML = rule.fix(yamlWithIncorrectOrder); - t.is( - normalizeYAML(fixedYAML), - normalizeYAML(yamlWithCorrectOrder), - 'The top-level properties should be reordered correctly.', - ); + t.is( + normalizeYAML(fixedYAML), + normalizeYAML(yamlWithCorrectOrder), + 'The top-level properties should be reordered correctly.', + ); }); // Custom order tests @@ -118,47 +122,49 @@ x-b: some-key: some-value `; -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.')); +// @ts-ignore TS2349 +test('TopLevelPropertiesOrderRule: should return warnings based on custom order', (t: ExecutionContext) => { + 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.', - ); +// @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, + 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/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'); +}); diff --git a/tests/util/files-finder.spec.ts b/tests/util/files-finder.spec.ts index f76e4eb..b1a9a86 100644 --- a/tests/util/files-finder.spec.ts +++ b/tests/util/files-finder.spec.ts @@ -1,9 +1,10 @@ /* 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'; +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'; @@ -19,90 +20,94 @@ 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 + Logger.init(false); // Initialize logger }); -const mockReaddirSync = (dir: string): string[] => { - if (dir === mockDirectory) { - return [...mockFilesInDirectory, ...mockDirectoriesInDirectory]; - } - if (dir === mockNodeModulesDirectory || dir === mockFolderDirectory) { - return mockFilesInSubDirectory; - } - return []; +const mockReaddirSync = (directory: string): string[] => { + if (directory === mockDirectory) { + return [...mockFilesInDirectory, ...mockDirectoriesInDirectory]; + } + if (directory === mockNodeModulesDirectory || directory === 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 }, - }, - ); +// @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', + { + '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 }, - }, - ); +// @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', + { + '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', - ); +// @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', + { + '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..f2750f2 100644 --- a/tests/util/line-finder.spec.ts +++ b/tests/util/line-finder.spec.ts @@ -1,8 +1,10 @@ import test from 'ava'; -import { findLineNumberByKey, findLineNumberByValue } from '../../src/util/line-finder.js'; +import type { ExecutionContext } from 'ava'; +import { findLineNumberByKey, findLineNumberByValue } from '../../src/util/line-finder'; -test('findLineNumberByKey: should return the correct line number when the key exists', (t) => { - const yamlContent = ` +// @ts-ignore TS2349 +test('findLineNumberByKey: should return the correct line number when the key exists', (t: ExecutionContext) => { + const yamlContent = ` version: '3' services: web: @@ -11,12 +13,13 @@ 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 = ` +// @ts-ignore TS2349 +test('findLineNumberByKey: should return 1 when the key does not exist', (t: ExecutionContext) => { + const yamlContent = ` version: '3' services: web: @@ -25,12 +28,13 @@ 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 = ` +// @ts-ignore TS2349 +test('findLineNumberByKey: should work for nested keys', (t: ExecutionContext) => { + const yamlContent = ` version: '3' services: web: @@ -41,12 +45,13 @@ 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 = ` +// @ts-ignore TS2349 +test('findLineNumberByValue: should return the correct line number when the value exists', (t: ExecutionContext) => { + const yamlContent = ` version: '3' services: web: @@ -55,12 +60,13 @@ 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 = ` +// @ts-ignore TS2349 +test('findLineNumberByValue: should return 0 when the value does not exist', (t: ExecutionContext) => { + const yamlContent = ` version: '3' services: web: @@ -69,12 +75,13 @@ 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 = ` +// @ts-ignore TS2349 +test('findLineNumberByValue: should return the correct line number for a value inside an array', (t: ExecutionContext) => { + const yamlContent = ` version: '3' services: web: @@ -83,6 +90,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..7ed6111 100644 --- a/tests/util/service-ports-parser.spec.ts +++ b/tests/util/service-ports-parser.spec.ts @@ -1,49 +1,133 @@ 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, + extractPublishedPortInterfaceValue, + parsePortsRange, +} from '../../src/util/service-ports-parser'; -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'); +// @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) => { - const scalarNode = new Scalar('127.0.0.1:3000'); - const result = extractPublishedPortValue(scalarNode); - t.is(result, '3000'); +// @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) => { - const mapNode = new YAMLMap(); - mapNode.set('published', '8080'); - const result = extractPublishedPortValue(mapNode); - t.is(result, '8080'); +// @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'); }); -test('extractPublishedPortValue should return empty string for unknown node type', (t) => { - const result = extractPublishedPortValue({}); - t.is(result, ''); +// @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('parsePortsRange should return array of ports for a range', (t) => { - t.deepEqual(parsePortsRange('3000-3002'), ['3000', '3001', '3002']); - t.deepEqual(parsePortsRange('3000-3000'), ['3000']); +// @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 single port when no range is specified', (t) => { - const result = parsePortsRange('8080'); - t.deepEqual(result, ['8080']); +// @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 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'), []); +// @ts-ignore TS2349 +test('parsePortsRange should return empty string from map node', (t: ExecutionContext) => { + const mapNode = new YAMLMap(); + const result = extractPublishedPortValue(mapNode); + t.is(result, ''); }); -test('parsePortsRange should return empty array when start port is greater than end port', (t) => { - t.deepEqual(parsePortsRange('3005-3002'), []); +// @ts-ignore TS2349 +test('extractPublishedPortInterfaceValue should return listen ip string for scalar without IP', (t: ExecutionContext) => { + const scalarNode = new Scalar('8080:8080'); + const result = extractPublishedPortInterfaceValue(scalarNode); + t.is(result, ''); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInterfaceValue should return listen ip string for scalar without IP and automapped port', (t: ExecutionContext) => { + const scalarNode = new Scalar('8080'); + const result = extractPublishedPortInterfaceValue(scalarNode); + t.is(result, ''); +}); + +// @ts-ignore TS2349 +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 = extractPublishedPortInterfaceValue(scalarNode); + t.is(result, '127.0.0.1'); +}); + +// @ts-ignore TS2349 +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 = extractPublishedPortInterfaceValue(scalarNode); + t.is(result, '0.0.0.0'); +}); + +// @ts-ignore TS2349 +test('extractPublishedPortInterfaceValue should return listen ip string for scalar on ::1 with automapped port', (t: ExecutionContext) => { + const scalarNode = new Scalar('[::1]:8080'); + const result = extractPublishedPortInterfaceValue(scalarNode); + t.is(result, '::1'); +}); + +// @ts-ignore TS2349 +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 = 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'); + t.deepEqual(result, ['8080']); +}); + +// @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'), []); + t.deepEqual(parsePortsRange('$TEST-$TEST'), []); + t.deepEqual(parsePortsRange('3000-$TEST-$TEST-5000'), []); +}); + +// @ts-ignore TS2349 +test('parsePortsRange should return empty array when start port is greater than end port', (t: ExecutionContext) => { + t.deepEqual(parsePortsRange('3005-3002'), []); }); 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"