diff --git a/.gitignore b/.gitignore index 2480fd52b..b3593c113 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,5 @@ tmp/ **.testproj dbs *.cds.json +.cds-extractor-cache + diff --git a/extractors/README.md b/extractors/README.md index 8f8a2f00e..0c394a9e5 100644 --- a/extractors/README.md +++ b/extractors/README.md @@ -30,16 +30,20 @@ pre-finalize.sh`"] JSE[[javascript extractor]] DTRAC[codeql database
trace-command] SPF[[pre-finalize.sh]] - DIDX[codeql database index-files
--language=cds
--include-extension=.cds] - SIF[[index-files.sh]] - SIT[[index-files.ts/js]] - NPM[[npm install & build]] - DETS[[Determine CDS command]] - FIND[[Find package.json dirs]] - INST[[Install dependencies]] - CC[[cds compiler]] + ABCMD[[autobuild.sh/cmd]] + ABT[[cds-extractor.ts/js]] + ENV[[setup & validate
environment]] + PDG[[build project
dependency graph]] + INSTC[[install dependencies
with caching]] + PROC[[process CDS files
to JSON]] + PMAP[[project-aware
dependency resolution]] + FIND[[find project for
CDS file]] + CDCMD[[determine CDS
command for project]] + COMP[[compile CDS
to JSON]] CDJ([.cds.json files]) + FILT[[configure LGTM
index filters]] JSA[[javascript extractor
autobuild script]] + DIAG[[add compilation
diagnostics]] TF([CodeQL TRAP files]) DBF[codeql database finalize
-- /path/to/database] @@ -54,20 +58,30 @@ pre-finalize.sh`"] JSE ==> |run autobuild within
the javascript extractor| DTRAC DTRAC ==> |run the build --command| SPF - SPF ==> |run codeql index-files
for CDS files| DIDX - DIDX ==> |invoke script via
--search-path| SIF - SIF ==> |runs TypeScript version
after npm install| NPM - NPM ==> |executes compiled
index-files.js| SIT + SPF ==> |run autobuilder
for CDS files| ABCMD + ABCMD ==> |runs TypeScript version
of CDS extractor| ABT - SIT ==> |finds project directories
with package.json| FIND - FIND ==> |install CDS dependencies
in project directories| INST - SIT ==> |determines which
cds command to use| DETS - DETS ==> |processes each CDS file| CC + ABT ==> |setup and validate
environment first| ENV + ABT ==> |build project dependency
graph for source root| PDG + PDG ==> |analyze CDS projects
structure & relationships| PMAP + + ABT ==> |efficiently install
required dependencies| INSTC + INSTC ==> |use cached approach for
dependency installation| PMAP + + ABT ==> |process each CDS file
to generate JSON files| PROC + PROC ==> |find which project
contains this CDS file| FIND + FIND ==> |uses project-aware
dependency resolution| PMAP + FIND ==> |determine appropriate
CDS command for project| CDCMD + + CDCMD ==> |compile CDS file to JSON
with project context| COMP + COMP ==> |generate JSON representation
with project awareness| CDJ + COMP --x |if compilation fails,
report diagnostics| DIAG + DIAG -.-> |diagnostics stored
in database| DB - CC ==> |compile .cds files to
create .cds.json files| CDJ CDJ -.-> |stored in same location
as original .cds files| DB - SIT ==> |configures extraction
filters for JSON files| JSA + ABT ==> |configure extraction
filters for JSON files| FILT + ABT ==> |run JavaScript extractor
to process JSON files| JSA JSA ==> |processes .cds.json files
via javascript extractor| CDJ CDJ ==> |javascript extractor
generates TRAP files| TF diff --git a/extractors/cds/tools/.gitignore b/extractors/cds/tools/.gitignore deleted file mode 100644 index 6f8fa7ef3..000000000 --- a/extractors/cds/tools/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Ignore the entire "out" directory as this is for the .js and .js.map files -# which are generated by the `tsc` build process. In the current project config, -# we require the platform-specific "index-files" shell/cmd script to run the -# `npm run build` command that generates the files for the correct platform and -# local environment. -out/ - -# Since we expect the build process to be run on the system where the CDS extractor -# is being run, we do not need/want to check-in our own package-lock.json version -# when we know it will be different / overwritten on each system. -package-lock.json - diff --git a/extractors/cds/tools/README.md b/extractors/cds/tools/README.md new file mode 100644 index 000000000..e80583517 --- /dev/null +++ b/extractors/cds/tools/README.md @@ -0,0 +1,282 @@ +# CodeQL CDS Extractor + +A robust CodeQL extractor for [Core Data Services (CDS)][CDS] files used in [SAP Cloud Application Programming (CAP)][CAP] model projects. This extractor processes `.cds` files and compiles them into `.cds.json` files for CodeQL analysis while maintaining project-aware parsing and dependency resolution. + +## Overview + +The CodeQL CDS extractor is designed to efficiently process CDS projects by: + +- **Project-Aware Processing**: Analyzes CDS files as related project configurations rather than independent definitions +- **Optimized Dependency Management**: Caches and reuses `@sap/cds` and `@sap/cds-dk` dependencies across projects +- **Enhanced Precision**: Reduces false-positives in CodeQL queries by understanding cross-file relationships +- **Performance Optimization**: Avoids duplicate processing and unnecessary dependency installations + +## Architecture + +The extractor uses an `autobuild` approach with the following key components: + +### Core Components + +- **`cds-extractor.ts`**: Main entry point that orchestrates the extraction process +- **`src/cds/parser/`**: CDS project discovery and dependency graph building +- **`src/cds/compiler/`**: Compilation orchestration and `.cds.json` generation +- **`src/packageManager/`**: Dependency installation and caching +- **`src/logging/`**: Unified logging and performance tracking +- **`src/environment.ts`**: Environment setup and validation +- **`src/codeql.ts`**: CodeQL JavaScript extractor integration + +### Extraction Process + +1. **Environment Setup**: Validates CodeQL tools and system requirements +2. **Project Discovery**: Recursively scans for CDS projects and builds dependency graph +3. **Dependency Management**: Installs and caches required CDS compiler dependencies +4. **CDS Compilation**: Compiles `.cds` files to `.cds.json` using project-aware compilation +5. **JavaScript Extraction**: Runs CodeQL's JavaScript extractor on source and compiled files + +## Usage + +### Prerequisites + +- Node.js (accessible via `node` command) +- CodeQL CLI tools +- SAP CDS projects with `.cds` files + +### Running the Extractor + +The extractor is typically invoked by CodeQL during database creation: + +```bash +codeql database create --language=cds --source-root=/path/to/project my-database +``` + +### Manual Execution + +For development and testing purposes: + +```bash +# Build the extractor +npm run build + +# Run directly (from project source root) +node dist/cds-extractor.js /path/to/source/root +``` + +## Development + +### Project Structure + +```text +extractors/cds/tools/ +├── cds-extractor.ts # Main entry point +├── src/ # Source code modules +│ ├── cds/ # CDS-specific functionality +│ │ ├── compiler/ # Compilation orchestration +│ │ └── parser/ # Project discovery and parsing +│ ├── logging/ # Logging and performance tracking +│ ├── packageManager/ # Dependency management +│ ├── codeql.ts # CodeQL integration +│ ├── diagnostics.ts # Error reporting +│ ├── environment.ts # Environment setup +│ ├── filesystem.ts # File system utilities +│ └── utils.ts # General utilities +├── test/ # Test suites +├── dist/ # Compiled JavaScript output +└── package.json # Project configuration +``` + +### Building + +```bash +# Install dependencies +npm install + +# Build TypeScript to JavaScript +npm run build + +# Run all checks and build +npm run build:all +``` + +### Testing + +```bash +# Run tests +npm test + +# Run tests with coverage +npm run test:coverage + +# Run tests in watch mode +npm run test:watch +``` + +### Code Quality + +```bash +# Lint TypeScript files +npm run lint + +# Auto-fix linting issues +npm run lint:fix + +# Format code +npm run format +``` + +## Configuration + +### Environment Variables + +The extractor respects several CodeQL environment variables: + +- `CODEQL_DIST`: Path to CodeQL distribution +- `CODEQL_EXTRACTOR_CDS_WIP_DATABASE`: Target database path +- `LGTM_INDEX_FILTERS`: File filtering configuration + +### CDS Project Detection + +Projects are detected based on: + +- Presence of `package.json` files +- CDS files (`.cds`) in the project directory tree +- Valid CDS dependencies (`@sap/cds`, `@sap/cds-dk`) in package.json + +### Compilation Strategy + +The extractor uses a sophisticated compilation approach: + +1. **Dependency Graph Building**: Maps relationships between CDS projects +2. **Smart Caching**: Reuses compiled outputs and dependency installations +3. **Error Recovery**: Handles compilation failures gracefully +4. **Performance Tracking**: Monitors compilation times and resource usage + +## Performance Features + +### Optimized Dependency Management + +- **Shared Dependency Cache**: Single installation per unique dependency combination +- **Isolated Environments**: Dependencies installed in temporary cache directories +- **No Source Modification**: Original project files remain unchanged + +### Efficient Processing + +- **Project-Level Compilation**: Compiles related CDS files together +- **Duplicate Avoidance**: Prevents redundant processing of imported files +- **Memory Tracking**: Monitors and reports memory usage throughout extraction + +### Scalability + +- **Large Codebase Support**: Optimized for enterprise-scale CDS projects +- **Parallel Processing**: Where possible, processes independent projects concurrently +- **Resource Management**: Cleans up temporary files and cached dependencies + +## Integration with `cds` CLI + +### Installation of CDS (Node) Dependencies + +#### Installation of `@sap/cds` and `@sap/cds-dk` + +The CDS extractor attempts to optimize performance for most projects by caching the installation of the unique combinations of resolved CDS dependencies across all projects under a given source root. + +The "unique combinations of resolved CDS dependencies" means that we resolve the **latest** available version **within the semantic version range** for each `@sap/cds` and `@sap/cds-dk` dependency specified in the `package.json` file for a given CAP project. + +In practice, this means that if "project-a" requires `@sap/cds@^6.0.0` and "project-b" requires `@sap/cds@^7.0.0` while the latest available version is `@sap/cds@9.0.0` (as a trivial example), the extractor will install `@sap/cds@9.0.0` once and reuse it for both projects. + +This is much faster than installing all dependencies for every project individually, especially for large projects with many CDS files. However, this approach has some limitations and trade-offs: + +- This latest-first approach is more likely to choose the same version for multiple projects, which can reduce analysis time and can improve consistency in analysis between projects. +- This approach does not read (or respect) the `package-lock.json` file, which means that we are more likely to use a `cds` version that is different from the one most recently tested/used by the project developers. +- We are more likely to encounter incompatibility issues where a particular project hasn't been tested with the latest version of `@sap/cds` or `@sap/cds-dk`. + +We can mitigate some of these issues through a (to be implemented) compilation retry mechanism for projects where some CDS compilation task(s) fail to produce the expected `.cds.json` output file(s). +The proposed retry mechanism would install the full set of dependencies for the affected project(s) while respecting the `package-lock.json` file, and then re-run the compilation for the affected project(s). + +```text +TODO: retry mechanism expected before next release of the CDS extractor +``` + +#### Installation of Additional Project-Specific Dependencies + +```text +TODO: implement installation of dependencies required for compilation to succeed for a given project +``` + +### Integration with `cds compile` command + +The CDS extractor uses the `cds compile` command to compile `.cds` files into `.cds.json` files, which are then processed by CodeQL's JavaScript extractor. + +Where possible, a single `model.cds.json` file is generated for each project, containing all the compiled definitions from the project's `.cds` files. This results in a faster extraction process overall with minimal duplication of CDS code elements (e.g., annotations, entities, services, etc.) within the CodeQL database created from the extraction process. + +Where project-level compilation is not possible (e.g., due to project structure), the extractor generates individual `.cds.json` files for each `.cds` file in the project. The main downside to this approach is that if one `.cds` file imports another `.cds` file, the imported definitions will be duplicated in the CodeQL database, which can lead to false positives in queries that expect unique definitions. + +```text +TODO: use the unique (session) ID of the CDS extractor run to as the `` part of `..cds.json` and set JS extractor env vars to only extractor `..cds.json` files +``` + +### Integration with `cds env` command + +The current version of the CDS extractor expects CAP projects to follow the [default project structure][CAP-project-structure], particularly regarding the names of the (`app`, `db`, & `srv`) subdirectories in which the extractor will look for `.cds` files to process (in addition to the root directory of the project). + +The proposed solution will use the `cds env` command to discover configurations that affect the structure of the project and/or the expected "compilation tasks" for the project, such as any user customization of environment configurations such as: + +- `cds.folders.app` +- `cds.folders.db` +- `cds.folders.srv` + +```text +TODO : add support for integration with `cds env` CLI command as a means of consistently getting configurations for CAP projects +``` + +## Integration with `codeql` CLI + +### File Processing + +The extractor processes both: + +- **Source Files**: Original `.cds` files for source code analysis +- **Compiled Files**: Generated `.cds.json` files for semantic analysis + +### Database Population + +- Integrates with CodeQL's JavaScript extractor for final database population +- Maintains proper file relationships and source locations +- Supports CodeQL's standard indexing and filtering mechanisms + +## Troubleshooting + +### Common Issues + +1. **Missing Node.js**: Ensure `node` command is available in PATH +2. **CDS Dependencies**: Verify projects have valid `@sap/cds` dependencies +3. **Compilation Failures**: Check CDS syntax and cross-file references +4. **Memory Issues**: Monitor memory usage for very large projects + +### Debugging + +The extractor provides comprehensive logging: + +- **Performance Tracking**: Times for each extraction phase +- **Memory Usage**: Memory consumption at key milestones +- **Error Reporting**: Detailed error messages with context +- **Project Discovery**: Information about detected CDS projects + +### Log Levels + +- `info`: General progress and milestone information +- `warn`: Non-critical issues that don't prevent extraction +- `error`: Critical failures that may affect extraction quality + +## References + +- [SAP Cloud Application Programming Model][CAP] + - [Default Structure of a CAP Project][CAP-project-structure] +- [Core Data Services (CDS)][CDS] + - [Project-Specific Configurations][CDS-ENV-project-configs] +- [Conceptual Definition Language (CDL)][CDL] +- [CodeQL Documentation](https://codeql.github.com/docs/) + +[CAP]: https://cap.cloud.sap/docs/about/ +[CAP-project-structure]: https://cap.cloud.sap/docs/get-started/#project-structure +[CDS]: https://cap.cloud.sap/docs/cds/ +[CDS-ENV-project-configs]: https://cap.cloud.sap/docs/node.js/cds-env#project-specific-configurations +[CDL]: https://cap.cloud.sap/docs/cds/cdl diff --git a/extractors/cds/tools/autobuild.md b/extractors/cds/tools/autobuild.md deleted file mode 100644 index 550a9ac3f..000000000 --- a/extractors/cds/tools/autobuild.md +++ /dev/null @@ -1,73 +0,0 @@ -# CodeQL CDS Extractor `autobuild` Re-write Guide - -## Goals - -The primary goals of this project are to create a more robust, well-tested, and maintainable CodeQL extractor for `.cds` files that implement [Core Data Services][CDS] ([CDS]) as part of the [Cloud Application Programming] ([CAP]) model. - -## Overview - -This document provides a guide for the multi-step process of re-writing the CodeQL extractor for [CDS] by using an approach based on `autobuild` rather than `index-files`. - -This document is meant to be a common reference and a project guide while the iterative re-write is in-progress, especially since there is more to this project than a simple re-write of the scripts that comprise CodeQL's extractor (tool) for [CDS]. - -## Challenges with the Current Extractor (using `index-files`) - -The current extractor for [CDS] is based on `index-files`, which has several limitations and challenges: - -1. **Performance** - - The current extractor is slow and inefficient, especially when dealing with large projects or complex [CDS] files. This is due to the way `index-files` processes files, which can lead to long processing times and increased resource usage. There are several performance improvements that could be made to the extractor, but they are all related to avoid work that we either do not need to do or that has already been done. - - - As one example of a performance problem, using the `index-files` approach means that we are provided with a list of all `.cds` files in the project and are expected to index them all, which makes sense for CodeQL (as we want our database to have a copy of every in-scope source code file) but is horribly inefficient from a [CDS] perspective as the [CDS] format allows for a single file to contain multiple [CDS] definitions. The extractor is expected to be able to handle this by parsing the declarative syntax of the `.cds` file in order to understand which other `.cds` files are to be imported as part of that top-level file, meaning that we are expected to avoid duplicate imports of files that are already (and only) used as library-style imports in top-level (project-level) [CDS] files. This is a non-trivial task, and the current extractor does not even try to parse the contents of the `.cds` files to determine which files are actually used in the project. Instead, it simply imports all `.cds` files that are found in the project, which can lead to duplicate imports and increased processing times. - - - Another example of a performance problem is that the current `index-files`-based extractor spends a lot of time installing node dependencies because it runs a `npm install` command in every "CDS project directory" that it finds, which is every directory that contains a `package.json` file and either directly contains a `.cds` file (as a sibling of the `package.json` file) or contains some subdirectory that contains either a `.cds` file or a subdirectory that contains a `.cds` file. This means that the extractor will install these dependencies in a directory that we would rather not make changes in just to be able to use a specific version of `@sap/cds` and/or `@sap/cds-dk` (the dependencies that are needed to run the extractor). This also means that if we have five project that all use the same version of `@sap/cds` and/or `@sap/cds-dk`, we will install that version five separate times in five separate locations, which is both a waste of time and creates a cleanup challenge as the install makes changes to the `package-lock.json` file in each of those five project directories (and also makes changes to the `node_modules` subdirectory of each project directory). - -2. **Precision** - - The root-causes of the `Performance` problems can also cause CDS-specific CodeQL queries to produce false-positives in some cases. - The `.cds` files for a given project must be parsed as a set of related configurations, rather than as independent definitions, in order to avoid false-positives in some CodeQL queries. For example: - - - [bookshop/srv/admin-service.cds](https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/admin-service.cds) is reported by [EntityExposedWithoutAuthn.ql](https://github.com/advanced-security/codeql-sap-js/blob/main/javascript/frameworks[…]hn-authz/EntityExposedWithoutAuthn/EntityExposedWithoutAuthn.ql) as unprotected. This result is actually a false-positive as the service (flagged in the query result) is annotated as `@requires: 'admin'` in a separate [bookshop/srv/access-control.cds](https://github.com/SAP-samples/cloud-cap-samples/blob/main/bookshop/srv/access-control.cds) file (from the same project). - - - Running the current implementation of the CDS extractor for the `bookshop` project will create `admin-service.cds.json` (from `admin-service.cds`) -- where the service is represented without access control; and will also create `access-control.cds.json` (from `access-control.cds`) -- which represent the service again but with access control. - - - In an improved CDS extractor, compiling the whole of the `bookshop` project together should allow us to produce a single `.cds.json` file -- with a single representation of the admin service that it is correctly annotated as having access control. - -## Goals for the Future Extractor (using `autobuild`) - -The main goals for the `autobuild`-based [CDS] extractor are to: - -1. **Improve the Performance of Running the [CDS] Extractor on Large Codebases**: - The performance problems with the current `index-files`-based [CDS] extractor are compounded when running the extractor on large codebases, where the duplicate import problem is magnified in large projects that make heavy use of library-style imports. The `autobuild`-based extractor will be able to avoid this problem by using a more efficient approach to parsing the `.cds` files and determining which files are actually used in the project. This will allow us to avoid duplicate imports and reduce processing times. - -2. **Improve the Precision of Query Results for [CDS] Services**: - The precision problems of the current [CDS] extractor are also compounded when running the extractor for complex [CAP] projects and/or large codebases, where a lack of project-aware-parsing has a cascading effect as some projects may be imported by other projects and/or may contain multiple `.cds` files that are related to each other. The `autobuild`-based extractor will be able to avoid this problem by using a more efficient approach to parsing the `.cds` files and determining which files are actually used in the project. This will allow us to avoid false-positives in some CodeQL queries and improve the precision of query results for [CDS] services. - -All other goals are secondary to and/or in support of the above goals. - -## Expected Technical Changes - -- The `autobuild.ts` script/code will need to be able to determine its own list of `.cds` files to process when given a "source root" directory to be scanned (recursively) for `.cds` files and will have to maintain some form of state while determining the most efficient way to process all of the applicable [CDS] statements without duplicating work. This will be done by using a combination of parsing the `.cds` files and using a cache to keep track of which files have already been processed. The cache will be stored in a JSON file that will be created and updated as the extractor runs. This will allow the extractor to avoid re-processing files that have already been processed, which will improve performance and reduce resource usage. - -- Instead of installing node dependencies directly in each CDS project directory, the CDS extractor should keep track of the unique set of `@sap/cds` and `@sap/cds-dk` dependency combinations that are used by any "project" directory found under the "source root" directory. For each unique combination of `@sap/cds` and `@sap/cds-dk` dependencies, the CDS extractor should also create a (.hidden) directory structure to cache the associated `package.json`, `package-lock.json`, and `./node_modules/`. This will allow the CDS extractor to: - - be much more efficient in terms of installing [CDS] compiler dependencies; - - be much more explicit about which version of the [CDS] compiler we are using for a given (sub-)project; - - avoid making changes to the `package.json` and `package-lock.json` and `node_modules/` within the project directories; - - avoid installing the same version of these dependencies multiple times; - - avoid installing project dependencies that we do not actually need for the purpose of running the [CDS] compiler; - - reduce the overall time it takes to run the [CDS] extractor; - - minimize and restrict any changes made on the system where the [CDS] extractor is run. - -- Use a new `autobuild.ts` script as the main entry point for the extractor's TypeScript code, meaning that the build process will compile the TypeScript code in `autobuild.ts` to JavaScript code in `autobuild.js`, which will then be run as the main entry point for the extractor. Instead of `index-files.cmd` and `index-files.sh`, we will have wrapper scripts such as `autobuild.cmd` and `autobuild.sh` that will be used to run the `autobuild.js` script in different environments (i.e. Windows and Unix-like environments). - -- The new [autobuild.ts](./autobuild.ts) script will be a kept as minimal as possible, with object-oriented code patterns used to encapsulate the functionality of the extractor in `.ts` files stored in a new `src` directory (project path would be `extractors/cds/tools/src`). This will allow us to break the extractor into smaller, more manageable pieces, and will also make it easier to test and maintain the code over time. The new `src` directory will contain all of the TypeScript code for the extractor, and will be organized into subdirectories based on functionality. For example, we might have a `parsers` subdirectory for parsing code, a `utils` subdirectory for utility functions, and so on. This will allow us to keep the code organized and easy to navigate. - -## References - -[CAP]: https://cap.cloud.sap/docs/about/ -[CDL]: https://cap.cloud.sap/docs/cds/cdl -[CDS]: https://cap.cloud.sap/docs/cds/ - -- The [Cloud Application Programming][CAP] Model. -- The [Conceptual Definition Language][CDL] [CDL] is a human-readable language for defining [CDS] models. -- [Core Data Services][CDS] (CDS) in the Cloud Application Programming (CAP) Model. diff --git a/extractors/cds/tools/cds-extractor.ts b/extractors/cds/tools/cds-extractor.ts new file mode 100644 index 000000000..03de7bab6 --- /dev/null +++ b/extractors/cds/tools/cds-extractor.ts @@ -0,0 +1,270 @@ +import { join } from 'path'; + +import { sync as globSync } from 'glob'; + +import { orchestrateCompilation } from './src/cds/compiler'; +import { buildCdsProjectDependencyGraph } from './src/cds/parser'; +import { runJavaScriptExtractor } from './src/codeql'; +import { addCompilationDiagnostic } from './src/diagnostics'; +import { configureLgtmIndexFilters, setupAndValidateEnvironment } from './src/environment'; +import { + cdsExtractorLog, + generateStatusReport, + logExtractorStart, + logExtractorStop, + logPerformanceMilestone, + logPerformanceTrackingStart, + logPerformanceTrackingStop, + setSourceRootDirectory, +} from './src/logging'; +import { installDependencies } from './src/packageManager'; +import { validateArguments } from './src/utils'; + +// Validate the script arguments. +const validationResult = validateArguments(process.argv); +if (!validationResult.isValid) { + console.warn(validationResult.usageMessage); + // Exit with an error code on invalid use of this script. + process.exit(1); +} + +// Get the validated and sanitized arguments. +const { sourceRoot } = validationResult.args!; + +// Initialize the unified logging system with the source root directory. +setSourceRootDirectory(sourceRoot); + +// Log the start of the CDS extractor session as a whole. +logExtractorStart(sourceRoot); + +// Setup the environment and validate all requirements first, before changing +// directory back to the "sourceRoot" directory. This ensures we can properly locate +// the CodeQL tools. +logPerformanceTrackingStart('Environment Setup'); +const { + success: envSetupSuccess, + errorMessages, + codeqlExePath, + autobuildScriptPath, + platformInfo, +} = setupAndValidateEnvironment(sourceRoot); +logPerformanceTrackingStop('Environment Setup'); + +if (!envSetupSuccess) { + const codeqlExe = platformInfo.isWindows ? 'codeql.exe' : 'codeql'; + cdsExtractorLog( + 'warn', + `'${codeqlExe} database index-files --language cds' terminated early due to: ${errorMessages.join( + ', ', + )}.`, + ); + // Exit with an error code when environment setup fails. + logExtractorStop(false, 'Terminated: Environment setup failed'); + process.exit(1); +} + +// Force this script, and any process it spawns, to use the project (source) root +// directory as the current working directory. +process.chdir(sourceRoot); + +cdsExtractorLog( + 'info', + `CodeQL CDS extractor using autobuild mode for scan of project source root directory '${sourceRoot}'.`, +); + +cdsExtractorLog('info', 'Building CDS project dependency graph...'); + +// Build the CDS project `dependencyGraph` as the foundation for the extraction process. +// This graph will contain all discovered CDS projects, their dependencies, the `.cds` +// files discovered within each project, the expected `.cds.json` files for each project +// and the compilation status of such `.cds.json` files. +// +// The `dependencyGraph` will be updated as CDS extractor phases progress, allowing for +// a single data structure to be used for planning, execution, retries (i.e. error handling), +// debugging, and final reporting. +let dependencyGraph; + +try { + logPerformanceTrackingStart('Dependency Graph Build'); + dependencyGraph = buildCdsProjectDependencyGraph(sourceRoot); + logPerformanceTrackingStop('Dependency Graph Build'); + + logPerformanceMilestone( + 'Dependency graph created', + `${dependencyGraph.projects.size} projects, ${dependencyGraph.statusSummary.totalCdsFiles} CDS files`, + ); + + // Log details about discovered projects for debugging + if (dependencyGraph.projects.size > 0) { + for (const [projectDir, project] of dependencyGraph.projects.entries()) { + cdsExtractorLog( + 'info', + `Project: ${projectDir}, Status: ${project.status}, CDS files: ${project.cdsFiles.length}, Compilations to run: ${project.cdsFilesToCompile.length}`, + ); + } + } else { + cdsExtractorLog( + 'error', + 'No CDS projects were detected. This is an unrecoverable error as there is nothing to scan.', + ); + // Let's also try to find CDS files directly as a backup check + try { + const allCdsFiles = Array.from( + new Set([ + ...globSync(join(sourceRoot, '**/*.cds'), { + ignore: ['**/node_modules/**', '**/.git/**'], + }), + ]), + ); + cdsExtractorLog( + 'info', + `Direct search found ${allCdsFiles.length} CDS files in the source tree.`, + ); + if (allCdsFiles.length > 0) { + cdsExtractorLog( + 'info', + `Sample CDS files: ${allCdsFiles.slice(0, 5).join(', ')}${allCdsFiles.length > 5 ? ', ...' : ''}`, + ); + cdsExtractorLog( + 'error', + 'CDS files were found but no projects were detected. This indicates a problem with project detection logic.', + ); + } else { + cdsExtractorLog( + 'info', + 'No CDS files found in the source tree. This may be expected if the source does not contain CAP/CDS projects.', + ); + } + } catch (globError) { + cdsExtractorLog('warn', `Could not perform direct CDS file search: ${String(globError)}`); + } + + // Exit early since we have no CDS projects to process + logExtractorStop(false, 'Terminated: No CDS projects detected'); + process.exit(1); + } +} catch (error) { + cdsExtractorLog('error', `Failed to build CDS dependency graph: ${String(error)}`); + // Exit with error since we can't continue without a proper dependency graph + logExtractorStop(false, 'Terminated: Dependency graph build failed'); + process.exit(1); +} + +logPerformanceTrackingStart('Dependency Installation'); +const projectCacheDirMap = installDependencies(dependencyGraph, sourceRoot, codeqlExePath); +logPerformanceTrackingStop('Dependency Installation'); + +// Check if dependency installation resulted in any usable project mappings +if (projectCacheDirMap.size === 0) { + cdsExtractorLog( + 'error', + 'No project cache directory mappings were created. This indicates that dependency installation failed for all discovered projects.', + ); + + // This is a critical error if we have projects but no cache mappings + if (dependencyGraph.projects.size > 0) { + cdsExtractorLog( + 'error', + `Found ${dependencyGraph.projects.size} CDS projects but failed to install dependencies for any of them. Cannot proceed with compilation.`, + ); + logExtractorStop(false, 'Terminated: Dependency installation failed for all projects'); + process.exit(1); + } + + // If we have no projects and no cache mappings, this should have been caught earlier + cdsExtractorLog( + 'warn', + 'No projects and no cache mappings - this should have been detected earlier.', + ); +} + +const cdsFilePathsToProcess: string[] = []; + +// Use the dependency graph to collect all `.cds` files from each project. +// We want to "extract" all `.cds` files from all projects so that we have a copy +// of each `.cds` source file in the CodeQL database. +for (const project of dependencyGraph.projects.values()) { + cdsFilePathsToProcess.push(...project.cdsFiles); +} + +// TODO : Improve logging / debugging of dependencyGraph.statusSummary. Just log the JSON? +cdsExtractorLog( + 'info', + `Found ${cdsFilePathsToProcess.length} total CDS files, ${dependencyGraph.statusSummary.totalCdsFiles} CDS files in dependency graph`, +); + +logPerformanceTrackingStart('CDS Compilation'); +try { + // Use the new orchestrated compilation approach (autobuild mode, no debug) + orchestrateCompilation(dependencyGraph, projectCacheDirMap, codeqlExePath); + + // Handle compilation failures for normal mode + if (!dependencyGraph.statusSummary.overallSuccess) { + cdsExtractorLog( + 'error', + `Compilation completed with failures: ${dependencyGraph.statusSummary.failedCompilations} failed out of ${dependencyGraph.statusSummary.totalCompilationTasks} total tasks`, + ); + + // Add diagnostics for critical errors + for (const error of dependencyGraph.errors.critical) { + cdsExtractorLog('error', `Critical error in ${error.phase}: ${error.message}`); + } + + // Don't exit with error - let the JavaScript extractor run on whatever was compiled + } + + logPerformanceTrackingStop('CDS Compilation'); + logPerformanceMilestone('CDS compilation completed'); +} catch (error) { + logPerformanceTrackingStop('CDS Compilation'); + cdsExtractorLog('error', `Compilation orchestration failed: ${String(error)}`); + + // Add diagnostic for the overall failure + if (cdsFilePathsToProcess.length > 0) { + addCompilationDiagnostic( + cdsFilePathsToProcess[0], // Use first file as representative + `Compilation orchestration failed: ${String(error)}`, + codeqlExePath, + ); + } +} + +// Configure the "LGTM" index filters for proper extraction. +configureLgtmIndexFilters(); + +// Run CodeQL's JavaScript extractor to process the .cds source files and +// the compiled .cds.json files. +logPerformanceTrackingStart('JavaScript Extraction'); +const extractionStartTime = Date.now(); +const extractorResult = runJavaScriptExtractor(sourceRoot, autobuildScriptPath, codeqlExePath); +const extractionEndTime = Date.now(); +logPerformanceTrackingStop('JavaScript Extraction'); + +// Update the dependency graph's performance metrics with the extraction duration +dependencyGraph.statusSummary.performance.extractionDurationMs = + extractionEndTime - extractionStartTime; + +// Calculate total duration by summing all phases +const totalDuration = + dependencyGraph.statusSummary.performance.parsingDurationMs + + dependencyGraph.statusSummary.performance.compilationDurationMs + + dependencyGraph.statusSummary.performance.extractionDurationMs; +dependencyGraph.statusSummary.performance.totalDurationMs = totalDuration; + +if (!extractorResult.success && extractorResult.error) { + cdsExtractorLog('error', `Error running JavaScript extractor: ${extractorResult.error}`); + logExtractorStop(false, 'JavaScript extractor failed'); +} else { + logExtractorStop(true, 'CDS extraction completed successfully'); +} + +cdsExtractorLog( + 'info', + 'CDS Extractor Status Report : Final...\n' + generateStatusReport(dependencyGraph), +); + +// Use the `cds-extractor.js` name in the log message as that is the name of the script +// that is actually run by the `codeql database index-files` command. This TypeScript +// file is where the code/logic is edited/implemented, but the runnable script is +// generated by the TypeScript compiler and is named `cds-extractor.js`. +console.log(`Completed run of the cds-extractor.js script for the CDS extractor.`); diff --git a/extractors/cds/tools/eslint.config.mjs b/extractors/cds/tools/eslint.config.mjs index 889550bdf..1f04d2653 100644 --- a/extractors/cds/tools/eslint.config.mjs +++ b/extractors/cds/tools/eslint.config.mjs @@ -1,192 +1,208 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import { fixupConfigRules, fixupPluginRules } from "@eslint/compat"; -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import _import from "eslint-plugin-import"; -import prettier from "eslint-plugin-prettier"; -import globals from "globals"; -import tsParser from "@typescript-eslint/parser"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; -import jestPlugin from "eslint-plugin-jest"; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Import ESLint related modules +import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import _import from 'eslint-plugin-import'; +import jestPlugin from 'eslint-plugin-jest'; +import prettier from 'eslint-plugin-prettier'; +import globals from 'globals'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, }); export default defineConfig([ - globalIgnores(["out/**/*", "**/node_modules", "**/coverage", "**/*.d.ts"]), - { - extends: fixupConfigRules(compat.extends( - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript", - "plugin:prettier/recommended", - )), - - plugins: { - "@typescript-eslint": fixupPluginRules(typescriptEslint), - import: fixupPluginRules(_import), - prettier: fixupPluginRules(prettier), - }, - - languageOptions: { - globals: { - ...globals.node, - }, + globalIgnores(['out/**/*', '**/node_modules', '**/coverage', '**/*.d.ts']), + { + extends: fixupConfigRules( + compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', + 'plugin:prettier/recommended', + ), + ), + + plugins: { + // @ts-expect-error - typescript-eslint is a valid plugin + '@typescript-eslint': fixupPluginRules(typescriptEslint), + // @ts-expect-error - import is a valid plugin + import: fixupPluginRules(_import), + // @ts-expect-error - prettier is a valid plugin + prettier: fixupPluginRules(prettier), + }, - parser: tsParser, - ecmaVersion: 2018, - sourceType: "module", + languageOptions: { + globals: { + ...globals.node, + }, + // @ts-expect-error - tsParser is a valid parser + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + parser: tsParser, + ecmaVersion: 2018, + sourceType: 'module', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + createDefaultProgram: true, + }, + }, - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: "/Users/data-douser/Git/data-douser/codeql-sap-js/extractors/cds/tools", - createDefaultProgram: true, - }, + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + project: './tsconfig.json', }, - - settings: { - "import/resolver": { - typescript: { - alwaysTryTypes: true, - project: "./tsconfig.json", - }, - - node: { - extensions: [".js", ".jsx", ".ts", ".tsx"], - }, - }, + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], }, + }, + }, - rules: { - "no-console": "off", - "no-duplicate-imports": "error", - "no-unused-vars": "off", - "no-use-before-define": "off", - "no-trailing-spaces": "error", - "@typescript-eslint/explicit-module-boundary-types": "off", - - "@typescript-eslint/no-unused-vars": ["warn", { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - }], - - "@typescript-eslint/no-use-before-define": ["error", { - functions: false, - classes: true, - }], - - "@typescript-eslint/explicit-function-return-type": ["warn", { - allowExpressions: true, - allowTypedFunctionExpressions: true, - }], - - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/ban-ts-comment": "warn", - "@typescript-eslint/prefer-nullish-coalescing": "warn", - "@typescript-eslint/prefer-optional-chain": "warn", - - "import/order": ["error", { - groups: ["builtin", "external", "internal", ["parent", "sibling"], "index"], - "newlines-between": "always", - - alphabetize: { - order: "asc", - caseInsensitive: true, - }, - }], - - "import/no-duplicates": "error", - - "prettier/prettier": ["error", { - singleQuote: true, - trailingComma: "all", - printWidth: 100, - tabWidth: 2, - }], + rules: { + 'no-console': 'off', + 'no-duplicate-imports': 'error', + 'no-unused-vars': 'off', + 'no-use-before-define': 'off', + 'no-trailing-spaces': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'off', + + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', }, - }, - { - files: ["**/*.ts"], + ], - languageOptions: { - ecmaVersion: 5, - sourceType: "script", + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + classes: true, + }, + ], - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: "/Users/data-douser/Git/data-douser/codeql-sap-js/extractors/cds/tools", - }, + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { + allowExpressions: true, + allowTypedFunctionExpressions: true, }, - }, - { - files: ["**/*.test.ts", "test/**/*.ts", "**/index-files.ts"], - extends: fixupConfigRules(compat.extends("plugin:jest/recommended")), - plugins: { - jest: fixupPluginRules(jestPlugin), + ], + + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/prefer-nullish-coalescing': 'warn', + '@typescript-eslint/prefer-optional-chain': 'warn', + + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, }, - languageOptions: { - ecmaVersion: 2018, - sourceType: "module", + ], - parserOptions: { - project: "./tsconfig.json", - tsconfigRootDir: "/Users/data-douser/Git/data-douser/codeql-sap-js/extractors/cds/tools", - }, - }, + 'import/no-duplicates': 'error', - rules: { - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/restrict-template-expressions": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/unbound-method": "off" + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'all', + printWidth: 100, + tabWidth: 2, }, + ], }, - // Add JavaScript-specific configuration that doesn't use TypeScript parser - { - files: ["**/*.js", "**/.prettierrc.js", "**/jest.config.js"], - languageOptions: { - // Use default parser for JS files (removes TS parser requirement) - parser: undefined, - ecmaVersion: 2018, - sourceType: "module" - }, - rules: { - // Disable TypeScript-specific rules for JS files - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/restrict-template-expressions": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/unbound-method": "off" - } + }, + { + files: ['**/*.ts'], + languageOptions: { + ecmaVersion: 5, + sourceType: 'script', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, }, - { - files: ["test/src/**/*.js"], - extends: fixupConfigRules(compat.extends("plugin:jest/recommended")), - plugins: { - jest: fixupPluginRules(jestPlugin), - }, - languageOptions: { - // Use default parser for JS files (removes TS parser requirement) - parser: undefined, - ecmaVersion: 2018, - sourceType: "module" - }, + }, + { + files: ['**/*.test.ts', 'test/**/*.ts', '**/cds-extractor.ts'], + extends: fixupConfigRules(compat.extends('plugin:jest/recommended')), + plugins: { + jest: fixupPluginRules(jestPlugin), + }, + languageOptions: { + ecmaVersion: 2018, + sourceType: 'module', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/unbound-method': 'off', + }, + }, + // Add JavaScript-specific configuration that doesn't use TypeScript parser + { + files: ['**/*.js', '**/.prettierrc.js', '**/jest.config.js'], + languageOptions: { + // Use default parser for JS files (removes TS parser requirement) + parser: undefined, + ecmaVersion: 2018, + sourceType: 'module', + }, + rules: { + // Disable TypeScript-specific rules for JS files + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/unbound-method': 'off', + }, + }, + { + files: ['test/src/**/*.js'], + extends: fixupConfigRules(compat.extends('plugin:jest/recommended')), + plugins: { + jest: fixupPluginRules(jestPlugin), + }, + languageOptions: { + // Use default parser for JS files (removes TS parser requirement) + parser: undefined, + ecmaVersion: 2018, + sourceType: 'module', }, -]); \ No newline at end of file + }, +]); diff --git a/extractors/cds/tools/index-files.cmd b/extractors/cds/tools/index-files.cmd index 063c0cf0b..ef14043fc 100644 --- a/extractors/cds/tools/index-files.cmd +++ b/extractors/cds/tools/index-files.cmd @@ -1,13 +1,12 @@ @echo off -if "%~1"=="" ( - echo Usage: %0 ^ - exit /b 1 -) +REM This script currently: +REM - ignores any arguments passed to it; +REM - assumes it is run from the root of the project source directory; where node >nul 2>nul if %ERRORLEVEL% neq 0 ( - echo node executable is required (in PATH) to run the 'index-files.js' script. Please install Node.js and try again. + echo node executable is required (in PATH) to run the 'cds-extractor.js' script. Please install Node.js and try again. exit /b 2 ) @@ -17,42 +16,55 @@ if %ERRORLEVEL% neq 0 ( exit /b 3 ) -set "_response_file_path=%~1" -set "_script_dir=%~dp0" -REM Set _cwd before changing the working directory to the script directory. -REM We assume this script is called from the source root directory of the -REM to be scanned project. +REM Set the _cwd variable to the present working directory as the directory +REM from which this script was called, which we assume is the "source root" directory +REM of the project that to be scanned / indexed. set "_cwd=%CD%" +set "_script_dir=%~dp0" +set "_cds_extractor_js_path=%_script_dir%dist\cds-extractor.js" +set "_cds_extractor_node_modules_dir=%_script_dir%node_modules" -echo Checking response file for CDS files to index - -REM Terminate early if the _response_file_path doesn't exist or is empty, -REM which indicates that no CDS files were selected or found. -if not exist "%_response_file_path%" ( - echo 'codeql database index-files --language cds' command terminated early as response file '%_response_file_path%' does not exist or is empty. This is because no CDS files were selected or found. - exit /b 0 -) - -REM Change to the directory of this script to ensure that npm looks up the +REM Change to the directory of this batch script to ensure that npm looks up the REM package.json file in the correct directory and installs the dependencies REM (i.e. node_modules) relative to this directory. This is technically a REM violation of the assumption that extractor scripts will be run with the REM current working directory set to the root of the project source, but we REM also need node_modules to be installed here and not in the project source REM root, so we make a compromise of: -REM 1. changing to this script's directory; -REM 2. installing node dependencies here; -REM 3. passing the original working directory as a parameter to the -REM index-files.js script; -REM 4. expecting the index-files.js script to immediately change back to -REM the original working (aka the project source root) directory. +REM 1. changing to this batch script's directory; +REM 2. passing the original working directory as a parameter to the +REM cds-extractor.js script; +REM 3. expecting the cds-extractor.js script to immediately change back to +REM original working (aka the project source root) directory. cd /d "%_script_dir%" && ^ -echo Installing node package dependencies && ^ -npm install --quiet --no-audit --no-fund && ^ -echo Building TypeScript code && ^ -npm run build && ^ -echo Running the 'index-files.js' script && ^ -node "%_script_dir%out\index-files.js" "%_response_file_path%" "%_cwd%" - -exit /b %ERRORLEVEL% \ No newline at end of file + +REM Check if the 'node_modules' directory exists in the current script's directory. +REM This is a highly imperfect check that CDS extractor dependencies have been installed. +if not exist "%_cds_extractor_node_modules_dir%" ( + echo Installing dependencies for the CDS extractor script in '%_cds_extractor_node_modules_dir%' directory. + npm install --quiet --no-audit --no-fund + if %ERRORLEVEL% equ 0 ( + echo CDS extractor dependencies installed successfully. + ) else ( + echo Error: Failed to install CDS extractor dependencies. + echo Please ensure that the dependencies have been installed by running 'npm install' in the 'extractors\cds\tools' directory. + exit /b 4 + ) +) + +REM Check if the 'cds-extractor.js' script exists at the expected path. +if not exist "%_cds_extractor_js_path%" ( + echo Building the 'cds-extractor.js' script from TypeScript source. + npm run build --silent + if %ERRORLEVEL% equ 0 ( + echo CDS extractor script built successfully. + ) else ( + echo Error: Failed to build the CDS extractor script. + echo Please ensure that the TypeScript source has been compiled to JavaScript in the 'dist' directory. + exit /b 5 + ) +) + +echo Running the 'cds-extractor.js' script && ^ +node "%_cds_extractor_js_path%" "%_cwd%" diff --git a/extractors/cds/tools/index-files.sh b/extractors/cds/tools/index-files.sh index 49256f769..20d8638b8 100755 --- a/extractors/cds/tools/index-files.sh +++ b/extractors/cds/tools/index-files.sh @@ -2,15 +2,14 @@ set -eu -if [ $# -ne 1 ] -then - echo "Usage: $0 " - exit 1 -fi +## This script currently: +## - ignores any arguments passed to it; +## - assumes it is run from the root of the project source directory; +## if ! command -v node > /dev/null then - echo "node executable is required (in PATH) to run the 'index-files.js' script. Please install Node.js and try again." + echo "node executable is required (in PATH) to run the 'cds-extractor.js' script. Please install Node.js and try again." exit 2 fi @@ -24,40 +23,50 @@ fi # from which this script was called, which we assume is the "source root" directory # of the project that to be scanned / indexed. _cwd="$PWD" -_response_file_path="$1" _script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +_cds_extractor_js_path="${_script_dir}/dist/cds-extractor.js" +_cds_extractor_node_modules_dir="${_script_dir}/node_modules" -echo "Checking response file for CDS files to index" - -# Terminate early if the _response_file_path doesn't exist or is empty, -# which indicates that no CDS files were selected or found. -if [ ! -f "$_response_file_path" ] || [ ! -s "$_response_file_path" ] -then - echo "'codeql database index-files --language cds' command terminated early as response file '$_response_file_path' does not exist or is empty. This is because no CDS files were selected or found." - # Exit without error to avoid failing any calling (javascript) - # extractor, and llow the tool the report the lack of coverage - # for CDS files. - exit 0 -fi - -# Change to the directory of this script to ensure that npm looks up the +# Change to the directory of this shell script to ensure that npm looks up the # package.json file in the correct directory and installs the dependencies -# (i.e. node_modules) relative to this directory. This is technically a -# violation of the assumption that extractor scripts will be run with the -# current working directory set to the root of the project source, but we -# also need node_modules to be installed here and not in the project source -# root, so we make a compromise of: -# 1. changing to this script's directory; -# 2. installing node dependencies here; -# 3. passing the original working directory as a parameter to the -# index-files.js script; -# 4. expecting the index-files.js script to immediately change back to +# (i.e. node_modules) relative to this directory. This is technically a violation +# of the assumption that extractor scripts will be run with the current working +# directory set to the root of the project source, but we also need node_modules +# to be installed here and not in the project source root, so we make the following +# compromise: +# +# 1. change to this shell script's directory; +# 2. pass the original working directory as a parameter to the +# cds-extractor.js script; +# 3. expect the cds-extractor.js script to immediately change back to # original working (aka the project source root) directory. cd "$_script_dir" && \ -echo "Installing node package dependencies" && \ -npm install --quiet --no-audit --no-fund && \ -echo "Building TypeScript code" && \ -npm run build && \ -echo "Running the 'index-files.js' script" && \ -node "$(dirname "$0")/out/index-files.js" "$_response_file_path" "${_cwd}" + +# Check if the 'node_modules' directory exists in the current script's directory. +# This is a highly imperfect check that CDS extractor dependencies have been installed. +if [ ! -d "${_cds_extractor_node_modules_dir}" ]; then + echo "Installing dependencies for the CDS extractor script in '${_cds_extractor_node_modules_dir}' directory." + if npm install --quiet --no-audit --no-fund ; then + echo "CDS extractor dependencies installed successfully." + else + echo "Error: Failed to install CDS extractor dependencies." + echo "Please ensure that the dependencies have been installed by running 'npm install' in the 'extractors/cds/tools' directory." + exit 4 + fi +fi + +# Check if the 'cds-extractor.js' script exists at the expected path. +if [ ! -f "${_cds_extractor_js_path}" ]; then + echo "Building the 'cds-extractor.js' script from TypeScript source." + if npm run build --silent; then + echo "CDS extractor script built successfully." + else + echo "Error: Failed to build the CDS extractor script." + echo "Please ensure that the TypeScript source has been compiled to JavaScript in the 'dist' directory." + exit 5 + fi +fi + +echo "Running the 'cds-extractor.js' script" && \ +node "${_cds_extractor_js_path}" "$_cwd" diff --git a/extractors/cds/tools/index-files.ts b/extractors/cds/tools/index-files.ts deleted file mode 100644 index 847041c21..000000000 --- a/extractors/cds/tools/index-files.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { compileCdsToJson, determineCdsCommand } from './src/cdsCompiler'; -import { runJavaScriptExtractor } from './src/codeql'; -import { addCompilationDiagnostic } from './src/diagnostics'; -import { configureLgtmIndexFilters, setupAndValidateEnvironment } from './src/environment'; -import { getCdsFilePathsToProcess } from './src/filesystem'; -import { findPackageJsonDirs, installDependencies } from './src/packageManager'; -import { validateArguments } from './src/utils'; - -// Validate arguments to this script. -if (!validateArguments(process.argv, 4)) { - // Exit with an error code on invalid use of this script. - process.exit(1); -} - -// Get command-line (CLI) arguments and store them in named variables for clarity. -const responseFile: string = process.argv[2]; -const sourceRoot: string = process.argv[3]; - -// Force this script, and any process it spawns, to use the project (source) root -// directory as the current working directory. -process.chdir(sourceRoot); - -console.log(`Indexing CDS files in project source directory: ${sourceRoot}`); - -// Setup the environment and validate all requirements. -const { - success: envSetupSuccess, - errorMessages, - codeqlExePath, - autobuildScriptPath, - platformInfo, -} = setupAndValidateEnvironment(sourceRoot); - -if (!envSetupSuccess) { - const codeqlExe = platformInfo.isWindows ? 'codeql.exe' : 'codeql'; - console.warn( - `'${codeqlExe} database index-files --language cds' terminated early due to: ${errorMessages.join( - ', ', - )}.`, - ); - // Exit with an error code when environment setup fails. - process.exit(1); -} - -// Validate response file and get the full paths of CDS files to process. -const filePathsResult = getCdsFilePathsToProcess(responseFile, platformInfo); -if (!filePathsResult.success) { - console.warn(filePathsResult.errorMessage); - // Exit with an error if unable to get a list of `.cds` file paths to process. - process.exit(1); -} - -// Get the validated list of CDS files to process -const cdsFilePathsToProcess = filePathsResult.cdsFilePaths; - -// Find all package.json directories that have a `@sap/cds` node dependency. -// Pass the source root to prevent searching above it -const packageJsonDirs = findPackageJsonDirs(cdsFilePathsToProcess, codeqlExePath, sourceRoot); - -// Install node dependencies in each directory. -console.log('Pre-installing required CDS compiler versions ...'); -installDependencies(packageJsonDirs, codeqlExePath); - -// Determine the CDS command to use. -const cdsCommand = determineCdsCommand(); - -console.log('Processing CDS files to JSON ...'); - -// Compile each `.cds` file to create a `.cds.json` file. -for (const rawCdsFilePath of cdsFilePathsToProcess) { - try { - // Use resolved path directly instead of passing through getArg - const compilationResult = compileCdsToJson(rawCdsFilePath, sourceRoot, cdsCommand); - - if (!compilationResult.success && compilationResult.message) { - console.error( - `ERROR: adding diagnostic for source file=${rawCdsFilePath} : ${compilationResult.message} ...`, - ); - addCompilationDiagnostic(rawCdsFilePath, compilationResult.message, codeqlExePath); - } - } catch (errorMessage) { - console.error( - `ERROR: adding diagnostic for source file=${rawCdsFilePath} : ${String(errorMessage)} ...`, - ); - addCompilationDiagnostic(rawCdsFilePath, String(errorMessage), codeqlExePath); - } -} - -// Configure the "LGTM" index filters for proper extraction. -configureLgtmIndexFilters(); - -// Run CodeQL's JavaScript extractor to process the compiled JSON files. -const extractorResult = runJavaScriptExtractor(sourceRoot, autobuildScriptPath, codeqlExePath); -if (!extractorResult.success && extractorResult.error) { - console.error(`Error running JavaScript extractor: ${extractorResult.error}`); -} - -// Use the `index-file.js` name in the log message as that is the name of the script -// that is actually run by the `codeql database index-files` command. This TypeScript -// file is where the code/logic is edited/implemented, but the runnable script is -// generated by the TypeScript compiler and is named `index-files.js`. -console.log(`Completed run of index-files.js script for CDS extractor.`); diff --git a/extractors/cds/tools/package-lock.json b/extractors/cds/tools/package-lock.json index 877014388..16131734b 100644 --- a/extractors/cds/tools/package-lock.json +++ b/extractors/cds/tools/package-lock.json @@ -8,31 +8,37 @@ "name": "@advanced-security/codeql-sap-js_index-cds-files", "version": "1.0.0", "dependencies": { + "@types/tmp": "^0.2.6", "child_process": "^1.0.2", "fs": "^0.0.1-security", + "glob": "^11.0.3", "os": "^0.1.2", "path": "^0.12.7", - "shell-quote": "^1.8.2" + "shell-quote": "^1.8.3", + "tmp": "^0.2.3" }, "devDependencies": { - "@eslint/compat": "^1.2.8", + "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.25.1", - "@types/jest": "^29.5.14", - "@types/node": "^22.15.3", + "@eslint/js": "^9.30.0", + "@types/glob": "^8.1.0", + "@types/jest": "^30.0.0", + "@types/mock-fs": "^4.13.4", + "@types/node": "^24.0.7", "@types/shell-quote": "^1.7.5", - "@typescript-eslint/eslint-plugin": "^8.31.1", - "@typescript-eslint/parser": "^8.31.1", - "eslint": "^9.25.1", - "eslint-config-prettier": "^10.1.2", - "eslint-import-resolver-typescript": "^4.3.4", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-prettier": "^5.2.6", - "globals": "^16.0.0", - "jest": "^29.7.0", - "prettier": "^3.5.3", - "ts-jest": "^29.3.2", + "@typescript-eslint/eslint-plugin": "^8.35.1", + "@typescript-eslint/parser": "^8.35.1", + "eslint": "^9.30.0", + "eslint-config-prettier": "^10.1.5", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-prettier": "^5.5.1", + "globals": "^16.2.0", + "jest": "^30.0.3", + "mock-fs": "^5.5.0", + "prettier": "^3.6.2", + "ts-jest": "^29.4.0", "typescript": "^5.8.3" } }, @@ -566,40 +572,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -630,16 +602,16 @@ } }, "node_modules/@eslint/compat": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.9.tgz", - "integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^9.10.0" + "eslint": "^8.40 || 9" }, "peerDependenciesMeta": { "eslint": { @@ -648,9 +620,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -663,9 +635,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -723,9 +695,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", + "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", "dev": true, "license": "MIT", "engines": { @@ -825,6 +797,123 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -943,61 +1032,61 @@ } }, "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.2.tgz", + "integrity": "sha512-krGElPU0FipAqpVZ/BRZOy0MZh/ARdJ0Nj+PiH1ykFY1+VpBlYNLjdjVA5CFKxnKR6PFqFutO4Z7cdK9BlGiDA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.1", "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.3.tgz", + "integrity": "sha512-Mgs1N+NSHD3Fusl7bOq1jyxv1JDAUwjy+0DhVR93Q6xcBP9/bAQ+oZhXb5TTnP5sQzAHgb7ROCKQ2SnovtxYtg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.0.2", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.2", + "jest-config": "30.0.3", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-resolve-dependencies": "30.0.3", + "jest-runner": "30.0.3", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "jest-watcher": "30.0.2", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1008,117 +1097,150 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.2.tgz", + "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "jest-mock": "^29.7.0" + "jest-mock": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "expect": "30.0.3", + "jest-snapshot": "30.0.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.3.tgz", + "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" + "@jest/get-type": "30.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.2.tgz", + "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", + "@jest/types": "30.0.1", + "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.3.tgz", + "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/types": "30.0.1", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@types/node": "*", + "jest-regex-util": "30.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.2.tgz", + "integrity": "sha512-l4QzS/oKf57F8WtPZK+vvF4Io6ukplc6XgNFu4Hd/QxaLEO9f+8dSFzUua62Oe0HKlCUjKHpltKErAgDiMJKsA==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", + "@jest/console": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", + "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", + "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1129,109 +1251,213 @@ } } }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", + "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.2.tgz", + "integrity": "sha512-KKMuBKkkZYP/GfHMhI+cH2/P3+taMZS3qnqqiPC1UXZTJskkCS+YU/ILCtw5anw1+YsTulDHFpDo70mmCedW8w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@jest/console": "30.0.2", + "@jest/types": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.2.tgz", + "integrity": "sha512-fbyU5HPka0rkalZ3MXVvq0hwZY8dx3Y6SCqR64zRmh+xXlDeFl0IdL4l9e7vp4gxEXTYHbwLFA1D+WW5CucaSw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", + "@jest/test-result": "30.0.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.2.tgz", + "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", + "@babel/core": "^7.27.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "write-file-atomic": "^5.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", + "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1287,19 +1513,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", - "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1338,6 +1551,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", @@ -1359,9 +1583,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", "dev": true, "license": "MIT" }, @@ -1376,24 +1600,13 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "@sinonjs/commons": "^3.0.1" } }, "node_modules/@types/babel__core": { @@ -1448,13 +1661,14 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", "dev": true, "license": "MIT", "dependencies": { + "@types/minimatch": "^5.1.2", "@types/node": "*" } }, @@ -1486,14 +1700,14 @@ } }, "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "expect": "^30.0.0", + "pretty-format": "^30.0.0" } }, "node_modules/@types/json-schema": { @@ -1510,14 +1724,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mock-fs": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "22.15.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", - "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", + "version": "24.0.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz", + "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/shell-quote": { @@ -1534,6 +1765,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1552,17 +1789,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", - "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", + "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/type-utils": "8.34.0", - "@typescript-eslint/utils": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/type-utils": "8.35.1", + "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1576,7 +1813,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.0", + "@typescript-eslint/parser": "^8.35.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1592,16 +1829,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", - "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", + "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4" }, "engines": { @@ -1617,14 +1854,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", - "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", + "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.0", - "@typescript-eslint/types": "^8.34.0", + "@typescript-eslint/tsconfig-utils": "^8.35.1", + "@typescript-eslint/types": "^8.35.1", "debug": "^4.3.4" }, "engines": { @@ -1639,14 +1876,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", - "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", + "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0" + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1657,9 +1894,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", - "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", + "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", "dev": true, "license": "MIT", "engines": { @@ -1674,14 +1911,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", - "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", + "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/typescript-estree": "8.35.1", + "@typescript-eslint/utils": "8.35.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1698,9 +1935,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", - "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", + "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", "dev": true, "license": "MIT", "engines": { @@ -1712,16 +1949,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", - "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", + "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.0", - "@typescript-eslint/tsconfig-utils": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/project-service": "8.35.1", + "@typescript-eslint/tsconfig-utils": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/visitor-keys": "8.35.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1764,302 +2001,82 @@ }, "funding": { "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", - "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", - "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.34.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.8.1.tgz", - "integrity": "sha512-OKuBTQdOb4Kjbe+y4KgbRhn+nu47hNyNU2K3qjD+SA/bnQouvZnRzEiR85xZAIyZ6z1C+O1Zg1dK4hGH1RPdYA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.8.1.tgz", - "integrity": "sha512-inaphBsOqqzauNvx6kSHrgqDLShicPg3+fInBcEdD7Ut8sUUbm2z19LL+S9ccGpHnYoNiJ+Qrf7/B8hRsCUvBw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.8.1.tgz", - "integrity": "sha512-LkGw7jDoLKEZO6yYwTKUlrboD6Qmy9Jkq7ZDPlJReq/FnCnNh0k1Z1hjtevpqPCMLz9hGW0ITMb04jdDZ796Cg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.8.1.tgz", - "integrity": "sha512-6vhu22scv64dynXTVmeClenn3OPI8cwdhtydLFDkoW4UJzNwcgJ5mVtzbtikDGM9PmIQa+ekpH6tdvKt0ToK3A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.8.1.tgz", - "integrity": "sha512-SrQ286JVFWlnZSm1/TJwulTgJVOdb1x8BWW2ecOK0Sx+acdRpoMf4WSxH+/+R4LyE/YYyekcEtUrPhSEgJ748g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.8.1.tgz", - "integrity": "sha512-I2s4L27V+2kAee43x/qAkFjTZJgmDvSd9vtnyINOdBEdz5+QqiG6ccd5pgOw06MsUwygkrhB4jOe4ZN4SA6IwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.8.1.tgz", - "integrity": "sha512-Drq80e/EQbdSVyJpheF65qVmfYy8OaDdQqoWV+09tZHz/P1SdSulvVtgtYrk216D++9hbx3c1bwVXwR5PZ2TzA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.8.1.tgz", - "integrity": "sha512-EninHQHw8Zkq8K5qB6KWNDqjCtUzTDsCRQ6LzAtQWIxic/VQxR5Kl36V/GCXNvQaR7W0AB5gvJLyQtJwkf+AJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.8.1.tgz", - "integrity": "sha512-s7Xu5PS4vWhsb5ZFAi+UBguTn0g8qDhN+BbB1t9APX23AdAI7TS4DRrJV5dBVdQ6a8MiergGr1Cjb0Q1V/sW8w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.8.1.tgz", - "integrity": "sha512-Ca+bVzOJtgQ3OrMkRSeDLYWJIjRmEylDHSZuSKqqPmZI2vgX6yZgzrKY28I6hjjG9idlW4DcJzLv/TjFXev+4Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.8.1.tgz", - "integrity": "sha512-ut1vBBFs6AC5EcerH8HorcmS/9wAy6iI1tfpzT7jy+SKnMgmPth/psc3W5V04njble7cyLPjFHwYJTlxmozQ/g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.8.1.tgz", - "integrity": "sha512-w5agLxesvrYKrCOlAsUkwRDogjnyRBi4/vEaujZRkXbeRCupJ9dFD0qUhLXZyIed+GSzJJIsJocUZIVzcTHYXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.8.1.tgz", - "integrity": "sha512-vk5htmWYCLRpfjn2wmCUne6pLvlcYUFDAAut4g02/2iWeGeZO/3GmSLmiZ9fcn9oH0FUzgetg0/zSo8oZ7liIg==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/utils": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", + "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.1", + "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/typescript-estree": "8.35.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.8.1.tgz", - "integrity": "sha512-RcsLTcrqDT5XW/TnhhIeM7lVLgUv/gvPEC4WaH+OhkLCkRfH6EEuhprwrcp1WhdlrtL/U5FkHh4NtFLnMXoeXA==", - "cpu": [ - "wasm32" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.35.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", + "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@typescript-eslint/types": "8.35.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">=14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.8.1.tgz", - "integrity": "sha512-XbSRLZY/gEi5weYv/aCkiUiSWvrNKkvec3m6/bDypDI+ZACwMllPH7smeOW/fdnIGhf9YtPATNliJHAS2GyMUA==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.8.1.tgz", - "integrity": "sha512-SbCJMKOmqOsIBCklT5c+t0DjVbOkseE7ZN0OtMxRnraLKdj1AAv7d3cjJMYkPd9ZGKosHoMXo66gBs02YM8KeA==", - "cpu": [ - "ia32" - ], + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "ISC" }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.8.1.tgz", - "integrity": "sha512-DdHqo7XbeUa/ZOcxq+q5iuO4sSxhwX9HR1JPL0JMOKEzgkIO4OKF2TPjqmo6UCCGZUXIMwrAycFXj/40sICagw==", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.8.1.tgz", + "integrity": "sha512-OKuBTQdOb4Kjbe+y4KgbRhn+nu47hNyNU2K3qjD+SA/bnQouvZnRzEiR85xZAIyZ6z1C+O1Zg1dK4hGH1RPdYA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ] }, "node_modules/acorn": { @@ -2122,7 +2139,6 @@ "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" @@ -2132,7 +2148,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2321,85 +2336,57 @@ } }, "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.2.tgz", + "integrity": "sha512-A5kqR1/EUTidM2YC2YMEUDP2+19ppgOwK0IAd9Swc3q2KqFb5f9PtRUXVeZcngu0z5mDMyZ9zH2huJZSOMLiTQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", + "@jest/transform": "30.0.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.8.0" + "@babel/core": "^7.11.0" } }, "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=12" } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -2430,20 +2417,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.11.0" } }, "node_modules/balanced-match": { @@ -2665,9 +2652,9 @@ "license": "ISC" }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", "dev": true, "funding": [ { @@ -2681,9 +2668,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", "dev": true, "license": "MIT" }, @@ -2724,7 +2711,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2737,7 +2723,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -2754,33 +2739,10 @@ "dev": true, "license": "MIT" }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2941,16 +2903,6 @@ "node": ">=8" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2979,6 +2931,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -3019,7 +2977,6 @@ "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/error-ex": { @@ -3205,19 +3162,19 @@ } }, "node_modules/eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", + "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.30.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3229,9 +3186,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3329,9 +3286,9 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.3.tgz", - "integrity": "sha512-elVDn1eWKFrWlzxlWl9xMt8LltjKl161Ix50JFC50tHXI5/TRP32SNEqlJ/bo/HV+g7Rou/tlPQU2AcRtIhrOg==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", "dev": true, "license": "ISC", "dependencies": { @@ -3339,7 +3296,7 @@ "eslint-import-context": "^0.1.8", "get-tsconfig": "^4.10.1", "is-bun-module": "^2.0.0", - "stable-hash-x": "^0.1.1", + "stable-hash-x": "^0.2.0", "tinyglobby": "^0.2.14", "unrs-resolver": "^1.7.11" }, @@ -3363,10 +3320,20 @@ } } }, + "node_modules/eslint-import-resolver-typescript/node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -3392,30 +3359,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -3446,20 +3413,20 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.13.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.13.3.tgz", - "integrity": "sha512-BwC7TkFKn59tyfi6Zd9p/bcVVYOjWqp80jeaQvMy1fNFo8iDF8D5XvoSMM7CPaL6lQXPXCgD+RD4onlSsFelIw==", + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.0.1.tgz", + "integrity": "sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/utils": "^8.0.0" }, "engines": { - "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + "node": "^20.12.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "jest": "*" }, "peerDependenciesMeta": { @@ -3472,9 +3439,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz", - "integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3660,30 +3627,32 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/fast-deep-equal": { @@ -3877,6 +3846,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", @@ -4060,22 +4057,23 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4094,6 +4092,21 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", @@ -4534,7 +4547,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4804,7 +4816,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4850,15 +4861,15 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" @@ -4878,6 +4889,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -4898,22 +4924,22 @@ } }, "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.3.tgz", + "integrity": "sha512-Uy8xfeE/WpT2ZLGDXQmaYNzw2v8NUKuYeKGtkS6sDxwsdQihdgYCXaKIYnph1h95DN5H35ubFDm0dfmsQnjn4Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" + "@jest/core": "30.0.3", + "@jest/types": "30.0.1", + "import-local": "^3.2.0", + "jest-cli": "30.0.3" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -4925,76 +4951,75 @@ } }, "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", + "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", + "execa": "^5.1.1", + "jest-util": "30.0.2", "p-limit": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.3.tgz", + "integrity": "sha512-rD9qq2V28OASJHJWDRVdhoBdRs6k3u3EmBzDYcyuMby8XCO3Ll1uq9kyqM41ZcC4fMiPulMVh3qMw0cBvDbnyg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.2", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", + "pretty-format": "30.0.2", + "pure-rand": "^7.0.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.3.tgz", + "integrity": "sha512-UWDSj0ayhumEAxpYRlqQLrssEi29kdQ+kddP94AuHhZknrE+mT0cR0J+zMHKFe9XPfX3dKQOc2TfWki3WhFTsA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" + "@jest/core": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "yargs": "^17.7.2" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -5005,216 +5030,299 @@ } } }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "node_modules/jest-config": { + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.3.tgz", + "integrity": "sha512-j0L4oRCtJwNyZktXIqwzEiDVQXBbQ4dqXuLD/TZdn++hXIcIfZmjHgrViEy5s/+j4HvITmAXbexVZpQ/jnr0bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.2", + "@jest/types": "30.0.1", + "babel-jest": "30.0.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-runner": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=16 || 14 >=14.18" }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.3.tgz", + "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", "dev": true, "license": "MIT", "dependencies": { - "detect-newline": "^3.0.0" + "detect-newline": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", + "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.2.tgz", + "integrity": "sha512-XsGtZ0H+a70RsxAQkKuIh0D3ZlASXdZdhpOSBq9WRPq6lhe0IoQHGW0w9ZUaPiZQ/CpkIdprvlfV1QcXcvIQLQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-mock": "30.0.2", + "jest-util": "30.0.2", + "jest-validate": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", + "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", + "@jest/types": "30.0.1", "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "micromatch": "^4.0.8", "walker": "^1.0.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "fsevents": "^2.3.3" } }, "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", + "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz", + "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.3", + "pretty-format": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", + "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", + "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.1", "@types/node": "*", - "jest-util": "^29.7.0" + "jest-util": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-pnp-resolver": { @@ -5236,183 +5344,284 @@ } }, "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", + "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.3.tgz", + "integrity": "sha512-FlL6u7LiHbF0Oe27k7DHYMq2T2aNpPhxnNo75F7lEtu4A6sSw+TKkNNUGNcVckdFoL0RCWREJsC1HsKDwKRZzQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.3.tgz", + "integrity": "sha512-CxYBzu9WStOBBXAKkLXGoUtNOWsiS1RRmUQb6SsdUdTcqVncOau7m8AJ4cW3Mz+YL1O9pOGPSYLyvl8HBdFmkQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.0.2", + "@jest/environment": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-haste-map": "30.0.2", + "jest-leak-detector": "30.0.2", + "jest-message-util": "30.0.2", + "jest-resolve": "30.0.2", + "jest-runtime": "30.0.3", + "jest-util": "30.0.2", + "jest-watcher": "30.0.2", + "jest-worker": "30.0.2", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.3.tgz", + "integrity": "sha512-Xjosq0C48G9XEQOtmgrjXJwPaUPaq3sPJwHDRaiC+5wi4ZWxO6Lx6jNkizK/0JmTulVNuxP8iYwt77LGnfg3/w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/globals": "30.0.3", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "version": "30.0.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.3.tgz", + "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.1", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.3", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.3", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", + "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.1", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", + "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "pretty-format": "30.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -5429,39 +5638,40 @@ } }, "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.2.tgz", + "integrity": "sha512-vYO5+E7jJuF+XmONr6CrbXdlYrgvZqtkn6pdkgjt/dU64UAdc0v1cAVaAeWtAfUUMScxNmnUjKPUMdCpNVASwg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" + "jest-util": "30.0.2", + "string-length": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", + "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-util": "^29.7.0", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.2", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -5564,16 +5774,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5752,6 +5952,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mock-fs": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", + "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6026,6 +6245,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6092,7 +6317,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6105,6 +6329,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6225,9 +6474,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { @@ -6254,18 +6503,18 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", + "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.1", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -6290,20 +6539,6 @@ "node": ">= 0.6.0" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6315,9 +6550,9 @@ } }, "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", "dev": true, "funding": [ { @@ -6477,16 +6712,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6643,7 +6868,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6656,16 +6880,15 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -6757,13 +6980,6 @@ "dev": true, "license": "ISC" }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6867,7 +7083,21 @@ "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/string-width-cjs": { + "name": "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==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6941,7 +7171,19 @@ "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/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7040,6 +7282,28 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -7085,6 +7349,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7119,16 +7392,15 @@ } }, "node_modules/ts-jest": { - "version": "29.3.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", - "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -7144,10 +7416,11 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { @@ -7165,6 +7438,9 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, @@ -7217,14 +7493,6 @@ "node": ">=4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7373,9 +7641,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, "license": "MIT" }, @@ -7491,7 +7759,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7620,6 +7887,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7628,17 +7913,30 @@ "license": "ISC" }, "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "signal-exit": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/y18n": { diff --git a/extractors/cds/tools/package.json b/extractors/cds/tools/package.json index 43d0ba893..f5096e6c5 100644 --- a/extractors/cds/tools/package.json +++ b/extractors/cds/tools/package.json @@ -2,45 +2,51 @@ "name": "@advanced-security/codeql-sap-js_index-cds-files", "version": "1.0.0", "description": "CodeQL extractor for DB indexing of .cds.json files produced by the 'cds' compiler.", - "main": "out/index-files.js", + "main": "dist/cds-extractor.js", "scripts": { "build": "tsc", "build:all": "npm run lint:fix && npm run test:coverage && npm run build", - "clean": "rm -rf out coverage", + "clean": "rm -rf coverage dist", "prebuild": "npm run clean", - "lint": "eslint --ext .ts src/", - "lint:fix": "eslint --ext .ts --fix src/", + "lint": "eslint --ext .ts cds-extractor.ts src/ test/src/", + "lint:fix": "eslint --ext .ts --fix cds-extractor.ts src/ test/src/", "format": "prettier --write 'src/**/*.ts'", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.ts'" }, "dependencies": { + "@types/tmp": "^0.2.6", "child_process": "^1.0.2", "fs": "^0.0.1-security", + "glob": "^11.0.3", "os": "^0.1.2", "path": "^0.12.7", - "shell-quote": "^1.8.2" + "shell-quote": "^1.8.3", + "tmp": "^0.2.3" }, "devDependencies": { - "@eslint/compat": "^1.2.8", + "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.25.1", - "@types/jest": "^29.5.14", - "@types/node": "^22.15.3", + "@eslint/js": "^9.30.0", + "@types/glob": "^8.1.0", + "@types/jest": "^30.0.0", + "@types/mock-fs": "^4.13.4", + "@types/node": "^24.0.7", "@types/shell-quote": "^1.7.5", - "@typescript-eslint/eslint-plugin": "^8.31.1", - "@typescript-eslint/parser": "^8.31.1", - "eslint": "^9.25.1", - "eslint-config-prettier": "^10.1.2", - "eslint-import-resolver-typescript": "^4.3.4", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-prettier": "^5.2.6", - "globals": "^16.0.0", - "jest": "^29.7.0", - "prettier": "^3.5.3", - "ts-jest": "^29.3.2", + "@typescript-eslint/eslint-plugin": "^8.35.1", + "@typescript-eslint/parser": "^8.35.1", + "eslint": "^9.30.0", + "eslint-config-prettier": "^10.1.5", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-prettier": "^5.5.1", + "globals": "^16.2.0", + "jest": "^30.0.3", + "mock-fs": "^5.5.0", + "prettier": "^3.6.2", + "ts-jest": "^29.4.0", "typescript": "^5.8.3" } } diff --git a/extractors/cds/tools/src/cds/compiler/command.ts b/extractors/cds/tools/src/cds/compiler/command.ts new file mode 100644 index 000000000..d382f0d86 --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/command.ts @@ -0,0 +1,253 @@ +import { execFileSync } from 'child_process'; +import { existsSync, readdirSync } from 'fs'; +import { join } from 'path'; + +import { quote } from 'shell-quote'; + +import { fileExists } from '../../filesystem'; +import { cdsExtractorLog } from '../../logging'; + +/** + * Cache for CDS command test results to avoid running the same CLI commands repeatedly. + */ +interface CdsCommandCache { + /** Map of command strings to their test results */ + commandResults: Map; + /** Available cache directories discovered during testing */ + availableCacheDirs: string[]; + /** Global command test results */ + globalCommand?: string; + /** Whether cache has been initialized */ + initialized: boolean; +} + +// Global cache instance to share results across all calls +const cdsCommandCache: CdsCommandCache = { + commandResults: new Map(), + availableCacheDirs: [], + initialized: false, +}; + +/** + * Determine the `cds` command to use based on the environment and cache directory. + * + * This function uses a caching strategy to minimize repeated CLI command testing: + * - Initializes a global cache on first call + * - Tests global commands once and caches results + * - Discovers all available cache directories upfront + * - Reuses test results across multiple calls + */ +export function determineCdsCommand(cacheDir: string | undefined, sourceRoot: string): string { + try { + // Always use the efficient path - debug information is collected separately + return getBestCdsCommand(cacheDir, sourceRoot); + } catch (error) { + const errorMessage = `Failed to determine CDS command: ${String(error)}`; + cdsExtractorLog('error', errorMessage); + throw new Error(errorMessage); + } +} + +/** + * Discover all available cache directories in the source tree + * @param sourceRoot The source root directory + * @returns Array of cache directory paths + */ +function discoverAvailableCacheDirs(sourceRoot: string): string[] { + if (cdsCommandCache.availableCacheDirs.length > 0) { + return cdsCommandCache.availableCacheDirs; + } + + const cacheRootDir = join(sourceRoot, '.cds-extractor-cache'); + const availableDirs: string[] = []; + + try { + if (existsSync(cacheRootDir)) { + const entries = readdirSync(cacheRootDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('cds-')) { + const cacheDir = join(cacheRootDir, entry.name); + const cdsBin = join(cacheDir, 'node_modules', '.bin', 'cds'); + if (fileExists(cdsBin)) { + availableDirs.push(cacheDir); + } + } + } + } + } catch (error) { + cdsExtractorLog('debug', `Failed to discover cache directories: ${String(error)}`); + } + + cdsCommandCache.availableCacheDirs = availableDirs; + return availableDirs; +} + +/** + * Get the best CDS command for a specific cache directory + * @param cacheDir Optional specific cache directory + * @param sourceRoot The source root directory + * @returns The best CDS command to use + */ +function getBestCdsCommand(cacheDir: string | undefined, sourceRoot: string): string { + // Initialize cache if needed + initializeCdsCommandCache(sourceRoot); + + // If a specific cache directory is provided and valid, prefer it + if (cacheDir) { + const localCdsBin = join(cacheDir, 'node_modules', '.bin', 'cds'); + if (fileExists(localCdsBin)) { + const result = testCdsCommand(localCdsBin, sourceRoot, true); + if (result.works) { + return localCdsBin; + } + } + } + + // Try any available cache directories + for (const availableCacheDir of cdsCommandCache.availableCacheDirs) { + const localCdsBin = join(availableCacheDir, 'node_modules', '.bin', 'cds'); + const result = testCdsCommand(localCdsBin, sourceRoot, true); + if (result.works) { + return localCdsBin; + } + } + + // Fall back to global command + if (cdsCommandCache.globalCommand) { + return cdsCommandCache.globalCommand; + } + + // Final fallback: test remaining npx options + const fallbackCommands = ['npx -y --package @sap/cds cds', 'npx --yes @sap/cds-dk cds']; + + for (const command of fallbackCommands) { + const result = testCdsCommand(command, sourceRoot, true); + if (result.works) { + return command; + } + } + + // Return the default fallback even if it doesn't work, as tests expect this behavior + return 'npx -y --package @sap/cds-dk cds'; +} + +/** + * Initialize the CDS command cache by testing global commands + * @param sourceRoot The source root directory + */ +function initializeCdsCommandCache(sourceRoot: string): void { + if (cdsCommandCache.initialized) { + return; + } + + cdsExtractorLog('info', 'Initializing CDS command cache...'); + + // Test global commands first (most commonly used) + const globalCommands = ['cds', 'npx -y --package @sap/cds-dk cds']; + + for (const command of globalCommands) { + const result = testCdsCommand(command, sourceRoot, true); // Silent testing + if (result.works) { + cdsCommandCache.globalCommand = command; + cdsExtractorLog( + 'info', + `Found working global CDS command: ${command} (v${result.version ?? 'unknown'})`, + ); + break; + } + } + + // Discover available cache directories + const cacheDirs = discoverAvailableCacheDirs(sourceRoot); + if (cacheDirs.length > 0) { + cdsExtractorLog( + 'info', + `Discovered ${cacheDirs.length} CDS cache director${cacheDirs.length === 1 ? 'y' : 'ies'}`, + ); + } + + cdsCommandCache.initialized = true; +} + +/** + * Reset the command cache - primarily for testing + */ +export function resetCdsCommandCache(): void { + cdsCommandCache.commandResults.clear(); + cdsCommandCache.availableCacheDirs = []; + cdsCommandCache.globalCommand = undefined; + cdsCommandCache.initialized = false; +} + +/** + * Check if a CDS command is available and working + * @param command The command to test + * @param sourceRoot The source root directory to use as cwd when testing the command + * @param silent Whether to suppress logging of test failures + * @returns Object with test result and version information + */ +function testCdsCommand( + command: string, + sourceRoot: string, + silent: boolean = false, +): { works: boolean; version?: string; error?: string } { + // Check cache first + const cachedResult = cdsCommandCache.commandResults.get(command); + if (cachedResult) { + return cachedResult; + } + + try { + // Try to run the command with --version to see if it works + // CRITICAL: Use sourceRoot as cwd and clean environment to avoid conflicts + let result: string; + + const cleanEnv = { + ...process.env, + // Remove any CodeQL-specific environment variables that might interfere + CODEQL_EXTRACTOR_CDS_WIP_DATABASE: undefined, + CODEQL_RUNNER: undefined, + }; + + if (command.includes('node ')) { + // For node commands, we need to split and execute properly + const parts = command.split(' '); + const nodeExecutable = parts[0]; // 'node' + const scriptPath = parts[1].replace(/"/g, ''); // Remove quotes from path + result = execFileSync(nodeExecutable, [scriptPath, '--version'], { + encoding: 'utf8', + stdio: 'pipe', + timeout: 5000, // Reduced timeout for faster failure + cwd: sourceRoot, + env: cleanEnv, + }).toString(); + } else { + // Use shell-quote to properly escape the command and prevent injection + const escapedCommand = quote([command, '--version']); + result = execFileSync('sh', ['-c', escapedCommand], { + encoding: 'utf8', + stdio: 'pipe', + timeout: 5000, // Reduced timeout for faster failure + cwd: sourceRoot, + env: cleanEnv, + }).toString(); + } + + // Extract version from output (typically in format "@sap/cds-dk: 6.1.3" or just "6.1.3") + const versionMatch = result.match(/(\d+\.\d+\.\d+)/); + const version = versionMatch ? versionMatch[1] : undefined; + + const testResult = { works: true, version }; + cdsCommandCache.commandResults.set(command, testResult); + return testResult; + } catch (error) { + const errorMessage = String(error); + if (!silent) { + cdsExtractorLog('debug', `CDS command test failed for '${command}': ${errorMessage}`); + } + + const testResult = { works: false, error: errorMessage }; + cdsCommandCache.commandResults.set(command, testResult); + return testResult; + } +} diff --git a/extractors/cds/tools/src/cds/compiler/compile.ts b/extractors/cds/tools/src/cds/compiler/compile.ts new file mode 100644 index 000000000..b4ac8af77 --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/compile.ts @@ -0,0 +1,440 @@ +import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { resolve, join, delimiter, relative } from 'path'; + +import { globSync } from 'glob'; + +import { CdsCompilationResult } from './types'; +import { getCdsVersion } from './version'; +import { fileExists, dirExists, recursivelyRenameJsonFiles } from '../../filesystem'; +import { cdsExtractorLog } from '../../logging'; +import { BasicCdsProject } from '../parser/types'; + +/** + * Compiles a CDS file to JSON using robust, project-aware compilation only. + * This function has been refactored to align with the autobuild.md vision by removing all + * forms of individual file compilation and ensuring only project-aware compilation is used. + * + * For root files, this will compile them to their 1:1 .cds.json representation if and only + * if the file is a true root file in a project. + * + * @param cdsFilePath The path to the CDS file to compile, relative to the `sourceRoot`. + * @param sourceRoot The source root directory scanned by the CDS extractor. + * CRITICAL: All spawned processes must use this as their cwd to ensure paths in generated + * JSON are relative to sourceRoot. + * + * @param cdsCommand The actual shell command to use for `cds compile`. + * @param cacheDir Full path to the cache directory where dependencies are stored. + * @param projectMap Map of project directories to {@link BasicCdsProject} instances. + * @param projectDir The project directory to which `cdsFilePath` belongs. + * + * @returns The {@link CdsCompilationResult} of the compilation attempt. + */ +export function compileCdsToJson( + cdsFilePath: string, + sourceRoot: string, + cdsCommand: string, + cacheDir: string | undefined, + projectMap: Map, + projectDir: string, +): CdsCompilationResult { + try { + const resolvedCdsFilePath = resolve(cdsFilePath); + if (!fileExists(resolvedCdsFilePath)) { + throw new Error(`Expected CDS file '${resolvedCdsFilePath}' does not exist.`); + } + + // Get and log the CDS version + const cdsVersion = getCdsVersion(cdsCommand, cacheDir); + const versionInfo = cdsVersion ? `with CDS v${cdsVersion}` : ''; + + // CRITICAL: Create spawn options with sourceRoot as cwd to ensure correct path generation + const spawnOptions = createSpawnOptions(sourceRoot, cdsCommand, cacheDir); + + // Throw an error if projectDir cannot be found in the projectMap. + if (!projectMap || !projectDir || !projectMap.has(projectDir)) { + throw new Error( + `Project directory '${projectDir}' not found in projectMap. Ensure the project is properly initialized.`, + ); + } + + const project = projectMap.get(projectDir); + const relativePath = relative(sourceRoot, resolvedCdsFilePath); + + // Check if this is a project-level compilation marker + if (shouldUseProjectLevelCompilation(project)) { + return compileProjectLevel( + resolvedCdsFilePath, + sourceRoot, + projectDir, + cdsCommand, + spawnOptions, + versionInfo, + ); + } + + // Check if this file is in the list of files to compile for this project + if (!shouldCompileIndividually(project, relativePath)) { + cdsExtractorLog( + 'info', + `${resolvedCdsFilePath} is imported by other files - will be compiled as part of a project ${versionInfo}...`, + ); + const cdsJsonOutPath = `${resolvedCdsFilePath}.json`; + return { + success: true, + outputPath: cdsJsonOutPath, + compiledAsProject: true, + message: 'File was compiled as part of a project-based compilation', + }; + } else { + // This is a root file - compile it using project-aware approach to its 1:1 representation + cdsExtractorLog( + 'info', + `${resolvedCdsFilePath} identified as a root CDS file - using project-aware compilation for root file ${versionInfo}...`, + ); + return compileRootFileAsProject( + resolvedCdsFilePath, + sourceRoot, + projectDir, + cdsCommand, + spawnOptions, + versionInfo, + ); + } + } catch (error) { + return { success: false, message: String(error) }; + } +} + +/** + * Handles project-level compilation for CAP projects with typical directory structure. + * CRITICAL: Uses the project directory as cwd and calculates paths relative to project directory. + * + * @param resolvedCdsFilePath The resolved CDS file path that triggered this compilation + * @param sourceRoot The source root directory + * @param projectDir The project directory (relative to sourceRoot) + * @param cdsCommand The CDS command to use + * @param spawnOptions Pre-configured spawn options with sourceRoot as cwd + * @param versionInfo Version information for logging + * @returns Compilation result + */ +function compileProjectLevel( + resolvedCdsFilePath: string, + sourceRoot: string, + projectDir: string, + cdsCommand: string, + spawnOptions: SpawnSyncOptions, + _versionInfo: string, +): CdsCompilationResult { + cdsExtractorLog( + 'info', + `${resolvedCdsFilePath} is part of a CAP project - using project-aware compilation ${_versionInfo}...`, + ); + + // For project-level compilation, compile the entire project together + // This follows the CAP best practice of compiling db and srv directories together + const projectAbsolutePath = join(sourceRoot, projectDir); + + // Common directories in CAP projects that should be compiled together + const capDirectories = ['db', 'srv', 'app']; + const existingDirectories: string[] = []; + + for (const dir of capDirectories) { + const dirPath = join(projectAbsolutePath, dir); + if (dirExists(dirPath)) { + existingDirectories.push(dir); + } + } + + // Check if there are any CDS files in the project at all before proceeding + const allCdsFiles = globSync(join(projectAbsolutePath, '**/*.cds'), { + nodir: true, + ignore: ['**/node_modules/**'], + }); + + if (allCdsFiles.length === 0) { + throw new Error( + `Project directory '${projectDir}' does not contain any CDS files and cannot be compiled`, + ); + } + + if (existingDirectories.length === 0) { + // If no standard directories, check if there are CDS files in the root + const rootCdsFiles = globSync(join(projectAbsolutePath, '*.cds')); + if (rootCdsFiles.length > 0) { + existingDirectories.push('.'); + } else { + // Find directories that contain CDS files + const cdsFileParents = new Set( + allCdsFiles.map((file: string) => { + const relativePath = relative(projectAbsolutePath, file); + const firstDir = relativePath.split('/')[0]; + return firstDir === relativePath ? '.' : firstDir; + }), + ); + existingDirectories.push(...Array.from(cdsFileParents)); + } + } + + // Generate output path for the compiled model - relative to sourceRoot for consistency + const relativeOutputPath = join(projectDir, 'model.cds.json'); + const projectJsonOutPath = join(sourceRoot, relativeOutputPath); + + // Use sourceRoot as working directory but provide project-relative paths + const projectSpawnOptions: SpawnSyncOptions = { + ...spawnOptions, + cwd: sourceRoot, // Use sourceRoot as working directory for consistency + }; + + // Convert directories to be relative to sourceRoot (include project prefix) + const projectRelativeDirectories = existingDirectories.map(dir => + dir === '.' ? projectDir : join(projectDir, dir), + ); + + const compileArgs = [ + 'compile', + ...projectRelativeDirectories, // Use paths relative to sourceRoot + '--to', + 'json', + '--dest', + join(projectDir, 'model.cds.json'), // Output to specific model.cds.json file + '--locations', + '--log-level', + 'warn', + ]; + + cdsExtractorLog('info', `Compiling CAP project directories: ${existingDirectories.join(', ')}`); + cdsExtractorLog( + 'info', + `Executing CDS command in directory ${projectAbsolutePath}: command='${cdsCommand}' args='${JSON.stringify(compileArgs)}'`, + ); + + // CRITICAL: Use the project directory as cwd + // Use array arguments for consistent test behavior + const result = spawnSync(cdsCommand, compileArgs, projectSpawnOptions); + + if (result.error) { + cdsExtractorLog('error', `SpawnSync error: ${result.error.message}`); + throw new Error(`Error executing CDS compiler: ${result.error.message}`); + } + + // Log stderr for debugging even on success (CDS often writes warnings to stderr) + if (result.stderr && result.stderr.length > 0) { + cdsExtractorLog('warn', `CDS stderr output: ${result.stderr.toString()}`); + } + + if (result.status !== 0) { + cdsExtractorLog('error', `CDS command failed with status ${result.status}`); + cdsExtractorLog( + 'error', + `Command: ${cdsCommand} ${compileArgs.map(arg => (arg.includes(' ') ? `"${arg}"` : arg)).join(' ')}`, + ); + cdsExtractorLog('error', `Stdout: ${result.stdout?.toString() || 'No stdout'}`); + cdsExtractorLog('error', `Stderr: ${result.stderr?.toString() || 'No stderr'}`); + throw new Error( + `Could not compile the CAP project ${projectDir}.\nReported error(s):\n\`\`\`\n${ + result.stderr?.toString() || 'Unknown error' + }\n\`\`\``, + ); + } + + if (!fileExists(projectJsonOutPath) && !dirExists(projectJsonOutPath)) { + throw new Error( + `CAP project '${projectDir}' was not compiled to JSON. This is likely because the project structure is invalid.`, + ); + } + + // Handle directory output if the CDS compiler generated a directory + if (dirExists(projectJsonOutPath)) { + cdsExtractorLog( + 'info', + `CDS compiler generated JSON to output directory: ${projectJsonOutPath}`, + ); + // Recursively rename all .json files to have a .cds.json extension + recursivelyRenameJsonFiles(projectJsonOutPath); + } else { + cdsExtractorLog('info', `CDS compiler generated JSON to file: ${projectJsonOutPath}`); + } + + return { + success: true, + outputPath: projectJsonOutPath, + compiledAsProject: true, + message: 'Project was compiled using project-aware compilation', + }; +} + +/** + * Compiles a root CDS file using project-aware approach for 1:1 .cds.json representation. + * This follows the autobuild.md vision of project-aware compilation only. + * + * @param resolvedCdsFilePath The resolved CDS file path + * @param sourceRoot The source root directory + * @param projectDir The project directory + * @param cdsCommand The CDS command to use + * @param spawnOptions Pre-configured spawn options + * @param versionInfo Version information for logging + * @returns Compilation result + */ +function compileRootFileAsProject( + resolvedCdsFilePath: string, + sourceRoot: string, + _projectDir: string, + cdsCommand: string, + spawnOptions: SpawnSyncOptions, + _versionInfo: string, +): CdsCompilationResult { + // Calculate relative path for the output file + const relativeCdsPath = relative(sourceRoot, resolvedCdsFilePath); + const cdsJsonOutPath = `${resolvedCdsFilePath}.json`; + + // Use project-aware compilation with specific file target + const compileArgs = [ + 'compile', + relativeCdsPath, // Compile the specific file relative to sourceRoot + '--to', + 'json', + '--dest', + `${relativeCdsPath}.json`, + '--locations', + '--log-level', + 'warn', + ]; + + cdsExtractorLog( + 'info', + `Compiling root CDS file using project-aware approach: ${relativeCdsPath}`, + ); + cdsExtractorLog( + 'info', + `Executing CDS command: command='${cdsCommand}' args='${JSON.stringify(compileArgs)}'`, + ); + + // Execute the compilation + const result = spawnSync(cdsCommand, compileArgs, spawnOptions); + + if (result.error) { + cdsExtractorLog('error', `SpawnSync error: ${result.error.message}`); + throw new Error(`Error executing CDS compiler: ${result.error.message}`); + } + + // Log stderr for debugging even on success + if (result.stderr && result.stderr.length > 0) { + cdsExtractorLog('warn', `CDS stderr output: ${result.stderr.toString()}`); + } + + if (result.status !== 0) { + cdsExtractorLog('error', `CDS command failed with status ${result.status}`); + cdsExtractorLog( + 'error', + `Command: ${cdsCommand} ${compileArgs.map(arg => (arg.includes(' ') ? `"${arg}"` : arg)).join(' ')}`, + ); + cdsExtractorLog('error', `Stdout: ${result.stdout?.toString() || 'No stdout'}`); + cdsExtractorLog('error', `Stderr: ${result.stderr?.toString() || 'No stderr'}`); + throw new Error( + `Could not compile the root CDS file ${relativeCdsPath}.\nReported error(s):\n\`\`\`\n${ + result.stderr?.toString() || 'Unknown error' + }\n\`\`\``, + ); + } + + if (!fileExists(cdsJsonOutPath) && !dirExists(cdsJsonOutPath)) { + throw new Error( + `Root CDS file '${relativeCdsPath}' was not compiled to JSON. Expected output: ${cdsJsonOutPath}`, + ); + } + + // Handle directory output if the CDS compiler generated a directory + if (dirExists(cdsJsonOutPath)) { + cdsExtractorLog('info', `CDS compiler generated JSON to output directory: ${cdsJsonOutPath}`); + // Recursively rename all .json files to have a .cds.json extension + recursivelyRenameJsonFiles(cdsJsonOutPath); + } else { + cdsExtractorLog('info', `CDS compiler generated JSON to file: ${cdsJsonOutPath}`); + } + + return { + success: true, + outputPath: cdsJsonOutPath, + compiledAsProject: true, + message: 'Root file compiled using project-aware compilation', + }; +} + +/** + * Creates spawn options for CDS compilation processes. + * CRITICAL: Always sets cwd to sourceRoot to ensure generated JSON paths are relative to sourceRoot. + * + * @param sourceRoot The source root directory - used as cwd for all spawned processes + * @param cdsCommand The CDS command to determine if we need Node.js environment setup + * @param cacheDir Optional cache directory for dependencies + * @returns Spawn options configured for CDS compilation + */ +function createSpawnOptions( + sourceRoot: string, + cdsCommand: string, + cacheDir?: string, +): SpawnSyncOptions { + const spawnOptions: SpawnSyncOptions = { + cwd: sourceRoot, // CRITICAL: Always use sourceRoot as cwd to ensure correct path generation + shell: false, // Use shell=false to ensure proper argument handling for paths with spaces + stdio: 'pipe', + env: { ...process.env }, + }; + + // Check if we're using a direct binary path (contains node_modules/.bin/) or npx-style command + const isDirectBinary = cdsCommand.includes('node_modules/.bin/'); + + // Only set up Node.js environment for npx-style commands, not for direct binary execution + if (cacheDir && !isDirectBinary) { + const nodePath = join(cacheDir, 'node_modules'); + + // Set up environment to use the cached dependencies + spawnOptions.env = { + ...process.env, + NODE_PATH: `${nodePath}${delimiter}${process.env.NODE_PATH ?? ''}`, + PATH: `${join(nodePath, '.bin')}${delimiter}${process.env.PATH}`, + // Add NPM configuration to ensure dependencies are resolved from the cache directory + npm_config_prefix: cacheDir, + // Ensure we don't pick up global CDS installations that might conflict + npm_config_global: 'false', + // Clear any existing CDS environment variables that might interfere + CDS_HOME: cacheDir, + }; + } else if (isDirectBinary) { + // For direct binary execution, use minimal environment to avoid conflicts + // Remove Node.js-specific environment variables that might interfere + const cleanEnv = { ...process.env }; + delete cleanEnv.NODE_PATH; + delete cleanEnv.npm_config_prefix; + delete cleanEnv.npm_config_global; + delete cleanEnv.CDS_HOME; + + spawnOptions.env = cleanEnv; + } + + return spawnOptions; +} + +/** + * Determines if a file should be compiled individually or skipped because it's part of a project. + * + * @param project The CDS project + * @param relativePath The relative path of the file being checked + * @returns true if the file should be compiled individually + */ +function shouldCompileIndividually( + project: BasicCdsProject | undefined, + relativePath: string, +): boolean { + return project?.cdsFilesToCompile?.includes(relativePath) ?? true; +} + +/** + * Determines if the given project should use project-level compilation. + * + * @param project The CDS project to check + * @returns true if project-level compilation should be used + */ +function shouldUseProjectLevelCompilation(project: BasicCdsProject | undefined): boolean { + return project?.cdsFilesToCompile?.includes('__PROJECT_LEVEL_COMPILATION__') ?? false; +} diff --git a/extractors/cds/tools/src/cds/compiler/graph.ts b/extractors/cds/tools/src/cds/compiler/graph.ts new file mode 100644 index 000000000..d4a26c1db --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/graph.ts @@ -0,0 +1,371 @@ +import { determineCdsCommand } from './command'; +import { compileCdsToJson } from './compile'; +import { CompilationAttempt, CompilationTask, CompilationConfig } from './types'; +import { addCompilationDiagnostic } from '../../diagnostics'; +import { cdsExtractorLog, generateStatusReport } from '../../logging'; +import { CdsDependencyGraph, CdsProject } from '../parser/types'; + +/** Attempt compilation with a specific command and configuration. */ +function attemptCompilation( + task: CompilationTask, + cdsCommand: string, + cacheDir: string | undefined, + dependencyGraph: CdsDependencyGraph, +): CompilationAttempt { + const attemptId = `${task.id}_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; + const startTime = new Date(); + + const attempt: CompilationAttempt = { + id: attemptId, + cdsCommand, + cacheDir, + timestamp: startTime, + result: { + success: false, + timestamp: startTime, + }, + }; + + try { + // For now, we'll use the first source file for compilation + // In a more sophisticated implementation, we might handle project-level compilation differently + const primarySourceFile = task.sourceFiles[0]; + + const compilationResult = compileCdsToJson( + primarySourceFile, + dependencyGraph.sourceRootDir, + cdsCommand, + cacheDir, + // Convert CDS projects to BasicCdsProject format expected by compileCdsToJson + new Map( + Array.from(dependencyGraph.projects.entries()).map(([key, value]) => [ + key, + { + cdsFiles: value.cdsFiles, + cdsFilesToCompile: value.cdsFilesToCompile, + expectedOutputFiles: value.expectedOutputFiles, + projectDir: value.projectDir, + dependencies: value.dependencies, + imports: value.imports, + packageJson: value.packageJson, + compilationConfig: value.compilationConfig, + }, + ]), + ), + task.projectDir, + ); + + const endTime = new Date(); + attempt.result = { + ...compilationResult, + timestamp: endTime, + durationMs: endTime.getTime() - startTime.getTime(), + commandUsed: cdsCommand, + cacheDir, + }; + + if (compilationResult.success && compilationResult.outputPath) { + dependencyGraph.statusSummary.jsonFilesGenerated++; + } + } catch (error) { + const endTime = new Date(); + attempt.error = { + message: String(error), + stack: error instanceof Error ? error.stack : undefined, + }; + attempt.result.timestamp = endTime; + attempt.result.durationMs = endTime.getTime() - startTime.getTime(); + } + + task.attempts.push(attempt); + return attempt; +} + +/** + * Create a compilation task for a project or individual file + */ +function createCompilationTask( + type: 'file' | 'project', + sourceFiles: string[], + expectedOutputFiles: string[], + projectDir: string, + useProjectLevelCompilation: boolean, +): CompilationTask { + return { + id: `${type}_${projectDir}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type, + status: 'pending', + sourceFiles, + expectedOutputFiles, + projectDir, + attempts: [], + useProjectLevelCompilation, + dependencies: [], + }; +} + +function createCompilationConfig( + cdsCommand: string, + cacheDir: string | undefined, + useProjectLevel: boolean, +): CompilationConfig { + return { + cdsCommand: cdsCommand, + cacheDir: cacheDir, + useProjectLevelCompilation: useProjectLevel, + versionCompatibility: { + isCompatible: true, // Will be validated during planning + }, + maxRetryAttempts: 3, + }; +} + +/** + * Execute a single compilation task + */ +function executeCompilationTask( + task: CompilationTask, + project: CdsProject, + dependencyGraph: CdsDependencyGraph, + codeqlExePath: string, +): void { + task.status = 'in_progress'; + + const config = project.enhancedCompilationConfig; + if (!config) { + throw new Error(`No compilation configuration found for project ${project.projectDir}`); + } + + const compilationAttempt = attemptCompilation( + task, + config.cdsCommand, + config.cacheDir, + dependencyGraph, + ); + + if (compilationAttempt.result.success) { + task.status = 'success'; + dependencyGraph.statusSummary.successfulCompilations++; + return; + } + + // Compilation failed - mark task as failed + const lastError = compilationAttempt.error + ? new Error(compilationAttempt.error.message) + : new Error('Compilation failed'); + + task.status = 'failed'; + task.errorSummary = lastError?.message || 'Compilation failed'; + dependencyGraph.statusSummary.failedCompilations++; + + // Add diagnostic for failed compilation + for (const sourceFile of task.sourceFiles) { + addCompilationDiagnostic(sourceFile, task.errorSummary, codeqlExePath); + } + + cdsExtractorLog('error', `Compilation failed for task ${task.id}: ${task.errorSummary}`); +} + +/** + * Executes all compilation tasks for the provided {@link CdsDependencyGraph}. + * Uses the provided `codeqlExePath` to run the CodeQL CLI, as needed, for + * generating diagnositic warnings and/or errors for problems encountered while + * running the CodeQL CDS extractor. + */ +function executeCompilationTasks(dependencyGraph: CdsDependencyGraph, codeqlExePath: string): void { + cdsExtractorLog('info', 'Starting compilation execution for all projects...'); + + dependencyGraph.currentPhase = 'compiling'; + const compilationStartTime = new Date(); + + // Collect all compilation tasks from all projects. + const allTasks: Array<{ task: CompilationTask; project: CdsProject }> = []; + + for (const project of dependencyGraph.projects.values()) { + for (const task of project.compilationTasks) { + allTasks.push({ task, project }); + } + } + + // Execute compilation tasks sequentially. There is room for optimization in the future. + // For now, we keep it simple to ensure consistent debug information collection. + cdsExtractorLog('info', `Executing ${allTasks.length} compilation task(s)...`); + for (const { task, project } of allTasks) { + try { + executeCompilationTask(task, project, dependencyGraph, codeqlExePath); + } catch (error) { + const errorMessage = `Failed to execute compilation task ${task.id}: ${String(error)}`; + cdsExtractorLog('error', errorMessage); + + dependencyGraph.errors.critical.push({ + phase: 'compiling', + message: errorMessage, + timestamp: new Date(), + stack: error instanceof Error ? error.stack : undefined, + }); + + task.status = 'failed'; + task.errorSummary = errorMessage; + dependencyGraph.statusSummary.failedCompilations++; + } + } + + // Update project statuses + for (const project of dependencyGraph.projects.values()) { + const allTasksCompleted = project.compilationTasks.every( + task => task.status === 'success' || task.status === 'failed', + ); + + if (allTasksCompleted) { + const hasFailedTasks = project.compilationTasks.some(task => task.status === 'failed'); + project.status = hasFailedTasks ? 'failed' : 'completed'; + project.timestamps.compilationCompleted = new Date(); + } + } + + const compilationEndTime = new Date(); + dependencyGraph.statusSummary.performance.compilationDurationMs = + compilationEndTime.getTime() - compilationStartTime.getTime(); + + cdsExtractorLog( + 'info', + `Compilation execution completed. Success: ${dependencyGraph.statusSummary.successfulCompilations}, Failed: ${dependencyGraph.statusSummary.failedCompilations}`, + ); +} + +/** + * Orchestrates the compilation process for CDS files based on a dependency graph. + * + * This function coordinates the planning and execution of compilation tasks, + * tracks the compilation status, and generates a post-compilation report. + * + * @param dependencyGraph - The {@link CdsDependencyGraph} representing the CDS projects, + * project dependencies, expected compilation tasks, and their statuses. + * @param projectCacheDirMap - A map from project identifiers to their cache directory paths. + * @param codeqlExePath - The path to the CodeQL executable. Used for generating diagnostic + * messages as part of the broader CodeQL (JavaScript) extraction process. + * @throws Will rethrow any errors encountered during compilation, after logging them. + */ +export function orchestrateCompilation( + dependencyGraph: CdsDependencyGraph, + projectCacheDirMap: Map, + codeqlExePath: string, +): void { + try { + planCompilationTasks(dependencyGraph, projectCacheDirMap); + + executeCompilationTasks(dependencyGraph, codeqlExePath); + + // Update overall status + const hasFailures = + dependencyGraph.statusSummary.failedCompilations > 0 || + dependencyGraph.errors.critical.length > 0; + + dependencyGraph.statusSummary.overallSuccess = !hasFailures; + dependencyGraph.currentPhase = hasFailures ? 'failed' : 'completed'; + + // Generate and log a "Post-Compilation" status report, aka before the JavaScript extractor runs. + const statusReport = generateStatusReport(dependencyGraph); + cdsExtractorLog('info', 'CDS Extractor Status Report : Post-Compilation...\n' + statusReport); + } catch (error) { + const errorMessage = `Compilation orchestration failed: ${String(error)}`; + cdsExtractorLog('error', errorMessage); + + dependencyGraph.errors.critical.push({ + phase: 'compiling', + message: errorMessage, + timestamp: new Date(), + stack: error instanceof Error ? error.stack : undefined, + }); + + dependencyGraph.currentPhase = 'failed'; + dependencyGraph.statusSummary.overallSuccess = false; + + throw error; + } +} + +/** Plan compilation tasks for all projects in the dependency graph. */ +function planCompilationTasks( + dependencyGraph: CdsDependencyGraph, + projectCacheDirMap: Map, +): void { + cdsExtractorLog('info', 'Planning compilation tasks for all projects...'); + + dependencyGraph.currentPhase = 'compilation_planning'; + + for (const [projectDir, project] of dependencyGraph.projects.entries()) { + try { + const cacheDir = projectCacheDirMap.get(projectDir); + + // Determine CDS command + const cdsCommand = determineCdsCommand(cacheDir, dependencyGraph.sourceRootDir); + + // Create compilation configuration + const compilationConfig = createCompilationConfig( + cdsCommand, + cacheDir, + project.cdsFilesToCompile.includes('__PROJECT_LEVEL_COMPILATION__'), + ); + + project.enhancedCompilationConfig = compilationConfig; + + // Create compilation tasks + if (project.cdsFilesToCompile.includes('__PROJECT_LEVEL_COMPILATION__')) { + // Project-level compilation + const task = createCompilationTask( + 'project', + project.cdsFiles, + project.expectedOutputFiles, + projectDir, + true, + ); + project.compilationTasks = [task]; + } else { + // Individual file compilation + const tasks: CompilationTask[] = []; + for (const cdsFile of project.cdsFilesToCompile) { + const expectedOutput = `${cdsFile}.json`; + const task = createCompilationTask( + 'file', + [cdsFile], + [expectedOutput], + projectDir, + false, + ); + tasks.push(task); + } + project.compilationTasks = tasks; + } + + project.status = 'compilation_planned'; + project.timestamps.compilationStarted = new Date(); + + cdsExtractorLog( + 'info', + `Planned ${project.compilationTasks.length} compilation task(s) for project ${projectDir}`, + ); + } catch (error) { + const errorMessage = `Failed to plan compilation for project ${projectDir}: ${String(error)}`; + cdsExtractorLog('error', errorMessage); + + dependencyGraph.errors.critical.push({ + phase: 'compilation_planning', + message: errorMessage, + timestamp: new Date(), + stack: error instanceof Error ? error.stack : undefined, + }); + + project.status = 'failed'; + } + } + + const totalTasks = Array.from(dependencyGraph.projects.values()).reduce( + (sum, project) => sum + project.compilationTasks.length, + 0, + ); + + dependencyGraph.statusSummary.totalCompilationTasks = totalTasks; + + cdsExtractorLog('info', `Compilation planning completed. Total tasks: ${totalTasks}`); +} diff --git a/extractors/cds/tools/src/cds/compiler/index.ts b/extractors/cds/tools/src/cds/compiler/index.ts new file mode 100644 index 000000000..c8d102794 --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/index.ts @@ -0,0 +1,5 @@ +export { determineCdsCommand, resetCdsCommandCache } from './command'; +export { compileCdsToJson } from './compile'; +export { orchestrateCompilation } from './graph'; +export { findProjectForCdsFile } from './project'; +export { getCdsVersion } from './version'; diff --git a/extractors/cds/tools/src/cds/compiler/project.ts b/extractors/cds/tools/src/cds/compiler/project.ts new file mode 100644 index 000000000..731b0c84b --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/project.ts @@ -0,0 +1,41 @@ +import { relative } from 'path'; + +/** + * Helper functions for mapping CDS files to their projects and cache directories + */ + +/** + * Find the project directory for a CDS file + * @param cdsFilePath Path to the CDS file + * @param sourceRoot Source root directory + * @param projectMap Map of project directories to project objects + * @returns The project directory the file belongs to, or undefined if not found + */ +export function findProjectForCdsFile( + cdsFilePath: string, + sourceRoot: string, + projectMap: Map, +): string | undefined { + // Get the relative path to the project directory for this CDS file + const relativeCdsFilePath = relative(sourceRoot, cdsFilePath); + + // If the file is outside the source root, path.relative() will start with '../' + // In this case, we should also check against the absolute path + const isOutsideSourceRoot = relativeCdsFilePath.startsWith('../'); + + // Find the project this file belongs to + for (const [projectDir, project] of projectMap.entries()) { + if ( + project.cdsFiles.some( + cdsFile => + cdsFile === relativeCdsFilePath || + relativeCdsFilePath.startsWith(projectDir) || + (isOutsideSourceRoot && cdsFile === cdsFilePath), + ) + ) { + return projectDir; + } + } + + return undefined; +} diff --git a/extractors/cds/tools/src/cds/compiler/types.ts b/extractors/cds/tools/src/cds/compiler/types.ts new file mode 100644 index 000000000..ce5f3beac --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/types.ts @@ -0,0 +1,91 @@ +/** Types for the `src/cds/compiler` package. */ + +/** Result of a CDS compilation attempt. */ +export interface CdsCompilationResult { + success: boolean; + message?: string; + outputPath?: string; + /** Flag indicating if this file was compiled directly or as part of a project */ + compiledAsProject?: boolean; + /** Timestamp when compilation was attempted */ + timestamp?: Date; + /** Duration of compilation in milliseconds */ + durationMs?: number; + /** Command used for compilation */ + commandUsed?: string; + /** Cache directory used during compilation */ + cacheDir?: string; +} + +/** Compilation attempt tracking for retry logic. */ +export interface CompilationAttempt { + /** Unique identifier for this attempt */ + id: string; + /** The CDS command used */ + cdsCommand: string; + /** Cache directory used, if any */ + cacheDir?: string; + /** Timestamp when attempt was made */ + timestamp: Date; + /** Result of the compilation attempt */ + result: CdsCompilationResult; + /** Error details if compilation failed */ + error?: { + code?: string | number; + message: string; + stack?: string; + }; +} + +/** Compilation configuration for managing compilation commands and retries. */ +export interface CompilationConfig { + /** CDS command to use */ + cdsCommand: string; + /** Cache directory */ + cacheDir?: string; + /** Whether to use project-level compilation */ + useProjectLevelCompilation: boolean; + /** Version compatibility information */ + versionCompatibility: { + isCompatible: boolean; + errorMessage?: string; + cdsVersion?: string; + expectedCdsVersion?: string; + }; + /** Maximum number of retry attempts allowed */ + maxRetryAttempts: number; + /** Command analysis details for debugging */ + commandAnalysis?: Record; +} + +/** Compilation status for tracking compilation attempts and results. */ +export type CompilationStatus = + | 'pending' // File/project is scheduled for compilation + | 'in_progress' // Compilation is currently running + | 'success' // Compilation completed successfully + | 'failed' // Compilation failed + | 'skipped'; // Compilation was skipped (e.g., already compiled) + +/** Represents an expected CDS compilation task for a file or project. */ +export interface CompilationTask { + /** Unique identifier for this task */ + id: string; + /** Type of compilation task */ + type: 'file' | 'project'; + /** Current status of the task */ + status: CompilationStatus; + /** Source file(s) involved in this task */ + sourceFiles: string[]; + /** Expected output file(s) */ + expectedOutputFiles: string[]; + /** Project directory this task belongs to */ + projectDir: string; + /** All compilation attempts for this task */ + attempts: CompilationAttempt[]; + /** Whether this task uses project-level compilation */ + useProjectLevelCompilation: boolean; + /** Tasks that this task depends on */ + dependencies: string[]; + /** Error summary if all attempts failed */ + errorSummary?: string; +} diff --git a/extractors/cds/tools/src/cds/compiler/version.ts b/extractors/cds/tools/src/cds/compiler/version.ts new file mode 100644 index 000000000..19080ddad --- /dev/null +++ b/extractors/cds/tools/src/cds/compiler/version.ts @@ -0,0 +1,47 @@ +import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { join, delimiter } from 'path'; + +/** + * Get the CDS compiler version from a specific command or cache directory. + * @param cdsCommand The CDS command to use. + * @param cacheDir Optional path to a directory containing installed dependencies. + * @returns The CDS compiler version string, or undefined if it couldn't be determined. + */ +export function getCdsVersion(cdsCommand: string, cacheDir?: string): string | undefined { + try { + // Set up environment vars if using a cache directory + const spawnOptions: SpawnSyncOptions = { + shell: true, + stdio: 'pipe', + env: { ...process.env }, + }; + + // If a cache directory is provided, set NODE_PATH to use that cache + if (cacheDir) { + const nodePath = join(cacheDir, 'node_modules'); + + // Set up environment to use the cached dependencies + spawnOptions.env = { + ...process.env, + NODE_PATH: `${nodePath}${delimiter}${process.env.NODE_PATH ?? ''}`, + PATH: `${join(nodePath, '.bin')}${delimiter}${process.env.PATH}`, + npm_config_prefix: cacheDir, + }; + } + + // Execute the CDS command with the --version flag + const result = spawnSync(cdsCommand, ['--version'], spawnOptions); + if (result.status === 0 && result.stdout) { + const versionOutput = result.stdout.toString().trim(); + // Extract version number, which is typically in formats like "@sap/cds: 6.1.3" or similar + const match = versionOutput.match(/@sap\/cds[^0-9]*([0-9]+\.[0-9]+\.[0-9]+)/); + if (match?.[1]) { + return match[1]; // Return just the version number + } + return versionOutput; // Return full output if we couldn't parse it + } + return undefined; + } catch { + return undefined; + } +} diff --git a/extractors/cds/tools/src/cds/parser/functions.ts b/extractors/cds/tools/src/cds/parser/functions.ts new file mode 100644 index 000000000..0750c326f --- /dev/null +++ b/extractors/cds/tools/src/cds/parser/functions.ts @@ -0,0 +1,657 @@ +import { existsSync, readFileSync, statSync } from 'fs'; +import { basename, dirname, join, relative, sep } from 'path'; + +import { sync } from 'glob'; + +import { CdsFilesToCompile, CdsImport, PackageJson } from './types'; +import { cdsExtractorLog } from '../../logging'; + +/** + * Determines the list of CDS files to be parsed for the specified project directory. + * + * @param sourceRootDir - The source root directory to search for CDS files. This is + * used to resolve relative paths in relation to a common (source root) directory for + * multiple projects. + * @param projectDir - The full, local filesystem path of the directory that contains + * the individual `.cds` definition files for some `CAP` project. + * @returns An array of strings representing the paths, relative to the source root + * directory, of the `.cds` files to be parsed for a given project. + */ +export function determineCdsFilesForProjectDir( + sourceRootDir: string, + projectDir: string, +): string[] { + if (!sourceRootDir || !projectDir) { + throw new Error( + `Unable to determine CDS files for project dir '${projectDir}'; both sourceRootDir and projectDir must be provided.`, + ); + } + + // Normalize paths by removing trailing slashes for comparison + const normalizedSourceRoot = sourceRootDir.replace(/[/\\]+$/, ''); + const normalizedProjectDir = projectDir.replace(/[/\\]+$/, ''); + + if ( + !normalizedProjectDir.startsWith(normalizedSourceRoot) && + normalizedProjectDir !== normalizedSourceRoot + ) { + throw new Error( + 'projectDir must be a subdirectory of sourceRootDir or equal to sourceRootDir.', + ); + } + + try { + // Use glob to find all .cds files under the project directory, excluding node_modules + const cdsFiles = sync(join(projectDir, '**/*.cds'), { + nodir: true, + ignore: ['**/node_modules/**', '**/*.testproj/**'], + }); + + // Convert absolute paths to paths relative to sourceRootDir + return cdsFiles.map(file => relative(sourceRootDir, file)); + } catch (error: unknown) { + cdsExtractorLog('error', `Error finding CDS files in ${projectDir}: ${String(error)}`); + return []; + } +} + +/** + * Determines the list of distinct CDS projects under the specified source + * directory. + * @param sourceRootDir - The source root directory to search for CDS projects. + * @returns An array of strings representing the paths, relative to the source + * root directory, of the detected CDS projects. + */ +export function determineCdsProjectsUnderSourceDir(sourceRootDir: string): string[] { + if (!sourceRootDir || !existsSync(sourceRootDir)) { + throw new Error(`Source root directory '${sourceRootDir}' does not exist.`); + } + + const foundProjects = new Set(); + + // Find all potential project directories by looking for package.json files and CDS files + const packageJsonFiles = sync(join(sourceRootDir, '**/package.json'), { + nodir: true, + ignore: ['**/node_modules/**', '**/*.testproj/**'], + }); + + const cdsFiles = sync(join(sourceRootDir, '**/*.cds'), { + nodir: true, + ignore: ['**/node_modules/**', '**/*.testproj/**'], + }); + + // Collect all potential project directories + const candidateDirectories = new Set(); + + // Add directories with package.json files + for (const packageJsonFile of packageJsonFiles) { + candidateDirectories.add(dirname(packageJsonFile)); + } + + // Add directories with CDS files and try to find their project roots + for (const cdsFile of cdsFiles) { + const cdsDir = dirname(cdsFile); + const projectRoot = findProjectRootFromCdsFile(cdsDir, sourceRootDir); + if (projectRoot) { + candidateDirectories.add(projectRoot); + } else { + candidateDirectories.add(cdsDir); + } + } + + // Filter candidates to only include likely CDS projects + for (const dir of candidateDirectories) { + if (isLikelyCdsProject(dir)) { + const relativePath = relative(sourceRootDir, dir); + const projectDir = relativePath || '.'; + + // Check if this project is already included as a parent or child of an existing project + let shouldAdd = true; + const existingProjects = Array.from(foundProjects); + + for (const existingProject of existingProjects) { + const existingAbsPath = join(sourceRootDir, existingProject); + + // Skip if this directory is a subdirectory of an existing project, + // but only if the parent is not a monorepo with its own CDS content + if (dir.startsWith(existingAbsPath + sep)) { + // Check if parent is a monorepo root with its own CDS content + const parentPackageJsonPath = join(existingAbsPath, 'package.json'); + const parentPackageJson = readPackageJsonFile(parentPackageJsonPath); + const isParentMonorepo = + parentPackageJson?.workspaces && + Array.isArray(parentPackageJson.workspaces) && + parentPackageJson.workspaces.length > 0; + + // If parent is a monorepo with CDS content, allow both parent and child + if ( + isParentMonorepo && + (hasStandardCdsContent(existingAbsPath) || hasDirectCdsContent(existingAbsPath)) + ) { + // Both parent and child can coexist as separate CDS projects + shouldAdd = true; + } else { + // Traditional case: exclude subdirectory + shouldAdd = false; + } + break; + } + + // Remove existing project if it's a subdirectory of the current directory, + // unless the current directory is a monorepo root and the existing project has its own CDS content + if (existingAbsPath.startsWith(dir + sep)) { + const currentPackageJsonPath = join(dir, 'package.json'); + const currentPackageJson = readPackageJsonFile(currentPackageJsonPath); + const isCurrentMonorepo = + currentPackageJson?.workspaces && + Array.isArray(currentPackageJson.workspaces) && + currentPackageJson.workspaces.length > 0; + + // If current is a monorepo and the existing project is a legitimate CDS project, keep both + if (!(isCurrentMonorepo && isLikelyCdsProject(existingAbsPath))) { + foundProjects.delete(existingProject); + } + } + } + + if (shouldAdd) { + foundProjects.add(projectDir); + } + } + } + + return Array.from(foundProjects).sort(); +} + +/** + * Parses a CDS file to extract import statements + * + * @param filePath - Path to the CDS file + * @returns Array of import statements found in the file + */ +export function extractCdsImports(filePath: string): CdsImport[] { + if (!existsSync(filePath)) { + throw new Error(`File does not exist: ${filePath}`); + } + + const content = readFileSync(filePath, 'utf8'); + const imports: CdsImport[] = []; + + // Regular expression to match using statements + // This handles: using X from 'path'; and using { X, Y } from 'path'; + // and also using X as Y from 'path'; + const usingRegex = + /using\s+(?:{[^}]+}|[\w.]+(?:\s+as\s+[\w.]+)?)\s+from\s+['"`]([^'"`]+)['"`]\s*;/g; + + let match; + while ((match = usingRegex.exec(content)) !== null) { + const path = match[1]; + imports.push({ + statement: match[0], + path, + isRelative: path.startsWith('./') || path.startsWith('../'), + isModule: !path.startsWith('./') && !path.startsWith('../') && !path.startsWith('/'), + }); + } + + return imports; +} + +/** + * Attempts to find the project root directory starting from a directory containing a CDS file + * + * @param cdsFileDir - Directory containing a CDS file + * @param sourceRootDir - Source root directory to limit the search + * @returns The project root directory or null if not found + */ +function findProjectRootFromCdsFile(cdsFileDir: string, sourceRootDir: string): string | null { + // Skip node_modules and testproj directories entirely + if (cdsFileDir.includes('node_modules') || cdsFileDir.includes('.testproj')) { + return null; + } + + let currentDir = cdsFileDir; + + // Limit the upward search to the sourceRootDir + while (currentDir.startsWith(sourceRootDir)) { + // Check if this directory looks like a project root + if (isLikelyCdsProject(currentDir)) { + // If this is a standard CAP subdirectory (srv, db, app), check if the parent + // directory should be the real project root + const currentDirName = basename(currentDir); + const isStandardSubdir = ['srv', 'db', 'app'].includes(currentDirName); + + if (isStandardSubdir) { + const parentDir = dirname(currentDir); + + if ( + parentDir !== currentDir && + parentDir.startsWith(sourceRootDir) && + !parentDir.includes('node_modules') && + !parentDir.includes('.testproj') && + isLikelyCdsProject(parentDir) + ) { + // The parent is also a CDS project, so it's likely the real project root + return parentDir; + } + } + + // For non-standard subdirectories, also check if the parent might be a better project root + const parentDir = dirname(currentDir); + + if ( + parentDir !== currentDir && + parentDir.startsWith(sourceRootDir) && + !parentDir.includes('node_modules') && + !parentDir.includes('.testproj') + ) { + const hasDbDir = + existsSync(join(parentDir, 'db')) && statSync(join(parentDir, 'db')).isDirectory(); + const hasSrvDir = + existsSync(join(parentDir, 'srv')) && statSync(join(parentDir, 'srv')).isDirectory(); + const hasAppDir = + existsSync(join(parentDir, 'app')) && statSync(join(parentDir, 'app')).isDirectory(); + + // Use the same CAP project structure logic as below + if ((hasDbDir && hasSrvDir) || (hasSrvDir && hasAppDir)) { + return parentDir; + } + } + + return currentDir; + } + + // Check for typical CAP project structure indicators + const hasDbDir = + existsSync(join(currentDir, 'db')) && statSync(join(currentDir, 'db')).isDirectory(); + const hasSrvDir = + existsSync(join(currentDir, 'srv')) && statSync(join(currentDir, 'srv')).isDirectory(); + const hasAppDir = + existsSync(join(currentDir, 'app')) && statSync(join(currentDir, 'app')).isDirectory(); + + if ((hasDbDir && hasSrvDir) || (hasSrvDir && hasAppDir)) { + return currentDir; + } + + // Move up one directory + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + // We've reached the root of the filesystem + break; + } + currentDir = parentDir; + } + + // If we couldn't determine a proper project root, return the original directory + return cdsFileDir; +} + +/** + * Determines if a directory likely contains a CAP project by checking for key + * indicators like package.json with CAP dependencies or .cds files in standard + * locations. + * + * @param dir - Directory to check + * @returns true if the directory likely contains a CAP project + */ +export function isLikelyCdsProject(dir: string): boolean { + try { + // Skip node_modules and testproj directories entirely + if (dir.includes('node_modules') || dir.includes('.testproj')) { + return false; + } + + // Check for CDS files in standard locations (checking both direct and nested files) + const hasStandardCdsDirectories = hasStandardCdsContent(dir); + const hasDirectCdsFiles = hasDirectCdsContent(dir); + const hasCdsFiles = hasStandardCdsDirectories || hasDirectCdsFiles; + + // Check if package.json exists and has CAP dependencies + const hasCapDependencies = hasPackageJsonWithCapDeps(dir); + + if (hasCapDependencies) { + // If there are CAP dependencies but no CDS files, there's nothing for us to do + if (!hasCdsFiles) { + return false; + } + + // Check if this is a monorepo root + const packageJsonPath = join(dir, 'package.json'); + const packageJson = readPackageJsonFile(packageJsonPath); + + if ( + packageJson?.workspaces && + Array.isArray(packageJson.workspaces) && + packageJson.workspaces.length > 0 + ) { + // This is likely a monorepo - only treat as CDS project if it has actual CDS content + if (!hasCdsFiles) { + // This is a monorepo root without its own CDS content + return false; + } + } + + return true; + } + + // If no CAP dependencies, only consider it a CDS project if it has CDS files + return hasCdsFiles; + } catch (error: unknown) { + cdsExtractorLog('error', `Error checking directory ${dir}: ${String(error)}`); + return false; + } +} + +/** + * Check if a directory has CDS content in standard CAP directories. + */ +function hasStandardCdsContent(dir: string): boolean { + const standardLocations = [join(dir, 'db'), join(dir, 'srv'), join(dir, 'app')]; + + for (const location of standardLocations) { + if (existsSync(location) && statSync(location).isDirectory()) { + // Check for any .cds files at any level under these directories. + const cdsFiles = sync(join(location, '**/*.cds'), { nodir: true }); + if (cdsFiles.length > 0) { + return true; + } + } + } + + return false; +} + +/** + * Check if a directory has direct CDS files. + */ +function hasDirectCdsContent(dir: string): boolean { + const directCdsFiles = sync(join(dir, '*.cds')); + return directCdsFiles.length > 0; +} + +/** + * Safely parses a package.json file, using the cache if available + * @param filePath - Path to the package.json file + * @returns The parsed package.json content or undefined if the file doesn't exist or can't be parsed + */ +export function readPackageJsonFile(filePath: string): PackageJson | undefined { + if (!existsSync(filePath)) { + return undefined; + } + + try { + const content = readFileSync(filePath, 'utf8'); + const packageJson = JSON.parse(content) as PackageJson; + return packageJson; + } catch (error) { + cdsExtractorLog('warn', `Error parsing package.json at ${filePath}: ${String(error)}`); + return undefined; + } +} + +/** + * Determines which CDS files should be compiled for a given project and what output files to expect. + * This function analyzes the project structure and dependencies to decide + * whether to use project-level compilation or individual file compilation. + * + * For CAP projects (identified by either having @sap/cds dependencies or + * typical CAP directory structure), it returns a special marker indicating + * project-level compilation should be used. For other projects, it attempts + * to identify root files (files that are not imported by others) and returns + * those for individual compilation. + * + * @param sourceRootDir - The source root directory + * @param project - The project to analyze, containing cdsFiles, imports, and projectDir + * @returns Object containing files to compile and expected output files + */ +export function determineCdsFilesToCompile( + sourceRootDir: string, + project: { + cdsFiles: string[]; + imports?: Map; + projectDir: string; + }, +): CdsFilesToCompile { + if (!project.cdsFiles || project.cdsFiles.length === 0) { + return { + filesToCompile: [], + expectedOutputFiles: [], + }; + } + + // If there's only one CDS file, it should be compiled individually. + if (project.cdsFiles.length === 1) { + const filesToCompile = [...project.cdsFiles]; + return { + filesToCompile, + expectedOutputFiles: computeExpectedOutputFiles(filesToCompile, project.projectDir), + }; + } + + const absoluteProjectDir = join(sourceRootDir, project.projectDir); + const hasCapStructure = hasTypicalCapDirectoryStructure(project.cdsFiles); + const hasCapDeps = hasPackageJsonWithCapDeps(absoluteProjectDir); + + // Use project-level compilation only if: + // 1. It has CAP package.json dependencies, OR + // 2. It has the typical CAP directory structure (db/, srv/ etc.) + if (project.cdsFiles.length > 1 && (hasCapStructure || hasCapDeps)) { + // For CAP projects, we should use project-level compilation + // Return a special marker that indicates the entire project should be compiled together + const filesToCompile = ['__PROJECT_LEVEL_COMPILATION__']; + return { + filesToCompile, + expectedOutputFiles: computeExpectedOutputFiles(filesToCompile, project.projectDir), + }; + } + + // For non-CAP projects or when we can't determine project type, + // fall back to the original logic of identifying root files + if (!project.imports || project.imports.size === 0) { + const filesToCompile = [...project.cdsFiles]; + return { + filesToCompile, + expectedOutputFiles: computeExpectedOutputFiles(filesToCompile, project.projectDir), + }; + } + + try { + // Create a map to track imported files in the project + const importedFiles = new Map(); + + // First pass: collect all imported files in the project + for (const file of project.cdsFiles) { + try { + const absoluteFilePath = join(sourceRootDir, file); + if (existsSync(absoluteFilePath)) { + // Get imports for this file + const imports = project.imports.get(file) ?? []; + + // Mark imported files + for (const importInfo of imports) { + if (importInfo.resolvedPath) { + importedFiles.set(importInfo.resolvedPath, true); + } + } + } + } catch (error) { + cdsExtractorLog('warn', `Error processing imports for ${file}: ${String(error)}`); + } + } + + // Second pass: identify root files (files that are not imported by others) + const rootFiles: string[] = []; + for (const file of project.cdsFiles) { + const relativePath = relative(sourceRootDir, join(sourceRootDir, file)); + const isImported = importedFiles.has(relativePath); + + if (!isImported) { + rootFiles.push(file); + } + } + + // If no root files were identified, fall back to compiling all files + if (rootFiles.length === 0) { + cdsExtractorLog( + 'warn', + `No root CDS files identified in project ${project.projectDir}, will compile all files`, + ); + const filesToCompile = [...project.cdsFiles]; + return { + filesToCompile, + expectedOutputFiles: computeExpectedOutputFiles(filesToCompile, project.projectDir), + }; + } + + return { + filesToCompile: rootFiles, + expectedOutputFiles: computeExpectedOutputFiles(rootFiles, project.projectDir), + }; + } catch (error) { + cdsExtractorLog( + 'warn', + `Error determining files to compile for project ${project.projectDir}: ${String(error)}`, + ); + // Fall back to compiling all files on error + const filesToCompile = [...project.cdsFiles]; + return { + filesToCompile, + expectedOutputFiles: computeExpectedOutputFiles(filesToCompile, project.projectDir), + }; + } +} + +/** + * Computes the expected output files for a given set of files to compile. + * This function predicts what .cds.json files will be generated during compilation. + * + * @param filesToCompile - Array of files to compile (may include special markers) + * @param projectDir - The project directory + * @returns Array of expected output file paths (relative to source root) + */ +function computeExpectedOutputFiles(filesToCompile: string[], projectDir: string): string[] { + const expectedFiles: string[] = []; + + // Check if this project uses project-level compilation. + const usesProjectLevelCompilation = filesToCompile.includes('__PROJECT_LEVEL_COMPILATION__'); + + // Validate that __PROJECT_LEVEL_COMPILATION__ element does not coexist with other + // files. We either expect a single project-level compilation marker or a list of + // individual files to compile, not both. + if (usesProjectLevelCompilation && filesToCompile.length !== 1) { + throw new Error( + `Invalid compilation configuration: '__PROJECT_LEVEL_COMPILATION__' must be the only element in filesToCompile array, but found ${filesToCompile.length} elements: ${filesToCompile.join(', ')}`, + ); + } + + if (usesProjectLevelCompilation) { + // For project-level compilation, expect a single model.cds.json file in the project + // root directory. + const projectModelFile = join(projectDir, 'model.cds.json'); + expectedFiles.push(projectModelFile); + } else { + // For individual file compilation, expect a .cds.json file for each .cds file compiled. + for (const cdsFile of filesToCompile) { + expectedFiles.push(`${cdsFile}.json`); + } + } + + return expectedFiles; +} + +/** + * Determines the expected output files for a project based on its compilation strategy. + * This function predicts what .cds.json files will be generated during compilation. + * + * @param project - The CDS project to analyze + * @returns Array of expected output file paths (relative to source root) + */ +export function determineExpectedOutputFiles(project: { + cdsFilesToCompile: string[]; + projectDir: string; +}): string[] { + const expectedFiles: string[] = []; + + // Check if this project uses project-level compilation. + const usesProjectLevelCompilation = project.cdsFilesToCompile.includes( + '__PROJECT_LEVEL_COMPILATION__', + ); + // Validate that __PROJECT_LEVEL_COMPILATION__ element does not coexist with other + // files. We either expect a single project-level compilation marker or a list of + // individual files to compile, not both. + if (usesProjectLevelCompilation && project.cdsFilesToCompile.length !== 1) { + throw new Error( + `Invalid compilation configuration: '__PROJECT_LEVEL_COMPILATION__' must be the only element in cdsFilesToCompile array, but found ${project.cdsFilesToCompile.length} elements: ${project.cdsFilesToCompile.join(', ')}`, + ); + } + + if (usesProjectLevelCompilation) { + // For project-level compilation, expect a single model.cds.json file in the project + // root directory. + const projectModelFile = join(project.projectDir, 'model.cds.json'); + expectedFiles.push(projectModelFile); + } else { + // For individual file compilation, expect a .cds.json file for each .cds file compiled. + for (const cdsFile of project.cdsFilesToCompile) { + expectedFiles.push(`${cdsFile}.json`); + } + } + + return expectedFiles; +} + +/** + * Checks if a project has a typical CAP directory structure by looking at the file paths. + * This is used as a heuristic to determine if project-level compilation should be used. + * + * @param cdsFiles - List of CDS files in the project (relative to source root) + * @returns true if the project appears to have a CAP structure + */ +function hasTypicalCapDirectoryStructure(cdsFiles: string[]): boolean { + // Check if there are files in common CAP directories + const hasDbFiles = cdsFiles.some(file => file.includes('db/') || file.includes('database/')); + const hasSrvFiles = cdsFiles.some(file => file.includes('srv/') || file.includes('service/')); + + // If we have both db and srv files, this looks like a CAP project + if (hasDbFiles && hasSrvFiles) { + return true; + } + + // Check if files are spread across multiple meaningful directories (not just the root) + const meaningfulDirectories = new Set( + cdsFiles.map(file => dirname(file)).filter(dir => dir !== '.' && dir !== ''), // Exclude root directory + ); + + // If there are multiple meaningful directories with CDS files, this might be a structured project + // But we need to be more selective - only consider it structured if there are actual subdirectories + return meaningfulDirectories.size >= 2; +} + +/** + * Checks if a directory has a package.json with CAP dependencies. + * This function is used to determine if a directory has the necessary CAP packages installed, + * which is one indicator that it might be a CAP project. + * + * @param dir - Directory to check for package.json with CAP dependencies + * @returns true if the directory has a package.json with CAP dependencies + */ +export function hasPackageJsonWithCapDeps(dir: string): boolean { + try { + const packageJsonPath = join(dir, 'package.json'); + const packageJson = readPackageJsonFile(packageJsonPath); + + if (packageJson) { + const dependencies = { + ...(packageJson.dependencies ?? {}), + ...(packageJson.devDependencies ?? {}), + }; + + // Check for common CAP dependencies + return !!(dependencies['@sap/cds'] || dependencies['@sap/cds-dk']); + } + + return false; + } catch { + return false; + } +} diff --git a/extractors/cds/tools/src/cds/parser/graph.ts b/extractors/cds/tools/src/cds/parser/graph.ts new file mode 100644 index 000000000..9a367bca9 --- /dev/null +++ b/extractors/cds/tools/src/cds/parser/graph.ts @@ -0,0 +1,333 @@ +import { dirname, join, resolve, sep } from 'path'; + +import { + determineCdsFilesForProjectDir, + determineCdsFilesToCompile, + determineCdsProjectsUnderSourceDir, + extractCdsImports, + readPackageJsonFile, +} from './functions'; +import { CdsDependencyGraph, CdsImport, CdsProject, BasicCdsProject } from './types'; +import { cdsExtractorLog } from '../../logging'; + +/** + * Builds a basic dependency graph of CDS projects and performs the initial parsing stage of the CDS extractor. + * This is the internal function that creates basic project structures. + * + * @param sourceRootDir - Source root directory + * @returns Map of project directories to their BasicCdsProject objects with dependency information + */ +function buildBasicCdsProjectDependencyGraph(sourceRootDir: string): Map { + // Find all CDS projects under the source directory + cdsExtractorLog('info', 'Detecting CDS projects...'); + const projectDirs = determineCdsProjectsUnderSourceDir(sourceRootDir); + + if (projectDirs.length === 0) { + cdsExtractorLog('info', 'No CDS projects found.'); + return new Map(); + } + + cdsExtractorLog('info', `Found ${projectDirs.length} CDS project(s) under source directory.`); + + const projectMap = new Map(); + + // First pass: create CdsProject objects for each project directory + for (const projectDir of projectDirs) { + const absoluteProjectDir = join(sourceRootDir, projectDir); + const cdsFiles = determineCdsFilesForProjectDir(sourceRootDir, absoluteProjectDir); + + // Try to load package.json if it exists + const packageJsonPath = join(absoluteProjectDir, 'package.json'); + const packageJson = readPackageJsonFile(packageJsonPath); + + projectMap.set(projectDir, { + projectDir, + cdsFiles, + cdsFilesToCompile: [], // Will be populated in the third pass + expectedOutputFiles: [], // Will be populated in the fourth pass + packageJson, + dependencies: [], + imports: new Map(), + }); + } + + // Second pass: analyze dependencies between projects + cdsExtractorLog('info', 'Analyzing dependencies between CDS projects...'); + for (const [projectDir, project] of projectMap.entries()) { + // Check each CDS file for imports + for (const relativeFilePath of project.cdsFiles) { + const absoluteFilePath = join(sourceRootDir, relativeFilePath); + + try { + const imports = extractCdsImports(absoluteFilePath); + const enrichedImports: CdsImport[] = []; + + // Process each import + for (const importInfo of imports) { + const enrichedImport: CdsImport = { ...importInfo }; + + if (importInfo.isRelative) { + // Resolve the relative import path + const importedFilePath = resolve(dirname(absoluteFilePath), importInfo.path); + const normalizedImportedPath = importedFilePath.endsWith('.cds') + ? importedFilePath + : `${importedFilePath}.cds`; + + // Store the resolved path relative to source root + try { + const relativeToDirPath = dirname(relativeFilePath); + const resolvedPath = resolve(join(sourceRootDir, relativeToDirPath), importInfo.path); + const normalizedResolvedPath = resolvedPath.endsWith('.cds') + ? resolvedPath + : `${resolvedPath}.cds`; + + // Convert to relative path from source root + if (normalizedResolvedPath.startsWith(sourceRootDir)) { + enrichedImport.resolvedPath = normalizedResolvedPath + .substring(sourceRootDir.length) + .replace(/^[/\\]/, ''); + } + } catch (error) { + cdsExtractorLog( + 'warn', + `Could not resolve import path for ${importInfo.path} in ${relativeFilePath}: ${String(error)}`, + ); + } + + // Find which project contains this imported file + for (const [otherProjectDir, otherProject] of projectMap.entries()) { + if (otherProjectDir === projectDir) continue; // Skip self + + const otherProjectAbsoluteDir = join(sourceRootDir, otherProjectDir); + + // Check if the imported file is in the other project + const isInOtherProject = otherProject.cdsFiles.some(otherFile => { + const otherAbsolutePath = join(sourceRootDir, otherFile); + return ( + otherAbsolutePath === normalizedImportedPath || + normalizedImportedPath.startsWith(otherProjectAbsoluteDir + sep) + ); + }); + + if (isInOtherProject) { + // Add dependency if not already present + project.dependencies ??= []; + + if (!project.dependencies.includes(otherProject)) { + project.dependencies.push(otherProject); + } + } + } + } + // For module imports, check package.json dependencies + else if (importInfo.isModule && project.packageJson) { + const dependencies = { + ...(project.packageJson.dependencies ?? {}), + ...(project.packageJson.devDependencies ?? {}), + }; + + // Extract module name from import path (e.g., '@sap/cds/common' -> '@sap/cds') + const moduleName = importInfo.path.split('/')[0].startsWith('@') + ? importInfo.path.split('/').slice(0, 2).join('/') + : importInfo.path.split('/')[0]; + + if (dependencies[moduleName]) { + // This is a valid module dependency, nothing more to do here + // In the future, we could track module dependencies separately + } + } + + enrichedImports.push(enrichedImport); + } + + // Store the enriched imports in the project + project.imports?.set(relativeFilePath, enrichedImports); + } catch (error: unknown) { + cdsExtractorLog( + 'warn', + `Error processing imports in ${absoluteFilePath}: ${String(error)}`, + ); + } + } + } + + // Third pass: determine CDS files to compile and expected output files for each project + cdsExtractorLog( + 'info', + 'Determining CDS files to compile and expected output files for each project...', + ); + for (const [, project] of projectMap.entries()) { + try { + const projectPlan = determineCdsFilesToCompile(sourceRootDir, project); + + // Assign the calculated values back to the project + project.cdsFilesToCompile = projectPlan.filesToCompile; + project.expectedOutputFiles = projectPlan.expectedOutputFiles; + + // Check if using project-level compilation + const usesProjectLevelCompilation = projectPlan.filesToCompile.includes( + '__PROJECT_LEVEL_COMPILATION__', + ); + + if (usesProjectLevelCompilation) { + cdsExtractorLog( + 'info', + `Project ${project.projectDir}: using project-level compilation for all ${project.cdsFiles.length} CDS files, expecting ${projectPlan.expectedOutputFiles.length} output files`, + ); + } else { + cdsExtractorLog( + 'info', + `Project ${project.projectDir}: ${projectPlan.filesToCompile.length} files to compile out of ${project.cdsFiles.length} total CDS files, expecting ${projectPlan.expectedOutputFiles.length} output files`, + ); + } + } catch (error) { + cdsExtractorLog( + 'warn', + `Error determining files to compile for project ${project.projectDir}: ${String(error)}`, + ); + // Fall back to compiling all files on error + project.cdsFilesToCompile = [...project.cdsFiles]; + project.expectedOutputFiles = []; + } + } + + return projectMap; +} + +/** + * Builds a CDS dependency graph with comprehensive tracking and debug information. + * This is the main function that returns a CdsDependencyGraph instead of a simple Map. + * The extractor now runs in autobuild mode by default. + * + * @param sourceRootDir - Source root directory + * @returns CDS dependency graph with comprehensive tracking + */ +export function buildCdsProjectDependencyGraph(sourceRootDir: string): CdsDependencyGraph { + const startTime = new Date(); + + // Create the initial dependency graph structure + const dependencyGraph: CdsDependencyGraph = { + id: `cds_graph_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + sourceRootDir, + projects: new Map(), + debugInfo: { + extractor: { + runMode: 'autobuild', + sourceRootDir, + startTime, + environment: { + nodeVersion: process.version, + platform: process.platform, + cwd: process.cwd(), + argv: process.argv, + }, + }, + parser: { + projectsDetected: 0, + cdsFilesFound: 0, + dependencyResolutionSuccess: true, + parsingErrors: [], + parsingWarnings: [], + }, + compiler: { + availableCommands: [], + selectedCommand: '', + cacheDirectories: [], + cacheInitialized: false, + }, + }, + currentPhase: 'parsing', + statusSummary: { + overallSuccess: false, + totalProjects: 0, + totalCdsFiles: 0, + totalCompilationTasks: 0, + successfulCompilations: 0, + failedCompilations: 0, + skippedCompilations: 0, + jsonFilesGenerated: 0, + criticalErrors: [], + warnings: [], + performance: { + totalDurationMs: 0, + parsingDurationMs: 0, + compilationDurationMs: 0, + extractionDurationMs: 0, + }, + }, + config: { + maxRetryAttempts: 3, + enableDetailedLogging: false, // Debug modes removed + generateDebugOutput: false, // Debug modes removed + compilationTimeoutMs: 30000, // 30 seconds + }, + errors: { + critical: [], + warnings: [], + }, + }; + + try { + // Use the existing function to build the basic project map + const basicProjectMap = buildBasicCdsProjectDependencyGraph(sourceRootDir); + + // Convert basic projects to CDS projects + for (const [projectDir, basicProject] of basicProjectMap.entries()) { + const cdsProject: CdsProject = { + ...basicProject, + id: `project_${projectDir.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}`, + enhancedCompilationConfig: undefined, // Will be set during compilation planning + compilationTasks: [], + parserDebugInfo: { + dependenciesResolved: [], + importErrors: [], + parseErrors: new Map(), + }, + status: 'discovered', + timestamps: { + discovered: new Date(), + }, + }; + + dependencyGraph.projects.set(projectDir, cdsProject); + } + + // Update summary statistics + dependencyGraph.statusSummary.totalProjects = dependencyGraph.projects.size; + dependencyGraph.statusSummary.totalCdsFiles = Array.from( + dependencyGraph.projects.values(), + ).reduce((sum, project) => sum + project.cdsFiles.length, 0); + + dependencyGraph.debugInfo.parser.projectsDetected = dependencyGraph.projects.size; + dependencyGraph.debugInfo.parser.cdsFilesFound = dependencyGraph.statusSummary.totalCdsFiles; + + // Mark dependency resolution phase as completed + dependencyGraph.currentPhase = 'dependency_resolution'; + + const endTime = new Date(); + dependencyGraph.debugInfo.extractor.endTime = endTime; + dependencyGraph.debugInfo.extractor.durationMs = endTime.getTime() - startTime.getTime(); + dependencyGraph.statusSummary.performance.parsingDurationMs = + dependencyGraph.debugInfo.extractor.durationMs; + + cdsExtractorLog( + 'info', + `CDS dependency graph created with ${dependencyGraph.projects.size} projects and ${dependencyGraph.statusSummary.totalCdsFiles} CDS files`, + ); + + return dependencyGraph; + } catch (error) { + const errorMessage = `Failed to build CDS dependency graph: ${String(error)}`; + cdsExtractorLog('error', errorMessage); + + dependencyGraph.errors.critical.push({ + phase: 'parsing', + message: errorMessage, + timestamp: new Date(), + stack: error instanceof Error ? error.stack : undefined, + }); + + dependencyGraph.currentPhase = 'failed'; + return dependencyGraph; + } +} diff --git a/extractors/cds/tools/src/cds/parser/index.ts b/extractors/cds/tools/src/cds/parser/index.ts new file mode 100644 index 000000000..c14f31c8a --- /dev/null +++ b/extractors/cds/tools/src/cds/parser/index.ts @@ -0,0 +1,3 @@ +export { buildCdsProjectDependencyGraph } from './graph'; +export * from './functions'; +export * from './types'; diff --git a/extractors/cds/tools/src/cds/parser/types.ts b/extractors/cds/tools/src/cds/parser/types.ts new file mode 100644 index 000000000..cb8902b76 --- /dev/null +++ b/extractors/cds/tools/src/cds/parser/types.ts @@ -0,0 +1,297 @@ +/** Types for CDS parsing. */ + +/** Result of determining CDS files to compile and their expected outputs */ +export interface CdsFilesToCompile { + /** CDS files that should be compiled (or special markers like __PROJECT_LEVEL_COMPILATION__) */ + filesToCompile: string[]; + + /** Expected JSON output files that will be generated (relative to source root) */ + expectedOutputFiles: string[]; +} + +/** Represents an import reference in a CDS file. */ +export interface CdsImport { + /** Whether the import is from a module (node_modules). */ + isModule: boolean; + + /** Whether the import is relative. */ + isRelative: boolean; + + /** Path to the imported resource. */ + path: string; + + /** Resolved absolute path of the imported resource (when applicable). */ + resolvedPath?: string; + + /** Original import statement. */ + statement: string; +} + +/** Represents a simplified package.json file structure with only the fields we need */ +export interface PackageJson { + /** The name of the package */ + name?: string; + + /** The version of the package */ + version?: string; + + /** Production dependencies */ + dependencies?: Record; + + /** Development dependencies */ + devDependencies?: Record; + + /** All other fields in package.json */ + [key: string]: unknown; +} + +/** Compilation configuration for a CDS project */ +export interface CdsCompilationConfig { + /** The CDS command that will be used for compilation */ + cdsCommand: string; + + /** The cache directory to use for dependencies, if any */ + cacheDir?: string; + + /** Whether to use project-level compilation */ + useProjectLevelCompilation: boolean; + + /** Version compatibility status */ + versionCompatibility: { + /** Whether the CDS versions are compatible */ + isCompatible: boolean; + /** Error message if incompatible */ + errorMessage?: string; + /** Detected CDS version */ + cdsVersion?: string; + /** Project's expected CDS version */ + expectedCdsVersion?: string; + }; +} + +/** + * Debug information for tracking extractor execution + */ +export interface ExtractorDebugInfo { + /** Run mode of the extractor */ + runMode: string; + /** Source root directory */ + sourceRootDir: string; + /** Timestamp when extraction started */ + startTime: Date; + /** Timestamp when extraction completed */ + endTime?: Date; + /** Total duration in milliseconds */ + durationMs?: number; + /** Environment information */ + environment: { + nodeVersion: string; + platform: string; + cwd: string; + argv: string[]; + }; +} + +/** + * Parser debug information + */ +export interface ParserDebugInfo { + /** Number of projects detected */ + projectsDetected: number; + /** Number of CDS files found */ + cdsFilesFound: number; + /** Dependency resolution success */ + dependencyResolutionSuccess: boolean; + /** Errors encountered during parsing */ + parsingErrors: string[]; + /** Warnings during parsing */ + parsingWarnings: string[]; + /** Debug output file path if generated */ + debugOutputPath?: string; +} + +/** + * Compiler debug information + */ +export interface CompilerDebugInfo { + /** Available CDS commands discovered */ + availableCommands: Array<{ + command: string; + version?: string; + strategy: string; + tested: boolean; + error?: string; + }>; + /** Selected primary command */ + selectedCommand: string; + /** Cache directories discovered */ + cacheDirectories: string[]; + /** Command cache initialization success */ + cacheInitialized: boolean; +} + +/** + * Status summary for the entire extraction process + */ +export interface ExtractionStatusSummary { + /** Overall success status */ + overallSuccess: boolean; + /** Total projects processed */ + totalProjects: number; + /** Total CDS files processed */ + totalCdsFiles: number; + /** Total compilation tasks */ + totalCompilationTasks: number; + /** Successful compilation tasks */ + successfulCompilations: number; + /** Failed compilation tasks */ + failedCompilations: number; + /** Skipped compilation tasks */ + skippedCompilations: number; + /** JSON files generated */ + jsonFilesGenerated: number; + /** Critical errors that stopped extraction */ + criticalErrors: string[]; + /** Non-critical warnings */ + warnings: string[]; + /** Performance metrics */ + performance: { + totalDurationMs: number; + parsingDurationMs: number; + compilationDurationMs: number; + extractionDurationMs: number; + }; +} + +/** Represents a basic CDS project with its directory and associated files. */ +export interface BasicCdsProject { + /** All CDS files within this project. */ + cdsFiles: string[]; + + /** CDS files that should be compiled to JSON (typically root files not imported by others). */ + cdsFilesToCompile: string[]; + + /** Expected JSON output files that will be generated (relative to source root). */ + expectedOutputFiles: string[]; + + /** Dependencies on other CDS projects. */ + dependencies?: BasicCdsProject[]; + + /** Map of file paths (relative to source root) to their import information. */ + imports?: Map; + + /** The package.json content if available. */ + packageJson?: PackageJson; + + /** The directory path of the project. */ + projectDir: string; + + /** Compilation configuration determined during project detection */ + compilationConfig?: CdsCompilationConfig; +} + +/** + * CDS project with comprehensive tracking and debug information + */ +export interface CdsProject extends BasicCdsProject { + /** Unique identifier for this project */ + id: string; + + /** Compilation configuration */ + enhancedCompilationConfig?: import('../compiler/types.js').CompilationConfig; + + /** Compilation tasks for this project */ + compilationTasks: import('../compiler/types.js').CompilationTask[]; + + /** Parser debug information for this project */ + parserDebugInfo?: { + /** Dependencies successfully resolved */ + dependenciesResolved: string[]; + /** Import resolution errors */ + importErrors: string[]; + /** Files that couldn't be parsed */ + parseErrors: Map; + }; + + /** Current status of the project */ + status: + | 'discovered' + | 'dependencies_resolved' + | 'compilation_planned' + | 'compiling' + | 'completed' + | 'failed'; + + /** Timestamps for tracking project processing */ + timestamps: { + discovered: Date; + dependenciesResolved?: Date; + compilationStarted?: Date; + compilationCompleted?: Date; + }; +} + +/** + * Comprehensive CDS dependency graph that supports all extractor phases + */ +export interface CdsDependencyGraph { + /** Unique identifier for this dependency graph */ + id: string; + + /** Source root directory */ + sourceRootDir: string; + + /** CDS projects with comprehensive tracking */ + projects: Map; + + /** Debug information for the entire extraction process */ + debugInfo: { + extractor: ExtractorDebugInfo; + parser: ParserDebugInfo; + compiler: CompilerDebugInfo; + }; + + /** Current phase of processing */ + currentPhase: + | 'initializing' + | 'parsing' + | 'dependency_resolution' + | 'compilation_planning' + | 'compiling' + | 'extracting' + | 'completed' + | 'failed'; + + /** Status summary updated as processing progresses */ + statusSummary: ExtractionStatusSummary; + + /** Configuration and settings */ + config: { + /** Maximum retry attempts for task re-execution */ + maxRetryAttempts: number; + /** Whether to enable detailed logging */ + enableDetailedLogging: boolean; + /** Whether to generate debug output files */ + generateDebugOutput: boolean; + /** Timeout for individual compilation tasks (ms) */ + compilationTimeoutMs: number; + }; + + /** Error tracking and reporting */ + errors: { + /** Critical errors that stop processing */ + critical: Array<{ + phase: string; + message: string; + timestamp: Date; + stack?: string; + }>; + /** Non-critical warnings */ + warnings: Array<{ + phase: string; + message: string; + timestamp: Date; + context?: string; + }>; + }; +} diff --git a/extractors/cds/tools/src/cdsCompiler.ts b/extractors/cds/tools/src/cdsCompiler.ts deleted file mode 100644 index 2177ddfb9..000000000 --- a/extractors/cds/tools/src/cdsCompiler.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { execFileSync, spawnSync, SpawnSyncReturns } from 'child_process'; -import { resolve } from 'path'; - -import { fileExists, dirExists, recursivelyRenameJsonFiles } from './filesystem'; - -/** - * Result of a CDS compilation - */ -export interface CdsCompilationResult { - success: boolean; - message?: string; - outputPath?: string; -} - -/** - * Compile a CDS file to JSON - * @param cdsFilePath Path to the CDS file - * @param sourceRoot The source root directory - * @param cdsCommand The CDS command to use - * @returns Result of the compilation - */ -export function compileCdsToJson( - cdsFilePath: string, - sourceRoot: string, - cdsCommand: string, -): CdsCompilationResult { - try { - const resolvedCdsFilePath = resolve(cdsFilePath); - if (!fileExists(resolvedCdsFilePath)) { - throw new Error(`Expected CDS file '${resolvedCdsFilePath}' does not exist.`); - } - - const cdsJsonOutPath = `${resolvedCdsFilePath}.json`; - console.log(`Processing CDS file ${resolvedCdsFilePath} to ${cdsJsonOutPath} ...`); - - const result: SpawnSyncReturns = spawnSync( - cdsCommand, - [ - 'compile', - resolvedCdsFilePath, - '--to', - 'json', - '--dest', - cdsJsonOutPath, - '--locations', - '--log-level', - 'warn', - ], - { cwd: sourceRoot, shell: true, stdio: 'pipe' }, - ); - - if (result.error) { - throw new Error(`Error executing CDS compiler: ${result.error.message}`); - } - - if (result.status !== 0) { - throw new Error( - `Could not compile the file ${resolvedCdsFilePath}.\nReported error(s):\n\`\`\`\n${ - result.stderr?.toString() || 'Unknown error' - }\n\`\`\``, - ); - } - - if (!fileExists(cdsJsonOutPath) && !dirExists(cdsJsonOutPath)) { - throw new Error( - `CDS source file '${resolvedCdsFilePath}' was not compiled to JSON. This is likely because the file does not exist or is not a valid CDS file.`, - ); - } - - // Handle directory output if the CDS compiler generated a directory - if (dirExists(cdsJsonOutPath)) { - console.log(`CDS compiler generated JSON to output directory: ${cdsJsonOutPath}`); - // Recursively rename all .json files to have a .cds.json extension - recursivelyRenameJsonFiles(cdsJsonOutPath); - } else { - console.log(`CDS compiler generated JSON to file: ${cdsJsonOutPath}`); - } - - return { success: true, outputPath: cdsJsonOutPath }; - } catch (error) { - return { success: false, message: String(error) }; - } -} -/** - * Determine the `cds` command to use based on the environment. - * @returns A string representing the CLI command to run to invoke the - * CDS compiler. - */ -export function determineCdsCommand(): string { - let cdsCommand = 'cds'; - // TODO : create a mapping of project sub-directories to the correct - // cds command to use, which will also determine the version of the cds - // compiler that will be used for compiling `.cds` files to `.cds.json` - // files for that sub-directory / project. - try { - execFileSync('cds', ['--version'], { stdio: 'ignore' }); - } catch (error) { - // Check if the error is specifically about the command not being found - const errorMsg = String(error); - if (errorMsg.includes('command not found')) { - // If 'cds' command is not available, use npx to run it - console.log('CDS command not found, falling back to npx...'); - } else if (errorMsg.includes('ENOENT') || errorMsg.includes('not recognized')) { - // If the error is related to the command not being recognized, use npx - console.log('CDS command not recognized, falling back to npx...'); - } else { - // For other errors, log them but still fall back to npx - console.warn( - `WARN: determining CDS command failed with error: ${errorMsg}. Falling back to npx...`, - ); - } - cdsCommand = 'npx -y --package @sap/cds-dk cds'; - } - return cdsCommand; -} diff --git a/extractors/cds/tools/src/codeql.ts b/extractors/cds/tools/src/codeql.ts index 48559856b..3cc8786e0 100644 --- a/extractors/cds/tools/src/codeql.ts +++ b/extractors/cds/tools/src/codeql.ts @@ -1,8 +1,7 @@ import { spawnSync, SpawnSyncReturns } from 'child_process'; -import { existsSync } from 'fs'; import { addJavaScriptExtractorDiagnostic } from './diagnostics'; -import { getPlatformInfo } from './environment'; +import { cdsExtractorLog } from './logging'; /** * Run the JavaScript extractor autobuild script @@ -16,7 +15,8 @@ export function runJavaScriptExtractor( autobuildScriptPath: string, codeqlExePath?: string, ): { success: boolean; error?: string } { - console.log( + cdsExtractorLog( + 'info', `Extracting the .cds.json files by running the 'javascript' extractor autobuild script: ${autobuildScriptPath}`, ); @@ -41,7 +41,7 @@ export function runJavaScriptExtractor( }); if (result.error) { - const errorMessage = `Error executing JavaScript extractor: ${result.error.message}`; + const errorMessage = `Error running JavaScript extractor: ${result.error.message}`; if (codeqlExePath) { addJavaScriptExtractorDiagnostic(sourceRoot, errorMessage, codeqlExePath); } @@ -52,7 +52,7 @@ export function runJavaScriptExtractor( } if (result.status !== 0) { - const errorMessage = `JavaScript extractor failed with exit code: ${String(result.status)}`; + const errorMessage = `JavaScript extractor failed with exit code ${String(result.status)}`; if (codeqlExePath) { addJavaScriptExtractorDiagnostic(sourceRoot, errorMessage, codeqlExePath); } @@ -64,62 +64,3 @@ export function runJavaScriptExtractor( return { success: true }; } - -/** - * Validate the required environment variables and paths - * @param sourceRoot The source root directory - * @param codeqlExePath Path to the CodeQL executable - * @param responseFile Path to the response file - * @param autobuildScriptPath Path to the autobuild script - * @param jsExtractorRoot JavaScript extractor root path - * @returns true if all validations pass, false otherwise - */ -export function validateRequirements( - sourceRoot: string, - codeqlExePath: string, - responseFile: string, - autobuildScriptPath: string, - jsExtractorRoot: string, -): boolean { - const errorMessages: string[] = []; - const { platform: osPlatform } = getPlatformInfo(); - const codeqlExe = osPlatform === 'win32' ? 'codeql.exe' : 'codeql'; - - // Check if the JavaScript extractor autobuild script exists - if (!existsSync(autobuildScriptPath)) { - errorMessages.push(`autobuild script '${autobuildScriptPath}' does not exist`); - } - - // Check if the CodeQL executable exists - if (!existsSync(codeqlExePath)) { - errorMessages.push(`codeql executable '${codeqlExePath}' does not exist`); - } - - // Check if the response file exists - if (!existsSync(responseFile)) { - errorMessages.push( - `response file '${responseFile}' does not exist. This is because no CDS files were selected or found`, - ); - } - - // Check if the JavaScript extractor root is set - if (!jsExtractorRoot) { - errorMessages.push(`CODEQL_EXTRACTOR_JAVASCRIPT_ROOT environment variable is not set`); - } - - // Check if the source root exists - if (!existsSync(sourceRoot)) { - errorMessages.push(`project root directory '${sourceRoot}' does not exist`); - } - - if (errorMessages.length > 0) { - console.warn( - `'${codeqlExe} database index-files --language cds' terminated early due to: ${errorMessages.join( - ', ', - )}.`, - ); - return false; - } - - return true; -} diff --git a/extractors/cds/tools/src/diagnostics.ts b/extractors/cds/tools/src/diagnostics.ts index ac229550f..e3fb5435e 100644 --- a/extractors/cds/tools/src/diagnostics.ts +++ b/extractors/cds/tools/src/diagnostics.ts @@ -3,6 +3,8 @@ import { resolve } from 'path'; import { quote } from 'shell-quote'; +import { cdsExtractorLog } from './logging'; + /** * Severity levels for diagnostics */ @@ -24,7 +26,7 @@ export enum DiagnosticSeverity { * @param logPrefix Prefix for the log message * @returns True if the diagnostic was added, false otherwise */ -export function addDiagnostic( +function addDiagnostic( filePath: string, message: string, codeqlExePath: string, @@ -50,11 +52,12 @@ export function addDiagnostic( '--', `${process.env.CODEQL_EXTRACTOR_CDS_WIP_DATABASE ?? ''}`, ]); - console.log(`Added ${severity} diagnostic for ${logPrefix}: ${filePath}`); + cdsExtractorLog('info', `Added ${severity} diagnostic for ${logPrefix}: ${filePath}`); return true; } catch (err) { - console.error( - `ERROR: Failed to add ${severity} diagnostic for ${logPrefix}=${filePath} : ${String(err)}`, + cdsExtractorLog( + 'error', + `Failed to add ${severity} diagnostic for ${logPrefix}=${filePath} : ${String(err)}`, ); return false; } @@ -83,52 +86,6 @@ export function addCompilationDiagnostic( ); } -/** - * Add a diagnostic error to the CodeQL database for a dependency installation failure - * @param packageJsonPath Path to the package.json file that has installation issues - * @param errorMessage The error message from the installation - * @param codeqlExePath Path to the CodeQL executable - * @returns True if the diagnostic was added, false otherwise - */ -export function addDependencyDiagnostic( - packageJsonPath: string, - errorMessage: string, - codeqlExePath: string, -): boolean { - return addDiagnostic( - packageJsonPath, - errorMessage, - codeqlExePath, - 'cds/dependency-failure', - 'Failure to install SAP CAP CDS dependencies', - DiagnosticSeverity.Error, - 'package.json file', - ); -} - -/** - * Add a diagnostic warning to the CodeQL database for a package.json parsing failure - * @param packageJsonPath Path to the package.json file that couldn't be parsed - * @param errorMessage The error message from the parsing attempt - * @param codeqlExePath Path to the CodeQL executable - * @returns True if the diagnostic was added, false otherwise - */ -export function addPackageJsonParsingDiagnostic( - packageJsonPath: string, - errorMessage: string, - codeqlExePath: string, -): boolean { - return addDiagnostic( - packageJsonPath, - errorMessage, - codeqlExePath, - 'cds/package-json-parsing-failure', - 'Failure to parse package.json file for SAP CAP CDS project', - DiagnosticSeverity.Warning, - 'package.json file', - ); -} - /** * Add a diagnostic error to the CodeQL database for a JavaScript extractor failure * @param filePath Path to a relevant file for the error context diff --git a/extractors/cds/tools/src/environment.ts b/extractors/cds/tools/src/environment.ts index dec5366ab..b19bfabd1 100644 --- a/extractors/cds/tools/src/environment.ts +++ b/extractors/cds/tools/src/environment.ts @@ -1,8 +1,10 @@ import { execFileSync } from 'child_process'; +import { existsSync } from 'fs'; import { arch, platform } from 'os'; import { join, resolve } from 'path'; import { dirExists } from './filesystem'; +import { cdsExtractorLog } from './logging'; /** * Interface for platform information @@ -45,41 +47,141 @@ export function getPlatformInfo(): PlatformInfo { } /** - * Get the path to the CodeQL executable - * @returns The resolved path to the CodeQL executable + * Get the path to the CodeQL executable. + * Prioritizes CODEQL_DIST if set and valid. Otherwise, tries to find CodeQL via system PATH. + * @returns The resolved path to the CodeQL executable, or an empty string if not found. */ export function getCodeQLExePath(): string { const platformInfo = getPlatformInfo(); - const codeqlExe: string = platformInfo.isWindows ? 'codeql.exe' : 'codeql'; + const codeqlExeName: string = platformInfo.isWindows ? 'codeql.exe' : 'codeql'; - // Safely get CODEQL_DIST environment variable - const codeqlDist = process.env.CODEQL_DIST ?? ''; - return resolve(join(codeqlDist, codeqlExe)); + // First, check if CODEQL_DIST is set and valid + const codeqlDist = process.env.CODEQL_DIST; + if (codeqlDist) { + const codeqlPathFromDist = resolve(join(codeqlDist, codeqlExeName)); + if (existsSync(codeqlPathFromDist)) { + cdsExtractorLog('info', `Using CodeQL executable from CODEQL_DIST: ${codeqlPathFromDist}`); + return codeqlPathFromDist; + } else { + cdsExtractorLog( + 'error', + `CODEQL_DIST is set to '${codeqlDist}', but CodeQL executable was not found at '${codeqlPathFromDist}'. Please ensure this path is correct. Falling back to PATH-based discovery.`, + ); + // Fall through to PATH-based discovery + } + } + + // CODEQL_DIST is not set or was invalid, attempt to find CodeQL via system PATH using 'codeql version --format=json' + cdsExtractorLog( + 'info', + 'CODEQL_DIST environment variable not set or invalid. Attempting to find CodeQL executable via system PATH using "codeql version --format=json".', + ); + try { + const versionOutput = execFileSync(codeqlExeName, ['version', '--format=json'], { + encoding: 'utf8', + timeout: 5000, // 5 seconds timeout + stdio: 'pipe', // Suppress output to console + }); + + interface CodeQLVersionInfo { + unpackedLocation?: string; + cliVersion?: string; // For potential future use or richer logging + } + + try { + const versionInfo = JSON.parse(versionOutput) as CodeQLVersionInfo; + + if ( + versionInfo && + typeof versionInfo.unpackedLocation === 'string' && + versionInfo.unpackedLocation + ) { + const resolvedPathFromVersion = resolve(join(versionInfo.unpackedLocation, codeqlExeName)); + if (existsSync(resolvedPathFromVersion)) { + cdsExtractorLog( + 'info', + `CodeQL executable found via 'codeql version --format=json' at: ${resolvedPathFromVersion}`, + ); + return resolvedPathFromVersion; + } + cdsExtractorLog( + 'warn', + `'codeql version --format=json' provided unpackedLocation '${versionInfo.unpackedLocation}', but executable not found at '${resolvedPathFromVersion}'.`, + ); + } else { + cdsExtractorLog( + 'warn', + "Could not determine CodeQL executable path from 'codeql version --format=json' output. 'unpackedLocation' field missing, empty, or invalid.", + ); + } + } catch (parseError) { + cdsExtractorLog( + 'warn', + `Failed to parse 'codeql version --format=json' output: ${String(parseError)}. Output was: ${versionOutput}`, + ); + } + } catch (error) { + let errorMessage = `INFO: Failed to find CodeQL executable via 'codeql version --format=json'. Error: ${String(error)}`; + if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { + errorMessage += `\nINFO: The command '${codeqlExeName}' was not found in your system PATH.`; + } + cdsExtractorLog('info', errorMessage); + } + + cdsExtractorLog( + 'error', + 'Failed to determine CodeQL executable path. Please ensure the CODEQL_DIST environment variable is set and points to a valid CodeQL distribution, or that the CodeQL CLI (codeql) is available in your system PATH and "codeql version --format=json" can provide its location.', + ); + return ''; // Return empty string if all attempts fail } /** - * Get the JavaScript extractor root path - * @param codeqlExePath The path to the CodeQL executable - * @returns The JavaScript extractor root path + * Get the JavaScript extractor root path. + * @param codeqlExePath The path to the CodeQL executable. If empty, resolution will be skipped. + * @returns The JavaScript extractor root path, or an empty string if not found or if codeqlExePath is empty. */ export function getJavaScriptExtractorRoot(codeqlExePath: string): string { let jsExtractorRoot = process.env.CODEQL_EXTRACTOR_JAVASCRIPT_ROOT ?? ''; - if (!jsExtractorRoot) { - try { - jsExtractorRoot = execFileSync(codeqlExePath, [ - 'resolve', - 'extractor', - '--language=javascript', - ]) - .toString() - .trim(); - } catch (error) { - console.error(`Error resolving JavaScript extractor root: ${String(error)}`); - return ''; - } + if (jsExtractorRoot) { + cdsExtractorLog( + 'info', + `Using JavaScript extractor root from environment variable CODEQL_EXTRACTOR_JAVASCRIPT_ROOT: ${jsExtractorRoot}`, + ); + return jsExtractorRoot; } + if (!codeqlExePath) { + cdsExtractorLog( + 'warn', + 'Cannot resolve JavaScript extractor root because the CodeQL executable path was not provided or found.', + ); + return ''; + } + + try { + jsExtractorRoot = execFileSync( + codeqlExePath, + ['resolve', 'extractor', '--language=javascript'], + { stdio: 'pipe' }, // Suppress output from the command itself + ) + .toString() + .trim(); + if (jsExtractorRoot) { + cdsExtractorLog('info', `JavaScript extractor root resolved to: ${jsExtractorRoot}`); + } else { + cdsExtractorLog( + 'warn', + `'codeql resolve extractor --language=javascript' using '${codeqlExePath}' returned an empty path.`, + ); + } + } catch (error) { + cdsExtractorLog( + 'error', + `Error resolving JavaScript extractor root using '${codeqlExePath}': ${String(error)}`, + ); + jsExtractorRoot = ''; // Ensure it's empty on error + } return jsExtractorRoot; } @@ -102,9 +204,10 @@ export function setupJavaScriptExtractorEnv(): void { /** * Get the path to the autobuild script * @param jsExtractorRoot The JavaScript extractor root path - * @returns The path to the autobuild script + * @returns The path to the autobuild script, or an empty string if jsExtractorRoot is empty. */ export function getAutobuildScriptPath(jsExtractorRoot: string): string { + if (!jsExtractorRoot) return ''; const platformInfo = getPlatformInfo(); const autobuildScriptName: string = platformInfo.isWindows ? 'autobuild.cmd' : 'autobuild.sh'; return resolve(join(jsExtractorRoot, 'tools', autobuildScriptName)); @@ -117,7 +220,11 @@ export function configureLgtmIndexFilters(): void { let excludeFilters = ''; if (process.env.LGTM_INDEX_FILTERS) { - console.log(`Found $LGTM_INDEX_FILTERS already set to:\n${process.env.LGTM_INDEX_FILTERS}`); + cdsExtractorLog( + 'info', + `Found $LGTM_INDEX_FILTERS already set to: +${process.env.LGTM_INDEX_FILTERS}`, + ); const allowedExcludePatterns = [join('exclude:**', '*'), join('exclude:**', '*.*')]; excludeFilters = @@ -146,9 +253,17 @@ export function configureLgtmIndexFilters(): void { } /** - * Sets up the environment and validates key components for CDS extractor - * @param sourceRoot The source root directory - * @returns The environment setup result + * Sets up the environment and validates key components for running the CDS extractor. + * This includes checking for the CodeQL executable, validating the source root directory, + * and setting up environment variables for the JavaScript extractor. + * + * @param sourceRoot The source root directory. + * + * @returns The {@link EnvironmentSetupResult} containing success status, error messages, + * CodeQL executable path, JavaScript extractor root, autobuild script path, + * and platform information. + * + * @throws Will throw an error if the environment setup fails. */ export function setupAndValidateEnvironment(sourceRoot: string): EnvironmentSetupResult { const errorMessages: string[] = []; @@ -156,19 +271,35 @@ export function setupAndValidateEnvironment(sourceRoot: string): EnvironmentSetu // Get the CodeQL executable path const codeqlExePath = getCodeQLExePath(); + if (!codeqlExePath) { + errorMessages.push( + 'Failed to find CodeQL executable. Ensure CODEQL_DIST is set and valid, or CodeQL CLI is in PATH.', + ); + } // Validate that the required source root directory exists if (!dirExists(sourceRoot)) { - errorMessages.push(`project root directory '${sourceRoot}' does not exist`); + errorMessages.push(`Project root directory '${sourceRoot}' does not exist.`); } - // Setup JavaScript extractor environment + // Get JavaScript extractor root const jsExtractorRoot = getJavaScriptExtractorRoot(codeqlExePath); if (!jsExtractorRoot) { - errorMessages.push(`CODEQL_EXTRACTOR_JAVASCRIPT_ROOT environment variable is not set`); + if (codeqlExePath) { + // Only add this error if codeqlExePath was found but JS extractor root wasn't + errorMessages.push( + 'Failed to determine JavaScript extractor root using the found CodeQL executable.', + ); + } else { + // If codeqlExePath is empty, the error from getCodeQLExePath is usually sufficient. + // However, we can add a more specific one if needed. + errorMessages.push( + 'Cannot determine JavaScript extractor root because CodeQL executable was not found.', + ); + } } - // Set environment variables for JavaScript extractor + // Set environment variables for JavaScript extractor only if jsExtractorRoot is valid if (jsExtractorRoot) { process.env.CODEQL_EXTRACTOR_JAVASCRIPT_ROOT = jsExtractorRoot; setupJavaScriptExtractorEnv(); @@ -176,12 +307,14 @@ export function setupAndValidateEnvironment(sourceRoot: string): EnvironmentSetu // Get autobuild script path const autobuildScriptPath = jsExtractorRoot ? getAutobuildScriptPath(jsExtractorRoot) : ''; + // Not having an autobuild script path might be an error depending on the run mode, + // but for now, the function just returns what it found. return { success: errorMessages.length === 0, errorMessages, - codeqlExePath, - jsExtractorRoot, + codeqlExePath, // Will be '' if not found + jsExtractorRoot, // Will be '' if not found autobuildScriptPath, platformInfo, }; diff --git a/extractors/cds/tools/src/filesystem.ts b/extractors/cds/tools/src/filesystem.ts index 2e2ecf7c2..db5aebe4a 100644 --- a/extractors/cds/tools/src/filesystem.ts +++ b/extractors/cds/tools/src/filesystem.ts @@ -1,6 +1,8 @@ -import { existsSync, readdirSync, readFileSync, renameSync, statSync } from 'fs'; +import { existsSync, readdirSync, renameSync, statSync } from 'fs'; import { format, join, parse } from 'path'; +import { cdsExtractorLog } from './logging'; + /** * Check if a directory exists * @param dirPath Path to the directory to check @@ -19,79 +21,6 @@ export function fileExists(filePath: string): boolean { return existsSync(filePath) && statSync(filePath).isFile(); } -/** - * Read and validate a response file to get the list of CDS files to process - * @param responseFile Path to the response file - * @param platformInfo Platform information object with isWindows property - * @returns Object containing success status, CDS file paths to process, and error message if any - */ -export function getCdsFilePathsToProcess( - responseFile: string, - platformInfo: { isWindows: boolean }, -): { - success: boolean; - cdsFilePaths: string[]; - errorMessage?: string; -} { - // First validate the response file exists - const responseFileValidation = validateResponseFile(responseFile); - if (!responseFileValidation.success) { - return { - success: false, - cdsFilePaths: [], - errorMessage: `'${ - platformInfo.isWindows ? 'codeql.exe' : 'codeql' - } database index-files --language cds' terminated early as ${responseFileValidation.errorMessage}`, - }; - } - - // Now read the file paths from the response file - try { - const cdsFilePathsToProcess = readResponseFile(responseFile); - - // Check if there are any file paths to process - if (!cdsFilePathsToProcess.length) { - return { - success: false, - cdsFilePaths: [], - errorMessage: `'${ - platformInfo.isWindows ? 'codeql.exe' : 'codeql' - } database index-files --language cds' terminated early as response file '${responseFile}' is empty. This is because no CDS files were selected or found.`, - }; - } - - return { - success: true, - cdsFilePaths: cdsFilePathsToProcess, - }; - } catch (err) { - return { - success: false, - cdsFilePaths: [], - errorMessage: `'${ - platformInfo.isWindows ? 'codeql.exe' : 'codeql' - } database index-files --language cds' terminated early as response file '${responseFile}' could not be read due to an error: ${String(err)}`, - }; - } -} - -/** - * Read response file contents and split into lines - * @param responseFile Path to the response file - * @returns Array of file paths from the response file - */ -export function readResponseFile(responseFile: string): string[] { - try { - // Read the response file and split it into lines, removing empty lines - const responseFiles = readFileSync(responseFile, 'utf-8').split('\n').filter(Boolean); - return responseFiles; - } catch (err) { - throw new Error( - `Response file '${responseFile}' could not be read due to an error: ${String(err)}`, - ); - } -} - /** * Recursively renames all .json files to .cds.json in the given directory and * its subdirectories, except for those that already have .cds.json extension. @@ -101,11 +30,10 @@ export function readResponseFile(responseFile: string): string[] { export function recursivelyRenameJsonFiles(dirPath: string): void { // Make sure the directory exists if (!dirExists(dirPath)) { - console.log(`Directory not found or not a directory: ${dirPath}`); + cdsExtractorLog('info', `Directory not found: ${dirPath}`); return; } - - console.log(`Processing JSON files in output directory: ${dirPath}`); + cdsExtractorLog('info', `Processing JSON files in directory: ${dirPath}`); // Get all entries in the directory const entries = readdirSync(dirPath, { withFileTypes: true }); @@ -124,25 +52,7 @@ export function recursivelyRenameJsonFiles(dirPath: string): void { // Rename .json files to .cds.json const newPath = format({ ...parse(fullPath), base: '', ext: '.cds.json' }); renameSync(fullPath, newPath); - console.log(`Renamed CDS output file from ${fullPath} to ${newPath}`); + cdsExtractorLog('info', `Renamed CDS output file from ${fullPath} to ${newPath}`); } } } - -/** - * Validate a response file exists and can be read - * @param responseFile Path to the response file - * @returns Object containing success status and error message if any - */ -export function validateResponseFile(responseFile: string): { - success: boolean; - errorMessage?: string; -} { - if (!fileExists(responseFile)) { - return { - success: false, - errorMessage: `response file '${responseFile}' does not exist. This is because no CDS files were selected or found`, - }; - } - return { success: true }; -} diff --git a/extractors/cds/tools/src/logging/cdsExtractorLog.ts b/extractors/cds/tools/src/logging/cdsExtractorLog.ts new file mode 100644 index 000000000..94b2660ee --- /dev/null +++ b/extractors/cds/tools/src/logging/cdsExtractorLog.ts @@ -0,0 +1,172 @@ +import type { LogLevel } from './types'; + +/** + * Source root directory for logging context. + */ +let sourceRootDirectory: string | undefined; + +/** + * Unique session ID for this CDS extractor run to help distinguish + * between multiple concurrent or sequential runs in logs. + * Uses the extractor start timestamp for uniqueness. + */ +const sessionId = Date.now().toString(); + +/** + * Start time of the CDS extractor session for performance tracking. + */ +const extractorStartTime = Date.now(); + +/** + * Performance tracking state for timing critical operations. + */ +const performanceTracking = new Map(); + +/** + * Unified logging function for the CDS extractor. Provides consistent + * log formatting with level prefixes, elapsed time, and session IDs. + * + * @param level - The log level ('debug', 'info', 'warn', 'error') + * @param message - The primary message or data to log + * @param optionalParams - Additional parameters to log (same as console.log) + */ +export function cdsExtractorLog( + level: LogLevel, + message: unknown, + ...optionalParams: unknown[] +): void { + if (!sourceRootDirectory) { + throw new Error('Source root directory is not set. Call setSourceRootDirectory() first.'); + } + + const currentTime = Date.now(); + const elapsedMs = currentTime - extractorStartTime; + const levelPrefix = `[CDS-${sessionId} ${elapsedMs}] ${level.toUpperCase()}: `; + + // Select the appropriate console function based on log level + switch (level) { + case 'debug': + case 'info': + if (typeof message === 'string') { + console.log(levelPrefix + message, ...optionalParams); + } else { + console.log(levelPrefix, message, ...optionalParams); + } + break; + case 'warn': + if (typeof message === 'string') { + console.warn(levelPrefix + message, ...optionalParams); + } else { + console.warn(levelPrefix, message, ...optionalParams); + } + break; + case 'error': + if (typeof message === 'string') { + console.error(levelPrefix + message, ...optionalParams); + } else { + console.error(levelPrefix, message, ...optionalParams); + } + break; + default: + // This should never happen due to TypeScript typing + throw new Error(`Invalid log level: ${String(level)}`); + } +} +/** + * Calculates elapsed time from start and formats it with appropriate units. + * + * @param startTime - The start timestamp in milliseconds + * @param endTime - The end timestamp in milliseconds (defaults to current time) + * @returns Formatted duration string + */ +function formatDuration(startTime: number, endTime: number = Date.now()): string { + const durationMs = endTime - startTime; + + if (durationMs < 1000) { + return `${durationMs}ms`; + } else if (durationMs < 60000) { + return `${(durationMs / 1000).toFixed(2)}s`; + } else { + const minutes = Math.floor(durationMs / 60000); + const seconds = ((durationMs % 60000) / 1000).toFixed(2); + return `${minutes}m ${seconds}s`; + } +} + +/** + * Logs the start of the CDS extractor session with session information. + * + * @param sourceRoot - The source root directory being processed + */ +export function logExtractorStart(sourceRoot: string): void { + cdsExtractorLog('info', `=== CDS EXTRACTOR START [${sessionId}] ===`); + cdsExtractorLog('info', `Source Root: ${sourceRoot}`); +} + +/** + * Logs the end of the CDS extractor session with final performance summary. + * + * @param success - Whether the extraction completed successfully + * @param additionalSummary - Optional additional summary information + */ +export function logExtractorStop(success: boolean = true, additionalSummary?: string): void { + const endTime = Date.now(); + const totalDuration = formatDuration(extractorStartTime, endTime); + const status = success ? 'SUCCESS' : 'FAILURE'; + + if (additionalSummary) { + cdsExtractorLog('info', additionalSummary); + } + + cdsExtractorLog('info', `=== CDS EXTRACTOR END [${sessionId}] - ${status} ===`); + cdsExtractorLog('info', `Total Duration: ${totalDuration}`); +} + +/** + * Logs a performance milestone with timing information. + * + * @param milestone - Description of the milestone reached + * @param additionalInfo - Optional additional information to include + */ +export function logPerformanceMilestone(milestone: string, additionalInfo?: string): void { + const currentTime = Date.now(); + const overallDuration = formatDuration(extractorStartTime, currentTime); + const info = additionalInfo ? ` - ${additionalInfo}` : ''; + cdsExtractorLog('info', `MILESTONE: ${milestone} (after ${overallDuration})${info}`); +} + +/** + * Starts tracking performance for a named operation. + * + * @param operationName - Name of the operation to track + */ +export function logPerformanceTrackingStart(operationName: string): void { + performanceTracking.set(operationName, Date.now()); + cdsExtractorLog('debug', `Started: ${operationName}`); +} + +/** + * Ends tracking performance for a named operation and logs the duration. + * + * @param operationName - Name of the operation to stop tracking + */ +export function logPerformanceTrackingStop(operationName: string): void { + const startTime = performanceTracking.get(operationName); + if (startTime) { + const duration = formatDuration(startTime); + performanceTracking.delete(operationName); + cdsExtractorLog('info', `Completed: ${operationName} (took ${duration})`); + } else { + cdsExtractorLog('warn', `No start time found for operation: ${operationName}`); + } +} + +/** + * Sets the source root directory for logging context. + * This should typically be called once at the start of the CDS extractor. + * + * @param sourceRoot - The absolute path to the source root directory + */ +export function setSourceRootDirectory(sourceRoot: string): void { + sourceRootDirectory = sourceRoot; +} diff --git a/extractors/cds/tools/src/logging/index.ts b/extractors/cds/tools/src/logging/index.ts new file mode 100644 index 000000000..549b4f312 --- /dev/null +++ b/extractors/cds/tools/src/logging/index.ts @@ -0,0 +1,11 @@ +export { + cdsExtractorLog, + setSourceRootDirectory, + logExtractorStart, + logExtractorStop, + logPerformanceMilestone, + logPerformanceTrackingStart, + logPerformanceTrackingStop, +} from './cdsExtractorLog'; +export { generateStatusReport } from './statusReport'; +export type { LogLevel } from './types'; diff --git a/extractors/cds/tools/src/logging/statusReport.ts b/extractors/cds/tools/src/logging/statusReport.ts new file mode 100644 index 000000000..26e3a6656 --- /dev/null +++ b/extractors/cds/tools/src/logging/statusReport.ts @@ -0,0 +1,79 @@ +import type { CdsDependencyGraph } from '../cds/parser'; + +/** + * Generate a comprehensive status report for the dependency graph + * Supports both normal execution and debug modes + */ +export function generateStatusReport(dependencyGraph: CdsDependencyGraph): string { + const summary = dependencyGraph.statusSummary; + const lines: string[] = []; + + lines.push('='.repeat(80)); + lines.push(`CDS EXTRACTOR STATUS REPORT`); + lines.push('='.repeat(80)); + lines.push(''); + + // Overall summary + lines.push('OVERALL SUMMARY:'); + lines.push(` Status: ${summary.overallSuccess ? 'SUCCESS' : 'FAILED'}`); + lines.push(` Current Phase: ${dependencyGraph.currentPhase.toUpperCase()}`); + lines.push(` Projects: ${summary.totalProjects}`); + lines.push(` CDS Files: ${summary.totalCdsFiles}`); + lines.push(` JSON Files Generated: ${summary.jsonFilesGenerated}`); + lines.push(''); + + // Compilation summary + lines.push('COMPILATION SUMMARY:'); + lines.push(` Total Tasks: ${summary.totalCompilationTasks}`); + lines.push(` Successful: ${summary.successfulCompilations}`); + lines.push(` Failed: ${summary.failedCompilations}`); + lines.push(` Skipped: ${summary.skippedCompilations}`); + lines.push(''); + + // Performance metrics + lines.push('PERFORMANCE:'); + lines.push(` Total Duration: ${summary.performance.totalDurationMs}ms`); + lines.push(` Parsing: ${summary.performance.parsingDurationMs}ms`); + lines.push(` Compilation: ${summary.performance.compilationDurationMs}ms`); + lines.push(` Extraction: ${summary.performance.extractionDurationMs}ms`); + + // Add percentage breakdown if total duration > 0 + if (summary.performance.totalDurationMs > 0) { + const parsingPct = Math.round( + (summary.performance.parsingDurationMs / summary.performance.totalDurationMs) * 100, + ); + const compilationPct = Math.round( + (summary.performance.compilationDurationMs / summary.performance.totalDurationMs) * 100, + ); + const extractionPct = Math.round( + (summary.performance.extractionDurationMs / summary.performance.totalDurationMs) * 100, + ); + + lines.push(' Breakdown:'); + lines.push(` Parsing: ${parsingPct}%`); + lines.push(` Compilation: ${compilationPct}%`); + lines.push(` Extraction: ${extractionPct}%`); + } + lines.push(''); + + // Errors and warnings + if (summary.criticalErrors.length > 0) { + lines.push('CRITICAL ERRORS:'); + for (const error of summary.criticalErrors) { + lines.push(` - ${error}`); + } + lines.push(''); + } + + if (summary.warnings.length > 0) { + lines.push('WARNINGS:'); + for (const warning of summary.warnings) { + lines.push(` - ${warning}`); + } + lines.push(''); + } + + lines.push('='.repeat(80)); + + return lines.join('\n'); +} diff --git a/extractors/cds/tools/src/logging/types.ts b/extractors/cds/tools/src/logging/types.ts new file mode 100644 index 000000000..57a0eb58b --- /dev/null +++ b/extractors/cds/tools/src/logging/types.ts @@ -0,0 +1,4 @@ +/** + * Log levels supported by the CDS extractor logging system + */ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; diff --git a/extractors/cds/tools/src/packageManager.ts b/extractors/cds/tools/src/packageManager.ts deleted file mode 100644 index 5573d04d7..000000000 --- a/extractors/cds/tools/src/packageManager.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { execFileSync } from 'child_process'; -import { existsSync, readFileSync } from 'fs'; -import { join, dirname, resolve } from 'path'; - -import { addDependencyDiagnostic, addPackageJsonParsingDiagnostic } from './diagnostics'; - -/** - * Interface for package.json structure - */ -export interface PackageJson { - name?: string; - dependencies?: Record; - devDependencies?: Record; -} - -/** - * Find directories containing package.json with a `@sap/cds` dependency. - * @param filePaths List of CDS file paths to check. - * @param codeqlExePath Path to the CodeQL executable (optional). - * @param sourceRoot The source root directory (optional) - Limits the search to - * never go above this directory. - * @returns Set of directories containing relevant package.json files. - */ -export function findPackageJsonDirs( - filePaths: string[], - codeqlExePath?: string, - sourceRoot?: string, -): Set { - const packageJsonDirs = new Set(); - const absoluteSourceRoot = sourceRoot ? resolve(sourceRoot) : undefined; - - filePaths.forEach(file => { - let dir = dirname(resolve(file)); - - // Check current directory and parent directories for package.json with a - // dependency on `@sap/cds`. Never look above the source root directory. - while (true) { - // Stop if we've reached or gone above the source root directory. - if (absoluteSourceRoot && !dir.startsWith(absoluteSourceRoot)) { - break; - } - - const packageJsonPath = join(dir, 'package.json'); - if (existsSync(packageJsonPath)) { - try { - const rawData = readFileSync(packageJsonPath, 'utf-8'); - const packageJsonData = JSON.parse(rawData) as PackageJson; - - if ( - packageJsonData.name && - packageJsonData.dependencies && - typeof packageJsonData.dependencies === 'object' && - Object.keys(packageJsonData.dependencies).includes('@sap/cds') - ) { - packageJsonDirs.add(dir); - break; - } - } catch (error) { - const errorMessage = `Failed to parse package.json at ${packageJsonPath}: ${String(error)}`; - console.warn(`WARN: ${errorMessage}`); - - if (codeqlExePath) { - addPackageJsonParsingDiagnostic(packageJsonPath, errorMessage, codeqlExePath); - } - } - } - // Move up one directory level - const parentDir = dirname(dir); - if (dir === parentDir) { - // We've reached the root directory, so break out of the loop - break; - } - dir = parentDir; - } - }); - - return packageJsonDirs; -} - -/** - * Install dependencies in the package.json directories - * @param packageJsonDirs Set of directories containing package.json files - * @param codeqlExePath Path to the CodeQL executable (optional) - */ -export function installDependencies(packageJsonDirs: Set, codeqlExePath?: string): void { - // Sanity check that we found at least one package.json directory - if (packageJsonDirs.size === 0) { - console.warn( - 'WARN: failed to detect any package.json directories for cds compiler installation.', - ); - return; - } - - packageJsonDirs.forEach(dir => { - console.log(`Installing node dependencies from ${dir}/package.json ...`); - try { - execFileSync('npm', ['install', '--quiet', '--no-audit', '--no-fund'], { - cwd: dir, - stdio: 'inherit', - }); - - // Order is important here. Install dependencies from package.json in the directory, - // then install the CDS development kit (`@sap/cds-dk`) in the directory. - console.log(`Installing '@sap/cds-dk' into ${dir} to enable CDS compilation ...`); - execFileSync( - 'npm', - ['install', '--quiet', '--no-audit', '--no-fund', '--no-save', '@sap/cds-dk'], - { cwd: dir, stdio: 'inherit' }, - ); - } catch (err) { - const errorMessage = `Failed to install dependencies in ${dir}: ${err instanceof Error ? err.message : String(err)}`; - console.error(errorMessage); - if (codeqlExePath) { - const packageJsonPath = join(dir, 'package.json'); - addDependencyDiagnostic(packageJsonPath, errorMessage, codeqlExePath); - } - } - }); -} diff --git a/extractors/cds/tools/src/packageManager/index.ts b/extractors/cds/tools/src/packageManager/index.ts new file mode 100644 index 000000000..ea1666150 --- /dev/null +++ b/extractors/cds/tools/src/packageManager/index.ts @@ -0,0 +1,15 @@ +// Export the new robust installer functionality (preferred) +export { installDependencies } from './installer'; +export type { CdsDependencyCombination } from './types'; + +// Export version resolver functionality +export { + checkVersionCompatibility, + compareVersions, + findBestAvailableVersion, + getAvailableVersions, + getCacheStatistics, + parseSemanticVersion, + resolveCdsVersions, + satisfiesRange, +} from './versionResolver'; diff --git a/extractors/cds/tools/src/packageManager/installer.ts b/extractors/cds/tools/src/packageManager/installer.ts new file mode 100644 index 000000000..722570e07 --- /dev/null +++ b/extractors/cds/tools/src/packageManager/installer.ts @@ -0,0 +1,417 @@ +import { execFileSync } from 'child_process'; +import { createHash } from 'crypto'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join, resolve } from 'path'; + +import type { CdsDependencyCombination } from './types'; +import { CdsDependencyGraph, CdsProject } from '../cds/parser/types'; +import { DiagnosticSeverity } from '../diagnostics'; +import { cdsExtractorLog } from '../logging'; +import { resolveCdsVersions } from './versionResolver'; + +const cacheSubDirName = '.cds-extractor-cache'; + +/** + * Add a warning diagnostic for dependency version fallback + * @param packageJsonPath Path to the package.json file + * @param warningMessage The warning message + * @param codeqlExePath Path to the CodeQL executable + * @returns True if the diagnostic was added, false otherwise + */ +function addDependencyVersionWarning( + packageJsonPath: string, + warningMessage: string, + codeqlExePath: string, +): boolean { + try { + execFileSync(codeqlExePath, [ + 'database', + 'add-diagnostic', + '--extractor-name=cds', + '--ready-for-status-page', + '--source-id=cds/dependency-version-fallback', + '--source-name=Using fallback versions for SAP CAP CDS dependencies', + `--severity=${DiagnosticSeverity.Warning}`, + `--markdown-message=${warningMessage}`, + `--file-path=${resolve(packageJsonPath)}`, + '--', + `${process.env.CODEQL_EXTRACTOR_CDS_WIP_DATABASE ?? ''}`, + ]); + cdsExtractorLog('info', `Added warning diagnostic for dependency fallback: ${packageJsonPath}`); + return true; + } catch (err) { + cdsExtractorLog( + 'error', + `Failed to add warning diagnostic for ${packageJsonPath}: ${String(err)}`, + ); + return false; + } +} + +/** + * Extracts unique dependency combinations from the dependency graph. + * @param projects A map of projects from the dependency graph. + * @returns An array of unique dependency combinations. + */ +function extractUniqueDependencyCombinations( + projects: Map, +): CdsDependencyCombination[] { + const combinations = new Map(); + + for (const project of Array.from(projects.values())) { + if (!project.packageJson) { + continue; + } + + const cdsVersion = project.packageJson.dependencies?.['@sap/cds'] ?? 'latest'; + const cdsDkVersion = project.packageJson.devDependencies?.['@sap/cds-dk'] ?? cdsVersion; + + // Resolve versions first to ensure we cache based on actual resolved versions + cdsExtractorLog( + 'info', + `Resolving available dependency versions for project '${project.projectDir}' with dependencies: [@sap/cds@${cdsVersion}, @sap/cds-dk@${cdsDkVersion}]`, + ); + const resolvedVersions = resolveCdsVersions(cdsVersion, cdsDkVersion); + const { resolvedCdsVersion, resolvedCdsDkVersion, ...rest } = resolvedVersions; + + // Log the resolved CDS dependency versions for the project + if (resolvedCdsVersion && resolvedCdsDkVersion) { + let statusMsg: string; + if (resolvedVersions.cdsExactMatch && resolvedVersions.cdsDkExactMatch) { + statusMsg = ' (exact match)'; + } else if (!resolvedVersions.isFallback) { + statusMsg = ' (compatible versions)'; + } else { + statusMsg = ' (using fallback versions)'; + } + cdsExtractorLog( + 'info', + `Resolved to: @sap/cds@${resolvedCdsVersion}, @sap/cds-dk@${resolvedCdsDkVersion}${statusMsg}`, + ); + } else { + cdsExtractorLog( + 'error', + `Failed to resolve CDS dependencies: @sap/cds@${cdsVersion}, @sap/cds-dk@${cdsDkVersion}`, + ); + } + + // Calculate hash based on resolved versions to ensure proper cache reuse + const actualCdsVersion = resolvedCdsVersion ?? cdsVersion; + const actualCdsDkVersion = resolvedCdsDkVersion ?? cdsDkVersion; + const hash = createHash('sha256') + .update(`${actualCdsVersion}|${actualCdsDkVersion}`) + .digest('hex'); + + if (!combinations.has(hash)) { + combinations.set(hash, { + cdsVersion, + cdsDkVersion, + hash, + resolvedCdsVersion: resolvedCdsVersion ?? undefined, + resolvedCdsDkVersion: resolvedCdsDkVersion ?? undefined, + ...rest, + }); + } + } + + return Array.from(combinations.values()); +} + +/** + * Install dependencies for CDS projects using a robust cache strategy with fallback logic + * @param dependencyGraph The dependency graph of the project + * @param sourceRoot Source root directory + * @param codeqlExePath Path to the CodeQL executable (optional) + * @returns Map of project directories to their corresponding cache directories + */ +export function installDependencies( + dependencyGraph: CdsDependencyGraph, + sourceRoot: string, + codeqlExePath?: string, +): Map { + // Sanity check that we found at least one project + if (dependencyGraph.projects.size === 0) { + cdsExtractorLog('info', 'No CDS projects found for dependency installation.'); + cdsExtractorLog( + 'info', + 'This is expected if the source contains no CAP/CDS projects and should be handled by the caller.', + ); + return new Map(); + } + + // Extract unique dependency combinations from all projects with version resolution + const dependencyCombinations = extractUniqueDependencyCombinations(dependencyGraph.projects); + + if (dependencyCombinations.length === 0) { + cdsExtractorLog( + 'error', + 'No CDS dependencies found in any project. This means projects were detected but lack proper @sap/cds dependencies.', + ); + cdsExtractorLog( + 'info', + 'Will attempt to use system-installed CDS tools if available, but compilation may fail.', + ); + return new Map(); + } + + cdsExtractorLog( + 'info', + `Found ${dependencyCombinations.length} unique CDS dependency combination(s).`, + ); + + // Log each dependency combination for transparency + for (const combination of dependencyCombinations) { + const { cdsVersion, cdsDkVersion, hash, resolvedCdsVersion, resolvedCdsDkVersion, isFallback } = + combination; + const actualCdsVersion = resolvedCdsVersion ?? cdsVersion; + const actualCdsDkVersion = resolvedCdsDkVersion ?? cdsDkVersion; + const fallbackNote = isFallback ? ' (using fallback versions)' : ''; + + cdsExtractorLog( + 'info', + `Dependency combination ${hash.substring(0, 8)}: @sap/cds@${actualCdsVersion}, @sap/cds-dk@${actualCdsDkVersion}${fallbackNote}`, + ); + } + + // Create a cache directory under the source root directory. + const cacheRootDir = join(sourceRoot, cacheSubDirName); + cdsExtractorLog( + 'info', + `Using cache directory '${cacheSubDirName}' within source root directory '${cacheRootDir}'`, + ); + + if (!existsSync(cacheRootDir)) { + try { + mkdirSync(cacheRootDir, { recursive: true }); + cdsExtractorLog('info', `Created cache directory: ${cacheRootDir}`); + } catch (err) { + cdsExtractorLog( + 'warn', + `Failed to create cache directory: ${err instanceof Error ? err.message : String(err)}`, + ); + cdsExtractorLog('info', 'Skipping dependency installation due to cache directory failure.'); + return new Map(); + } + } else { + cdsExtractorLog('info', `Cache directory already exists: ${cacheRootDir}`); + } + + // Map to track which cache directory to use for each project + const projectCacheDirMap = new Map(); + let successfulInstallations = 0; + + // Install each unique dependency combination in its own cache directory + for (const combination of dependencyCombinations) { + const { cdsVersion, cdsDkVersion, hash } = combination; + const { resolvedCdsVersion, resolvedCdsDkVersion } = combination; + const cacheDirName = `cds-${hash}`; + const cacheDir = join(cacheRootDir, cacheDirName); + + cdsExtractorLog( + 'info', + `Processing dependency combination ${hash.substring(0, 8)} in cache directory: ${cacheDirName}`, + ); + + // Create the cache directory if it doesn't exist + if (!existsSync(cacheDir)) { + try { + mkdirSync(cacheDir, { recursive: true }); + cdsExtractorLog('info', `Created cache subdirectory: ${cacheDirName}`); + } catch (err) { + cdsExtractorLog( + 'error', + `Failed to create cache directory for combination ${hash.substring(0, 8)} (${cacheDirName}): ${ + err instanceof Error ? err.message : String(err) + }`, + ); + continue; + } + + // Create a package.json for this dependency combination using resolved versions + const actualCdsVersion = resolvedCdsVersion ?? cdsVersion; + const actualCdsDkVersion = resolvedCdsDkVersion ?? cdsDkVersion; + + const packageJson = { + name: `cds-extractor-cache-${hash}`, + version: '1.0.0', + private: true, + dependencies: { + '@sap/cds': actualCdsVersion, + '@sap/cds-dk': actualCdsDkVersion, + }, + }; + + try { + writeFileSync(join(cacheDir, 'package.json'), JSON.stringify(packageJson, null, 2)); + cdsExtractorLog('info', `Created package.json in cache subdirectory: ${cacheDirName}`); + } catch (err) { + cdsExtractorLog( + 'error', + `Failed to create package.json in cache directory ${cacheDirName}: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + continue; + } + } + + // Try to install dependencies in the cache directory + // Get the first project package.json path for diagnostic purposes + const samplePackageJsonPath = Array.from(dependencyGraph.projects.values()).find( + project => project.packageJson, + )?.projectDir; + const packageJsonPath = samplePackageJsonPath + ? join(sourceRoot, samplePackageJsonPath, 'package.json') + : undefined; + + const installSuccess = installDependenciesInCache( + cacheDir, + combination, + cacheDirName, + packageJsonPath, + codeqlExePath, + ); + + if (!installSuccess) { + cdsExtractorLog( + 'warn', + `Skipping failed dependency combination ${hash.substring(0, 8)} (cache directory: ${cacheDirName})`, + ); + continue; + } + + successfulInstallations++; + + // Associate projects with this dependency combination + for (const [projectDir, project] of Array.from(dependencyGraph.projects.entries())) { + if (!project.packageJson) { + continue; + } + const p_cdsVersion = project.packageJson.dependencies?.['@sap/cds'] ?? 'latest'; + const p_cdsDkVersion = project.packageJson.devDependencies?.['@sap/cds-dk'] ?? p_cdsVersion; + + // Resolve the project's versions to match against the combination's resolved versions + const projectResolvedVersions = resolveCdsVersions(p_cdsVersion, p_cdsDkVersion); + const projectActualCdsVersion = projectResolvedVersions.resolvedCdsVersion ?? p_cdsVersion; + const projectActualCdsDkVersion = + projectResolvedVersions.resolvedCdsDkVersion ?? p_cdsDkVersion; + + // Match based on resolved versions since that's what the hash is based on + const combinationActualCdsVersion = combination.resolvedCdsVersion ?? combination.cdsVersion; + const combinationActualCdsDkVersion = + combination.resolvedCdsDkVersion ?? combination.cdsDkVersion; + + if ( + projectActualCdsVersion === combinationActualCdsVersion && + projectActualCdsDkVersion === combinationActualCdsDkVersion + ) { + projectCacheDirMap.set(projectDir, cacheDir); + } + } + } + + // Log final status + if (successfulInstallations === 0) { + cdsExtractorLog('error', 'Failed to install any dependency combinations.'); + if (dependencyCombinations.length > 0) { + cdsExtractorLog( + 'error', + `All ${dependencyCombinations.length} dependency combination(s) failed to install. This will likely cause compilation failures.`, + ); + } + } else if (successfulInstallations < dependencyCombinations.length) { + cdsExtractorLog( + 'warn', + `Successfully installed ${successfulInstallations} out of ${dependencyCombinations.length} dependency combinations.`, + ); + } else { + cdsExtractorLog('info', 'All dependency combinations installed successfully.'); + } + + // Log project-to-cache-directory mappings for transparency. + if (projectCacheDirMap.size > 0) { + cdsExtractorLog('info', `Project to cache directory mappings:`); + for (const [projectDir, cacheDir] of Array.from(projectCacheDirMap.entries())) { + const cacheDirName = join(cacheDir).split('/').pop() ?? 'unknown'; + cdsExtractorLog('info', ` ${projectDir} → ${cacheDirName}`); + } + } else { + cdsExtractorLog( + 'warn', + 'No project to cache directory mappings created. Projects may not have compatible dependencies installed.', + ); + } + + return projectCacheDirMap; +} + +/** + * Attempt to install dependencies in a cache directory with fallback logic + * @param cacheDir Cache directory path + * @param combination Dependency combination to install + * @param cacheDirName Name of the cache directory for logging + * @param packageJsonPath Optional package.json path for diagnostics + * @param codeqlExePath Optional CodeQL executable path for diagnostics + * @returns True if installation succeeded, false otherwise + */ +function installDependenciesInCache( + cacheDir: string, + combination: CdsDependencyCombination, + cacheDirName: string, + packageJsonPath?: string, + codeqlExePath?: string, +): boolean { + const { resolvedCdsVersion, resolvedCdsDkVersion, isFallback, warning } = combination; + + // Check if node_modules directory already exists in the cache dir + const nodeModulesExists = + existsSync(join(cacheDir, 'node_modules', '@sap', 'cds')) && + existsSync(join(cacheDir, 'node_modules', '@sap', 'cds-dk')); + + if (nodeModulesExists) { + cdsExtractorLog( + 'info', + `Using cached dependencies for @sap/cds@${resolvedCdsVersion} and @sap/cds-dk@${resolvedCdsDkVersion} from ${cacheDirName}`, + ); + + // Add warning diagnostic if using fallback versions + if (isFallback && warning && packageJsonPath && codeqlExePath) { + addDependencyVersionWarning(packageJsonPath, warning, codeqlExePath); + } + + return true; + } + + if (!resolvedCdsVersion || !resolvedCdsDkVersion) { + cdsExtractorLog('error', 'Cannot install dependencies: no compatible versions found'); + return false; + } + + // Install dependencies in the cache directory + cdsExtractorLog( + 'info', + `Installing @sap/cds@${resolvedCdsVersion} and @sap/cds-dk@${resolvedCdsDkVersion} in cache directory: ${cacheDirName}`, + ); + + if (isFallback && warning) { + cdsExtractorLog('warn', warning); + } + + try { + execFileSync('npm', ['install', '--quiet', '--no-audit', '--no-fund'], { + cwd: cacheDir, + stdio: 'inherit', + }); + + // Add warning diagnostic if using fallback versions + if (isFallback && warning && packageJsonPath && codeqlExePath) { + addDependencyVersionWarning(packageJsonPath, warning, codeqlExePath); + } + + return true; + } catch (err) { + const errorMessage = `Failed to install resolved dependencies in cache directory ${cacheDir}: ${err instanceof Error ? err.message : String(err)}`; + cdsExtractorLog('error', errorMessage); + return false; + } +} diff --git a/extractors/cds/tools/src/packageManager/types.ts b/extractors/cds/tools/src/packageManager/types.ts new file mode 100644 index 000000000..46d90b4a0 --- /dev/null +++ b/extractors/cds/tools/src/packageManager/types.ts @@ -0,0 +1,26 @@ +/** Interface types for the CDS extractor `packageMangager` package. */ + +/** + * Represents a unique combination of @sap/cds and @sap/cds-dk dependencies. + */ +export interface CdsDependencyCombination { + cdsVersion: string; + cdsDkVersion: string; + hash: string; + resolvedCdsVersion?: string; + resolvedCdsDkVersion?: string; + isFallback?: boolean; + warning?: string; +} + +/** + * Represents a semantic version. + */ +export interface SemanticVersion { + major: number; + minor: number; + patch: number; + prerelease?: string; + build?: string; + original: string; +} diff --git a/extractors/cds/tools/src/packageManager/versionResolver.ts b/extractors/cds/tools/src/packageManager/versionResolver.ts new file mode 100644 index 000000000..61648adeb --- /dev/null +++ b/extractors/cds/tools/src/packageManager/versionResolver.ts @@ -0,0 +1,367 @@ +import { execSync } from 'child_process'; + +import type { SemanticVersion } from './types'; +import { cdsExtractorLog } from '../logging'; + +/** + * Cache for storing available versions for npm packages to avoid duplicate + * `npm view` calls. + */ +const availableVersionsCache = new Map(); + +/** + * Cache statistics for debugging purposes + */ +const cacheStats = { + hits: 0, + misses: 0, + get hitRate() { + const total = this.hits + this.misses; + return total > 0 ? ((this.hits / total) * 100).toFixed(1) : '0.0'; + }, +}; + +/** + * Check if @sap/cds and @sap/cds-dk versions are likely compatible. + * @param cdsVersion The @sap/cds version + * @param cdsDkVersion The @sap/cds-dk version + * @returns Object with compatibility information and warnings + */ +export function checkVersionCompatibility( + cdsVersion: string, + cdsDkVersion: string, +): { + isCompatible: boolean; + warning?: string; +} { + // If either version is 'latest', assume they are compatible + if (cdsVersion === 'latest' || cdsDkVersion === 'latest') { + return { isCompatible: true }; + } + + const parsedCds = parseSemanticVersion(cdsVersion); + const parsedCdsDk = parseSemanticVersion(cdsDkVersion); + + if (!parsedCds || !parsedCdsDk) { + return { + isCompatible: false, + warning: 'Unable to parse version numbers for compatibility check', + }; + } + + // Generally, @sap/cds and @sap/cds-dk should have the same major version + // and ideally the same minor version for best compatibility + const majorVersionsMatch = parsedCds.major === parsedCdsDk.major; + const minorVersionsMatch = parsedCds.minor === parsedCdsDk.minor; + + if (!majorVersionsMatch) { + return { + isCompatible: false, + warning: `Major version mismatch: @sap/cds ${cdsVersion} and @sap/cds-dk ${cdsDkVersion} may not be compatible`, + }; + } + + if (!minorVersionsMatch) { + return { + isCompatible: true, + warning: `Minor version difference: @sap/cds ${cdsVersion} and @sap/cds-dk ${cdsDkVersion} - consider aligning versions for best compatibility`, + }; + } + + return { isCompatible: true }; +} + +/** + * Compare two semantic versions + * @param a First version + * @param b Second version + * @returns Negative if a < b, 0 if equal, positive if a > b + */ +export function compareVersions(a: SemanticVersion, b: SemanticVersion): number { + if (a.major !== b.major) return a.major - b.major; + if (a.minor !== b.minor) return a.minor - b.minor; + if (a.patch !== b.patch) return a.patch - b.patch; + + // Handle prerelease versions (prerelease < release) + if (a.prerelease && !b.prerelease) return -1; + if (!a.prerelease && b.prerelease) return 1; + if (a.prerelease && b.prerelease) { + return a.prerelease.localeCompare(b.prerelease); + } + + return 0; +} + +/** + * Find the best available version from a list of versions for a given requirement + * @param availableVersions List of available version strings + * @param requiredVersion Required version string + * @returns Best matching version or null if no compatible version found + */ +export function findBestAvailableVersion( + availableVersions: string[], + requiredVersion: string, +): string | null { + const parsedVersions = availableVersions + .map(v => parseSemanticVersion(v)) + .filter((v): v is SemanticVersion => v !== null); + + if (parsedVersions.length === 0) { + return null; + } + + // First, try to find versions that satisfy the range + const satisfyingVersions = parsedVersions.filter(v => satisfiesRange(v, requiredVersion)); + + if (satisfyingVersions.length > 0) { + // Sort in descending order (newest first) and return the best match + satisfyingVersions.sort((a, b) => compareVersions(b, a)); + return satisfyingVersions[0].original; + } + + // If no exact match, prefer newer versions over older ones + // Sort all versions in descending order and return the newest + parsedVersions.sort((a, b) => compareVersions(b, a)); + return parsedVersions[0].original; +} + +/** + * Get available versions for an npm package with caching to avoid duplicate npm view calls + * @param packageName Name of the npm package + * @returns Array of available version strings + */ +export function getAvailableVersions(packageName: string): string[] { + // Check cache first + if (availableVersionsCache.has(packageName)) { + cacheStats.hits++; + return availableVersionsCache.get(packageName)!; + } + + // Cache miss - fetch from npm + cacheStats.misses++; + try { + const output = execSync(`npm view ${packageName} versions --json`, { + encoding: 'utf8', + timeout: 30000, // 30 second timeout + }); + + const versions: unknown = JSON.parse(output); + let versionArray: string[] = []; + + if (Array.isArray(versions)) { + versionArray = versions.filter((v): v is string => typeof v === 'string'); + } else if (typeof versions === 'string') { + versionArray = [versions]; + } + + // Cache the result + availableVersionsCache.set(packageName, versionArray); + + return versionArray; + } catch (error) { + cdsExtractorLog('warn', `Failed to fetch versions for ${packageName}: ${String(error)}`); + // Cache empty array to avoid repeated failures + availableVersionsCache.set(packageName, []); + return []; + } +} + +/** + * Get cache statistics for debugging purposes + * @returns Object with cache hit/miss statistics + */ +export function getCacheStatistics(): { + hits: number; + misses: number; + hitRate: string; + cachedPackages: string[]; +} { + return { + hits: cacheStats.hits, + misses: cacheStats.misses, + hitRate: cacheStats.hitRate, + cachedPackages: Array.from(availableVersionsCache.keys()), + }; +} + +/** + * Parse a semantic version string + * @param version Version string to parse (e.g., "6.1.3", "^6.0.0", "~6.1.0", "latest") + * @returns Parsed semantic version or null if invalid + */ +export function parseSemanticVersion(version: string): SemanticVersion | null { + if (version === 'latest') { + // Return a very high version number for 'latest' to ensure it's preferred + return { + major: 999, + minor: 999, + patch: 999, + original: version, + }; + } + + // Remove common version prefixes + const cleanVersion = version.replace(/^[\^~>=<]+/, ''); + + // Basic semver regex + const semverRegex = /^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/; + const match = cleanVersion.match(semverRegex); + + if (!match) { + return null; + } + + return { + major: parseInt(match[1], 10), + minor: parseInt(match[2], 10), + patch: parseInt(match[3], 10), + prerelease: match[4], + build: match[5], + original: version, + }; +} + +/** + * Check if a resolved version satisfies the originally requested version. + * @param resolvedVersion The version that was resolved + * @param requestedVersion The originally requested version + * @returns true if the resolved version satisfies the requested version range + */ +function isSatisfyingVersion(resolvedVersion: string, requestedVersion: string): boolean { + // Exact string match or 'latest' case + if (resolvedVersion === requestedVersion || requestedVersion === 'latest') { + return true; + } + + const parsedResolved = parseSemanticVersion(resolvedVersion); + if (!parsedResolved) { + return false; + } + + return satisfiesRange(parsedResolved, requestedVersion); +} + +/** + * Resolve the best available version for CDS dependencies + * @param cdsVersion Required @sap/cds version + * @param cdsDkVersion Required @sap/cds-dk version + * @returns Object with resolved versions and compatibility info + */ +export function resolveCdsVersions( + cdsVersion: string, + cdsDkVersion: string, +): { + resolvedCdsVersion: string | null; + resolvedCdsDkVersion: string | null; + cdsExactMatch: boolean; + cdsDkExactMatch: boolean; + warning?: string; + isFallback?: boolean; +} { + const cdsVersions = getAvailableVersions('@sap/cds'); + const cdsDkVersions = getAvailableVersions('@sap/cds-dk'); + + const resolvedCdsVersion = findBestAvailableVersion(cdsVersions, cdsVersion); + const resolvedCdsDkVersion = findBestAvailableVersion(cdsDkVersions, cdsDkVersion); + + // Check if resolved versions are exact matches (string equality or 'latest' case). + const cdsExactMatch = + resolvedCdsVersion === cdsVersion || (cdsVersion === 'latest' && resolvedCdsVersion !== null); + const cdsDkExactMatch = + resolvedCdsDkVersion === cdsDkVersion || + (cdsDkVersion === 'latest' && resolvedCdsDkVersion !== null); + + // Check if resolved versions satisfy the requested ranges (including exact matches). + const cdsSatisfiesRange = resolvedCdsVersion + ? isSatisfyingVersion(resolvedCdsVersion, cdsVersion) + : false; + const cdsDkSatisfiesRange = resolvedCdsDkVersion + ? isSatisfyingVersion(resolvedCdsDkVersion, cdsDkVersion) + : false; + + // Only consider it a fallback if we couldn't find a satisfying version. + const isFallback = !cdsSatisfiesRange || !cdsDkSatisfiesRange; + + let warning: string | undefined; + + // Check compatibility between resolved versions (only if both were resolved). + // Show warnings when: + // 1. We're using fallback versions (couldn't find compatible versions), OR + // 2. At least one version isn't an exact match (version range was used), OR + // 3. Resolved versions have actual compatibility issues (e.g., major version mismatch). + if (resolvedCdsVersion && resolvedCdsDkVersion) { + const compatibility = checkVersionCompatibility(resolvedCdsVersion, resolvedCdsDkVersion); + + const shouldShowWarning = + isFallback || + !cdsExactMatch || + !cdsDkExactMatch || + (compatibility.warning && !compatibility.isCompatible); + + if (compatibility.warning && shouldShowWarning) { + warning = compatibility.warning; + } + } + + return { + resolvedCdsVersion, + resolvedCdsDkVersion, + cdsExactMatch, + cdsDkExactMatch, + warning, + isFallback, + }; +} + +/** + * Check if version satisfies a version range. + * @param version Version to check + * @param range Version range (e.g., "^6.0.0", "~6.1.0", ">=6.0.0") + * @returns true if version satisfies the range + */ +export function satisfiesRange(version: SemanticVersion, range: string): boolean { + if (range === 'latest') { + return true; + } + + const rangeVersion = parseSemanticVersion(range); + if (!rangeVersion) { + return false; + } + + if (range.startsWith('^')) { + // Caret range: compatible within same major version + return version.major === rangeVersion.major && compareVersions(version, rangeVersion) >= 0; + } else if (range.startsWith('~')) { + // Tilde range: compatible within same minor version + return ( + version.major === rangeVersion.major && + version.minor === rangeVersion.minor && + compareVersions(version, rangeVersion) >= 0 + ); + } else if (range.startsWith('>=')) { + // Greater than or equal + return compareVersions(version, rangeVersion) >= 0; + } else if (range.startsWith('>')) { + // Greater than + return compareVersions(version, rangeVersion) > 0; + } else if (range.startsWith('<=')) { + // Less than or equal + return compareVersions(version, rangeVersion) <= 0; + } else if (range.startsWith('<')) { + // Less than + return compareVersions(version, rangeVersion) < 0; + } else { + // Exact match + return compareVersions(version, rangeVersion) === 0; + } +} + +/** + * Test-only exports - DO NOT USE IN PRODUCTION CODE + * These are exported only for testing purposes + */ +export const __testOnly__ = { + availableVersionsCache, + cacheStats, +}; diff --git a/extractors/cds/tools/src/utils.ts b/extractors/cds/tools/src/utils.ts index 0e2066c8b..18195eea2 100644 --- a/extractors/cds/tools/src/utils.ts +++ b/extractors/cds/tools/src/utils.ts @@ -1,33 +1,41 @@ -import { resolve } from 'path'; - -/** - * Safely get a command-line parameter and properly resolve the path. - * @param `args` - Command line arguments array. - * @param `index` - Index of the argument to get. - * @param `defaultValue` - Default value to return if argument is not present. - * @returns The resolved argument value or the default value - */ -export function getArg(args: string[], index: number, defaultValue = ''): string { - if (index < args.length) { - // Handle the path resolution properly without unnecessary quoting - return resolve(args[index]); - } - return defaultValue; -} +const USAGE_MESSAGE = `\tUsage: node