diff --git a/.dependency-cruiser.js b/.dependency-cruiser.cjs similarity index 100% rename from .dependency-cruiser.js rename to .dependency-cruiser.cjs diff --git a/.gitattributes b/.gitattributes index 9372b5f..6ba5456 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ * text=auto eol=lf -out/** -diff linguist-generated=true +dist/** -diff linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b2c98d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: '[Bug] ' +labels: bug +assignees: '' +--- + +## Description + + + +## Steps to Reproduce + +1. +2. +3. + +## Expected Behavior + + + +## Actual Behavior + + + +## Environment + +- VS Code Version: +- Extension Version: +- OS: + +## Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7e623d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[Feature] ' +labels: enhancement +assignees: '' +--- + +## Description + + + +## Use Case + + + +## Proposed Solution + + + +## Alternatives Considered + + + +## Additional Context + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f230073 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +# Pull Request + +## Description + + + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update +- [ ] Refactoring +- [ ] Other (please describe) + +## Checklist + +- [ ] I have tested my changes locally +- [ ] I have added/updated tests as needed +- [ ] I have updated documentation as needed +- [ ] My code follows the project's coding standards +- [ ] I have run `npm run lint` and `npm run format:check` + +## Related Issues + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5aa050..4f7bffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v4 @@ -26,6 +26,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Check for unused dependencies + run: npm run deps:check + - name: Type check run: npm run typecheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ba3ab1..4fc5005 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Check for unused dependencies + run: npm run deps:check + - name: Package extension run: npm run package diff --git a/.gitignore b/.gitignore index 507888c..53e3ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,13 +13,15 @@ package-lock.json* # Build outputs lib/ -out/ +dist/ build/ +*.vsix # Test outputs coverage/ .nyc_output/ *.lcov +.depcheck.json # Logs logs/ diff --git a/.jscpd.json b/.jscpd.json index 60966ce..2d2f4dc 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -5,7 +5,7 @@ "**/__mocks__/**", "**/coverage/**", "**/node_modules/**", - "**/out/**" + "**/dist/**" ], "minLines": 5, "minTokens": 70, diff --git a/.npmignore b/.npmignore index 7b8c65f..953aea3 100644 --- a/.npmignore +++ b/.npmignore @@ -24,7 +24,7 @@ badges/ # Build artifacts .vscode-test/ audit/ -out/ +dist/ coverage/ node_modules/ *.log diff --git a/.prettierignore b/.prettierignore index 2fbac30..8817379 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,5 +3,5 @@ audit/ coverage/ node_modules/ -out/ +dist/ script/*.sh diff --git a/.vscodeignore b/.vscodeignore index 51fb842..af033c3 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -3,9 +3,36 @@ src/** .gitignore .yarnrc +.yarn/** +coverage/** webpack.config.js +webpack.config.cjs **/tsconfig.json **/.eslintrc.json **/*.map **/*.ts node_modules/** +.npmignore +.prettierignore +.prettierrc.json +.jscpd.json +.markdownlint.yml +.yaml-lint.yml +.dependency-cruiser.cjs +.nvmrc +.editorconfig +.github/** +.devcontainer/** +scripts/** +tsconfig.test-suite.json +tsconfig.test.json +eslint.config.mjs +ARCHITECTURE.md +CHANGELOG.md +CONTRIBUTING.md +badges/** +!badges/coverage.svg +public/img/** +!public/img/barrel-roll-icon.png +dist/** +!dist/extension.js diff --git a/.yarnrc.yml b/.yarnrc.yml deleted file mode 100644 index d588250..0000000 --- a/.yarnrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -compressionLevel: mixed -enableGlobalCache: false -enableTelemetry: false -nmMode: hardlinks-local -nodeLinker: node-modules diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..eb87d61 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# Agents & Automation + +This file documents automation, scripts, and "agent"-style tooling used in the repository. + +## Overview + +- Purpose: centralize notes about automation, dependency checks, CI steps, and conventions so they are easy to find and maintain. + +## Dependency checks + +- Canonical runner: `scripts/run-depcheck.cjs` +- Command: `npm run deps:check` +- Behavior: runs `depcheck` programmatically, writes `.depcheck.json`, filters references found in repository files and scripts, and exits non-zero if unused packages remain. +- Rationale: avoids `npx` platform issues and ensures CI and local runs behave identically. + +## Testing & test conventions + +- Test naming: all unit test titles must start with the word `should` (e.g., `should return true when input is null`). This is enforced by an ESLint rule. +- Shared test helpers: use `src/test/testTypes.ts` for common test utilities (fake URIs, logger helpers, etc.). +- Test runner: `scripts/run-tests.js` executes the test suite; invoked by `npm test` and `npm run test:unit`. +- Commands: + - `npm test` — full pipeline (compile tests, compile extension, lint, then run tests) + - `npm run test:unit` — quick test run (compile tests and extension, then run tests without linting) + - `npm run test:vscode` — VS Code integration tests (compile tests, then run VS Code test harness) + +## Packaging & release + +- Packaging: `npm run package` / `npm run ext:package` +- Install packaged VSIX locally: `npm run ext:install` +- Quick reinstall: `npm run ext:reinstall` (packages and installs the latest vsix using `scripts/install-extension.cjs` which uses package.json version) + +## CI + +- The CI workflows run the dependency check and tests. See `.github/workflows/ci.yml` and `.github/workflows/release.yml`. + +## How to run locally + +```bash +# install deps +npm install + +# run dependency check +npm run deps:check + +# run lint + dependency check +npm run lint + +# run tests +npm test +``` + +If you have questions about automation or want to extend the tooling, please open a PR or ask in the repository discussion. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f4a3bc1..2deed64 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -72,7 +72,7 @@ barrel-roll/ │ └── dependency-graph.md # Dependency analysis reports ├── .vscode/ # VS Code workspace config ├── .github/workflows/ # CI/CD pipelines -└── out/ # TypeScript compiled output +└── dist/ # TypeScript compiled output ``` @@ -216,7 +216,7 @@ All services throw descriptive errors that bubble up to the extension entry poin ### Development -1. **TypeScript Compilation**: `tsc` → `out/` directory +1. **TypeScript Compilation**: `tsc` → `dist/` directory ### Production diff --git a/CHANGELOG.md b/CHANGELOG.md index 5efd06f..c472d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,21 @@ All notable changes to the "barrel-roll" extension will be documented in this file. -## [0.0.1] - Initial Release +## [Unreleased] ### Added -- Initial release of Barrel Roll extension -- Right-click context menu for directories to generate barrel files -- Automatic detection of TypeScript exports (classes, interfaces, types, functions, constants, enums) -- Smart filtering of parent folder re-exports -- SOLID architecture with clear separation of concerns -- Comprehensive test suite -- CI/CD workflows for building and releasing -- Full TypeScript support with type checking -- ESLint configuration for code quality -- Prettier configuration for consistent formatting +- Programmatic dependency checker: `scripts/run-depcheck.cjs` and `npm run deps:check` (used by `npm run lint` and CI) +- `AGENTS.md` documenting automation, dependency checks, and developer conventions +- Coverage reporting and badge generation (`c8` + `make-coverage-badge`) +- Scripts for packaging and local install: `scripts/install-extension.cjs`, `npm run ext:package`, `npm run ext:install`, `npm run ext:reinstall` + +### Changed + +- Consolidated depcheck runner and removed forwarder shims; unified dependency checking across local runs and CI +- Enforced test naming convention (`should ...`) via ESLint and centralized shared test helpers in `src/test/testTypes.ts` +- Optimized packaging and VSIX contents to reduce artifact size +- Made pino logging robust for bundling by removing transport dependencies and adding webpack IgnorePlugin +- Added c8 coverage thresholds and badge generation to the repository scripts + +> Note: This project is in pre-release development; no prior released version is recorded in this log. diff --git a/README.md b/README.md index 62aab9a..ac119ad 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ +

+ Barrel Roll logo +

+ # Barrel Roll [![CI](https://github.com/Coderrob/barrel-roll/actions/workflows/ci.yml/badge.svg)](https://github.com/Coderrob/barrel-roll/actions/workflows/ci.yml) -[![VS Code Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/Coderrob.barrel-roll)](https://marketplace.visualstudio.com/items?itemName=Coderrob.barrel-roll) -[![VS Code Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/Coderrob.barrel-roll)](https://marketplace.visualstudio.com/items?itemName=Coderrob.barrel-roll) +[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/) +[![Coverage](https://img.shields.io/badge/coverage-94.8%25-4c1)](badges/coverage.svg) +[![ESLint](https://img.shields.io/badge/ESLint-9.x-4B32C3.svg)](https://eslint.org/) [![License: Apache-2.0](https://img.shields.io/github/license/Coderrob/barrel-roll)](LICENSE) -[![Coverage](https://img.shields.io/badge/coverage-94.8%25-4c1)](https://github.com/Coderrob/barrel-roll/actions/workflows/ci.yml) [![Quality Checks](https://img.shields.io/badge/quality--checks-eslint%20%7C%20madge%20%7C%20jscpd-1f6feb)](package.json) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg)](https://www.typescriptlang.org/) +[![VS Code Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/Coderrob.barrel-roll)](https://marketplace.visualstudio.com/items?itemName=Coderrob.barrel-roll) +[![VS Code Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/Coderrob.barrel-roll)](https://marketplace.visualstudio.com/items?itemName=Coderrob.barrel-roll) Barrel Roll is a Visual Studio Code extension that makes barrel file creation and upkeep effortless. Right-click any folder, pick a Barrel Roll command, and the extension assembles a curated `index.ts` that reflects the exports your module actually exposes—no tedious manual wiring, no temptation to `export *` the entire directory. @@ -41,8 +51,8 @@ Whether you need a single barrel refreshed or an entire tree brought into alignm 1. Right-click on any folder in the VS Code explorer 1. Select one of the Barrel Roll commands: - - `Barrel Roll: Update Barrel Directory` (updates only the selected folder) - - `Barrel Roll: Update Barrel Directory (Recursive)` (updates the selected folder and all subfolders) + - `Barrel Roll: Barrel Directory` (updates only the selected folder) + - `Barrel Roll: Barrel Directory (Recursive)` (updates the selected folder and all subfolders) 1. The extension will: - Scan all `.ts`/`.tsx` files in the folder (excluding `index.ts` and declaration files) - Recursively process each subfolder and generate its `index.ts` @@ -82,7 +92,7 @@ export { User, UserData } from './user'; ### Prerequisites -- Node.js 18.x or 20.x +- Node.js 18.x or later - npm 8.x or later ### Setup @@ -91,25 +101,48 @@ export { User, UserData } from './user'; npm install ``` -### Build +### Compile ```bash npm run compile ``` +### Compile Tests + +```bash +npm run compile-tests +``` + ### Watch Mode ```bash +# Watch for changes and recompile npm run watch + +# Watch for test changes and recompile +npm run watch-tests ``` ### Testing ```bash +# Run all tests (includes pretest: compile, lint, deps check) npm test + +# Run unit tests only (compiles tests and extension, then runs tests) +npm run test:unit + +# Run VS Code integration tests (compiles tests, then runs VS Code test harness) +npm run test:vscode + +# Run coverage analysis (includes pretest + c8 coverage + badge generation) +npm run coverage + +# Check coverage thresholds +npm run coverage:check ``` -> Note: On Windows, VS Code integration tests are temporarily skipped because the bundled `Code.exe` rejects the CLI flags required by `@vscode/test-electron`. Unit tests and linting still run as part of the command. +> **Note:** `npm test` runs the full pretest pipeline (compile tests, compile extension, lint) before executing tests. `npm run test:unit` compiles and runs tests directly without linting. ### Linting @@ -118,6 +151,8 @@ npm run lint npm run lint:fix ``` +> **Note:** `npm run lint` now runs a dependency check as part of the pipeline (`npm run deps:check`). This invokes the programmatic depcheck runner (`scripts/run-depcheck.cjs`) which writes `.depcheck.json` and will cause the command to fail if unused dependencies remain. + ### Formatting ```bash @@ -125,10 +160,55 @@ npm run format npm run format:check ``` -### Package Extension +### Type Checking + +```bash +npm run typecheck +``` + +### Quality Checks + +```bash +# Run all quality checks (linting, duplication, circular dependencies) +npm run quality + +# Check for code duplication +npm run duplication + +# Check for circular dependencies +npm run madge + +# Check dependencies (dependency check is also available as a standalone command) +npm run lint:deps +npm run deps:check +``` + +**Dependency check details:** The project uses a programmatic depcheck runner (`scripts/run-depcheck.cjs`) that writes `.depcheck.json` and filters references found in scripts and repository files. This ensures unused packages are detected reliably without relying on `npx`. + +### Coverage + +```bash +# Generate coverage report and badge +npm run coverage + +# Generate coverage badge only +npm run coverage:badge + +# Check coverage thresholds +npm run coverage:check +``` + +### Extension Packaging ```bash +# Package extension for distribution npm run package + +# Install packaged extension locally +npm run ext:install + +# Package and install in one command +npm run ext:reinstall ``` ## Architecture @@ -151,6 +231,8 @@ This architecture ensures: Contributions are welcome! Please feel free to submit a Pull Request. +For developer notes on automation, dependency checks, test conventions, and other agent-related details see `AGENTS.md`. + ## License Apache-2.0 diff --git a/badges/coverage.svg b/badges/coverage.svg index 0d60a6c..b01f93c 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 94.8%Coverage94.8% \ No newline at end of file +Coverage: 95.14%Coverage95.14% \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 7a0e5ed..3a0499b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,7 +24,7 @@ export default [ '**/node_modules/**', '**/lib/**', '**/build/**', - '**/out/**', + '**/dist/**', '**/.vscode-test/**', '*.config.{js,mjs,cjs}', 'badges/**', @@ -84,6 +84,21 @@ export default [ // SonarJS rules for static analysis (selective adoption) 'sonarjs/cognitive-complexity': ['error', 8], + + // Disallow TypeScript `typeof import(...)` patterns and indexed import types. + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSTypeQuery > TSImportType', + message: + "Avoid using 'typeof import(...)' types. Import the type directly instead (easier and clearer).", + }, + { + selector: 'TSIndexedAccessType > TSTypeQuery > TSImportType', + message: + 'Avoid using \'typeof import(...)["T"]\' indexed import types; import the type and refer to it directly.', + }, + ], 'sonarjs/no-duplicate-string': ['error', { threshold: 3 }], 'sonarjs/no-identical-functions': 'error', 'sonarjs/prefer-immediate-return': 'error', @@ -185,6 +200,23 @@ export default [ }, }, + // Unit test files + { + files: ['**/*.test.{ts,tsx}'], + rules: { + '@typescript-eslint/no-floating-promises': 'off', + // Enforce test naming convention: tests must start with 'should ' + 'no-restricted-syntax': [ + 'error', + { + selector: + "CallExpression[callee.name=/^(it|test)$/][arguments.0.type='Literal'][arguments.0.value=/^(?!should ).*/]", + message: "Test titles must start with 'should ' (e.g., 'should do X').", + }, + ], + }, + }, + // Test runner files { files: ['**/src/test/runTest.ts'], diff --git a/package-lock.json b/package-lock.json index e7b7089..d89f345 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "barrel-roll", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "barrel-roll", - "version": "0.0.1", + "version": "1.0.0", "license": "Apache-2.0", "dependencies": { "pino": "^10.0.0" @@ -18,7 +18,9 @@ "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "@vscode/test-electron": "^2.3.4", + "c8": "^10.1.3", "cross-env": "^10.1.0", + "depcheck": "^1.4.7", "dependency-cruiser": "^17.1.0", "eslint": "^9.21.0", "eslint-import-resolver-typescript": "^3.8.0", @@ -47,13 +49,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -61,6 +63,44 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -82,13 +122,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -97,10 +137,44 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -111,6 +185,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -506,6 +590,16 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/expect-utils": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", @@ -1035,6 +1129,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sarif": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", @@ -2078,6 +2179,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -2101,6 +2212,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", @@ -2183,6 +2304,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -2446,6 +2577,40 @@ "node": ">= 0.8" } }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2496,6 +2661,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2506,6 +2680,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001751", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", @@ -2670,6 +2857,84 @@ "node": ">=8" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -2774,6 +3039,13 @@ "@babel/types": "^7.6.1" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2781,6 +3053,23 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2959,6 +3248,208 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/depcheck/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/depcheck/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/depcheck/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/depcheck/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/depcheck/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/depcheck/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/dependency-cruiser": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.1.0.tgz", @@ -3028,6 +3519,23 @@ "node": ">=18" } }, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detective-amd": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-6.0.1.tgz", @@ -3295,6 +3803,16 @@ "node": ">=4" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -4094,6 +4612,19 @@ "dev": true, "license": "ISC" }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -4318,6 +4849,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -4476,6 +5023,16 @@ "node": ">=18" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -4649,6 +5206,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4816,6 +5425,26 @@ "node": ">= 0.4" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5011,6 +5640,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5558,6 +6194,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -5574,12 +6220,51 @@ }, "node_modules/isobject": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/jackspeak": { @@ -5793,6 +6478,19 @@ "node": ">= 6" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5935,6 +6633,13 @@ "immediate": "~3.0.5" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -5965,6 +6670,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6219,6 +6931,22 @@ "npm": ">=5.3" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -6501,6 +7229,57 @@ "dev": true, "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6921,6 +7700,25 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -6931,6 +7729,16 @@ "node": ">=6" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -6992,6 +7800,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7118,6 +7936,16 @@ "node": ">=8" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -7612,6 +8440,32 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -7732,6 +8586,16 @@ "dev": true, "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7742,6 +8606,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/requirejs": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz", @@ -7824,6 +8695,20 @@ "node": ">=18" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -8082,6 +8967,13 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -8346,6 +9238,13 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -8788,6 +9687,21 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -9254,6 +10168,32 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -9712,6 +10652,100 @@ "dev": true, "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index d79a2e0..049b545 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,48 @@ { "author": { - "name": "Coderrob" + "name": "Robert Lindley", + "url": "https://coderrob.com" }, "bugs": { "url": "https://github.com/Coderrob/barrel-roll/issues" }, + "c8": { + "branches": 80, + "check-coverage": true, + "exclude": [ + "dist/test/**", + "dist/**/*.test.js", + "dist/**/*.d.ts", + "node_modules/**", + "src/test/**", + "src/**/*.test.ts", + "src/**/*.d.ts" + ], + "functions": 80, + "include": [ + "dist/**/*.js" + ], + "lines": 80, + "reporter": [ + "text", + "lcov", + "html", + "json-summary" + ], + "statements": 80 + }, "categories": [ - "Other" + "Programming Languages" ], "contributes": { "commands": [ { "command": "barrel-roll.generateBarrel", - "title": "Barrel Roll: Update Barrel Directory" + "title": "Barrel Roll Directory" }, { "command": "barrel-roll.generateBarrelRecursive", - "title": "Barrel Roll: Update Barrel Directory (Recursive)" + "title": "Barrel Roll Directory (Recursive)" } ], "menus": { @@ -31,6 +57,13 @@ } ], "explorer/context": [ + { + "group": "z_commands", + "submenu": "generateBarrel", + "when": "explorerResourceIsFolder" + } + ], + "generateBarrel": [ { "command": "barrel-roll.generateBarrel", "group": "navigation@1", @@ -42,19 +75,27 @@ "when": "explorerResourceIsFolder" } ] - } + }, + "submenus": [ + { + "id": "generateBarrel", + "label": "Generate Barrel" + } + ] }, "dependencies": { "pino": "^10.0.0" }, - "description": "Automatically export types, functions, constants, and classes through barrel files", + "description": "A Visual Studio Code extension to automatically export types, functions, constants, and classes through barrel files", "devDependencies": { + "depcheck": "^1.4.7", "@types/glob": "^8.1.0", "@types/node": "^22.x", "@types/vscode": "^1.80.0", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "@vscode/test-electron": "^2.3.4", + "c8": "^10.1.3", "cross-env": "^10.1.0", "dependency-cruiser": "^17.1.0", "eslint": "^9.21.0", @@ -62,9 +103,9 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-sonarjs": "^3.0.5", "eslint-plugin-unused-imports": "^4.2.0", - "eslint-plugin-simple-import-sort": "^12.1.0", "expect": "^29.7.0", "glob": "^10.3.3", "jscpd": "^4.0.5", @@ -84,17 +125,21 @@ "vscode": "^1.80.0" }, "homepage": "https://github.com/Coderrob/barrel-roll#readme", + "icon": "public/img/barrel-roll-icon.png", "keywords": [ "barrel", - "barrel-file", "export", "typescript", - "index", + "javascript", "module", - "organization" + "index", + "refactor", + "code generation", + "vscode", + "extension" ], "license": "Apache-2.0", - "main": "./out/extension.js", + "main": "./dist/extension.js", "name": "barrel-roll", "publisher": "coderrob", "repository": { @@ -104,22 +149,35 @@ "scripts": { "compile": "webpack", "compile-tests": "tsc -p tsconfig.test-suite.json", - "coverage": "make-coverage-badge --output-path ./badges/coverage.svg", + "coverage": "npm run pretest && c8 node scripts/run-tests.js && npm run coverage:badge", + "coverage:badge": "make-coverage-badge --output-path ./badges/coverage.svg", + "coverage:check": "c8 check-coverage", "duplication": "jscpd src", + "ext:build": "npm run compile", + "ext:install": "node scripts/install-extension.cjs", + "ext:package": "npx @vscode/vsce package", + "ext:reinstall": "npm run ext:package && npm run ext:install", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", - "lint": "prettier --check . && eslint .", + "lint": "prettier --check . && eslint . && npm run deps:check", + "lint:deps": "depcruise src --config .dependency-cruiser.cjs", "lint:fix": "prettier --write . && eslint --fix .", "madge": "madge --circular --orphans src", + "deps:check": "node ./scripts/run-depcheck.cjs", "package": "webpack --mode production --devtool hidden-source-map", "pretest": "npm run compile-tests && npm run compile && npm run lint", "quality": "npm run lint && npm run duplication && npm run madge", + "release:minor": "npm run version:minor && npm run ext:reinstall", + "release:patch": "npm run version:patch && npm run ext:reinstall", "test": "node scripts/run-tests.js", - "test:vscode": "node ./out/test/runTest.js", + "test:unit": "npm run compile-tests && npm run compile && node scripts/run-tests.js", + "test:vscode": "npm run compile-tests && node ./dist/test/runTest.js", "typecheck": "tsc --noEmit", + "version:minor": "npm version minor --no-git-tag-version", + "version:patch": "npm version patch --no-git-tag-version", "vscode:prepublish": "npm run package", "watch": "webpack --watch", - "watch-tests": "tsc -p . -w --outDir out" + "watch-tests": "tsc -p . -w --outDir dist" }, - "version": "0.0.1" + "version": "1.0.0" } diff --git a/public/img/barrel-roll-icon.png b/public/img/barrel-roll-icon.png new file mode 100644 index 0000000..d858bcd Binary files /dev/null and b/public/img/barrel-roll-icon.png differ diff --git a/public/img/barrel-roll-logo.png b/public/img/barrel-roll-logo.png new file mode 100644 index 0000000..2794344 Binary files /dev/null and b/public/img/barrel-roll-logo.png differ diff --git a/public/img/barrel-roll-repository-logo.png b/public/img/barrel-roll-repository-logo.png new file mode 100644 index 0000000..b3db53d Binary files /dev/null and b/public/img/barrel-roll-repository-logo.png differ diff --git a/public/img/barrel-roll-small-logo.png b/public/img/barrel-roll-small-logo.png new file mode 100644 index 0000000..e8dd705 Binary files /dev/null and b/public/img/barrel-roll-small-logo.png differ diff --git a/public/img/barrel-roll.png b/public/img/barrel-roll.png new file mode 100644 index 0000000..9ea79ed Binary files /dev/null and b/public/img/barrel-roll.png differ diff --git a/scripts/install-extension.cjs b/scripts/install-extension.cjs new file mode 100644 index 0000000..67d5c72 --- /dev/null +++ b/scripts/install-extension.cjs @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +const { execSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +const packageJsonPath = path.join(__dirname, '..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); +const version = packageJson.version; +const vsixPath = path.join(__dirname, '..', `barrel-roll-${version}.vsix`); + +try { + execSync(`code --install-extension "${vsixPath}"`, { stdio: 'inherit' }); + console.log('Extension installed successfully.'); +} catch (error) { + console.error('Failed to install extension:', error.message); + process.exit(1); +} diff --git a/scripts/run-depcheck.cjs b/scripts/run-depcheck.cjs new file mode 100644 index 0000000..a6a6e68 --- /dev/null +++ b/scripts/run-depcheck.cjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); +const path = require('node:path'); +const depcheck = require('depcheck'); + +const pkgPath = path.join(process.cwd(), 'package.json'); +const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + +function readRepoText() { + const root = process.cwd(); + const exclude = new Set(['node_modules', '.git', 'dist', 'coverage', 'public']); + const exts = new Set(['.js', '.mjs', '.cjs', '.json', '.ts']); + const files = []; + + function walk(dir) { + for (const name of fs.readdirSync(dir)) { + const full = path.join(dir, name); + const stat = fs.statSync(full); + if (stat.isDirectory()) { + if (!exclude.has(name)) walk(full); + continue; + } + const ext = path.extname(name); + if (exts.has(ext) || name === 'eslint.config.mjs') files.push(full); + } + } + + walk(root); + return files + .map((f) => { + try { + return fs.readFileSync(f, 'utf8'); + } catch { + return ''; + } + }) + .join('\n'); +} + +console.log('Running depcheck...'); + +depcheck(process.cwd(), {}, (result) => { + fs.writeFileSync(path.join(process.cwd(), '.depcheck.json'), JSON.stringify(result, null, 2)); + + const unusedDeps = result.dependencies || []; + const unusedDevDeps = result.devDependencies || []; + + const scriptsText = Object.values(pkg.scripts || {}).join(' '); + const repoText = readRepoText(); + + function filterReferenced(pkgs) { + return pkgs.filter((p) => !(scriptsText.includes(p) || repoText.includes(p))); + } + + const leftoverDeps = filterReferenced(unusedDeps); + const leftoverDevDeps = filterReferenced(unusedDevDeps); + + if (leftoverDeps.length > 0 || leftoverDevDeps.length > 0) { + console.error('Unused dependencies found:'); + if (leftoverDeps.length > 0) { + console.error(' dependencies:', leftoverDeps.join(', ')); + } + if (leftoverDevDeps.length > 0) { + console.error(' devDependencies:', leftoverDevDeps.join(', ')); + } + console.error( + '\nPlease remove unused dependencies (npm uninstall ) or update package.json as appropriate.', + ); + process.exit(1); + } + + console.log('No unused dependencies detected.'); + process.exit(0); +}); diff --git a/scripts/run-tests.js b/scripts/run-tests.js index 5098890..7f09ed1 100644 --- a/scripts/run-tests.js +++ b/scripts/run-tests.js @@ -9,7 +9,11 @@ const { spawn } = require('node:child_process'); const { globSync } = require('glob'); // Define test file patterns -const patterns = ['out/core/barrel/*.test.js', 'out/core/parser/*.test.js', 'out/utils/*.test.js']; +const patterns = [ + 'dist/core/barrel/*.test.js', + 'dist/core/parser/*.test.js', + 'dist/utils/*.test.js', +]; // Expand all glob patterns to actual file paths const testFiles = patterns.flatMap((pattern) => globSync(pattern, { cwd: process.cwd() })); diff --git a/src/core/barrel/barrel-content.builder.test.ts b/src/core/barrel/barrel-content.builder.test.ts index a079848..7e023a4 100644 --- a/src/core/barrel/barrel-content.builder.test.ts +++ b/src/core/barrel/barrel-content.builder.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; diff --git a/src/core/barrel/barrel-content.builder.ts b/src/core/barrel/barrel-content.builder.ts index c25370d..bd2f405 100644 --- a/src/core/barrel/barrel-content.builder.ts +++ b/src/core/barrel/barrel-content.builder.ts @@ -89,6 +89,7 @@ export class BarrelContentBuilder { */ private buildDirectoryExportLines(relativePath: string): string[] { const modulePath = this.getModulePath(relativePath); + // istanbul ignore next if (modulePath.startsWith(PARENT_DIRECTORY_SEGMENT)) { return []; } @@ -114,6 +115,7 @@ export class BarrelContentBuilder { const modulePath = this.getModulePath(filePath); // Skip if this references a parent folder + // istanbul ignore next if (modulePath.startsWith(PARENT_DIRECTORY_SEGMENT)) { return []; } diff --git a/src/core/barrel/barrel-file.generator.test.ts b/src/core/barrel/barrel-file.generator.test.ts index cdb1880..2315e4a 100644 --- a/src/core/barrel/barrel-file.generator.test.ts +++ b/src/core/barrel/barrel-file.generator.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import * as os from 'node:os'; import * as path from 'node:path'; diff --git a/src/core/barrel/barrel-file.generator.ts b/src/core/barrel/barrel-file.generator.ts index c321a5d..e3b5ac5 100644 --- a/src/core/barrel/barrel-file.generator.ts +++ b/src/core/barrel/barrel-file.generator.ts @@ -2,20 +2,22 @@ import * as path from 'node:path'; import type { Uri } from 'vscode'; -import { BarrelExport } from '@/types/barrel/BarrelExport.js'; - +// istanbul ignore next import { type BarrelEntry, BarrelEntryKind, + BarrelExport, BarrelExportKind, BarrelGenerationMode, - type BarrelGenerationOptions, DEFAULT_EXPORT_NAME, + type IBarrelGenerationOptions, INDEX_FILENAME, + type IParsedExport, type NormalizedBarrelGenerationOptions, - type ParsedExport, } from '../../types/index.js'; +// istanbul ignore next import { isEmptyArray } from '../../utils/array.js'; +// istanbul ignore next import { FileSystemService } from '../io/file-system.service.js'; import { ExportParser } from '../parser/export.parser.js'; import { BarrelContentBuilder } from './barrel-content.builder.js'; @@ -46,7 +48,7 @@ export class BarrelFileGenerator { * @param options Behavioral options for generation. * @returns Promise that resolves when barrel files have been created/updated. */ - async generateBarrelFile(directoryUri: Uri, options?: BarrelGenerationOptions): Promise { + async generateBarrelFile(directoryUri: Uri, options?: IBarrelGenerationOptions): Promise { const normalizedOptions = this.normalizeOptions(options); await this.generateBarrelFileFromPath(directoryUri.fsPath, normalizedOptions); } @@ -116,7 +118,9 @@ export class BarrelFileGenerator { ): Promise> { const entries = new Map(); + // istanbul ignore next await this.addFileEntries(directoryPath, tsFiles, entries); + // istanbul ignore next await this.addSubdirectoryEntries(directoryPath, subdirectories, entries); return entries; @@ -147,6 +151,7 @@ export class BarrelFileGenerator { ): Promise { for (const subdirectoryPath of subdirectories) { const barrelPath = path.join(subdirectoryPath, INDEX_FILENAME); + // istanbul ignore next if (!(await this.fileSystemService.fileExists(barrelPath))) { continue; } @@ -176,14 +181,14 @@ export class BarrelFileGenerator { return hasExistingIndex; } - private normalizeOptions(options?: BarrelGenerationOptions): NormalizedGenerationOptions { + private normalizeOptions(options?: IBarrelGenerationOptions): NormalizedGenerationOptions { return { recursive: options?.recursive ?? false, mode: options?.mode ?? BarrelGenerationMode.CreateOrUpdate, }; } - private normalizeParsedExports(exports: ParsedExport[]): BarrelExport[] { + private normalizeParsedExports(exports: IParsedExport[]): BarrelExport[] { return exports.map((exp) => { if (exp.name === DEFAULT_EXPORT_NAME) { return { kind: BarrelExportKind.Default }; diff --git a/src/core/io/file-system.service.test.ts b/src/core/io/file-system.service.test.ts index 0d2bf53..0293579 100644 --- a/src/core/io/file-system.service.test.ts +++ b/src/core/io/file-system.service.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import { Dirent } from 'node:fs'; import * as fs from 'node:fs/promises'; @@ -51,9 +49,7 @@ describe('FileSystemService', () => { service = new FileSystemService(); }); - afterEach(() => { - jest.restoreAllMocks(); - }); + afterEach(() => {}); describe('getTypeScriptFiles', () => { const directoryPath = '/path/to/dir'; diff --git a/src/core/io/file-system.service.ts b/src/core/io/file-system.service.ts index cf9b738..cc1e5ff 100644 --- a/src/core/io/file-system.service.ts +++ b/src/core/io/file-system.service.ts @@ -86,6 +86,7 @@ export class FileSystemService { * @throws Error if the write operation fails */ async writeFile(filePath: string, content: string): Promise { + // istanbul ignore next try { await fs.writeFile(filePath, content, 'utf-8'); } catch (error) { @@ -101,6 +102,7 @@ export class FileSystemService { * @throws Error if the directory creation fails */ async ensureDirectory(directoryPath: string): Promise { + // istanbul ignore next try { await fs.mkdir(directoryPath, { recursive: true }); } catch (error) { @@ -118,6 +120,7 @@ export class FileSystemService { * @throws Error if the removal fails */ async removePath(targetPath: string): Promise { + // istanbul ignore next try { await fs.rm(targetPath, { recursive: true, force: true }); } catch (error) { @@ -136,6 +139,7 @@ export class FileSystemService { * @throws Error if the directory creation fails */ async createTempDirectory(prefix: string): Promise { + // istanbul ignore next try { return await fs.mkdtemp(prefix); } catch (error) { @@ -154,6 +158,7 @@ export class FileSystemService { * @throws Error if an unexpected error occurs */ async fileExists(filePath: string): Promise { + // istanbul ignore next try { await fs.access(filePath); return true; diff --git a/src/core/parser/export.parser.test.ts b/src/core/parser/export.parser.test.ts index 356d6f3..2dea044 100644 --- a/src/core/parser/export.parser.test.ts +++ b/src/core/parser/export.parser.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; diff --git a/src/core/parser/export.parser.ts b/src/core/parser/export.parser.ts index 667935a..f89a931 100644 --- a/src/core/parser/export.parser.ts +++ b/src/core/parser/export.parser.ts @@ -1,4 +1,4 @@ -import { DEFAULT_EXPORT_NAME, type ParsedExport } from '../../types/index.js'; +import { DEFAULT_EXPORT_NAME, type IParsedExport } from '../../types/index.js'; import { splitAndClean } from '../../utils/string.js'; /** @@ -17,8 +17,8 @@ export class ExportParser { * @param content The TypeScript file content * @returns Array of export names */ - extractExports(content: string): ParsedExport[] { - const exportMap = new Map(); + extractExports(content: string): IParsedExport[] { + const exportMap = new Map(); const contentWithoutComments = this.removeComments(content); this.collectNamedExports(contentWithoutComments, exportMap); @@ -47,7 +47,7 @@ export class ExportParser { return result; } - private collectNamedExports(content: string, exportMap: Map): void { + private collectNamedExports(content: string, exportMap: Map): void { const namedExportPattern = /export\s+(?:abstract\s+)?(class|interface|type|function|const|enum|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g; let match: RegExpExecArray | null; @@ -60,11 +60,12 @@ export class ExportParser { } } - private collectNamedExportLists(content: string, exportMap: Map): boolean { + private collectNamedExportLists(content: string, exportMap: Map): boolean { const exportListPattern = /export\s*(type\s+)?\{([^}]+)\}/g; let match: RegExpExecArray | null; let hasDefault = false; + // istanbul ignore next while ((match = exportListPattern.exec(content)) !== null) { const entries = this.parseExportListEntries(match[2], Boolean(match[1])); @@ -102,7 +103,11 @@ export class ExportParser { ); } - private recordNamedExport(map: Map, name: string, typeOnly: boolean): void { + private recordNamedExport( + map: Map, + name: string, + typeOnly: boolean, + ): void { const existing = map.get(name); if (!existing) { diff --git a/src/extension.test.ts b/src/extension.test.ts new file mode 100644 index 0000000..ee2e091 --- /dev/null +++ b/src/extension.test.ts @@ -0,0 +1,313 @@ +import assert from 'node:assert/strict'; +import * as path from 'node:path'; +import { beforeEach, describe, it, mock } from 'node:test'; +import type { + FakeUri, + CommandHandler, + TestWindowApi, + TestCommandsApi, + ActivateFn, + DeactivateFn, +} from './test/testTypes.js'; +import { uriFile } from './test/testTypes.js'; +import { BarrelGenerationMode } from './types/index.js'; +import type { ExtensionContext, ProgressOptions } from 'vscode'; + +type ProgressCall = { + options: ProgressOptions; +}; + +const commandHandlers = new Map(); +let createOutputChannelCalls: string[]; +let createdOutputChannels: Array<{ appendLine: (value: string) => void }>; +let outputChannelMessages: string[]; +let informationMessages: string[]; +let errorMessages: string[]; +let progressCalls: ProgressCall[]; +let showOpenDialogResult: FakeUri[] | undefined; +let showOpenDialogCalls: number; +let workspaceStatImpl: (uri: FakeUri) => Promise<{ type: number }>; +let configuredOutputChannel: { appendLine: (value: string) => void } | undefined; +const generatorInstances: FakeBarrelFileGenerator[] = []; +let generatorFailure: unknown; + +const FileType = { + Unknown: 0, + File: 1, + Directory: 2, + SymbolicLink: 64, +} satisfies Record; + +const ProgressLocation = { + Window: 10, +}; + +const windowApi: TestWindowApi = { + createOutputChannel(name: string) { + createOutputChannelCalls.push(name); + const channel = { + appendLine(value: string) { + outputChannelMessages.push(value); + }, + }; + createdOutputChannels.push(channel); + return channel; + }, + showInformationMessage(message: string) { + informationMessages.push(message); + return undefined; + }, + showErrorMessage(message: string) { + errorMessages.push(message); + return undefined; + }, + async showOpenDialog() { + showOpenDialogCalls += 1; + return showOpenDialogResult; + }, + async withProgress(options: ProgressOptions, task: () => Promise) { + progressCalls.push({ options }); + return task(); + }, +}; + +const commandsApi: TestCommandsApi = { + registerCommand(command: string, handler: CommandHandler): { dispose(): void } { + commandHandlers.set(command, handler); + return { + dispose() { + commandHandlers.delete(command); + }, + }; + }, +}; + +const uriApi: { file(fsPath: string): FakeUri } = { + file(fsPath: string): FakeUri { + return uriFile(fsPath); + }, +}; + +const workspaceApi: { fs: { stat(uri: FakeUri): Promise<{ type: number }> } } = { + fs: { + stat(uri: FakeUri) { + return workspaceStatImpl(uri); + }, + }, +}; + +class FakeBarrelFileGenerator { + public readonly calls: Array<{ targetDirectory: FakeUri; options: unknown }> = []; + + constructor() { + generatorInstances.push(this); + } + + async generateBarrelFile(targetDirectory: FakeUri, options: unknown): Promise { + this.calls.push({ targetDirectory, options }); + if (generatorFailure) { + throw generatorFailure; + } + } +} + +class PinoLoggerStub { + static configureOutputChannel( + channel: { appendLine: (value: string) => void } | undefined, + ): void { + configuredOutputChannel = channel; + } +} + +mock.module('vscode', { + namedExports: { + Uri: uriApi, + FileType, + ProgressLocation, + window: windowApi, + commands: commandsApi, + workspace: workspaceApi, + }, +}); + +mock.module('./core/barrel/barrel-file.generator.js', { + namedExports: { + BarrelFileGenerator: FakeBarrelFileGenerator, + }, +}); + +mock.module('./logging/pino.logger.js', { + namedExports: { + PinoLogger: PinoLoggerStub, + }, +}); + +let activate: ActivateFn; +let deactivate: DeactivateFn; + +function resetState(): void { + commandHandlers.clear(); + createOutputChannelCalls = []; + createdOutputChannels = []; + outputChannelMessages = []; + informationMessages = []; + errorMessages = []; + progressCalls = []; + showOpenDialogResult = undefined; + showOpenDialogCalls = 0; + workspaceStatImpl = async () => ({ type: FileType.Directory }); + configuredOutputChannel = undefined; + generatorInstances.length = 0; + generatorFailure = undefined; +} + +beforeEach(async () => { + resetState(); + ({ activate, deactivate } = await import('./extension.js')); +}); + +function createContext(): ExtensionContext { + const base = { subscriptions: [] as unknown[] }; + return base as unknown as ExtensionContext; +} + +function getCommand(id: string): CommandHandler { + const handler = commandHandlers.get(id); + assert.ok(handler, `Command ${id} was not registered`); + return handler; +} + +function lastGeneratorCall(): { targetDirectory: FakeUri; options: unknown } { + assert.ok(generatorInstances.length > 0, 'No generator instances were created'); + const instance = generatorInstances.at(-1)!; + assert.ok(instance.calls.length > 0, 'Generator was not invoked'); + return instance.calls.at(-1)!; +} + +describe('extension activation', () => { + it('should register commands and configure logging', async () => { + const context = createContext(); + + await activate(context); + + assert.deepStrictEqual(createOutputChannelCalls, ['Barrel Roll']); + assert.strictEqual(configuredOutputChannel, createdOutputChannels[0]); + assert.deepStrictEqual(outputChannelMessages, ['Barrel Roll: logging initialized']); + assert.deepStrictEqual(Array.from(commandHandlers.keys()), [ + 'barrel-roll.generateBarrel', + 'barrel-roll.generateBarrelRecursive', + ]); + assert.strictEqual(context.subscriptions.length, 3); + assert.strictEqual(context.subscriptions[0], createdOutputChannels[0]); + + deactivate(); + }); + + it('should generate a barrel when the command is invoked with a directory', async () => { + await activate(createContext()); + + const command = getCommand('barrel-roll.generateBarrel'); + const uri = uriApi.file('C:/workspace/src'); + + await command(uri); + + const call = lastGeneratorCall(); + assert.deepStrictEqual(call, { + targetDirectory: uri, + options: { + recursive: false, + mode: BarrelGenerationMode.CreateOrUpdate, + }, + }); + assert.deepStrictEqual( + progressCalls.map((entry) => entry.options.title), + ['Barrel Roll: Updating barrel...'], + ); + assert.deepStrictEqual(informationMessages, ['Barrel Roll: index.ts updated.']); + assert.deepStrictEqual(errorMessages, []); + }); + + it('should use the folder picker when no URI is provided', async () => { + await activate(createContext()); + + const command = getCommand('barrel-roll.generateBarrel'); + const selected = uriApi.file('C:/projects/chosen'); + showOpenDialogResult = [selected]; + + await command(); + + const call = lastGeneratorCall(); + assert.strictEqual(call.targetDirectory, selected); + assert.strictEqual(showOpenDialogCalls, 1); + }); + + it('should not run when folder selection is cancelled', async () => { + await activate(createContext()); + + const command = getCommand('barrel-roll.generateBarrel'); + showOpenDialogResult = undefined; + + await command(); + + assert.strictEqual(generatorInstances.length, 0); + assert.deepStrictEqual(informationMessages, []); + assert.deepStrictEqual(errorMessages, []); + assert.strictEqual(showOpenDialogCalls, 1); + }); + + it('should resolve file URIs to their parent directory', async () => { + await activate(createContext()); + + workspaceStatImpl = async () => ({ type: FileType.File }); + const command = getCommand('barrel-roll.generateBarrelRecursive'); + const fileUri = uriApi.file('C:/workspace/src/file.ts'); + + await command(fileUri); + + const call = lastGeneratorCall(); + assert.strictEqual(call.targetDirectory.fsPath, path.normalize('C:/workspace/src')); + }); + + it('should return the original URI when the resource type is unknown', async () => { + await activate(createContext()); + + workspaceStatImpl = async () => ({ type: FileType.SymbolicLink }); + const command = getCommand('barrel-roll.generateBarrel'); + const dirUri = uriApi.file('C:/repo/src'); + + await command(dirUri); + + const call = lastGeneratorCall(); + assert.strictEqual(call.targetDirectory, dirUri); + }); + + it('should surface errors from the generator', async () => { + await activate(createContext()); + + const command = getCommand('barrel-roll.generateBarrel'); + const uri = uriApi.file('C:/repo/src'); + generatorFailure = new Error('generation failed'); + + await command(uri); + + assert.deepStrictEqual(errorMessages, ['Barrel Roll: generation failed']); + assert.deepStrictEqual(informationMessages, []); + }); + + it('should wrap stat errors in a friendly message', async () => { + await activate(createContext()); + + const command = getCommand('barrel-roll.generateBarrel'); + const uri = uriApi.file('C:/repo/src'); + workspaceStatImpl = async () => { + throw new Error('permission denied'); + }; + + await command(uri); + + assert.deepStrictEqual(errorMessages, [ + 'Barrel Roll: Unable to access selected resource: permission denied', + ]); + assert.strictEqual(generatorInstances.length, 0); + }); +}); diff --git a/src/extension.ts b/src/extension.ts index 6183788..c37dbef 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,11 +4,11 @@ import * as vscode from 'vscode'; import { BarrelFileGenerator } from './core/barrel/barrel-file.generator.js'; import { PinoLogger } from './logging/pino.logger.js'; -import { BarrelGenerationMode, type BarrelGenerationOptions } from './types/index.js'; +import { BarrelGenerationMode, type IBarrelGenerationOptions } from './types/index.js'; type CommandDescriptor = { id: string; - options: BarrelGenerationOptions; + options: IBarrelGenerationOptions; progressTitle: string; successMessage: string; }; diff --git a/src/logging/pino.logger.test.ts b/src/logging/pino.logger.test.ts new file mode 100644 index 0000000..9672f9d --- /dev/null +++ b/src/logging/pino.logger.test.ts @@ -0,0 +1,286 @@ +import assert from 'node:assert/strict'; +import { afterEach, beforeEach, describe, it, mock } from 'node:test'; +import type { Logger, LoggerOptions } from 'pino'; +import type { OutputChannel } from 'vscode'; +import type { PinoLoggerConstructor } from '../test/testTypes.js'; + +describe('PinoLogger', () => { + type CallStore = { + info: Array<[unknown, string]>; + debug: Array<[unknown, string]>; + warn: Array<[unknown, string]>; + error: Array<[unknown, string]>; + fatal: Array<[unknown, string]>; + }; + + const createCallStore = (): CallStore => ({ + info: [], + debug: [], + warn: [], + error: [], + fatal: [], + }); + + const makeLogger = (calls: CallStore, childArgs: unknown[], childLogger?: Logger): Logger => { + return { + info(metadata: unknown, message: string) { + calls.info.push([metadata, message]); + }, + debug(metadata: unknown, message: string) { + calls.debug.push([metadata, message]); + }, + warn(metadata: unknown, message: string) { + calls.warn.push([metadata, message]); + }, + error(metadata: unknown, message: string) { + calls.error.push([metadata, message]); + }, + fatal(metadata: unknown, message: string) { + calls.fatal.push([metadata, message]); + }, + child(metadata: unknown) { + childArgs.push(metadata); + return childLogger ?? makeLogger(createCallStore(), childArgs); + }, + } as unknown as Logger; + }; + + const mockIsoTime = () => '2025-01-01T00:00:00.000Z'; + const restoreLogLevel = (value: string | undefined): void => { + if (value === undefined) { + delete process.env.LOG_LEVEL; + return; + } + process.env.LOG_LEVEL = value; + }; + + let mockPinoLogger: Logger; + let mockChildLogger: Logger; + let rootCalls: CallStore; + let childCalls: CallStore; + let childMetadataArgs: unknown[]; + let outputLines: string[]; + let lastOptions: LoggerOptions | undefined; + let shouldThrowOnCreate = false; + let consoleWarnings: unknown[][]; + let PinoLogger: PinoLoggerConstructor; + let originalWarn: typeof console.warn; + let previousLogLevel: string | undefined; + + mock.module('pino', { + defaultExport: Object.assign( + (options?: LoggerOptions) => { + lastOptions = options; + if (shouldThrowOnCreate) { + throw new Error('pino init failure'); + } + return mockPinoLogger; + }, + { + stdTimeFunctions: { + isoTime: mockIsoTime, + }, + }, + ), + }); + + beforeEach(async () => { + previousLogLevel = process.env.LOG_LEVEL; + + rootCalls = createCallStore(); + childCalls = createCallStore(); + childMetadataArgs = []; + outputLines = []; + lastOptions = undefined; + shouldThrowOnCreate = false; + consoleWarnings = []; + + // Capture and override console.warn for test inspection + originalWarn = console.warn.bind(console); + console.warn = (...args: unknown[]) => { + consoleWarnings.push(args); + }; + + mockChildLogger = makeLogger(childCalls, childMetadataArgs); + mockPinoLogger = makeLogger(rootCalls, childMetadataArgs, mockChildLogger); + + ({ PinoLogger } = (await import('./pino.logger.js')) as unknown as { + PinoLogger: PinoLoggerConstructor; + }); + + PinoLogger.configureOutputChannel({ + appendLine(line: string) { + outputLines.push(line); + }, + } as OutputChannel); + }); + + afterEach(() => { + PinoLogger.configureOutputChannel(undefined); + console.warn = originalWarn; + // Restore LOG_LEVEL if tests changed it + restoreLogLevel(previousLogLevel); + }); + + it('should use default configuration when LOG_LEVEL env is set', () => { + const previousLogLevel = process.env.LOG_LEVEL; + process.env.LOG_LEVEL = 'warn'; + try { + const logger = new PinoLogger(); + + assert.ok(lastOptions); + assert.strictEqual(lastOptions?.level, 'warn'); + assert.strictEqual(logger.isLoggerAvailable(), true); + } finally { + restoreLogLevel(previousLogLevel); + } + }); + + it('should set timestamp and formatter defaults when no options are provided', () => { + const logger = new PinoLogger(); + assert.ok(logger); + + assert.strictEqual(lastOptions?.transport, undefined); + assert.strictEqual(lastOptions?.timestamp, mockIsoTime); + assert.strictEqual(typeof lastOptions?.formatters?.level, 'function'); + }); + + it('should pass provided options through to pino', () => { + const options: LoggerOptions = { level: 'debug' }; + const logger = new PinoLogger(options); + + assert.strictEqual(lastOptions, options); + logger.info('custom message'); + assert.strictEqual(rootCalls.info.length, 1); + }); + + it('should log info messages with metadata', () => { + const logger = new PinoLogger(); + + logger.info('initialized', { service: 'barrel' }); + + assert.deepStrictEqual(rootCalls.info, [[{ service: 'barrel' }, 'initialized']]); + assert.deepStrictEqual(outputLines, ['[INFO] initialized {"service":"barrel"}']); + }); + + it('should omit metadata from debug output when none is provided', () => { + const logger = new PinoLogger(); + + logger.debug('diagnostic'); + + assert.deepStrictEqual(rootCalls.debug, [[{}, 'diagnostic']]); + assert.deepStrictEqual(outputLines, ['[DEBUG] diagnostic']); + }); + + it('should log warnings with metadata', () => { + const logger = new PinoLogger(); + + logger.warn('threshold exceeded', { attempt: 3 }); + + assert.deepStrictEqual(rootCalls.warn, [[{ attempt: 3 }, 'threshold exceeded']]); + assert.deepStrictEqual(outputLines, ['[WARN] threshold exceeded {"attempt":3}']); + }); + + it('should normalize errors before logging', () => { + const logger = new PinoLogger(); + const error = new Error('boom'); + + logger.error('operation failed', { error }); + + assert.deepStrictEqual(rootCalls.error, [[{ error }, 'operation failed']]); + assert.deepStrictEqual(outputLines, ['[ERROR] operation failed {"error":"boom"}']); + }); + + it('should leave string errors unchanged during normalization', () => { + const logger = new PinoLogger(); + const normalizeError = ( + logger as unknown as { normalizeError(error: unknown): string } + ).normalizeError.bind(logger); + + assert.strictEqual(normalizeError('fail'), 'fail'); + }); + + it('should prefix fatal messages for action failures', () => { + const logger = new PinoLogger(); + + logger.fatal('deploy'); + + assert.deepStrictEqual(rootCalls.fatal, [[{}, 'Action failed: deploy']]); + assert.deepStrictEqual(outputLines, ['[FATAL] Action failed: deploy']); + }); + + it('should skip output channel writes when none is configured', () => { + const logger = new PinoLogger(); + PinoLogger.configureOutputChannel(undefined); + + logger.info('quiet'); + + assert.deepStrictEqual(outputLines, []); + }); + + it('should stringify circular metadata safely', () => { + const logger = new PinoLogger(); + const circular: Record = {}; + circular.self = circular; + + logger.info('circular', circular); + + assert.deepStrictEqual(rootCalls.info, [[circular, 'circular']]); + assert.deepStrictEqual(outputLines, ['[INFO] circular [object Object]']); + }); + + it('should use a child logger when grouping operations and restore afterward', async () => { + const logger = new PinoLogger(); + + const result = await ( + logger as unknown as { group(name: string, fn: () => Promise): Promise } + ).group('build', async () => 42); + + assert.strictEqual(result, 42); + assert.deepStrictEqual(childMetadataArgs, [{ group: 'build' }]); + assert.deepStrictEqual(childCalls.info, [ + [{}, 'Starting group: build'], + [{}, 'Completed group: build'], + ]); + assert.deepStrictEqual(outputLines, [ + '[GROUP] Starting group: build', + '[GROUP] Completed group: build', + ]); + + logger.debug('post-group'); + assert.deepStrictEqual(childCalls.debug, []); + assert.deepStrictEqual(rootCalls.debug.at(-1), [{}, 'post-group']); + }); + + it('should propagate errors from grouped operations with normalized metadata', async () => { + const logger = new PinoLogger(); + const failure = { code: 'EFAIL' }; + + await assert.rejects( + ( + logger as unknown as { group(name: string, fn: () => Promise): Promise } + ).group('failures', async () => { + throw failure; + }), + (error) => error === failure, + ); + + const expectedMetadata = JSON.stringify({ error: JSON.stringify(failure) }); + assert.deepStrictEqual(childCalls.error, [[{ error: failure }, 'Failed in group: failures']]); + assert.strictEqual(outputLines.at(-1), `[ERROR] Failed in group: failures ${expectedMetadata}`); + }); + + it('should fall back to a no-op logger when pino initialization fails', () => { + shouldThrowOnCreate = true; + + const logger = new PinoLogger(); + + assert.strictEqual(logger.isLoggerAvailable(), false); + assert.strictEqual(consoleWarnings.length, 1); + + logger.info('fallback'); + + assert.deepStrictEqual(rootCalls.info, []); + assert.deepStrictEqual(outputLines, ['[INFO] fallback']); + }); +}); diff --git a/src/logging/pino.logger.ts b/src/logging/pino.logger.ts index bd1ed97..035ccc3 100644 --- a/src/logging/pino.logger.ts +++ b/src/logging/pino.logger.ts @@ -21,6 +21,7 @@ import type { OutputChannel } from 'vscode'; import { isObject, isString } from '../utils/guards.js'; type LogMetadata = Record; +type PinoLevel = 'info' | 'debug' | 'warn' | 'error' | 'fatal'; /** * Minimal structured logger used by the extension. Falls back to a no-op implementation if pino @@ -34,19 +35,7 @@ export class PinoLogger { constructor(options?: pino.LoggerOptions) { try { - this.logger = pino( - options || { - level: process.env.LOG_LEVEL || 'info', - transport: { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: 'yyyy-mm-dd HH:MM:ss', - ignore: 'pid,hostname', - }, - }, - }, - ); + this.logger = pino(this.resolveOptions(options)); } catch (error) { this.isAvailable = false; console.warn('Pino logger initialization failed:', error); @@ -76,8 +65,7 @@ export class PinoLogger { * @param metadata - Optional metadata to include with the log. */ info(message: string, metadata?: LogMetadata): void { - this.logger.info(metadata || {}, message); - this.appendToOutputChannel('INFO', message, metadata); + this.logWithLevel('info', message, metadata); } /** @@ -85,8 +73,7 @@ export class PinoLogger { * @param message - The message to log. */ debug(message: string): void { - this.logger.debug({}, message); - this.appendToOutputChannel('DEBUG', message); + this.logWithLevel('debug', message); } /** @@ -95,8 +82,7 @@ export class PinoLogger { * @param metadata - Optional metadata to include with the log. */ warn(message: string, metadata?: LogMetadata): void { - this.logger.warn(metadata || {}, message); - this.appendToOutputChannel('WARN', message, metadata); + this.logWithLevel('warn', message, metadata); } /** @@ -105,8 +91,7 @@ export class PinoLogger { * @param metadata - Optional metadata to include with the log. */ error(message: string, metadata?: LogMetadata): void { - this.logger.error(metadata || {}, message); - this.appendToOutputChannel('ERROR', message, metadata); + this.logWithLevel('error', message, metadata); } /** @@ -115,8 +100,7 @@ export class PinoLogger { * @param metadata - Optional metadata to include with the failure. */ fatal(message: string, metadata?: LogMetadata): void { - this.logger.fatal(metadata || {}, `Action failed: ${message}`); - this.appendToOutputChannel('FATAL', `Action failed: ${message}`, metadata); + this.logWithLevel('fatal', `Action failed: ${message}`, metadata); } /** @@ -151,21 +135,12 @@ export class PinoLogger { private appendToOutputChannel(level: string, message: string, metadata?: LogMetadata): void { const channel = PinoLogger.sharedOutputChannel; - if (!channel) { - return; - } - - const formattedMetadata = this.formatMetadata(metadata); - const line = formattedMetadata - ? `[${level}] ${message} ${formattedMetadata}` - : `[${level}] ${message}`; - channel.appendLine(line); + if (!channel) return; + channel.appendLine(this.formatOutputLine(level, message, metadata)); } private formatMetadata(metadata?: LogMetadata): string | undefined { - if (!metadata || Object.keys(metadata).length === 0) { - return undefined; - } + if (!metadata || Object.keys(metadata).length === 0) return; const normalized = Object.entries(metadata).reduce>( (accumulator, [key, value]) => { @@ -179,25 +154,44 @@ export class PinoLogger { } private normalizeError(error: unknown): string { - if (error instanceof Error) { - return error.stack || error.message; - } - if (isObject(error) && error !== null) { - return this.safeStringify(error); - } + if (error instanceof Error) return error.stack || error.message; + if (isObject(error)) return this.safeStringify(error); return String(error); } private safeStringify(value: unknown): string { - if (isString(value)) { - return value; - } + if (isString(value)) return value; try { return JSON.stringify(value); } catch { return String(value); } } + + private logWithLevel(level: PinoLevel, message: string, metadata?: LogMetadata): void { + const payload = metadata ?? {}; + this.logger[level](payload, message); + this.appendToOutputChannel(level.toUpperCase(), message, metadata); + } + + private formatOutputLine(level: string, message: string, metadata?: LogMetadata): string { + const formattedMetadata = this.formatMetadata(metadata); + return formattedMetadata + ? `[${level}] ${message} ${formattedMetadata}` + : `[${level}] ${message}`; + } + + private resolveOptions(options?: pino.LoggerOptions): pino.LoggerOptions { + if (options) return options; + return { + level: process.env.LOG_LEVEL || 'info', + // Avoid transports (pino-pretty, thread-stream) that don't work when bundled + formatters: { + level: (label) => ({ level: label }), + }, + timestamp: pino.stdTimeFunctions.isoTime, + }; + } } function createFallbackLogger(): pino.Logger { diff --git a/src/test/suite/barrel-content.builder.test.ts b/src/test/suite/barrel-content.builder.test.ts index 472f5b6..d0e2a23 100644 --- a/src/test/suite/barrel-content.builder.test.ts +++ b/src/test/suite/barrel-content.builder.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; diff --git a/src/test/suite/barrel-file.generator.test.ts b/src/test/suite/barrel-file.generator.test.ts index 1de019a..fea24be 100644 --- a/src/test/suite/barrel-file.generator.test.ts +++ b/src/test/suite/barrel-file.generator.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import * as fs from 'node:fs/promises'; import * as os from 'node:os'; diff --git a/src/test/suite/export.parser.test.ts b/src/test/suite/export.parser.test.ts index bf88e14..c76ecce 100644 --- a/src/test/suite/export.parser.test.ts +++ b/src/test/suite/export.parser.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; diff --git a/src/test/testTypes.ts b/src/test/testTypes.ts new file mode 100644 index 0000000..10cf0b5 --- /dev/null +++ b/src/test/testTypes.ts @@ -0,0 +1,44 @@ +import * as path from 'node:path'; + +import type { ExtensionContext, ProgressOptions, Uri as VsCodeUri } from 'vscode'; + +export type FakeUri = Pick; + +export function uriFile(fsPath: string): FakeUri { + return { fsPath: path.normalize(fsPath) }; +} + +export type CommandHandler = (uri?: FakeUri) => unknown; + +export type TestWindowApi = { + createOutputChannel(name: string): { appendLine(value: string): void }; + showInformationMessage(message: string): unknown; + showErrorMessage(message: string): unknown; + showOpenDialog(): Promise; + withProgress(options: ProgressOptions, task: () => Promise): Promise; +}; + +export type TestCommandsApi = { + registerCommand(command: string, handler: CommandHandler): { dispose(): void }; +}; + +export type TestWorkspaceApi = { fs: { stat(uri: FakeUri): Promise<{ type: number }> } }; + +export type ActivateFn = (context: ExtensionContext) => Promise | void; +export type DeactivateFn = () => void; + +// Minimal runtime shape for the PinoLogger class used in tests +export interface PinoLoggerInstance { + isLoggerAvailable(): boolean; + info(message: string, metadata?: Record): void; + debug(message: string, metadata?: Record): void; + warn(message: string, metadata?: Record): void; + error(message: string, metadata?: Record): void; + fatal(message: string, metadata?: Record): void; + group?(name: string, fn: () => Promise): Promise; +} + +export interface PinoLoggerConstructor { + new (...args: unknown[]): PinoLoggerInstance; + configureOutputChannel(channel?: { appendLine(value: string): void }): void; +} diff --git a/src/types/barrel.ts b/src/types/barrel.ts new file mode 100644 index 0000000..fa8e8c2 --- /dev/null +++ b/src/types/barrel.ts @@ -0,0 +1,73 @@ +/** + * Defines the kinds of exports that can be present in a barrel file. + */ +export enum BarrelExportKind { + Value = 'value', + Type = 'type', + Default = 'default', +} + +/** + * Defines the modes for generating barrel files. + */ +export enum BarrelGenerationMode { + CreateOrUpdate = 'createOrUpdate', + UpdateExisting = 'updateExisting', +} + +/** + * Defines the kinds of entries that can exist within a barrel. + */ +export enum BarrelEntryKind { + File = 'file', + Directory = 'directory', +} + +/** + * Options for generating barrel files. + */ +export interface IBarrelGenerationOptions { + recursive?: boolean; + mode?: BarrelGenerationMode; +} + +/** + * Represents a parsed export statement with its name and whether it is type-only. + */ +export interface IParsedExport { + name: string; + typeOnly: boolean; +} + +/** + * Represents an entry in a barrel, which can be either a file with exports or a directory. + */ +export type BarrelEntry = + | { + kind: BarrelEntryKind.File; + exports: BarrelExport[]; + } + | { + kind: BarrelEntryKind.Directory; + }; + +/** + * Represents an export within a barrel, which can be a value export, type export, or default export. + */ +export type BarrelExport = + | { + kind: BarrelExportKind.Value; + name: string; + } + | { + kind: BarrelExportKind.Type; + name: string; + } + | { + kind: BarrelExportKind.Default; + }; + +/** + * Normalized options for generating barrel files, with all properties required. + */ +export type NormalizedBarrelGenerationOptions = Required; diff --git a/src/types/barrel/BarrelEntry.ts b/src/types/barrel/BarrelEntry.ts deleted file mode 100644 index e593229..0000000 --- a/src/types/barrel/BarrelEntry.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BarrelEntryKind } from './BarrelEntryKind'; -import { BarrelExport } from './BarrelExport'; - -/** - * Represents an entry in a barrel, which can be either a file with exports or a directory. - */ -export type BarrelEntry = - | { - kind: BarrelEntryKind.File; - exports: BarrelExport[]; - } - | { - kind: BarrelEntryKind.Directory; - }; diff --git a/src/types/barrel/BarrelEntryKind.ts b/src/types/barrel/BarrelEntryKind.ts deleted file mode 100644 index a473b83..0000000 --- a/src/types/barrel/BarrelEntryKind.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Defines the kinds of entries that can exist within a barrel. - */ -export enum BarrelEntryKind { - File = 'file', - Directory = 'directory', -} diff --git a/src/types/barrel/BarrelExport.ts b/src/types/barrel/BarrelExport.ts deleted file mode 100644 index e7ab32f..0000000 --- a/src/types/barrel/BarrelExport.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BarrelExportKind } from './BarrelExportKind'; - -/** - * Represents an export within a barrel, which can be a value export, type export, or default export. - */ -export type BarrelExport = - | { - kind: BarrelExportKind.Value; - name: string; - } - | { - kind: BarrelExportKind.Type; - name: string; - } - | { - kind: BarrelExportKind.Default; - }; diff --git a/src/types/barrel/BarrelExportKind.ts b/src/types/barrel/BarrelExportKind.ts deleted file mode 100644 index 0c2bab8..0000000 --- a/src/types/barrel/BarrelExportKind.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Defines the kinds of exports that can be present in a barrel file. - */ -export enum BarrelExportKind { - Value = 'value', - Type = 'type', - Default = 'default', -} diff --git a/src/types/barrel/BarrelGenerationMode.ts b/src/types/barrel/BarrelGenerationMode.ts deleted file mode 100644 index aca1b6c..0000000 --- a/src/types/barrel/BarrelGenerationMode.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Defines the modes for generating barrel files. - */ -export enum BarrelGenerationMode { - CreateOrUpdate = 'createOrUpdate', - UpdateExisting = 'updateExisting', -} diff --git a/src/types/barrel/BarrelGenerationOptions.ts b/src/types/barrel/BarrelGenerationOptions.ts deleted file mode 100644 index e000eb9..0000000 --- a/src/types/barrel/BarrelGenerationOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BarrelGenerationMode } from './BarrelGenerationMode'; - -/** - * Options for generating barrel files. - */ -export interface BarrelGenerationOptions { - recursive?: boolean; - mode?: BarrelGenerationMode; -} diff --git a/src/types/barrel/NormalizedBarrelGenerationOptions.ts b/src/types/barrel/NormalizedBarrelGenerationOptions.ts deleted file mode 100644 index e39c54b..0000000 --- a/src/types/barrel/NormalizedBarrelGenerationOptions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BarrelGenerationOptions } from './BarrelGenerationOptions'; - -/** - * Normalized options for generating barrel files, with all properties required. - */ -export type NormalizedBarrelGenerationOptions = Required; diff --git a/src/types/barrel/ParsedExport.ts b/src/types/barrel/ParsedExport.ts deleted file mode 100644 index 5e41a8f..0000000 --- a/src/types/barrel/ParsedExport.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Represents a parsed export statement with its name and whether it is type-only. - */ -export interface ParsedExport { - name: string; - typeOnly: boolean; -} diff --git a/src/types/barrel/index.ts b/src/types/barrel/index.ts deleted file mode 100644 index 290da49..0000000 --- a/src/types/barrel/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './BarrelEntry.js'; -export * from './BarrelEntryKind.js'; -export * from './BarrelExport.js'; -export * from './BarrelExportKind.js'; -export * from './BarrelGenerationMode.js'; -export * from './BarrelGenerationOptions.js'; -export * from './NormalizedBarrelGenerationOptions.js'; -export * from './ParsedExport.js'; diff --git a/src/types/index.ts b/src/types/index.ts index bb1a004..c66f17e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,5 +2,7 @@ export { type IEnvironmentVariables } from './env.js'; // Export barrel-related shared types and constants -export * from './barrel/index.js'; +// istanbul ignore next +export * from './barrel.js'; +// istanbul ignore next export * from './constants.js'; diff --git a/src/utils/array.test.ts b/src/utils/array.test.ts new file mode 100644 index 0000000..52a3464 --- /dev/null +++ b/src/utils/array.test.ts @@ -0,0 +1,29 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { isEmptyArray } from './array.js'; + +function formatValue(value: unknown): string { + return Array.isArray(value) ? `[${value.map(String).join(', ')}]` : String(value); +} + +describe('array utils', () => { + describe('isEmptyArray', () => { + const cases: ReadonlyArray<[unknown, boolean]> = [ + [null, true], + [undefined, true], + [[], true], + [[1, 2, 3], false], + [['a'], false], + [['test'] as readonly string[], false], + [[] as readonly string[], true], + ]; + + for (const [input, expected] of cases) { + const description = `should return ${expected} when input is ${formatValue(input)}`; + + it(description, () => { + assert.strictEqual(isEmptyArray(input as readonly unknown[] | null | undefined), expected); + }); + } + }); +}); diff --git a/src/utils/guards.test.ts b/src/utils/guards.test.ts new file mode 100644 index 0000000..5f44dab --- /dev/null +++ b/src/utils/guards.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { isObject, isString } from './guards.js'; + +describe('isObject', () => { + it('should return true for plain objects', () => { + assert.equal(isObject({}), true); + assert.equal(isObject({ key: 'value' }), true); + }); + + it('should return true for object instances', () => { + assert.equal(isObject(new Object()), true); + }); + + void it('should return false for null', () => { + assert.equal(isObject(null), false); + }); + + it('should return false for undefined', () => { + assert.equal(isObject(undefined), false); + }); + + it('should return false for primitives', () => { + assert.equal(isObject(42), false); + assert.equal(isObject('string'), false); + assert.equal(isObject(true), false); + }); + + it('should return true for arrays (as they are objects)', () => { + assert.equal(isObject([]), true); + assert.equal(isObject([1, 2, 3]), true); + }); + + it('should return false for functions', () => { + assert.equal( + isObject(() => {}), + false, + ); + }); +}); + +describe('isString', () => { + it('should return true for string primitives', () => { + assert.equal(isString('hello'), true); + assert.equal(isString(''), true); + assert.equal(isString(`template`), true); + }); + + it('should return false for String coercions', () => { + const boxedString = Reflect.construct(String, ['hello']); + assert.equal(isString(boxedString), false); + }); + + it('should return false for null', () => { + assert.equal(isString(null), false); + }); + + it('should return false for undefined', () => { + assert.equal(isString(undefined), false); + }); + + it('should return false for other primitives', () => { + assert.equal(isString(42), false); + assert.equal(isString(true), false); + }); + + it('should return false for objects', () => { + assert.equal(isString({}), false); + assert.equal(isString([]), false); + }); +}); diff --git a/src/utils/string.test.ts b/src/utils/string.test.ts index 7e9f608..b935977 100644 --- a/src/utils/string.test.ts +++ b/src/utils/string.test.ts @@ -1,62 +1,62 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { splitAndClean, sortAlphabetically } from './string.js'; -describe('splitAndClean', () => { - it('should split by default comma delimiter, trim, and remove empty fragments', () => { - assert.deepStrictEqual(splitAndClean('a, b , c'), ['a', 'b', 'c']); - assert.deepStrictEqual(splitAndClean('a,,b, ,c'), ['a', 'b', 'c']); - }); - - it('should split by custom string delimiter', () => { - assert.deepStrictEqual(splitAndClean('a;b;c', ';'), ['a', 'b', 'c']); - }); - - it('should split by RegExp delimiter', () => { - assert.deepStrictEqual(splitAndClean('a1b2c', /\d/), ['a', 'b', 'c']); - }); - - it('should handle empty string', () => { - assert.deepStrictEqual(splitAndClean(''), []); - }); - - it('should handle string with only delimiters and spaces', () => { - assert.deepStrictEqual(splitAndClean(', , ,'), []); - }); - - it('should handle no splits needed', () => { - assert.deepStrictEqual(splitAndClean('abc'), ['abc']); - }); -}); - -describe('sortAlphabetically', () => { - it('should sort strings alphabetically', () => { - assert.deepStrictEqual(sortAlphabetically(['c', 'a', 'b']), ['a', 'b', 'c']); - }); - - it('should handle empty array', () => { - assert.deepStrictEqual(sortAlphabetically([]), []); - }); - - it('should handle single item', () => { - assert.deepStrictEqual(sortAlphabetically(['a']), ['a']); - }); - - it('should sort with locale', () => { - assert.deepStrictEqual(sortAlphabetically(['ä', 'a'], 'de'), ['a', 'ä']); - }); - - it('should sort with options', () => { - assert.deepStrictEqual(sortAlphabetically(['A', 'a'], undefined, { sensitivity: 'base' }), [ - 'A', - 'a', - ]); - }); - - it('should handle iterable input', () => { - assert.deepStrictEqual(sortAlphabetically(new Set(['c', 'a', 'b'])), ['a', 'b', 'c']); +describe('string utils', () => { + describe('splitAndClean', () => { + it('should split by default comma delimiter, trim, and remove empty fragments', () => { + assert.deepStrictEqual(splitAndClean('a, b , c'), ['a', 'b', 'c']); + assert.deepStrictEqual(splitAndClean('a,,b, ,c'), ['a', 'b', 'c']); + }); + + it('should split by custom string delimiter', () => { + assert.deepStrictEqual(splitAndClean('a;b;c', ';'), ['a', 'b', 'c']); + }); + + it('should split by RegExp delimiter', () => { + assert.deepStrictEqual(splitAndClean('a1b2c', /\d/), ['a', 'b', 'c']); + }); + + it('should handle empty string', () => { + assert.deepStrictEqual(splitAndClean(''), []); + }); + + it('should handle string with only delimiters and spaces', () => { + assert.deepStrictEqual(splitAndClean(', , ,'), []); + }); + + it('should handle no splits needed', () => { + assert.deepStrictEqual(splitAndClean('abc'), ['abc']); + }); + }); + + describe('sortAlphabetically', () => { + it('should sort strings alphabetically', () => { + assert.deepStrictEqual(sortAlphabetically(['c', 'a', 'b']), ['a', 'b', 'c']); + }); + + it('should handle empty array', () => { + assert.deepStrictEqual(sortAlphabetically([]), []); + }); + + it('should handle single item', () => { + assert.deepStrictEqual(sortAlphabetically(['a']), ['a']); + }); + + it('should sort with locale', () => { + assert.deepStrictEqual(sortAlphabetically(['ä', 'a'], 'de'), ['a', 'ä']); + }); + + it('should sort with options', () => { + assert.deepStrictEqual(sortAlphabetically(['A', 'a'], undefined, { sensitivity: 'base' }), [ + 'A', + 'a', + ]); + }); + + it('should handle iterable input', () => { + assert.deepStrictEqual(sortAlphabetically(new Set(['c', 'a', 'b'])), ['a', 'b', 'c']); + }); }); }); diff --git a/tsconfig.json b/tsconfig.json index 224cfac..437e551 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, - "outDir": "./out", + "outDir": "./dist", "paths": { "@/*": ["src/*"] }, @@ -26,6 +26,6 @@ "target": "ES2022", "useUnknownInCatchVariables": true }, - "exclude": [".vscode-test", "tests", "test-utils", "node_modules", "out", "src/**/*.test.ts"], + "exclude": [".vscode-test", "tests", "test-utils", "node_modules", "dist", "src/**/*.test.ts"], "include": ["src/**/*.ts"] } diff --git a/tsconfig.test-suite.json b/tsconfig.test-suite.json index eb712e6..f17510c 100644 --- a/tsconfig.test-suite.json +++ b/tsconfig.test-suite.json @@ -1,16 +1,13 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", "compilerOptions": { "allowJs": true, "module": "NodeNext", "moduleResolution": "NodeNext", - "noEmit": false, - "outDir": "./out", "removeComments": false, - "sourceMap": false, "types": ["node"] }, - "include": ["src/**/*.ts"], - "exclude": ["src/__tests__/**/*", "node_modules", "out", ".vscode-test"] + "exclude": ["src/__tests__/**/*", "node_modules", "dist", ".vscode-test"], + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"] } diff --git a/tsconfig.test.json b/tsconfig.test.json index 8a43339..605ff1d 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,6 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", "compilerOptions": { "allowJs": true, "module": "NodeNext", @@ -9,6 +8,7 @@ "sourceMap": true, "types": ["node"] }, - "include": ["src/**/*.ts", "__tests__/**/*", "__mocks__/**/*"], - "exclude": ["out", "node_modules", ".vscode-test"] + "exclude": ["dist", "node_modules", ".vscode-test"], + "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "__tests__/**/*", "__mocks__/**/*"] } diff --git a/webpack.config.cjs b/webpack.config.cjs index c4b3eaa..95e3ee8 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -2,7 +2,8 @@ 'use strict'; -const path = require('path'); +const path = require('node:path'); +const webpack = require('webpack'); /**@type {import('webpack').Configuration}*/ const config = { @@ -10,12 +11,16 @@ const config = { entry: './src/extension.ts', mode: 'production', output: { - path: path.resolve(__dirname, 'out'), + path: path.resolve(__dirname, 'dist'), filename: 'extension.js', libraryTarget: 'commonjs2', devtoolModuleFilenameTemplate: '../[resource-path]', }, devtool: 'source-map', + plugins: [ + // Ignore optional transports and thread-stream used by pino to avoid bundling them + new webpack.IgnorePlugin({ resourceRegExp: /pino-pretty|thread-stream/ }), + ], externals: { vscode: 'commonjs vscode', },