diff --git a/.github/CLA.md b/.github/CLA.md new file mode 100644 index 00000000..9da08f50 --- /dev/null +++ b/.github/CLA.md @@ -0,0 +1,43 @@ +# Contributor License Agreement + +# Table of Contents + +- [Why Require a CLA?](#why-require-a-cla) +- [Legal Terms and Agreement](#legal-terms-and-agreement) + +# Why Require a CLA? +Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission to use your contribution at a later date, and that Telefónica Innovación Digital has permission to use your contribution in our commercial products. + +This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently adopt our projects. At the same time, the CLA ensures that all contributions to our open source projects are licensed under the project's respective open source license, such as Apache 2.0, MPL2 or MIT. + +Requiring a CLA is a common and well-accepted practice in open source. Major open source projects require CLAs such as Apache Software Foundation projects, Facebook projects (such as React), Google projects (including Go), Python, Django, and more. Each of these projects remains licensed under permissive OSS licenses such as MIT, Apache, BSD, and more. + +Signing the CLA +Open a pull request ("PR") to any of our projects to sign the CLA. A bot will comment on the PR asking you to sign the CLA if you haven't already. + +Follow the steps given by the bot to sign the CLA. This will require you to log in with GitHub (we only request public information from your account) and to add a comment to the PR. We will store your Github user name in a branch in this repository. We will only use this information for CLA tracking; none of your information will be used for marketing purposes. + +You only have to sign the CLA once. Once you've signed the CLA, future contributions to any Telefónica Innovación Digital project will not require you to sign again. + +# Legal Terms and Agreement +In order to clarify the intellectual property license granted with Contributions from any person or entity, Telefónica Innovación Digital, Inc. ("Telefónica Innovación Digital") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use your own Contributions for any other purpose. + +You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Telefónica Innovación Digital. Except for the license granted herein to Telefónica Innovación Digital and recipients of software distributed by Telefónica Innovación Digital, You reserve all right, title, and interest in and to Your Contributions. + +Definitions. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Telefónica Innovación Digital. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is or previously has been intentionally submitted by You to Telefónica Innovación Digital for inclusion in, or documentation of, any of the products owned or managed by Telefónica Innovación Digital (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Telefónica Innovación Digital or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Telefónica Innovación Digital for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Telefónica Innovación Digital and to recipients of software distributed by Telefónica Innovación Digital a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. + +Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Telefónica Innovación Digital and to recipients of software distributed by Telefónica Innovación Digital a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that you will have received permission from your current and future employers for all future Contributions, that your applicable employer has waived such rights for all of your current and future Contributions to Telefónica Innovación Digital, or that your employer has executed a separate Corporate CLA with Telefónica Innovación Digital. + +You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. + +You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +Should You wish to submit work that is not Your original creation, You may submit it to Telefónica Innovación Digital separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". + +You agree to notify Telefónica Innovación Digital of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d2e22a0e --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at javier.breaalcocer@telefonica.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..0a10f378 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,140 @@ +# How to contribute + +Thank you for being part of the Telefónica Innovación Digital Open Source Community! + +# Table of Contents + +- [Getting started](#getting-started) +- [Component tasks](#component-tasks) +- [License](#license) + - [Licensing of new files](#licensing-of-new-files) + - [Public Domain](#public-domain) +- [Pull Request](#pull-request) +- [Code of Conduct](#code-of-conduct) +- [Contributor License Agreement](#contributor-license-agreement) + +# Getting started + +This repository is a [Pnpm]https://pnpm.io/es/ and [Nx](https://nx.dev/react) monorepo that contains multiple components. Each component is a separate package that can be built, tested, and linted independently. + +Every component has to be created in the `components` folder. Each one must contain its own package.json file, and it can have its own dependencies. Common dependencies for development and testing should be added to the root package.json file. + +## Installation + +To get started, clone the repository and install the dependencies: + +```bash +pnpm install +``` + +# Component tasks + +Component task names are standardized across the repository. This enables to define common dependencies and files impacting them in the root `nx.json` file. The following are the most common tasks: + +* `lint`: Lints the component. +* `check:types`: Checks the TypeScript types in the component. +* `check:spell`: Checks the spelling in the component. +* `build`: Builds the component. +* `test:unit`: Runs the unit tests. +* `test:component`: Runs the component tests. +* `check:all`: Run all the checks and build the component. + +You can also rewrite the tasks to fit the component's needs. For example, if a component has special requirements for unit tests, you can define a `test:unit` task in the component's `project.json` file, redefining the Nx inputs, outputs, and dependencies in order to fit the component's needs and optimize the cache accordingly. _(See how the `markdown-confluence-sync` component does this for an example)_ + +> [!WARNING] +> It is crucial to configure properly the tasks dependencies, input, and output files, so __Nx can keep or clean the cache correctly, avoiding running unnecessary tasks__, both locally or in the pipeline. + +## Running tasks in components + +Nx provides a way to run commands in a specific component, taking care of the task dependencies. To run a command in a component, use the following syntax: `pnpm nx run `. For example, to run the unit tests in the `child-process-manager` component, use the following command: + +```bash +pnpm nx test:unit child-process-manager +``` + +> ![TIP] +> Using Nx also has the advantage of being able to cache the results of tasks, so if you run the same command again, it will be faster if any file impacting the task has not changed. + +## Running a task in all components + +To run a task in all components, use the following syntax: `pnpm nx run-many --all`. For example, to run the unit tests in all components, use the following command: + +```bash +pnpm nx run-many test:unit --all +``` + +This will run the `test:unit` task in all components and also the corresponding dependencies, in the right order, so everything is built and tested correctly. + +# License + +By contributing to this project, you agree that your contributions will be licensed under the [LICENSE](../LICENSE) file in the root of this repository, and that you agree to the [Contributor License Agreement](#contributor-license-agreement). + +## Licensing of new files + +This project adheres to the [Software Package Data Exchange (SPDX)](https://spdx.dev/). SPDX is a standard format for communicating the components, licenses, and copyrights associated with software packages. It is a simple and concise way to communicate licensing information. Read more about how to define headers using the SPDX ids [here](https://spdx.dev/learn/handling-license-info/). + +This license must be used for all new code, unless the containing project, module or externally-imported codebase uses a different license. If you can't put a header in the file due to its structure, please put it in a LICENSE file in the same directory. + +``` +// SPDX-FileCopyrightText: {{ year }} Telefónica Innovación Digital and contributors. All rights reserved +// SPDX-License-Identifier: Apache-2.0 + +# SPDX-FileCopyrightText: {{ year }} Telefónica Innovación Digital and contributors. All rights reserved +# SPDX-License-Identifier: Apache-2.0 + + + +SPDX-FileCopyrightText: {{ year }} Telefónica Innovación Digital and contributors. All rights reserved +SPDX-License-Identifier: Apache-2.0 +``` + +> ![TIP] +> When modifying an existing file, you should not change the license year. Instead, please add " - {{ year }}" to the existing year. For example, if the existing license is `2019` and you are doing the change at 2024, you should change it to `2019 - 2024`. + +## MIT License + +This license can be used for test scripts and other short code snippets, at the discretion of the author. + +``` +// SPDX-FileCopyrightText: {{ year }} Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +# SPDX-FileCopyrightText: {{ year }} Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + + + +SPDX-FileCopyrightText: {{ year }} Telefónica Innovación Digital and contributors +SPDX-License-Identifier: MIT +``` + +# Pull Request + +When you're finished with the changes, create a pull request, also known as a PR. + +* Fill the PR template. This template helps reviewers understand your changes as well as the purpose of your pull request. +* Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue) if you are solving one. +* Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, a maintainer will review your proposal. We may ask questions or request additional information. +* We may ask for changes to be made before a PR can be merged, either using suggested changes or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. +* As you update your PR and apply changes, mark each conversation as resolved. +* If you run into any merge issues, checkout this git tutorial to help you resolve merge conflicts and other issues. + +# Code of Conduct + +Please read our [Code of Conduct](../.github/CODE_OF_CONDUCT.md) before contributing. + +# Contributor License Agreement + +This is a human-readable summary of (and not a substitute for) the [full agreement](./CLA.md). This highlights only some of the key terms of the CLA. It has no legal value and you should carefully review all the terms of the [actual CLA before agreeing](./CLA.md). + +* __Grant of copyright license__. You give Telefónica Innovación Digital permission to use your copyrighted work in commercial products. +* __Grant of patent license__. If your contributed work uses a patent, you give Telefónica Innovación Digital a license to use that patent including within commercial products. You also agree that you have permission to grant this license. +* __No Warranty or Support Obligations__. By making a contribution, you are not obligating yourself to provide support for the contribution, and you are not taking on any warranty obligations or providing any assurances about how it will perform. + +The [CLA](./CLA.md) does not change the terms of the underlying license used by our software such as the Business Source License, Mozilla Public License, or MIT License. You are still free to use our projects within your own projects or businesses, republish modified source code, and more subject to the terms of the project license. Please reference the appropriate license for the project you're contributing to to learn more. diff --git a/.github/ISSUE_TEMPLATE/BUG.yml b/.github/ISSUE_TEMPLATE/BUG.yml new file mode 100644 index 00000000..49a094ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG.yml @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +name: Bug Report +description: File a bug report. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: > + **Thanks :heart: for taking the time to fill out this feature request report!** We kindly ask that you search to + see if an issue [already exists](https://github.com/Telefonica/cross-confluence-tools/issues?q=is%3Aissue+sort%3Acreated-desc+) for + your feature. + + We are also happy to accept contributions from our users. For more details read our [contributing guidelines](https://github.com/Telefonica/cross-confluence-tools/blob/main/.github/CONTRIBUTING.md). + - type: dropdown + id: package + attributes: + label: Package + description: The package where you found the bug. + options: + - child-process-manager + - confluence-sync + - markdown-confluence-sync + default: 0 + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: dropdown + id: version + attributes: + label: Version + description: What version are you using? + options: + - 1.x (Default) + default: 0 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Telefonica/cross-confluence-tools/blob/main/.github/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml new file mode 100644 index 00000000..e99fe928 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +--- +name: Feature Request +description: Suggest an idea to help us improve +title: "[Feature]: " +labels: + - "feature" +body: + - type: markdown + attributes: + value: > + **Thanks :heart: for taking the time to fill out this feature request report!** We kindly ask that you search to + see if an issue [already exists](https://github.com/Telefonica/cross-confluence-tools/issues?q=is%3Aissue+sort%3Acreated-desc+) for + your feature. + + We are also happy to accept contributions from our users. For more details read our [contributing guidelines](https://github.com/Telefonica/cross-confluence-tools/blob/main/.github/CONTRIBUTING.md). + - type: dropdown + id: package + attributes: + label: Package + description: The package where you'd like to see this feature. + options: + - child-process-manager + - confluence-sync + - markdown-confluence-sync + default: 0 + validations: + required: true + - type: textarea + attributes: + label: Description + description: | + A clear and concise description of the feature you're interested in. + value: | + + validations: + required: true + - type: textarea + attributes: + label: Suggested Solution + description: > + Describe the solution you'd like. A clear and concise description of what you want to happen. If you have + considered alternatives, please describe them. + value: | + + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Telefonica/cross-confluence-tools/blob/main/.github/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..391bd888 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +blank_issues_enabled: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..0dd9eb11 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +# [Add your title here] + +## Description + +[__Please provide enough information and context so that others can review your pull request easily__] + +## Agreement + +Please check the following boxes after you have read and understood each item. + +* [ ] I have read the [CONTRIBUTING](./.github/CONTRIBUTING.md) document +* [ ] I have read the [CODE_OF_CONDUCT](./.github/CODE_OF_CONDUCT.md) document +* [ ] By submitting this pull request, I confirm that my contribution is made under the terms of the [project's license](./LICENSE). +* [ ] I accept that, by signing the Contributor License Agreement through a comment in the PR, my Github user name will be stored by in a branch of this repository for future reference. + +In case this is your first contribution to this project, you will also have to **add a comment with the following text: "_I have read the CLA Document and I hereby sign the CLA_"**, otherwise the PR status will fail and our bot will request you to add it. Once you have signed it in a PR, you will not have to sign it again for future contributions. diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml new file mode 100644 index 00000000..0f93f09a --- /dev/null +++ b/.github/actions/install/action.yml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +name: Install and cache +description: Setup the runner environment, by installing dependencies and restoring caches + +runs: + using: composite + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'pnpm' + - name: Cache dependencies + id: cache-pnpm + uses: actions/cache@v4 + with: + path: node_modules + key: pnpm-${{ hashFiles('pnpm-lock.yaml') }} + - name: Install Node.js dependencies + shell: bash + run: pnpm install diff --git a/.github/actions/run-nx-target/action.yml b/.github/actions/run-nx-target/action.yml new file mode 100644 index 00000000..dcf1c853 --- /dev/null +++ b/.github/actions/run-nx-target/action.yml @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +name: Run Nx target +description: Setup the Nx cache, and run a target + +inputs: + target: + description: The Nx target to run + required: true + github_token: + description: GitHub token + default: '${{ github.token }}' + +runs: + using: composite + steps: + - uses: ./.github/actions/install + id: install + + # Restore the cache for the current branch in PRs + - name: Restore Nx cache + uses: actions/cache/restore@v4 + if: github.event_name == 'pull_request' + with: + path: .nx + key: nx-cache-${{ github.ref }} + # Try restoring the cache also from the PR branch, the head branch of the PR, the base branch of the PR, the release branch, and the main branch + restore-keys: | + nx-cache-refs/heads/${{ github.event.pull_request.head.ref }} + nx-cache-refs/heads/${{ github.event.pull_request.base.ref }} + nx-cache-refs/heads/release + nx-cache-refs/heads/main + nx-cache-${{ github.event.pull_request.head.ref }} + nx-cache-${{ github.event.pull_request.base.ref }} + nx-cache-release + nx-cache-main + nx-cache- + # Restore the cache for the current branch in non-PRs + - name: Restore Nx cache + uses: actions/cache/restore@v4 + if: github.event_name != 'pull_request' + with: + path: .nx + key: nx-cache-${{ github.ref }} + # Try restoring the cache also from the release branch and the main branch + restore-keys: | + nx-cache-refs/heads/release + nx-cache-refs/heads/main + nx-cache-release + nx-cache-main + nx-cache- + + # Prepare the SHAs for the base and head for the `nx affected` commands + - name: Derive appropriate SHAs for base and head for `nx affected` commands + if: github.event_name == 'pull_request' + uses: nrwl/nx-set-shas@v2 + with: + main-branch-name: ${{ github.event.pull_request.base.ref }} + # Run the affected target in PRs + - name: Run - Affected + if: github.event_name == 'pull_request' + shell: bash + run: NX_REJECT_UNKNOWN_LOCAL_CACHE=0 pnpm nx affected -t ${{ inputs.target }} --base origin/${{ github.event.pull_request.base.ref }} --head HEAD + + # Run the target in non-PRs + - name: Run + if: github.event_name != 'pull_request' + shell: bash + run: NX_REJECT_UNKNOWN_LOCAL_CACHE=0 pnpm nx run-many --target=${{ inputs.target }} --all + + # Check if the cache for the current branch exists + - name: Check Nx cache to delete + uses: actions/cache/restore@v4 + id: cache-check + with: + path: .nx + key: nx-cache-${{ github.ref }} + lookup-only: true + # Delete the cache for the current branch if it exists + - name: Delete Previous Nx Cache + if: steps.cache-check.outputs.cache-hit == 'true' + continue-on-error: true + shell: bash + run: | + gh extension install actions/gh-actions-cache + gh actions-cache delete "nx-cache-${{ github.ref }}" --confirm + env: + GH_TOKEN: ${{ inputs.github_token }} + # Save the cache for the current branch + - name: Save Nx cache + uses: actions/cache/save@v4 + with: + path: .nx + key: nx-cache-${{ github.ref }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..0481e3ef --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +name: "Check and build" +on: + push: + branches: + - release + - main + pull_request: + branches: + - release + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + actions: write + +jobs: + lint-root: + name: Lint root folder + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./.github/actions/install + - name: Lint + run: pnpm lint + - name: Check spell + run: pnpm check:spell + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Lint + uses: ./.github/actions/run-nx-target + with: + target: lint + check-spell: + needs: lint + name: Check spell + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check spell + uses: ./.github/actions/run-nx-target + with: + target: check:spell + check-types: + needs: check-spell + name: Check types + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check types + uses: ./.github/actions/run-nx-target + with: + target: check:types + test-unit: + needs: check-types + name: Test unit + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Test unit + uses: ./.github/actions/run-nx-target + with: + target: test:unit + - name: Upload coverage results + uses: actions/upload-artifact@v3 + with: + name: unit-test-coverage + path: components/*/coverage + if-no-files-found: ignore + retention-days: 1 + build: + needs: test-unit + name: Build + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Build + uses: ./.github/actions/run-nx-target + with: + target: build + test-component: + needs: build + name: Test component + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Test component + uses: ./.github/actions/run-nx-target + with: + target: test:component diff --git a/.github/workflows/contributor-license-agreement.yml b/.github/workflows/contributor-license-agreement.yml new file mode 100644 index 00000000..37ae89d8 --- /dev/null +++ b/.github/workflows/contributor-license-agreement.yml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +name: "Contributor License Agreement" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + require-contributor-license-agreement: + runs-on: ubuntu-latest + steps: + - name: "Require Contributor License Agreement" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + create-file-commit-message: 'Creating file for storing CLA Signatures' + signed-commit-message: '$contributorName has signed the CLA in $owner/$repo#$pullRequestNo' + path-to-document: 'https://github.com/Telefonica/cross-confluence-tools/blob/master/.github/CLA.md' + branch: 'cla-signatures' + # the below is the list of users who are allowed to sign the CLA without any check + # allowlist: user1,bot* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..0ce60a09 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,14 @@ +name: Publish to NPM +on: + release: + types: [published] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install + - run: pnpm nx run-many check:all --all + - run: pnpm -r publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index c6bba591..34068d34 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,9 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# MacOS +.DS_store + +# NX +.nx diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..628e04fb --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +if ! [ -x "$(command -v pnpm)" ]; then + echo 'Pnpm is not installed, skipping lint hook' >&2 + exit 0 +else + pnpm lint:staged +fi diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..41ed4e8e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "nrwl.angular-console", + "streetsidesoftware.code-spell-checker", + "dbaeumer.vscode-eslint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b18e3270 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.associations": { + "nx.json": "jsonc", + "**/project.json": "jsonc" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index 276dafa4..b4e0f489 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,31 @@ -# cross-confluence-tools -Confluence tools for developers +# Confluence tools for developers + +This repository contains a set of tools that can be used to interact with Confluence. + +They are distributed as NPM packages. Please refer to the documentation of each package for more information. + +## Main packages + +* [Markdown Confluence Sync](components/markdown-confluence-sync/README.md): A tool to __synchronize a folder containing Markdown files with Confluence pages__. Supports Mermaid diagrams and per-page configuration using [frontmatter metadata](https://jekyllrb.com/docs/front-matter/). Works great with [Docusaurus](https://docusaurus.io/). +* [Confluence Sync](components/confluence-sync/README.md): A tool that __creates/updates/deletes Confluence pages based on a list of objects containing the page contents__. Supports nested pages and attachments upload. + +## Other tools + +These tools are used by the main packages, but are also published as separate packages: + +* [child-process-manager](components/child-process-manager/README.md): A tool to manage child processes. Useful to execute shell commands from tests and check their output, for example. + +## Internal tools + +Some other components in the repository are not published, because they are used only in the development of the main packages: + +* [eslint-config](components/eslint-config/README.md): Base configuration for ESLint, enabling to extend it on each different component. +* [cspell-config](components/cspell-config/README.md): Base configuration for cspell, enabling to extend it on each different component. + +## Contributing + +Please read our [Contributing Guidelines](./.github/CONTRIBUTING.md) for details on how to contribute to this project before submitting a pull request. + +## License + +This project is licensed under the Apache-2.0 License - see the [LICENSE](./LICENSE) file for details. diff --git a/components/child-process-manager/.gitignore b/components/child-process-manager/.gitignore new file mode 100644 index 00000000..769c13ee --- /dev/null +++ b/components/child-process-manager/.gitignore @@ -0,0 +1,3 @@ +# Generated +/coverage +/dist diff --git a/components/child-process-manager/CHANGELOG.md b/components/child-process-manager/CHANGELOG.md new file mode 100644 index 00000000..fb154656 --- /dev/null +++ b/components/child-process-manager/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +#### Added +#### Changed +#### Fixed +#### Deprecated +#### Removed + +## [1.0.0-beta.1] + +### Added + +* feat: Initial release diff --git a/components/child-process-manager/CONTRIBUTING.md b/components/child-process-manager/CONTRIBUTING.md new file mode 100644 index 00000000..80a467a4 --- /dev/null +++ b/components/child-process-manager/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# How to contribute + +First of all, thank you for considering contributing to the this project! 🎉 + +# Table of contents + +- [Development](#development) + - [Installation](#installation) + - [Monorepo tool](#monorepo-tool) + - [Unit tests](#unit-tests) + - [Build](#build) + - [NPM scripts reference](#npm-scripts-reference) +- [License, Code of Conduct, and Contribution License Agreement](#license-code-of-conduct-and-contribution-license-agreement) +- [Pull requests](#pull-requests) + +## Development + +### Installation + +This repository use Pnpm as dependencies manager. So, to start working on them, you have to install the dependencies by running `pnpm install` in the root folder of the repository. + +Please refer to the monorepo README file for further information about [common requirements](../../README.md#requirements) and [installation process](../../README.md#installation). + +### Monorepo tool + +The repository uses [Nx](https://nx.dev/) as monorepo tool for managing dependencies between component tasks, so, you should run any command for this component from the repository root folder, and Nx will take care of executing the dependent commands in the right order. + +For example, a command that could be executed like this in this folder: + +```sh title="Execute unit tests of the component inside its folder" +pnpm run test:unit +``` + +> ![WARNING] No management of task dependencies +> The previous command will only execute the unit tests of the component, which may fail if the component has dependencies that are not built yet, for example. + +Should be executed like this in any folder of the repository: + +```sh title="Execute unit tests of the component, and all needed dependencies, from any folder" +pnpm nx test:unit child-process-manager +``` + +> ![TIP] Management of task dependencies +> The previous command will execute the unit tests of the component and all the dependencies needed to run them, and will take advantage of the cache to avoid unnecessary executions. + +### Unit tests + +Unit tests are executed using [Jest](https://jestjs.io/). To run them, execute the following command: + +```sh +pnpm nx test:unit child-process-manager +``` + +### Build + +This command generates the library into the `dist` directory. __Note that other components in the repository won't be able to use the library until this command is executed.__ + +```sh +pnpm nx build child-process-manager +``` + +### NPM scripts reference + +- `build` - Build the library. +- `check:all` - Run all checks, tests, and build. +- `test:unit` - Run unit tests. +- `check:spell` - Checks spelling. +- `check:types` - Checks the TypeScript types. +- `lint` - Lint the code. + +## License, Code of Conduct, and Contribution License Agreement + +By contributing to this project, you agree that your contributions will be licensed under the [LICENSE](./LICENSE) file in this folder, and that you agree to the terms described in the [CONTRIBUTING.md](../../.github/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](../../.github/CODE_OF_CONDUCT.md) files in this repository. + +## Pull requests + +Please follow the recommendations in the main [CONTRIBUTING.md](../../.github/CONTRIBUTING.md) file in this repository before submitting a pull request. diff --git a/components/child-process-manager/LICENSE b/components/child-process-manager/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/components/child-process-manager/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/components/child-process-manager/README.md b/components/child-process-manager/README.md new file mode 100644 index 00000000..3216e6bf --- /dev/null +++ b/components/child-process-manager/README.md @@ -0,0 +1,74 @@ +# child-process-manager + +Async child process manager providing access to exit code and stdout/stderr logs. + +## Table of Contents + +- [Description](#description) +- [Installation](#installation) +- [Example](#example) +- [API](./API.md) +- [Contributing](#contributing) +- [License](#license) + +## Description + +This library enables to create a child process using [`cross-spawn`](https://github.com/moxystudio/node-cross-spawn), which provides compatibility across different operating systems, and manage it asynchronously. It mainly provides: + +* Enables to manage child processes as promises. The promise is resolved with the exit code and logs when the process finishes. +* Method to kill the child process at any time. It returns a promise that is resolved when the process is killed. +* Method to get the logs of the child process at any moment. It returns an array with the stdout/stderr of the process until that moment. + +It is useful to execute shell commands from tests and check their output, for example. + +## Installation + +```bash +npm install @tid-cross/child-process-manager +``` + +## Example + +Import the library, create a child process and wait for it to finish. It will return the exit code and the array of logs in the resolved object. + +```js title="Example" +import { ChildProcessManager } from '@tid-cross/child-process-manager'; + +const childProcess = new ChildProcessManager(["echo", 'Hello world!']); + +const { logs, exitCode } = await childProcess.run(); + +console.log(logs); // ["Hello world!"] +console.log(exitCode); // 0 +``` + +## API + +### Constructor + +* __`ChildProcessManager(command, options?, spawnOptions?): ChildProcessManager`__: Creates a new child process manager with the given command and options. + * `command` - `string[]`: Command to be executed, and its arguments. + * `options` - `object` : Object containing next properties: + * `env` - `object`: Environment key-value pairs to be added to the child process. + * `silent` - `boolean`: If true, the child process will not output anything to the console (but it will still be stored in the logs). Default is `false`. + * `cwd` - `string`: Current working directory of the child process. Default is `process.cwd()`. + * `spawnOptions` - `object`: Options to be passed directly to the [`cross-spawn` library](https://github.com/moxystudio/node-cross-spawn) when creating the child process. + +### Methods + +* __`run(): Promise<{ logs: string[], exitCode: number }> `__: Runs the child process and returns a promise that resolves with the logs and exit code when the process finishes. +* __`kill(): Promise<{ logs: string[], exitCode: number | null }>`__: Kills the child process and returns a promise that resolves when the process is killed. In case the process did not start yet, it will return `null` as the exit code. + +### Properties + +* __`logs: string[]`__: Array containing the logs of the child process. It is updated in real-time as the process outputs data. +* __`exitCode: number | null`__: Exit code of the child process. It is `null` if the process did not finish yet. +* __`exitPromise`__: Promise that resolves when the child process finishes. It is resolved with the logs and exit code. It will be `undefined` if the process did not start yet. + +## Contributing + +Please read our [Contributing Guidelines](./CONTRIBUTING.md) for details on how to contribute to this project before submitting a pull request. + +## License + +This project is licensed under the Apache-2.0 License - see the [LICENSE](./LICENSE) file for details. diff --git a/components/child-process-manager/babel.config.js b/components/child-process-manager/babel.config.js new file mode 100644 index 00000000..978ec5ae --- /dev/null +++ b/components/child-process-manager/babel.config.js @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = (api) => { + const isTest = api.env("test"); + if (isTest) { + return { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], + plugins: [ + [ + "module-resolver", + { + root: ["."], + alias: { + "@src": "./src", + "@support": "./test/unit/support", + }, + }, + ], + ], + }; + } +}; diff --git a/components/child-process-manager/cspell.config.js b/components/child-process-manager/cspell.config.js new file mode 100644 index 00000000..9b25de7b --- /dev/null +++ b/components/child-process-manager/cspell.config.js @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const { createConfig } = require("../cspell-config/index.js"); + +module.exports = createConfig(); diff --git a/components/child-process-manager/eslint.config.mjs b/components/child-process-manager/eslint.config.mjs new file mode 100644 index 00000000..b5ba81a2 --- /dev/null +++ b/components/child-process-manager/eslint.config.mjs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import path from "path"; + +import { + defaultConfigWithoutTypescript, + typescriptConfig, + jestConfig, +} from "../eslint-config/index.js"; + +function componentPath() { + return path.resolve.apply(null, [import.meta.dirname, ...arguments]); +} + +export default [ + ...defaultConfigWithoutTypescript, + { + ...typescriptConfig, + settings: { + ...typescriptConfig.settings, + "import/resolver": { + ...typescriptConfig.settings["import/resolver"], + alias: { + map: [["@src", componentPath("src")]], + extensions: [".ts", ".js", ".jsx", ".json"], + }, + }, + }, + }, + jestConfig, +]; diff --git a/components/child-process-manager/jest.config.js b/components/child-process-manager/jest.config.js new file mode 100644 index 00000000..c4819fe5 --- /dev/null +++ b/components/child-process-manager/jest.config.js @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["src/**/*.ts", "!src/index.ts"], + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "/test/unit/specs/*.spec.ts", + "/test/unit/specs/**/*.test.ts", + ], + + // The test environment that will be used for testing + testEnvironment: "node", + + reporters: [ + "default", + [ + "jest-sonar", + { outputDirectory: "coverage", outputName: "TEST-junit-report.xml" }, + ], + ], +}; diff --git a/components/child-process-manager/package.json b/components/child-process-manager/package.json new file mode 100644 index 00000000..287f7bc9 --- /dev/null +++ b/components/child-process-manager/package.json @@ -0,0 +1,69 @@ +{ + "name": "@tid-cross/child-process-manager", + "description": "Async child process manager providing access to exit code and stdout/stderr", + "version": "1.0.0-beta.1", + "license": "Apache-2.0", + "author": "Telefónica Innovación Digital", + "repository": { + "type": "git", + "url": "https://github.com/Telefonica/cross-confluence-tools", + "directory": "components/child-process-manager" + }, + "homepage": "https://github.com/Telefonica/cross-confluence-tools/tree/main/components/child-process-manager", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "spawn", + "cross", + "async", + "promise", + "kill", + "stderr", + "stdout", + "logs", + "child", + "process", + "child process", + "manager", + "test tools", + "testing", + "utilities" + ], + "scripts": { + "build": "tsc", + "check:all": "echo 'All checks passed'", + "check:spell": "cspell .", + "check:types:test": "tsc --noEmit --project ./test/unit/tsconfig.json", + "check:types:lib": "tsc --noEmit", + "check:types": "npm run check:types:test && npm run check:types:lib", + "lint": "eslint .", + "test:unit": "jest --config jest.config.js" + }, + "nx": { + "includedScripts": [ + "build", + "check:all", + "check:spell", + "check:types", + "lint", + "test:unit" + ] + }, + "dependencies": { + "cross-spawn": "7.0.3", + "tree-kill": "1.2.2" + }, + "devDependencies": { + "@types/cross-spawn": "6.0.6" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "engines": { + "node": ">=18", + "pnpm": ">=8" + } +} diff --git a/components/child-process-manager/project.json b/components/child-process-manager/project.json new file mode 100644 index 00000000..e96a94f2 --- /dev/null +++ b/components/child-process-manager/project.json @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "child-process-manager", + "projectType": "library", + "tags": [ + "type:node:lib" + ], + "implicitDependencies": [ + "eslint-config", + "cspell-config" + ] +} diff --git a/components/child-process-manager/src/ChildProcessManager.ts b/components/child-process-manager/src/ChildProcessManager.ts new file mode 100644 index 00000000..6fc4f7af --- /dev/null +++ b/components/child-process-manager/src/ChildProcessManager.ts @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import crossSpawn from "cross-spawn"; +import treeKill from "tree-kill"; +import { Readable } from "stream"; + +import { Logger, log } from "./Logger"; +import type { LoggerInterface } from "./Logger.types"; +import type { + ChildProcessManagerInterface, + ChildProcessManagerConstructor, + ChildProcessManagerOptions, + ChildProcessManagerExitCode, + ChildProcessManagerResult, +} from "./types"; +import { SpawnOptions } from "child_process"; + +const ENCODING_TYPE = "utf8"; + +/** + * Class to manage child processes + */ +export const ChildProcessManager: ChildProcessManagerConstructor = class ChildProcessManager + implements ChildProcessManagerInterface +{ + private _logger: LoggerInterface; + private _command: { name: string; params: string[] }; + private _silent: boolean; + private _cwd: string; + private _env?: Record; + private _exitPromise: Promise; + private _resolveExitPromise: () => void; + private _cliProcess: ReturnType | null; + private _exitCode: ChildProcessManagerExitCode; + private _crossSpawnOptions: SpawnOptions; + + /** + * Creates a new instance of ChildProcessManager + * @param commandAndArguments Array with the command and arguments + * @param options Options to customize the process {@link ChildProcessManagerOptions} + * @param crossSpawnOptions Options to be passed to cross-spawn {@link SpawnOptions} + */ + constructor( + commandAndArguments: string[], + options: ChildProcessManagerOptions = {}, + crossSpawnOptions?: SpawnOptions, + ) { + this._command = this._getCommandToExecute(commandAndArguments); + this._silent = options.silent || false; + this._cwd = options.cwd || process.cwd(); + this._crossSpawnOptions = crossSpawnOptions || {}; + this._env = options.env; + this._logger = new Logger({ silent: this._silent }); + this._cliProcess = null; + } + + /** + * Returns the process exit promise + */ + public get exitPromise() { + return this._exitPromise; + } + + /** + * Returns the process exit code + */ + public get exitCode() { + return this._exitCode; + } + + /** + * Returns the process logs + */ + public get logs() { + return this._logger.logs; + } + + /** + * Runs the process and returns a promise that resolves when the process is finished + * @returns Promise that resolves when the process is finished + */ + public async run(): Promise { + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = () => { + resolve({ + exitCode: this._exitCode, + logs: this._logger.logs, + }); + }; + }); + + try { + this._cliProcess = crossSpawn(this._command.name, this._command.params, { + cwd: this._cwd, + env: { + ...process.env, + ...this._env, + }, + ...this._crossSpawnOptions, + }); + + const stdout = this._cliProcess.stdout as Readable; + const stderr = this._cliProcess.stderr as Readable; + + stdout.setEncoding(ENCODING_TYPE); + stderr.setEncoding(ENCODING_TYPE); + + stdout.on("data", this._logger.log); + stderr.on("data", this._logger.log); + + this._cliProcess.on("error", (error) => { + this._logger.log(error.message); + log(error); + }); + this._cliProcess.on("close", (code) => { + this._exitCode = code; + this._resolveExitPromise(); + }); + } catch (error) { + log("Error starting process"); + log(error); + this._exitCode = 1; + this._resolveExitPromise(); + } + return this._exitPromise; + } + + /** + * Kills the process and returns a promise that resolves when the process is killed + * @returns Promise that resolves when the process is killed + */ + public async kill(): Promise { + if (this._cliProcess?.pid) { + treeKill(this._cliProcess.pid); + return this._exitPromise; + } + return { + exitCode: null, + logs: [], + }; + } + + /** + * Returns the command to execute + * @param commandAndArguments Array with the command and arguments + * @returns Object with the command and arguments + */ + private _getCommandToExecute(commandAndArguments: string[]): { + name: string; + params: string[]; + } { + return { + name: commandAndArguments[0], + params: commandAndArguments.splice(1, commandAndArguments.length - 1), + }; + } +}; diff --git a/components/child-process-manager/src/ChildProcessManager.types.ts b/components/child-process-manager/src/ChildProcessManager.types.ts new file mode 100644 index 00000000..06431abe --- /dev/null +++ b/components/child-process-manager/src/ChildProcessManager.types.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Logs } from "./Logger.types"; + +/** Options for creating a child process manager */ +export interface ChildProcessManagerOptions { + /** Print process stdout while the process or running or not. Logs will be stored anyway */ + silent?: boolean; + + /** Path where to execute the process */ + cwd?: string; + + /** Environment variables to be used in the process */ + env?: Record; +} + +export type ChildProcessManagerExitCode = number | null; + +/** Result of the child process promise */ +export interface ChildProcessManagerResult { + /** Process exit code */ + exitCode: ChildProcessManagerExitCode; + /** Process logs */ + logs: Logs; +} + +/** Creates ChildProcessManager interface */ +export interface ChildProcessManagerConstructor { + /** Returns ChildProcessManager interface + * @param commandAndArguments - Terminal command and arguments, each one in one different item into the array + * @param options - Options for creating a child process manager {@link ChildProcessManagerOptions}. + * @returns Child process manager instance {@link ChildProcessManagerInterface}. + * @example const childProcessManager = new ChildProcessManager(["echo", '"Hello world!"'], { silent: true }); + */ + new ( + commandAndArguments: string[], + options?: ChildProcessManagerOptions, + ): ChildProcessManagerInterface; +} + +export interface ChildProcessManagerInterface { + /** Kills the process and returns a promise that resolves when the process is killed */ + kill(): Promise; + + /** Runs the process and returns a promise that resolves when the process is finished */ + run(): Promise; + + /** Returns the process exit promise. It may be useful in case you want to start the process, do some things, and wait for the process to finish afterwards */ + get exitPromise(): Promise; + + /** Returns the process exit code */ + get exitCode(): ChildProcessManagerExitCode; + + /** Returns the process logs */ + get logs(): Logs; +} diff --git a/components/child-process-manager/src/Logger.ts b/components/child-process-manager/src/Logger.ts new file mode 100644 index 00000000..a38a08b8 --- /dev/null +++ b/components/child-process-manager/src/Logger.ts @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + LoggerInterface, + LoggerOptions, + LoggerConstructor, + Logs, +} from "./Logger.types"; + +/** + * Log to console + * @param args - Arguments to log + */ +export function log(...args: unknown[]) { + // eslint-disable-next-line no-console + return console.log(...args); +} + +/** Logger class */ +export const Logger: LoggerConstructor = class Logger + implements LoggerInterface +{ + private _silent: boolean; + private _logs: string[]; + + /** + * Creates a new instance of Logger + * @param options Options to customize the logger {@link LoggerOptions} + */ + constructor(options: LoggerOptions) { + this._silent = options.silent || false; + this._logs = []; + + this.log = this.log.bind(this); + } + + /** + * Returns the logs array + */ + public get logs(): Logs { + return this._logs; + } + + /** + * Add a message to the logs array, and print it in case silent option is not enabled + * In case the message has multiple lines, it will be split and each line will be added to the logs array separately + * @param message - Message to log + */ + public log(message: string): void { + const cleanMessage = message.trim(); + const messages = cleanMessage.split(/[\r\n]|[\n]/gim); + if (messages.length > 1) { + messages.forEach((lineMessage) => this.log(lineMessage)); + return; + } + const messageToLog = messages[0]; + if (messageToLog.length) { + this._logs.push(messageToLog); + if (!this._silent) { + log(messageToLog); + } + } + } +}; diff --git a/components/child-process-manager/src/Logger.types.ts b/components/child-process-manager/src/Logger.types.ts new file mode 100644 index 00000000..d07b73bc --- /dev/null +++ b/components/child-process-manager/src/Logger.types.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export type Logs = string[]; + +/** Options for creating a logger */ +export interface LoggerOptions { + /** Print messages to console or not */ + silent?: boolean; +} + +/** Creates Logger interface */ +export interface LoggerConstructor { + /** Returns Logger interface + * @param options - Options for creating a logger {@link LoggerOptions}. + * @returns Logger instance {@link LoggerInstance}. + * @example const childProcessManager = new ChildProcessManager(["echo", '"Hello world!"'], { silent: true }); + */ + new (options: LoggerOptions): LoggerInterface; +} + +export interface LoggerInterface { + /** Add a message to the logs array, and print it in case silent option is not enabled */ + log(message: string): void; + + /** Returns the logs array */ + get logs(): Logs; +} diff --git a/components/child-process-manager/src/index.ts b/components/child-process-manager/src/index.ts new file mode 100644 index 00000000..a74b385d --- /dev/null +++ b/components/child-process-manager/src/index.ts @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./types"; +export * from "./ChildProcessManager"; diff --git a/components/child-process-manager/src/types.ts b/components/child-process-manager/src/types.ts new file mode 100644 index 00000000..423ee8ed --- /dev/null +++ b/components/child-process-manager/src/types.ts @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./ChildProcessManager.types"; +export * from "./Logger.types"; diff --git a/components/child-process-manager/test/unit/specs/Component.spec.ts b/components/child-process-manager/test/unit/specs/Component.spec.ts new file mode 100644 index 00000000..00b5700e --- /dev/null +++ b/components/child-process-manager/test/unit/specs/Component.spec.ts @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ChildProcessManager } from "@src/index"; + +describe("childProcessManager", () => { + describe("run method", () => { + it("should run the process and return exit code and logs", async () => { + const childProcessManager = new ChildProcessManager( + ["echo", "Hello world!"], + { + silent: true, + }, + ); + const { logs, exitCode } = await childProcessManager.run(); + + expect(logs).toEqual(["Hello world!"]); + expect(exitCode).toBe(0); + }); + + it("should print logs if silent option is not provided", async () => { + jest.spyOn(console, "log").mockImplementation(() => { + // do nothing + }); + const childProcessManager = new ChildProcessManager([ + "echo", + "Hello world!", + ]); + await childProcessManager.run(); + + // eslint-disable-next-line no-console + expect(console.log).toHaveBeenCalledWith("Hello world!"); + }); + + it("should return exit code 1 when there is an error starting the process", async () => { + // @ts-expect-error Force error passing a number as command + const childProcessManager = new ChildProcessManager([2], { + silent: true, + cwd: "foo", + }); + const { exitCode } = await childProcessManager.run(); + + expect(exitCode).toBe(1); + }); + }); + + describe("exitCode getter", () => { + it("should return 0 when process finish ok", async () => { + const childProcessManager = new ChildProcessManager( + ["echo", "Hello world!"], + { + silent: true, + }, + ); + await childProcessManager.run(); + + expect(childProcessManager.exitCode).toBe(0); + }); + + it("should return code error when process fails", async () => { + const childProcessManager = new ChildProcessManager(["foo-command"], { + silent: false, + }); + await childProcessManager.run(); + + expect(childProcessManager.exitCode).toBe(-2); + }); + + it("should return null when process is killed", async () => { + const childProcessManager = new ChildProcessManager(["sleep", "10"], { + silent: true, + }); + childProcessManager.run(); + + await childProcessManager.kill(); + + expect(childProcessManager.exitCode).toBeNull(); + }); + }); + + describe("logs getter", () => { + it("should return logs when process finish ok", async () => { + const childProcessManager = new ChildProcessManager( + ["echo", "Hello world!"], + { + silent: true, + }, + ); + await childProcessManager.run(); + + expect(childProcessManager.logs).toEqual(["Hello world!"]); + }); + + it("logs should include error message when process fails", async () => { + const childProcessManager = new ChildProcessManager(["foo-command"], { + silent: false, + }); + await childProcessManager.run(); + + expect(childProcessManager.logs).toEqual(["spawn foo-command ENOENT"]); + }); + + it("logs should be separated for each different line", async () => { + const childProcessManager = new ChildProcessManager( + ["echo", "Foo\nVar\nBaz\n"], + { + silent: false, + }, + ); + await childProcessManager.run(); + + expect(childProcessManager.logs).toEqual(["Foo", "Var", "Baz"]); + }); + + it("logs should ignore empty log lines and spaces before or after the content in each line", async () => { + const childProcessManager = new ChildProcessManager( + ["echo", "Foo\n Var \n \nBaz \n"], + { + silent: false, + }, + ); + await childProcessManager.run(); + + expect(childProcessManager.logs).toEqual(["Foo", "Var", "Baz"]); + }); + }); + + describe("exitPromise getter", () => { + it("should return a promise resolved when process finish", async () => { + const childProcessManager = new ChildProcessManager(["sleep", "1"], { + silent: true, + }); + childProcessManager.run(); + + await childProcessManager.exitPromise; + + expect(childProcessManager.exitCode).toBe(0); + }); + }); + + describe("kill method", () => { + it("should kill the child process", async () => { + const beforeStart = Date.now(); + const childProcessManager = new ChildProcessManager(["sleep", "10"], { + silent: true, + }); + childProcessManager.run(); + + await childProcessManager.kill(); + const afterStop = Date.now(); + + expect(afterStop - beforeStart).toBeLessThan(2000); + }); + + it("should do nothing when the process is not running", async () => { + const childProcessManager = new ChildProcessManager( + ["echo", "Hello world!"], + { + silent: true, + }, + ); + + const { logs, exitCode } = await childProcessManager.kill(); + + expect(logs).toEqual([]); + expect(exitCode).toBeNull(); + }); + }); +}); diff --git a/components/child-process-manager/test/unit/tsconfig.json b/components/child-process-manager/test/unit/tsconfig.json new file mode 100644 index 00000000..cabd5a36 --- /dev/null +++ b/components/child-process-manager/test/unit/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@src/*": ["../../src/*"], + "@support/*": ["./support/*"] + } + }, + "include": ["**/*"] +} diff --git a/components/child-process-manager/tsconfig.base.json b/components/child-process-manager/tsconfig.base.json new file mode 100644 index 00000000..79bcbe89 --- /dev/null +++ b/components/child-process-manager/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "skipLibCheck": true, + "rootDir": ".", + "outDir": "dist", + "declaration": true, + "target": "ES2022", + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true, + "moduleResolution": "node", + "module": "commonjs", + "useDefineForClassFields": true, + "importsNotUsedAsValues": "remove", + "forceConsistentCasingInFileNames": true, + "noUnusedParameters": true, + "isolatedModules": true, + "strictPropertyInitialization": false + } +} diff --git a/components/child-process-manager/tsconfig.json b/components/child-process-manager/tsconfig.json new file mode 100644 index 00000000..e203040e --- /dev/null +++ b/components/child-process-manager/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/components/confluence-sync/.gitignore b/components/confluence-sync/.gitignore new file mode 100644 index 00000000..769c13ee --- /dev/null +++ b/components/confluence-sync/.gitignore @@ -0,0 +1,3 @@ +# Generated +/coverage +/dist diff --git a/components/confluence-sync/CHANGELOG.md b/components/confluence-sync/CHANGELOG.md new file mode 100644 index 00000000..fb154656 --- /dev/null +++ b/components/confluence-sync/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +#### Added +#### Changed +#### Fixed +#### Deprecated +#### Removed + +## [1.0.0-beta.1] + +### Added + +* feat: Initial release diff --git a/components/confluence-sync/CONTRIBUTING.md b/components/confluence-sync/CONTRIBUTING.md new file mode 100644 index 00000000..101de7a0 --- /dev/null +++ b/components/confluence-sync/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# How to contribute + +First of all, thank you for considering contributing to the this project! 🎉 + +# Table of contents + +- [Development](#development) + - [Installation](#installation) + - [Monorepo tool](#monorepo-tool) + - [Unit tests](#unit-tests) + - [Build](#build) + - [NPM scripts reference](#npm-scripts-reference) +- [License, Code of Conduct, and Contribution License Agreement](#license-code-of-conduct-and-contribution-license-agreement) +- [Pull requests](#pull-requests) + +## Development + +### Installation + +This repository use Pnpm as dependencies manager. So, to start working on them, you have to install the dependencies by running `pnpm install` in the root folder of the repository. + +Please refer to the monorepo README file for further information about [common requirements](../../README.md#requirements) and [installation process](../../README.md#installation). + +### Monorepo tool + +The repository uses [Nx](https://nx.dev/) as monorepo tool for managing dependencies between component tasks, so, you should run any command for this component from the repository root folder, and Nx will take care of executing the dependent commands in the right order. + +For example, a command that could be executed like this in this folder: + +```sh title="Execute unit tests of the component inside its folder" +pnpm run test:unit +``` + +> ![WARNING] No management of task dependencies +> The previous command will only execute the unit tests of the component, which may fail if the component has dependencies that are not built yet, for example. + +Should be executed like this in any folder of the repository: + +```sh title="Execute unit tests of the component, and all needed dependencies, from any folder" +pnpm nx test:unit confluence-sync +``` + +> ![TIP] Management of task dependencies +> The previous command will execute the unit tests of the component and all the dependencies needed to run them, and will take advantage of the cache to avoid unnecessary executions. + +### Unit tests + +Unit tests are executed using [Jest](https://jestjs.io/). To run them, execute the following command: + +```sh +pnpm nx test:unit confluence-sync +``` + +### Build + +This command generates the library into the `dist` directory. __Note that other components in the repository won't be able to use the library until this command is executed.__ + +```sh +pnpm nx build confluence-sync +``` + +### NPM scripts reference + +- `build` - Build the library. +- `check:all` - Run all checks, tests, and build. +- `test:unit` - Run unit tests. +- `check:spell` - Checks spelling. +- `check:types` - Checks the TypeScript types. +- `lint` - Lint the code. + +## License, Code of Conduct, and Contribution License Agreement + +By contributing to this project, you agree that your contributions will be licensed under the [LICENSE](./LICENSE) file in this folder, and that you agree to the terms described in the [CONTRIBUTING.md](../../.github/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](../../.github/CODE_OF_CONDUCT.md) files in this repository. + +## Pull requests + +Please follow the recommendations in the main [CONTRIBUTING.md](../../.github/CONTRIBUTING.md) file in this repository before submitting a pull request. diff --git a/components/confluence-sync/LICENSE b/components/confluence-sync/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/components/confluence-sync/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/components/confluence-sync/README.md b/components/confluence-sync/README.md new file mode 100644 index 00000000..dcedb445 --- /dev/null +++ b/components/confluence-sync/README.md @@ -0,0 +1,245 @@ +# confluence-sync + +Creates/updates/deletes Confluence pages based on a list of objects containing the page contents. Supports nested pages and attachments upload. + +Also supports updating specific pages directly by providing their id. + +Read [Features](#features) for more information about the two sync modes available, "tree" and "flat". + +## Table of Contents + +- [Requirements](#requirements) + - [Compatibility](#compatibility) +- [Installation](#installation) +- [Example](#example) +- [Features](#features) + - [Tree mode](#tree-mode) + - [Flat mode](#flat-mode) + - [Updating specific pages](#updating-specific-pages) +- [Attachments](#attachments) +- [How to get the root page id](#how-to-get-the-root-page-id) +- [Sync modes in detail](#sync-modes-in-detail) + - [Tree](#tree-mode-1) + - [Flat](#flat-mode-1) + - [Updating specific pages](#updating-specific-pages-1) +- [API](#api) + - [`ConfluenceSyncPages`](#confluencesyncpages) + - [`sync`](#sync) +- [Contributing](#contributing) +- [License](#license) + +## Requirements + +This library requires: + +* A Confluence instance. +* The id of the Confluence space where the pages will be created. +* A personal access token to authenticate. You can create a personal access token following the instructions in the [Atlassian documentation](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/). + +### Compatibility + +> [!WARNING] +> This library has been tested only with Confluence 8.5.x. It may work with other versions, but it has not been tested. + +## Installation + +```bash +npm install @tid-cross/confluence-sync +``` + +## Example + +Import it and pass to it a list of pages to sync: + +```js title="Example" +import { ConfluenceSyncPages } from '@tid-cross/confluence-sync'; + +const confluenceSyncPages = new ConfluenceSyncPages({ + url: "https://your.confluence.com", + personalAccessToken: "*******", + spaceId: "your-space-id", + rootPageId: "12345678" + logLevel: "debug", + dryRun: false, +}); + +await confluenceSyncPages.sync([ + { + title: 'Welcome to the documentation', + content: 'This is the content of the page', + }, + { + title: 'Introduction to the documentation', + content: 'This is the content of the page', + ancestors: ['Welcome to the documentation'], + }, + { + title: 'How to get started', + content: 'This is the content of the page', + ancestors: ['Welcome to the documentation', 'Introduction to the documentation'], + attachments: { + 'image.png': '/path/to/image.png', + }, + } +]); +``` + +## Features + +It is possible to use two different sync modes, `tree` and `flat`. And it also supports updating specific pages using their id in Confluence. + +Every mode supports uploading attachments to the pages. The main differences between them are: + +### Tree mode + +In tree mode, the library receives an object defining a tree of Confluence pages, and it creates/deletes/updates the corresponding Confluence pages. All the pages are created under a __root page, which must be also provided__. + +Note that the __root page must exist before running the sync process__, and that __all pages not present in the list will be deleted__. + +* Creates Confluence pages from a list of pages with their corresponding paths, under a root page. +* Supports nested pages. +* Creates Confluence pages if they don't exist. +* Updates Confluence pages if they already exist. +* Deletes Confluence pages that are not present in the list. + +> [!WARNING] +> All pages not present in the list will be deleted. + +### Flat mode + +It is also possible to use a flat mode, where all pages will be always created under a root page, without nested levels. Differences from the tree mode are: + +* Creates Confluence pages from a list of pages __always under the root page.__ +* It __does not support nested pages__. So, all pages without id will be created/updated under the root page. So, the `ancestors` property is not supported in this mode. + +### Updating specific pages + +In flat mode, it is also possible to provide a Confluence id for each page. In this case, the library will always update the corresponding Confluence page with the id provided. __It may be under the root page or not__. + +* The page must exist in Confluence before running the sync process. +* Passing an id __is only supported in flat mode.__ + +## Attachments + +The library will create a new attachment if it doesn't exist, or delete it and create it again if it already exists. + +When defining the attachments, you can use paths relative to the `process.cwd()` or absolute paths. + +> [!NOTE] +> Deleting attachments that already exist in Confluence without uploading a new one to replace it is not supported. + +## How to get the root page id + +To get the ID of the root page, you can use the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/api-group-content/#api-api-content-get) or follow the next steps: + +* Enter to Confluence. +* Go to the page of the space where you want to create the pages. +* Click on the `...` button and select `Page information`. +* Copy the ID of the page from the URL. For example, if the URL is `https://confluence.tid.es/pages/viewpage.action?pageId=12345678`, the ID of the page is `12345678`. + +## Sync modes in detail + +To get an idea of how the library works in each mode, please read the [features](#features) section. The following sections provide more details about each mode. + +### Tree mode + +This is the default mode of the library. It creates a tree of Confluence pages under a root page, following the hierarchy provided in the list of pages. + +* The root page must exist before running the sync process. +* The library assumes that the __Confluence instance is using the default page hierarchy, where pages are organized in spaces__. It creates all the pages under a root page of the space. +* __The pages are identified by their title__. If a page with the same title already exists, it will be updated. If it doesn't exist, it will be created. +* The ancestors of each page should be ordered always from the root page to the page itself. + * For example, if you want to create a page under the page `Introduction to the documentation`, which is under the page `Welcome to the documentation`, you should provide the following list of ancestors: `['Welcome to the documentation', 'Introduction to the documentation']`. + * So, the first ancestor of each page must be the root page, if not, it will be added automatically. +* Updates Confluence pages if they already exist under the root page. +* __The pages under the root page that are not present in the list will be deleted.__ +* __Updating the root page is not supported__. + +### Flat mode + +To enable the flat mode, you have to set the `syncMode` property of the configuration object to `flat`. Differences from the tree mode are: + +* Creates Confluence pages from a list of pages __always under the root page.__ +* It __does not support nested pages__. So, all pages without id will be created/updated under the root page. The `ancestors` property is not supported in this mode. +* It supports to pass specific ids for the pages. Read [Updating specific pages](#updating-specific-pages) for more information. + +### Updating specific pages + +As mentioned in the [features](#features) section, it is possible to update specific pages by using their Confluence id. This is __only supported in flat mode__. + +> [!CAUTION] +> If all pages in the list have an id, __you should not provide a root page id__. If you provide it, the library will consider that you don't want any page under it, and, therefore, __it will delete all root page children.__ + +```js title="Example updating specific pages" +import { ConfluenceSyncPages, SyncModes } from '@tid-cross/confluence-sync'; + +const confluenceSyncPages = new ConfluenceSyncPages({ + url: "https://my.confluence.es", + personalAccessToken: "*******", + spaceId: "MY-SPACE", + logLevel: "debug", + dryRun: false, + syncMode: SyncModes.FLAT, +}); + +await confluenceSyncPages.sync([ + { + id: '12345678', + title: 'Welcome to the documentation', + content: 'This is the content of the page', + }, + { + id: '23456789', + title: 'Introduction to the documentation', + content: 'This is the content of the page', + }, + { + id: '34567890', + title: 'How to get started', + content: 'This is the content of the page', + attachments: { + 'image.png': '/path/to/image.png', + }, + } +]); +``` + +### API + +#### `ConfluenceSyncPages` + +The main class of the library. It receives a configuration object with the following properties: + +* `url`: URL of the Confluence instance. +* `personalAccessToken`: Personal access token to authenticate in Confluence. +* `spaceId`: Key of the space where the pages will be created. +* `rootPageId`: ID of the root page under the pages will be created. It only can be missing if the sync mode is `flat` and all the pages provided have an id. +* `logLevel`: One of `silly`, `debug`, `info`, `warn`, `error` or `silent`. Default is `silent`. +* `dryRun`: If `true`, the pages will not be created/updated/deleted, but the library will log the actions that would be performed. Default is `false`. +* `syncMode`: One of `tree` or `flat`. If `tree`, the pages will be created/updated/deleted under the root page, following the hierarchy provided in the list of pages. If `flat`, the sync method will update the confluence pages that correspond to the ids provided in the pages passed as input, and pages without confluence id will be created/updated under confluence root page corresponding to the rootPageId. Default is `tree`. + +When an instance is created, it expose next methods: + +#### `sync` + +This method receives a list of pages to sync, and it creates/deletes/updates the corresponding Confluence pages. All the pages are created under a root page, which must be also provided. Note that the root page must exist before running the sync process, and that all pages not present in the list will be deleted. + +The list of pages to sync is an array of objects with the following properties: + +* `id`: _(optional)_ ID of the page. Only used if the sync mode is `flat`. +* `title`: Title of the page. +* `content`: Content of the page. +* `ancestors`: List of ancestors of the page. It must be an array of page ids. **If the sync mode is `flat`, this property is not supported and any page without id will be created under the root page.** +* `attachments`: Record of images to attach to the page. The key is the name of the image, and the value is the path to the image. The image will be attached to the page with the name provided in the key. The path to the image should be absolute. + +### get `logger` + +This getter returns the [logger instance](https://github.com/mocks-server/main/tree/master/packages/logger) used internally. You may want to use it to attach listeners, write tests, etc. + +## Contributing + +Please read our [Contributing Guidelines](./CONTRIBUTING.md) for details on how to contribute to this project before submitting a pull request. + +## License + +This project is licensed under the Apache-2.0 License - see the [LICENSE](./LICENSE) file for details. diff --git a/components/confluence-sync/babel.config.js b/components/confluence-sync/babel.config.js new file mode 100644 index 00000000..978ec5ae --- /dev/null +++ b/components/confluence-sync/babel.config.js @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = (api) => { + const isTest = api.env("test"); + if (isTest) { + return { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], + plugins: [ + [ + "module-resolver", + { + root: ["."], + alias: { + "@src": "./src", + "@support": "./test/unit/support", + }, + }, + ], + ], + }; + } +}; diff --git a/components/confluence-sync/cspell.config.js b/components/confluence-sync/cspell.config.js new file mode 100644 index 00000000..9b25de7b --- /dev/null +++ b/components/confluence-sync/cspell.config.js @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const { createConfig } = require("../cspell-config/index.js"); + +module.exports = createConfig(); diff --git a/components/confluence-sync/eslint.config.mjs b/components/confluence-sync/eslint.config.mjs new file mode 100644 index 00000000..b35ce0bd --- /dev/null +++ b/components/confluence-sync/eslint.config.mjs @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import { + defaultConfigWithoutTypescript, + typescriptConfig, + jestConfig, +} from "../eslint-config/index.js"; +import path from "path"; + +function componentPath() { + return path.resolve.apply(null, [import.meta.dirname, ...arguments]); +} + +export default [ + ...defaultConfigWithoutTypescript, + { + ...typescriptConfig, + settings: { + ...typescriptConfig.settings, + "import/resolver": { + ...typescriptConfig.settings["import/resolver"], + alias: { + map: [ + ["@src", componentPath("src")], + ["@support", componentPath("test", "unit", "support")], + ], + extensions: [".ts", ".js", ".jsx", ".json"], + }, + }, + }, + }, + { + ...jestConfig, + files: [...jestConfig.files, "test/unit/support/**/*.ts"], + }, + { + files: ["test/component/**/*.spec.ts"], + rules: { + "jest/max-expects": [ + "error", + { + max: 30, + }, + ], + }, + }, + { + files: ["test/unit/**/*.spec.ts"], + rules: { + "jest/max-expects": [ + "error", + { + max: 10, + }, + ], + }, + }, +]; diff --git a/components/confluence-sync/jest.component.config.js b/components/confluence-sync/jest.component.config.js new file mode 100644 index 00000000..5909e8e5 --- /dev/null +++ b/components/confluence-sync/jest.component.config.js @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: false, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "/test/component/specs/*.spec.ts", + "/test/component/specs/**/*.test.ts", + ], + + // The test environment that will be used for testing + testEnvironment: "node", +}; diff --git a/components/confluence-sync/jest.unit.config.js b/components/confluence-sync/jest.unit.config.js new file mode 100644 index 00000000..c4819fe5 --- /dev/null +++ b/components/confluence-sync/jest.unit.config.js @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["src/**/*.ts", "!src/index.ts"], + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "/test/unit/specs/*.spec.ts", + "/test/unit/specs/**/*.test.ts", + ], + + // The test environment that will be used for testing + testEnvironment: "node", + + reporters: [ + "default", + [ + "jest-sonar", + { outputDirectory: "coverage", outputName: "TEST-junit-report.xml" }, + ], + ], +}; diff --git a/components/confluence-sync/mocks.config.js b/components/confluence-sync/mocks.config.js new file mode 100644 index 00000000..645b271f --- /dev/null +++ b/components/confluence-sync/mocks.config.js @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +// For a detailed explanation regarding each configuration property, visit: +// https://www.mocks-server.org/docs/configuration/how-to-change-settings +// https://www.mocks-server.org/docs/configuration/options + +/** @type {import('@mocks-server/core').Configuration} */ + +module.exports = { + mock: { + collections: { + // Selected collection + selected: "base", + }, + }, + files: { + babelRegister: { + // Load @babel/register + enabled: true, + // Options for @babel/register + options: { + configFile: false, + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], + }, + }, + }, +}; diff --git a/components/confluence-sync/mocks/collections.ts b/components/confluence-sync/mocks/collections.ts new file mode 100644 index 00000000..c6ee4385 --- /dev/null +++ b/components/confluence-sync/mocks/collections.ts @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CollectionDefinition } from "@mocks-server/core"; + +const collection: CollectionDefinition[] = [ + { + id: "base", + routes: ["spy-get-requests:send", "spy-reset-requests:reset"], + }, + { + id: "empty-root", + from: "base", + routes: [ + "confluence-get-page:empty-root", + "confluence-create-page:empty-root", + // "confluence-update-page:success", + // "confluence-delete-page:success", + ], + }, + { + id: "default-root", + from: "base", + routes: [ + "confluence-get-page:default-root", + "confluence-create-page:default-root", + "confluence-update-page:default-root", + "confluence-delete-page:default-root", + "confluence-get-attachments:default-root", + "confluence-create-attachments:default-root", + ], + }, + { + id: "hierarchical-empty-root", + from: "base", + routes: [ + "confluence-get-page:hierarchical-empty-root", + "confluence-create-page:hierarchical-empty-root", + // "confluence-update-page:hierarchical-empty-root", + // "confluence-delete-page:hierarchical-empty-root", + ], + }, + { + id: "hierarchical-default-root", + from: "base", + routes: [ + "confluence-get-page:hierarchical-default-root", + "confluence-create-page:hierarchical-default-root", + "confluence-update-page:hierarchical-default-root", + "confluence-delete-page:hierarchical-default-root", + ], + }, + { + id: "flat-mode", + from: "base", + routes: [ + "confluence-get-page:flat-mode", + "confluence-create-page:flat-mode", + "confluence-update-page:flat-mode", + "confluence-delete-page:flat-mode", + "confluence-get-attachments:flat-mode", + ], + }, + { + id: "renamed-page", + from: "base", + routes: [ + "confluence-get-page:renamed-page", + "confluence-create-page:renamed-page", + "confluence-delete-page:renamed-page", + "confluence-get-attachments:renamed-page", + ], + }, +]; + +export default collection; diff --git a/components/confluence-sync/mocks/routes/Confluence.ts b/components/confluence-sync/mocks/routes/Confluence.ts new file mode 100644 index 00000000..5af1055e --- /dev/null +++ b/components/confluence-sync/mocks/routes/Confluence.ts @@ -0,0 +1,437 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + NextFunction, + RouteDefinition, + ScopedCoreInterface, + Request as ServerRequest, + Response as ServerResponse, +} from "@mocks-server/core"; + +import { + PAGES_DEFAULT_ROOT_CREATE, + PAGES_DEFAULT_ROOT_DELETE, + PAGES_DEFAULT_ROOT_GET, + PAGES_DEFAULT_ROOT_UPDATE, + PAGES_EMPTY_ROOT, + ATTACHMENTS_DEFAULT_ROOT, + PAGES_FLAT_MODE, + ATTACHMENTS_FLAT_MODE, + RENAMED_PAGE, + RENAMED_PAGE_CREATE, + RENAMED_PAGE_ATTACHMENTS, +} from "../support/fixtures/ConfluencePages"; +import { + PAGES_DEFAULT_ROOT_CREATE as PAGES_DEFAULT_ROOT_CREATE_HIERARCHICAL, + PAGES_DEFAULT_ROOT_DELETE as PAGES_DEFAULT_ROOT_DELETE_HIERARCHICAL, + PAGES_DEFAULT_ROOT_GET as PAGES_DEFAULT_ROOT_GET_HIERARCHICAL, + PAGES_DEFAULT_ROOT_UPDATE as PAGES_DEFAULT_ROOT_UPDATE_HIERARCHICAL, + PAGES_EMPTY_ROOT as PAGES_EMPTY_ROOT_HIERARCHICAL, +} from "../support/fixtures/HierarchicalConfluencePages"; +import { addRequest } from "../support/SpyStorage"; + +function getPageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Requested page with id ${req.params.pageId} to Confluence`, + ); + + addRequest("confluence-get-page", req); + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + const pageData = { + id: page.id, + title: page.title, + content: "", + version: { number: 1 }, + ancestors: page.ancestors, + children: page.children, + }; + core.logger.info(`Sending page ${JSON.stringify(pageData)}`); + res.status(200).json(pageData); + } else { + core.logger.error( + `Page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + +function createPageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Creating page in Confluence: ${JSON.stringify(req.body)}`, + ); + + addRequest("confluence-create-page", req); + + const page = pages.find( + (pageCandidate) => pageCandidate.title === req.body.title, + ); + if (page) { + res.status(200).json({ + title: req.body.title, + id: page.id, + version: { number: 1 }, + ancestors: page.ancestors, + }); + } else { + core.logger.error( + `Bad request creating page in Confluence: ${JSON.stringify(req.body)}`, + ); + core.logger.error(`Available pages: ${JSON.stringify(pages)}`); + res.status(400).send(); + } + }; +} + +function updatePageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Updating page in Confluence: ${JSON.stringify(req.body)}`, + ); + + addRequest("confluence-update-page", req); + + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + res.status(200).json({ + title: req.body.title, + id: page.id, + version: { number: 1 }, + ancestors: page.ancestors, + }); + } else { + core.logger.error( + `Bad request creating page in Confluence: ${JSON.stringify(req.body)}`, + ); + core.logger.error(`Available pages: ${JSON.stringify(pages)}`); + res.status(400).send(); + } + }; +} + +function deletePageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Deleting page in Confluence: ${JSON.stringify(req.params.pageId)}`, + ); + + addRequest("confluence-delete-page", req); + + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + res.status(204).send(); + } else { + core.logger.error( + `Page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + +function getAttachmentsMiddleware(attachments) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Requested attachments for page with id ${req.params.pageId} to Confluence`, + ); + + addRequest("confluence-get-attachments", req); + const pageAttachments = attachments.find( + (attachment) => attachment.id === req.params.pageId, + ); + if (pageAttachments) { + core.logger.info( + `Sending attachments ${JSON.stringify(pageAttachments)}`, + ); + res.status(200).json(pageAttachments.attachments); + } else { + core.logger.error( + `Attachments for page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + +function createAttachmentsMiddleware(attachments) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Creating attachments for page with id ${req.params.pageId} in Confluence: ${JSON.stringify( + req.body, + )}`, + ); + + addRequest("confluence-create-attachments", req); + + const attachmentsResponse = attachments.find( + (attachment) => attachment.id === req.params.pageId, + ); + if (attachmentsResponse) { + res.status(200).send(); + } else { + core.logger.error( + `Bad request creating attachments for page with id ${ + req.params.pageId + } in Confluence: ${JSON.stringify(req.body)}`, + ); + core.logger.error( + `Available attachments: ${JSON.stringify(attachments, null, 2)}`, + ); + res.status(400).send(); + } + }; +} + +const confluenceRoutes: RouteDefinition[] = [ + { + id: "confluence-get-page", + url: "/rest/api/content/:pageId", + method: "GET", + variants: [ + { + id: "empty-root", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_EMPTY_ROOT), + }, + }, + { + id: "default-root", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_DEFAULT_ROOT_GET), + }, + }, + { + id: "hierarchical-empty-root", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_EMPTY_ROOT_HIERARCHICAL), + }, + }, + { + id: "hierarchical-default-root", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_DEFAULT_ROOT_GET_HIERARCHICAL), + }, + }, + { + id: "flat-mode", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_FLAT_MODE), + }, + }, + { + id: "renamed-page", + type: "middleware", + options: { + middleware: getPageMiddleware(RENAMED_PAGE), + }, + }, + ], + }, + { + id: "confluence-create-page", + url: "/rest/api/content", + method: "POST", + variants: [ + { + id: "empty-root", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_EMPTY_ROOT), + }, + }, + { + id: "default-root", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_DEFAULT_ROOT_CREATE), + }, + }, + { + id: "hierarchical-empty-root", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_EMPTY_ROOT_HIERARCHICAL), + }, + }, + { + id: "hierarchical-default-root", + type: "middleware", + options: { + middleware: createPageMiddleware( + PAGES_DEFAULT_ROOT_CREATE_HIERARCHICAL, + ), + }, + }, + { + id: "flat-mode", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_FLAT_MODE), + }, + }, + { + id: "renamed-page", + type: "middleware", + options: { + middleware: createPageMiddleware(RENAMED_PAGE_CREATE), + }, + }, + ], + }, + { + id: "confluence-update-page", + url: "/rest/api/content/:pageId", + method: "PUT", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: updatePageMiddleware(PAGES_DEFAULT_ROOT_UPDATE), + }, + }, + { + id: "hierarchical-default-root", + type: "middleware", + options: { + middleware: updatePageMiddleware( + PAGES_DEFAULT_ROOT_UPDATE_HIERARCHICAL, + ), + }, + }, + { + id: "flat-mode", + type: "middleware", + options: { + middleware: updatePageMiddleware(PAGES_FLAT_MODE), + }, + }, + ], + }, + { + id: "confluence-delete-page", + url: "/rest/api/content/:pageId", + method: "DELETE", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: deletePageMiddleware(PAGES_DEFAULT_ROOT_DELETE), + }, + }, + { + id: "hierarchical-default-root", + type: "middleware", + options: { + middleware: deletePageMiddleware( + PAGES_DEFAULT_ROOT_DELETE_HIERARCHICAL, + ), + }, + }, + { + id: "flat-mode", + type: "middleware", + options: { + middleware: deletePageMiddleware(PAGES_FLAT_MODE), + }, + }, + { + id: "renamed-page", + type: "middleware", + options: { + middleware: deletePageMiddleware(RENAMED_PAGE), + }, + }, + ], + }, + { + id: "confluence-get-attachments", + url: "/rest/api/content/:pageId/child/attachment", + method: "GET", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: getAttachmentsMiddleware(ATTACHMENTS_DEFAULT_ROOT), + }, + }, + { + id: "flat-mode", + type: "middleware", + options: { + middleware: getAttachmentsMiddleware(ATTACHMENTS_FLAT_MODE), + }, + }, + { + id: "renamed-page", + type: "middleware", + options: { + middleware: getAttachmentsMiddleware(RENAMED_PAGE_ATTACHMENTS), + }, + }, + ], + }, + { + id: "confluence-create-attachments", + url: "/rest/api/content/:pageId/child/attachment", + method: "POST", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: createAttachmentsMiddleware(ATTACHMENTS_DEFAULT_ROOT), + }, + }, + ], + }, +]; + +export default confluenceRoutes; diff --git a/components/confluence-sync/mocks/routes/Spy.ts b/components/confluence-sync/mocks/routes/Spy.ts new file mode 100644 index 00000000..e6766588 --- /dev/null +++ b/components/confluence-sync/mocks/routes/Spy.ts @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + Request as ServerRequest, + Response as ServerResponse, + RouteDefinition, +} from "@mocks-server/core"; + +import { getRequests, resetRequests } from "../support/SpyStorage"; + +const spyRoutes: RouteDefinition[] = [ + { + id: "spy-get-requests", + url: "/spy/requests", + method: "GET", + variants: [ + { + id: "send", + type: "middleware", + options: { + middleware: (_req: ServerRequest, res: ServerResponse) => { + res.status(200).send(getRequests()); + }, + }, + }, + ], + }, + { + id: "spy-reset-requests", + url: "/spy/requests", + method: "DELETE", + variants: [ + { + id: "reset", + type: "middleware", + options: { + middleware: (_req: ServerRequest, res: ServerResponse) => { + resetRequests(); + res.status(200).send({ reset: true }); + }, + }, + }, + ], + }, +]; + +export default spyRoutes; diff --git a/components/confluence-sync/mocks/support/SpyStorage.ts b/components/confluence-sync/mocks/support/SpyStorage.ts new file mode 100644 index 00000000..b2fbd8ba --- /dev/null +++ b/components/confluence-sync/mocks/support/SpyStorage.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Request as ServerRequest } from "@mocks-server/core"; + +import type { SpyRequest } from "./SpyStorage.types"; + +let requests: SpyRequest[] = []; + +export function addRequest(routeId: string, request: ServerRequest) { + requests.push({ + routeId, + url: request.url, + method: request.method, + headers: request.headers, + body: request.body, + params: request.params, + }); +} + +export function getRequests(): SpyRequest[] { + return requests; +} + +export function resetRequests(): void { + requests = []; +} diff --git a/components/confluence-sync/mocks/support/SpyStorage.types.ts b/components/confluence-sync/mocks/support/SpyStorage.types.ts new file mode 100644 index 00000000..005f579a --- /dev/null +++ b/components/confluence-sync/mocks/support/SpyStorage.types.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +/** Request to the mock server */ +export interface SpyRequest { + /** Route ID */ + routeId: string; + /** Request URL */ + url: string; + /** Request method */ + method: string; + /** Request headers */ + headers: Record; + /** Request body */ + body?: Record; + /** Request query params */ + params?: Record; +} diff --git a/components/confluence-sync/mocks/support/fixtures/ConfluencePages.ts b/components/confluence-sync/mocks/support/fixtures/ConfluencePages.ts new file mode 100644 index 00000000..24413542 --- /dev/null +++ b/components/confluence-sync/mocks/support/fixtures/ConfluencePages.ts @@ -0,0 +1,451 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export const PAGES_EMPTY_ROOT = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-child2-id", + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, + { + id: "foo-grandChild3-id", + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "foo-child2-title" }, + ], + }, + { + id: "foo-grandChild4-id", + title: "foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "foo-child2-title" }, + ], + }, + { + id: "foo-child3-id", + title: "foo-child3-title", + content: "foo-child3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild5-id", + title: "foo-grandChild5-title", + content: "foo-grandChild5-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child3-id", title: "foo-child3-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_GET = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: { + page: { results: [{ id: "foo-parent-id", title: "foo-parent-title" }] }, + }, + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + children: { + page: { results: [{ id: "foo-child1-id", title: "foo-child1-title" }] }, + }, + }, + { + id: "foo-child1-id", + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + children: { + page: { + results: [ + { id: "foo-grandChild1-id", title: "foo-grandChild1-title" }, + { id: "foo-grandChild2-id", title: "foo-grandChild2-title" }, + ], + }, + }, + }, + { + id: "foo-grandChild1-id", + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_CREATE = [ + { + id: "foo-child2-id", + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-child3-id", + title: "foo-child3-title", + content: "foo-child3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild3-id", + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "foo-child2-title" }, + ], + }, + { + id: "foo-grandChild4-id", + title: "foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "foo-child2-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_UPDATE = [ + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + version: { number: 2 }, + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "foo-child1-title", + content: "foo-child1-content", + version: { number: 2 }, + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + version: { number: 2 }, + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_DELETE = [ + { + id: "foo-grandChild1-id", + title: "foo-grandChild1-title", + }, + { + id: "foo-grandChild2-id", + title: "foo-grandChild2-title", + }, + { + id: "foo-grandChild1-attachment-id", + title: "foo-grandChild1-attachment-title", + }, +]; + +export const ATTACHMENTS_DEFAULT_ROOT = [ + { + id: "foo-parent-id", + attachments: { + results: [], + }, + }, + { + id: "foo-child1-id", + attachments: { + results: [], + }, + }, + { + id: "foo-grandChild1-id", + attachments: { + results: [ + { + id: "foo-grandChild1-attachment-id", + title: "foo-grandChild1-attachment-title", + }, + ], + }, + }, + { + id: "foo-grandChild3-id", + attachments: { + results: [], + }, + }, +]; + +export const PAGES_FLAT_MODE = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + version: { number: 1 }, + children: { + page: { + results: [ + { id: "foo-page-1-id", title: "foo-page-1-title" }, + { id: "foo-page-2-id", title: "foo-page-2-title" }, + ], + }, + }, + }, + { + id: "foo-page-1-id", + title: "foo-page-1-title", + content: "foo-page-1-content", + version: { number: 1 }, + }, + { + id: "foo-page-2-id", + title: "foo-page-2-title", + content: "foo-page-2-content", + version: { number: 1 }, + }, + { + id: "foo-page-3-id", + title: "foo-page-3-title", + content: "foo-page-3-content", + version: { number: 1 }, + }, +]; + +export const ATTACHMENTS_FLAT_MODE = [ + { + id: "foo-page-1-id", + attachments: { + results: [], + }, + }, + { + id: "foo-page-2-id", + attachments: { + results: [], + }, + }, + { + id: "foo-page-3-id", + attachments: { + results: [], + }, + }, +]; + +export const RENAMED_PAGE = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: { + page: { results: [{ id: "foo-parent-id", title: "foo-parent-title" }] }, + }, + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + children: { + page: { results: [{ id: "foo-child1-id", title: "foo-child1-title" }] }, + }, + }, + { + id: "foo-child1-id", + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + children: { + page: { + results: [ + { id: "foo-grandChild1-id", title: "foo-grandChild1-title" }, + { id: "foo-grandChild2-id", title: "foo-grandChild2-title" }, + ], + }, + }, + }, + { + id: "foo-grandChild1-id", + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, +]; + +export const RENAMED_PAGE_CREATE = [ + { + id: "foo-renamed-id", + title: "foo-renamed-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-renamed-id", title: "foo-renamed-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-renamed-id", title: "foo-renamed-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-renamed-id", title: "foo-renamed-title" }, + { id: "foo-child1-id", title: "foo-child1-title" }, + ], + }, +]; + +export const RENAMED_PAGE_ATTACHMENTS = [ + { + id: "foo-renamed-id", + attachments: { + results: [], + }, + }, + { + id: "foo-child1-id", + attachments: { + results: [], + }, + }, + { + id: "foo-grandChild1-id", + attachments: { + results: [], + }, + }, + { + id: "foo-grandChild2-id", + attachments: { + results: [], + }, + }, +]; diff --git a/components/confluence-sync/mocks/support/fixtures/HierarchicalConfluencePages.ts b/components/confluence-sync/mocks/support/fixtures/HierarchicalConfluencePages.ts new file mode 100644 index 00000000..02926b67 --- /dev/null +++ b/components/confluence-sync/mocks/support/fixtures/HierarchicalConfluencePages.ts @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export const PAGES_EMPTY_ROOT = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-child2-id", + title: "[foo-parent-title] foo-child2-title", + content: "foo-child2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + { + id: "foo-grandChild3-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, + { + id: "foo-grandChild4-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, + { + id: "foo-child3-id", + title: "[foo-parent-title] foo-child3-title", + content: "foo-child3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild5-id", + title: "[foo-parent-title][foo-child3-title] foo-grandChild5-title", + content: "foo-grandChild5-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child3-id", title: "[foo-parent-title] foo-child3-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_GET = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: { + page: { results: [{ id: "foo-parent-id", title: "foo-parent-title" }] }, + }, + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + children: { + page: { + results: [ + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + }, + }, + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + children: { + page: { + results: [ + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + }, + ], + }, + }, + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_CREATE = [ + { + id: "foo-child2-id", + title: "[foo-parent-title] foo-child2-title", + content: "foo-child2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-child3-id", + title: "[foo-parent-title] foo-child3-title", + content: "foo-child3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild3-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, + { + id: "foo-grandChild4-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_UPDATE = [ + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + version: { number: 2 }, + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + content: "foo-child1-content", + version: { number: 2 }, + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + content: "foo-grandChild1-content", + version: { number: 2 }, + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_DELETE = [ + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + }, +]; diff --git a/components/confluence-sync/mocks/tsconfig.json b/components/confluence-sync/mocks/tsconfig.json new file mode 100644 index 00000000..6beff1fb --- /dev/null +++ b/components/confluence-sync/mocks/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["**/*"] +} diff --git a/components/confluence-sync/package.json b/components/confluence-sync/package.json new file mode 100644 index 00000000..65ad2c3f --- /dev/null +++ b/components/confluence-sync/package.json @@ -0,0 +1,83 @@ +{ + "name": "@tid-cross/confluence-sync", + "description": "Creates/updates/deletes Confluence pages based on a list of objects containing the page contents. Supports nested pages and attachments upload", + "version": "1.0.0-beta.1", + "license": "Apache-2.0", + "author": "Telefónica Innovación Digital", + "repository": { + "type": "git", + "url": "https://github.com/Telefonica/cross-confluence-tools", + "directory": "components/confluence-sync" + }, + "homepage": "https://github.com/Telefonica/cross-confluence-tools/tree/main/components/confluence-sync", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "confluence", + "html", + "content", + "page", + "pages", + "tree", + "sync", + "update", + "create", + "delete", + "cli", + "node", + "json", + "attachments", + "images" + ], + "scripts": { + "build": "tsc", + "check:all": "echo 'All checks passed'", + "check:spell": "cspell .", + "check:types": "npm run check:types:test:unit && npm run check:types:test:component && npm run check:types:lib", + "check:types:lib": "tsc --noEmit", + "check:types:test:component": "tsc --noEmit --project ./test/component/tsconfig.json", + "check:types:test:unit": "tsc --noEmit --project ./test/unit/tsconfig.json", + "confluence:mock": "mocks-server", + "confluence:mock:ci": "mocks-server --no-plugins.inquirerCli.enabled", + "lint": "eslint .", + "test:component": "start-server-and-test confluence:mock:ci http-get://127.0.0.1:3110/api/about test:component:run", + "test:component:run": "jest --config jest.component.config.js --runInBand", + "test:unit": "jest --config jest.unit.config.js" + }, + "nx": { + "includedScripts": [ + "build", + "check:all", + "check:spell", + "check:types", + "lint", + "test:unit", + "test:component" + ] + }, + "dependencies": { + "@mocks-server/logger": "2.0.0-beta.2", + "axios": "1.6.7", + "confluence.js": "1.7.4", + "fastq": "1.17.1" + }, + "devDependencies": { + "@mocks-server/admin-api-client": "8.0.0-beta.2", + "@mocks-server/core": "5.0.0-beta.3", + "@mocks-server/main": "5.0.0-beta.4", + "@types/tmp": "0.2.6", + "cross-fetch": "4.0.0", + "start-server-and-test": "2.0.8", + "tmp": "0.2.3" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "engines": { + "node": ">=18", + "pnpm": ">=8" + } +} diff --git a/components/confluence-sync/project.json b/components/confluence-sync/project.json new file mode 100644 index 00000000..5654f592 --- /dev/null +++ b/components/confluence-sync/project.json @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "confluence-sync", + "projectType": "library", + "tags": [ + "type:node:lib" + ], + "implicitDependencies": [ + "eslint-config", + "cspell-config" + ] +} diff --git a/components/confluence-sync/src/ConfluenceSyncPages.ts b/components/confluence-sync/src/ConfluenceSyncPages.ts new file mode 100644 index 00000000..cee44a9c --- /dev/null +++ b/components/confluence-sync/src/ConfluenceSyncPages.ts @@ -0,0 +1,412 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { readFile } from "node:fs/promises"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import type { queueAsPromised } from "fastq"; +import { promise } from "fastq"; + +import { CustomConfluenceClient } from "./confluence/CustomConfluenceClient"; +import type { + ConfluenceInputPage, + ConfluenceSyncPagesConfig, + ConfluenceSyncPagesConstructor, + ConfluenceSyncPagesCreateAttachmentsTask, + ConfluenceSyncPagesCreateTask, + ConfluenceSyncPagesDeleteAttachmentsTask, + ConfluenceSyncPagesDeleteTask, + ConfluenceSyncPagesInitSyncTask, + ConfluenceSyncPagesInterface, + ConfluenceSyncPagesJob, + ConfluenceSyncPagesTask, + ConfluenceSyncPagesUpdateTask, +} from "./ConfluenceSyncPages.types"; +import { SyncModes } from "./ConfluenceSyncPages.types"; +import { CompoundError } from "./errors/CompoundError"; +import { NotAncestorsExpectedValidationError } from "./errors/NotAncestorsExpectedValidationError"; +import { PendingPagesToSyncError } from "./errors/PendingPagesToSyncError"; +import { RootPageRequiredException } from "./errors/RootPageRequiredException"; +import { getPagesTitles } from "./support/Pages"; +import type { + ConfluenceClientInterface, + ConfluencePage, + ConfluencePageBasicInfo, +} from "./types"; + +const LOGGER_NAMESPACE = "confluence-sync-pages"; +const DEFAULT_LOG_LEVEL = "silent"; + +export const ConfluenceSyncPages: ConfluenceSyncPagesConstructor = class ConfluenceSyncPages + implements ConfluenceSyncPagesInterface +{ + private _logger: LoggerInterface; + private _confluenceClient: ConfluenceClientInterface; + private _rootPageId: string | undefined; + private _syncMode: SyncModes; + + constructor({ + logLevel, + url, + spaceId, + personalAccessToken, + dryRun, + syncMode, + rootPageId, + }: ConfluenceSyncPagesConfig) { + this._logger = new Logger(LOGGER_NAMESPACE, { + level: logLevel ?? DEFAULT_LOG_LEVEL, + }); + this._confluenceClient = new CustomConfluenceClient({ + url, + spaceId, + personalAccessToken, + logger: this._logger.namespace("confluence"), + dryRun, + }); + this._syncMode = syncMode ?? SyncModes.TREE; + if (this._syncMode === SyncModes.TREE && rootPageId === undefined) { + throw new RootPageRequiredException(SyncModes.TREE); + } + this._rootPageId = rootPageId; + } + + public get logger(): LoggerInterface { + return this._logger; + } + + public async sync(pages: ConfluenceInputPage[]): Promise { + const syncMode = this._syncMode; + const confluencePages = new Map(); + const pagesMap = new Map(pages.map((page) => [page.title, page])); + const tasksDone = new Array(); + const errors = new Array(); + const queue: queueAsPromised = promise( + this._handleTask.bind(this), + 1, + ); + function enqueueTask(task: ConfluenceSyncPagesTask) { + queue.push({ + task, + enqueueTask, + getConfluencePageByTitle, + getPageByTitle, + getPagesByAncestor, + storeConfluencePage, + }); + } + function getConfluencePageByTitle(pageTitle: string) { + return confluencePages.get(pageTitle); + } + function storeConfluencePage(page: ConfluencePage) { + confluencePages.set(page.title, page); + } + function getPageByTitle(pageTitle: string) { + return pagesMap.get(pageTitle); + } + function getPagesByAncestor(ancestor: string, isRoot = false) { + if (syncMode === SyncModes.FLAT) { + // NOTE: in flat mode all pages without id are considered + // children of root page. Otherwise, they are pages with mirror + // page in Confluence and have no ancestors. + if (isRoot) { + return pages.filter((page) => page.id === undefined); + } + return []; + } + if (isRoot) { + return pages + .filter( + (page) => + page.ancestors === undefined || page.ancestors.length === 0, + ) + .concat(pages.filter((page) => page.ancestors?.at(-1) === ancestor)); + } + return pages.filter((page) => page.ancestors?.at(-1) === ancestor); + } + queue.error((e, job) => { + if (e) { + errors.push(e); + return; + } + const task = job.task; + tasksDone.push(task); + }); + if (this._syncMode === SyncModes.FLAT) { + this._validateFlatMode(pages); + pages + .filter((page) => page.id !== undefined) + .forEach((page) => { + enqueueTask({ type: "update", pageId: page.id as string, page }); + }); + } + if (this._rootPageId !== undefined) + enqueueTask({ type: "init", pageId: this._rootPageId }); + await queue.drained(); + if (errors.length > 0) { + const e = new CompoundError(...errors); + this._logger.error(`Error occurs during sync: ${e}`); + throw e; + } + this._assertPendingPagesToCreate(tasksDone, pages); + this._reportTasks(tasksDone); + } + + private _validateFlatMode(pages: ConfluenceInputPage[]): void { + const pagesWithoutId = pages.filter((page) => page.id === undefined); + if (pagesWithoutId.length > 0 && this._rootPageId === undefined) { + throw new RootPageRequiredException(SyncModes.FLAT, pagesWithoutId); + } + const pagesWithAncestors = pages.filter( + (page) => page.ancestors !== undefined && page.ancestors.length > 0, + ); + if (pagesWithAncestors.length > 0) { + throw new NotAncestorsExpectedValidationError(pagesWithAncestors); + } + } + + private _assertPendingPagesToCreate( + tasksDone: ConfluenceSyncPagesTask[], + _pages: ConfluenceInputPage[], + ) { + const createdOrUpdatedPages = tasksDone + .filter< + ConfluenceSyncPagesCreateTask | ConfluenceSyncPagesUpdateTask + >((task): task is ConfluenceSyncPagesCreateTask | ConfluenceSyncPagesUpdateTask => task.type === "create" || task.type === "update") + .map((task) => task.page.title); + const pendingPagesToCreate = _pages.filter( + (page) => !createdOrUpdatedPages.includes(page.title), + ); + if (pendingPagesToCreate.length > 0) { + const e = new PendingPagesToSyncError(pendingPagesToCreate); + this._logger.error(e.message); + throw e; + } + } + + private _reportTasks(tasks: ConfluenceSyncPagesTask[]): void { + const createdPages = tasks.filter( + (task): task is ConfluenceSyncPagesCreateTask => task.type === "create", + ); + this._logger.debug(`Created pages: ${createdPages.length} + ${createdPages.map((task) => `+ ${task.page.title}`).join("\n")}`); + const updatedPages = tasks.filter( + (task): task is ConfluenceSyncPagesUpdateTask => task.type === "update", + ); + this._logger.debug(`Updated pages: ${updatedPages.length} + ${updatedPages.map((task) => `⟳ #${task.pageId} ${task.page.title}`).join("\n")}`); + const deletedPages = tasks.filter( + (task): task is ConfluenceSyncPagesDeleteTask => task.type === "delete", + ); + this._logger.debug(`Deleted pages: ${deletedPages.length} + ${deletedPages.map((task) => `- #${task.pageId}`).join("\n")}`); + this._logger.info("Sync finished"); + } + + private _handleTask(job: ConfluenceSyncPagesJob): Promise { + const task = job.task; + switch (task.type) { + case "init": + return this._initSync({ ...job, task }); + case "create": + return this._createPage({ ...job, task }); + case "update": + return this._updatePage({ ...job, task }); + case "delete": + return this._deletePage({ ...job, task }); + case "createAttachments": + return this._createAttachments({ ...job, task }); + case "deleteAttachments": + return this._deleteAttachments({ ...job, task }); + } + } + + private async _initSync( + job: ConfluenceSyncPagesJob, + ): Promise { + this._logger.debug(`Reading page ${job.task.pageId}`); + const confluencePage = await this._confluenceClient.getPage( + job.task.pageId, + ); + job.storeConfluencePage(confluencePage); + this._enqueueChildrenPages(job, confluencePage, true); + } + + private async _createPage( + job: ConfluenceSyncPagesJob, + ): Promise { + this._logger.debug(`Creating page ${JSON.stringify(job.task.page)}`); + const ancestors: ConfluencePageBasicInfo[] | undefined = + job.task.page.ancestors?.map((ancestorTitle) => { + const ancestor = job.getConfluencePageByTitle(ancestorTitle); + // NOTE: This should never happen. Defensively check anyway. + // istanbul ignore next + if (ancestor === undefined) { + throw new Error(`Could not find ancestor ${ancestorTitle}`); + } + return { id: ancestor.id, title: ancestor.title }; + }); + const confluencePage = await this._confluenceClient.createPage({ + ...job.task.page, + ancestors, + }); + job.storeConfluencePage(confluencePage); + if ( + job.task.page.attachments !== undefined && + Object.entries(job.task.page.attachments).length > 0 + ) + job.enqueueTask({ + type: "createAttachments", + pageId: confluencePage.id, + pageTitle: confluencePage.title, + attachments: job.task.page.attachments, + }); + const descendants = job.getPagesByAncestor(job.task.page.title); + for (const descendant of descendants) { + job.enqueueTask({ type: "create", page: descendant }); + } + } + + private async _updatePage( + job: ConfluenceSyncPagesJob, + ): Promise { + this._logger.debug(`Updating page ${job.task.page.title}`); + const confluencePage = await this._confluenceClient.getPage( + job.task.pageId, + ); + const updatedConfluencePage = await this._confluenceClient.updatePage({ + id: confluencePage.id, + title: job.task.page.title, + content: job.task.page.content, + ancestors: confluencePage.ancestors, + version: confluencePage.version + 1, + }); + job.storeConfluencePage(updatedConfluencePage); + const attachments = await this._confluenceClient.getAttachments( + confluencePage.id, + ); + for (const attachment of attachments) { + this._logger.debug( + `Enqueueing delete attachment ${attachment.title} for page ${confluencePage.title}`, + ); + job.enqueueTask({ type: "deleteAttachments", pageId: attachment.id }); + } + if ( + job.task.page.attachments !== undefined && + Object.entries(job.task.page.attachments).length > 0 + ) { + job.enqueueTask({ + type: "createAttachments", + pageId: confluencePage.id, + pageTitle: confluencePage.title, + attachments: job.task.page.attachments, + }); + } + if (this._syncMode === SyncModes.TREE) { + this._enqueueChildrenPages(job, confluencePage); + } + } + private async _deletePage( + job: ConfluenceSyncPagesJob, + ): Promise { + this._logger.debug(`Deleting page ${job.task.pageId}`); + const confluencePage = await this._confluenceClient.getPage( + job.task.pageId, + ); + for (const descendant of confluencePage.children ?? []) { + job.enqueueTask({ type: "delete", pageId: descendant.id }); + } + await this._confluenceClient.deleteContent(job.task.pageId); + } + + private async _deleteAttachments( + job: ConfluenceSyncPagesJob, + ): Promise { + this._logger.debug(`Deleting attachment ${job.task.pageId}`); + await this._confluenceClient.deleteContent(job.task.pageId); + } + + private async _createAttachments( + job: ConfluenceSyncPagesJob, + ): Promise { + this._logger.debug( + `Creating attachments for page ${job.task.pageTitle}, attachments: ${JSON.stringify( + job.task.attachments, + )}`, + ); + const attachments = await Promise.all( + Object.entries(job.task.attachments).map(async ([name, path]) => ({ + filename: name, + file: await readFile(path), + })), + ); + await this._confluenceClient.createAttachments( + job.task.pageId, + attachments, + ); + } + + private _enqueueChildrenPages( + job: ConfluenceSyncPagesJob, + confluencePage: ConfluencePage, + isRoot = false, + ) { + const descendants = job.getPagesByAncestor(confluencePage.title, isRoot); + const confluenceDescendants = confluencePage.children ?? []; + let descendantsWithPageId: string[] = []; + if (isRoot) { + descendants.forEach((descendant) => { + descendant.ancestors = [confluencePage.title]; + }); + if (this._syncMode === SyncModes.FLAT) { + const confluenceDescendantsInputPages = confluenceDescendants + .map(({ title }) => job.getPageByTitle(title)) + .filter(Boolean) as ConfluenceInputPage[]; + descendantsWithPageId = getPagesTitles( + confluenceDescendantsInputPages.filter( + (page) => page.id !== undefined, + ), + ); + if (descendantsWithPageId.length) + this._logger.warn( + `Some children of root page contains id: ${descendantsWithPageId.join(", ")}`, + ); + } + } + const descendantsToCreate = descendants.filter( + (descendant) => + !confluenceDescendants.some( + (other) => other.title === descendant.title, + ), + ); + const descendantsToUpdate = confluenceDescendants + .filter((descendant) => + descendants.some((other) => other.title === descendant.title), + ) + .map((descendant) => { + const page = job.getPageByTitle( + descendant.title, + ) as ConfluenceInputPage; + return { pageId: descendant.id, page }; + }); + const descendantsToDelete = confluenceDescendants.filter( + (descendant) => + !descendants.some((other) => other.title === descendant.title) && + !descendantsWithPageId.includes(descendant.title), + ); + for (const descendant of descendantsToDelete) { + job.enqueueTask({ type: "delete", pageId: descendant.id }); + } + for (const descendant of descendantsToCreate) { + job.enqueueTask({ type: "create", page: descendant }); + } + for (const descendant of descendantsToUpdate) { + job.enqueueTask({ + type: "update", + pageId: descendant.pageId, + page: descendant.page, + }); + } + } +}; diff --git a/components/confluence-sync/src/ConfluenceSyncPages.types.ts b/components/confluence-sync/src/ConfluenceSyncPages.types.ts new file mode 100644 index 00000000..b7ae973c --- /dev/null +++ b/components/confluence-sync/src/ConfluenceSyncPages.types.ts @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LogLevel, LoggerInterface } from "@mocks-server/logger"; + +import type { + ConfluencePage, + ConfluencePageBasicInfo, + ConfluenceId, +} from "./confluence/CustomConfluenceClient.types"; + +export enum SyncModes { + TREE = "tree", + FLAT = "flat", +} + +/** Type for dictionary values */ +export type ConfluencePagesDictionaryItem = { + /** Confluence page id */ + id: ConfluenceId; + /** Confluence page title */ + title: string; + /** Confluence page version */ + version: number; + /** Confluence page ancestors */ + ancestors?: ConfluencePageBasicInfo[]; + /** Boolean to indicate if the page was updated */ + visited?: boolean; +}; + +/** Type of input pages */ +export type ConfluenceInputPage = { + /** Input page id */ + id?: ConfluenceId; + /** Input page title */ + title: string; + /** Input page content */ + content?: string; + /** Input page ancestors */ + ancestors?: string[]; + /** Input page attachments */ + attachments?: Record; +}; + +/** Confluence page dictionary */ +export type ConfluencePagesDictionary = Record< + string, + ConfluencePagesDictionaryItem +>; + +export interface ConfluenceSyncPagesConfig { + /** Confluence url */ + url: string; + /** Confluence space id */ + spaceId: string; + /** Confluence page under which all pages will be synced */ + rootPageId?: ConfluenceId; + /** Confluence personal access token */ + personalAccessToken: string; + /** Log level */ + logLevel?: LogLevel; + /** Dry run option */ + dryRun?: boolean; + /** Sync mode */ + syncMode?: SyncModes; +} + +/** Creates a ConfluenceSyncPages interface */ +export interface ConfluenceSyncPagesConstructor { + /** Returns ConfluenceSyncPages interface + * @param config - Config for creating a ConfluenceSyncPages interface {@link ConfluenceSyncPagesConfig}. + * @returns ConfluenceSyncPages instance {@link ConfluenceSyncPagesInterface}. + * @example const confluenceSyncPages = new ConfluenceSyncPages({ personalAccessToken: "foo", url: "https://bar.com", spaceId: "CTO", rootPageId: "foo-root-id"}); + */ + new (config: ConfluenceSyncPagesConfig): ConfluenceSyncPagesInterface; +} + +export interface ConfluenceSyncPagesInterface { + /** Library logger. You can attach events, consult logs store, etc. */ + logger: LoggerInterface; + /** Sync pages in Confluence. + * @param pages - Pages data {@link ConfluenceInputPage}. + */ + sync(pages: ConfluenceInputPage[]): Promise; +} + +export interface ConfluenceSyncPagesCreateTask { + type: "create"; + page: ConfluenceInputPage; +} + +export interface ConfluenceSyncPagesInitSyncTask { + type: "init"; + pageId: string; +} + +export interface ConfluenceSyncPagesUpdateTask { + type: "update"; + pageId: string; + page: ConfluenceInputPage; +} + +export interface ConfluenceSyncPagesDeleteTask { + type: "delete"; + pageId: string; +} + +export interface ConfluenceSyncPagesCreateAttachmentsTask { + type: "createAttachments"; + pageId: string; + pageTitle: string; + attachments: Record; +} + +export interface ConfluenceSyncPagesDeleteAttachmentsTask { + type: "deleteAttachments"; + pageId: string; +} + +export type ConfluenceSyncPagesTask = + | ConfluenceSyncPagesCreateTask + | ConfluenceSyncPagesInitSyncTask + | ConfluenceSyncPagesUpdateTask + | ConfluenceSyncPagesDeleteTask + | ConfluenceSyncPagesCreateAttachmentsTask + | ConfluenceSyncPagesDeleteAttachmentsTask; + +export interface ConfluenceSyncPagesJob< + Task extends ConfluenceSyncPagesTask = ConfluenceSyncPagesTask, +> { + task: Task; + enqueueTask: (task: ConfluenceSyncPagesTask) => void; + getConfluencePageByTitle: (pageTitle: string) => ConfluencePage | undefined; + getPageByTitle: (pageTitle: string) => ConfluenceInputPage | undefined; + getPagesByAncestor: ( + ancestor: string, + isRoot?: boolean, + ) => ConfluenceInputPage[]; + storeConfluencePage: (page: ConfluencePage) => void; +} diff --git a/components/confluence-sync/src/confluence/CustomConfluenceClient.ts b/components/confluence-sync/src/confluence/CustomConfluenceClient.ts new file mode 100644 index 00000000..b304d701 --- /dev/null +++ b/components/confluence-sync/src/confluence/CustomConfluenceClient.ts @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; +import type { Models } from "confluence.js"; +import { ConfluenceClient } from "confluence.js"; + +import type { + Attachments, + ConfluenceClientConfig, + ConfluenceClientConstructor, + ConfluenceClientInterface, + ConfluencePage, + ConfluencePageBasicInfo, + ConfluenceId, + CreatePageParams, +} from "./CustomConfluenceClient.types"; +import { AttachmentsNotFoundError } from "./errors/AttachmentsNotFoundError"; +import { toConfluenceClientError } from "./errors/AxiosErrors"; +import { CreateAttachmentsError } from "./errors/CreateAttachmentsError"; +import { CreatePageError } from "./errors/CreatePageError"; +import { DeletePageError } from "./errors/DeletePageError"; +import { PageNotFoundError } from "./errors/PageNotFoundError"; +import { UpdatePageError } from "./errors/UpdatePageError"; + +export const CustomConfluenceClient: ConfluenceClientConstructor = class CustomConfluenceClient + implements ConfluenceClientInterface +{ + private _config: ConfluenceClientConfig; + private _client: ConfluenceClient; + private _logger: LoggerInterface; + + constructor(config: ConfluenceClientConfig) { + this._config = config; + this._client = new ConfluenceClient({ + host: config.url, + authentication: { + personalAccessToken: config.personalAccessToken, + }, + apiPrefix: "/rest/", + }); + this._logger = config.logger; + } + + // Exposed mainly for testing purposes + public get logger() { + return this._logger; + } + + public async getPage(id: string): Promise { + try { + this._logger.silly(`Getting page with id ${id}`); + const response: Models.Content = + await this._client.content.getContentById({ + id, + expand: ["ancestors", "version.number", "children.page"], + }); + this._logger.silly( + `Get page response: ${JSON.stringify(response, null, 2)}`, + ); + return { + title: response.title, + id: response.id, + version: response.version?.number as number, + ancestors: response.ancestors?.map((ancestor) => + this._convertToConfluencePageBasicInfo(ancestor), + ), + children: response.children?.page?.results?.map((child) => + this._convertToConfluencePageBasicInfo(child), + ), + }; + } catch (error) { + throw new PageNotFoundError(id, { cause: error }); + } + } + + public async createPage({ + title, + content, + ancestors, + }: CreatePageParams): Promise { + if (!this._config.dryRun) { + const createContentBody = { + type: "page", + title, + space: { + key: this._config.spaceId, + }, + ancestors: this.handleAncestors(ancestors), + body: { + storage: { + value: content || "", + representation: "storage", + }, + }, + }; + try { + this._logger.silly(`Creating page with title ${title}`); + const response = + await this._client.content.createContent(createContentBody); + this._logger.silly( + `Create page response: ${JSON.stringify(response, null, 2)}`, + ); + + return { + title: response.title, + id: response.id, + version: response.version?.number as number, + ancestors: response.ancestors?.map((ancestor) => + this._convertToConfluencePageBasicInfo(ancestor), + ), + }; + } catch (e) { + const error = toConfluenceClientError(e); + throw new CreatePageError(title, { cause: error }); + } + } else { + this._logger.info(`Dry run: creating page with title ${title}`); + return { + title, + id: "1234", + version: 1, + ancestors, + }; + } + } + + public async updatePage({ + id, + title, + content, + version, + ancestors, + }: ConfluencePage): Promise { + if (!this._config.dryRun) { + const updateContentBody = { + id, + type: "page", + title, + ancestors: this.handleAncestors(ancestors), + version: { + number: version, + }, + body: { + storage: { + value: content || "", + representation: "storage", + }, + }, + }; + try { + this._logger.silly(`Updating page with title ${title}`); + const response = + await this._client.content.updateContent(updateContentBody); + this._logger.silly( + `Update page response: ${JSON.stringify(response, null, 2)}`, + ); + + return { + title: response.title, + id: response.id, + version: response.version?.number as number, + ancestors: response.ancestors?.map((ancestor) => + this._convertToConfluencePageBasicInfo(ancestor), + ), + }; + } catch (e) { + const error = toConfluenceClientError(e); + throw new UpdatePageError(id, title, { cause: error }); + } + } else { + this._logger.info(`Dry run: updating page with title ${title}`); + return { + title, + id, + version, + ancestors, + }; + } + } + + public async deleteContent(id: ConfluenceId): Promise { + if (!this._config.dryRun) { + try { + this._logger.silly(`Deleting content with id ${id}`); + await this._client.content.deleteContent({ id }); + } catch (error) { + throw new DeletePageError(id, { cause: error }); + } + } else { + this._logger.info(`Dry run: deleting content with id ${id}`); + } + } + + public async getAttachments( + id: ConfluenceId, + ): Promise { + try { + this._logger.silly(`Getting attachments of page with id ${id}`); + const response = await this._client.contentAttachments.getAttachments({ + id, + }); + this._logger.silly( + `Get attachments response: ${JSON.stringify(response, null, 2)}`, + ); + return ( + response.results?.map((attachment) => ({ + id: attachment.id, + title: attachment.title, + })) || [] + ); + } catch (error) { + throw new AttachmentsNotFoundError(id, { cause: error }); + } + } + + public async createAttachments( + id: ConfluenceId, + attachments: Attachments, + ): Promise { + if (!this._config.dryRun) { + try { + const bodyRequest = attachments.map((attachment) => ({ + minorEdit: true, + ...attachment, + })); + this._logger.silly( + `Creating attachments of page with id ${id}, attachments: ${attachments + .map((attachment) => attachment.filename) + .join(", ")}`, + ); + const response = + await this._client.contentAttachments.createAttachments({ + id, + attachments: bodyRequest, + }); + this._logger.silly( + `Create attachments response: ${JSON.stringify(response, null, 2)}`, + ); + } catch (error) { + throw new CreateAttachmentsError(id, { cause: error }); + } + } else { + this._logger + .info(`Dry run: creating attachments of page with id ${id}, attachments: ${attachments + .map((attachment) => attachment.filename) + .join(", ")} + `); + } + } + + private handleAncestors( + ancestors?: ConfluencePageBasicInfo[], + ): { id: string }[] | undefined { + if (ancestors && ancestors.length) { + const id = ancestors.at(-1)?.id as string; + return [{ id }]; + } else { + return undefined; + } + } + + private _convertToConfluencePageBasicInfo( + rawInfo: Models.Content, + ): ConfluencePageBasicInfo { + return { + id: rawInfo.id, + title: rawInfo.title, + }; + } +}; diff --git a/components/confluence-sync/src/confluence/CustomConfluenceClient.types.ts b/components/confluence-sync/src/confluence/CustomConfluenceClient.types.ts new file mode 100644 index 00000000..3e408397 --- /dev/null +++ b/components/confluence-sync/src/confluence/CustomConfluenceClient.types.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +export type ConfluenceId = string; +export type ConfluencePageBasicInfo = Pick; +export type CreatePageParams = Pick< + ConfluencePage, + "title" | "content" | "ancestors" +>; +export type Attachments = Attachment[]; + +interface Attachment { + filename: string; + file: Buffer; +} +export interface ConfluencePage { + /** Page title */ + title: string; + /** Page id */ + id: ConfluenceId; + /** Page version */ + version: number; + /** Page content */ + content?: string; + /** Page ancestor */ + ancestors?: ConfluencePageBasicInfo[]; + /** Page children */ + children?: ConfluencePageBasicInfo[]; +} + +/** Config for creating a Confluence client */ +export interface ConfluenceClientConfig { + /** Confluence personal access token */ + personalAccessToken: string; + /** Confluence url */ + url: string; + /** Confluence space id */ + spaceId: string; + /** Logger */ + logger: LoggerInterface; + /** Dry run */ + dryRun?: boolean; +} + +/** Creates a ConfluenceClient interface */ +export interface ConfluenceClientConstructor { + /** Returns ConfluenceClient interface + * @param config - Config for creating a Confluence client {@link ConfluenceClientConfig}. + * @returns ConfluenceClient instance {@link ConfluenceClientInterface}. + * @example const confluenceClient = new ConfluenceClient({ personalAccessToken: "foo", url: "https://bar.com", spaceId: "CTO", parentPageId: "foo-page-id"}); + */ + new (config: ConfluenceClientConfig): ConfluenceClientInterface; +} + +export interface ConfluenceClientInterface { + /** Library logger. You can attach events, consult logs store, etc. */ + logger: LoggerInterface; + /** Returns a page in Confluence + * @param key - Page key. + * @returns Page data {@link ConfluencePage}. + * @example const page = await confluenceClient.getPage("foo-page-id"); + */ + getPage(key: string): Promise; + + /** Creates a page in Confluence + * @param page - Page data {@link ConfluencePage}. + * @returns Page data {@link ConfluencePage}. + */ + createPage(page: CreatePageParams): Promise; + + /** Updates a page in Confluence + * @param page - Page data {@link ConfluencePage}. + * @returns Page data {@link ConfluencePage}. + */ + updatePage(page: ConfluencePage): Promise; + + /** Deletes a page in Confluence + * @param id - Id of the page to delete. + */ + deleteContent(id: ConfluenceId): Promise; + + /** Gets all the attachments of a page in Confluence + * @param id - Id of the page to get the attachments from. + * @returns Attachments data. + * @example const attachments = await confluenceClient.getAttachments("foo-page-id"); + */ + getAttachments(id: ConfluenceId): Promise; + + /** Creates all the attachments of a page in Confluence + * @param id - Id of the page to attach the file to. + * @param attachments - Attachments to create. + */ + createAttachments(id: ConfluenceId, attachments: Attachments): Promise; +} diff --git a/components/confluence-sync/src/confluence/errors/AttachmentsNotFoundError.ts b/components/confluence-sync/src/confluence/errors/AttachmentsNotFoundError.ts new file mode 100644 index 00000000..6c0b38f1 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/AttachmentsNotFoundError.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class AttachmentsNotFoundError extends Error { + constructor(id: string, options?: ErrorOptions) { + super( + `Error getting attachments of page with id ${id}: ${options?.cause}`, + options, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/AxiosErrors.ts b/components/confluence-sync/src/confluence/errors/AxiosErrors.ts new file mode 100644 index 00000000..e82d83ad --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/AxiosErrors.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AxiosError } from "axios"; +import { HttpStatusCode } from "axios"; + +import { BadRequestError } from "./axios/BadRequestError"; +import { InternalServerError } from "./axios/InternalServerError"; +import { UnauthorizedError } from "./axios/UnauthorizedError"; +import { UnexpectedError } from "./axios/UnexpectedError"; +import { UnknownAxiosError } from "./axios/UnknownAxiosError"; + +export function toConfluenceClientError(error: unknown): Error { + if ((error as AxiosError).name === "AxiosError") { + const axiosError = error as AxiosError; + if (axiosError.response?.status === HttpStatusCode.BadRequest) { + return new BadRequestError(axiosError); + } + if ( + axiosError.response?.status === HttpStatusCode.Unauthorized || + axiosError.response?.status === HttpStatusCode.Forbidden + ) { + return new UnauthorizedError(axiosError); + } + if (axiosError.response?.status === HttpStatusCode.InternalServerError) { + return new InternalServerError(axiosError); + } + + return new UnknownAxiosError(axiosError); + } + return new UnexpectedError(error); +} diff --git a/components/confluence-sync/src/confluence/errors/CreateAttachmentsError.ts b/components/confluence-sync/src/confluence/errors/CreateAttachmentsError.ts new file mode 100644 index 00000000..4d982c1c --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/CreateAttachmentsError.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class CreateAttachmentsError extends Error { + constructor(id: string, options?: ErrorOptions) { + super( + `Error creating attachments of page with id ${id}: ${options?.cause}`, + options, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/CreatePageError.ts b/components/confluence-sync/src/confluence/errors/CreatePageError.ts new file mode 100644 index 00000000..11e8f00b --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/CreatePageError.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class CreatePageError extends Error { + constructor(title: string, options?: ErrorOptions) { + super( + `Error creating page with title ${title}: ${options?.cause}`, + options, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/DeletePageError.ts b/components/confluence-sync/src/confluence/errors/DeletePageError.ts new file mode 100644 index 00000000..86dc6c86 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/DeletePageError.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class DeletePageError extends Error { + constructor(id: string, options?: ErrorOptions) { + super(`Error deleting content with id ${id}: ${options?.cause}`, options); + } +} diff --git a/components/confluence-sync/src/confluence/errors/PageNotFoundError.ts b/components/confluence-sync/src/confluence/errors/PageNotFoundError.ts new file mode 100644 index 00000000..cedcdc6d --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/PageNotFoundError.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class PageNotFoundError extends Error { + constructor(id: string, options?: ErrorOptions) { + super(`Error getting page with id ${id}: ${options?.cause}`, options); + } +} diff --git a/components/confluence-sync/src/confluence/errors/UpdatePageError.ts b/components/confluence-sync/src/confluence/errors/UpdatePageError.ts new file mode 100644 index 00000000..f27d3231 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/UpdatePageError.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class UpdatePageError extends Error { + constructor(id: string, title: string, options?: ErrorOptions) { + super( + `Error updating page with id ${id} and title ${title}: ${options?.cause}`, + options, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/axios/BadRequestError.ts b/components/confluence-sync/src/confluence/errors/axios/BadRequestError.ts new file mode 100644 index 00000000..39b2358f --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/BadRequestError.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AxiosError } from "axios"; + +export class BadRequestError extends Error { + constructor(error: AxiosError) { + super( + `Bad Request + Response: ${JSON.stringify(error.response?.data, null, 2)}`, + { cause: error }, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/axios/InternalServerError.ts b/components/confluence-sync/src/confluence/errors/axios/InternalServerError.ts new file mode 100644 index 00000000..a93f6736 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/InternalServerError.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AxiosError } from "axios"; + +export class InternalServerError extends Error { + constructor(error: AxiosError) { + super( + `Internal Server Error + Response: ${JSON.stringify(error.response?.data, null, 2)}`, + { cause: error }, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/axios/UnauthorizedError.ts b/components/confluence-sync/src/confluence/errors/axios/UnauthorizedError.ts new file mode 100644 index 00000000..8779db42 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/UnauthorizedError.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AxiosError } from "axios"; + +export class UnauthorizedError extends Error { + constructor(error: AxiosError) { + super( + `Unauthorized + Response: ${JSON.stringify(error.response?.data, null, 2)}`, + { cause: error }, + ); + } +} diff --git a/components/confluence-sync/src/confluence/errors/axios/UnexpectedError.ts b/components/confluence-sync/src/confluence/errors/axios/UnexpectedError.ts new file mode 100644 index 00000000..1c169659 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/UnexpectedError.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class UnexpectedError extends Error { + constructor(error: unknown) { + super(`Unexpected Error: ${error}`); + } +} diff --git a/components/confluence-sync/src/confluence/errors/axios/UnknownAxiosError.ts b/components/confluence-sync/src/confluence/errors/axios/UnknownAxiosError.ts new file mode 100644 index 00000000..3a471c49 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/UnknownAxiosError.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AxiosError } from "axios"; + +export class UnknownAxiosError extends Error { + constructor(error: AxiosError) { + super( + `Axios Error + Response: ${JSON.stringify(error.response?.data, null, 2)}`, + { cause: error }, + ); + } +} diff --git a/components/confluence-sync/src/confluence/types.ts b/components/confluence-sync/src/confluence/types.ts new file mode 100644 index 00000000..00a7169c --- /dev/null +++ b/components/confluence-sync/src/confluence/types.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./CustomConfluenceClient.types"; diff --git a/components/confluence-sync/src/errors/CompoundError.ts b/components/confluence-sync/src/errors/CompoundError.ts new file mode 100644 index 00000000..ff823b09 --- /dev/null +++ b/components/confluence-sync/src/errors/CompoundError.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class CompoundError extends Error { + public readonly errors: Error[]; + + constructor(...errors: Error[]) { + super(errors.map((error) => error.message).join("\n")); + this.errors = errors; + } +} diff --git a/components/confluence-sync/src/errors/NotAncestorsExpectedValidationError.ts b/components/confluence-sync/src/errors/NotAncestorsExpectedValidationError.ts new file mode 100644 index 00000000..29fd7982 --- /dev/null +++ b/components/confluence-sync/src/errors/NotAncestorsExpectedValidationError.ts @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceInputPage } from "../ConfluenceSyncPages.types"; +import { getPagesTitlesCommaSeparated } from "../support/Pages"; + +export class NotAncestorsExpectedValidationError extends Error { + constructor( + pagesWithAncestors: ConfluenceInputPage[], + options?: ErrorOptions, + ) { + super( + `Pages with ancestors are not supported in FLAT sync mode: ${getPagesTitlesCommaSeparated( + pagesWithAncestors, + )}`, + options, + ); + } +} diff --git a/components/confluence-sync/src/errors/PendingPagesToSyncError.ts b/components/confluence-sync/src/errors/PendingPagesToSyncError.ts new file mode 100644 index 00000000..072f12eb --- /dev/null +++ b/components/confluence-sync/src/errors/PendingPagesToSyncError.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceInputPage } from "../ConfluenceSyncPages.types"; +import { getPagesTitlesCommaSeparated } from "../support/Pages"; + +export class PendingPagesToSyncError extends Error { + constructor( + pendingPagesToSync: ConfluenceInputPage[], + options?: ErrorOptions, + ) { + super( + `There still are ${ + pendingPagesToSync.length + } pages to create after sync: ${getPagesTitlesCommaSeparated( + pendingPagesToSync, + )}, check if they have their ancestors created.`, + options, + ); + } +} diff --git a/components/confluence-sync/src/errors/RootPageRequiredException.ts b/components/confluence-sync/src/errors/RootPageRequiredException.ts new file mode 100644 index 00000000..b36e630f --- /dev/null +++ b/components/confluence-sync/src/errors/RootPageRequiredException.ts @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceInputPage } from "../ConfluenceSyncPages.types"; +import { SyncModes } from "../ConfluenceSyncPages.types"; +import { getPagesTitlesCommaSeparated } from "../support/Pages"; + +export class RootPageRequiredException extends Error { + constructor( + mode: SyncModes, + pagesWithoutId?: ConfluenceInputPage[], + options?: ErrorOptions, + ) { + /* istanbul ignore else */ + if (mode === SyncModes.TREE) { + super("rootPageId is required for TREE sync mode", options); + } else if (mode === SyncModes.FLAT) { + super( + `rootPageId is required for FLAT sync mode when there are pages without id: ${getPagesTitlesCommaSeparated( + pagesWithoutId as ConfluenceInputPage[], + )}`, + options, + ); + } else { + /* istanbul ignore next */ + super("Unknown sync mode", options); + } + } +} diff --git a/components/confluence-sync/src/index.ts b/components/confluence-sync/src/index.ts new file mode 100644 index 00000000..2504317c --- /dev/null +++ b/components/confluence-sync/src/index.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./types"; + +export * from "./ConfluenceSyncPages"; diff --git a/components/confluence-sync/src/support/Pages.ts b/components/confluence-sync/src/support/Pages.ts new file mode 100644 index 00000000..09d32f20 --- /dev/null +++ b/components/confluence-sync/src/support/Pages.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceInputPage } from "../ConfluenceSyncPages.types"; + +export function getPagesTitles(pages: ConfluenceInputPage[]): string[] { + return pages.map((page) => page.title); +} + +export function getPagesTitlesCommaSeparated( + pages: ConfluenceInputPage[], +): string { + return getPagesTitles(pages).join(", "); +} diff --git a/components/confluence-sync/src/types.ts b/components/confluence-sync/src/types.ts new file mode 100644 index 00000000..5b6536fa --- /dev/null +++ b/components/confluence-sync/src/types.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export type { + ConfluenceInputPage, + ConfluenceSyncPagesConfig, + ConfluenceSyncPagesInterface, +} from "./ConfluenceSyncPages.types"; +export { SyncModes } from "./ConfluenceSyncPages.types"; +export * from "./confluence/types"; diff --git a/components/confluence-sync/test/component/specs/Sync.spec.ts b/components/confluence-sync/test/component/specs/Sync.spec.ts new file mode 100644 index 00000000..78134c9b --- /dev/null +++ b/components/confluence-sync/test/component/specs/Sync.spec.ts @@ -0,0 +1,723 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceSyncPagesInterface } from "@src/index"; +import { ConfluenceSyncPages, SyncModes } from "@src/index"; + +import type { SpyRequest } from "../../../mocks/support/SpyStorage.types"; +import { + createWrongPages, + deleteWrongPages, + flatPages, + flatPagesWithRoot, + pagesNoRoot, + pagesWithRoot, + renamedPage, + updateWrongPages, + wrongPages, +} from "../support/fixtures/Pages"; +import { + changeMockCollection, + getRequests, + getRequestsByRouteId, + resetRequests, +} from "../support/mock/Client"; + +describe("confluence-sync-pages library", () => { + describe("sync method", () => { + let createRequests: SpyRequest[]; + let updateRequests: SpyRequest[]; + let deleteRequests: SpyRequest[]; + let getAttachmentsRequests: SpyRequest[]; + let createAttachmentsRequests: SpyRequest[]; + let confluenceSyncPages: ConfluenceSyncPagesInterface; + + function findRequestByTitle(title: string, collection: SpyRequest[]) { + return collection.find((request) => request?.body?.title === title); + } + + function findRequestById(id: string, collection: SpyRequest[]) { + return collection.find((request) => request?.params?.pageId === id); + } + + beforeAll(async () => { + confluenceSyncPages = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + rootPageId: "foo-root-id", + url: "http://127.0.0.1:3100", + logLevel: "debug", + }); + }); + + afterEach(async () => { + await resetRequests(); + }); + + describe("when the root page has no children (pagesNoRoot input and empty-root mock)", () => { + beforeAll(async () => { + await changeMockCollection("empty-root"); + await confluenceSyncPages.sync(pagesNoRoot); + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have created 7 pages", async () => { + expect(createRequests).toHaveLength(7); + }); + + it("should have sent data of page with title foo-parent-title", async () => { + const pageRequest = findRequestByTitle( + "foo-parent-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-parent-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-root-id", + }, + ], + body: { + storage: { + value: "foo-parent-content", + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-child1-title", async () => { + const pageRequest = findRequestByTitle( + "foo-child1-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-child1-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: "foo-child1-content", + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-child2-title", async () => { + const pageRequest = findRequestByTitle( + "foo-child2-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-child2-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: "foo-child2-content", + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-grandChild1-title", async () => { + const pageRequest = findRequestByTitle( + "foo-grandChild1-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-grandChild1-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child1-id", + }, + ], + body: { + storage: { + value: "foo-grandChild1-content", + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-grandChild3-title", async () => { + const pageRequest = findRequestByTitle( + "foo-grandChild3-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-grandChild3-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child2-id", + }, + ], + body: { + storage: { + value: "foo-grandChild3-content", + representation: "storage", + }, + }, + }); + }); + }); + + describe("when the root page has children (default-root mock)", () => { + beforeAll(async () => { + await changeMockCollection("default-root"); + }); + + describe("when input is pageWithRoot", () => { + beforeAll(async () => { + await confluenceSyncPages.sync(pagesWithRoot); + + createRequests = await getRequestsByRouteId("confluence-create-page"); + updateRequests = await getRequestsByRouteId("confluence-update-page"); + deleteRequests = await getRequestsByRouteId("confluence-delete-page"); + getAttachmentsRequests = await getRequestsByRouteId( + "confluence-get-attachments", + ); + createAttachmentsRequests = await getRequestsByRouteId( + "confluence-create-attachments", + ); + }); + + it("should have created 3 pages", async () => { + expect(createRequests).toHaveLength(3); + }); + + it("should have create page with title foo-child2-title", async () => { + const pageRequest = findRequestByTitle( + "foo-child2-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-child2-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: "foo-child2-content", + representation: "storage", + }, + }, + }); + }); + + it("should have updated 3 page", async () => { + expect(updateRequests).toHaveLength(3); + }); + + it("should have update page with title foo-parent-title", async () => { + const pageRequest = findRequestByTitle( + "foo-parent-title", + updateRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content/foo-parent-id"); + expect(pageRequest?.method).toBe("PUT"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-parent-title", + version: { + number: 2, + }, + ancestors: [ + { + id: "foo-root-id", + }, + ], + body: { + storage: { + value: "foo-parent-content", + representation: "storage", + }, + }, + }); + }); + + it("should have deleted 1 page and 1 attachment", async () => { + expect(deleteRequests).toHaveLength(2); + }); + + it("should have delete page with title foo-grandChild2-title", async () => { + const pageRequest = findRequestById( + "foo-grandChild2-id", + deleteRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content/foo-grandChild2-id"); + expect(pageRequest?.method).toBe("DELETE"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({}); + }); + + it("should have delete attachment with title foo-grandChild1-attachment-title", async () => { + const pageRequest = findRequestById( + "foo-grandChild1-attachment-id", + deleteRequests, + ); + + expect(pageRequest?.url).toBe( + "/rest/api/content/foo-grandChild1-attachment-id", + ); + expect(pageRequest?.method).toBe("DELETE"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({}); + }); + + it("should have get attachments of 3 pages", async () => { + expect(getAttachmentsRequests).toHaveLength(3); + }); + + it("should have create attachment for page foo-grandChild3", async () => { + const pageRequest = findRequestById( + "foo-grandChild3-id", + createAttachmentsRequests, + ); + + expect(pageRequest?.url).toBe( + "/rest/api/content/foo-grandChild3-id/child/attachment", + ); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.headers["x-atlassian-token"]).toBe("no-check"); + expect(pageRequest?.headers["content-type"]).toContain( + "multipart/form-data", + ); + }); + }); + + describe("when input tree is wrong", () => { + let error: string; + + beforeAll(async () => { + try { + await confluenceSyncPages.sync(wrongPages); + } catch (e) { + error = (e as Error).message; + } + + createRequests = await getRequestsByRouteId("confluence-create-page"); + updateRequests = await getRequestsByRouteId("confluence-update-page"); + deleteRequests = await getRequestsByRouteId("confluence-delete-page"); + }); + + it("should throw an error", async () => { + expect(error).toBe( + `There still are 1 pages to create after sync: foo-wrongPage-title, check if they have their ancestors created.`, + ); + }); + + it("should have created 0 pages", async () => { + expect(createRequests).toHaveLength(0); + }); + + it("should have updated 2 page", async () => { + expect(updateRequests).toHaveLength(2); + }); + + it("should have deleted 2 pages", async () => { + expect(deleteRequests).toHaveLength(2); + }); + }); + + describe("when a request fails", () => { + let error: string; + + describe("when a create request fails", () => { + beforeAll(async () => { + try { + await confluenceSyncPages.sync(createWrongPages); + } catch (e) { + error = (e as Error).message; + } + + createRequests = await getRequestsByRouteId( + "confluence-create-page", + ); + updateRequests = await getRequestsByRouteId( + "confluence-update-page", + ); + deleteRequests = await getRequestsByRouteId( + "confluence-delete-page", + ); + }); + + it("should throw an error", async () => { + expect(error).toContain( + `Error creating page with title foo-wrongPage-title: Error: Bad Request`, + ); + }); + + it("should have created 2 pages and tried with the failed one", async () => { + expect(createRequests).toHaveLength(3); + }); + + it("should have updated 2 page", async () => { + expect(updateRequests).toHaveLength(2); + }); + + it("should have deleted 2 pages", async () => { + expect(deleteRequests).toHaveLength(2); + }); + }); + + describe("when an update request fails", () => { + beforeAll(async () => { + try { + await confluenceSyncPages.sync(updateWrongPages); + } catch (e) { + error = (e as Error).message; + } + + createRequests = await getRequestsByRouteId( + "confluence-create-page", + ); + updateRequests = await getRequestsByRouteId( + "confluence-update-page", + ); + deleteRequests = await getRequestsByRouteId( + "confluence-delete-page", + ); + }); + + it("should throw an error", async () => { + expect(error).toContain( + `Error updating page with id foo-grandChild2-id and title foo-grandChild2-title: Error: Bad Request`, + ); + }); + + it("should have created 2 pages", async () => { + expect(createRequests).toHaveLength(2); + }); + + it("should have updated 2 pages and tried with the failed one", async () => { + expect(updateRequests).toHaveLength(3); + }); + + it("should have deleted 1 pages", async () => { + expect(deleteRequests).toHaveLength(1); + }); + }); + + describe("when a delete request fails", () => { + beforeAll(async () => { + try { + await confluenceSyncPages.sync(deleteWrongPages); + } catch (e) { + error = (e as Error).message; + } + + createRequests = await getRequestsByRouteId( + "confluence-create-page", + ); + updateRequests = await getRequestsByRouteId( + "confluence-update-page", + ); + deleteRequests = await getRequestsByRouteId( + "confluence-delete-page", + ); + }); + + it("should throw an error", async () => { + expect(error).toBe( + `Error deleting content with id foo-child1-id: AxiosError: Request failed with status code 404`, + ); + }); + + it("should have created 2 pages", async () => { + expect(createRequests).toHaveLength(2); + }); + + it("should have updated 1 page", async () => { + expect(updateRequests).toHaveLength(1); + }); + + it("should have deleted 2 pages and tried with the failed one", async () => { + expect(deleteRequests).toHaveLength(3); + }); + }); + }); + + describe("when a page has been renamed", () => { + let requests: SpyRequest[]; + let getPageRequests: SpyRequest[]; + + beforeAll(async () => { + await changeMockCollection("renamed-page"); + await confluenceSyncPages.sync(renamedPage); + + requests = await getRequests(); + getPageRequests = await getRequestsByRouteId("confluence-get-page"); + createRequests = await getRequestsByRouteId("confluence-create-page"); + deleteRequests = await getRequestsByRouteId("confluence-delete-page"); + }); + + it("should execute delete requests before create requests", async () => { + // First request is the one to get the root page + expect(requests[0].routeId).toBe("confluence-get-page"); + expect(getPageRequests[0].params?.pageId).toBe("foo-root-id"); + // Second request is the one to get the parent page, child of the root page + expect(requests[1].routeId).toBe("confluence-get-page"); + expect(getPageRequests[1].params?.pageId).toBe("foo-parent-id"); + // Third request has to be the one to delete the parent page + expect(requests[2].routeId).toBe("confluence-delete-page"); + expect(deleteRequests[0].params?.pageId).toBe("foo-parent-id"); + // Fourth request has to be the one to create the renamed page because is child of the root page + expect(requests[3].routeId).toBe("confluence-create-page"); + expect(createRequests[0].body?.title).toBe("foo-renamed-title"); + // Fifth request has to be the one to get the child1 page which is child of the parent page + expect(requests[4].routeId).toBe("confluence-get-page"); + expect(getPageRequests[2].params?.pageId).toBe("foo-child1-id"); + // Sixth request has to be the one to delete the child1 page + expect(requests[5].routeId).toBe("confluence-delete-page"); + expect(deleteRequests[1].params?.pageId).toBe("foo-child1-id"); + // Seventh request has to be the one to create the child1 page because is child of the renamed page + expect(requests[6].routeId).toBe("confluence-create-page"); + expect(createRequests[1].body?.title).toBe("foo-child1-title"); + // Eighth request has to be the one to get the grandChild1 page which is child of the child1 page child of parent page + expect(requests[7].routeId).toBe("confluence-get-page"); + expect(getPageRequests[3].params?.pageId).toBe("foo-grandChild1-id"); + // Ninth request has to be the one to delete the grandChild1 page + expect(requests[8].routeId).toBe("confluence-delete-page"); + expect(deleteRequests[2].params?.pageId).toBe("foo-grandChild1-id"); + // Tenth request has to be the one to get the grandChild2 page because is child of the child1 page child of parent page + expect(requests[9].routeId).toBe("confluence-get-page"); + expect(getPageRequests[4].params?.pageId).toBe("foo-grandChild2-id"); + // Eleventh request has to be the one to delete the grandChild2 page + expect(requests[10].routeId).toBe("confluence-delete-page"); + expect(deleteRequests[3].params?.pageId).toBe("foo-grandChild2-id"); + // Twelfth request has to be the one to create the grandChild1 page because is child of the child1 page child of the renamed page + expect(requests[11].routeId).toBe("confluence-create-page"); + expect(createRequests[2].body?.title).toBe("foo-grandChild1-title"); + // Thirteenth request has to be the one to create the grandChild2 page because is child of the child1 page child of the renamed page + expect(requests[12].routeId).toBe("confluence-create-page"); + expect(createRequests[3].body?.title).toBe("foo-grandChild2-title"); + }); + + it("should create all the children pages of the renamed page", async () => { + expect(createRequests).toHaveLength(4); + }); + }); + }); + + describe("when flat mode is enabled", () => { + beforeAll(async () => { + confluenceSyncPages = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + syncMode: SyncModes.FLAT, + url: "http://127.0.0.1:3100", + logLevel: "debug", + }); + + await changeMockCollection("flat-mode"); + await confluenceSyncPages.sync(flatPages); + updateRequests = await getRequestsByRouteId("confluence-update-page"); + }); + + it("should have updated 3 pages", async () => { + expect(updateRequests).toHaveLength(3); + }); + + it("should have updated page foo-page-1-title", async () => { + const pageRequest = findRequestById("foo-page-1-id", updateRequests); + + expect(pageRequest?.url).toBe("/rest/api/content/foo-page-1-id"); + expect(pageRequest?.method).toBe("PUT"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-page-1-title", + version: { + number: 2, + }, + body: { + storage: { + value: "foo-page-1-content", + representation: "storage", + }, + }, + }); + }); + + describe("when a page does not exist in confluence", () => { + it("should throw an error", async () => { + const wrongPage = { + id: "foo-wrongPage-id", + title: "foo-wrongPage-title", + content: "foo-wrongPage-content", + }; + + await expect(confluenceSyncPages.sync([wrongPage])).rejects.toThrow( + `Error getting page with id ${wrongPage.id}: AxiosError: Request failed with status code 404`, + ); + }); + }); + + describe("when a page has no id", () => { + it("should throw an error", async () => { + const wrongPage = { + title: "foo-wrongPage-title", + content: "foo-wrongPage-content", + }; + + await expect(confluenceSyncPages.sync([wrongPage])).rejects.toThrow( + `rootPageId is required for FLAT sync mode when there are pages without id: ${wrongPage.title}`, + ); + }); + }); + + describe("when root page is provided", () => { + beforeAll(async () => { + confluenceSyncPages = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + syncMode: SyncModes.FLAT, + url: "http://127.0.0.1:3100", + logLevel: "debug", + rootPageId: "foo-root-id", + }); + + await changeMockCollection("flat-mode"); + await confluenceSyncPages.sync(flatPagesWithRoot); + createRequests = await getRequestsByRouteId("confluence-create-page"); + updateRequests = await getRequestsByRouteId("confluence-update-page"); + deleteRequests = await getRequestsByRouteId("confluence-delete-page"); + }); + + it("should have created 1 page", async () => { + expect(createRequests).toHaveLength(1); + }); + + it("should have updated 1 page", async () => { + expect(updateRequests).toHaveLength(1); + }); + + it("should have deleted 1 page", async () => { + expect(deleteRequests).toHaveLength(1); + }); + + it("should have created page foo-page-3-title", async () => { + const pageRequest = findRequestByTitle( + "foo-page-3-title", + createRequests, + ); + + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-page-3-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-root-id", + }, + ], + body: { + storage: { + value: "foo-page-3-content", + representation: "storage", + }, + }, + }); + }); + + it("should have updated page foo-page-1-title", async () => { + const pageRequest = findRequestById("foo-page-1-id", updateRequests); + + expect(pageRequest?.url).toBe("/rest/api/content/foo-page-1-id"); + expect(pageRequest?.method).toBe("PUT"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-page-1-title", + version: { + number: 2, + }, + body: { + storage: { + value: "foo-page-1-content", + representation: "storage", + }, + }, + }); + }); + + it("should have deleted page foo-page-2-title", async () => { + const pageRequest = findRequestById("foo-page-2-id", deleteRequests); + + expect(pageRequest?.url).toBe("/rest/api/content/foo-page-2-id"); + expect(pageRequest?.method).toBe("DELETE"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + }); + }); + }); + }); +}); diff --git a/components/confluence-sync/test/component/support/fixtures/Pages.ts b/components/confluence-sync/test/component/support/fixtures/Pages.ts new file mode 100644 index 00000000..6abd35db --- /dev/null +++ b/components/confluence-sync/test/component/support/fixtures/Pages.ts @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceInputPage } from "@src/index"; + +export const pagesNoRoot: ConfluenceInputPage[] = [ + { + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [], + }, + { + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: ["foo-parent-title"], + }, + { + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: ["foo-parent-title"], + }, + { + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: ["foo-parent-title", "foo-child1-title"], + }, + { + title: "foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: ["foo-parent-title", "foo-child1-title"], + }, + { + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: ["foo-parent-title", "foo-child2-title"], + }, + { + title: "foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: ["foo-parent-title", "foo-child2-title"], + }, +]; + +export const pagesWithRoot: ConfluenceInputPage[] = [ + { + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: ["foo-root-title"], + }, + { + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child1-title"], + }, + { + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child2-title"], + attachments: { + "foo-grandChild3-attachment1-title": + "test/component/support/fixtures/image.png", + }, + }, + { + title: "foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child2-title"], + }, +]; + +export const wrongPages: ConfluenceInputPage[] = [ + { + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: ["foo-root-title"], + }, + { + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-wrongPage-title", + content: "foo-wrongPage-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-inexistent-title"], + }, +]; + +export const createWrongPages: ConfluenceInputPage[] = [ + { + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: ["foo-root-title"], + }, + { + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-wrongPage-title", + content: "This page returns a 400 error when creating", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child2-title"], + }, +]; + +export const updateWrongPages: ConfluenceInputPage[] = [ + { + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: ["foo-root-title"], + }, + { + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child2-title"], + }, + { + title: "foo-grandChild2-title", + content: "This page returns a 400 error when updating", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child1-title"], + }, +]; + +export const deleteWrongPages: ConfluenceInputPage[] = [ + { + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: ["foo-root-title"], + }, + { + title: "foo-child2-title", + content: "foo-child2-content", + ancestors: ["foo-root-title", "foo-parent-title"], + }, + { + title: "foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: ["foo-root-title", "foo-parent-title", "foo-child2-title"], + }, + // when deleting child1 it will return a 400 error +]; + +export const flatPages: ConfluenceInputPage[] = [ + { + title: "foo-page-1-title", + content: "foo-page-1-content", + id: "foo-page-1-id", + }, + { + title: "foo-page-2-title", + content: "foo-page-2-content", + id: "foo-page-2-id", + }, + { + title: "foo-page-3-title", + content: "foo-page-3-content", + id: "foo-page-3-id", + }, +]; + +export const flatPagesWithRoot: ConfluenceInputPage[] = [ + { + id: "foo-page-1-id", + title: "foo-page-1-title", + content: "foo-page-1-content", + }, + { + title: "foo-page-3-title", + content: "foo-page-3-content", + }, +]; + +export const renamedPage: ConfluenceInputPage[] = [ + { + title: "foo-renamed-title", + content: "foo-parent-content", + ancestors: ["foo-root-title"], + }, + { + title: "foo-child1-title", + content: "foo-child1-content", + ancestors: ["foo-root-title", "foo-renamed-title"], + }, + { + title: "foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: ["foo-root-title", "foo-renamed-title", "foo-child1-title"], + }, + { + title: "foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: ["foo-root-title", "foo-renamed-title", "foo-child1-title"], + }, +]; diff --git a/components/confluence-sync/test/component/support/fixtures/image.png b/components/confluence-sync/test/component/support/fixtures/image.png new file mode 100644 index 00000000..9b5a9620 Binary files /dev/null and b/components/confluence-sync/test/component/support/fixtures/image.png differ diff --git a/components/confluence-sync/test/component/support/mock/Client.ts b/components/confluence-sync/test/component/support/mock/Client.ts new file mode 100644 index 00000000..79f2b5b2 --- /dev/null +++ b/components/confluence-sync/test/component/support/mock/Client.ts @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AdminApiClient } from "@mocks-server/admin-api-client"; +import crossFetch from "cross-fetch"; + +import type { SpyRequest } from "../../../../mocks/support/SpyStorage.types"; + +const BASE_URL = "http://127.0.0.1:3100"; + +const DEFAULT_REQUEST_OPTIONS = { + method: "GET", +}; + +function mockUrl(path: string) { + return `${BASE_URL}/${path}`; +} + +async function doRequest(path: string, options: RequestInit = {}) { + const response = await crossFetch(mockUrl(path), { + ...DEFAULT_REQUEST_OPTIONS, + ...options, + }); + return response.json(); +} + +export function resetRequests(): Promise { + return doRequest("spy/requests", { + method: "DELETE", + }); +} + +export function getRequests(): Promise { + return doRequest("spy/requests"); +} + +export async function getRequestsByRouteId( + routeId: string, +): Promise { + const requests = await getRequests(); + return requests.filter((request) => request.routeId === routeId); +} + +export async function changeMockCollection(collectionId: string) { + const mockClient = new AdminApiClient(); + await mockClient.updateConfig({ + mock: { + collections: { + selected: collectionId, + }, + }, + }); +} diff --git a/components/confluence-sync/test/component/tsconfig.json b/components/confluence-sync/test/component/tsconfig.json new file mode 100644 index 00000000..cabd5a36 --- /dev/null +++ b/components/confluence-sync/test/component/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@src/*": ["../../src/*"], + "@support/*": ["./support/*"] + } + }, + "include": ["**/*"] +} diff --git a/components/confluence-sync/test/unit/specs/ConfluenceSyncPages.spec.ts b/components/confluence-sync/test/unit/specs/ConfluenceSyncPages.spec.ts new file mode 100644 index 00000000..905abf08 --- /dev/null +++ b/components/confluence-sync/test/unit/specs/ConfluenceSyncPages.spec.ts @@ -0,0 +1,699 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { readFile } from "node:fs/promises"; + +import type { FileResult } from "tmp"; + +import { + convertPagesAncestorsToConfluenceAncestors, + createChildPage, + createConfluencePage, + createInputPage, + createInputTree, + createTree, +} from "@support/fixtures/Pages"; +import { customClient } from "@support/mocks/CustomConfluenceClient"; +import { TempFiles } from "@support/utils/TempFiles"; +const { fileSync } = new TempFiles(); + +import type { + ConfluencePage, + ConfluencePageBasicInfo, +} from "@src/confluence/CustomConfluenceClient.types"; +import { ConfluenceSyncPages } from "@src/ConfluenceSyncPages"; +import { + SyncModes, + type ConfluenceInputPage, + type ConfluencePagesDictionaryItem, + type ConfluenceSyncPagesInterface, +} from "@src/ConfluenceSyncPages.types"; + +import { CompoundError } from "../../../src/errors/CompoundError"; +import { cleanLogs } from "../support/Logs"; + +describe("confluenceSyncPages", () => { + let confluenceSynchronizer: ConfluenceSyncPagesInterface; + let confluenceTree: ConfluencePage[]; + let pages: ConfluenceInputPage[]; + let rootPage: ConfluencePage; + let rootPageAsAncestor: ConfluencePageBasicInfo; + let dictionary: Record; + let tempFile: FileResult; + let tempFile2: FileResult; + let file1: Promise; + let file2: Promise; + + beforeEach(() => { + confluenceSynchronizer = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + rootPageId: "foo-root-id", + url: "foo-url", + }); + + confluenceTree = createTree(); + pages = createInputTree(); + rootPage = createConfluencePage({ + name: "root", + children: [{ id: confluenceTree[0].id, title: confluenceTree[0].title }], + }); + rootPageAsAncestor = { id: rootPage.id, title: rootPage.title }; + confluenceTree.forEach((page) => { + if (page.ancestors) { + page.ancestors.unshift({ id: rootPage.id, title: rootPage.title }); + } else { + page.ancestors = [{ id: rootPage.id, title: rootPage.title }]; + } + }); + pages.forEach((page) => { + if (page.ancestors) { + page.ancestors.unshift(rootPage.title); + } else { + page.ancestors = [rootPage.title]; + } + }); + dictionary = {}; + dictionary[rootPage.id] = rootPage; + confluenceTree.forEach((page) => { + dictionary[page.id] = page; + }); + + customClient.getPage.mockImplementation((id) => { + return dictionary[id]; + }); + customClient.createPage.mockImplementation((page) => { + return Promise.resolve({ + id: `foo-${page.title}-id`, + title: page.title, + version: 1, + content: page.content, + children: [], + ancestors: page.ancestors, + }); + }); + customClient.getAttachments.mockImplementation(() => []); + tempFile = fileSync(); + tempFile2 = fileSync(); + file1 = readFile(tempFile.name); + file2 = readFile(tempFile2.name); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("sync method", () => { + describe("when root page has children", () => { + it("should call confluence client to get the root page and all its children to create the tree", async () => { + await confluenceSynchronizer.sync(pages); + + expect(customClient.getPage).toHaveBeenCalledTimes(8); + expect(customClient.getPage).toHaveBeenCalledWith("foo-root-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-parent-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-child1-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-child2-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-grandChild1-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-grandChild2-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-grandChild3-id"); + expect(customClient.getPage).toHaveBeenCalledWith("foo-grandChild4-id"); + }); + + it("should call confluence client to create the pages that are in input pages but are not in the tree", async () => { + const createdChild1 = createChildPage(pages[2], "createdChild1"); + const createdChild2 = createChildPage(pages[3], "createdChild2"); + await confluenceSynchronizer.sync([ + ...pages, + createdChild1, + createdChild2, + ]); + + expect(customClient.createPage).toHaveBeenCalledTimes(2); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdChild1, + ancestors: [ + rootPageAsAncestor, + ...convertPagesAncestorsToConfluenceAncestors( + pages[2], + confluenceTree, + ), + { id: confluenceTree[2].id, title: confluenceTree[2].title }, + ], + }); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdChild2, + ancestors: [ + rootPageAsAncestor, + ...convertPagesAncestorsToConfluenceAncestors( + pages[3], + confluenceTree, + ), + { id: confluenceTree[3].id, title: confluenceTree[3].title }, + ], + }); + }); + + it("should call confluence client to update the pages that are in the tree and are in input pages", async () => { + await confluenceSynchronizer.sync(pages); + + expect(customClient.updatePage).toHaveBeenCalledTimes(7); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[0].id, + version: confluenceTree[0].version + 1, + ...pages[0], + ancestors: confluenceTree[0].ancestors, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[1].id, + version: confluenceTree[1].version + 1, + ...pages[1], + ancestors: confluenceTree[1].ancestors, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[2].id, + version: confluenceTree[2].version + 1, + ...pages[2], + ancestors: confluenceTree[2].ancestors, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[3].id, + version: confluenceTree[3].version + 1, + ...pages[3], + ancestors: confluenceTree[3].ancestors, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[4].id, + version: confluenceTree[4].version + 1, + ...pages[4], + ancestors: confluenceTree[4].ancestors, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[5].id, + version: confluenceTree[5].version + 1, + ...pages[5], + ancestors: confluenceTree[5].ancestors, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: confluenceTree[6].id, + version: confluenceTree[6].version + 1, + ...pages[6], + ancestors: confluenceTree[6].ancestors, + }); + }); + + it("should call confluence client to delete the pages that are not in input pages but are in the tree", async () => { + const pagesSlice = pages.slice(0, 5); + await confluenceSynchronizer.sync(pagesSlice); + + expect(customClient.deleteContent).toHaveBeenCalledTimes(2); + expect(customClient.deleteContent).toHaveBeenCalledWith( + confluenceTree[5].id, + ); + expect(customClient.deleteContent).toHaveBeenCalledWith( + confluenceTree[6].id, + ); + }); + + describe("when there are pages to create that have attachments", () => { + it("should call confluence client to create the pages and its attachments", async () => { + const tempAttachments = {} as Record; + tempAttachments["tempFile"] = tempFile.name; + tempAttachments["tempFile2"] = tempFile2.name; + const files = [await file1, await file2]; + const createdPage = { + ...createChildPage(pages[0], "createdPage"), + attachments: tempAttachments, + }; + await confluenceSynchronizer.sync([...pages, createdPage]); + + expect(customClient.createPage).toHaveBeenCalledTimes(1); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdPage, + ancestors: [ + rootPageAsAncestor, + { id: confluenceTree[0].id, title: confluenceTree[0].title }, + ], + }); + expect(customClient.createAttachments).toHaveBeenCalledTimes(1); + expect(customClient.createAttachments).toHaveBeenCalledWith( + `foo-${createdPage.title}-id`, + Object.entries( + createdPage.attachments as Record, + ).map((attachment, i) => ({ + filename: attachment[0], + file: files[i], + })), + ); + }); + }); + + describe("when there are pages to update that have attachments", () => { + describe("when the confluence page has attachments too", () => { + let attachments: ConfluencePageBasicInfo[]; + + beforeEach(() => { + customClient.getAttachments.mockImplementation((id) => { + attachments = [ + { id: "foo-attachment-id", title: "foo-attachment-title" }, + { id: "foo-attachment-id-2", title: "foo-attachment-title-2" }, + ]; + if (id === confluenceTree[0].id) { + return attachments; + } else return []; + }); + }); + + it("should call confluence client to delete the attachments and create the new ones", async () => { + const tempAttachments = {} as Record; + tempAttachments["tempFile"] = tempFile.name; + tempAttachments["tempFile2"] = tempFile2.name; + const files = [await file1, await file2]; + const updatedPage = { ...pages[0], attachments: tempAttachments }; + await confluenceSynchronizer.sync([...pages.slice(1), updatedPage]); + + expect(customClient.deleteContent).toHaveBeenCalledWith( + attachments[0].id, + ); + expect(customClient.deleteContent).toHaveBeenCalledWith( + attachments[1].id, + ); + expect(customClient.createAttachments).toHaveBeenCalledWith( + confluenceTree[0].id, + Object.entries( + updatedPage.attachments as Record, + ).map((attachment, i) => ({ + filename: attachment[0], + file: files[i], + })), + ); + }); + }); + }); + + describe("when there are pages that are not in the tree and have children", () => { + it("should end up creating the pages and its children", async () => { + const createdPageParent = createChildPage( + pages[0], + "createdPageParent", + ); + const createdPageChild = createChildPage( + createdPageParent, + "createdPageChild", + ); + await confluenceSynchronizer.sync([ + ...pages, + createdPageParent, + createdPageChild, + ]); + + expect(customClient.createPage).toHaveBeenCalledTimes(2); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdPageParent, + ancestors: [ + rootPageAsAncestor, + { id: confluenceTree[0].id, title: confluenceTree[0].title }, + ], + }); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdPageChild, + ancestors: [ + rootPageAsAncestor, + { id: confluenceTree[0].id, title: confluenceTree[0].title }, + { + id: `foo-${createdPageParent.title}-id`, + title: createdPageParent.title, + }, + ], + }); + }); + + describe("when one of the page to create returns an error", () => { + beforeEach(() => { + customClient.createPage.mockImplementation((page) => { + if (page.title === "foo-wrongPage-title") + return Promise.reject(new Error("error")); + else { + return Promise.resolve({ + id: `foo-${page.title}-id`, + title: page.title, + version: 1, + content: page.content, + children: [], + ancestors: page.ancestors, + }); + } + }); + }); + + it("should create the other pages and throw an error", async () => { + const createdPageParent = createChildPage( + pages[0], + "createdPageParent", + ); + const createdPageChild = createChildPage( + createdPageParent, + "createdPageChild", + ); + const wrongPage = createChildPage(pages[0], "wrongPage"); + + await expect( + confluenceSynchronizer.sync([ + ...pages, + createdPageParent, + createdPageChild, + wrongPage, + ]), + ).rejects.toThrow(CompoundError); + + expect(customClient.createPage).toHaveBeenCalledTimes(3); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdPageParent, + ancestors: [ + rootPageAsAncestor, + { id: confluenceTree[0].id, title: confluenceTree[0].title }, + ], + }); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...createdPageChild, + ancestors: [ + rootPageAsAncestor, + { id: confluenceTree[0].id, title: confluenceTree[0].title }, + { + id: `foo-${createdPageParent.title}-id`, + title: createdPageParent.title, + }, + ], + }); + }); + }); + }); + + describe("when there are pages that are not in the tree whose parent is not in the tree or in the input pages either", () => { + let missingPage: ConfluenceInputPage; + let wrongPage1: ConfluenceInputPage; + let wrongPage2: ConfluenceInputPage; + let wrongInputPages: ConfluenceInputPage[]; + + beforeEach(() => { + missingPage = createChildPage(pages[0], "missingPage"); + wrongPage1 = createChildPage(missingPage, "wrongPage1"); + wrongPage2 = createChildPage(wrongPage1, "wrongPage2"); + wrongInputPages = [...pages, wrongPage1, wrongPage2]; + }); + + it("should throw an error because it is not possible to create the page", async () => { + await expect( + confluenceSynchronizer.sync(wrongInputPages), + ).rejects.toThrow( + `There still are 2 pages to create after sync: ${wrongPage1.title}, ${wrongPage2.title}, check if they have their ancestors created.`, + ); + }); + + it("should log the pages that are not possible to create", async () => { + confluenceSynchronizer.logger.setLevel("error", { + transport: "store", + }); + try { + await confluenceSynchronizer.sync(wrongInputPages); + } catch { + // do nothing + } + + expect(cleanLogs(confluenceSynchronizer.logger.store)).toEqual( + expect.arrayContaining([ + expect.stringContaining( + `There still are 2 pages to create after sync: ${wrongPage1.title}, ${wrongPage2.title}, check if they have their ancestors created.`, + ), + ]), + ); + }); + }); + }); + + describe("when input pages has no ancestors", () => { + it("should add root ancestor by default and create that pages under root page", async () => { + const pageWithoutAncestor1 = createInputPage({ + name: "pageWithoutAncestors1", + }); + const pageWithoutAncestor2 = createInputPage({ + name: "pageWithoutAncestors2", + }); + await confluenceSynchronizer.sync([ + pageWithoutAncestor1, + pageWithoutAncestor2, + ]); + + expect(customClient.createPage).toHaveBeenCalledTimes(2); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...pageWithoutAncestor1, + ancestors: [rootPageAsAncestor], + }); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...pageWithoutAncestor2, + ancestors: [rootPageAsAncestor], + }); + }); + }); + + describe("when root page children as undefined", () => { + beforeEach(() => { + customClient.getPage.mockImplementation((id) => { + if (id === rootPage.id) { + return { + ...rootPage, + children: undefined, + }; + } else { + return dictionary[id]; + } + }); + }); + + it("should call only once to getPage", async () => { + const pageToBeCreated = createInputPage({ name: "pageToBeCreated" }); + await confluenceSynchronizer.sync([pageToBeCreated]); + + expect(customClient.getPage.mock.calls[0][0]).toEqual(rootPage.id); + expect(customClient.getPage).toHaveBeenCalledTimes(1); + }); + }); + + it("should have silent log level by default", async () => { + const confluenceSynchronizer2 = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + rootPageId: "foo-root-id", + url: "foo-url", + }); + await confluenceSynchronizer2.sync([]); + + expect(confluenceSynchronizer2.logger.store).toHaveLength(0); + }); + + describe("when flat mode is enabled and root page is not provided", () => { + let pagesToUpdate: ConfluenceInputPage[]; + + beforeEach(() => { + confluenceSynchronizer = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + syncMode: SyncModes.FLAT, + url: "foo-url", + }); + pagesToUpdate = [ + createInputPage({ name: "pageToUpdate" }), + createInputPage({ name: "pageToUpdate2" }), + createInputPage({ name: "pageToUpdate3" }), + createInputPage({ name: "pageToUpdate4" }), + ].map((page, i) => ({ ...page, id: `foo-page${i}-id` })); + const pageMap = Object.fromEntries( + pagesToUpdate.map((page) => [page.id, { ...page, version: 1 }]), + ); + customClient.getPage.mockImplementation((id) => { + if (id === "foo-inexistent-id") { + throw new Error(`No content found with id ${id}`); + } else { + return pageMap[id]; + } + }); + }); + + it("should update the pages passed as input", async () => { + await confluenceSynchronizer.sync(pagesToUpdate); + + expect(customClient.updatePage).toHaveBeenCalledTimes(4); + expect(customClient.updatePage).toHaveBeenCalledWith({ + ...pagesToUpdate[0], + version: 2, + }); + }); + + it("should fail if any of the pages has no id", async () => { + const wrongPage = createInputPage({ name: "wrongPage" }); + + await expect( + confluenceSynchronizer.sync([...pagesToUpdate, wrongPage]), + ).rejects.toThrow( + `rootPageId is required for FLAT sync mode when there are pages without id: ${wrongPage.title}`, + ); + }); + + it("should fail if any of the pages does not exist in confluence but still try to update the rest", async () => { + const inexistentPage = { + ...createInputPage({ name: "inexistentPage" }), + id: "foo-inexistent-id", + }; + + await expect( + confluenceSynchronizer.sync([...pagesToUpdate, inexistentPage]), + ).rejects.toThrow(`No content found with id ${inexistentPage.id}`); + expect(customClient.updatePage).toHaveBeenCalledTimes(4); + }); + }); + + describe("when flat mode is enabled and root page is provided", () => { + let pagesToSync: ConfluenceInputPage[]; + let pagesToCreate: ConfluenceInputPage[]; + let pagesToUpdateWithId: ConfluenceInputPage[]; + let pageToUpdateInAlreadyTree: ConfluenceInputPage[]; + + beforeEach(() => { + confluenceSynchronizer = new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + rootPageId: "foo-root-id", + syncMode: SyncModes.FLAT, + url: "foo-url", + }); + pagesToCreate = [createInputPage({ name: "pageToCreate" })]; + pagesToUpdateWithId = [ + { id: "foo-page1-id", ...createInputPage({ name: "pageToUpdate1" }) }, + { id: "foo-page2-id", ...createInputPage({ name: "pageToUpdate2" }) }, + ]; + pageToUpdateInAlreadyTree = [ + createInputPage({ name: "pageToUpdateInTree" }), + ]; + pagesToSync = [ + ...pagesToUpdateWithId, + ...pagesToCreate, + ...pageToUpdateInAlreadyTree, + ]; + customClient.getPage.mockImplementation((id) => { + const pageMap = Object.fromEntries( + pagesToUpdateWithId.map((page) => [ + page.id, + { ...page, version: 1 }, + ]), + ); + pageMap[rootPage.id] = { + ...rootPage, + children: [ + pagesToUpdateWithId[0], + { + id: "foo-pageToDelete-id", + title: "pageToDelete", + }, + { + ...pageToUpdateInAlreadyTree[0], + id: "foo-pageToUpdateInTree-id", + }, + ], + }; + pageMap["foo-pageToDelete-id"] = { + id: "foo-pageToDelete-id", + title: "pageToDelete", + }; + pageMap["foo-pageToUpdateInTree-id"] = { + ...pageToUpdateInAlreadyTree[0], + id: "foo-pageToUpdateInTree-id", + version: 1, + }; + + return pageMap[id]; + }); + }); + + it("should update the pages with id", async () => { + await confluenceSynchronizer.sync(pagesToSync); + + expect(customClient.updatePage).toHaveBeenCalledWith({ + ...pagesToUpdateWithId[0], + version: 2, + }); + expect(customClient.updatePage).toHaveBeenCalledWith({ + ...pagesToUpdateWithId[1], + version: 2, + }); + }); + + it("should create the pages without id, that are not under root page", async () => { + await confluenceSynchronizer.sync(pagesToCreate); + + expect(customClient.createPage).toHaveBeenCalledTimes(1); + expect(customClient.createPage).toHaveBeenCalledWith({ + ...pagesToCreate[0], + ancestors: [rootPageAsAncestor], + }); + }); + + it("should delete the pages that are under root page but not in the input", async () => { + await confluenceSynchronizer.sync(pagesToSync); + + expect(customClient.deleteContent).toHaveBeenCalledTimes(1); + expect(customClient.deleteContent).toHaveBeenCalledWith( + "foo-pageToDelete-id", + ); + }); + + it("should update the pages that are under root page and in the input", async () => { + await confluenceSynchronizer.sync(pagesToSync); + + expect(customClient.updatePage).toHaveBeenCalledWith({ + id: "foo-pageToUpdateInTree-id", + ...pageToUpdateInAlreadyTree[0], + version: 2, + }); + }); + + it("should log a warning if any of the pages that are under root page and in the input has id", async () => { + confluenceSynchronizer.logger.setLevel("warn"); + await confluenceSynchronizer.sync(pagesToSync); + + expect(cleanLogs(confluenceSynchronizer.logger.store)).toEqual( + expect.arrayContaining([ + expect.stringContaining( + `Some children of root page contains id: ${pagesToSync[0].title}`, + ), + ]), + ); + }); + + it("should fail if any of the pages without id has ancestors", async () => { + const wrongPage = createInputPage({ + name: "wrongPage", + ancestors: ["ancestor"], + }); + + await expect( + confluenceSynchronizer.sync([...pagesToSync, wrongPage]), + ).rejects.toThrow( + `Pages with ancestors are not supported in FLAT sync mode: ${wrongPage.title}`, + ); + }); + }); + + describe("when sync mode is tree but rootPageId is not passed", () => { + it("should throw an error", () => { + expect( + () => + new ConfluenceSyncPages({ + personalAccessToken: "foo-token", + spaceId: "foo-space-id", + syncMode: SyncModes.TREE, + url: "foo-url", + }), + ).toThrow("rootPageId is required for TREE sync mode"); + }); + }); + }); +}); diff --git a/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts b/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts new file mode 100644 index 00000000..9c7d696b --- /dev/null +++ b/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts @@ -0,0 +1,694 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import type { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; +import type { Models } from "confluence.js"; + +import { cleanLogs } from "@support/Logs"; +import { confluenceClient } from "@support/mocks/ConfluenceClient"; + +import { CustomConfluenceClient } from "@src/confluence/CustomConfluenceClient"; +import type { + Attachments, + ConfluenceClientConfig, + ConfluenceClientInterface, + ConfluencePage, +} from "@src/index"; + +describe("customConfluenceClient class", () => { + let logger: LoggerInterface; + let config: ConfluenceClientConfig; + let customConfluenceClient: ConfluenceClientInterface; + let page: ConfluencePage; + let defaultResponse: Models.Content; + + beforeEach(() => { + logger = new Logger("", { level: "error" }); + logger.setLevel("silent", { transport: "console" }); + config = { + spaceId: "foo-space-id", + url: "foo-url", + personalAccessToken: "foo-token", + logger, + }; + page = { + title: "foo-title", + id: "foo-id", + content: "foo-content", + version: 1, + ancestors: [{ id: "foo-id-ancestor", title: "foo-ancestor" }], + }; + customConfluenceClient = new CustomConfluenceClient(config); + + defaultResponse = { + title: "foo-title", + id: "foo-id", + version: { number: 1 } as Models.Version, + ancestors: [ + { id: "foo-id-ancestor", title: "foo-ancestor", type: "page" }, + ] as Models.Content[], + } as Models.Content; + }); + + describe("getPage method", () => { + it("should call confluence.js lib to get a page getting the body, ancestors, version and children, and passing the id", async () => { + await customConfluenceClient.getPage("foo-id"); + + expect(confluenceClient.content.getContentById).toHaveBeenCalledWith({ + id: "foo-id", + expand: ["ancestors", "version.number", "children.page"], + }); + }); + + it("should return a page with the right properties", async () => { + confluenceClient.content.getContentById.mockImplementation(() => ({ + title: "foo-title", + id: "foo-id", + version: { number: 1 }, + ancestors: [ + { id: "foo-id-ancestor", title: "foo-ancestor", type: "page" }, + ], + children: { + page: { + results: [ + { id: "foo-child-1-id", title: "foo-child-1" }, + { id: "foo-child-2-id", title: "foo-child-2" }, + ], + }, + }, + })); + const response = await customConfluenceClient.getPage("foo-id"); + + expect(response).toEqual({ + title: "foo-title", + id: "foo-id", + version: 1, + ancestors: [{ id: "foo-id-ancestor", title: "foo-ancestor" }], + children: [ + { id: "foo-child-1-id", title: "foo-child-1" }, + { id: "foo-child-2-id", title: "foo-child-2" }, + ], + }); + }); + + it("should throw an error if confluence.js lib throws an error", async () => { + jest + .spyOn(confluenceClient.content, "getContentById") + .mockImplementation() + .mockRejectedValueOnce("foo-error"); + + await expect(customConfluenceClient.getPage("foo-id")).rejects.toThrow( + "Error getting page with id foo-id: foo-error", + ); + }); + }); + + describe("createPage method", () => { + it("should call confluence.js lib to create a page with right parameters", async () => { + await customConfluenceClient.createPage(page); + + expect(confluenceClient.content.createContent).toHaveBeenCalledWith({ + type: "page", + title: page.title, + space: { + key: config.spaceId, + }, + ancestors: [{ id: page.ancestors?.at(-1)?.id }], + body: { + storage: { + value: page.content, + representation: "storage", + }, + }, + }); + }); + + it("should return the created page with the right properties", async () => { + confluenceClient.content.createContent.mockImplementation( + () => defaultResponse, + ); + const response = await customConfluenceClient.createPage(page); + + expect(response).toEqual({ + ...defaultResponse, + version: 1, + ancestors: [{ id: "foo-id-ancestor", title: "foo-ancestor" }], + }); + }); + + describe("when the page has no ancestors", () => { + it("should call confluence.js lib to create a page with right parameters but ancestor", async () => { + await customConfluenceClient.createPage({ + ...page, + ancestors: undefined, + }); + + expect(confluenceClient.content.createContent).toHaveBeenCalledWith({ + type: "page", + title: page.title, + space: { + key: config.spaceId, + }, + body: { + storage: { + value: page.content, + representation: "storage", + }, + }, + }); + }); + }); + + describe("when the page has no content", () => { + it("should call confluence.js lib to create a page with right parameters but content", async () => { + await customConfluenceClient.createPage({ + ...page, + content: undefined, + }); + + expect(confluenceClient.content.createContent).toHaveBeenCalledWith({ + type: "page", + title: page.title, + ancestors: [{ id: page.ancestors?.at(-1)?.id }], + space: { + key: config.spaceId, + }, + body: { + storage: { + value: "", + representation: "storage", + }, + }, + }); + }); + }); + + it("should throw an error if confluence.js lib throws an axios bad request error", async () => { + jest + .spyOn(confluenceClient.content, "createContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 400, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.createPage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error creating page with title foo-title: Error: Bad Request", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios unauthorized error", async () => { + jest + .spyOn(confluenceClient.content, "createContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 401, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.createPage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error creating page with title foo-title: Error: Unauthorized", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios forbidden error", async () => { + jest + .spyOn(confluenceClient.content, "createContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 403, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.createPage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error creating page with title foo-title: Error: Unauthorized", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios internal server error", async () => { + jest + .spyOn(confluenceClient.content, "createContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 500, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.createPage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error creating page with title foo-title: Error: Internal Server Error", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios any error", async () => { + jest + .spyOn(confluenceClient.content, "createContent") + .mockImplementation() + .mockRejectedValueOnce(new AxiosError()); + + await expect(customConfluenceClient.createPage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error creating page with title foo-title: Error: Axios Error", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an error", async () => { + jest + .spyOn(confluenceClient.content, "createContent") + .mockImplementation() + .mockRejectedValueOnce("foo-error"); + + await expect(customConfluenceClient.createPage(page)).rejects.toThrow( + "Error creating page with title foo-title: Error: Unexpected Error: foo-error", + ); + }); + }); + + describe("updatePage method", () => { + it("should call confluence.js lib to update a page with right parameters", async () => { + await customConfluenceClient.updatePage(page); + + expect(confluenceClient.content.updateContent).toHaveBeenCalledWith({ + id: page.id, + type: "page", + title: page.title, + ancestors: [{ id: page.ancestors?.at(-1)?.id }], + version: { + number: page.version, + }, + body: { + storage: { + value: page.content, + representation: "storage", + }, + }, + }); + }); + + it("should return the updated page with the right properties", async () => { + confluenceClient.content.updateContent.mockImplementation( + () => defaultResponse, + ); + const response = await customConfluenceClient.updatePage(page); + + expect(response).toEqual({ + ...defaultResponse, + version: 1, + ancestors: [{ id: "foo-id-ancestor", title: "foo-ancestor" }], + }); + }); + + describe("when the page has no ancestors", () => { + it("should call confluence.js lib to update a page with right parameters but ancestor", async () => { + await customConfluenceClient.updatePage({ + ...page, + ancestors: undefined, + }); + + expect(confluenceClient.content.updateContent).toHaveBeenCalledWith({ + id: page.id, + type: "page", + title: page.title, + version: { + number: page.version, + }, + body: { + storage: { + value: page.content, + representation: "storage", + }, + }, + }); + }); + }); + + describe("when the page has no content", () => { + it("should call confluence.js lib to update a page with right parameters but content", async () => { + await customConfluenceClient.updatePage({ + ...page, + content: undefined, + }); + + expect(confluenceClient.content.updateContent).toHaveBeenCalledWith({ + id: page.id, + type: "page", + title: page.title, + ancestors: [{ id: page.ancestors?.at(-1)?.id }], + version: { + number: page.version, + }, + body: { + storage: { + value: "", + representation: "storage", + }, + }, + }); + }); + }); + + it("should throw an error if confluence.js lib throws an axios bad request error", async () => { + jest + .spyOn(confluenceClient.content, "updateContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 400, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.updatePage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error updating page with id foo-id and title foo-title: Error: Bad Request", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios unauthorized error", async () => { + jest + .spyOn(confluenceClient.content, "updateContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 401, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.updatePage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error updating page with id foo-id and title foo-title: Error: Unauthorized", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios forbidden error", async () => { + jest + .spyOn(confluenceClient.content, "updateContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 403, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.updatePage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error updating page with id foo-id and title foo-title: Error: Unauthorized", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios internal server error", async () => { + jest + .spyOn(confluenceClient.content, "updateContent") + .mockImplementation() + .mockRejectedValueOnce( + new AxiosError(undefined, undefined, undefined, undefined, { + status: 500, + } as AxiosResponse), + ); + + await expect(customConfluenceClient.updatePage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error updating page with id foo-id and title foo-title: Error: Internal Server Error", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an axios any error", async () => { + jest + .spyOn(confluenceClient.content, "updateContent") + .mockImplementation() + .mockRejectedValueOnce(new AxiosError()); + + await expect(customConfluenceClient.updatePage(page)).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "Error updating page with id foo-id and title foo-title: Error: Axios Error", + ), + }), + ); + }); + + it("should throw an error if confluence.js lib throws an error", async () => { + jest + .spyOn(confluenceClient.content, "updateContent") + .mockImplementation() + .mockRejectedValueOnce("foo-error"); + + await expect(customConfluenceClient.updatePage(page)).rejects.toThrow( + "Error updating page with id foo-id and title foo-title: Error: Unexpected Error: foo-error", + ); + }); + }); + + describe("deleteContent method", () => { + it("should call confluence.js lib to delete a content with the id passed", async () => { + await customConfluenceClient.deleteContent(page.id); + + expect(confluenceClient.content.deleteContent).toHaveBeenCalledWith({ + id: page.id, + }); + }); + + it("should throw an error if confluence.js lib throws an error", async () => { + jest + .spyOn(confluenceClient.content, "deleteContent") + .mockImplementation() + .mockRejectedValueOnce("foo-error"); + + await expect( + customConfluenceClient.deleteContent(page.id), + ).rejects.toThrow("Error deleting content with id foo-id: foo-error"); + }); + }); + + describe("getAttachments method", () => { + it("should call confluence.js lib to get attachments with the id passed", async () => { + await customConfluenceClient.getAttachments(page.id); + + expect( + confluenceClient.contentAttachments.getAttachments, + ).toHaveBeenCalledWith({ + id: page.id, + }); + }); + + it("should return the attachments with the right properties", async () => { + confluenceClient.contentAttachments.getAttachments.mockImplementation( + () => ({ + results: [ + { + id: "foo-id-attachment", + title: "foo-attachment", + _links: { + download: "foo-download", + }, + }, + ], + }), + ); + const response = await customConfluenceClient.getAttachments(page.id); + + expect(response).toEqual([ + { + id: "foo-id-attachment", + title: "foo-attachment", + }, + ]); + }); + + it("should throw an error if confluence.js lib throws an error", async () => { + confluenceClient.contentAttachments.getAttachments.mockRejectedValueOnce( + "foo-error", + ); + + await expect( + customConfluenceClient.getAttachments(page.id), + ).rejects.toThrow( + "Error getting attachments of page with id foo-id: foo-error", + ); + }); + }); + + describe("createAttachments method", () => { + let attachments: Attachments; + + beforeAll(() => { + attachments = [ + { + filename: "foo-name", + file: new ArrayBuffer(8) as Buffer, + }, + ]; + }); + + it("should call confluence.js lib to create the attachments of a page with the id passed", async () => { + await customConfluenceClient.createAttachments(page.id, attachments); + + expect( + confluenceClient.contentAttachments.createAttachments, + ).toHaveBeenCalledWith({ + id: page.id, + attachments: [ + { + filename: "foo-name", + minorEdit: true, + file: new ArrayBuffer(8), + }, + ], + }); + }); + + it("should throw an error if confluence.js lib throws an error", async () => { + confluenceClient.contentAttachments.createAttachments.mockRejectedValueOnce( + "foo-error", + ); + + await expect( + customConfluenceClient.createAttachments(page.id, attachments), + ).rejects.toThrow( + "Error creating attachments of page with id foo-id: foo-error", + ); + }); + }); + + describe("when dryRun mode is enabled", () => { + let customConfluenceClientDryRun: ConfluenceClientInterface; + + beforeEach(() => { + customConfluenceClientDryRun = new CustomConfluenceClient({ + ...config, + dryRun: true, + }); + }); + + describe("createPage method", () => { + it("should not call confluence.js lib to create a page", async () => { + await customConfluenceClientDryRun.createPage(page); + + expect(confluenceClient.content.createContent).not.toHaveBeenCalled(); + }); + + it("should log the page that would have been created", async () => { + logger.setLevel("info", { transport: "store" }); + + await customConfluenceClientDryRun.createPage(page); + + expect( + cleanLogs(customConfluenceClientDryRun.logger.globalStore), + ).toContain(`Dry run: creating page with title ${page.title}`); + }); + }); + + describe("updatePage method", () => { + it("should not call confluence.js lib to update a page", async () => { + await customConfluenceClientDryRun.updatePage(page); + + expect(confluenceClient.content.updateContent).not.toHaveBeenCalled(); + }); + + it("should log the page that would have been updated", async () => { + logger.setLevel("info", { transport: "store" }); + + await customConfluenceClientDryRun.updatePage(page); + + expect( + cleanLogs(customConfluenceClientDryRun.logger.globalStore), + ).toContain(`Dry run: updating page with title ${page.title}`); + }); + }); + + describe("deleteContent method", () => { + it("should not call confluence.js lib to delete a page", async () => { + await customConfluenceClientDryRun.deleteContent(page.id); + + expect(confluenceClient.content.deleteContent).not.toHaveBeenCalled(); + }); + + it("should log the page that would have been deleted", async () => { + logger.setLevel("info", { transport: "store" }); + + await customConfluenceClientDryRun.deleteContent(page.id); + + expect( + cleanLogs(customConfluenceClientDryRun.logger.globalStore), + ).toContain(`Dry run: deleting content with id ${page.id}`); + }); + }); + + describe("createAttachments method", () => { + let attachments: Attachments; + + beforeAll(() => { + attachments = [ + { + filename: "foo-name", + file: new ArrayBuffer(8) as Buffer, + }, + ]; + }); + + it("should not call confluence.js lib to create the attachments of a page", async () => { + await customConfluenceClientDryRun.createAttachments( + page.id, + attachments, + ); + + expect( + confluenceClient.contentAttachments.createAttachments, + ).not.toHaveBeenCalled(); + }); + + it("should log the attachments that would have been created", async () => { + logger.setLevel("info", { transport: "store" }); + + await customConfluenceClientDryRun.createAttachments( + page.id, + attachments, + ); + + expect( + cleanLogs(customConfluenceClientDryRun.logger.globalStore), + ).toContain( + `Dry run: creating attachments of page with id ${page.id}, attachments: ${attachments + .map((attachment) => attachment.filename) + .join(", ")}`, + ); + }); + }); + }); +}); diff --git a/components/confluence-sync/test/unit/support/Logs.ts b/components/confluence-sync/test/unit/support/Logs.ts new file mode 100644 index 00000000..9efc2ecf --- /dev/null +++ b/components/confluence-sync/test/unit/support/Logs.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export function cleanLogs(logs: string[]) { + return logs.map((log) => log.replace(/^(\S|\s)*\]/, "").trim()); +} diff --git a/components/confluence-sync/test/unit/support/fixtures/Pages.ts b/components/confluence-sync/test/unit/support/fixtures/Pages.ts new file mode 100644 index 00000000..10adfc02 --- /dev/null +++ b/components/confluence-sync/test/unit/support/fixtures/Pages.ts @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + ConfluencePage, + ConfluencePageBasicInfo, +} from "@src/confluence/types"; +import type { ConfluenceInputPage } from "@src/ConfluenceSyncPages.types"; + +export function createConfluencePage(options: { + name: string; + children?: ConfluencePageBasicInfo[]; + ancestors?: ConfluencePageBasicInfo[]; +}): ConfluencePage { + const basePage = { + id: `foo-${options.name}-id`, + title: `foo-${options.name}-title`, + version: 1, + content: `foo-${options.name}-content`, + children: options?.children, + ancestors: options?.ancestors, + }; + + return basePage; +} + +export function createInputPage(options: { + name: string; + ancestors?: string[]; +}): ConfluenceInputPage { + const inputPage = { + title: `foo-${options.name}-title`, + content: `foo-${options.name}-content`, + ancestors: options?.ancestors, + }; + + return inputPage; +} + +export function createTree(): ConfluencePage[] { + const parentPage = createConfluencePage({ name: "parent" }); + const childPage1 = createConfluencePage({ + name: "child1", + ancestors: [convertToPageBasicInfo(parentPage)], + }); + const childPage2 = createConfluencePage({ + name: "child2", + ancestors: [convertToPageBasicInfo(parentPage)], + }); + const grandChildPage1 = createConfluencePage({ + name: "grandChild1", + ancestors: [ + convertToPageBasicInfo(parentPage), + convertToPageBasicInfo(childPage1), + ], + }); + const grandChildPage2 = createConfluencePage({ + name: "grandChild2", + ancestors: [ + convertToPageBasicInfo(parentPage), + convertToPageBasicInfo(childPage1), + ], + }); + const grandChildPage3 = createConfluencePage({ + name: "grandChild3", + ancestors: [ + convertToPageBasicInfo(parentPage), + convertToPageBasicInfo(childPage2), + ], + }); + const grandChildPage4 = createConfluencePage({ + name: "grandChild4", + ancestors: [ + convertToPageBasicInfo(parentPage), + convertToPageBasicInfo(childPage2), + ], + }); + + childPage1.children = [ + convertToPageBasicInfo(grandChildPage1), + convertToPageBasicInfo(grandChildPage2), + ]; + childPage2.children = [ + convertToPageBasicInfo(grandChildPage3), + convertToPageBasicInfo(grandChildPage4), + ]; + parentPage.children = [ + convertToPageBasicInfo(childPage1), + convertToPageBasicInfo(childPage2), + ]; + + return [ + parentPage, + childPage1, + childPage2, + grandChildPage1, + grandChildPage2, + grandChildPage3, + grandChildPage4, + ]; +} + +export function createInputTree(): ConfluenceInputPage[] { + const parentPage = createInputPage({ name: "parent" }); + const childPage1 = createInputPage({ + name: "child1", + ancestors: [parentPage.title], + }); + const childPage2 = createInputPage({ + name: "child2", + ancestors: [parentPage.title], + }); + const grandChildPage1 = createInputPage({ + name: "grandChild1", + ancestors: [parentPage.title, childPage1.title], + }); + const grandChildPage2 = createInputPage({ + name: "grandChild2", + ancestors: [parentPage.title, childPage1.title], + }); + const grandChildPage3 = createInputPage({ + name: "grandChild3", + ancestors: [parentPage.title, childPage2.title], + }); + const grandChildPage4 = createInputPage({ + name: "grandChild4", + ancestors: [parentPage.title, childPage2.title], + }); + + return [ + parentPage, + childPage1, + childPage2, + grandChildPage1, + grandChildPage2, + grandChildPage3, + grandChildPage4, + ]; +} + +export function createChildPage( + parent: ConfluenceInputPage, + name: string, +): ConfluenceInputPage { + const parentAncestors = parent.ancestors || []; + const childPage = createInputPage({ + name, + ancestors: [...parentAncestors, parent.title], + }); + + return childPage; +} + +export function createAttachments( + page: ConfluenceInputPage, + count: number, +): ConfluenceInputPage { + const attachments: Record = {}; + for (let i = 0; i < count; i++) { + attachments[`${page.title}-attachment-${i}`] = + `${page.title}-path-to-attachment-${i}`; + } + return { ...page, attachments }; +} + +function convertToPageBasicInfo(page: ConfluencePage): ConfluencePageBasicInfo { + return { + id: page.id, + title: page.title, + }; +} + +export function convertPagesAncestorsToConfluenceAncestors( + page: ConfluenceInputPage, + confluenceTree: ConfluencePage[], +) { + const pageAncestors = page.ancestors || []; + return pageAncestors.slice(1).map((ancestor, i) => { + return { id: confluenceTree[i].id, title: ancestor }; + }); +} diff --git a/components/confluence-sync/test/unit/support/mocks/ConfluenceClient.ts b/components/confluence-sync/test/unit/support/mocks/ConfluenceClient.ts new file mode 100644 index 00000000..96f08089 --- /dev/null +++ b/components/confluence-sync/test/unit/support/mocks/ConfluenceClient.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("confluence.js"); + +import * as confluenceLibrary from "confluence.js"; + +export const confluenceClient = { + content: { + getContentById: jest.fn().mockResolvedValue({}), + createContent: jest.fn().mockResolvedValue({}), + updateContent: jest.fn().mockResolvedValue({}), + deleteContent: jest.fn().mockResolvedValue({}), + }, + contentAttachments: { + getAttachments: jest.fn().mockResolvedValue({}), + createAttachments: jest.fn().mockResolvedValue({}), + }, +}; + +/* ts ignore next line because it expects a mock with the same parameters as the confluence client + * but there are a lot of them useless for the test */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-next-line +jest.spyOn(confluenceLibrary, "ConfluenceClient").mockImplementation(() => { + return confluenceClient; +}); diff --git a/components/confluence-sync/test/unit/support/mocks/CustomConfluenceClient.ts b/components/confluence-sync/test/unit/support/mocks/CustomConfluenceClient.ts new file mode 100644 index 00000000..0adcd814 --- /dev/null +++ b/components/confluence-sync/test/unit/support/mocks/CustomConfluenceClient.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("@src/confluence/CustomConfluenceClient"); + +import type { LoggerInterface } from "@mocks-server/logger"; + +import * as customClientLib from "@src/confluence/CustomConfluenceClient"; + +export const customClient = { + getPage: jest.fn(), + createPage: jest.fn().mockImplementation((page) => Promise.resolve(page)), + updatePage: jest.fn().mockImplementation((page) => Promise.resolve(page)), + deleteContent: jest.fn(), + getAttachments: jest.fn(), + createAttachments: jest.fn(), + logger: {} as LoggerInterface, +}; + +jest.spyOn(customClientLib, "CustomConfluenceClient").mockImplementation(() => { + return customClient; +}); diff --git a/components/confluence-sync/test/unit/support/utils/TempFiles.ts b/components/confluence-sync/test/unit/support/utils/TempFiles.ts new file mode 100644 index 00000000..01e06d6a --- /dev/null +++ b/components/confluence-sync/test/unit/support/utils/TempFiles.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DirOptions, FileOptions } from "tmp"; +import { fileSync, dirSync } from "tmp"; + +/** + * Class to wrap tmp package functions and solve problems with windows + */ +export const TempFiles = class TempFiles { + public dirSync(this: void, options: DirOptions) { + return dirSync({ ...options }); + } + + /** + * FIX: Add discardDescriptor option to correct error when removing temporary + * files in Windows when using the removeCallback option of the dirSync function + * @param {FileOptions} options + * @returns {FileResult} + */ + public fileSync(this: void, options: FileOptions = {}) { + return fileSync({ discardDescriptor: true, ...options }); + } +}; diff --git a/components/confluence-sync/test/unit/tsconfig.json b/components/confluence-sync/test/unit/tsconfig.json new file mode 100644 index 00000000..cabd5a36 --- /dev/null +++ b/components/confluence-sync/test/unit/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@src/*": ["../../src/*"], + "@support/*": ["./support/*"] + } + }, + "include": ["**/*"] +} diff --git a/components/confluence-sync/tsconfig.base.json b/components/confluence-sync/tsconfig.base.json new file mode 100644 index 00000000..79bcbe89 --- /dev/null +++ b/components/confluence-sync/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "skipLibCheck": true, + "rootDir": ".", + "outDir": "dist", + "declaration": true, + "target": "ES2022", + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true, + "moduleResolution": "node", + "module": "commonjs", + "useDefineForClassFields": true, + "importsNotUsedAsValues": "remove", + "forceConsistentCasingInFileNames": true, + "noUnusedParameters": true, + "isolatedModules": true, + "strictPropertyInitialization": false + } +} diff --git a/components/confluence-sync/tsconfig.json b/components/confluence-sync/tsconfig.json new file mode 100644 index 00000000..e203040e --- /dev/null +++ b/components/confluence-sync/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/components/cspell-config/LICENSE b/components/cspell-config/LICENSE new file mode 100644 index 00000000..c3049132 --- /dev/null +++ b/components/cspell-config/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Telefónica Innovación Digital and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/cspell-config/README.md b/components/cspell-config/README.md new file mode 100644 index 00000000..cbc600a7 --- /dev/null +++ b/components/cspell-config/README.md @@ -0,0 +1,75 @@ +# cspell-config + +This is a shared configuration for cspell in the monorepo. + +## Installation + +This component is not published to npm, so it can be used only in the monorepo, and you should not add the dependency to the `package.json` file. The dependency will be managed by Nx, the monorepo tool. + +To use this configuration in other component in the monorepo, add the following to your `package.json` + +```json +{ + "scripts": { + "check:spell": "cspell .", + } +} +``` + +> [!NOTE] +> Nx will automatically detect the task name and add the dependency with the configuration in this component, so, when any file here is modified, the spell check will be also executed in the other components when corresponding. + +You should also add the implicit dependency to the `project.json` file: + +```json +{ + "implicitDependencies": [ + "cspell-config" + ] +} +``` + +Then you should add the following file to the root of your component, and name it `cspell.config.js`: + +```js +const { createConfig } = require("./index.js"); + +module.exports = createConfig(); +``` + +> [!WARNING] +> This component is a `commonjs` module because some issues has been detected in VSCode extensions for cspell when using `esm` modules. So, you should use the appropriated file extension depending on the type of your component. You should use `.cjs` extension if your component is an `esm` module, or `.js` if it is a `commonjs` module. + +## Usage + +Once you have installed the configuration, you can run the spell check in your component by using the following command: + +```sh +pnpm nx check:spell your-component-name +``` + +It will be also automatically executed before committing any change due to the repository pre-commit hooks. + +## Extending the configuration + +It is possible to extend the configuration by passing an object to the `createConfig` function. For example: + +```js +const { createConfig } = require("./index.js"); + +module.exports = createConfig({ + ignorePaths: ["**/*.md"], +}); +``` + +## Custom dictionaries + +You can add words to the custom dictionaries in this component. They will be available to all components in the monorepo. To do that, add the words to the corresponding file in the `dictionaries` directory. + +## Contributing + +Please read the repository [Contributing Guidelines](../../.github/CONTRIBUTING.md) for details on how to contribute to this project before submitting a pull request. + +## License + +This component is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. diff --git a/components/cspell-config/cspell.config.js b/components/cspell-config/cspell.config.js new file mode 100644 index 00000000..d07659ea --- /dev/null +++ b/components/cspell-config/cspell.config.js @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const { createConfig } = require("./index.js"); + +module.exports = createConfig(); diff --git a/components/cspell-config/dictionaries/company.txt b/components/cspell-config/dictionaries/company.txt new file mode 100644 index 00000000..88e51bdc --- /dev/null +++ b/components/cspell-config/dictionaries/company.txt @@ -0,0 +1,3 @@ +Telefónica +Innovación +Digital diff --git a/components/cspell-config/dictionaries/missing-en.txt b/components/cspell-config/dictionaries/missing-en.txt new file mode 100644 index 00000000..e69de29b diff --git a/components/cspell-config/dictionaries/node.txt b/components/cspell-config/dictionaries/node.txt new file mode 100644 index 00000000..4f2b89de --- /dev/null +++ b/components/cspell-config/dictionaries/node.txt @@ -0,0 +1,9 @@ +camelcase +commonmark +deepmerge +esmodules +fastq +mdast +mmdc +rehype +vfile diff --git a/components/cspell-config/dictionaries/people.txt b/components/cspell-config/dictionaries/people.txt new file mode 100644 index 00000000..ba265832 --- /dev/null +++ b/components/cspell-config/dictionaries/people.txt @@ -0,0 +1 @@ +dbaeumer diff --git a/components/cspell-config/dictionaries/tech.txt b/components/cspell-config/dictionaries/tech.txt new file mode 100644 index 00000000..9478d938 --- /dev/null +++ b/components/cspell-config/dictionaries/tech.txt @@ -0,0 +1,3 @@ +cacheable +frontmatter +nrwl diff --git a/components/cspell-config/eslint.config.mjs b/components/cspell-config/eslint.config.mjs new file mode 100644 index 00000000..e7ce3054 --- /dev/null +++ b/components/cspell-config/eslint.config.mjs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import config from "../eslint-config/index.js"; + +export default config; diff --git a/components/cspell-config/index.js b/components/cspell-config/index.js new file mode 100644 index 00000000..84037ca5 --- /dev/null +++ b/components/cspell-config/index.js @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const deepMerge = require("deepmerge"); +const { resolve } = require("path"); + +const DICTIONARIES_BASE_PATH = resolve(__dirname, "dictionaries"); + +function createConfig(config = {}) { + return deepMerge( + { + // Version of the setting file. Always 0.2 + version: "0.2", + // Paths to be ignored + ignorePaths: [ + "**/node_modules/**", + ".husky/**", + "**/pnpm-lock.yaml", + "**/components/cspell-config/*.txt", + "**/.gitignore", + "**/coverage/**", + "**/dist/**", + ], + caseSensitive: false, + // Language - current active spelling language + language: "en", + // Dictionaries to be used + dictionaryDefinitions: [ + { name: "company", path: `${DICTIONARIES_BASE_PATH}/company.txt` }, + { name: "node-custom", path: `${DICTIONARIES_BASE_PATH}/node.txt` }, + { + name: "missing-en", + path: `${DICTIONARIES_BASE_PATH}/missing-en.txt`, + }, + { name: "people", path: `${DICTIONARIES_BASE_PATH}/people.txt` }, + { name: "tech", path: `${DICTIONARIES_BASE_PATH}/tech.txt` }, + ], + dictionaries: ["company", "node-custom", "missing-en", "people", "tech"], + languageSettings: [ + { + // In markdown files + languageId: "markdown", + // Exclude code blocks from spell checking + ignoreRegExpList: ["/^\\s*```[\\s\\S]*?^\\s*```/gm"], + }, + ], + // The minimum length of a word before it is checked. + minWordLength: 4, + // cspell:disable-next-line FlagWords - list of words to be always considered incorrect. This is useful for offensive words and common spelling errors. For example "hte" should be "the" + flagWords: ["hte"], + }, + config, + ); +} + +module.exports = { + createConfig, +}; diff --git a/components/cspell-config/package.json b/components/cspell-config/package.json new file mode 100644 index 00000000..0b76718c --- /dev/null +++ b/components/cspell-config/package.json @@ -0,0 +1,19 @@ +{ + "name": "@tid-cross/cspell-config", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "scripts": { + "check:all": "echo 'All checks passed'", + "check:spell": "cspell .", + "cspell:config": "echo 'cspell config ready'", + "lint": "eslint ." + }, + "dependencies": { + "deepmerge": "4.3.1" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8" + } +} diff --git a/components/cspell-config/project.json b/components/cspell-config/project.json new file mode 100644 index 00000000..16b74eda --- /dev/null +++ b/components/cspell-config/project.json @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "cspell-config", + "projectType": "library", + "tags": [ + "type:node:config" + ], + "implicitDependencies": [ + "eslint-config" + ], + "targets": { + "cspell:config": { + "cache": true, + "inputs": ["default"], + "outputs": [ + "{projectRoot}/**/*" + ] + } + } +} diff --git a/components/eslint-config/LICENSE b/components/eslint-config/LICENSE new file mode 100644 index 00000000..c3049132 --- /dev/null +++ b/components/eslint-config/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Telefónica Innovación Digital and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/eslint-config/README.md b/components/eslint-config/README.md new file mode 100644 index 00000000..db6de771 --- /dev/null +++ b/components/eslint-config/README.md @@ -0,0 +1,97 @@ +# eslint-config + +This is a shared configuration for ESLint in the monorepo. + +## Installation + +This component is not published to npm, so it can be used only in the monorepo, and you should not add the dependency to the `package.json` file. The dependency will be managed by Nx, the monorepo tool. + +To use this configuration in other component in the monorepo, add the following to your `package.json` + +```json +{ + "scripts": { + "lint": "eslint ." + } +} +``` + +> [!NOTE] +> Nx will automatically detect the task name and add the dependency with the configuration in this component, so, when any file here is modified, the linter will be also executed in the other components when corresponding. + +You should also add the Nx implicit dependency to the `project.json` file: + +```json +{ + "implicitDependencies": [ + "eslint-config" + ] +} +``` + +Then you should add the following file to the root of your component, and name it `eslint.config.js`: + +```js +import eslintConfig from "../eslint-config/index.js"; +export default eslintConfig; +``` + +> [!WARNING] +> Use the appropriated file extension depending on the type of your component. This component is an `esm` module, so, you should use `.mjs` extension if your component is a `commonjs` module, or `.js` if it is a `esm` module. + +## Usage + +Once you have installed the configuration, you can run the linter in your component by using the following command: + +```sh +pnpm nx lint your-component-name +``` + +It will be also automatically executed before committing any change due to the repository pre-commit hooks. + +## Extending the configuration + +It is possible to extend the configuration. This module exports separated values for each type of configuration, so you can import them and merge with your own configuration. One common case is to configure TypeScript aliases for your own component. + +> [!TIP] +> Check the [`./index.js` file](./index.js) to see the exported configurations. + +```js +import path from "path"; + +import { + defaultConfigWithoutTypescript, + typescriptConfig, + jestConfig, +} from "../eslint-config/index.js"; + +function componentPath() { + return path.resolve.apply(null, [import.meta.dirname, ...arguments]); +} + +export default [ + ...defaultConfigWithoutTypescript, + { + ...typescriptConfig, + settings: { + ...typescriptConfig.settings, + "import/resolver": { + ...typescriptConfig.settings["import/resolver"], + alias: { + map: [["@src", componentPath("src")]], + extensions: [".ts", ".js", ".jsx", ".json"], + }, + }, + }, + }, + jestConfig, +]; +``` + +## Contributing + +Please read the repository [Contributing Guidelines](../../.github/CONTRIBUTING.md) for details on how to contribute to this project before submitting a pull request. + +## License + +This component is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. diff --git a/components/eslint-config/cspell.config.cjs b/components/eslint-config/cspell.config.cjs new file mode 100644 index 00000000..9b25de7b --- /dev/null +++ b/components/eslint-config/cspell.config.cjs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const { createConfig } = require("../cspell-config/index.js"); + +module.exports = createConfig(); diff --git a/components/eslint-config/eslint.config.js b/components/eslint-config/eslint.config.js new file mode 100644 index 00000000..9239d0c0 --- /dev/null +++ b/components/eslint-config/eslint.config.js @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import config from "./index.js"; + +export default config; diff --git a/components/eslint-config/index.js b/components/eslint-config/index.js new file mode 100644 index 00000000..0fbc2f6a --- /dev/null +++ b/components/eslint-config/index.js @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import json from "@eslint/json"; +import markdown from "@eslint/markdown"; +import prettier from "eslint-plugin-prettier"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import eslintConfigPrettier from "eslint-config-prettier"; +import js from "@eslint/js"; +import globals from "globals"; +// eslint-disable-next-line import/no-unresolved +import typescriptParser from "@typescript-eslint/parser"; +// eslint-disable-next-line import/no-unresolved +import typescriptEslintPlugin from "@typescript-eslint/eslint-plugin"; +import pluginJest from "eslint-plugin-jest"; +import importPlugin from "eslint-plugin-import"; + +export const jestConfig = { + files: ["**/*.spec.js", "**/*.test.js", "**/*.spec.ts", "**/*.test.ts"], + plugins: { + jest: pluginJest, + }, + ...pluginJest.configs["flat/recommended"], + languageOptions: { + globals: pluginJest.environments.globals.globals, + }, + rules: { + ...pluginJest.configs["flat/all"].rules, + "jest/no-disabled-tests": "error", + "jest/no-focused-tests": "error", + "jest/no-identical-title": "error", + "jest/prefer-to-have-length": "error", + "jest/valid-expect": "error", + "jest/prefer-strict-equal": [0], + "jest/prefer-importing-jest-globals": [0], + "jest/prefer-expect-assertions": [0], + "jest/no-hooks": [0], + "jest/prefer-called-with": [0], + "jest/require-to-throw-message": [0], + }, +}; + +export const ignores = { + ignores: ["node_modules/**", ".husky/**", "pnpm-lock.yaml", "dist/**"], +}; + +export const jsonConfig = { + files: ["**/*.json"], + ignores: ["nx.json", "**/project.json"], + language: "json/json", + plugins: { + json, + }, + rules: { + "json/no-duplicate-keys": "error", + "json/no-empty-keys": "error", + }, +}; + +export const jsoncConfig = { + files: ["**/*.jsonc", "nx.json", "**/project.json"], + language: "json/jsonc", + plugins: { + json, + }, + rules: { + "json/no-duplicate-keys": "error", + "json/no-empty-keys": "error", + }, +}; + +export const markdownConfig = { + files: ["**/*.md"], + plugins: { + markdown, + }, + language: "markdown/commonmark", + rules: { + "markdown/no-html": "error", + }, +}; + +export const jsBaseConfig = { + files: ["**/*.js", "**/*.cjs", "**/*.mjs", "**/*.jsx", "**/*.ts", "**/*.tsx"], + plugins: { + prettier, + import: importPlugin, + }, + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.node, + }, + }, + rules: { + ...importPlugin.flatConfigs.recommended.rules, + ...js.configs.recommended.rules, + ...eslintConfigPrettier.rules, + ...eslintPluginPrettierRecommended.rules, + camelcase: [2, { properties: "never" }], + "no-console": [2, { allow: ["warn", "error"] }], + "no-shadow": [2, { builtinGlobals: true, hoist: "all" }], + "no-undef": [2], + "no-unused-vars": [ + 2, + { vars: "all", args: "after-used", ignoreRestSiblings: false }, + ], + }, +}; + +export const commonJsConfig = { + files: ["**/*.cjs"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "commonjs", + }, +}; + +export const typescriptConfig = { + files: ["**/*.ts"], + languageOptions: { + parser: typescriptParser, + parserOptions: { + projectService: true, + }, + }, + plugins: { + "@typescript-eslint": typescriptEslintPlugin, + }, + rules: { + ...typescriptEslintPlugin.configs.recommended.rules, + }, + settings: { + "import/resolver": { + typescript: { + extensions: [".ts", ".tsx"], + alwaysTryTypes: true, + }, + node: true, + }, + }, +}; + +export const defaultConfigWithoutTypescript = [ + ignores, + jsonConfig, + jsoncConfig, + markdownConfig, + jsBaseConfig, + commonJsConfig, +]; + +export default [...defaultConfigWithoutTypescript, typescriptConfig]; diff --git a/components/eslint-config/package.json b/components/eslint-config/package.json new file mode 100644 index 00000000..59d8d4d1 --- /dev/null +++ b/components/eslint-config/package.json @@ -0,0 +1,16 @@ +{ + "name": "@tid-cross/eslint-config", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "check:all": "echo 'All checks passed'", + "check:spell": "cspell .", + "eslint:config": "echo 'eslint config ready'", + "lint": "eslint ." + }, + "engines": { + "node": ">=18", + "pnpm": ">=8" + } +} diff --git a/components/eslint-config/project.json b/components/eslint-config/project.json new file mode 100644 index 00000000..3374b1aa --- /dev/null +++ b/components/eslint-config/project.json @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "eslint-config", + "projectType": "library", + "tags": [ + "type:node:config" + ], + "implicitDependencies": [ + "cspell-config" + ], + "targets": { + "eslint:config": { + "cache": true, + "inputs": ["default"], + "outputs": [ + "{projectRoot}/**/*" + ] + } + } +} diff --git a/components/markdown-confluence-sync/.gitignore b/components/markdown-confluence-sync/.gitignore new file mode 100644 index 00000000..e052847d --- /dev/null +++ b/components/markdown-confluence-sync/.gitignore @@ -0,0 +1,9 @@ +# MacOS +.DS_store + +# Dependencies +node_modules + +# Generated +/coverage +/dist diff --git a/components/markdown-confluence-sync/CHANGELOG.md b/components/markdown-confluence-sync/CHANGELOG.md new file mode 100644 index 00000000..fb154656 --- /dev/null +++ b/components/markdown-confluence-sync/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +#### Added +#### Changed +#### Fixed +#### Deprecated +#### Removed + +## [1.0.0-beta.1] + +### Added + +* feat: Initial release diff --git a/components/markdown-confluence-sync/CONTRIBUTING.md b/components/markdown-confluence-sync/CONTRIBUTING.md new file mode 100644 index 00000000..4d6b6ef9 --- /dev/null +++ b/components/markdown-confluence-sync/CONTRIBUTING.md @@ -0,0 +1,102 @@ +# How to contribute + +First of all, thank you for considering contributing to the this project! 🎉 + +# Table of contents + +- [Development](#development) + - [Installation](#installation) + - [Monorepo tool](#monorepo-tool) + - [Unit tests](#unit-tests) + - [Component tests](#component-tests) + - [Build](#build) + - [NPM scripts reference](#npm-scripts-reference) +- [License, Code of Conduct, and Contribution License Agreement](#license-code-of-conduct-and-contribution-license-agreement) +- [Pull requests](#pull-requests) + +## Development + +### Installation + +This repository use Pnpm as dependencies manager. So, to start working on them, you have to install the dependencies by running `pnpm install` in the root folder of the repository. + +Please refer to the monorepo README file for further information about [common requirements](../../README.md#requirements) and [installation process](../../README.md#installation). + +### Monorepo tool + +The repository uses [Nx](https://nx.dev/) as monorepo tool for managing dependencies between component tasks, so, you should run any command for this component from the repository root folder, and Nx will take care of executing the dependent commands in the right order. + +For example, a command that could be executed like this in this folder: + +```sh title="Execute unit tests of the component inside its folder" +pnpm run test:unit +``` + +> ![WARNING] No management of task dependencies +> The previous command will only execute the unit tests of the component, which may fail if the component has dependencies that are not built yet, for example. + +Should be executed like this in any folder of the repository: + +```sh title="Execute unit tests of the component, and all needed dependencies, from any folder" +pnpm nx test:unit markdown-confluence-sync +``` + +> ![TIP] Management of task dependencies +> The previous command will execute the unit tests of the component and all the dependencies needed to run them, and will take advantage of the cache to avoid unnecessary executions. + +### Unit tests + +Unit tests are executed using [Jest](https://jestjs.io/). To run them, execute the following command: + +```sh +pnpm nx test:unit markdown-confluence-sync +``` + +### Component tests + +Component tests are executed using [Jest](https://jestjs.io/) also, but they use a child process to start the component's CLI and verify that it calls to the Confluence API as expected. The Confluence API is mocked using a mock server, so the component doesn't need to connect to a real Confluence instance. + +There are different docs fixtures in the `test/component/fixtures` directory that are used to test different scenarios. + +To run them, execute the following command, which will start the mock server and execute the tests: + +```sh +pnpm nx test:component markdown-confluence-sync +``` + +You can also start the mock server in a separate terminal, and then execute the tests, which will allow you to see and change the requests and responses in the mock server in real time, so you can better understand what is happening, and debug the tests: + +```sh +pnpm nx confluence:mock markdown-confluence-sync +``` + +And, in a separate terminal: + +```sh +pnpm nx test:component:run markdown-confluence-sync +``` + +### Build + +This command generates the library into the `dist` directory. __Note that other components in the repository won't be able to use the library until this command is executed.__ + +```sh +pnpm nx build markdown-confluence-sync +``` + +### NPM scripts reference + +- `build` - Build the library. +- `check:all` - Run all checks, tests, and build. +- `test:unit` - Run unit tests. +- `check:spell` - Checks spelling. +- `check:types` - Checks the TypeScript types. +- `lint` - Lint the code. + +## License, Code of Conduct, and Contribution License Agreement + +By contributing to this project, you agree that your contributions will be licensed under the [LICENSE](./LICENSE) file in this folder, and that you agree to the terms described in the [CONTRIBUTING.md](../../.github/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](../../.github/CODE_OF_CONDUCT.md) files in this repository. + +## Pull requests + +Please follow the recommendations in the main [CONTRIBUTING.md](../../.github/CONTRIBUTING.md) file in this repository before submitting a pull request. \ No newline at end of file diff --git a/components/markdown-confluence-sync/LICENSE b/components/markdown-confluence-sync/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/components/markdown-confluence-sync/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/components/markdown-confluence-sync/README.md b/components/markdown-confluence-sync/README.md new file mode 100644 index 00000000..7476d401 --- /dev/null +++ b/components/markdown-confluence-sync/README.md @@ -0,0 +1,427 @@ +# markdown-confluence-sync + +Creates/updates/deletes [Confluence](https://www.atlassian.com/es/software/confluence) pages based on markdown files in a directory. Supports Mermaid diagrams and per-page configuration using [frontmatter metadata](https://jekyllrb.com/docs/front-matter/). Works great with [Docusaurus](https://docusaurus.io/). + +## Table of Contents + +- [Requirements](#requirements) + - [Compatibility](#compatibility) +- [Features](#features) +- [Alternatives](#alternatives) +- [Quick start](#quick-start) + - [Installation](#installation) + - [Usage](#usage) +- [Sync modes](#sync-modes) + - [Tree mode](#tree-mode) + - [Page names](#page-names) + - [Index files](#index-files) + - [Category files](#category-files) + - [Example](#example) + - [Flat mode](#flat-mode) +- [Configuration](#configuration) + - [Configuration file](#configuration-file) + - [Arguments](#arguments) + - [Environment variables](#environment-variables) +- [Configuration per page](#configuration-per-page) +- [Automation notice](#automation-notice) +- [Markdown conversion](#markdown-conversion) + - [Supported features](#supported-features) + - [Unsupported features](#unsupported-features) + - [Docusaurus compatibility](#docusaurus-compatibility) +- [Programmatic usage](#programmatic-usage) + - [API](#api) +- [Contributing](#contributing) +- [License](#license) + +## Requirements + +In order to be able to sync the markdown files with Confluence, you need to have the following: + +* A [Confluence](https://www.atlassian.com/es/software/confluence) instance. +* The id of the Confluence space where the pages will be created. +* A personal access token to authenticate. You can create a personal access token following the instructions in the [Atlassian documentation](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/). +* A folder containing the markdown files to be synced with Confluence. It can be a Docusaurus project docs folder. + +### Compatibility + +> [!WARNING] +> This library has been tested only with Confluence 8.5.x. It may work with other versions, but it has not been tested. + +## Features + +The library reads the markdown files in a given folder and create/delete/update the corresponding Confluence pages following the same hierarchical structure under a provided Confluence page depending on the [synchronization mode](#sync-modes) to use. + +Markdown documents to be synced __must have a `title` property, and a `sync_to_confluence` property set to `true` in the [frontmatter metadata](https://jekyllrb.com/docs/front-matter/).__ + +The library has __two modes for syncing__: +* `tree` sync mode - Mirrors the hierarchical pages structure from given folder under a Confluence root page. Some files are used for [representing indices in the hierarchy](#index-files). +* `flat` sync mode - Synchronize a list of markdown files matched by a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) as children page of a Confluence root page, without any hierarchy. + * As an extra in this mode, a Confluence id can be provided to each page using the frontmatter, and, in such case, the corresponding Confluence page will be always updated, even when it is not a children of the Confluence root page. + +Other features are: + +* The library adds a __notice message at the beginning of every pages content__ indicating that it has been generated automatically. Read [Automation notice](#automation-notice) for more information. +* Converts Mermaid diagrams to images. +* Supports __configuration per page using [frontmatter metadata](https://jekyllrb.com/docs/front-matter/).__ Some of the things you can configure are: + * Title of the page in Confluence. + * Adding ancestors title to every page title. + +## Alternatives + +* [Markdown-Confluence-CLI](https://github.com/markdown-confluence/markdown-confluence/tree/main/packages/cli) - A CLI tool to sync markdown files with Confluence. It converts the markdown files to [ADF format](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/) and sends them to Confluence. Some Confluence versions do not work well with ADF format, so it may not work in all cases. +* [Markdown-to-Confluence](https://github.com/duo-labs/markdown-to-confluence) - A CLI tool to sync markdown files with Confluence made in Python. It uses the Confluence REST API to create pages. It seems to not support hierarchical structures, or Mermaid diagrams. + +## Quick start + +### Installation + +```bash +npm install @tid-cross/markdown-confluence-sync +``` + +### Usage + +The library provides an NPM binary named `markdown-confluence-sync`. To use it, you can add a script like this to your `package.json` file: + +```json title="package.json" +{ + "scripts": { + "sync": "markdown-confluence-sync" + } +} +``` + +As a starting point, you can create a `markdown-confluence-sync.config.js` file in the root of your project with the following content (read the [Configuration](#configuration) section for more information and other configuration methods): + +```js title="markdown-confluence-sync.config.js" +module.exports = { + docsDir: "docs", + confluence: { + url: "https://my-confluence.es", + personalAccessToken: "*******", + spaceKey: "MY-SPACE", + rootPageId: "my-root-page-id" + } +} +``` + +Then, you could run the synchronization with: + +```sh +npm run sync +``` + +## Sync modes + +The library has two modes for reading markdown files, `tree` and `flat`: + +### Tree mode + +The `tree` mode will creates a hierarchy based on the markdown files structure in the folder to sync, and send the pages to Confluence respecting it. This is the default mode. + +#### Page names + +__Confluence requires unique page names within a space__. To meet this requirement, pages are created by combining the titles of their ancestors with their own title. Ancestors refer to parent pages or categories that the page belongs to. For example: + * If we have a page named `Page C` with ancestors `Category A` and `Category B` it will be created with the title `[Category A][Category B] Page C` in Confluence. + +#### Index files + +In `tree` mode, some files are used for representing categories in the hierarchy. These files are called **index files** and are used to create a parent page in Confluence. The rest of the files in the same folder will be created as children of the parent page. + +These files __represent the content of the category itself and contain it's metadata__. These **index files**, can be one of the following: + +* `index.md` +* `index.mdx` +* `README.md` +* `README.mdx` +* Files with same name as the category's folder name (eg: for category with name `categoryA` either `.md` and `.mdx` will be considered indices). + +> [!NOTE] +> In presence of multiple index files in a folder, the library will use the first one found in the following order: `index.md`, `index.mdx`, `README.md` and `README.mdx`, and files with same name as the category's folder name with `.md` and `.mdx` extensions. **The rest of the index files will be ignored.** + +Other considerations about the **index files**: + +* The library uses the **index file** in a folder to create a "parent page" in Confluence. Other docs in the same folder will be created as children of the parent page. +* If the **index file** has not `sync_to_confluence` set to `true` in its frontmatter metadata, the library will stop searching for possible children pages in the folder. +* If the folder does not contain an **index file**, the library will create an empty page in Confluence with the same name as the folder, but only when it has children pages to be synced (other pages in the folder or children folders with `sync_to_confluence` set to `true`). +* **Index file** in the root directory will be ignored. For the moment, the library is only able to create pages from files in root directory and under children folders. + +#### Category files + +It is possible to specify category item metadata using `_category_.json` or `_category_.yml` files in the respective folder. You can set a `label` field in the file to overwrite the category page title. + +Setting this field will overwrite the page title defined in the `index` file, and also the title of the empty page created in Confluence when the folder does not contain an `index` file. + +> [!TIP] +> Note that these files are compatible with [Docusaurus category files](https://docusaurus.io/docs/sidebar/autogenerated#category-item-metadata), but the library only uses the `label` field to overwrite the category page title. + +#### Example + +Here you have an example of a markdown files structure in a repository's docs folder. Each page details the corresponding Confluence page that will be created: + +```title="Docusaurus project" +repository/ +├── docs/ +│ ├── index.md -> IGNORED +│ ├── category-a/ +│ │ ├── index.md -> category-a +│ │ ├── page-a.md -> category-a/page-a +│ │ ├── page-b.md -> category-a/page-b +│ │ └── category-b/ +│ │ ├── index.md -> category-a/category-b +│ │ ├── page-a.md -> category-a/category-b/page-a +│ │ └── page-b.md -> category-a/category-b/page-b +│ ├── category-c/ -> Empty page created in Confluence as category-c +│ │ ├── page-a.md -> category-c/page-a +│ │ └── page-b.md -> category-c/page-b +│ ├── category-d/ -> Empty page created in Confluence as category-d +│ │ ├── _category_.yml -> Rename category-d title based on the label field +│ ├── category-e/ +│ │ ├── _category_.yml -> Rename category-e title based on the label field +│ │ └── index.md -> category-e +│ ├── page-a.md -> page-a +│ └── page-b.md -> page-b +├── markdown-confluence-sync.config.js <- Markdown Confluence Sync configuration +└── package.json +``` + +### Flat mode + +The `flat` mode syncs all markdown files matching a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) just under the root Confluence page. It does not create a nested hierarchy. + +It also supports defining a Confluence id in the frontmatter metadata of the markdown files. In this case, the library will always update the Confluence page with that id, even when it is not a children of the Confluence root page. + +To enable it, you have to set the `mode` property to `flat` in the configuration file, and provide a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) to filter the files to sync using the `filesPattern` property. + +> [!WARNING] +> The `filePattern` option searches all files in the directory but filters the pattern results and ignores all files that do not have one of the following extensions: `md` or `mdx`. + +For example, with the provided configuration, `flat` synchronization mode will get all files starting with the "check" word, and having the extensions `md` or `mdx`: + +```js title="markdown-confluence-sync.config.js" +module.exports = { + docsDir: "docs", + confluence: { + url: "https://my-confluence.es", + personalAccessToken: "*******", + spaceKey: "MY-SPACE", + rootPageId: "my-root-page-id" + } +} +``` + +## Configuration + +All of the configuration properties can be provided through a configuration file, CLI arguments, or environment variables (Read the [`@mocks-server/config` package](https://github.com/mocks-server/main/tree/master/packages/config) for further info, which is used under the hood). + +The namespace for the configuration of this library is `markdown-confluence-sync`, so, for example, to set environment variables you have to prefix the variable name with `MARKDOWN_CONFLUENCE_SYNC_`. + +| Property | Type | Description | Default | +| --- | --- | --- | --- | +| `logLevel` | `string` | Log level. One of `silly`, `debug`, `info`, `warn`, `error`, `silent` | `info` | +| `mode` | `string` | Mode to read the pages to send to Confluence. One of `tree`, `flat`. | `tree` | +| `filesPattern` | `string` | Pattern to read the pages to send to Confluence. This option is mandatory when used `flat` sync mode. | | +| `docsDir` | `string` | Path to the docs directory. | `./docs` | +| `confluence.url` | `string` | URL of the Confluence instance. | | +| `confluence.personalAccessToken` | `string` | Personal access token to authenticate against the Confluence instance. | | +| `confluence.spaceKey` | `string` | Key of the Confluence space where the pages will be synced. | | +| `confluence.rootPageId` | `string` | Id of the Confluence parent page where the pages will be synced. | | +| `confluence.rootPageName` | `string` | Customize Confluence page titles by adding a prefix to all of them for improved organization and clarity | | +| `confluence.noticeMessage` | `string` | Notice message to add at the beginning of the Confluence pages. | | +| `confluence.noticeTemplate` | `string` | Template string to use for the notice message. | | +| `confluence.dryRun` | `boolean` | Log create, update or delete requests to Confluence instead of really making them | `false` | +| `config.readArguments` | `boolean` | Read configuration from arguments or not | `false` | +| `config.readFile` | `boolean` | Read configuration from file or not | `false` | +| `config.readEnvironment` | `boolean` | Read configuration from environment or not | `false` | + +### Configuration file + +As mentioned above, the library supports defining the config in a configuration file. It supports many patterns for naming the file, as well as file formats. Read the [`@mocks-server/config` package](https://github.com/mocks-server/main/tree/master/packages/config) for further info about the supported patterns and formats. Just take into account that the namespace for the configuration is `markdown-confluence-sync`, so, a possible configuration file may be named `markdown-confluence-sync.config.js`. + +```js title="markdown-confluence-sync.config.js" +export default { + docsDir: "docs", + confluence: { + url: "https://my-confluence.es", + personalAccessToken: "*******", + spaceKey: "MY-SPACE", + rootPageId: "my-root-page-id" + } +} +``` + +### Arguments + +Configuration properties can be provided through CLI arguments. The name of the argument is the property name prefixed with `--`. For example, to set the `docsDir` property, you have to set the `--docsDir` argument. For boolean properties with a default value of `true`, you can set the `--no-` prefix to set the property to `false`. For example, to set the `config.readArguments` property to `false`, you have to set the `--no-config.readArguments` argument. + +```sh +npx markdown-confluence-sync --docsDir ./docs --logLevel debug +``` + +### Environment variables + +Configuration properties can be provided through environment variables. The name of the variable is the property name prefixed with `MARKDOWN_CONFLUENCE_SYNC_` and converted to uppercase. + +For example, to set the `docsDir` property, you have to set the `MARKDOWN_CONFLUENCE_SYNC_DOCS_DIR` environment variable. + +```sh +MARKDOWN_CONFLUENCE_SYNC_DOCS_DIR=./docs MARKDOWN_CONFLUENCE_SYNC_LOG=debug npx markdown-confluence-sync +``` + +## Configuration per page + +It is possible to set some properties for each page using the frontmatter metadata in the markdown files. The properties that can be set are: + +* `confluence_title` - Title of the page in Confluence. It will force the title of the page in Confluence to be the value of this property, ignoring the title of the page in the markdown file. +* `confluence_short_name` - Adding ancestors title to its children's title may produce an unnecessarily long titles. To avoid this, you can use this property to replace the title of a parent page in its children's title. It should be used only in **index files** for categories. + For example, if the child's title is "`Page`" and the parent, with the title "`Parent Category`," has the property `confluence_short_name` set to "`Parent`," it will appear in Confluence as follows: + ```diff + - [Parent Category] Page + + [Parent] Page + ``` + +## Automation notice + +The library adds a notice message at the beginning of every pages content indicating that it has been generated automatically: + +```html +

AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost

+``` + +This message can be customized by using the `confluence.noticeMessage` option. But note that the resultant message will always be wrapped in a `

` and a `` tag. + +You can also use the `confluence.noticeTemplate` option to provide a custom template for the resulting message. Under the hood, [Handlebars](https://handlebarsjs.com/) is used to provide the next variables to the template for your convenience: + * `{{relativePath}}`: The relative path of the markdown file that generated the Confluence page. + * `{{relativePathWithoutExtension}}`: The relative path of the markdown file that generated the Confluence page, but without the file extension. + * `{{title}}`: The title of the page. + * `{{message}}`: The message set by `confluence.noticeMessage`. + * `{{default}}`: The default message. + +```js +/** @type {import('@tid-cross/markdown-confluence-sync').Configuration} */ + +module.exports = { + docsDir: "./docs", + confluence: { + noticeTemplate: + '{{default}}. Edit url: Github', + } +}; +``` + +> [!WARNING] +> **Caveat**: The template is evaluated as **raw HTML** in Confluence, so use it with caution. + +## Markdown conversion + +Some markdown features that are not easy to convert to Confluence HTML format without defining a custom style for them. + +### Supported features + +Apart of supporting the most common markdown features, the library also supports the following features: + +* [Frontmatter metadata](https://jekyllrb.com/docs/front-matter/) - Frontmatter metadata is removed from the content. +* [Supports MDX files](https://docusaurus.io/docs/mdx) - MDX files are processed, but MDX syntax will be removed, expect for the Docusaurus Tabs tags (read [Docusaurus compatibility](#docusaurus-compatibility) section for more information). +* [Mermaid diagrams](https://github.blog/developer-skills/github/include-diagrams-markdown-files-mermaid/) - Mermaid diagrams are converted to images. + * The image is generated with `SVG` format, and synced to confluence with a with of 1000 pixels. + * The plugin generates a SVG image for each diagram and uploads it to Confluence. Then the image is then linked in the page. + * The images are stored in a folder named `mermaid-diagrams` in the directory of the page. + * To ignore the autogenerated images in your repository, you can add the following line to your `.gitignore` file: + ```gitignore + **/mermaid-diagrams/ + ``` +* [Details](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) - Details HTML tags are converted to a Confluence expand macro. + * The plugin converts the details to an expand macro, and the content of the details is added as the body of the expand macro. + * The details tag must have a summary tag as its first child. It will be used as the title of the expand macro. + * The plugin does not support any details properties, like `open`. + * Do not use mdx syntax inside the details tag (except for tabs tags, which are supported). + * For example, the following details in a markdown file: + ```markdown +

+ Click to expand + This is the content of the details. +
+ ``` + will be converted to: + ```markdown + + Click to expand +

This is the content of the details.

+
+ ``` + +### Unsupported features + +* [Footnotes](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes) - Footnotes are removed. + +### Docusaurus compatibility + +The library is designed to work with [Docusaurus projects](https://docusaurus.io/) without much further configuration, so it also supports some Docusaurus specific features: + +* [Admonitions](https://docusaurus.io/docs/markdown-features/admonitions) - Docusaurus admonitions are converted to block quotes keeping their content. +* [Docusaurus MDX code blocks](https://docusaurus.io/docs/i18n/crowdin#mdx-solutions) will be removed, except for the next tags: + * [Tabs](https://docusaurus.io/docs/markdown-features/tabs) - This is a Docusaurus feature that is not supported by Confluence, so the plugin tries to convert it to a list. + * The plugin converts the tabs to lists, and the content of each tab is added as a list item. + * If there are nested tabs, the plugin will add a sub-list to the list item. + * It is important to mention that the plugin does not support any tab properties, like `groupId`, `values`, or `labels`. + * The only supported and mandatory property is the `label` property in each tab item. This property is used as the title of the list item. + * CAUTION: Only tabs in .mdx files are supported. Tabs in .md files are not supported and will cause an error. + * For example, the following tabs in an MDX file: + ```markdown + + + This is the first tab. + + + This is the second tab. + + + ``` + will be converted to: + ```markdown + - First Tab + - This is the first tab. + - Second Tab + - This is the second tab. + ``` + +## Programmatic usage + +You can also import the library in your code and use it programmatically. In this case, you have to provide the configuration as an object when creating the instance, and you can call the `sync` method to start the sync process: + +```js title="Programmatic usage" +import path from "path"; +import { MarkdownConfluenceSync } from '@tid-cross/markdown-confluence-sync'; + +const markdownConfluenceSync = new MarkdownConfluenceSync({ + docsDir: path.resolve(__dirname, "..", "docs"); + confluence: { + url: "https://my.confluence.es", + personalAccessToken: "*******", + spaceKey: "MY-SPACE", + rootPageId: "my-root-page-id" + } +}); + +await markdownConfluenceSync.sync(); +``` + +### API + +#### `MarkdownConfluenceSync` + +This is the main class of the library. It receives the configuration as an object in the constructor. The configuration properties are the same as the ones described in the [Configuration](#configuration) section. + +Once it is instantiated, it exposes the following methods: + +##### `sync` + +This method starts the sync process. It returns a promise that resolves when the sync process finishes. + +## Contributing + +Please read our [Contributing Guidelines](./CONTRIBUTING.md) for details on how to contribute to this project before submitting a pull request. + +## License + +This project is licensed under the Apache-2.0 License - see the [LICENSE](./LICENSE) file for details. diff --git a/components/markdown-confluence-sync/babel.config.cjs b/components/markdown-confluence-sync/babel.config.cjs new file mode 100644 index 00000000..9e846534 --- /dev/null +++ b/components/markdown-confluence-sync/babel.config.cjs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = (api) => { + const isTest = api.env("test"); + if (isTest) { + return { + presets: [ + [ + "@babel/preset-env", + { targets: { node: "current", esmodules: true } }, + ], + "@babel/preset-typescript", + ], + plugins: [ + "babel-plugin-transform-import-meta", + [ + "module-resolver", + { + root: ["."], + alias: { + "@src": "./src", + "@support": "./test/unit/support", + }, + }, + ], + ], + }; + } +}; diff --git a/components/markdown-confluence-sync/bin/markdown-confluence-sync.mjs b/components/markdown-confluence-sync/bin/markdown-confluence-sync.mjs new file mode 100755 index 00000000..240afc28 --- /dev/null +++ b/components/markdown-confluence-sync/bin/markdown-confluence-sync.mjs @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { run } from "../dist/Cli.js"; +run(); diff --git a/components/markdown-confluence-sync/config/puppeteer-config.json b/components/markdown-confluence-sync/config/puppeteer-config.json new file mode 100644 index 00000000..c57f79da --- /dev/null +++ b/components/markdown-confluence-sync/config/puppeteer-config.json @@ -0,0 +1,3 @@ +{ + "args": ["--no-sandbox"] +} diff --git a/components/markdown-confluence-sync/cspell.config.cjs b/components/markdown-confluence-sync/cspell.config.cjs new file mode 100644 index 00000000..9b25de7b --- /dev/null +++ b/components/markdown-confluence-sync/cspell.config.cjs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const { createConfig } = require("../cspell-config/index.js"); + +module.exports = createConfig(); diff --git a/components/markdown-confluence-sync/eslint.config.js b/components/markdown-confluence-sync/eslint.config.js new file mode 100644 index 00000000..7451a8d9 --- /dev/null +++ b/components/markdown-confluence-sync/eslint.config.js @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import { + defaultConfigWithoutTypescript, + typescriptConfig, + jestConfig, +} from "../eslint-config/index.js"; +import path from "path"; + +function componentPath() { + return path.resolve.apply(null, [import.meta.dirname, ...arguments]); +} + +export default [ + ...defaultConfigWithoutTypescript, + { + ...typescriptConfig, + files: ["src/**/*.ts", "mocks/**/*.ts"], + }, + { + ...typescriptConfig, + files: ["test/component/**/*.ts"], + settings: { + ...typescriptConfig.settings, + "import/resolver": { + ...typescriptConfig.settings["import/resolver"], + alias: { + map: [ + ["@src", componentPath("src")], + ["@support", componentPath("test", "component", "support")], + ], + extensions: [".ts", ".js", ".jsx", ".json"], + }, + }, + }, + }, + { + ...typescriptConfig, + files: ["test/unit/**/*.ts"], + settings: { + ...typescriptConfig.settings, + "import/resolver": { + ...typescriptConfig.settings["import/resolver"], + alias: { + map: [ + ["@src", componentPath("src")], + ["@support", componentPath("test", "unit", "support")], + ], + extensions: [".ts", ".js", ".jsx", ".json"], + }, + }, + }, + }, + { + ...jestConfig, + files: [ + ...jestConfig.files, + "test/unit/support/**/*.ts", + "test/component/support/**/*.ts", + ], + }, + { + files: [ + "test/component/**/*.spec.ts", + "test/component/**/*.test.ts", + "test/unit/**/*.spec.ts", + "test/unit/**/*.test.ts", + ], + rules: { + "jest/max-expects": [ + "error", + { + max: 30, + }, + ], + }, + }, + { + ignores: ["test/**/fixtures/**/*.*"], + }, +]; diff --git a/components/markdown-confluence-sync/jest.component.config.cjs b/components/markdown-confluence-sync/jest.component.config.cjs new file mode 100644 index 00000000..f9d92371 --- /dev/null +++ b/components/markdown-confluence-sync/jest.component.config.cjs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: false, + + testTimeout: 120000, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "/test/component/specs/*.spec.ts", + "/test/component/specs/**/*.test.ts", + ], + + // The test environment that will be used for testing + testEnvironment: "node", + + // Remove all import extensions + moduleNameMapper: { + "^(\\.{1,2}/.*)(?:/index)?\\.js$": "$1", + }, +}; diff --git a/components/markdown-confluence-sync/jest.unit.config.cjs b/components/markdown-confluence-sync/jest.unit.config.cjs new file mode 100644 index 00000000..81c46401 --- /dev/null +++ b/components/markdown-confluence-sync/jest.unit.config.cjs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["src/**/*.ts", "!src/index.ts"], + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 99, + functions: 99, + lines: 99, + statements: 99, + }, + }, + + // The glob patterns Jest uses to detect test files + testMatch: [ + "/test/unit/specs/*.spec.ts", + "/test/unit/specs/**/*.test.ts", + ], + + // The test environment that will be used for testing + testEnvironment: "node", + + // Ignore the following packages from being transformed + transformIgnorePatterns: [ + "/node_modules/(?!(remark-parse|rehype-stringify|unist-util-find-after)/)", + ], + + // Remove all import extensions + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + + reporters: [ + "default", + [ + "jest-sonar", + { outputDirectory: "coverage", outputName: "TEST-junit-report.xml" }, + ], + ], +}; diff --git a/components/markdown-confluence-sync/mocks.config.cjs b/components/markdown-confluence-sync/mocks.config.cjs new file mode 100644 index 00000000..645b271f --- /dev/null +++ b/components/markdown-confluence-sync/mocks.config.cjs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +// For a detailed explanation regarding each configuration property, visit: +// https://www.mocks-server.org/docs/configuration/how-to-change-settings +// https://www.mocks-server.org/docs/configuration/options + +/** @type {import('@mocks-server/core').Configuration} */ + +module.exports = { + mock: { + collections: { + // Selected collection + selected: "base", + }, + }, + files: { + babelRegister: { + // Load @babel/register + enabled: true, + // Options for @babel/register + options: { + configFile: false, + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], + }, + }, + }, +}; diff --git a/components/markdown-confluence-sync/mocks/collections.ts b/components/markdown-confluence-sync/mocks/collections.ts new file mode 100644 index 00000000..093aacb9 --- /dev/null +++ b/components/markdown-confluence-sync/mocks/collections.ts @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { CollectionDefinition } from "@mocks-server/core"; + +const collection: CollectionDefinition[] = [ + { + id: "base", + routes: ["spy-get-requests:send", "spy-reset-requests:reset"], + }, + { + id: "empty-root", + from: "base", + routes: [ + "confluence-get-page:empty-root", + "confluence-create-page:empty-root", + "confluence-create-attachments:empty-root", + // "confluence-update-page:success", + // "confluence-delete-page:success", + ], + }, + { + id: "default-root", + from: "base", + routes: [ + "confluence-get-page:default-root", + "confluence-create-page:default-root", + "confluence-update-page:default-root", + "confluence-delete-page:default-root", + "confluence-get-attachments:default-root", + "confluence-create-attachments:default-root", + ], + }, + { + id: "with-root-page-name", + from: "base", + routes: [ + "confluence-get-page:with-root-page-name", + "confluence-create-page:with-root-page-name", + ], + }, + { + id: "with-mdx-files", + from: "base", + routes: [ + "confluence-get-page:with-mdx-files", + "confluence-create-page:with-mdx-files", + ], + }, + { + id: "with-confluence-title", + from: "base", + routes: [ + "confluence-get-page:with-confluence-title", + "confluence-create-page:with-confluence-title", + ], + }, + { + id: "with-alternative-index-files", + from: "base", + routes: [ + "confluence-get-page:with-alternative-index-files", + "confluence-create-page:with-alternative-index-files", + ], + }, + { + id: "with-confluence-page-id", + from: "base", + routes: [ + "confluence-get-page:with-confluence-page-id", + "confluence-create-page:with-confluence-page-id", + "confluence-update-page:with-confluence-page-id", + "confluence-get-attachments:with-confluence-page-id", + ], + }, +]; + +export default collection; diff --git a/components/markdown-confluence-sync/mocks/routes/Confluence.ts b/components/markdown-confluence-sync/mocks/routes/Confluence.ts new file mode 100644 index 00000000..0a326352 --- /dev/null +++ b/components/markdown-confluence-sync/mocks/routes/Confluence.ts @@ -0,0 +1,413 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + NextFunction, + RouteDefinition, + ScopedCoreInterface, + Request as ServerRequest, + Response as ServerResponse, +} from "@mocks-server/core"; + +import { + ATTACHMENTS_DEFAULT_ROOT, + PAGES_DEFAULT_ROOT_CREATE, + PAGES_DEFAULT_ROOT_DELETE, + PAGES_DEFAULT_ROOT_GET, + PAGES_DEFAULT_ROOT_UPDATE, + PAGES_EMPTY_ROOT, + PAGES_WITH_CONFLUENCE_PAGE_ID, + PAGES_WITH_CONFLUENCE_PAGE_ID_ATTACHMENTS, + PAGES_WITH_CONFLUENCE_TITLE, + PAGES_WITH_MDX_FILES, + PAGES_WITH_ROOT_PAGE_NAME, + PAGES_WITH_ALTERNATIVE_INDEX_FILES, +} from "../support/fixtures/ConfluencePages"; +import { addRequest } from "../support/SpyStorage"; + +function getPageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Requested page with id ${req.params.pageId} to Confluence`, + ); + + addRequest("confluence-get-page", req); + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + const pageData = { + id: page.id, + title: page.title, + content: "", + version: { number: 1 }, + ancestors: page.ancestors, + children: page.children, + }; + core.logger.info(`Sending page ${JSON.stringify(pageData)}`); + res.status(200).json(pageData); + } else { + core.logger.error( + `Page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + +function createPageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Creating page in Confluence: ${JSON.stringify(req.body)}`, + ); + + addRequest("confluence-create-page", req); + + const page = pages.find( + (pageCandidate) => pageCandidate.title === req.body.title, + ); + if (page) { + res.status(200).json({ + title: req.body.title, + id: page.id, + version: { number: 1 }, + ancestors: page.ancestors, + }); + } else { + core.logger.error( + `Bad request creating page in Confluence: ${JSON.stringify(req.body)}`, + ); + core.logger.error(`Available pages: ${JSON.stringify(pages)}`); + res.status(400).send(); + } + }; +} + +function updatePageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Updating page in Confluence: ${JSON.stringify(req.body)}`, + ); + + addRequest("confluence-update-page", req); + + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + res.status(200).json({ + title: req.body.title, + id: page.id, + version: { number: 1 }, + ancestors: page.ancestors, + }); + } else { + core.logger.error( + `Bad request creating page in Confluence: ${JSON.stringify(req.body)}`, + ); + core.logger.error(`Available pages: ${JSON.stringify(pages)}`); + res.status(400).send(); + } + }; +} + +function deletePageMiddleware(pages) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Deleting page in Confluence: ${JSON.stringify(req.params.pageId)}`, + ); + + addRequest("confluence-delete-page", req); + + const page = pages.find( + (pageCandidate) => pageCandidate.id === req.params.pageId, + ); + if (page) { + res.status(204).send(); + } else { + core.logger.error( + `Page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + +function getAttachmentsMiddleware(attachments) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Requested attachments for page with id ${req.params.pageId} to Confluence`, + ); + + addRequest("confluence-get-attachments", req); + const pageAttachments = attachments.find( + (attachment) => attachment.id === req.params.pageId, + ); + if (pageAttachments) { + core.logger.info( + `Sending attachments ${JSON.stringify(pageAttachments)}`, + ); + res.status(200).json(pageAttachments.attachments); + } else { + core.logger.error( + `Attachments for page with id ${req.params.pageId} not found in Confluence`, + ); + res.status(404).send(); + } + }; +} + +function createAttachmentsMiddleware(attachments) { + return ( + req: ServerRequest, + res: ServerResponse, + _next: NextFunction, + core: ScopedCoreInterface, + ) => { + core.logger.info( + `Creating attachments for page with id ${req.params.pageId} in Confluence: ${JSON.stringify( + req.body, + )}`, + ); + + addRequest("confluence-create-attachments", req); + + const attachmentsResponse = attachments.find( + (attachment) => attachment.id === req.params.pageId, + ); + if (attachmentsResponse) { + res.status(200).send(); + } else { + core.logger.error( + `Bad request creating attachments for page with id ${ + req.params.pageId + } in Confluence: ${JSON.stringify(req.body)}`, + ); + core.logger.error( + `Available attachments: ${JSON.stringify(attachments)}`, + ); + res.status(400).send(); + } + }; +} + +const confluenceRoutes: RouteDefinition[] = [ + { + id: "confluence-get-page", + url: "/rest/api/content/:pageId", + method: "GET", + variants: [ + { + id: "empty-root", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_EMPTY_ROOT), + }, + }, + { + id: "default-root", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_DEFAULT_ROOT_GET), + }, + }, + { + id: "with-root-page-name", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_WITH_ROOT_PAGE_NAME), + }, + }, + { + id: "with-mdx-files", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_WITH_MDX_FILES), + }, + }, + { + id: "with-confluence-title", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_WITH_CONFLUENCE_TITLE), + }, + }, + { + id: "with-alternative-index-files", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_WITH_ALTERNATIVE_INDEX_FILES), + }, + }, + { + id: "with-confluence-page-id", + type: "middleware", + options: { + middleware: getPageMiddleware(PAGES_WITH_CONFLUENCE_PAGE_ID), + }, + }, + ], + }, + { + id: "confluence-create-page", + url: "/rest/api/content", + method: "POST", + variants: [ + { + id: "empty-root", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_EMPTY_ROOT), + }, + }, + { + id: "default-root", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_DEFAULT_ROOT_CREATE), + }, + }, + { + id: "with-root-page-name", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_WITH_ROOT_PAGE_NAME), + }, + }, + { + id: "with-mdx-files", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_WITH_MDX_FILES), + }, + }, + { + id: "with-confluence-title", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_WITH_CONFLUENCE_TITLE), + }, + }, + { + id: "with-alternative-index-files", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_WITH_ALTERNATIVE_INDEX_FILES), + }, + }, + { + id: "with-confluence-page-id", + type: "middleware", + options: { + middleware: createPageMiddleware(PAGES_WITH_CONFLUENCE_PAGE_ID), + }, + }, + ], + }, + { + id: "confluence-update-page", + url: "/rest/api/content/:pageId", + method: "PUT", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: updatePageMiddleware(PAGES_DEFAULT_ROOT_UPDATE), + }, + }, + { + id: "with-confluence-page-id", + type: "middleware", + options: { + middleware: updatePageMiddleware(PAGES_WITH_CONFLUENCE_PAGE_ID), + }, + }, + ], + }, + { + id: "confluence-delete-page", + url: "/rest/api/content/:pageId", + method: "DELETE", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: deletePageMiddleware(PAGES_DEFAULT_ROOT_DELETE), + }, + }, + ], + }, + { + id: "confluence-get-attachments", + url: "/rest/api/content/:pageId/child/attachment", + method: "GET", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: getAttachmentsMiddleware(ATTACHMENTS_DEFAULT_ROOT), + }, + }, + { + id: "with-confluence-page-id", + type: "middleware", + options: { + middleware: getAttachmentsMiddleware( + PAGES_WITH_CONFLUENCE_PAGE_ID_ATTACHMENTS, + ), + }, + }, + ], + }, + { + id: "confluence-create-attachments", + url: "/rest/api/content/:pageId/child/attachment", + method: "POST", + variants: [ + { + id: "default-root", + type: "middleware", + options: { + middleware: createAttachmentsMiddleware(ATTACHMENTS_DEFAULT_ROOT), + }, + }, + { + id: "empty-root", + type: "middleware", + options: { + middleware: createAttachmentsMiddleware(ATTACHMENTS_DEFAULT_ROOT), + }, + }, + ], + }, +]; + +export default confluenceRoutes; diff --git a/components/markdown-confluence-sync/mocks/routes/Spy.ts b/components/markdown-confluence-sync/mocks/routes/Spy.ts new file mode 100644 index 00000000..e6766588 --- /dev/null +++ b/components/markdown-confluence-sync/mocks/routes/Spy.ts @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + Request as ServerRequest, + Response as ServerResponse, + RouteDefinition, +} from "@mocks-server/core"; + +import { getRequests, resetRequests } from "../support/SpyStorage"; + +const spyRoutes: RouteDefinition[] = [ + { + id: "spy-get-requests", + url: "/spy/requests", + method: "GET", + variants: [ + { + id: "send", + type: "middleware", + options: { + middleware: (_req: ServerRequest, res: ServerResponse) => { + res.status(200).send(getRequests()); + }, + }, + }, + ], + }, + { + id: "spy-reset-requests", + url: "/spy/requests", + method: "DELETE", + variants: [ + { + id: "reset", + type: "middleware", + options: { + middleware: (_req: ServerRequest, res: ServerResponse) => { + resetRequests(); + res.status(200).send({ reset: true }); + }, + }, + }, + ], + }, +]; + +export default spyRoutes; diff --git a/components/markdown-confluence-sync/mocks/support/SpyStorage.ts b/components/markdown-confluence-sync/mocks/support/SpyStorage.ts new file mode 100644 index 00000000..b2fbd8ba --- /dev/null +++ b/components/markdown-confluence-sync/mocks/support/SpyStorage.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Request as ServerRequest } from "@mocks-server/core"; + +import type { SpyRequest } from "./SpyStorage.types"; + +let requests: SpyRequest[] = []; + +export function addRequest(routeId: string, request: ServerRequest) { + requests.push({ + routeId, + url: request.url, + method: request.method, + headers: request.headers, + body: request.body, + params: request.params, + }); +} + +export function getRequests(): SpyRequest[] { + return requests; +} + +export function resetRequests(): void { + requests = []; +} diff --git a/components/markdown-confluence-sync/mocks/support/SpyStorage.types.ts b/components/markdown-confluence-sync/mocks/support/SpyStorage.types.ts new file mode 100644 index 00000000..005f579a --- /dev/null +++ b/components/markdown-confluence-sync/mocks/support/SpyStorage.types.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +/** Request to the mock server */ +export interface SpyRequest { + /** Route ID */ + routeId: string; + /** Request URL */ + url: string; + /** Request method */ + method: string; + /** Request headers */ + headers: Record; + /** Request body */ + body?: Record; + /** Request query params */ + params?: Record; +} diff --git a/components/markdown-confluence-sync/mocks/support/fixtures/ConfluencePages.ts b/components/markdown-confluence-sync/mocks/support/fixtures/ConfluencePages.ts new file mode 100644 index 00000000..970fb0bd --- /dev/null +++ b/components/markdown-confluence-sync/mocks/support/fixtures/ConfluencePages.ts @@ -0,0 +1,539 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export const PAGES_EMPTY_ROOT = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-child2-id", + title: "[foo-parent-title] foo-child2-title", + content: "foo-child2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + { + id: "foo-grandChild3-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, + { + id: "foo-grandChild4-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, + { + id: "foo-child3-id", + title: "[foo-parent-title] foo-child3-title", + content: "foo-child3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild5-id", + title: "[foo-parent-title][foo-child3-title] foo-grandChild5-title", + content: "foo-grandChild5-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child3-id", title: "[foo-parent-title] foo-child3-title" }, + ], + }, + { + id: "foo-child4-id", + title: "[foo-parent-title] foo-child4-title", + content: "foo-child4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild6-id", + title: "[foo-parent-title][child4] foo-grandChild6-title", + content: "foo-grandChild6-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child4-id", title: "[foo-parent-title] foo-child4-title" }, + ], + }, + { + id: "foo-grandChild7-id", + title: "[foo-parent-title][child4] foo-grandChild7-title", + content: "foo-grandChild7-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child4-id", title: "[foo-parent-title] foo-child4-title" }, + ], + }, + { + id: "foo-child5-id", + title: "[foo-parent-title] foo-child5-title", + content: "foo-child5-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild8-id", + title: "[foo-parent-title][child5] foo-grandChild8-title", + content: "foo-grandChild8-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child5-id", title: "[foo-parent-title] foo-child5-title" }, + ], + }, + { + id: "foo-child6-id", + title: "[foo-parent-title] foo-child6-title", + content: "", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild9-id", + title: "[foo-parent-title][foo-child6-title] foo-grandChild9-title", + content: "", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child6-id", title: "[foo-parent-title] foo-child6-title" }, + ], + }, + { + id: "foo-greatGrandChild1-id", + title: + "[foo-parent-title][foo-child6-title][foo-grandChild9-title] foo-greatGrandChild-title", + content: "foo-greatGrandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child6-id", title: "[foo-parent-title] foo-child6-title" }, + { + id: "foo-grandChild9-id", + title: "[foo-parent-title][foo-child6-title] foo-grandChild9-title", + }, + ], + }, + { + id: "foo-grandChild10-id", + title: "[foo-parent-title][foo-child6-title] foo-grandChild10-title", + content: "", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child6-id", title: "[foo-parent-title] foo-child6-title" }, + ], + }, + { + id: "foo-greatGrandChild2-id", + title: + "[foo-parent-title][foo-child6-title][foo-grandChild10-title] foo-greatGrandChild-title", + content: "foo-greatGrandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child6-id", title: "[foo-parent-title] foo-child6-title" }, + { + id: "foo-grandChild10-id", + title: "[foo-parent-title][foo-child6-title] foo-grandChild10-title", + }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_GET = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: { + page: { results: [{ id: "foo-parent-id", title: "foo-parent-title" }] }, + }, + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + children: { + page: { + results: [ + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + }, + }, + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + content: "foo-child1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + children: { + page: { + results: [ + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + }, + ], + }, + }, + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + content: "foo-grandChild1-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + content: "foo-grandChild2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_CREATE = [ + { + id: "foo-child2-id", + title: "[foo-parent-title] foo-child2-title", + content: "foo-child2-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-child3-id", + title: "[foo-parent-title] foo-child3-title", + content: "foo-child3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild3-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + content: "foo-grandChild3-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, + { + id: "foo-grandChild4-id", + title: "[foo-parent-title][foo-child2-title] foo-grandChild4-title", + content: "foo-grandChild4-content", + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child2-id", title: "[foo-parent-title] foo-child2-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_UPDATE = [ + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + version: { number: 2 }, + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + content: "foo-child1-content", + version: { number: 2 }, + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + ], + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + content: "foo-grandChild1-content", + version: { number: 2 }, + ancestors: [ + { id: "foo-root-id", title: "foo-root-title" }, + { id: "foo-parent-id", title: "foo-parent-title" }, + { id: "foo-child1-id", title: "[foo-parent-title] foo-child1-title" }, + ], + }, +]; + +export const PAGES_DEFAULT_ROOT_DELETE = [ + { + id: "foo-child1-id", + title: "[foo-parent-title] foo-child1-title", + }, + { + id: "foo-grandChild1-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + }, + { + id: "foo-grandChild2-id", + title: "[foo-parent-title][foo-child1-title] foo-grandChild2-title", + }, + { + id: "foo-grandChild1-attachment-id", + title: "foo-grandChild1-attachment-title", + }, +]; + +export const PAGES_WITH_ROOT_PAGE_NAME = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "foo-parent-id", + title: "[foo-root-name] foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, +]; + +export const PAGES_WITH_MDX_FILES = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "foo-parent-id", + title: "foo-parent-title", + content: "foo-parent-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, +]; + +export const ATTACHMENTS_DEFAULT_ROOT = [ + { + id: "foo-parent-id", + attachments: { + results: [], + }, + }, + { + id: "foo-child1-id", + attachments: { + results: [], + }, + }, + { + id: "foo-grandChild1-id", + attachments: { + results: [ + { + id: "foo-grandChild1-attachment-id", + title: "foo-grandChild1-attachment-title", + }, + ], + }, + }, +]; + +export const PAGES_WITH_CONFLUENCE_TITLE = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "foo-parent-id", + title: "Confluence title", + }, + { + id: "foo-child1-id", + title: "[Confluence title] foo-child1-title", + }, + { + id: "foo-grandChild1-id", + title: "[Confluence title][foo-child1-title] Confluence grandChild 1", + }, +]; + +export const PAGES_WITH_ALTERNATIVE_INDEX_FILES = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + children: [], + }, + { + id: "README-id", + title: "README", + }, + { + id: "README-child-id", + title: "[README] child", + }, + { + id: "README-mdx-id", + title: "README-mdx", + }, + { + id: "README-child-mdx-id", + title: "[README-mdx] child", + }, + { + id: "directory-name-id", + title: "directory-name", + }, + { + id: "directory-name-child-id", + title: "[directory-name] child", + }, + { + id: "directory-name-2-mdx-id", + title: "directory-name-2-mdx", + }, + { + id: "directory-name-2-child-mdx-id", + title: "[directory-name-2-mdx] child", + }, + { + id: "index-id.md", + title: "index.md", + }, + { + id: "index.mdx-id", + title: "index.mdx", + }, +]; + +export const PAGES_WITH_CONFLUENCE_PAGE_ID = [ + { + id: "foo-root-id", + title: "foo-root-title", + content: "foo-root-content", + ancestors: [], + }, + { + id: "foo-parent", + title: "[FLAT] foo-parent-title", + ancestors: [], + }, + { + id: "foo-child1", + title: "[FLAT] foo-child1-title", + ancestors: [], + }, + { + id: "foo-grandChild1", + title: "[FLAT] foo-grandChild1-title", + ancestors: [], + }, + { + id: "foo-grandChild2", + title: "[FLAT] foo-grandChild2-title", + ancestors: [], + }, + { + id: "foo-child-2-grandChild1", + title: "[FLAT] foo-grandChild1-title", + ancestors: [], + }, + { + id: "foo-child-2-child1-grandChild1", + title: "[FLAT] foo-grandChild1-title", + ancestors: [], + }, +]; + +export const PAGES_WITH_CONFLUENCE_PAGE_ID_ATTACHMENTS = [ + { + id: "foo-child1", + attachments: { + results: [], + }, + }, +]; diff --git a/components/markdown-confluence-sync/mocks/tsconfig.json b/components/markdown-confluence-sync/mocks/tsconfig.json new file mode 100644 index 00000000..6beff1fb --- /dev/null +++ b/components/markdown-confluence-sync/mocks/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["**/*"] +} diff --git a/components/markdown-confluence-sync/package.json b/components/markdown-confluence-sync/package.json new file mode 100644 index 00000000..16066dd0 --- /dev/null +++ b/components/markdown-confluence-sync/package.json @@ -0,0 +1,143 @@ +{ + "name": "@tid-cross/markdown-confluence-sync", + "description": "Creates/updates/deletes Confluence pages based on markdown files in a directory. Supports Mermaid diagrams and per-page configuration using frontmatter metadata. Works great with Docusaurus", + "version": "1.0.0-beta.1", + "license": "Apache-2.0", + "author": "Telefónica Innovación Digital", + "repository": { + "type": "git", + "url": "https://github.com/Telefonica/cross-confluence-tools", + "directory": "components/markdown-confluence-sync" + }, + "homepage": "https://github.com/Telefonica/cross-confluence-tools/tree/main/components/markdown-confluence-sync", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "Confluence", + "markdown", + "Docusaurus", + "sync", + "page", + "pages", + "md", + "mdx", + "frontmatter", + "metadata", + "files", + "folder", + "directory", + "tree", + "sync", + "update", + "create", + "delete", + "cli", + "node", + "json", + "attachments", + "images" + ], + "scripts": { + "build": "tsc", + "check:all": "echo 'All checks passed'", + "check:spell": "cspell .", + "check:types": "npm run check:types:test:unit && npm run check:types:test:component && npm run check:types:lib", + "check:types:lib": "tsc --noEmit", + "check:types:test:component": "tsc --noEmit --project ./test/component/tsconfig.json", + "check:types:test:unit": "tsc --noEmit --project ./test/unit/tsconfig.json", + "confluence:mock": "mocks-server", + "confluence:mock:ci": "mocks-server --no-plugins.inquirerCli.enabled", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:component": "start-server-and-test confluence:mock:ci http-get://127.0.0.1:3110/api/about test:component:run", + "test:component:run": "jest --config jest.component.config.cjs --runInBand", + "test:unit": "jest --config jest.unit.config.cjs" + }, + "nx": { + "includedScripts": [ + "build", + "check:all", + "check:spell", + "check:types", + "lint", + "test:unit", + "test:component" + ] + }, + "dependencies": { + "@mermaid-js/mermaid-cli": "11.4.0", + "@mocks-server/config": "2.0.0-beta.3", + "@mocks-server/logger": "2.0.0-beta.2", + "@tid-cross/confluence-sync": "workspace:*", + "fs-extra": "11.2.0", + "handlebars": "4.7.8", + "hast": "1.0.0", + "hast-util-to-string": "2.0.0", + "mdast-util-mdx": "3.0.0", + "mdast-util-mdx-jsx": "3.1.3", + "mdast-util-to-markdown": "2.1.1", + "rehype-raw": "5.1.0", + "rehype-stringify": "9.0.4", + "remark": "14.0.3", + "remark-directive": "2.0.1", + "remark-frontmatter": "4.0.1", + "remark-gfm": "3.0.1", + "remark-mdx": "2.3.0", + "remark-parse": "10.0.2", + "remark-parse-frontmatter": "1.0.3", + "remark-rehype": "10.1.0", + "remark-stringify": "10.0.3", + "remark-unlink": "4.0.1", + "to-vfile": "7.2.4", + "unified": "10.1.2", + "unist-util-find": "1.0.4", + "unist-util-find-after": "4.0.1", + "unist-util-is": "5.2.1", + "unist-util-remove": "3.1.1", + "unist-util-visit": "4.1.2", + "unist-util-visit-parents": "5.1.3", + "vfile": "5.3.7", + "which": "3.0.1", + "yaml": "2.3.4", + "zod": "3.22.4" + }, + "devDependencies": { + "@mocks-server/admin-api-client": "8.0.0-beta.2", + "@mocks-server/core": "5.0.0-beta.3", + "@mocks-server/main": "5.0.0-beta.4", + "@tid-cross/child-process-manager": "workspace:*", + "@types/fs-extra": "11.0.4", + "@types/glob": "8.1.0", + "@types/hast": "2.3.10", + "@types/mdast": "3.0.15", + "@types/tmp": "0.2.6", + "@types/unist": "2.0.11", + "@types/which": "3.0.4", + "babel-plugin-transform-import-meta": "2.2.1", + "cross-fetch": "4.0.0", + "glob": "10.3.10", + "rehype": "12.0.1", + "rehype-parse": "8.0.5", + "remark-stringify": "10.0.3", + "start-server-and-test": "2.0.8", + "tmp": "0.2.3", + "ts-dedent": "2.2.0", + "unist-builder": "4.0.0" + }, + "files": [ + "dist", + "bin", + "config" + ], + "bin": { + "markdown-confluence-sync": "./bin/markdown-confluence-sync.mjs" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "engines": { + "node": ">=18", + "pnpm": ">=8" + } +} diff --git a/components/markdown-confluence-sync/project.json b/components/markdown-confluence-sync/project.json new file mode 100644 index 00000000..5ab296f0 --- /dev/null +++ b/components/markdown-confluence-sync/project.json @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "markdown-confluence-sync", + "projectType": "library", + "tags": [ + "type:node:app", + "type:node:lib" + ], + "targets": { + // Redefine the lint target to include the dependency of building sync-confluence to resolve imports + "lint": { + "cache": true, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default" + ], + "dependsOn": [ + { + "target": "eslint:config", + "projects": ["eslint-config"] + }, + // Build the dev dependency of sync-confluence to resolve imports + { + "target": "build", + "projects": ["child-process-manager"] + }, + // Build the package itself and dependencies to resolve imports + "build" + ] + }, + // Redefine the build target to include the config directory as an output + "build": { + "cache": true, + "dependsOn": [ + { + "projects": ["confluence-sync"], + "target": "build" + } + ], + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default", + "production" + ], + "outputs": [ + "{projectRoot}/dist/**/*", + "{projectRoot}/package.json", + "{projectRoot}/README.md", + "{projectRoot}/CHANGELOG.md", + "{projectRoot}/bin/**/*", + "{projectRoot}/config/**/*" + ] + } + }, + "implicitDependencies": [ + "eslint-config", + "cspell-config" + ] +} diff --git a/components/markdown-confluence-sync/src/Cli.ts b/components/markdown-confluence-sync/src/Cli.ts new file mode 100644 index 00000000..a69de317 --- /dev/null +++ b/components/markdown-confluence-sync/src/Cli.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { MarkdownConfluenceSync } from "./lib/index.js"; + +export async function run() { + const markdownToConfluence = new MarkdownConfluenceSync({ + config: { + readArguments: true, + readEnvironment: true, + readFile: true, + }, + }); + await markdownToConfluence.sync(); +} diff --git a/components/markdown-confluence-sync/src/index.ts b/components/markdown-confluence-sync/src/index.ts new file mode 100644 index 00000000..74fcbe35 --- /dev/null +++ b/components/markdown-confluence-sync/src/index.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./lib/index.js"; diff --git a/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts new file mode 100644 index 00000000..d05f1ad0 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.ts @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfigInterface as customMarkdownConfluenceSyncClass } from "@mocks-server/config"; +import { Config } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import { SyncModes } from "@tid-cross/confluence-sync"; + +import { ConfluenceSync } from "./confluence/ConfluenceSync.js"; +import type { + ConfluenceSyncInterface, + ConfluenceSyncPage, +} from "./confluence/ConfluenceSync.types.js"; +import { DocusaurusPages } from "./docusaurus/DocusaurusPages.js"; +import type { + DocusaurusPage, + DocusaurusPagesInterface, +} from "./docusaurus/DocusaurusPages.types.js"; +import type { + MarkdownConfluenceSyncConstructor, + MarkdownConfluenceSyncInterface, + Configuration, + LogLevelOption, + LogLevelOptionDefinition, + ModeOptionDefinition, + FilesPatternOptionDefinition, + ModeOption, + FilesPatternOption, +} from "./MarkdownConfluenceSync.types.js"; + +const MODULE_NAME = "markdown-confluence-sync"; +const DOCUSAURUS_NAMESPACE = "docusaurus"; +const CONFLUENCE_NAMESPACE = "confluence"; + +const DEFAULT_CONFIG: Configuration["config"] = { + readArguments: false, + readEnvironment: false, + readFile: false, +}; + +const logLevelOption: LogLevelOptionDefinition = { + name: "logLevel", + type: "string", + default: "info", +}; + +const modeOption: ModeOptionDefinition = { + name: "mode", + type: "string", + default: SyncModes.TREE, +}; + +const filesPatternOption: FilesPatternOptionDefinition = { + name: "filesPattern", + type: "string", +}; + +export const MarkdownConfluenceSync: MarkdownConfluenceSyncConstructor = class MarkdownConfluenceSync + implements MarkdownConfluenceSyncInterface +{ + private _docusaurusPages: DocusaurusPagesInterface; + private _confluenceSync: ConfluenceSyncInterface; + private _configuration: customMarkdownConfluenceSyncClass; + private _initialized = false; + private _config: Configuration; + private _logger: LoggerInterface; + private _logLevelOption: LogLevelOption; + private _modeOption: ModeOption; + private _filesPatternOption: FilesPatternOption; + + constructor(config: Configuration) { + this._config = config; + if (!this._config) { + throw new Error("Please provide configuration"); + } + + this._configuration = new Config({ moduleName: MODULE_NAME }); + this._logger = new Logger(MODULE_NAME); + this._logLevelOption = this._configuration.addOption( + logLevelOption, + ) as LogLevelOption; + this._modeOption = this._configuration.addOption(modeOption) as ModeOption; + this._filesPatternOption = this._configuration.addOption( + filesPatternOption, + ) as FilesPatternOption; + + const docusaurusLogger = this._logger.namespace(DOCUSAURUS_NAMESPACE); + + const confluenceConfig = + this._configuration.addNamespace(CONFLUENCE_NAMESPACE); + const confluenceLogger = this._logger.namespace(CONFLUENCE_NAMESPACE); + + this._docusaurusPages = new DocusaurusPages({ + config: this._configuration, + logger: docusaurusLogger, + mode: this._modeOption, + filesPattern: this._filesPatternOption, + }); + this._confluenceSync = new ConfluenceSync({ + config: confluenceConfig, + logger: confluenceLogger, + mode: this._modeOption, + }); + } + + public async sync(): Promise { + await this._init(); + const pages = await this._docusaurusPages.read(); + await this._confluenceSync.sync( + this._docusaurusPagesToConfluencePages(pages), + ); + } + + private async _init() { + if (!this._initialized) { + await this._configuration.load({ + config: { ...DEFAULT_CONFIG, ...this._config.config }, + ...this._config, + }); + this._logger.setLevel(this._logLevelOption.value); + this._initialized = true; + } + } + + private _docusaurusPagesToConfluencePages( + docusaurusPages: DocusaurusPage[], + ): ConfluenceSyncPage[] { + this._logger.info( + `Converting ${docusaurusPages.length} Docusaurus pages to Confluence pages...`, + ); + return docusaurusPages; + } +}; diff --git a/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts new file mode 100644 index 00000000..996641a9 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/MarkdownConfluenceSync.types.ts @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + OptionDefinition, + OptionInterfaceOfType, +} from "@mocks-server/config"; +import type { LogLevel } from "@mocks-server/logger"; +import type { SyncModes } from "@tid-cross/confluence-sync"; + +export type FilesPattern = string | string[]; + +declare global { + //eslint-disable-next-line @typescript-eslint/no-namespace + namespace MarkdownConfluenceSync { + interface Config { + /** Configuration options */ + config?: { + /** Read configuration from file */ + readFile?: boolean; + /** Read configuration from arguments */ + readArguments?: boolean; + /** Read configuration from environment */ + readEnvironment?: boolean; + }; + /** Log level */ + logLevel?: LogLevel; + /** Mode to structure pages */ + mode?: SyncModes; + /** + * Pattern to search files when flat mode is active + * @see {@link https://github.com/isaacs/node-glob#globpattern-string--string-options-globoptions--promisestring--path | Node Glob Pattern} + * @see {@link https://github.com/isaacs/node-glob#glob-primer} + * */ + filesPattern?: FilesPattern; + } + } +} + +// eslint-disable-next-line no-undef +export type Configuration = MarkdownConfluenceSync.Config; + +export type LogLevelOptionDefinition = OptionDefinition< + LogLevel, + { hasDefault: true } +>; + +export type LogLevelOption = OptionInterfaceOfType< + LogLevel, + { hasDefault: true } +>; + +export type ModeOptionDefinition = OptionDefinition< + SyncModes, + { hasDefault: true } +>; + +export type ModeOption = OptionInterfaceOfType; + +export type FilesPatternOptionDefinition = OptionDefinition; + +export type FilesPatternOption = OptionInterfaceOfType; + +/** Creates a MarkdownConfluenceSync interface */ +export interface MarkdownConfluenceSyncConstructor { + /** Returns MarkdownConfluenceSync interface + * @returns MarkdownConfluenceSync instance {@link MarkdownConfluenceSyncInterface}. + */ + // eslint-disable-next-line no-undef + new (options: MarkdownConfluenceSync.Config): MarkdownConfluenceSyncInterface; +} + +export interface MarkdownConfluenceSyncInterface { + /** Sync pages in Confluence*/ + sync(): Promise; +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.ts b/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.ts new file mode 100644 index 00000000..bad82fc5 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.ts @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; +import type { + ConfluenceInputPage, + ConfluenceSyncPagesInterface, +} from "@tid-cross/confluence-sync"; +import { ConfluenceSyncPages, SyncModes } from "@tid-cross/confluence-sync"; + +import type { ModeOption } from "../MarkdownConfluenceSync.types.js"; +import { isStringWithLength } from "../support/typesValidations.js"; + +import type { + NoticeMessageOption, + NoticeMessageOptionDefinition, + ConfluenceSyncConstructor, + ConfluenceSyncInterface, + ConfluenceSyncOptions, + ConfluenceSyncPage, + DryRunOption, + DryRunOptionDefinition, + PersonalAccessTokenOption, + PersonalAccessTokenOptionDefinition, + RootPageIdOption, + RootPageIdOptionDefinition, + RootPageNameOption, + RootPageNameOptionDefinition, + SpaceKeyOption, + SpaceKeyOptionDefinition, + UrlOption, + UrlOptionDefinition, + NoticeTemplateOptionDefinition, + NoticeTemplateOption, +} from "./ConfluenceSync.types.js"; +import { ConfluencePageTransformer } from "./transformer/ConfluencePageTransformer.js"; +import type { ConfluencePageTransformerInterface } from "./transformer/ConfluencePageTransformer.types.js"; +import { PageIdRequiredException } from "./transformer/errors/PageIdRequiredException.js"; + +const urlOption: UrlOptionDefinition = { + name: "url", + type: "string", +}; + +const personalAccessTokenOption: PersonalAccessTokenOptionDefinition = { + name: "personalAccessToken", + type: "string", +}; + +const spaceKeyOption: SpaceKeyOptionDefinition = { + name: "spaceKey", + type: "string", +}; + +const rootPageIdOption: RootPageIdOptionDefinition = { + name: "rootPageId", + type: "string", +}; + +const rootPageNameOption: RootPageNameOptionDefinition = { + name: "rootPageName", + type: "string", +}; + +const noticeMessageOption: NoticeMessageOptionDefinition = { + name: "noticeMessage", + type: "string", +}; + +const noticeTemplateOption: NoticeTemplateOptionDefinition = { + name: "noticeTemplate", + type: "string", +}; + +const dryRunOption: DryRunOptionDefinition = { + name: "dryRun", + type: "boolean", + default: false, +}; + +export const ConfluenceSync: ConfluenceSyncConstructor = class ConfluenceSync + implements ConfluenceSyncInterface +{ + private _confluencePageTransformer: ConfluencePageTransformerInterface; + private _confluenceSyncPages: ConfluenceSyncPagesInterface; + private _urlOption: UrlOption; + private _personalAccessTokenOption: PersonalAccessTokenOption; + private _spaceKeyOption: SpaceKeyOption; + private _rootPageIdOption: RootPageIdOption; + private _rootPageNameOption: RootPageNameOption; + private _noticeMessageOption: NoticeMessageOption; + private _noticeTemplateOption: NoticeTemplateOption; + private _dryRunOption: DryRunOption; + private _initialized = false; + private _logger: LoggerInterface; + private _modeOption: ModeOption; + + constructor({ config, logger, mode }: ConfluenceSyncOptions) { + this._urlOption = config.addOption(urlOption) as UrlOption; + this._personalAccessTokenOption = config.addOption( + personalAccessTokenOption, + ) as PersonalAccessTokenOption; + this._spaceKeyOption = config.addOption(spaceKeyOption) as SpaceKeyOption; + this._rootPageIdOption = config.addOption( + rootPageIdOption, + ) as RootPageIdOption; + this._rootPageNameOption = config.addOption( + rootPageNameOption, + ) as RootPageNameOption; + this._noticeMessageOption = config.addOption( + noticeMessageOption, + ) as NoticeMessageOption; + this._noticeTemplateOption = config.addOption( + noticeTemplateOption, + ) as NoticeTemplateOption; + this._dryRunOption = config.addOption(dryRunOption) as DryRunOption; + this._modeOption = mode; + this._logger = logger; + } + + public async sync(confluencePages: ConfluenceSyncPage[]): Promise { + await this._init(); + this._logger.debug(`confluence.url option is ${this._urlOption.value}`); + this._logger.debug( + `confluence.spaceKey option is ${this._spaceKeyOption.value}`, + ); + this._logger.debug( + `confluence.dryRun option is ${this._dryRunOption.value}`, + ); + this._logger.info( + `Confluence pages to sync: ${confluencePages.map((page) => page.title).join(", ")}`, + ); + this._logger.silly( + `Extended version: ${JSON.stringify(confluencePages, null, 2)}`, + ); + const pages = + await this._confluencePageTransformer.transform(confluencePages); + this._checkConfluencePagesIds(pages); + this._logger.info( + `Confluence pages to sync after transformation: ${pages + .map((page) => page.title) + .join(", ")}`, + ); + this._logger.silly(`Extended version: ${JSON.stringify(pages, null, 2)}`); + await this._confluenceSyncPages.sync(pages); + } + + private _init() { + if (!this._initialized) { + if (!this._urlOption.value) { + throw new Error( + "Confluence URL is required. Please set confluence.url option.", + ); + } + if (!this._personalAccessTokenOption.value) { + throw new Error( + "Confluence personal access token is required. Please set confluence.personalAccessToken option.", + ); + } + if (!this._spaceKeyOption.value) { + throw new Error( + "Confluence space id is required. Please set confluence.spaceId option.", + ); + } + if ( + !this._rootPageIdOption.value && + this._modeOption.value === SyncModes.TREE + ) { + throw new Error( + "Confluence root page id is required for TREE sync mode. Please set confluence.rootPageId option.", + ); + } + + this._confluencePageTransformer = new ConfluencePageTransformer({ + noticeMessage: this._noticeMessageOption.value, + noticeTemplate: this._noticeTemplateOption.value, + rootPageName: this._rootPageNameOption.value, + spaceKey: this._spaceKeyOption.value, + logger: this._logger.namespace("transformer"), + }); + + this._confluenceSyncPages = new ConfluenceSyncPages({ + url: this._urlOption.value, + personalAccessToken: this._personalAccessTokenOption.value, + spaceId: this._spaceKeyOption.value, + rootPageId: this._rootPageIdOption.value, + logLevel: this._logger.level, + dryRun: this._dryRunOption.value, + syncMode: this._modeOption.value as SyncModes, + }); + this._initialized = true; + } + } + + private _checkConfluencePagesIds(pages: ConfluenceInputPage[]) { + if ( + !this._rootPageIdOption.value && + this._modeOption.value === SyncModes.FLAT + ) { + const allPagesHaveId = pages.every(({ id }) => + isStringWithLength(id as string), + ); + if (!allPagesHaveId) { + throw new PageIdRequiredException(); + } + } + } +}; diff --git a/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.types.ts b/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.types.ts new file mode 100644 index 00000000..7ca5574c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.types.ts @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + ConfigNamespaceInterface, + OptionInterfaceOfType, + OptionDefinition, +} from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import type { ConfluenceInputPage } from "@tid-cross/confluence-sync"; + +import type { ModeOption } from "../MarkdownConfluenceSync.types"; + +type UrlOptionValue = string; +type PersonalAccessTokenOptionValue = string; +type SpaceKeyOptionValue = string; +type RootPageIdOptionValue = string; +type RootPageNameOptionValue = string; +type NoticeMessageOptionValue = string; +type NoticeTemplateOptionValue = string; +type DryRunOptionValue = boolean; + +declare global { + //eslint-disable-next-line @typescript-eslint/no-namespace + namespace MarkdownConfluenceSync { + interface Config { + confluence?: { + /** Confluence URL */ + url?: UrlOptionValue; + /** Confluence personal access token */ + personalAccessToken?: PersonalAccessTokenOptionValue; + /** Confluence space key */ + spaceKey?: SpaceKeyOptionValue; + /** Confluence root page id */ + rootPageId?: RootPageIdOptionValue; + /** Confluence dry run */ + dryRun?: DryRunOptionValue; + }; + } + } +} + +export type UrlOptionDefinition = OptionDefinition; +export type PersonalAccessTokenOptionDefinition = + OptionDefinition; +export type SpaceKeyOptionDefinition = OptionDefinition; +export type RootPageIdOptionDefinition = + OptionDefinition; +export type RootPageNameOptionDefinition = + OptionDefinition; +export type NoticeMessageOptionDefinition = + OptionDefinition; +export type NoticeTemplateOptionDefinition = + OptionDefinition; +export type DryRunOptionDefinition = OptionDefinition< + DryRunOptionValue, + { hasDefault: true } +>; + +export type UrlOption = OptionInterfaceOfType; +export type PersonalAccessTokenOption = + OptionInterfaceOfType; +export type SpaceKeyOption = OptionInterfaceOfType; +export type RootPageIdOption = OptionInterfaceOfType; +export type RootPageNameOption = OptionInterfaceOfType; +export type NoticeMessageOption = + OptionInterfaceOfType; +export type NoticeTemplateOption = + OptionInterfaceOfType; +export type DryRunOption = OptionInterfaceOfType< + DryRunOptionValue, + { hasDefault: true } +>; + +export interface ConfluenceSyncOptions { + /** Configuration interface */ + config: ConfigNamespaceInterface; + /** Logger interface */ + logger: LoggerInterface; + /** Sync mode option */ + mode: ModeOption; +} + +/** Creates a ConfluenceSyncInterface interface */ +export interface ConfluenceSyncConstructor { + /** Returns ConfluenceSyncInterface interface + * @returns ConfluenceSync instance {@link ConfluenceSyncInterface}. + */ + new (options: ConfluenceSyncOptions): ConfluenceSyncInterface; +} + +export interface ConfluenceSyncInterface { + /** Sync pages to Confluence */ + sync(pages: ConfluenceSyncPage[]): Promise; +} + +/** Represents a Confluence page with its path */ +export interface ConfluenceSyncPage extends ConfluenceInputPage { + /** + * Confluence page ancestors + * @override + * @see {@link DocusaurusPage} + */ + ancestors: string[]; + /** Confluence page path */ + path: string; + /** Confluence page path relative to docs root dir */ + relativePath: string; + /** + * Confluence page name + * + * Forces the confluence page title in child pages' title. + */ + name?: string; +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.ts new file mode 100644 index 00000000..414aeaf5 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.ts @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { dirname, resolve } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import type { ConfluenceInputPage } from "@tid-cross/confluence-sync"; +import type { TemplateDelegate } from "handlebars"; +import Handlebars from "handlebars"; +import rehypeRaw from "rehype-raw"; +import rehypeStringify from "rehype-stringify"; +import { remark } from "remark"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkGfm from "remark-gfm"; +import remarkRehype from "remark-rehype"; +import { toVFile } from "to-vfile"; + +import type { ConfluenceSyncPage } from "../ConfluenceSync.types.js"; + +import type { + ConfluencePageTransformerConstructor, + ConfluencePageTransformerInterface, + ConfluencePageTransformerOptions, + ConfluencePageTransformerTemplateData, +} from "./ConfluencePageTransformer.types.js"; +import { InvalidTemplateError } from "./errors/InvalidTemplateError.js"; +import rehypeAddAttachmentsImages from "./support/rehype/rehype-add-attachments-images.js"; +import type { ImagesMetadata } from "./support/rehype/rehype-add-attachments-images.types.js"; +import rehypeAddNotice from "./support/rehype/rehype-add-notice.js"; +import rehypeReplaceDetails from "./support/rehype/rehype-replace-details.js"; +import rehypeReplaceImgTags from "./support/rehype/rehype-replace-img-tags.js"; +import rehypeReplaceInternalReferences from "./support/rehype/rehype-replace-internal-references.js"; +import rehypeReplaceStrikethrough from "./support/rehype/rehype-replace-strikethrough.js"; +import rehypeReplaceTaskList from "./support/rehype/rehype-replace-task-list.js"; +import remarkRemoveFootnotes from "./support/remark/remark-remove-footnotes.js"; +import remarkRemoveMdxCodeBlocks from "./support/remark/remark-remove-mdx-code-blocks.js"; +import remarkReplaceMermaid from "./support/remark/remark-replace-mermaid.js"; + +const DEFAULT_NOTICE_MESSAGE = + "AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost"; + +const DEFAULT_MERMAID_DIAGRAMS_LOCATION = "mermaid-diagrams"; + +export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = class ConfluenceTransformer + implements ConfluencePageTransformerInterface +{ + private readonly _noticeMessage?: string; + private readonly _noticeTemplateRaw?: string; + private readonly _noticeTemplate?: TemplateDelegate; + private readonly _rootPageName?: string; + private readonly _spaceKey: string; + private readonly _logger?: LoggerInterface; + + constructor({ + noticeMessage, + noticeTemplate, + rootPageName, + spaceKey, + logger, + }: ConfluencePageTransformerOptions) { + this._noticeMessage = noticeMessage; + this._noticeTemplateRaw = noticeTemplate; + this._noticeTemplate = noticeTemplate + ? Handlebars.compile(noticeTemplate, { noEscape: true }) + : undefined; + this._rootPageName = rootPageName; + this._spaceKey = spaceKey; + this._logger = logger; + } + + public async transform( + _pages: ConfluenceSyncPage[], + ): Promise { + const pages = this._transformPageTitles(_pages); + const pagesMap = new Map(pages.map((page) => [page.path, page])); + return Promise.all( + pages.map((page) => this._transformPage(page, pagesMap)), + ); + } + + private async _transformPageContent( + page: ConfluenceSyncPage, + pages: Map, + ): Promise { + const noticeMessage: string = this._composeNoticeMessage(page); + const mermaidDiagramsDir = resolve( + dirname(page.path), + DEFAULT_MERMAID_DIAGRAMS_LOCATION, + ); + try { + const content = remark() + .use(remarkGfm) + .use(remarkFrontmatter) + .use(remarkRemoveFootnotes) + .use(remarkRemoveMdxCodeBlocks) + .use(remarkReplaceMermaid, { + outDir: mermaidDiagramsDir, + }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeAddNotice, { noticeMessage }) + .use(rehypeReplaceDetails) + .use(rehypeReplaceStrikethrough) + .use(rehypeReplaceTaskList) + .use(rehypeAddAttachmentsImages) + .use(rehypeReplaceImgTags) + .use(rehypeReplaceInternalReferences, { + spaceKey: this._spaceKey, + pages, + removeMissing: true, + }) + .use(rehypeStringify, { + allowDangerousHtml: true, + closeSelfClosing: true, + tightSelfClosing: true, + }) + .processSync(toVFile({ value: page.content, path: page.path })); + if (content.messages.length > 0) + this._logger?.silly( + `Transformed page content: ${JSON.stringify(content.messages, null, 2)}`, + ); + return { + id: page.id, + title: page.title, + content: content.toString(), + attachments: content.data.images as ImagesMetadata, + ancestors: page.ancestors, + }; + } catch (e) { + this._logger?.error( + `Error occurs while transforming page content ${page.path}: ${e}`, + ); + throw e; + } + } + + private _composeNoticeMessage(page: ConfluenceSyncPage): string { + let noticeMessage: string | undefined; + try { + noticeMessage = this._noticeTemplate + ? this._noticeTemplate({ + relativePath: page.relativePath, + relativePathWithoutExtension: page.relativePath + .split(".") + .slice(0, -1) + .join("."), + title: page.title, + message: this._noticeMessage ?? "", + default: DEFAULT_NOTICE_MESSAGE, + }) + : undefined; + } catch (e) { + const error = new InvalidTemplateError( + `Invalid notice template: ${this._noticeTemplateRaw}`, + { cause: e }, + ); + this._logger?.error(`Error occurs while rendering template: ${error}`); + throw error; + } + if (typeof noticeMessage === "string") { + return noticeMessage; + } + return this._noticeMessage ?? DEFAULT_NOTICE_MESSAGE; + } + + private async _transformPage( + page: ConfluenceSyncPage, + pages: Map, + ): Promise { + const confluenceInputPage = await this._transformPageContent(page, pages); + this._logger?.silly( + `Transformed page: ${JSON.stringify(confluenceInputPage, null, 2)}`, + ); + return confluenceInputPage; + } + + private _transformPageTitles( + pages: ConfluenceSyncPage[], + ): ConfluenceSyncPage[] { + const pagesMap = new Map(pages.map((page) => [page.path, page])); + const rootPageAncestor = + this._rootPageName !== undefined ? [this._rootPageName] : []; + const pageTitleLookupTable = new Map( + pages.map((page) => { + const ancestors = this._resolveAncestorsTitles(page, pagesMap); + const ancestorsTitle = rootPageAncestor + .concat(ancestors) + .map((ancestor) => `[${ancestor}]`) + .join(""); + const title = + ancestorsTitle !== "" + ? `${ancestorsTitle} ${page.title}` + : page.title; + return [page.path, title]; + }), + ); + this._logger?.debug( + `pageTitleLookupTable: ${JSON.stringify(Object.fromEntries(pageTitleLookupTable), null, 2)}`, + ); + return pages.map((page) => ({ + ...page, + title: pageTitleLookupTable.get(page.path) as string, + ancestors: page.ancestors.map( + (ancestor) => pageTitleLookupTable.get(ancestor) as string, + ), + })); + } + + private _resolveAncestorsTitles( + page: ConfluenceSyncPage, + pages: Map, + ): string[] { + return page.ancestors.map((ancestor) => { + const ancestorPage = pages.get(ancestor); + // NOTE: Coverage ignored because it is unreachable from tests. Defensive programming. + // istanbul ignore next + if (!ancestorPage) { + throw new Error(`Ancestor page not found: ${ancestor}`); + } + return ancestorPage.name ?? ancestorPage.title; + }); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.types.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.types.ts new file mode 100644 index 00000000..ada181c3 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.types.ts @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; +import type { ConfluenceInputPage } from "@tid-cross/confluence-sync"; + +import type { ConfluenceSyncPage } from "../ConfluenceSync.types.js"; + +export interface ConfluencePageTransformerOptions { + /** Confluence page notice message */ + noticeMessage?: string; + /** Confluence page notice template */ + noticeTemplate?: string; + /** + * Confluence root page short name to be added to children titles + * + * @example + * const confluenceSyncPages = new ConfluenceSyncPages({..., rootPageName: "My Root Page" }); + * confluenceSyncPages.sync([{ title: "My Page" }]); + * // Will create a page with title "[My Root Page] My Page" + */ + rootPageName?: string; + /** Confluence space key */ + spaceKey: string; + /** Logger */ + logger?: LoggerInterface; +} + +/** Creates a ConfluencePageTransformer interface */ +export interface ConfluencePageTransformerConstructor { + /** Returns ConfluencePageTransformer interface + * @returns ConfluencePageTransformer instance {@link ConfluencePageTransformerInterface}. + */ + new ( + options: ConfluencePageTransformerOptions, + ): ConfluencePageTransformerInterface; +} + +export interface ConfluencePageTransformerInterface { + /** Transform pages from Docusaurus to Confluence + * @param pages - Docusaurus pages + * @returns Confluence pages + */ + transform(pages: ConfluenceSyncPage[]): Promise; +} + +export interface ConfluencePageTransformerTemplateData { + /** Confluence page relative path to docs dir */ + relativePath: string; + /** Confluence page relative path to docs dir without file extension */ + relativePathWithoutExtension: string; + /** Confluence page title */ + title: string; + /** Confluence page notice message */ + message: string; + /** Confluence default page notice message */ + default: string; +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidDetailsTagMissingSummaryError.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidDetailsTagMissingSummaryError.ts new file mode 100644 index 00000000..82871736 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidDetailsTagMissingSummaryError.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class InvalidDetailsTagMissingSummaryError extends Error { + constructor() { + super("Invalid details tag. The details tag must have a summary tag."); + } +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidTemplateError.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidTemplateError.ts new file mode 100644 index 00000000..f372bdf3 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidTemplateError.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class InvalidTemplateError extends Error {} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/PageIdRequiredException.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/PageIdRequiredException.ts new file mode 100644 index 00000000..d26313ed --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/PageIdRequiredException.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class PageIdRequiredException extends Error { + constructor() { + super( + "Confluence root page id is required for FLAT synchronization mode when there are pages without an id. Set the confluence.rootPageId option or add an id for all pages.", + ); + } +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.ts new file mode 100644 index 00000000..27de52bd --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +import type { Element as HastElement, Root } from "hast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import { visit } from "unist-util-visit"; + +import type { ImagesMetadata } from "./rehype-add-attachments-images.types.js"; + +function isImage(node: HastElement): boolean { + return ( + node.tagName.toLowerCase() === "img" && + node.properties != null && + "src" in node.properties + ); +} + +/** + * Plugin to add attachments images in frontmatter + */ +const rehypeAddAttachmentsImages: UnifiedPlugin<[], Root> = + function rehypeAddAttachmentsImages() { + return function (tree, file) { + const images: ImagesMetadata = {}; + + visit(tree, "element", function (node) { + if (isImage(node)) { + const base = file.dirname + ? path.resolve(file.cwd, file.dirname) + : file.cwd; + const url = path.resolve(base, node.properties?.src as string); + const baseName = path.basename(url); + images[baseName] = url; + node.properties = { ...node.properties, src: baseName }; + } + }); + file.data.images = images; + }; + }; + +export default rehypeAddAttachmentsImages; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.types.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.types.ts new file mode 100644 index 00000000..11005c15 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.types.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export type ImagesMetadata = Record; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.ts new file mode 100644 index 00000000..ca7cd2af --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Element as HastElement, Root } from "hast"; +import type { Plugin as UnifiedPlugin } from "unified"; + +import type { RehypeAddNoticeOptions } from "./rehype-add-notice.types.js"; + +function composeNotice(message: string): HastElement { + return { + type: "element", + tagName: "p", + children: [ + { + type: "element", + tagName: "strong", + children: [{ type: "raw", value: message }], + }, + ], + }; +} + +/** + * UnifiedPlugin to add a notice to the AST. + */ +const rehypeAddNotice: UnifiedPlugin<[RehypeAddNoticeOptions], Root> = + function rehypeAddNotice(options) { + return function (tree: Root) { + tree.children.unshift(composeNotice(options.noticeMessage)); + }; + }; + +export default rehypeAddNotice; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.types.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.types.ts new file mode 100644 index 00000000..1a798925 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.types.ts @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export interface RehypeAddNoticeOptions { + /** The notice message to add. */ + noticeMessage: string; +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.ts new file mode 100644 index 00000000..908b1d30 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.ts @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Element as HastElement, Node as HastNode, Root } from "hast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import { remove } from "unist-util-remove"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; + +import type { RehypeRemoveLinksOptions } from "./rehype-remove-links.types.js"; + +function isLink(node: HastNode): node is HastElement { + return (node as HastElement).tagName === "a"; +} + +function isExternalLink(node: HastNode): node is HastElement { + return ( + isLink(node) && + (node.properties?.href?.toString().startsWith("http") ?? false) + ); +} + +function isInternalLink(node: HastNode): node is HastElement { + return ( + isLink(node) && (node.properties?.href?.toString().startsWith(".") ?? false) + ); +} + +function isImage(node: HastNode): node is HastElement { + return (node as HastElement).tagName === "img"; +} + +// FIXME: remove this plugin +/** + * UnifiedPlugin to remove links in html + * + * @deprecated Not required anymore + */ +const rehypeRemoveLinks: UnifiedPlugin<[RehypeRemoveLinksOptions], Root> = + function rehypeRemoveLinks(options) { + return function (tree) { + if (options.anchors !== false) { + if (options.anchors === true || options.anchors?.external === true) { + replace(tree, isExternalLink, (node) => { + return { + type: "element" as const, + tagName: "span", + children: node.children, + }; + }); + } + if (options.anchors === true || options.anchors?.internal === true) { + replace(tree, isInternalLink, (node) => { + return { + type: "element" as const, + tagName: "span", + children: node.children, + }; + }); + } + } + if (options.images === true) { + remove(tree, { cascade: true }, isImage); + } + }; + }; + +export default rehypeRemoveLinks; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.types.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.types.ts new file mode 100644 index 00000000..cd4c425c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.types.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +interface AnchorOptions { + /** Remove external */ + external?: boolean; + /** Remove internal */ + internal?: boolean; +} + +export interface RehypeRemoveLinksOptions { + /** Remove anchors */ + anchors?: boolean | AnchorOptions; + /** Remove images */ + images?: boolean; +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-details.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-details.ts new file mode 100644 index 00000000..a6cae2de --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-details.ts @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + Element as HastElement, + Node as HastNode, + Root as HastRoot, + ElementContent, +} from "hast"; +import type { Root } from "mdast"; +import rehypeParse from "rehype-parse"; +import rehypeStringify from "rehype-stringify"; +import { remark } from "remark"; +import remarkRehype from "remark-rehype"; +import type { Plugin as UnifiedPlugin } from "unified"; +import type { VFile } from "vfile"; + +import { replace } from "../../../../../lib/support/unist/unist-util-replace.js"; +import { InvalidDetailsTagMissingSummaryError } from "../../errors/InvalidDetailsTagMissingSummaryError.js"; + +/** + * UnifiedPlugin to replace \ HastElements from tree. + * + * @example + *
+ * Greetings + *

Hi

+ *
+ * // becomes + * + * Greetings + *

Hi

+ *
+ * @throws {InvalidDetailsTagMissingSummaryError} if \ tag does not have a \ tag + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details} + */ +const rehypeReplaceDetails: UnifiedPlugin< + Array, + Root +> = function rehypeReplaceDetails() { + return function (tree) { + // FIXME: typescript error inferring the types of replace function + // Getting Typescript following error when running `check:types:test:unit: + // TS2589: Type instantiation is excessively deep and possibly infinite. + // @ts-expect-error TS2589 + replace(tree, { type: "element", tagName: "details" }, replaceDetailsTag); + }; +}; + +function replaceDetailsTag(node: HastElement): HastElement { + const detailTitle = node.children.find( + (child) => child.type === "element" && child.tagName === "summary", + ) as HastElement | undefined; + if (detailTitle === undefined) { + throw new InvalidDetailsTagMissingSummaryError(); + } + const childrenCopy = [...node.children]; + childrenCopy.splice(childrenCopy.indexOf(detailTitle), 1); + const childrenProcessed = processDetailsTagChildren(childrenCopy); + + return { + type: "element" as const, + tagName: "ac:structured-macro", + properties: { + "ac:name": "expand", + }, + children: [ + { + type: "element" as const, + tagName: "ac:parameter", + properties: { + "ac:name": "title", + }, + children: [...detailTitle.children], + }, + { + type: "element" as const, + tagName: "ac:rich-text-body", + children: childrenProcessed, + }, + ], + }; +} + +/** Parse a string and return a hast HastElement of type body having as children the parsed content + * @param file - The file to parse + * @returns The hast HastElement of type body + * @example + * parseStringToElement("

Hello, world!

Bye, world!") + * // returns + * { + * type: 'element', + * tagName: 'body', + * properties: {}, + * children: [ + * { + * type: 'element', + * tagName: 'h1', + * properties: {}, + * children: [ + * { + * type: 'text', + * value: 'Hello, world!' + * } + * ] + * }, + * { + * type: 'text', + * value: 'Bye, world!' + * } + * ] + * } + */ +function parseStringToElement(file?: string | VFile): HastElement { + return (remark().use(rehypeParse).parse(file).children[0] as HastElement) + .children[1] as HastElement; +} + +/** Convert a hast node to a string + * @param node - The hast node to convert + * @returns The string representation of the node + * @example + * convertHastNodeToString({ type: 'element', tagName: 'h1', properties: {}, children: [{ type: 'text', value: 'Hello, world!'}]}) + * // returns + * '

Hello, world!

' + */ +function convertHastNodeToString(node: HastNode): string { + return remark() + .use(rehypeStringify, { + allowDangerousHtml: true, + closeSelfClosing: true, + tightSelfClosing: true, + }) + .stringify(node as HastRoot); +} + +function processDetailsTagChildren( + children: ElementContent[], +): ElementContent[] { + return children + .map((child) => { + if (child.type === "element" && child.tagName === "details") { + return convertHastNodeToString(replaceDetailsTag(child)); + } + if (child.type === "text") { + return remark() + .use(remarkRehype) + .use(rehypeStringify, { + allowDangerousHtml: true, + closeSelfClosing: true, + tightSelfClosing: true, + }) + .processSync(child.value); + } + return convertHastNodeToString(child); + }) + .map(parseStringToElement) + .map((child) => child.children) + .flat(); +} +export default rehypeReplaceDetails; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-img-tags.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-img-tags.ts new file mode 100644 index 00000000..ba82861f --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-img-tags.ts @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root, Properties } from "hast"; +import type { Plugin as UnifiedPlugin } from "unified"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; + +/** + * UnifiedPlugin to replace `` HastElements with Confluence storage image macro. + * + * @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format } + * + * @example + * + * // becomes + * + * + * + * + * @example + * + * // becomes + * + * + * + */ +const rehypeReplaceImgTags: UnifiedPlugin<[], Root> = + function rehypeReplaceImgTags() { + return function transformer(tree) { + replace(tree, { type: "element", tagName: "img" }, (node) => { + const src = node.properties?.src; + if (typeof src !== "string" || src.toString().length === 0) { + return node; + } + if (src.startsWith("http")) { + return { + type: "element" as const, + tagName: "ac:image", + children: [ + { + type: "element" as const, + tagName: "ri:url", + properties: { + "ri:value": src, + }, + children: [], + }, + ], + }; + } + const properties = obtainSvgProperties(src); + return { + type: "element" as const, + tagName: "ac:image", + properties, + children: [ + { + type: "element" as const, + tagName: "ri:attachment", + properties: { + "ri:filename": src, + }, + children: [], + }, + ], + }; + }); + }; + }; + +/** + * Check if image is svg to adding width property + * @param src Image source + * @returns object with svg properties + */ +function obtainSvgProperties(src: string): Properties { + const mermaidFilePattern = new RegExp("autogenerated.+.svg$", "g"); + return mermaidFilePattern.test(src) + ? { + "ac:width": "1000", + } + : {}; +} + +export default rehypeReplaceImgTags; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.ts new file mode 100644 index 00000000..de28b788 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { dirname, resolve } from "path"; + +import type { Element as HastElement, Node as HastNode, Root } from "hast"; +import { toString as hastToString } from "hast-util-to-string"; +import type { Plugin as UnifiedPlugin } from "unified"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; + +import type { RehypeReplaceInternalReferences } from "./rehype-replace-internal-references.types.js"; + +function checkAnchors(node: HastNode): node is HastElement { + return node.type === "element" && (node as HastElement).tagName === "a"; +} + +function composeChildren(node: HastElement): HastElement[] { + if (node.children.length === 0) { + return []; + } + const firstChild = { + type: "element" as const, + tagName: "span", + children: node.children, + }; + return [ + { + type: "element" as const, + tagName: "ac:plain-text-link-body", + children: [ + { + type: "raw" as const, + value: ``, + }, + ], + }, + ]; +} + +/** + * UnifiedPlugin to replace internal references in Confluence Storage Format + * + * @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format } + */ +const rehypeConfluenceStorage: UnifiedPlugin< + [RehypeReplaceInternalReferences], + Root +> = function rehypeConfluenceStorage({ spaceKey, pages, removeMissing }) { + return function transformer(tree, file) { + replace(tree, checkAnchors, (node: HastElement) => { + if (typeof node.properties?.href !== "string") { + file.message("Internal reference without href", node.position); + return node; + } + // Skip external references + if (!node.properties.href.startsWith(".")) { + return node; + } + const referencedPagePath = resolve( + dirname(file.path), + node.properties.href, + ); + const referencedPage = pages.get(referencedPagePath); + if (referencedPage === undefined) { + file.message("Internal reference to non-existing page", node.position); + return removeMissing === true + ? { + type: "element" as const, + tagName: "span", + children: node.children, + } + : node; + } + const children = composeChildren(node); + return { + type: "element" as const, + tagName: "ac:link", + children: [ + { + type: "element" as const, + tagName: "ri:page", + properties: { + "ri:content-title": referencedPage.title, + "ri:space-key": spaceKey, + }, + children: [], + }, + ...children, + ], + }; + }); + }; +}; + +export default rehypeConfluenceStorage; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.types.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.types.ts new file mode 100644 index 00000000..9504eb19 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.types.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfluenceSyncPage } from "../../../ConfluenceSync.types.js"; +/** + * Options for the RehypeReplaceInternalReferences transformer. + */ +export interface RehypeReplaceInternalReferences { + /** The space key where the page will be created. */ + spaceKey: string; + /** Pages map */ + pages: Map; + /** Remove relative links if missing target */ + removeMissing?: boolean; +} diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-strikethrough.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-strikethrough.ts new file mode 100644 index 00000000..fb20aef5 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-strikethrough.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root } from "hast"; +import type { Plugin as UnifiedPlugin } from "unified"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; + +/** + * UnifiedPlugin to replace `` HastElements with a `` + * tag with the `text-decoration: line-through;` style. + * + * @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format } + * + * @example + * Deleted text + * // becomes + * Deleted text + */ +const rehypeReplaceStrikethrough: UnifiedPlugin<[], Root> = + function rehypeReplaceStrikethrough() { + return function transformer(tree) { + replace(tree, { type: "element", tagName: "del" }, (node) => { + return { + type: "element" as const, + tagName: "span", + properties: { + style: "text-decoration: line-through;", + }, + children: node.children, + }; + }); + }; + }; + +export default rehypeReplaceStrikethrough; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-task-list.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-task-list.ts new file mode 100644 index 00000000..b9e2737a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-task-list.ts @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Element as HastElement, ElementContent, Root } from "hast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import find from "unist-util-find"; +import { remove } from "unist-util-remove"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; + +/** + * UnifiedPlugin to replace task lists in Confluence Storage Format + * + * @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format } + */ +const rehypeReplaceTaskList: UnifiedPlugin<[], Root> = + function rehypeReplaceTaskList() { + return function (tree: Root) { + replace(tree, "element", replaceTaskList); + }; + }; + +function replaceTaskList(node: HastElement) { + if (node.tagName !== "ul") return node; + if (!(node.properties?.className as string[])?.includes("contains-task-list")) + return node; + return { + type: "element" as const, + tagName: "ac:task-list", + children: node.children.map((child) => + child.type !== "element" + ? child + : { + type: "element" as const, + tagName: "ac:task", + children: [ + { + type: "element" as const, + tagName: "ac:task-status", + children: [ + { + type: "text" as const, + value: find(child, { type: "element", tagName: "input" }) + ?.properties.checked + ? "complete" + : "incomplete", + }, + ], + }, + { + type: "element" as const, + tagName: "ac:task-body", + children: processChildren( + remove( + child, + find(child, { type: "element", tagName: "input" }), + ), + ), + }, + ], + }, + ), + }; +} + +function processChildren(node: HastElement | null): ElementContent[] { + if (!node?.children) return []; + const childrenToProcess = node.children as HastElement[]; + return childrenToProcess.map(replaceTaskList); +} +export default rehypeReplaceTaskList; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-footnotes.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-footnotes.ts new file mode 100644 index 00000000..069a3d1c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-footnotes.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root } from "mdast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import { remove } from "unist-util-remove"; + +/** + * UnifiedPlugin to remove footnotes from the AST. + * + * @see {@link https://github.com/syntax-tree/mdast#footnotes | Footnotes} + * @see {@link https://github.com/syntax-tree/mdast#footnotedefinition | GFM Footnote Definition} + * @see {@link https://github.com/syntax-tree/mdast#footnotereference | GFM Footnote Reference} + */ +const remarkRemoveFootnotes: UnifiedPlugin< + Array, + Root +> = function remarkRemoveFootnotes() { + return function (tree) { + remove(tree, "footnoteDefinition"); + remove(tree, "footnoteReference"); + }; +}; + +export default remarkRemoveFootnotes; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.ts new file mode 100644 index 00000000..be061fd9 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root, Code } from "mdast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import type { Node as UnistNode } from "unist"; +import { remove } from "unist-util-remove"; + +function isMdxCodeBlock(node: UnistNode): node is Code { + return ( + (node as Code).type === "code" && + (node as Code).lang?.toLowerCase() === "mdx-code-block" + ); +} + +/** + * UnifiedPlugin to remove mdx-code-block from the AST. + * + * @see {@link https://github.com/syntax-tree/mdast#code | code} + */ +const remarkRemoveMdxCodeBlocks: UnifiedPlugin< + Array, + Root +> = function remarkRemoveMdxCodeBlocks() { + return function (tree) { + remove(tree, isMdxCodeBlock); + }; +}; + +export default remarkRemoveMdxCodeBlocks; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.ts new file mode 100644 index 00000000..979b7079 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.ts @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { rmSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; + +import { ensureDirSync } from "fs-extra"; +import type { Code, Root } from "mdast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import type { Node as UnistNode } from "unist"; +import type { VFile } from "vfile"; +import which from "which"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; +import { DEPENDENCIES_BIN_PATH, PACKAGE_ROOT } from "../../../../util/paths.js"; + +import type { RemarkReplaceMermaidOptions } from "./remark-replace-mermaid.types.js"; + +const AUTOGENERATED_FILE_PREFIX = "autogenerated-"; + +/** + * UnifiedPlugin to remove footnotes from the AST. + */ +const remarkReplaceMermaid: UnifiedPlugin<[RemarkReplaceMermaidOptions], Root> = + function remarkReplaceMermaid(options) { + return function (tree, file) { + const outDir = options.outDir; + replaceMermaidCodeBlocks(tree, file, outDir); + }; + }; + +/** + * FIXME: This function was throwing the error TS2589: Type instantiation is excessively deep and possibly infinite, but it's not throwing anymore. + * Investigate why it was throwing and why it's not throwing anymore. + */ +function replaceMermaidCodeBlocks(tree: Root, file: VFile, outDir: string) { + replace(tree, isMermaidCode, function (node) { + try { + const url: string = render(outDir, node.value); + return { + type: "image" as const, + url, + }; + } catch (e) { + // FIXME: replace with file.fail(e as Error, node, "remark-replace-mermaid"); + // It changes due to lack of time to debug possible issues with mermaid cli. + file.message(e as Error, node, "remark-replace-mermaid"); + return node; + } + }); +} + +function isMermaidCode(node: UnistNode): node is Code { + return node.type === "code" && (node as Code).lang === "mermaid"; +} + +function render(dir: string, code: string): string { + const tempFileName = randomBytes(4).toString("hex"); + const mmdTempFileName = `${tempFileName}.mmd`; + const mmdcTempFile = join(dir, mmdTempFileName); + ensureDirSync(dir); + try { + // HACK: which is a Common JS module, so we to use default import here. + + const mmdcExec = which.sync("mmdc", { + path: DEPENDENCIES_BIN_PATH, + }); + const svgTempFilename = `${AUTOGENERATED_FILE_PREFIX}${tempFileName}.svg`; + const svgTempFile = join(dir, svgTempFilename); + writeFileSync(mmdcTempFile, code, { flag: "w+" }); + const puppeteerConfigFile = resolve( + join(PACKAGE_ROOT, "config"), + "puppeteer-config.json", + ); + const child = spawnSync( + mmdcExec, + [ + "--input", + mmdcTempFile, + "--output", + svgTempFile, + "--backgroundColor", + "transparent", + "--puppeteerConfigFile", + puppeteerConfigFile, + ], + { stdio: [0, "ignore", "pipe"] }, + ); + if (child.status !== 0) { + throw new Error( + `mmdc failed with exit code ${child.status}:\n${child.output}`, + { + cause: child.error, + }, + ); + } + return svgTempFile; + } finally { + rmSync(mmdcTempFile, { force: true }); + } +} + +export default remarkReplaceMermaid; diff --git a/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.types.ts b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.types.ts new file mode 100644 index 00000000..49c9ab54 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.types.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export interface RemarkReplaceMermaidOptions { + outDir: string; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.ts new file mode 100644 index 00000000..3afe3e1a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { relative } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import { glob } from "glob"; + +import type { FilesPattern } from "../MarkdownConfluenceSync.types.js"; +import { isStringWithLength } from "../support/typesValidations.js"; + +import type { + DocusaurusFlatPagesConstructor, + DocusaurusFlatPagesOptions, +} from "./DocusaurusFlatPages.types.js"; +import type { + DocusaurusPage, + DocusaurusPagesInterface, +} from "./DocusaurusPages.types.js"; +import { DocusaurusDocPageFactory } from "./pages/DocusaurusDocPageFactory.js"; + +export const DocusaurusFlatPages: DocusaurusFlatPagesConstructor = class DocusaurusFlatPages + implements DocusaurusPagesInterface +{ + private _path: string; + private _logger: LoggerInterface; + private _initialized = false; + private _filesPattern: FilesPattern; + + constructor({ logger, filesPattern }: DocusaurusFlatPagesOptions) { + this._path = process.cwd(); + this._filesPattern = filesPattern as FilesPattern; + this._logger = logger.namespace("doc-flat"); + } + + public async read(): Promise { + await this._init(); + const filesPaths = await this._obtainedFilesPaths(); + this._logger.debug( + `Found ${filesPaths.length} files in ${this._path} matching the pattern '${this._filesPattern}'`, + ); + return await this._transformFilePathsToDocusaurusPages(filesPaths); + } + + private async _obtainedFilesPaths(): Promise { + return await glob(this._filesPattern, { + cwd: this._path, + absolute: true, + ignore: { + ignored: (p) => !/\.mdx?$/.test(p.name), + }, + }); + } + + private async _transformFilePathsToDocusaurusPages( + filesPaths: string[], + ): Promise { + const files = filesPaths.map((filePath) => + DocusaurusDocPageFactory.fromPath(filePath, { logger: this._logger }), + ); + const pages = files.map((item) => ({ + title: item.meta.confluenceTitle || item.meta.title, + id: item.meta.confluencePageId, + path: item.path, + relativePath: relative(this._path, item.path), + content: item.content, + ancestors: [], + name: item.meta.confluenceShortName, + })); + this._logger.debug(`Found ${pages.length} pages in ${this._path}`); + return pages; + } + + private _init() { + if (!this._initialized) { + if (!isStringWithLength(this._filesPattern as string)) { + throw new Error("File pattern can't be empty in flat mode"); + } + this._filesPattern = this._filesPattern as FilesPattern; + this._initialized = true; + } + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.types.ts new file mode 100644 index 00000000..c4b4b0b3 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.types.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { FilesPattern } from "../MarkdownConfluenceSync.types"; + +import type { + DocusaurusPagesInterface, + DocusaurusPagesModeOptions, +} from "./DocusaurusPages.types"; + +export interface DocusaurusFlatPagesOptions extends DocusaurusPagesModeOptions { + /** Pattern to search files when flat mode is active */ + filesPattern?: FilesPattern; +} + +/** Creates a DocusaurusFlatPagesMode interface */ +export interface DocusaurusFlatPagesConstructor { + /** Returns DocusaurusPagesInterface interface + * @param {DocusaurusFlatPagesOptions} options + * @returns DocusaurusPagesMode instance {@link DocusaurusPagesInterface}. + */ + new (options: DocusaurusFlatPagesOptions): DocusaurusPagesInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.ts new file mode 100644 index 00000000..4cf65fc4 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.ts @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { resolve } from "node:path"; + +import type { ConfigInterface } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { + FilesPattern, + FilesPatternOption, + ModeOption, +} from "../MarkdownConfluenceSync.types.js"; + +import type { + DocsDirOption, + DocsDirOptionDefinition, + DocusaurusPage, + DocusaurusPagesConstructor, + DocusaurusPagesInterface, + DocusaurusPagesOptions, +} from "./DocusaurusPages.types.js"; +import { DocusaurusPagesFactory } from "./DocusaurusPagesFactory.js"; + +const DEFAULT_DOCS_DIR = "docs"; + +const docsDirOption: DocsDirOptionDefinition = { + name: "docsDir", + type: "string", + default: DEFAULT_DOCS_DIR, +}; + +export const DocusaurusPages: DocusaurusPagesConstructor = class DocusaurusPages + implements DocusaurusPagesInterface +{ + private _docsDirOption: DocsDirOption; + private _modeOption: ModeOption; + private _initialized = false; + private _path: string; + private _pages: DocusaurusPagesInterface; + private _logger: LoggerInterface; + private _config: ConfigInterface; + private _filesPattern?: FilesPatternOption; + + constructor({ config, logger, mode, filesPattern }: DocusaurusPagesOptions) { + this._docsDirOption = config.addOption(docsDirOption); + this._modeOption = mode; + this._filesPattern = filesPattern; + this._config = config; + this._logger = logger; + } + + public async read(): Promise { + await this._init(); + this._logger.debug(`docsDir option is ${this._docsDirOption.value}`); + return await this._pages.read(); + } + + private _init() { + this._logger.debug(`mode option is ${this._modeOption.value}`); + if (!this._initialized) { + const path = resolve(process.cwd(), this._docsDirOption.value); + this._path = path; + this._pages = DocusaurusPagesFactory.fromMode(this._modeOption.value, { + config: this._config, + logger: this._logger, + path: this._path, + filesPattern: this._filesPattern?.value as FilesPattern, + }); + this._initialized = true; + } + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.types.ts new file mode 100644 index 00000000..07effe08 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.types.ts @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + OptionInterfaceOfType, + OptionDefinition, + ConfigInterface, +} from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { + FilesPatternOption, + ModeOption, +} from "../MarkdownConfluenceSync.types.js"; + +export type DocusaurusPageId = string; + +type DocsDirOptionValue = string; + +declare global { + //eslint-disable-next-line @typescript-eslint/no-namespace + namespace MarkdownConfluenceSync { + interface Config { + /** Documents directory */ + docsDir?: DocsDirOptionValue; + } + } +} + +export type DocsDirOptionDefinition = OptionDefinition< + DocsDirOptionValue, + { hasDefault: true } +>; + +export type DocsDirOption = OptionInterfaceOfType< + DocsDirOptionValue, + { hasDefault: true } +>; + +export interface DocusaurusPagesOptions { + /** Configuration interface */ + config: ConfigInterface; + /** Logger */ + logger: LoggerInterface; + /** Sync mode option */ + mode: ModeOption; + /** Pattern to search files when flat mode is active */ + filesPattern?: FilesPatternOption; +} + +/** Data about one Docusaurus page */ +export interface DocusaurusPage { + /** Docusaurus page title */ + title: string; + /** Docusaurus page path */ + path: string; + /** Docusaurus page path relative to docs root dir */ + relativePath: string; + /** Docusaurus page content */ + content: string; + /** Docusaurus page ancestors */ + ancestors: string[]; + /** + * Docusaurus page name + * + * Replaces title page in children's title. + */ + name?: string; +} + +/** Creates a DocusaurusToConfluence interface */ +export interface DocusaurusPagesConstructor { + /** Returns DocusaurusPagesInterface interface + * @returns DocusaurusPages instance {@link DocusaurusPagesInterface}. + */ + new (options: DocusaurusPagesOptions): DocusaurusPagesInterface; +} + +export interface DocusaurusPagesInterface { + /** Read Docusaurus pages and return a list of Docusaurus page objects */ + read(): Promise; +} + +export interface DocusaurusPagesModeOptions { + /** Configuration interface */ + config: ConfigInterface; + /** Logger */ + logger: LoggerInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.ts new file mode 100644 index 00000000..93f1e825 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { SyncModes } from "@tid-cross/confluence-sync"; + +import { DocusaurusFlatPages } from "./DocusaurusFlatPages.js"; +import type { DocusaurusFlatPagesOptions } from "./DocusaurusFlatPages.types.js"; +import type { DocusaurusPagesInterface } from "./DocusaurusPages.types.js"; +import type { + DocusaurusPagesFactoryInterface, + DocusaurusPagesFactoryOptions, +} from "./DocusaurusPagesFactory.types.js"; +import { DocusaurusTreePages } from "./DocusaurusTreePages.js"; +import type { DocusaurusTreePagesOptions } from "./DocusaurusTreePages.types.js"; + +export const DocusaurusPagesFactory: DocusaurusPagesFactoryInterface = class DocusaurusPagesFactory { + public static fromMode( + mode: SyncModes, + options: DocusaurusPagesFactoryOptions, + ): DocusaurusPagesInterface { + if (!this._isValidMode(mode)) { + throw new Error(`"mode" option must be one of "tree" or "flat"`); + } + if (this._isFlatMode(mode)) { + return new DocusaurusFlatPages(options as DocusaurusFlatPagesOptions); + } + return new DocusaurusTreePages(options as DocusaurusTreePagesOptions); + } + + private static _isFlatMode(mode: string): boolean { + return mode === SyncModes.FLAT; + } + + private static _isValidMode(mode: string): boolean { + return mode === SyncModes.FLAT || mode === SyncModes.TREE; + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.types.ts new file mode 100644 index 00000000..d82459bb --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.types.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfigInterface } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import type { SyncModes } from "@tid-cross/confluence-sync"; + +import type { FilesPattern } from ".."; + +import type { DocusaurusPagesInterface } from "./DocusaurusPages.types"; + +export interface DocusaurusPagesFactoryOptions { + /** Configuration interface */ + config: ConfigInterface; + /** Logger */ + logger: LoggerInterface; + /** Docusaurus page path */ + path: string; + /** Pattern to search files when flat mode is active */ + filesPattern?: FilesPattern; +} + +/** + * Factory for creating DocusaurusPages instances. + * + * + * @export DocusaurusDocFactory + */ +export interface DocusaurusPagesFactoryInterface { + /** + * Creates a new page from the category index. + * + * If the mode is flat {@link DocusaurusFlatPages} will be obtained pages in flat mode. + * Otherwise, the {@link DocusaurusTreePages} will be obtained pages in tree mode. + * + * @param options ${DocusaurusPagesFactoryOptions} - The options to obtained docusaurus pages. + * + * @returns A new DocusaurusPagesInterface instance. + */ + fromMode( + mode: SyncModes, + options: DocusaurusPagesFactoryOptions, + ): DocusaurusPagesInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.ts new file mode 100644 index 00000000..eaa94b89 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.ts @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { join, basename, dirname, relative, sep } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { + DocusaurusPage, + DocusaurusPagesInterface, +} from "./DocusaurusPages.types.js"; +import type { + DocusaurusTreePagesConstructor, + DocusaurusTreePagesOptions, +} from "./DocusaurusTreePages.types.js"; +import { DocusaurusDocTree } from "./tree/DocusaurusDocTree.js"; +import type { DocusaurusDocTreeInterface } from "./tree/DocusaurusDocTree.types.js"; +import { buildIndexFileRegExp, getIndexFileFromPaths } from "./util/files.js"; + +export const DocusaurusTreePages: DocusaurusTreePagesConstructor = class DocusaurusTreePages + implements DocusaurusPagesInterface +{ + private _path: string; + private _tree: DocusaurusDocTreeInterface; + private _logger: LoggerInterface; + + constructor({ logger, path }: DocusaurusTreePagesOptions) { + this._path = path as string; + this._logger = logger; + this._tree = new DocusaurusDocTree(this._path, { + logger: this._logger.namespace("doc-tree"), + }); + } + + public async read(): Promise { + const items = await this._tree.flatten(); + const pages = items.map((item) => ({ + title: item.meta.confluenceTitle || item.meta.title, + path: item.path, + relativePath: relative(this._path, item.path), + content: item.content, + ancestors: [], + name: item.meta.confluenceShortName, + })); + this._logger.debug(`Found ${pages.length} pages in ${this._path}`); + const pagePaths = pages.map(({ path }) => path); + return pages.map((page) => ({ + ...page, + ancestors: this._getItemAncestors(page, pagePaths), + })); + } + + private _getItemAncestors( + page: DocusaurusPage, + paths: string[], + ): DocusaurusPage["ancestors"] { + // HACK: Added filter to removed empty string because windows separator + // add double slash and this cause empty string in the end of array + const dirnamePath = basename(dirname(page.path)); + const idSegments = relative(this._path, page.path) + .replace(buildIndexFileRegExp(sep, dirnamePath), "") + .split(sep) + .filter((value) => value !== ""); + if (idSegments.length === 1) return []; + return idSegments + .slice(0, -1) + .map((_idSegment, index) => + getIndexFileFromPaths( + join(this._path, ...idSegments.slice(0, index + 1)), + paths, + ), + ); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.types.ts new file mode 100644 index 00000000..1a396d56 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.types.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + DocusaurusPagesInterface, + DocusaurusPagesModeOptions, +} from "./DocusaurusPages.types"; + +export interface DocusaurusTreePagesOptions extends DocusaurusPagesModeOptions { + /** Docusaurus page path */ + path?: string; +} + +/** Creates a DocusaurusTreePagesMode interface */ +export interface DocusaurusTreePagesConstructor { + /** Returns DocusaurusPagesInterface interface + * @param {DocusaurusTreePagesOptions} options + * @returns DocusaurusPagesMode instance {@link DocusaurusPagesInterface}. + */ + new (options: DocusaurusTreePagesOptions): DocusaurusPagesInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.ts new file mode 100644 index 00000000..dbcc1e4d --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.ts @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { existsSync, lstatSync } from "node:fs"; +import { join } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import { remark } from "remark"; +import remarkDirective from "remark-directive"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkGfm from "remark-gfm"; +import remarkParseFrontmatter from "remark-parse-frontmatter"; +import type { VFile } from "vfile"; + +import { + isSupportedFile, + readMarkdownAndPatchDocusaurusAdmonitions, +} from "../util/files.js"; + +import type { + DocusaurusDocPageConstructor, + DocusaurusDocPageInterface, + DocusaurusDocPageMeta, + DocusaurusDocPageOptions, +} from "./DocusaurusDocPage.types.js"; +import { InvalidMarkdownFormatException } from "./errors/InvalidMarkdownFormatException.js"; +import { InvalidPathException } from "./errors/InvalidPathException.js"; +import { PathNotExistException } from "./errors/PathNotExistException.js"; +import remarkReplaceAdmonitions from "./support/remark/remark-replace-admonitions.js"; +import remarkValidateFrontmatter from "./support/remark/remark-validate-frontmatter.js"; +import type { FrontMatter } from "./support/validators/FrontMatterValidator.js"; +import { FrontMatterValidator } from "./support/validators/FrontMatterValidator.js"; + +export const DocusaurusDocPage: DocusaurusDocPageConstructor = class DocusaurusDocPage + implements DocusaurusDocPageInterface +{ + protected _vFile: VFile; + + constructor(path: string, options?: DocusaurusDocPageOptions) { + if (!existsSync(join(path))) { + throw new PathNotExistException(`Path ${path} does not exist`); + } + if (!lstatSync(path).isFile()) { + throw new InvalidPathException(`Path ${path} is not a file`); + } + if (!isSupportedFile(path)) { + throw new InvalidPathException(`Path ${path} is not a markdown file`); + } + try { + this._vFile = this._parseFile(path, options); + } catch (e) { + throw new InvalidMarkdownFormatException( + `Invalid markdown format: ${path}`, + { cause: e }, + ); + } + } + + public get isCategory(): boolean { + return false; + } + + public get path(): string { + return this._vFile.path; + } + + public get meta(): DocusaurusDocPageMeta { + const frontmatter = this._vFile.data.frontmatter as FrontMatter; + return { + title: frontmatter.title, + syncToConfluence: frontmatter.sync_to_confluence, + confluenceShortName: frontmatter.confluence_short_name, + confluenceTitle: frontmatter.confluence_title, + confluencePageId: frontmatter.confluence_page_id, + }; + } + + public get content(): string { + return this._vFile.toString(); + } + + protected _parseFile( + path: string, + options?: { logger?: LoggerInterface }, + ): VFile { + return remark() + .use(remarkGfm) + .use(remarkFrontmatter) + .use(remarkDirective) + .use(remarkParseFrontmatter) + .use(remarkValidateFrontmatter, FrontMatterValidator) + .use(remarkReplaceAdmonitions) + .processSync( + readMarkdownAndPatchDocusaurusAdmonitions(path, { + logger: options?.logger, + }), + ); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.types.ts new file mode 100644 index 00000000..4a801015 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.types.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +/** Docusaurus file metadata */ +export interface DocusaurusDocPageMeta { + /** Returns file title */ + readonly title: string; + /** Returns true if the file needs to be synch with Confluence */ + readonly syncToConfluence: boolean; + /** + * Returns Confluence page name + * + * Replace page title in children's titles. + */ + readonly confluenceShortName?: string; + /** + * Returns Confluence page title + * Replace page title in Confluence. + */ + readonly confluenceTitle?: string; + /** + * If the flat mode is active you can return the confluence page id to use it as root page. + * + */ + readonly confluencePageId?: string; +} + +export interface DocusaurusDocPageInterface { + /** Returns true if the file is a category, false otherwise */ + isCategory: boolean; + /** Returns path to the file represented by the file */ + path: string; + /** + * Returns the file meta information + * @see {@link DocusaurusDocPageMeta} + */ + meta: DocusaurusDocPageMeta; + /** Returns the file content in HTML format*/ + content: string; +} + +export interface DocusaurusDocPageOptions { + /** Logger */ + logger?: LoggerInterface; +} + +/** Creates DocusaurusDocPage interface */ +export interface DocusaurusDocPageConstructor { + /** Returns DocusaurusDocPage interface + * + * @param {string} path - Path to the page + * @returns {DocusaurusDocPage} instance {@link DocusaurusDocPageInterface}. + * @throws {Error} If the path does not exist. + * @throws {Error} If the path is not a markdown file. + */ + new ( + path: string, + options?: DocusaurusDocPageOptions, + ): DocusaurusDocPageInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.ts new file mode 100644 index 00000000..771d9a34 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { DocusaurusDocPage } from "./DocusaurusDocPage.js"; +import type { DocusaurusDocPageInterface } from "./DocusaurusDocPage.types.js"; +import type { + DocusaurusDocPageFactoryFromPathOptions, + DocusaurusDocPageFactoryInterface, +} from "./DocusaurusDocPageFactory.types.js"; +import { DocusaurusDocPageMdx } from "./DocusaurusDocPageMdx.js"; + +export const DocusaurusDocPageFactory: DocusaurusDocPageFactoryInterface = class DocusaurusDocPageFactory { + public static fromPath( + path: string, + options?: DocusaurusDocPageFactoryFromPathOptions, + ): DocusaurusDocPageInterface { + if (path.endsWith(".mdx")) { + return new DocusaurusDocPageMdx(path, options); + } + return new DocusaurusDocPage(path, options); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.types.ts new file mode 100644 index 00000000..25939a7e --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.types.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { DocusaurusDocPageInterface } from "./DocusaurusDocPage.types"; + +export interface DocusaurusDocPageFactoryFromPathOptions { + /** Logger */ + logger?: LoggerInterface; +} + +/** + * Factory for creating DocusaurusDocPage instances. + * + * @export DocusaurusDocPageFactory + */ +export interface DocusaurusDocPageFactoryInterface { + /** + * Creates a new DocusaurusDocPage instance from the given path. + * + * If the path is an mdx file, the {@link DocusaurusDocPageMdx} will be parsed with mdx instructions. + * Otherwise, the {@link DocusaurusDocPage} will be the parser with md instructions. + * + * @param path - The path to create the DocusaurusDocPage from. + * + * @returns A new DocusaurusDocPage instance. + */ + fromPath( + path: string, + options?: DocusaurusDocPageFactoryFromPathOptions, + ): DocusaurusDocPageInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageMdx.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageMdx.ts new file mode 100644 index 00000000..d1eaf3a2 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageMdx.ts @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; +import { remark } from "remark"; +import remarkDirective from "remark-directive"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkGfm from "remark-gfm"; +import remarkMdx from "remark-mdx"; +import remarkParseFrontmatter from "remark-parse-frontmatter"; +import type { VFile } from "vfile"; + +import { readMarkdownAndPatchDocusaurusAdmonitions } from "../util/files.js"; + +import { DocusaurusDocPage } from "./DocusaurusDocPage.js"; +import type { + DocusaurusDocPageConstructor, + DocusaurusDocPageOptions, +} from "./DocusaurusDocPage.types.js"; +import remarkRemoveMdxCode from "./support/remark/remark-remove-mdx-code.js"; +import remarkReplaceAdmonitions from "./support/remark/remark-replace-admonitions.js"; +import remarkReplaceTabs from "./support/remark/remark-replace-tabs.js"; +import remarkTransformDetails from "./support/remark/remark-transform-details.js"; +import remarkValidateFrontmatter from "./support/remark/remark-validate-frontmatter.js"; +import { FrontMatterValidator } from "./support/validators/FrontMatterValidator.js"; + +export const DocusaurusDocPageMdx: DocusaurusDocPageConstructor = class DocusaurusDocPageMdx extends DocusaurusDocPage { + constructor(path: string, options?: DocusaurusDocPageOptions) { + super(path, options); + } + + protected _parseFile( + path: string, + options?: { logger?: LoggerInterface }, + ): VFile { + return remark() + .use(remarkMdx) + .use(remarkGfm) + .use(remarkFrontmatter) + .use(remarkReplaceTabs) + .use(remarkTransformDetails) + .use(remarkRemoveMdxCode) + .use(remarkDirective) + .use(remarkParseFrontmatter) + .use(remarkValidateFrontmatter, FrontMatterValidator) + .use(remarkReplaceAdmonitions) + .processSync( + readMarkdownAndPatchDocusaurusAdmonitions(path, { + logger: options?.logger, + }), + ); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidMarkdownFormatException.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidMarkdownFormatException.ts new file mode 100644 index 00000000..8e0726f4 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidMarkdownFormatException.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class InvalidMarkdownFormatException extends Error { + constructor(path: string, options?: ErrorOptions) { + super(`Invalid markdown format: ${path}`, options); + } +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidPathException.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidPathException.ts new file mode 100644 index 00000000..9e80b5cc --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidPathException.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class InvalidPathException extends Error { + constructor(path: string, options?: ErrorOptions) { + super(`Invalid file: ${path}`, options); + } +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabItemMissingLabelError.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabItemMissingLabelError.ts new file mode 100644 index 00000000..22269b8b --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabItemMissingLabelError.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class InvalidTabItemMissingLabelError extends Error { + constructor() { + super("Invalid TabItem tag. The TabItem tag must have a label property."); + } +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabsFormatError.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabsFormatError.ts new file mode 100644 index 00000000..b17ec87a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabsFormatError.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class InvalidTabsFormatError extends Error { + constructor() { + super("Invalid Tabs tag. The Tabs tag must have only TabItem children."); + } +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/PathNotExistException.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/PathNotExistException.ts new file mode 100644 index 00000000..aca3a6b5 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/PathNotExistException.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class PathNotExistException extends Error { + constructor(path: string, options?: ErrorOptions) { + super(`Path not exist: ${path}`, options); + } +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-remove-mdx-code.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-remove-mdx-code.ts new file mode 100644 index 00000000..65f8c956 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-remove-mdx-code.ts @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root } from "mdast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import { remove } from "unist-util-remove"; + +const removedMdxTypes = [ + "mdxJsxFlowElement", + "mdxTextExpression", + "mdxJsxTextElement", + "mdxJsxAttribute", + "mdxJsxExpressionAttribute", + "mdxJsxAttributeValueExpression", + "mdxJsEsm", + "mdxFlowExpression", +]; + +/** + * UnifiedPlugin to remove mdx code from the AST. + * + * @see {@link https://github.com/syntax-tree/mdast-util-mdx-expression#syntax-tree} + * @see {@link https://github.com/syntax-tree/mdast-util-mdx-jsx#syntax-tree} + * @see {@link https://github.com/syntax-tree/mdast-util-mdxjs-esm#syntax-tree} + */ +const remarkRemoveMdxCode: UnifiedPlugin< + Array, + Root +> = function remarkRemoveMdxCode() { + return function (tree) { + remove(tree, removedMdxTypes); + }; +}; + +export default remarkRemoveMdxCode; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-admonitions.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-admonitions.ts new file mode 100644 index 00000000..e203ab75 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-admonitions.ts @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Blockquote, Content, Paragraph, Root } from "mdast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import find from "unist-util-find"; + +import { replace } from "../../../../../lib/support/unist/unist-util-replace.js"; + +// FIXME: eslint false positive +// https://github.com/typescript-eslint/typescript-eslint/issues/325 + +enum DocusaurusAdmonitionType { + Note = "note", + Tip = "tip", + Info = "info", + Caution = "caution", + Danger = "danger", +} + +const admonitionMessage: Record = { + [DocusaurusAdmonitionType.Note]: "Note", + [DocusaurusAdmonitionType.Tip]: "Tip", + [DocusaurusAdmonitionType.Info]: "Info", + [DocusaurusAdmonitionType.Caution]: "Caution", + [DocusaurusAdmonitionType.Danger]: "Danger", +}; + +function composeTitle(type: string, title: Paragraph | undefined): Paragraph { + const typeNode = { + type: "text" as const, + value: `${admonitionMessage[type as DocusaurusAdmonitionType]}:`, + }; + const children = title?.children ?? []; + return { + type: "paragraph" as const, + children: [ + { + type: "strong" as const, + children: + children.length === 0 + ? [typeNode] + : [typeNode, { type: "text", value: " " }, ...children], + }, + ], + }; +} + +/** + * UnifiedPlugin to replace Docusaurus' Admonitions with block-quotes from tree. + * + * @throws {Error} if the admonitions is not well constructed. + * + * @see {@link https://docusaurus.io/docs/markdown-features/admonitions | Docusaurus Admonitions} + */ +const remarkRemoveAdmonitions: UnifiedPlugin< + Array, + Root +> = function remarkRemoveAdmonitions() { + return function (tree) { + replace(tree, "containerDirective", (node): Blockquote => { + const admonitionTitle = find( + node, + (child: Content) => + child.type === "paragraph" && child.data?.directiveLabel, + ) as Paragraph | undefined; + if (admonitionTitle !== undefined) { + node.children.splice(node.children.indexOf(admonitionTitle), 1); + } + return { + type: "blockquote" as const, + children: [composeTitle(node.name, admonitionTitle), ...node.children], + }; + }); + }; +}; + +export default remarkRemoveAdmonitions; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-tabs.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-tabs.ts new file mode 100644 index 00000000..fdf5ef90 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-tabs.ts @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Content, Root } from "mdast"; +import type { + MdxJsxAttribute, + MdxJsxExpressionAttribute, + MdxJsxFlowElement, +} from "mdast-util-mdx-jsx"; +import type { Plugin as UnifiedPlugin } from "unified"; + +import { replace } from "../../../../../lib/support/unist/unist-util-replace.js"; +import { InvalidTabItemMissingLabelError } from "../../errors/InvalidTabItemMissingLabelError.js"; +import { InvalidTabsFormatError } from "../../errors/InvalidTabsFormatError.js"; + +const mdxElement = "mdxJsxFlowElement"; +/** + * UnifiedPlugin to replace \ elements from tree. + * + * @throws {InvalidTabsFormatError} if \ tag does not have only TabItem children. + * @throws {InvalidTabItemMissingLabelError} if \ tag does not have a label property. + * @see {@link https://docusaurus.io/docs/markdown-features/tabs | Docusaurus Details} + */ +const remarkReplaceTabs: UnifiedPlugin< + Array, + Root +> = function remarkReplaceTabs() { + return function (tree) { + replace(tree, mdxElement, replaceTabsTag); + }; +}; + +function replaceTabsTag(node: MdxJsxFlowElement) { + if (node.name !== "Tabs") { + return node; + } + const tabsSections = [...node.children] as MdxJsxFlowElement[]; + if ( + !tabsSections.every( + (child) => child.type === mdxElement && child.name === "TabItem", + ) + ) { + throw new InvalidTabsFormatError(); + } + if ( + !tabsSections.every((tabItem) => + tabItem.attributes.find(checkLabelAttribute), + ) + ) { + throw new InvalidTabItemMissingLabelError(); + } + return { + type: "list", + ordered: false, + children: tabsSections.map((tabItem) => processTabItem(tabItem)).flat(), + }; +} + +function processTabItem(tabItem: MdxJsxFlowElement) { + return { + type: "listItem", + children: [ + { + type: "text", + value: `${tabItem.attributes.find(checkLabelAttribute)?.value}`, + }, + ...processChildren(tabItem.children as Content[]), + ], + }; +} + +const checkLabelAttribute = ( + attr: MdxJsxAttribute | MdxJsxExpressionAttribute, +) => attr.type === "mdxJsxAttribute" && attr.name === "label"; + +function processChildren(children: Content[]): Content[] { + return children.map((child) => + child.type === mdxElement + ? (replaceTabsTag(child as MdxJsxFlowElement) as Content) + : child, + ); +} + +export default remarkReplaceTabs; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-transform-details.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-transform-details.ts new file mode 100644 index 00000000..4e28fd79 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-transform-details.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root } from "mdast"; +import { mdxToMarkdown } from "mdast-util-mdx"; +import type { MdxJsxFlowElement } from "mdast-util-mdx-jsx"; +import { toMarkdown } from "mdast-util-to-markdown"; +import type { Plugin as UnifiedPlugin } from "unified"; + +import { replace } from "../../../../support/unist/unist-util-replace.js"; + +const mdxElement = "mdxJsxFlowElement"; +/** + * UnifiedPlugin to prevent \ elements from being removed from the tree. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details} + */ +const remarkTransformDetails: UnifiedPlugin< + Array, + Root +> = function remarkTransformDetails() { + return function (tree) { + replace(tree, mdxElement, transformDetailsTag); + }; +}; + +function transformDetailsTag(node: MdxJsxFlowElement) { + if (node.name !== "details") { + return node; + } + return { + type: "html", + value: toMarkdown(node, { + extensions: [mdxToMarkdown()], + }), + }; +} + +export default remarkTransformDetails; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-validate-frontmatter.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-validate-frontmatter.ts new file mode 100644 index 00000000..b0b1e3a6 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-validate-frontmatter.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Root } from "mdast"; +import type { Plugin as UnifiedPlugin } from "unified"; +import type { Schema } from "zod"; +import { ZodError } from "zod"; + +/** + * UnifiedPlugin to validate FrontMatter metadata with given schema. + * + * @throws {Error} if the admonitions is not well constructed. + * + * @see {@link https://docusaurus.io/docs/markdown-features/admonitions | Docusaurus Admonitions} + */ +const remarkValidateFrontmatter: UnifiedPlugin< + Array, + Root +> = function remarkRemoveAdmonitions(schema) { + return function (_tree, file) { + try { + file.data.frontmatter = schema.parse(file.data.frontmatter); + } catch (e) { + if (e instanceof ZodError) { + const message = e.errors.map((error) => error.message).join("\n"); + file.fail(message, undefined, "remark-validate-frontmatter"); + } else { + throw e; + } + } + }; +}; + +export default remarkValidateFrontmatter; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/validators/FrontMatterValidator.ts b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/validators/FrontMatterValidator.ts new file mode 100644 index 00000000..0a5d6b71 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/validators/FrontMatterValidator.ts @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import z from "zod"; + +/** + * Validator for FrontMatter. + * + * @see {@link https://docusaurus.io/docs/create-doc#doc-front-matter | Doc front matter} + */ +export const FrontMatterValidator = z.object({ + title: z.string().nonempty(), + sync_to_confluence: z.boolean().optional().default(false), + confluence_short_name: z.string().nonempty().optional(), + confluence_title: z.string().nonempty().optional(), + confluence_page_id: z.string().optional(), +}); + +export type FrontMatter = z.infer; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.ts new file mode 100644 index 00000000..e6b67c4a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { lstatSync } from "fs"; + +import type { + DocusaurusDocItemFactoryFromPathOptions, + DocusaurusDocItemFactoryInterface, +} from "./DocusaurusDocItemFactory.types.js"; +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; +import { DocusaurusDocTreeCategory } from "./DocusaurusDocTreeCategory.js"; +import { DocusaurusDocTreePageFactory } from "./DocusaurusDocTreePageFactory.js"; + +export const DocusaurusDocItemFactory: DocusaurusDocItemFactoryInterface = class DocusaurusDocItemFactory { + public static fromPath( + path: string, + options?: DocusaurusDocItemFactoryFromPathOptions, + ): DocusaurusDocTreeItem { + if (lstatSync(path).isDirectory()) { + return new DocusaurusDocTreeCategory(path, options); + } + return DocusaurusDocTreePageFactory.fromPath( + path, + options, + ) as DocusaurusDocTreeItem; + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.types.ts new file mode 100644 index 00000000..cf3276a0 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.types.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; + +export interface DocusaurusDocItemFactoryFromPathOptions { + /** Logger */ + logger?: LoggerInterface; +} + +/** + * Factory for creating DocusaurusDocTreeItem instances. + * + * @export DocusaurusDocItemFactory + */ +export interface DocusaurusDocItemFactoryInterface { + /** + * Creates a new DocusaurusDocTreeItem instance from the given path. + * + * If the path is a file, the {@link DocusaurusDocTreeCategory} will be a leaf node. + * Otherwise, the {@link DocusaurusDocTreePage} will be a parent node. + * + * @param path - The path to create the DocusaurusDocTreeItem from. + * + * @returns A new DocusaurusDocTreeItem instance. + */ + fromPath( + path: string, + options?: DocusaurusDocItemFactoryFromPathOptions, + ): DocusaurusDocTreeItem; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.ts new file mode 100644 index 00000000..4e50bfae --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.ts @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { existsSync, lstatSync } from "fs"; +import { readdir } from "fs/promises"; +import { join } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; + +import { DocusaurusDocItemFactory } from "./DocusaurusDocItemFactory.js"; +import type { + DocusaurusDocTreeConstructor, + DocusaurusDocTreeInterface, + DocusaurusDocTreeItem, + DocusaurusDocTreeOptions, +} from "./DocusaurusDocTree.types.js"; + +export const DocusaurusDocTree: DocusaurusDocTreeConstructor = class DocusaurusDocTree + implements DocusaurusDocTreeInterface +{ + private _path: string; + private _logger?: LoggerInterface; + + constructor(path: string, options?: DocusaurusDocTreeOptions) { + if (!existsSync(path)) { + throw new Error(`Path ${path} does not exist`); + } + this._path = path; + this._logger = options?.logger; + } + + public async flatten(): Promise { + const rootPaths = await readdir(this._path); + if (rootPaths.some((path) => path.endsWith("index.md"))) { + this._logger?.warn("Ignoring index.md file in root directory."); + } + const rootDirs = rootPaths + .map((path) => join(this._path, path)) + .filter( + (path) => + lstatSync(path).isDirectory() || + (path.endsWith(".md") && !path.endsWith("index.md")), + ); + const roots = rootDirs.map((path) => + DocusaurusDocItemFactory.fromPath(path, { + logger: this._logger?.namespace(path.replace(this._path, "")), + }), + ); + const rootsPages = await Promise.all(roots.map((root) => root.visit())); + return rootsPages.flat(); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.types.ts new file mode 100644 index 00000000..024911bf --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.types.ts @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { + DocusaurusDocPageInterface, + DocusaurusDocPageMeta, +} from "../pages/DocusaurusDocPage.types"; + +export interface DocusaurusDocTreeOptions { + /** Logger */ + logger?: LoggerInterface; +} + +/** Creates a DocusaurusDocTree interface */ +export interface DocusaurusDocTreeConstructor { + /** Returns DocusaurusDocTree interface + * @param {string} path - Path to the docs directory + * @param {DocusaurusDocTreeOptions} [options] - Options + * @returns DocusaurusDocTree instance + * @example const docusaurusDocTree = new DocusaurusDocTree("./docs"); + */ + new ( + path: string, + options?: DocusaurusDocTreeOptions, + ): DocusaurusDocTreeInterface; +} + +export interface DocusaurusDocTreeInterface { + /** Returns an array of all Docusaurus tree nodes in prefix order to be synchronize + * @async + * @returns {Promise} An array of all Docusaurus tree nodes in prefix order to be synchronize + * @example + * const docusaurusDocTree = new DocusaurusDocTree("./docs"); + * const tree = await docusaurusDocTree.flatten(); + */ + flatten(): Promise; +} + +/** Docusaurus Tree Item */ +export interface DocusaurusDocTreeItem extends DocusaurusDocPageInterface { + /** + * Returns items children. + * + * In case of a category, it returns the category children. + * Otherwise, it returns an array containing itself. + * + * @async + * @returns {Promise} An array of DocusaurusDocTreeItem + * @see {@link DocusaurusDocTreeCategory#visit} + * @see {@link DocusaurusDocTreePage#visit} + */ + visit(): Promise; +} + +/** Docusaurus Tree Item metadata */ +export type DocusaurusDocTreeItemMeta = DocusaurusDocPageMeta; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.ts new file mode 100644 index 00000000..6faa85ef --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.ts @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { existsSync, lstatSync, readFileSync } from "node:fs"; +import { readdir } from "node:fs/promises"; +import { basename, join } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import { parse as parseYaml } from "yaml"; + +import { InvalidPathException } from "../pages/errors/InvalidPathException.js"; +import { PathNotExistException } from "../pages/errors/PathNotExistException.js"; +import { isValidFile } from "../util/files.js"; + +import { DocusaurusDocItemFactory } from "./DocusaurusDocItemFactory.js"; +import type { + DocusaurusDocTreeItem, + DocusaurusDocTreeItemMeta, +} from "./DocusaurusDocTree.types.js"; +import type { + DocusaurusDocTreeCategoryConstructor, + DocusaurusDocTreeCategoryInterface, + DocusaurusDocTreeCategoryMeta, + DocusaurusDocTreeCategoryOptions, +} from "./DocusaurusDocTreeCategory.types.js"; +import type { DocusaurusDocTreePageInterface } from "./DocusaurusDocTreePage.types.js"; +import { DocusaurusDocTreePageFactory } from "./DocusaurusDocTreePageFactory.js"; +import { CategoryItemMetadataValidator } from "./support/validators/CategoryItemMetadata.js"; + +export const DocusaurusDocTreeCategory: DocusaurusDocTreeCategoryConstructor = class DocusaurusDocTreeCategory + implements DocusaurusDocTreeCategoryInterface +{ + private _path: string; + private _index: DocusaurusDocTreePageInterface | undefined; + private _meta: DocusaurusDocTreeCategoryMeta | undefined; + private _logger: LoggerInterface | undefined; + + constructor(path: string, options?: DocusaurusDocTreeCategoryOptions) { + if (!existsSync(path)) { + throw new PathNotExistException(`Path ${path} does not exist`); + } + if (!lstatSync(path).isDirectory()) { + throw new InvalidPathException(`Path ${path} is not a directory`); + } + try { + this._index = DocusaurusDocTreePageFactory.fromCategoryIndex( + path, + options, + ); + } catch (e) { + if (e instanceof PathNotExistException) { + options?.logger?.warn(e.message); + } else { + throw e; + } + } + this._path = path; + this._meta = DocusaurusDocTreeCategory._processCategoryItemMetadata(path); + this._logger = options?.logger; + } + + public get isCategory(): boolean { + return true; + } + + public get meta(): DocusaurusDocTreeItemMeta { + return { + title: + this._meta?.title ?? this._index?.meta.title ?? basename(this._path), + syncToConfluence: this._index?.meta.syncToConfluence ?? true, + confluenceShortName: this._index?.meta.confluenceShortName, + confluenceTitle: this._index?.meta.confluenceTitle, + }; + } + + public get content(): string { + return this._index?.content ?? ""; + } + + public get path(): string { + // NOTE: fake index.md path to be able reference following the same logic as for pages + return this._index?.path ?? join(this._path, "index.md"); + } + + private get containsIndex(): boolean { + return this._index !== undefined; + } + + private static _detectCategoryItemFile(path: string): string | null { + if (existsSync(join(path, "_category_.yml"))) { + return join(path, "_category_.yml"); + } + if (existsSync(join(path, "_category_.yaml"))) { + return join(path, "_category_.yaml"); + } + if (existsSync(join(path, "_category_.json"))) { + return join(path, "_category_.json"); + } + return null; + } + + private static _processCategoryItemMetadata( + path: string, + ): DocusaurusDocTreeCategoryMeta | undefined { + const categoryItemFile = + DocusaurusDocTreeCategory._detectCategoryItemFile(path); + if (categoryItemFile === null) { + return undefined; + } + try { + const categoryMeta = parseYaml(readFileSync(categoryItemFile).toString()); + const { label } = CategoryItemMetadataValidator.parse(categoryMeta); + return { + title: label, + }; + } catch (e) { + throw new Error(`Path ${path} has an invalid _category_.yml file`, { + cause: e, + }); + } + } + + public async visit(): Promise { + if (!this.meta.syncToConfluence) { + this._logger?.debug( + `Category ${this._path} is not set to sync to Confluence`, + ); + return []; + } + const paths = await readdir(this._path); + const childrenPaths = paths + .map((path) => join(this._path, path)) + .filter(this._isDirectoryOrNotIndexFile); + const childrenItems = await Promise.all( + childrenPaths.map((path) => + DocusaurusDocItemFactory.fromPath(path, { + logger: this._logger?.namespace(path.replace(this._path, "")), + }), + ), + ); + const flattenedItems = await Promise.all( + childrenItems.map((root) => root.visit()), + ); + const items = flattenedItems.flat(); + this._logger?.debug(`Category ${this._path} has ${items.length} children`); + if (items.length === 0) { + return this.containsIndex ? [this] : []; + } + return [this, ...items]; + } + + private _isDirectoryOrNotIndexFile(path: string): boolean { + return lstatSync(path).isDirectory() || isValidFile(path); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.types.ts new file mode 100644 index 00000000..50fa8526 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.types.ts @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; + +export interface DocusaurusDocTreeCategoryOptions { + /** Logger */ + logger?: LoggerInterface; +} + +/** Creates DocusaurusDocTreeCategory interface */ +export interface DocusaurusDocTreeCategoryConstructor { + /** + * Returns DocusaurusDocTreeCategory interface + * + * Creates a new DocusaurusDocTreeCategory instance from a path. + * If it contains an index.md file, its title, syncToConfluence configuration and content + * will be obtained from it. Otherwise, its title will be its path's basename and it will + * enable the syncToConfluence config. + * + * In addition, the category information will be extended by metadata contained in + * the _category_.[json|yaml] file, if existent. + * + * @param {string} path - Path to the category + * @returns {DocusaurusDocTreeCategory} instance {@link DocusaurusDocTreeCategoryInterface}. + * @throws {Error} If the path does not exist. + * @throws {Error} If the path is not a directory. + * @throws {Error} If the path does not contain an index.md file. + * @example + * // A category with an index.md file + * // docs/ + * // ├── index.md + * const category = new DocusaurusDocTreeCategory("/docs"); + * // will create a category with the following properties: + * // { + * // path: "/docs", + * // meta: { + * // title: "Docs", + * // syncToConfluence: true, + * // }, + * // isCategory: true, + * // content: "......" + * // } + * + * @example + * // A category without an index.md file + * // docs/ + * const category = new DocusaurusDocTreeCategory("/docs"); + * // will create a category with the following properties: + * // { + * // path: "/docs", + * // meta: { + * // title: "docs", + * // syncToConfluence: true, + * // }, + * // isCategory: true, + * // content: "" + * // } + * + * @see {@link https://docusaurus.io/docs/sidebar/autogenerated#category-item-metadata | Category Item Metadata} + */ + new ( + path: string, + options?: DocusaurusDocTreeCategoryOptions, + ): DocusaurusDocTreeCategoryInterface; +} + +/** + * DocusaurusDocTreeCategory interface + * + * @extends DocusaurusDocTreeItem + */ +export type DocusaurusDocTreeCategoryInterface = DocusaurusDocTreeItem; + +/** Docusaurus Tree Category meta */ +export interface DocusaurusDocTreeCategoryMeta { + /** Category title */ + readonly title?: string; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.ts new file mode 100644 index 00000000..78ac5328 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { basename } from "node:path"; + +import { isStringWithLength } from "../../support/typesValidations.js"; +import { DocusaurusDocPage } from "../pages/DocusaurusDocPage.js"; +import type { DocusaurusDocPageOptions } from "../pages/DocusaurusDocPage.types.js"; +import { isNotIndexFile } from "../util/files.js"; + +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; +import type { + DocusaurusDocTreePageConstructor, + DocusaurusDocTreePageInterface, +} from "./DocusaurusDocTreePage.types.js"; + +export const DocusaurusDocTreePage: DocusaurusDocTreePageConstructor = class DocusaurusDocTreePage + extends DocusaurusDocPage + implements DocusaurusDocTreePageInterface +{ + constructor(path: string, options?: DocusaurusDocPageOptions) { + super(path, options); + if ( + isNotIndexFile(path) && + isStringWithLength(this.meta.confluenceShortName as string) + ) { + options?.logger?.warn( + `An unnecessary confluence short name has been set for ${basename( + path, + )} that is not an index file. This confluence short name will be ignored.`, + ); + } + } + + public async visit(): Promise { + return this.meta.syncToConfluence ? [this] : []; + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.types.ts new file mode 100644 index 00000000..02f54f22 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.types.ts @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; + +export interface DocusaurusDocTreePageOptions { + /** Logger */ + logger?: LoggerInterface; +} + +/** Creates DocusaurusDocTreePage interface */ +export interface DocusaurusDocTreePageConstructor { + /** Returns DocusaurusDocTreePage interface + * + * @param {string} path - Path to the page + * @returns {DocusaurusDocTreePage} instance {@link DocusaurusDocTreePageInterface}. + * @throws {Error} If the path does not exist. + * @throws {Error} If the path is not a markdown file. + */ + new ( + path: string, + options?: DocusaurusDocTreePageOptions, + ): DocusaurusDocTreePageInterface; +} + +/** + * DocusaurusDocTreePage interface + * + * @extends DocusaurusDocTreeItem + */ +export type DocusaurusDocTreePageInterface = DocusaurusDocTreeItem; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.ts new file mode 100644 index 00000000..0eef1c81 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { getIndexFile } from "../util/files.js"; + +import type { DocusaurusDocItemFactoryFromPathOptions } from "./DocusaurusDocItemFactory.types.js"; +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; +import { DocusaurusDocTreePage } from "./DocusaurusDocTreePage.js"; +import type { DocusaurusDocTreePageFactoryInterface } from "./DocusaurusDocTreePageFactory.types.js"; +import { DocusaurusDocTreePageMdx } from "./DocusaurusDocTreePageMdx.js"; + +export const DocusaurusDocTreePageFactory: DocusaurusDocTreePageFactoryInterface = class DocusaurusDocTreePageFactory { + public static fromPath( + path: string, + options?: DocusaurusDocItemFactoryFromPathOptions, + ): DocusaurusDocTreeItem { + return this._obtainedDocusaurusDocTreePage(path, options); + } + + public static fromCategoryIndex( + path: string, + options?: DocusaurusDocItemFactoryFromPathOptions, + ): DocusaurusDocTreeItem { + const indexPath = getIndexFile(path, options); + return this._obtainedDocusaurusDocTreePage(indexPath, options); + } + + private static _obtainedDocusaurusDocTreePage( + path: string, + options?: DocusaurusDocItemFactoryFromPathOptions, + ): DocusaurusDocTreeItem { + if (path.endsWith(".mdx")) { + return new DocusaurusDocTreePageMdx(path, options); + } + return new DocusaurusDocTreePage(path, options); + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.types.ts new file mode 100644 index 00000000..bfd03545 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.types.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DocusaurusDocPageFactoryInterface } from "../pages/DocusaurusDocPageFactory.types.js"; + +import type { DocusaurusDocItemFactoryFromPathOptions } from "./DocusaurusDocItemFactory.types.js"; +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; + +/** + * Factory for creating DocusaurusDocTreeItem instances from docusaurus pages. + * + * + * @export DocusaurusDocTreePageFactory + * @extends DocusaurusDocPageFactoryInterface + */ +export interface DocusaurusDocTreePageFactoryInterface + extends DocusaurusDocPageFactoryInterface { + /** + * Creates a new page from the category index. + * + * If the path is an mdx file, the {@link DocusaurusDocTreePageMdx} will be parsed with mdx instructions. + * Otherwise, the {@link DocusaurusDocTreePage} will be the parser with md instructions. + * + * @param path - The path to create the page. + * + * @returns A new DocusaurusDocTreeItem instance. + */ + fromCategoryIndex( + path: string, + options?: DocusaurusDocItemFactoryFromPathOptions, + ): DocusaurusDocTreeItem; +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageMdx.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageMdx.ts new file mode 100644 index 00000000..5162c17a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageMdx.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { isStringWithLength } from "../../support/typesValidations.js"; +import type { DocusaurusDocPageOptions } from "../pages/DocusaurusDocPage.types.js"; +import { DocusaurusDocPageMdx } from "../pages/DocusaurusDocPageMdx.js"; +import { isNotIndexFile } from "../util/files.js"; + +import type { DocusaurusDocTreeItem } from "./DocusaurusDocTree.types.js"; +import type { + DocusaurusDocTreePageConstructor, + DocusaurusDocTreePageInterface, +} from "./DocusaurusDocTreePage.types.js"; + +export const DocusaurusDocTreePageMdx: DocusaurusDocTreePageConstructor = class DocusaurusDocTreePageMdx + extends DocusaurusDocPageMdx + implements DocusaurusDocTreePageInterface +{ + constructor(path: string, options?: DocusaurusDocPageOptions) { + super(path, options); + if ( + isNotIndexFile(path) && + isStringWithLength(this.meta.confluenceShortName as string) + ) { + options?.logger?.warn( + "An unnecessary confluence short name has been set for a file that is not index.md or index.mdx. This confluence short name will be ignored.", + ); + } + } + + public async visit(): Promise { + return this.meta.syncToConfluence ? [this] : []; + } +}; diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/errors/CategoryIndexNotFoundException.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/errors/CategoryIndexNotFoundException.ts new file mode 100644 index 00000000..a6b1adcb --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/errors/CategoryIndexNotFoundException.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export class CategoryIndexNotFoundException extends Error { + constructor(category: string, options?: ErrorOptions) { + super(`Category index not found: ${category}`, options); + } +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/tree/support/validators/CategoryItemMetadata.ts b/components/markdown-confluence-sync/src/lib/docusaurus/tree/support/validators/CategoryItemMetadata.ts new file mode 100644 index 00000000..0cda60d2 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/support/validators/CategoryItemMetadata.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import z from "zod"; + +/** + * Validator for CategoryItemMetadata. + * + * @see {@link https://docusaurus.io/docs/sidebar#category-item-metadata | Docusaurus Category Item Metadata} + */ +export const CategoryItemMetadataValidator = z.object({ + label: z.string().nonempty().optional(), +}); diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/util/files.ts b/components/markdown-confluence-sync/src/lib/docusaurus/util/files.ts new file mode 100644 index 00000000..28b6f012 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/util/files.ts @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { basename, dirname, join } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import { globSync } from "glob"; +import { readSync, toVFile } from "to-vfile"; +import type { VFile } from "vfile"; + +import { PathNotExistException } from "../pages/errors/PathNotExistException.js"; + +import type { GetIndexFileOptions } from "./files.types.js"; +/** + * Checked if file is valid in docusaurus + * @param path - Page path + * @returns {boolean} + */ +export function isValidFile(path: string): boolean { + return isSupportedFile(path) && isNotIndexFile(path); +} + +/** + * Check if file ended with md or mdx + * @param path - Page path + * @returns {boolean} + */ +export function isSupportedFile(path: string): boolean { + return /mdx?$/.test(path); +} + +/** + * Check if file is not an index file + * index files are the following: + * - index.md + * - index.mdx + * - README.md + * - README.mdx + * - [directory-name].md + * - [directory-name].mdx + * @param path - Page path + * @returns {boolean} + */ +export function isNotIndexFile(path: string): boolean { + const dirnamePath = basename(dirname(path)); + const pattern = buildIndexFileRegExp("", dirnamePath); + return !pattern.test(path); +} + +/** + * Replace docusaurus admonitions titles to a valid remark-directive + * @param {string} path - Path to the docs directory + * @param options - Options with {LoggerInterface} logger + * @returns {VFile} - File + */ +export function readMarkdownAndPatchDocusaurusAdmonitions( + path: string, + options?: { logger?: LoggerInterface }, +): VFile { + const file = toVFile(readSync(path)); + // HACK: fix docusaurus directive syntax + // Docusaurus directive syntax is not compatible with remark-directive. + // Docusaurus allows title following directive type, but remark-directive does not. + // So, we replace `:::type title` to `:::type[title]` here. + file.value = file.value + .toString() + .replace(/^:::([a-z]+) +(.+)$/gm, (_match, type, title) => { + options?.logger?.debug( + `Fix docusaurus directive syntax: "${_match}" => ":::${type}[${title}]"`, + ); + return `:::${type}[${title}]`; + }); + return file; +} + +/** + * Search for index file in the path + * @param {string} path - Path to the docs directory + * @param options - Options with {LoggerInterface} logger + * @returns {string} - Index file path + */ +export function getIndexFile( + path: string, + options?: GetIndexFileOptions, +): string { + const indexFilesGlob = `{index,README,Readme,${basename(path)}}.{md,mdx}`; + const indexFilesFounded = globSync(indexFilesGlob, { cwd: path }); + + if (indexFilesFounded.length === 0) { + throw new PathNotExistException( + `Index file does not exist in this path ${path}`, + ); + } + if (indexFilesFounded.length > 1) { + options?.logger?.warn( + `Multiple index files found in ${basename(path)} directory. Using ${ + indexFilesFounded[0] + } as index file. Ignoring the rest.`, + ); + } + + return join(path, indexFilesFounded[0]); +} + +/** + * Search for index file in the path from a list of paths + * @param {string} path - Path to search + * @param {string[]} paths - Available paths + * @returns {string} - Index file path + */ +export function getIndexFileFromPaths(path: string, paths: string[]): string { + const indexFiles = [ + "index.md", + "index.mdx", + "README.md", + "Readme.md", + "README.mdx", + "Readme.mdx", + `${basename(path)}.md`, + `${basename(path)}.mdx`, + ]; + + const indexFilesFounded = indexFiles.find((indexFile) => + paths.includes(join(path, indexFile)), + ); + + // This should never happen, because we are checking if the path exists before + // istanbul ignore next + if (!indexFilesFounded) + throw new PathNotExistException( + `Index file does not exist in this path ${path}`, + ); + + return join(path, indexFilesFounded); +} + +/** + * Build index file regexp + * @param sep - Separator + * @param dirnamePath - Directory name + * @returns {RegExp} - RegExp to match with any index file + */ +export function buildIndexFileRegExp(sep: string, dirnamePath: string) { + //HACK Use this check to correct an irregular expression when executing unit tests in windows when using parentheses. + const pathSep = sep === "\\" ? "\\\\" : sep; + return new RegExp(pathSep + `(index|README|${dirnamePath}).mdx?$`); +} diff --git a/components/markdown-confluence-sync/src/lib/docusaurus/util/files.types.ts b/components/markdown-confluence-sync/src/lib/docusaurus/util/files.types.ts new file mode 100644 index 00000000..ad07707e --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/util/files.types.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { LoggerInterface } from "@mocks-server/logger"; + +export interface GetIndexFileOptions { + /** Logger */ + logger?: LoggerInterface; +} diff --git a/components/markdown-confluence-sync/src/lib/index.ts b/components/markdown-confluence-sync/src/lib/index.ts new file mode 100644 index 00000000..750cf969 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/index.ts @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./types.js"; +export * from "./MarkdownConfluenceSync.js"; diff --git a/components/markdown-confluence-sync/src/lib/support/typesValidations.ts b/components/markdown-confluence-sync/src/lib/support/typesValidations.ts new file mode 100644 index 00000000..7bedb818 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/support/typesValidations.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +/** + * Check if the value is a string and not empty + * @param value + * @returns {boolean} + */ +export function isStringWithLength(value: string): boolean { + return typeof value === "string" && value.length !== 0; +} diff --git a/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.ts b/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.ts new file mode 100644 index 00000000..7835e846 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.ts @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Data, Node as UnistNode } from "unist"; +import type { Test } from "unist-util-is"; +import { visit } from "unist-util-visit"; + +import type { BuildReplacer } from "./unist-util-replace.types.js"; + +/** + * Replace nodes in a tree given a test and a replacement function. + * + * @param tree - Root node of the tree to visit. + * @param is - Test to check if a node should be replaced. + * @param replacement - Function to build the replacement node. + */ +export function replace, Check extends Test>( + tree: Tree, + is: Check, + replacement: BuildReplacer, +) { + visit(tree, is, (node, index, parent) => { + // NOTE: Coverage ignored because it is unreachable from tests. Defensive programming. + /* istanbul ignore if */ + if (index === null || parent === null) { + throw new SyntaxError("Unexpected null value"); + } + const newUnistNode = replacement(node, index, parent); + parent.children.splice(index, 1, newUnistNode); + }); +} diff --git a/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.types.ts b/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.types.ts new file mode 100644 index 00000000..26c89347 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.types.ts @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Data, Node as UnistNode, Parent } from "unist"; +import type { Test } from "unist-util-is"; +import type { ParentsOf } from "unist-util-visit/lib/index.js"; +import type { + InclusiveDescendant, + Matches, +} from "unist-util-visit-parents/complex-types.js"; + +/** + * @module "unist-util-replace" + * Types module based on unist-util-visit types + * + * @see {@link https://github.com/syntax-tree/unist-util-visit/blob/main/lib/index.js | unist-util-visit - types} + */ + +/** + * Handle a node (matching `test`, if given). + * + * Visitors are free to transform `node`. + * They can also transform `parent`. + * + * Replacing `node` itself, if `SKIP` is not returned, still causes its + * descendants to be walked (which is a bug). + * + * When adding or removing previous siblings of `node` (or next siblings, in + * case of reverse), the `Visitor` should return a new `Index` to specify the + * sibling to traverse after `node` is traversed. + * Adding or removing next siblings of `node` (or previous siblings, in case + * of reverse) is handled as expected without needing to return a new `Index`. + * + * Removing the children property of `parent` still results in them being + * traversed. + */ +export type Replacer< + Visited extends UnistNode = UnistNode, + Ancestor extends Parent, Data> = Parent< + UnistNode, + Data + >, + ReplacerResult = Ancestor["children"][0], +> = ( + node: Visited, + index: Visited extends UnistNode ? number | null : never, + parent: Ancestor extends UnistNode ? Ancestor | null : never, +) => ReplacerResult; + +/** + * Build a typed `Visitor` function from a node and all possible parents. + * + * It will infer which values are passed as `node` and which as `parent`. + */ +export type BuildReplacerFromMatch< + Visited extends UnistNode, + Ancestor extends Parent, Data>, +> = Replacer>; + +/** + * Build a typed `Visitor` function from a list of descendants and a test. + * + * It will infer which values are passed as `node` and which as `parent`. + */ +export type BuildReplacerFromDescendants< + Descendant extends UnistNode, + Check extends Test, +> = BuildReplacerFromMatch< + Matches, + Extract +>; + +/** + * Build a typed `Visitor` function from a tree and a test. + * + * It will infer which values are passed as `node` and which as `parent`. + */ +export type BuildReplacer< + Tree extends UnistNode = UnistNode, + Check extends Test = string, +> = BuildReplacerFromDescendants, Check>; diff --git a/components/markdown-confluence-sync/src/lib/types.ts b/components/markdown-confluence-sync/src/lib/types.ts new file mode 100644 index 00000000..143a3cf6 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/types.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./MarkdownConfluenceSync.types.js"; +export * from "./confluence/ConfluenceSync.types.js"; +export * from "./docusaurus/DocusaurusPages.types.js"; diff --git a/components/markdown-confluence-sync/src/lib/util/paths.ts b/components/markdown-confluence-sync/src/lib/util/paths.ts new file mode 100644 index 00000000..7c15aef8 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/util/paths.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +export const PACKAGE_ROOT = resolve( + dirname(fileURLToPath(import.meta.url)), + "..", + "..", + "..", +); + +export const DEPENDENCIES_BIN_PATH = resolve( + PACKAGE_ROOT, + "node_modules", + ".bin", +); diff --git a/components/markdown-confluence-sync/src/types.ts b/components/markdown-confluence-sync/src/types.ts new file mode 100644 index 00000000..b2d76377 --- /dev/null +++ b/components/markdown-confluence-sync/src/types.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from "./lib/types.js"; diff --git a/components/markdown-confluence-sync/src/types/unist-util-find.d.ts b/components/markdown-confluence-sync/src/types/unist-util-find.d.ts new file mode 100644 index 00000000..e4739264 --- /dev/null +++ b/components/markdown-confluence-sync/src/types/unist-util-find.d.ts @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +declare module "unist-util-find"; diff --git a/components/markdown-confluence-sync/test/component/fixtures/basic/docs/category/index.md b/components/markdown-confluence-sync/test/component/fixtures/basic/docs/category/index.md new file mode 100644 index 00000000..16ee2839 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/basic/docs/category/index.md @@ -0,0 +1,6 @@ +--- +title: Category +sync_to_confluence: true +--- + +# Category diff --git a/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/docs/category/index.md b/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/docs/category/index.md new file mode 100644 index 00000000..16ee2839 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/docs/category/index.md @@ -0,0 +1,6 @@ +--- +title: Category +sync_to_confluence: true +--- + +# Category diff --git a/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..46e1099b --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/markdown-confluence-sync.config.cjs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const path = require("node:path"); + +module.exports = { + confluence: { + // Force config error to test error handling + url: 2, + spaceKey: "CTO", + }, + docsDir: path.join(__dirname, "./docs"), +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/config-file/docs/category/index.md b/components/markdown-confluence-sync/test/component/fixtures/config-file/docs/category/index.md new file mode 100644 index 00000000..fef4aac7 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/config-file/docs/category/index.md @@ -0,0 +1,5 @@ +--- +title: Category +--- + +# Category diff --git a/components/markdown-confluence-sync/test/component/fixtures/config-file/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/config-file/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..483feac3 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/config-file/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const path = require("node:path"); + +module.exports = { + confluence: { + url: "https://my-confluence.com", + spaceKey: "CTO", + }, + docsDir: path.join(__dirname, "./docs"), +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child1/grandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child1/grandChild1.md new file mode 100644 index 00000000..ef809868 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child1/grandChild1.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild1 +title: foo-grandChild1-title +sync_to_confluence: true +--- + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child1/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child1/index.md new file mode 100644 index 00000000..fc77c255 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child1/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child1 +title: foo-child1-title +sync_to_confluence: true +--- + +# Here goes the child1 title + +This is the child1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child2/grandChild3.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child2/grandChild3.md new file mode 100644 index 00000000..beb4b199 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child2/grandChild3.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild3 +title: foo-grandChild3-title +sync_to_confluence: true +--- + +# Here goes the grandChild3 title + +This is the grandChild3 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child2/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child2/index.md new file mode 100644 index 00000000..ea9a8636 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child2/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child2 +title: foo-child2-title +sync_to_confluence: true +--- + +# Here goes the child2 title + +This is the child2 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child3/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child3/index.md new file mode 100644 index 00000000..1fae1f17 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/child3/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child3 +title: foo-child3-title +sync_to_confluence: true +--- + +# Here goes the child3 title + +This is the child3 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/image.png b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/image.png new file mode 100644 index 00000000..9b5a9620 Binary files /dev/null and b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/image.png differ diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/index.md new file mode 100644 index 00000000..74fb6831 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/docs/parent/index.md @@ -0,0 +1,23 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +# Title + +:::note +⭐ this is an admonition +::: + +## Image + +This is an image: + +![image](./image.png) + +## Link + +This is a link: + +[link](./link.md) diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/grandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/grandChild1.md new file mode 100644 index 00000000..ef809868 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/grandChild1.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild1 +title: foo-grandChild1-title +sync_to_confluence: true +--- + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/grandChild2.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/grandChild2.md new file mode 100644 index 00000000..0a1c9719 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/grandChild2.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild2 +title: foo-grandChild2-title +sync_to_confluence: true +--- + +# Here goes the grandChild2 title + +This is the grandChild2 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/index.md new file mode 100644 index 00000000..fc77c255 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child1/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child1 +title: foo-child1-title +sync_to_confluence: true +--- + +# Here goes the child1 title + +This is the child1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/grandChild3.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/grandChild3.md new file mode 100644 index 00000000..beb4b199 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/grandChild3.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild3 +title: foo-grandChild3-title +sync_to_confluence: true +--- + +# Here goes the grandChild3 title + +This is the grandChild3 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/grandChild4.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/grandChild4.md new file mode 100644 index 00000000..bd9f51b1 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/grandChild4.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild4 +title: foo-grandChild4-title +sync_to_confluence: true +--- + +# Here goes the grandChild4 title + +This is the grandChild4 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/index.md new file mode 100644 index 00000000..ea9a8636 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child2/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child2 +title: foo-child2-title +sync_to_confluence: true +--- + +# Here goes the child2 title + +This is the child2 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child3/_category_.yml b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child3/_category_.yml new file mode 100644 index 00000000..37f1d3d2 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child3/_category_.yml @@ -0,0 +1,2 @@ +--- +label: foo-child3-title diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child3/grandChild5.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child3/grandChild5.md new file mode 100644 index 00000000..7f9db830 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child3/grandChild5.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild5 +title: foo-grandChild5-title +sync_to_confluence: true +--- + +# Here goes the grandChild5 title + +This is the grandChild5 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/grandChild6.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/grandChild6.md new file mode 100644 index 00000000..4b2e15f7 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/grandChild6.md @@ -0,0 +1,10 @@ +--- +id: foo-grandChild6 +title: foo-grandChild6-title +sync_to_confluence: true +confluence_short_name: grandchild6 +--- + +# Here goes the grandChild6 title + +This is the grandChild6 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/grandChild7.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/grandChild7.md new file mode 100644 index 00000000..ca609176 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/grandChild7.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild7 +title: foo-grandChild7-title +sync_to_confluence: true +--- + +# Here goes the grandChild7 title + +This is the grandChild7 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/index.md new file mode 100644 index 00000000..96bd55f2 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child4/index.md @@ -0,0 +1,10 @@ +--- +id: foo-child4 +title: foo-child4-title +sync_to_confluence: true +confluence_short_name: child4 +--- + +# Here goes the child4 title + +This is the child4 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/_category_.yml b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/_category_.yml new file mode 100644 index 00000000..db706a84 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/_category_.yml @@ -0,0 +1,2 @@ +--- +label: foo-child5-title diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/grandChild8.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/grandChild8.md new file mode 100644 index 00000000..3220124d --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/grandChild8.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild8 +title: foo-grandChild8-title +sync_to_confluence: true +--- + +# Here goes the grandChild8 title + +This is the grandChild8 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/index.md new file mode 100644 index 00000000..957ff1e6 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child5/index.md @@ -0,0 +1,10 @@ +--- +id: foo-child5 +title: foo-child5-title +sync_to_confluence: true +confluence_short_name: child5 +--- + +# Here goes the child5 title + +This is the child5 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/_category_.yml b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/_category_.yml new file mode 100644 index 00000000..0f92bde6 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/_category_.yml @@ -0,0 +1,2 @@ +--- +label: foo-child6-title diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild10/_category_.yml b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild10/_category_.yml new file mode 100644 index 00000000..0d6778b8 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild10/_category_.yml @@ -0,0 +1,2 @@ +--- +label: foo-grandChild10-title diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild10/greatGrandChild2.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild10/greatGrandChild2.md new file mode 100644 index 00000000..e09448b3 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild10/greatGrandChild2.md @@ -0,0 +1,9 @@ +--- +id: foo-greatGrandChild2 +title: foo-greatGrandChild-title +sync_to_confluence: true +--- + +# Here goes the greatGrandChild2 title + +This is the greatGrandChild2 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild9/_category_.yml b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild9/_category_.yml new file mode 100644 index 00000000..62d6973c --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild9/_category_.yml @@ -0,0 +1,2 @@ +--- +label: foo-grandChild9-title diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild9/greatGrandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild9/greatGrandChild1.md new file mode 100644 index 00000000..cac2319d --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/child6/grandChild9/greatGrandChild1.md @@ -0,0 +1,9 @@ +--- +id: foo-greatGrandChild1 +title: foo-greatGrandChild-title +sync_to_confluence: true +--- + +# Here goes the greatGrandChild1 title + +This is the greatGrandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/index.md new file mode 100644 index 00000000..f225e3d5 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/docs/parent/index.md @@ -0,0 +1,48 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +# Title + +:::note +⭐ this is an admonition +::: + + +## External Link + +This is a link: + +[External link](https://httpbin.org) + +## Internal Link + +This is a link: + +[Internal link](./child1/index.md) + +## Mdx Code Block + +This is a mdx code block: +```mdx-code-block +

Mdx code block test

+``` + +## Details + +
Details +```markdown + :::caution Status + Proposed + ::: +``` +
+ +## Footnotes + +This is a paragraph with a footnote[^1]. + +[^1]: This is a footnote. + diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/README.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/README.md new file mode 100644 index 00000000..94797d59 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/README.md @@ -0,0 +1,6 @@ +--- +title: README +sync_to_confluence: true +--- + +# README \ No newline at end of file diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/all-index-files.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/all-index-files.md new file mode 100644 index 00000000..11a9cfed --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/all-index-files.md @@ -0,0 +1,6 @@ +--- +title: All Index Files +sync_to_confluence: true +--- + +# All Index Files \ No newline at end of file diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/index.md new file mode 100644 index 00000000..09c5483e --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/index.md @@ -0,0 +1,6 @@ +--- +title: index.md +sync_to_confluence: true +--- + +# index.md is the highest priority index file \ No newline at end of file diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/index.mdx b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/index.mdx new file mode 100644 index 00000000..b29f3be7 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/all-index-files/index.mdx @@ -0,0 +1,6 @@ +--- +title: index.mdx +sync_to_confluence: true +--- + +# index.mdx \ No newline at end of file diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component1/README.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component1/README.md new file mode 100644 index 00000000..8d34f2a4 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component1/README.md @@ -0,0 +1,6 @@ +--- +title: README +sync_to_confluence: true +--- + +# README diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component1/child.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component1/child.md new file mode 100644 index 00000000..3b6969ad --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component1/child.md @@ -0,0 +1,6 @@ +--- +title: child +sync_to_confluence: true +--- + +# README-child diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component2/README.mdx b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component2/README.mdx new file mode 100644 index 00000000..18203b71 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component2/README.mdx @@ -0,0 +1,6 @@ +--- +title: README-mdx +sync_to_confluence: true +--- + +# README-mdx diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component2/child.mdx b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component2/child.mdx new file mode 100644 index 00000000..cbb54345 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/component2/child.mdx @@ -0,0 +1,6 @@ +--- +title: child +sync_to_confluence: true +--- + +# README-child-mdx diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name-2/child.mdx b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name-2/child.mdx new file mode 100644 index 00000000..2ac7847a --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name-2/child.mdx @@ -0,0 +1,6 @@ +--- +title: child +sync_to_confluence: true +--- + +# directory-name-2-child-mdx diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name-2/directory-name-2.mdx b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name-2/directory-name-2.mdx new file mode 100644 index 00000000..221b8a23 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name-2/directory-name-2.mdx @@ -0,0 +1,6 @@ +--- +title: directory-name-2-mdx +sync_to_confluence: true +--- + +# directory-name-2-mdx diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name/child.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name/child.md new file mode 100644 index 00000000..71163c1e --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name/child.md @@ -0,0 +1,6 @@ +--- +title: child +sync_to_confluence: true +--- + +# directory-name-child diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name/directory-name.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name/directory-name.md new file mode 100644 index 00000000..60e23df6 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/docs/directory-name/directory-name.md @@ -0,0 +1,6 @@ +--- +title: directory-name +sync_to_confluence: true +--- + +# directory-name diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/child1/grandChild2.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/child1/grandChild2.md new file mode 100644 index 00000000..ebab1220 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/child1/grandChild2.md @@ -0,0 +1,10 @@ +--- +id: foo-grandChild2 +title: foo-grandChild2-title +sync_to_confluence: true +confluence_page_id: 'foo-child1' +--- + +# Here goes the grandChild2 title + +This is the grandChild2 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/grandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/grandChild1.md new file mode 100644 index 00000000..ef809868 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/grandChild1.md @@ -0,0 +1,9 @@ +--- +id: foo-grandChild1 +title: foo-grandChild1-title +sync_to_confluence: true +--- + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/index.md new file mode 100644 index 00000000..fc77c255 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child1/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child1 +title: foo-child1-title +sync_to_confluence: true +--- + +# Here goes the child1 title + +This is the child1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/child1/grandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/child1/grandChild1.md new file mode 100644 index 00000000..6e2dd272 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/child1/grandChild1.md @@ -0,0 +1,9 @@ +--- +id: foo-child-2-child1-grandChild1 +title: foo-grandChild1-title +sync_to_confluence: true +--- + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/child1/grandChild1.txt b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/child1/grandChild1.txt new file mode 100644 index 00000000..8fdc0cfb --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/child1/grandChild1.txt @@ -0,0 +1,4 @@ + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/grandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/grandChild1.md new file mode 100644 index 00000000..37e72919 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/child2/grandChild1.md @@ -0,0 +1,9 @@ +--- +id: foo-child-2-grandChild1 +title: foo-grandChild1-title +sync_to_confluence: true +--- + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/index.md new file mode 100644 index 00000000..acac9a95 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/docs/parent/index.md @@ -0,0 +1,30 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +# Title + +:::note +⭐ this is an admonition +::: + +## External Link + +This is a link: + +[External link](https://httpbin.org) + +## Internal Link + +This is a link: + +[Internal link](./child1/index.md) + +## Footnotes + +This is a paragraph with a footnote[^1]. + +[^1]: This is a footnote. + diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..380ed886 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageName: "FLAT", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/child1/grandChild1.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/child1/grandChild1.md new file mode 100644 index 00000000..a2eddc2c --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/child1/grandChild1.md @@ -0,0 +1,10 @@ +--- +id: foo-grandChild1 +title: foo-grandChild1-title +sync_to_confluence: true +confluence_title: Confluence grandChild 1 +--- + +# Here goes the grandChild1 title + +This is the grandChild1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/child1/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/child1/index.md new file mode 100644 index 00000000..fc77c255 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/child1/index.md @@ -0,0 +1,9 @@ +--- +id: foo-child1 +title: foo-child1-title +sync_to_confluence: true +--- + +# Here goes the child1 title + +This is the child1 content diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/index.md new file mode 100644 index 00000000..944aa482 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/docs/parent/index.md @@ -0,0 +1,8 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +confluence_title: Confluence title +--- + +# Hello World diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/ignored-parent.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/ignored-parent.md new file mode 100644 index 00000000..12121845 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/ignored-parent.md @@ -0,0 +1,7 @@ +--- +id: foo-ignored-parent +title: foo-ignored-parent-title +sync_to_confluence: false +--- + +# Hello World diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/index.md new file mode 100644 index 00000000..1f8256db --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/index.md @@ -0,0 +1,7 @@ +--- +id: foo-ignored-index +title: foo-ignored-index-title +sync_to_confluence: false +--- + +# Hello World diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/parent.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/parent.md new file mode 100644 index 00000000..77a0ac0f --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/docs/parent.md @@ -0,0 +1,7 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +# Hello World diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/docs/parent/index.mdx b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/docs/parent/index.mdx new file mode 100644 index 00000000..699b8358 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/docs/parent/index.mdx @@ -0,0 +1,38 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +import Tabs from '@theme/Tabs'; + +## Mdx Code Block + +This is a mdx code block: +```mdx-code-block +

Hello world

+``` + + + + Tab Item Content +:::tip title +This is a tip +::: + + + Tab Item Content + + + Tab Item Content +:::note +This is a note +::: + + + + + Tab Item Content + + + diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/docs/parent.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/docs/parent.md new file mode 100644 index 00000000..75711935 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/docs/parent.md @@ -0,0 +1,18 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +# Code block +```javascript +function foo() { + return 'bar'; +} +``` + +# Mermaid Diagram +```mermaid +graph LR + A-->B +``` diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..de8e57ef --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/markdown-confluence-sync.config.cjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/docs/parent/index.md b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/docs/parent/index.md new file mode 100644 index 00000000..acac9a95 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/docs/parent/index.md @@ -0,0 +1,30 @@ +--- +id: foo-parent +title: foo-parent-title +sync_to_confluence: true +--- + +# Title + +:::note +⭐ this is an admonition +::: + +## External Link + +This is a link: + +[External link](https://httpbin.org) + +## Internal Link + +This is a link: + +[Internal link](./child1/index.md) + +## Footnotes + +This is a paragraph with a footnote[^1]. + +[^1]: This is a footnote. + diff --git a/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/markdown-confluence-sync.config.cjs b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/markdown-confluence-sync.config.cjs new file mode 100644 index 00000000..cca0b6c5 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/markdown-confluence-sync.config.cjs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +module.exports = { + logLevel: "info", + confluence: { + url: "http://127.0.0.1:3100", + personalAccessToken: "foo-token", + spaceKey: "foo-space-id", + rootPageId: "foo-root-id", + rootPageName: "foo-root-name", + }, +}; diff --git a/components/markdown-confluence-sync/test/component/specs/config.spec.ts b/components/markdown-confluence-sync/test/component/specs/config.spec.ts new file mode 100644 index 00000000..61a86d6d --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/config.spec.ts @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { ChildProcessManager } from "@tid-cross/child-process-manager"; +import type { ChildProcessManagerInterface } from "@tid-cross/child-process-manager"; + +import { cleanLogs } from "../support/Logs"; +import { + getFixtureFolder, + getBinaryPathFromFixtureFolder, +} from "../support/Paths"; + +describe("configuration", () => { + let cli: ChildProcessManagerInterface; + + beforeEach(() => { + process.env.MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL = "debug"; + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("basic"), + silent: true, + }); + }); + + afterEach(async () => { + await cli.kill(); + }); + + describe("when providing config using env vars", () => { + it("should exit with code 1 when Confluence url is not set", async () => { + delete process.env.MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_URL; + const { exitCode, logs } = await cli.run(); + + expect(cleanLogs(logs)).toContain( + `Error: Confluence URL is required. Please set confluence.url option.`, + ); + expect(exitCode).toBe(1); + }); + }); + + describe("when providing config using config file", () => { + it("should exit with code 1 when Confluence url is not set", async () => { + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("config-file-wrong"), + silent: true, + }); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toContain( + `Error: /confluence/url: type must be string`, + ); + }); + }); + + describe("when providing config using arguments", () => { + it("should exit with code 1 when Confluence url is wrongly typed", async () => { + cli = new ChildProcessManager( + [ + getBinaryPathFromFixtureFolder(), + "--confluence.foo-url=https://foo-confluence.com", + ], + { + cwd: getFixtureFolder("basic"), + silent: true, + }, + ); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toContain( + `error: unknown option '--confluence.foo-url=https://foo-confluence.com'`, + ); + }); + + it("should display a log text when mode is not set", async () => { + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("basic"), + silent: true, + }); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toContain(`mode option is tree`); + }); + + it("should display a log text when mode is flat", async () => { + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--mode=flat"], + { + cwd: getFixtureFolder("basic"), + silent: true, + }, + ); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toContain(`mode option is flat`); + }); + + it(`should fail and throw log error when mode isn't valid mode`, async () => { + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--mode=foo"], + { + cwd: getFixtureFolder("basic"), + silent: true, + }, + ); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`must be one of "tree" or "flat"`), + ]), + ); + }); + + it("should fail and throw log error when mode is flat and filesPattern is empty", async () => { + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--mode=flat"], + { + cwd: getFixtureFolder("basic"), + silent: true, + }, + ); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`File pattern can't be empty in flat mode`), + ]), + ); + }); + + it("should fail and throw log error because mode is flat and filesPattern is empty", async () => { + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--mode=flat"], + { + cwd: getFixtureFolder("basic"), + silent: true, + }, + ); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`can't be empty in flat mode`), + ]), + ); + }); + + it("should display a log text when mode is flat and filesPattern not empty", async () => { + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--filesPattern=**/index*"], + { + cwd: getFixtureFolder("basic"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_MODE: "flat", + }, + }, + ); + const { exitCode, logs } = await cli.run(); + + expect(exitCode).toBe(1); + + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`matching the pattern`), + ]), + ); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/component/specs/flat.spec.ts b/components/markdown-confluence-sync/test/component/specs/flat.spec.ts new file mode 100644 index 00000000..4aa3ee64 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/flat.spec.ts @@ -0,0 +1,271 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ChildProcessManagerInterface } from "@tid-cross/child-process-manager"; +import { ChildProcessManager } from "@tid-cross/child-process-manager"; + +import { cleanLogs } from "../support/Logs"; +import { + changeMockCollection, + getRequestsByRouteId, + resetRequests, +} from "../support/Mock"; +import type { SpyRequest } from "../support/Mock.types"; +import { + getBinaryPathFromFixtureFolder, + getFixtureFolder, +} from "../support/Paths"; + +describe("markdown-confluence-sync binary", () => { + let cli: ChildProcessManagerInterface; + let logs: string[]; + let exitCode: number | null; + let createRequests: SpyRequest[]; + + describe("with flat mode active", () => { + describe("when no file pattern is provided", () => { + beforeAll(async () => { + await changeMockCollection("with-mdx-files"); + await resetRequests(); + + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--mode=flat"], + { + cwd: getFixtureFolder("mock-server-with-mdx-files"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + }, + }, + ); + + const result = await cli.run(); + logs = result.logs; + exitCode = result.exitCode; + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 1", async () => { + expect(exitCode).toBe(1); + }); + + it("should fail and throw error because file pattern can't be empty", async () => { + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`File pattern can't be empty in flat mode`), + ]), + ); + }); + }); + + describe("when filesPattern option found more than one page", () => { + beforeAll(async () => { + await changeMockCollection("with-confluence-page-id"); + await resetRequests(); + + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--filesPattern=**/grandChild1*"], + { + cwd: getFixtureFolder("mock-server-with-confluence-page-id"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + MARKDOWN_CONFLUENCE_SYNC_MODE: "flat", + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_ROOT_PAGE_ID: "foo-root-id", + }, + }, + ); + + const result = await cli.run(); + logs = result.logs; + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should display a log text containing 'matching the pattern", async () => { + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`matching the pattern`), + ]), + ); + }); + + it("you should have created 3 pages that have ancestors with the root page id", async () => { + const ancestors = [{ id: "foo-root-id" }]; + + expect(createRequests).toHaveLength(3); + expect(createRequests.at(0)?.body?.ancestors).toStrictEqual(ancestors); + expect(createRequests.at(1)?.body?.ancestors).toStrictEqual(ancestors); + expect(createRequests.at(2)?.body?.ancestors).toStrictEqual(ancestors); + }); + }); + + describe("when the options have the option filesPattern and no rootPageId", () => { + let updateRequest: SpyRequest[]; + + beforeAll(async () => { + await changeMockCollection("with-confluence-page-id"); + await resetRequests(); + + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--filesPattern=**/grandChild2*"], + { + cwd: getFixtureFolder("mock-server-with-confluence-page-id"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + MARKDOWN_CONFLUENCE_SYNC_MODE: "flat", + }, + }, + ); + + const result = await cli.run(); + logs = result.logs; + exitCode = result.exitCode; + updateRequest = await getRequestsByRouteId("confluence-update-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have updated 1 page with the confluence page identifier given in the file", async () => { + expect(updateRequest).toHaveLength(1); + }); + }); + + describe("when filesPattern option searches for a txt file", () => { + beforeAll(async () => { + await changeMockCollection("with-confluence-page-id"); + await resetRequests(); + + cli = new ChildProcessManager( + [ + getBinaryPathFromFixtureFolder(), + "--filesPattern=**/grandChild1.txt", + ], + { + cwd: getFixtureFolder("mock-server-with-confluence-page-id"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + MARKDOWN_CONFLUENCE_SYNC_MODE: "flat", + }, + }, + ); + + const result = await cli.run(); + logs = result.logs; + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should not have created pages because the file filter is looking for md or mdx files", async () => { + expect(createRequests).toHaveLength(0); + }); + }); + + describe("when filesPattern option searches files with 'check' pattern", () => { + beforeAll(async () => { + await changeMockCollection("with-confluence-page-id"); + await resetRequests(); + + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--filesPattern=**/check*"], + { + cwd: getFixtureFolder("mock-server-with-confluence-page-id"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + MARKDOWN_CONFLUENCE_SYNC_MODE: "flat", + }, + }, + ); + + const result = await cli.run(); + logs = result.logs; + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should not have created pages because files not matches pattern", async () => { + expect(createRequests).toHaveLength(0); + }); + }); + + describe("when no rootPageId is provided and there are pages without confluence id", () => { + beforeAll(async () => { + await changeMockCollection("with-confluence-page-id"); + await resetRequests(); + + cli = new ChildProcessManager( + [getBinaryPathFromFixtureFolder(), "--filesPattern=**/grandChild1*"], + { + cwd: getFixtureFolder("mock-server-with-confluence-page-id"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + MARKDOWN_CONFLUENCE_SYNC_MODE: "flat", + }, + }, + ); + + const result = await cli.run(); + logs = result.logs; + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 1", async () => { + expect(exitCode).toBe(1); + }); + + it("should display a log text containing 'when there are pages without an id' because all pages haven't confluence pages ids", async () => { + expect(cleanLogs(logs)).toEqual( + expect.arrayContaining([ + expect.stringContaining(`when there are pages without an id`), + ]), + ); + }); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/component/specs/sync.spec.ts b/components/markdown-confluence-sync/test/component/specs/sync.spec.ts new file mode 100644 index 00000000..dcdbd769 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/sync.spec.ts @@ -0,0 +1,1683 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { rm } from "fs/promises"; +import { resolve } from "path"; + +import type { ChildProcessManagerInterface } from "@tid-cross/child-process-manager"; +import { ChildProcessManager } from "@tid-cross/child-process-manager"; +import { glob } from "glob"; +import { dedent } from "ts-dedent"; + +import { cleanLogs } from "../support/Logs"; +import { + changeMockCollection, + getRequestsByRouteId, + resetRequests, +} from "../support/Mock"; +import type { SpyRequest } from "../support/Mock.types"; +import { + getBinaryPathFromFixtureFolder, + getFixtureFolder, +} from "../support/Paths"; + +describe("markdown-confluence-sync binary", () => { + describe("when executed", () => { + let createRequests: SpyRequest[]; + let updateRequests: SpyRequest[]; + let deleteRequests: SpyRequest[]; + let createAttachmentsRequests: SpyRequest[]; + let cli: ChildProcessManagerInterface; + let exitCode: number | null; + let logs: string[]; + + function findRequestByTitle(title: string, collection: SpyRequest[]) { + return collection.find((request) => request?.body?.title === title); + } + + function findRequestById(id: string, collection: SpyRequest[]) { + return collection.find((request) => request?.params?.pageId === id); + } + + describe("when the root page has no children (pagesNoRoot input and empty-root mock)", () => { + beforeAll(async () => { + await changeMockCollection("empty-root"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-empty-root"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have logged pages to sync", async () => { + expect(cleanLogs(logs)).toContain( + `Converting 19 Docusaurus pages to Confluence pages...`, + ); + }); + + it("should have debug log level", async () => { + expect(cleanLogs(logs)).toContain( + `Found 19 pages in ${resolve(getFixtureFolder("mock-server-empty-root"), "docs")}`, + ); + }); + + it("should have created 19 pages", async () => { + expect(createRequests).toHaveLength(19); + }); + + it("should have sent data of page with title foo-parent-title", async () => { + const pageRequest = findRequestByTitle( + "foo-parent-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "foo-parent-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-root-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost

Title

+
+

Note:

+

⭐ this is an admonition

+
+

External Link

+

This is a link:

+

External link

+

Internal Link

+

This is a link:

+

+

Mdx Code Block

+

This is a mdx code block:

+

Details

+ + Details
    :::caution Status
+                    Proposed
+                    :::
+                
+

Footnotes

+

This is a paragraph with a footnote.

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("when file contains mdx code blocks should have removed the mdx code blocks", async () => { + const pageRequest = findRequestByTitle( + "foo-parent-title", + createRequests, + ); + + expect(pageRequest?.body?.body).toEqual({ + storage: expect.objectContaining({ + value: expect.not.stringContaining( + dedent` +

Mdx Code Block

+

This is a mdx code block:

+
Mdx code block test
+                
+ `, + ), + }), + }); + }); + + it("should have sent data of page with title foo-child1-title", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title] foo-child1-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child1-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the child1 title

+

This is the child1 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-child2-title", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title] foo-child2-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child2-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the child2 title

+

This is the child2 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-grandChild1-title", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][foo-child1-title] foo-grandChild1-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child1-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild1 title

+

This is the grandChild1 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have sent data of page with title foo-grandChild3-title", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child2-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild3 title

+

This is the grandChild3 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should create pages where the category does not have index.md", async () => { + const emptyCategoryRequest = findRequestByTitle( + "[foo-parent-title] foo-child3-title", + createRequests, + ); + + expect(emptyCategoryRequest?.url).toBe("/rest/api/content"); + expect(emptyCategoryRequest?.method).toBe("POST"); + expect(emptyCategoryRequest?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(emptyCategoryRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child3-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.any(String), + representation: "storage", + }, + }, + }); + + const pageRequest = findRequestByTitle( + "[foo-parent-title][foo-child3-title] foo-grandChild5-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][foo-child3-title] foo-grandChild5-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child3-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild5 title

+

This is the grandChild5 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + }); + + describe("when creating pages with name", () => { + it("should create category propagate name to children's titles", async () => { + const categoryWithSlug = findRequestByTitle( + "[foo-parent-title] foo-child4-title", + createRequests, + ); + + expect(categoryWithSlug?.url).toBe("/rest/api/content"); + expect(categoryWithSlug?.method).toBe("POST"); + expect(categoryWithSlug?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(categoryWithSlug?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child4-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the child4 title

+

This is the child4 content

+ `), + ), + representation: "storage", + }, + }, + }); + + const subCategoryWithSlugRequest = findRequestByTitle( + "[foo-parent-title][child4] foo-grandChild6-title", + createRequests, + ); + + expect(subCategoryWithSlugRequest?.url).toBe("/rest/api/content"); + expect(subCategoryWithSlugRequest?.method).toBe("POST"); + expect(subCategoryWithSlugRequest?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(subCategoryWithSlugRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][child4] foo-grandChild6-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child4-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild6 title

+

This is the grandChild6 content

+ `), + ), + representation: "storage", + }, + }, + }); + + const pageWithSlugRequest = findRequestByTitle( + "[foo-parent-title][child4] foo-grandChild7-title", + createRequests, + ); + + expect(pageWithSlugRequest?.url).toBe("/rest/api/content"); + expect(pageWithSlugRequest?.method).toBe("POST"); + expect(pageWithSlugRequest?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(pageWithSlugRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][child4] foo-grandChild7-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child4-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild7 title

+

This is the grandChild7 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should create category propagate name to children's titles when category metadata is present", async () => { + const categoryWithSlug = findRequestByTitle( + "[foo-parent-title] foo-child5-title", + createRequests, + ); + + expect(categoryWithSlug?.url).toBe("/rest/api/content"); + expect(categoryWithSlug?.method).toBe("POST"); + expect(categoryWithSlug?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(categoryWithSlug?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child5-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the child5 title

+

This is the child5 content

+ `), + ), + representation: "storage", + }, + }, + }); + + const pageWithoutSlugRequest = findRequestByTitle( + "[foo-parent-title][child5] foo-grandChild8-title", + createRequests, + ); + + expect(pageWithoutSlugRequest?.url).toBe("/rest/api/content"); + expect(pageWithoutSlugRequest?.method).toBe("POST"); + expect(pageWithoutSlugRequest?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(pageWithoutSlugRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][child5] foo-grandChild8-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child5-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild8 title

+

This is the grandChild8 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + describe("great grand children with same title", () => { + it("should create pages with same title", async () => { + const firstPageWithSameTitle = findRequestByTitle( + "[foo-parent-title][foo-child6-title][foo-grandChild9-title] foo-greatGrandChild-title", + createRequests, + ); + + expect(firstPageWithSameTitle?.url).toBe("/rest/api/content"); + expect(firstPageWithSameTitle?.method).toBe("POST"); + expect(firstPageWithSameTitle?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(firstPageWithSameTitle?.body).toEqual({ + type: "page", + title: + "[foo-parent-title][foo-child6-title][foo-grandChild9-title] foo-greatGrandChild-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-grandChild9-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the greatGrandChild1 title

+

This is the greatGrandChild1 content

+ `), + ), + representation: "storage", + }, + }, + }); + + const secondPageWithSameTitle = findRequestByTitle( + "[foo-parent-title][foo-child6-title][foo-grandChild10-title] foo-greatGrandChild-title", + createRequests, + ); + + expect(secondPageWithSameTitle?.url).toBe("/rest/api/content"); + expect(secondPageWithSameTitle?.method).toBe("POST"); + expect(secondPageWithSameTitle?.headers?.authorization).toBe( + "Bearer foo-token", + ); + expect(secondPageWithSameTitle?.body).toEqual({ + type: "page", + title: + "[foo-parent-title][foo-child6-title][foo-grandChild10-title] foo-greatGrandChild-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-grandChild10-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the greatGrandChild2 title

+

This is the greatGrandChild2 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + }); + }); + + describe("when dryRun option is true", () => { + beforeAll(async () => { + await changeMockCollection("empty-root"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-empty-root"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_DRY_RUN: "true", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have logged pages to sync", async () => { + expect(cleanLogs(logs)).toContain( + `Converting 19 Docusaurus pages to Confluence pages...`, + ); + }); + + it("should have created 0 pages", async () => { + expect(createRequests).toHaveLength(0); + }); + }); + + describe("when the root page has children (pagesNoRoot input and default-root mock)", () => { + beforeAll(async () => { + await changeMockCollection("default-root"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-default-root"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + updateRequests = await getRequestsByRouteId("confluence-update-page"); + deleteRequests = await getRequestsByRouteId("confluence-delete-page"); + createAttachmentsRequests = await getRequestsByRouteId( + "confluence-create-attachments", + ); + }); + + afterAll(async () => { + await cli.kill(); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have logged pages to sync", async () => { + expect(cleanLogs(logs)).toContain( + `Converting 6 Docusaurus pages to Confluence pages...`, + ); + }); + + it("should have debug log level", async () => { + expect(cleanLogs(logs)).toContain( + `Found 6 pages in ${resolve(getFixtureFolder("mock-server-default-root"), "docs")}`, + ); + }); + + it("should have created 3 pages", async () => { + expect(createRequests).toHaveLength(3); + }); + + it("should have create page with title foo-child3-title which folder only have an index.md", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title] foo-child3-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child3-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the child3 title

+

This is the child3 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have create page with title foo-grandChild3-title", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title][foo-child2-title] foo-grandChild3-title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child2-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild3 title

+

This is the grandChild3 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have updated 3 pages", async () => { + expect(updateRequests).toHaveLength(3); + }); + + it("should have update page with title foo-child1-title", async () => { + const pageRequest = findRequestByTitle( + "[foo-parent-title] foo-child1-title", + updateRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content/foo-child1-id"); + expect(pageRequest?.method).toBe("PUT"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[foo-parent-title] foo-child1-title", + version: { + number: 2, + }, + ancestors: [ + { + id: "foo-parent-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the child1 title

+

This is the child1 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have deleted 1 page and 1 attachment", async () => { + expect(deleteRequests).toHaveLength(2); + }); + + it("should have delete page with id foo-grandChild2-id", async () => { + const pageRequest = findRequestById( + "foo-grandChild2-id", + deleteRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content/foo-grandChild2-id"); + expect(pageRequest?.method).toBe("DELETE"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({}); + }); + + it("should have delete attachment with id foo-grandChild1-attachment-id", async () => { + const pageRequest = findRequestById( + "foo-grandChild1-attachment-id", + deleteRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe( + "/rest/api/content/foo-grandChild1-attachment-id", + ); + expect(pageRequest?.method).toBe("DELETE"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({}); + }); + + it("should have created 1 attachment", async () => { + expect(createAttachmentsRequests).toHaveLength(1); + }); + + it("should have create attachment for page with id foo-parent-id", async () => { + const pageRequest = findRequestById( + "foo-parent-id", + createAttachmentsRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe( + "/rest/api/content/foo-parent-id/child/attachment", + ); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.headers?.["x-atlassian-token"]).toBe("no-check"); + expect(pageRequest?.headers?.["content-type"]).toContain( + "multipart/form-data", + ); + }); + }); + + describe("when pages have confluence_title", () => { + beforeAll(async () => { + await changeMockCollection("with-confluence-title"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-confluence-title"), + silent: true, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 3 pages", async () => { + expect(createRequests).toHaveLength(3); + }); + + it("should have create page with title Confluence title", async () => { + const pageRequest = findRequestByTitle( + "Confluence title", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "Confluence title", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-root-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Hello World

+ `), + ), + representation: "storage", + }, + }, + }); + }); + + it("should have create page with title [Confluence title][foo-child1-title] Confluence grandChild 1", async () => { + const pageRequest = findRequestByTitle( + "[Confluence title][foo-child1-title] Confluence grandChild 1", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.url).toBe("/rest/api/content"); + expect(pageRequest?.method).toBe("POST"); + expect(pageRequest?.headers?.authorization).toBe("Bearer foo-token"); + expect(pageRequest?.body).toEqual({ + type: "page", + title: "[Confluence title][foo-child1-title] Confluence grandChild 1", + space: { + key: "foo-space-id", + }, + ancestors: [ + { + id: "foo-child1-id", + }, + ], + body: { + storage: { + value: expect.stringContaining( + dedent(` +

Here goes the grandChild1 title

+

This is the grandChild1 content

+ `), + ), + representation: "storage", + }, + }, + }); + }); + }); + + describe("when files in the root directory", () => { + beforeAll(async () => { + await changeMockCollection("empty-root"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-files-in-root"), + silent: true, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should ignore index.md in the root directory", () => { + expect(cleanLogs(logs)).toContain( + `Ignoring index.md file in root directory.`, + ); + }); + + it("should ignore pages not configured to sync", () => { + expect(createRequests).not.toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "foo-ignored-parent-title", + }), + }), + ); + }); + + it("should create pages configured to sync", () => { + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "foo-parent-title", + }), + }), + ); + }); + }); + + describe("index files", () => { + beforeAll(async () => { + await changeMockCollection("with-alternative-index-files"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-alternative-index-files"), + silent: false, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "warn", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should take index.md as index file when all the possible index files are present", () => { + const pageRequest = findRequestByTitle("index.md", createRequests); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "index.md", + ancestors: [ + { + id: "foo-root-id", + }, + ], + }), + ); + }); + + it("should log a warning when more than one file that can be considered as index file is present", () => { + expect(logs).toEqual( + expect.arrayContaining([ + expect.stringContaining( + `Multiple index files found in all-index-files directory. Using index.md as index file. Ignoring the rest.`, + ), + ]), + ); + }); + + it("should have send data of README.md", async () => { + const pageRequest = findRequestByTitle("README", createRequests); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "README", + ancestors: [ + { + id: "foo-root-id", + }, + ], + }), + ); + }); + + it("should have send data of a child page with title [README] child", async () => { + const pageRequest = findRequestByTitle( + "[README] child", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "[README] child", + ancestors: [ + { + id: "README-id", + }, + ], + }), + ); + }); + + it("should have send data of directory-name.md", async () => { + const pageRequest = findRequestByTitle( + "directory-name", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "directory-name", + ancestors: [ + { + id: "foo-root-id", + }, + ], + }), + ); + }); + + it("should have send data of a child page with title [directory-name] child", async () => { + const pageRequest = findRequestByTitle( + "[directory-name] child", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "[directory-name] child", + ancestors: [ + { + id: "directory-name-id", + }, + ], + }), + ); + }); + + it("should have send data of README.mdx", async () => { + const pageRequest = findRequestByTitle("README-mdx", createRequests); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "README-mdx", + ancestors: [ + { + id: "foo-root-id", + }, + ], + }), + ); + }); + + it("should have send data of a child page with title [README-mdx] child", async () => { + const pageRequest = findRequestByTitle( + "[README-mdx] child", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "[README-mdx] child", + ancestors: [ + { + id: "README-mdx-id", + }, + ], + }), + ); + }); + + it("should have send data of directory-name-2.mdx", async () => { + const pageRequest = findRequestByTitle( + "directory-name-2-mdx", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "directory-name-2-mdx", + ancestors: [ + { + id: "foo-root-id", + }, + ], + }), + ); + }); + + it("should have send data of a child page with title [directory-name-2-mdx] child", async () => { + const pageRequest = findRequestByTitle( + "[directory-name-2-mdx] child", + createRequests, + ); + + expect(pageRequest).toBeDefined(); + expect(pageRequest?.body).toEqual( + expect.objectContaining({ + title: "[directory-name-2-mdx] child", + ancestors: [ + { + id: "directory-name-2-mdx-id", + }, + ], + }), + ); + }); + }); + }); + + describe("with root page name option", () => { + let createRequests: SpyRequest[]; + let cli: ChildProcessManagerInterface; + let exitCode: number | null; + + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "[foo-root-name] foo-parent-title", + }), + }), + ); + }); + + describe("notice option", () => { + describe("with default message", () => { + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "[foo-root-name] foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining( + "AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost", + ), + }), + }), + }), + }), + ); + }); + }); + + describe("with notice message", () => { + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_NOTICE_MESSAGE: + "This is a warning notice", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "[foo-root-name] foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining("This is a warning notice"), + }), + }), + }), + }), + ); + }); + }); + + describe("with notice template", () => { + describe("invalid format", () => { + let logs: string[]; + + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "error", + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_NOTICE_TEMPLATE: + "{{relativePath}", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + logs = result.logs; + + createRequests = await getRequestsByRouteId( + "confluence-create-page", + ); + }); + + it("should have exit code 1", async () => { + expect(exitCode).toBe(1); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(logs).toContainEqual( + expect.stringContaining( + "Error occurs while rendering template: Error: Invalid notice template: {{relativePath}", + ), + ); + }); + }); + + describe("and without notice message", () => { + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_NOTICE_TEMPLATE: + "This page was generated from {{relativePath}} with title {{title}}. {{default}}", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId( + "confluence-create-page", + ); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "[foo-root-name] foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining( + "This page was generated from parent/index.md with title [foo-root-name] foo-parent-title. AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost", + ), + }), + }), + }), + }), + ); + }); + }); + + describe("and with notice message", () => { + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_NOTICE_TEMPLATE: + "This page was generated from {{relativePath}} with title {{title}}. {{message}}", + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_NOTICE_MESSAGE: + "This is a warning notice", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId( + "confluence-create-page", + ); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "[foo-root-name] foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining( + "This page was generated from parent/index.md with title [foo-root-name] foo-parent-title. This is a warning notice", + ), + }), + }), + }), + }), + ); + }); + }); + }); + }); + }); + + describe("with notice option", () => { + let createRequests: SpyRequest[]; + let cli: ChildProcessManagerInterface; + let exitCode: number | null; + + beforeAll(async () => { + await changeMockCollection("with-root-page-name"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-root-page-name"), + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_CONFLUENCE_NOTICE_MESSAGE: + "This is a warning notice", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page with title foo-root-title", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "[foo-root-name] foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining("This is a warning notice"), + }), + }), + }), + }), + ); + }); + }); + + // TODO: Investigate why this test is failing in CI + // eslint-disable-next-line jest/no-disabled-tests + describe.skip("mermaid diagrams", () => { + let createRequests: SpyRequest[]; + let cli: ChildProcessManagerInterface; + let exitCode: number | null; + let cwd: string; + + beforeAll(async () => { + await changeMockCollection("empty-root"); + await resetRequests(); + + cwd = getFixtureFolder("mock-server-with-mermaid-diagrams"); + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd, + silent: true, + env: { + MARKDOWN_CONFLUENCE_SYNC_LOG_LEVEL: "debug", + }, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + afterAll(async () => { + const autogeneratedImages = await glob("**/mermaid-diagrams", { + cwd, + absolute: true, + }); + for (const image of autogeneratedImages) { + await rm(resolve(process.cwd(), image), { + force: true, + recursive: true, + }); + } + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page without mermaid code block", async () => { + expect(createRequests).toHaveLength(1); + }); + + describe("body content", () => { + let createRequest: SpyRequest; + + beforeAll(async () => { + createRequest = createRequests[0]; + }); + + it("should not contain mermaid code block", async () => { + expect(createRequest).toEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.not + .stringContaining(dedent`

Mermaid Diagram

+
graph LR
+                    A-->B
+                  
+ `), + }), + }), + }), + }), + ); + }); + + // TODO: implement this when image attachment is supported + it.todo("should not contain mermaid diagram image attachment"); + }); + + it("should create mermaid diagram image in page folder", async () => { + const autogeneratedImages = await glob( + "**/mermaid-diagrams/autogenerated-*.svg", + { cwd }, + ); + + expect(autogeneratedImages).toHaveLength(1); + }); + + // TODO: implement this when image attachment is supported + it.todo("should upload mermaid diagrams as attachments"); + }); + + describe("with mdx files", () => { + let createRequests: SpyRequest[]; + let cli: ChildProcessManagerInterface; + let exitCode: number | null; + + beforeAll(async () => { + await changeMockCollection("with-mdx-files"); + await resetRequests(); + + cli = new ChildProcessManager([getBinaryPathFromFixtureFolder()], { + cwd: getFixtureFolder("mock-server-with-mdx-files"), + silent: true, + }); + + const result = await cli.run(); + exitCode = result.exitCode; + + createRequests = await getRequestsByRouteId("confluence-create-page"); + }); + + it("should have exit code 0", async () => { + expect(exitCode).toBe(0); + }); + + it("should have created 1 page containing the text 'Mdx Code Block'", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining("Mdx Code Block"), + }), + }), + }), + }), + ); + }); + + it("should have created 1 page converting Tabs tag", async () => { + expect(createRequests).toHaveLength(1); + expect(createRequests).toContainEqual( + expect.objectContaining({ + body: expect.objectContaining({ + title: "foo-parent-title", + body: expect.objectContaining({ + storage: expect.objectContaining({ + value: expect.stringContaining( + dedent(`

Mdx Code Block

+

This is a mdx code block:

+
    +
  • +

    File tree

    +

    Tab Item Content

    +
    +

    Tip: title

    +

    This is a tip

    +
    +
      +
    • +

      File tree

      +

      Tab Item Content

      +
    • +
    • +

      File tree

      +

      Tab Item Content

      +
      +

      Note:

      +

      This is a note

      +
      +
    • +
    +
  • +
  • +

    File tree

    +

    Tab Item Content

    +
  • +
`), + ), + }), + }), + }), + }), + ); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/component/support/Logs.ts b/components/markdown-confluence-sync/test/component/support/Logs.ts new file mode 100644 index 00000000..9efc2ecf --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Logs.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +export function cleanLogs(logs: string[]) { + return logs.map((log) => log.replace(/^(\S|\s)*\]/, "").trim()); +} diff --git a/components/markdown-confluence-sync/test/component/support/Mock.ts b/components/markdown-confluence-sync/test/component/support/Mock.ts new file mode 100644 index 00000000..168a55bc --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Mock.ts @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { AdminApiClient } from "@mocks-server/admin-api-client"; +import crossFetch from "cross-fetch"; + +import type { SpyRequest } from "./Mock.types"; + +const BASE_URL = "http://127.0.0.1:3100"; + +const DEFAULT_REQUEST_OPTIONS = { + method: "GET", +}; + +function mockUrl(path: string) { + return `${BASE_URL}/${path}`; +} + +async function doRequest(path: string, options: RequestInit = {}) { + const response = await crossFetch(mockUrl(path), { + ...DEFAULT_REQUEST_OPTIONS, + ...options, + }); + return response.json(); +} + +export function resetRequests(): Promise { + return doRequest("spy/requests", { + method: "DELETE", + }); +} + +export function getRequests(): Promise { + return doRequest("spy/requests"); +} + +export async function getRequestsByRouteId( + routeId: string, +): Promise { + const requests = await getRequests(); + return requests.filter((request) => request.routeId === routeId); +} + +export async function changeMockCollection(collectionId: string) { + const mockClient = new AdminApiClient(); + await mockClient.updateConfig({ + mock: { + collections: { + selected: collectionId, + }, + }, + }); +} diff --git a/components/markdown-confluence-sync/test/component/support/Mock.types.ts b/components/markdown-confluence-sync/test/component/support/Mock.types.ts new file mode 100644 index 00000000..005f579a --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Mock.types.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +/** Request to the mock server */ +export interface SpyRequest { + /** Route ID */ + routeId: string; + /** Request URL */ + url: string; + /** Request method */ + method: string; + /** Request headers */ + headers: Record; + /** Request body */ + body?: Record; + /** Request query params */ + params?: Record; +} diff --git a/components/markdown-confluence-sync/test/component/support/Paths.ts b/components/markdown-confluence-sync/test/component/support/Paths.ts new file mode 100644 index 00000000..4e01c7ec --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Paths.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import path from "path"; + +const TEST_COMPONENT_PATH = path.resolve(__dirname, ".."); + +export function getFixtureFolder(fixtureFolder: string): string { + return path.resolve(TEST_COMPONENT_PATH, "fixtures", fixtureFolder); +} + +export function getBinaryPathFromFixtureFolder(): string { + return "../../../../bin/markdown-confluence-sync.mjs"; +} diff --git a/components/markdown-confluence-sync/test/component/tsconfig.json b/components/markdown-confluence-sync/test/component/tsconfig.json new file mode 100644 index 00000000..20338a5f --- /dev/null +++ b/components/markdown-confluence-sync/test/component/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "moduleResolution": "node", + "paths": { + "@src/*": ["../../src/*"], + "@support/*": ["./support/*"] + } + }, + "include": ["**/*", "../../types/*"] +} diff --git a/components/markdown-confluence-sync/test/unit/specs/Cli.test.ts b/components/markdown-confluence-sync/test/unit/specs/Cli.test.ts new file mode 100644 index 00000000..cce3b49d --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/Cli.test.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { customMarkdownConfluenceSync } from "@support/mocks/DocusaurusToConfluence"; + +import { run } from "@src/Cli"; + +describe("cli", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should export run function", () => { + expect(run).toBeDefined(); + }); + + it("should call DocusaurusToConfluence sync function", async () => { + customMarkdownConfluenceSync.sync.mockReturnValue(true); + await run(); + + await expect(customMarkdownConfluenceSync.sync).toHaveBeenCalled(); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/Library.spec.ts b/components/markdown-confluence-sync/test/unit/specs/Library.spec.ts new file mode 100644 index 00000000..58a1fb54 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/Library.spec.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import * as library from "@src/index"; + +describe("library", () => { + it("should export MarkdownConfluenceSync class", () => { + expect(library.MarkdownConfluenceSync).toBeDefined(); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts b/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts new file mode 100644 index 00000000..73030f34 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/MarkdownConfluenceSync.test.ts @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { customConfluenceSync } from "@support/mocks/ConfluenceSync"; +import { customDocusaurusPages } from "@support/mocks/DocusaurusPages"; + +import { MarkdownConfluenceSync } from "@src/lib"; + +const CONFIG = { + config: { + readArguments: false, + readFile: false, + readEnvironment: false, + }, +}; + +describe("markdownConfluenceSync", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(MarkdownConfluenceSync).toBeDefined(); + }); + + it("should fail if not pass the configuration", async () => { + // Assert + //@ts-expect-error Ignore the next line to don't pass configuration + expect(() => new MarkdownConfluenceSync()).toThrow( + "Please provide configuration", + ); + }); + + it("when called twice, it should send to synchronize the pages to confluence twice", async () => { + // Arrange + const markdownConfluenceSync = new MarkdownConfluenceSync(CONFIG); + customDocusaurusPages.read.mockResolvedValue([]); + + // Act + await markdownConfluenceSync.sync(); + await markdownConfluenceSync.sync(); + + // Assert + expect(customConfluenceSync.sync.mock.calls).toHaveLength(2); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/ConfluenceSync.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/ConfluenceSync.test.ts new file mode 100644 index 00000000..eaec2658 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/ConfluenceSync.test.ts @@ -0,0 +1,320 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + ConfigInterface, + ConfigNamespaceInterface, +} from "@mocks-server/config"; +import { Config } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import type { DirResult } from "tmp"; + +import { customConfluencePage } from "@support/mocks/ConfluencePageTransformer"; +import { customConfluenceSyncPages } from "@support/mocks/ConfluenceSyncPages"; +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync } = new TempFiles(); + +import type { ConfluenceSyncOptions, ModeOption } from "@src/lib"; +import { ConfluenceSync } from "@src/lib/confluence/ConfluenceSync"; + +const CONFIG = { + config: { + readArguments: false, + readFile: false, + readEnvironment: false, + }, +}; + +describe("confluenceSync", () => { + let dir: DirResult; + let config: ConfigInterface; + let namespace: ConfigNamespaceInterface; + let logger: LoggerInterface; + let confluenceSyncOptions: ConfluenceSyncOptions; + + beforeEach(async () => { + dir = dirSync({ unsafeCleanup: true }); + config = new Config({ + moduleName: "markdown-confluence-sync", + }); + config.addOption({ + name: "mode", + type: "string", + default: "tree", + }); + namespace = config.addNamespace("confluence"); + logger = new Logger(""); + logger.setLevel("silent", { transport: "console" }); + confluenceSyncOptions = { + config: namespace, + logger, + mode: config.option("mode") as ModeOption, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + dir.removeCallback(); + }); + + it("should be defined", () => { + expect(ConfluenceSync).toBeDefined(); + }); + + it("should log the pages to sync", async () => { + // Arrange + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + const loggerSpy = jest.spyOn(logger, "info"); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + rootPageId: "foo-root-id", + }, + }); + customConfluencePage.transform.mockResolvedValue([ + { title: "foo-after-transformation-title" }, + ]); + + // Act + await confluenceSync.sync([ + { ancestors: [], title: "foo-title", path: "foo", relativePath: "foo" }, + ]); + + // Assert + expect(loggerSpy.mock.calls[0]).toStrictEqual([ + "Confluence pages to sync: foo-title", + ]); + expect(loggerSpy.mock.calls[1]).toStrictEqual([ + "Confluence pages to sync after transformation: foo-after-transformation-title", + ]); + }); + + describe("read", () => { + it("should fail if the url option is not defined", async () => { + // Arrange + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: {}, + }); + + // Act + // Assert + await expect(async () => await confluenceSync.sync([])).rejects.toThrow( + "Confluence URL is required. Please set confluence.url option.", + ); + }); + + it("should fail if the personalAccessToken option is not defined", async () => { + // Arrange + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + }, + }); + + // Act + // Assert + await expect(async () => await confluenceSync.sync([])).rejects.toThrow( + "Confluence personal access token is required. Please set confluence.personalAccessToken option.", + ); + }); + + it("should fail if the spaceKey option is not defined", async () => { + // Arrange + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + personalAccessToken: "bar", + }, + }); + + // Act + // Assert + await expect(async () => await confluenceSync.sync([])).rejects.toThrow( + "Confluence space id is required. Please set confluence.spaceId option.", + ); + }); + + it("should fail if the rootPageId option is not defined", async () => { + // Arrange + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + }, + }); + + // Act + // Assert + await expect(async () => await confluenceSync.sync([])).rejects.toThrow( + "Confluence root page id is required for TREE sync mode. Please set confluence.rootPageId option.", + ); + }); + + it("should send the appropriate pages tree to sync-to-confluence", async () => { + // Arrange + const transformReturnValue = [ + { + id: "foo-id", + content: "foo-content", + }, + ]; + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + rootPageId: "foo-root-id", + }, + }); + + customConfluencePage.transform.mockResolvedValue(transformReturnValue); + + // Act + await confluenceSync.sync([]); + + // Assert + expect(customConfluenceSyncPages.sync).toHaveBeenCalledWith( + transformReturnValue, + ); + }); + + describe("pages transformation", () => { + it("should send the appropriate pages tree to sync-to-confluence", async () => { + // Arrange + const transformReturnValue = [ + { + id: "foo-id", + content: "foo-content", + }, + ]; + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + rootPageId: "foo-root-id", + }, + }); + + customConfluencePage.transform.mockResolvedValue(transformReturnValue); + + // Act + await confluenceSync.sync([]); + + // Assert + expect(customConfluenceSyncPages.sync).toHaveBeenCalledWith( + transformReturnValue, + ); + }); + }); + + it("when called twice, it should send to synchronize the pages to confluence twice", async () => { + // Arrange + const transformReturnValue = [ + { + id: "foo-id", + title: "foo", + content: "foo-content", + ancestors: [{ id: "foo-root-id", title: "foo-root-title" }], + }, + ]; + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + rootPageId: "foo-root-id", + }, + }); + + customConfluencePage.transform.mockResolvedValue(transformReturnValue); + + // Act + await confluenceSync.sync([]); + await confluenceSync.sync([]); + + // Assert + expect(customConfluenceSyncPages.sync.mock.calls).toHaveLength(2); + }); + + it("when sync mode is flat and page haven't id, the function throw an error", async () => { + // Arrange + const transformReturnValue = [ + { + title: "foo", + content: "foo-content", + ancestors: [], + }, + ]; + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + mode: "flat", + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + }, + }); + + customConfluencePage.transform.mockResolvedValue(transformReturnValue); + + // Act + // Assert + await expect(async () => await confluenceSync.sync([])).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining( + "when there are pages without an id", + ), + }), + ); + }); + + it("when sync mode is flat and page have id, the function not throw an error", async () => { + // Arrange + const transformReturnValue = [ + { + id: "foo-id", + title: "foo", + content: "foo-content", + ancestors: [], + }, + ]; + const confluenceSync = new ConfluenceSync(confluenceSyncOptions); + await config.load({ + ...CONFIG, + mode: "flat", + confluence: { + url: "foo", + personalAccessToken: "bar", + spaceKey: "baz", + }, + }); + + customConfluencePage.transform.mockResolvedValue(transformReturnValue); + + // Act + // Assert + await expect(async () => await confluenceSync.sync([])).not.toThrow(); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/ConfluencePageTransformer.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/ConfluencePageTransformer.test.ts new file mode 100644 index 00000000..c61d1a24 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/ConfluencePageTransformer.test.ts @@ -0,0 +1,726 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { join, resolve } from "path"; + +import type { ConfluenceInputPage } from "@tid-cross/confluence-sync"; +import { glob } from "glob"; +import rehypeStringify from "rehype-stringify/lib"; +import { remark } from "remark"; +import remarkGfm from "remark-gfm"; +import remarkRehype from "remark-rehype/lib"; +import type { DirResult } from "tmp"; +import * as toVFile from "to-vfile"; +import { dedent } from "ts-dedent"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync } = new TempFiles(); + +import { ConfluencePageTransformer } from "@src/lib/confluence/transformer/ConfluencePageTransformer"; +import type { ConfluencePageTransformerInterface } from "@src/lib/confluence/transformer/ConfluencePageTransformer.types"; +import { InvalidTemplateError } from "@src/lib/confluence/transformer/errors/InvalidTemplateError"; + +import type { ConfluenceSyncPage } from "../../../../../src"; + +jest.mock("to-vfile", () => { + return { + __esModule: true, + ...jest.requireActual("to-vfile"), + }; +}); + +describe("confluencePageTransformer", () => { + let transformer: ConfluencePageTransformerInterface; + + beforeEach(() => { + transformer = new ConfluencePageTransformer({ spaceKey: "space-key" }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(ConfluencePageTransformer).toBeDefined(); + }); + + it("should throw error if transform fails", async () => { + // Arrange + jest.spyOn(toVFile, "toVFile").mockImplementationOnce(() => { + throw new Error("Error"); + }); + const pages = [ + { + path: join(__dirname, "./page-1.md"), + content: 0, + ancestors: [], + title: "Page 1", + }, + ]; + + // Act & Assert + // @ts-expect-error Transform method should throw an error with invalid content + await expect(transformer.transform(pages)).rejects.toThrow(); + }); + + it("should transform pages", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + { + title: "Page 2", + path: join(__dirname, "./page-2.md"), + relativePath: "./page-2.md", + content: "Page 2 content", + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining("

Page 1 content

"), + attachments: {}, + }, + { + title: "Page 2", + ancestors: [], + content: expect.stringContaining("

Page 2 content

"), + attachments: {}, + }, + ]); + }); + + it("should transform pages with internal references", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + { + title: "Page 2", + path: join(__dirname, "./page-2.md"), + relativePath: "./page-2.md", + content: "[Page 1](./page-1.md)", + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining("

Page 1 content

"), + attachments: {}, + }, + { + title: "Page 2", + ancestors: [], + content: expect.stringContaining( + '

', + ), + attachments: {}, + }, + ]); + }); + + it("should transform pages with internal references and strikethrough", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "~~strikethrough~~", + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining( + '

strikethrough

', + ), + attachments: {}, + }, + ]); + }); + + it("should support images", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: dedent` + # Hello world + + ![image](./image.png) + `, + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining( + '', + ), + attachments: expect.objectContaining({ + "image.png": expect.stringContaining("image.png"), + }), + }, + ]); + }); + + describe("transform page title", () => { + it("should transform page title with parents title", async () => { + // Arrange + const pages = [ + { + title: "Parent 1", + path: join(__dirname, "./parent/index.md"), + relativePath: "./parent/index.md", + content: "Parent 1 content", + ancestors: [], + }, + { + title: "Page 1", + path: join(__dirname, "./parent/page-1/index.md"), + relativePath: "./parent/page-1/index.md", + content: "Page 1 content", + ancestors: [join(__dirname, "./parent/index.md")], + }, + { + title: "Child 1", + path: join(__dirname, "./parent/page-1/child-1.md"), + relativePath: "./parent/page-1/child-1.md", + content: "Child 1 content", + ancestors: [ + join(__dirname, "./parent/index.md"), + join(__dirname, "./parent/page-1/index.md"), + ], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Parent 1", + ancestors: [], + content: expect.stringContaining("

Parent 1 content

"), + attachments: {}, + }, + { + title: "[Parent 1] Page 1", + ancestors: ["Parent 1"], + content: expect.stringContaining("

Page 1 content

"), + attachments: {}, + }, + { + title: "[Parent 1][Page 1] Child 1", + ancestors: ["Parent 1", "[Parent 1] Page 1"], + content: expect.stringContaining("

Child 1 content

"), + attachments: {}, + }, + ]); + }); + + it("should propagate parent name to children title", async () => { + // Arrange + const pages = [ + { + title: "Parent 1", + path: join(__dirname, "./parent/index.md"), + relativePath: "./parent/index.md", + content: "Parent 1 content", + ancestors: [], + name: "Root", + }, + { + title: "Title Page 1", + path: join(__dirname, "./parent/page-1/index.md"), + relativePath: "./parent/page-1/index.md", + content: "Page 1 content", + ancestors: [join(__dirname, "./parent/index.md")], + name: "Page 1", + }, + { + title: "Child 1", + path: join(__dirname, "./parent/page-1/child-1.md"), + relativePath: "./parent/page-1/child-1.md", + content: "Child 1 content", + ancestors: [ + join(__dirname, "./parent/index.md"), + join(__dirname, "./parent/page-1/index.md"), + ], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Parent 1", + ancestors: [], + content: expect.stringContaining("

Parent 1 content

"), + attachments: {}, + }, + { + title: "[Root] Title Page 1", + ancestors: ["Parent 1"], + content: expect.stringContaining("

Page 1 content

"), + attachments: {}, + }, + { + title: "[Root][Page 1] Child 1", + ancestors: ["Parent 1", "[Root] Title Page 1"], + content: expect.stringContaining("

Child 1 content

"), + attachments: {}, + }, + ]); + }); + }); + + it("should add root page name to children title", async () => { + // Arrange + const transformerWithRootPageName = new ConfluencePageTransformer({ + rootPageName: "Root", + spaceKey: "space-key", + }); + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + { + title: "Child 1", + path: join(__dirname, "./page-1/child-1.md"), + relativePath: "./page-1/child-1.md", + content: "Child 1 content", + ancestors: [join(__dirname, "./page-1.md")], + }, + ]; + + // Act + const transformedPages = await transformerWithRootPageName.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "[Root] Page 1", + ancestors: [], + content: expect.stringContaining("

Page 1 content

"), + attachments: {}, + }, + { + title: "[Root][Page 1] Child 1", + ancestors: ["[Root] Page 1"], + content: expect.stringContaining("

Child 1 content

"), + attachments: {}, + }, + ]); + }); + + describe("notice message", () => { + it("should add default notice message to page content", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + ]; + const transformerWithDefaultNoticeMessage = new ConfluencePageTransformer( + { + spaceKey: "space-key", + }, + ); + + // Act + const transformedPages = + await transformerWithDefaultNoticeMessage.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + attachments: {}, + content: expect.stringContaining( + "

AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost

", + ), + }, + ]); + }); + + it("should add notice message to page content", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + ]; + const noticeMessage = + "This page was generated from a markdown file. Do not edit it directly."; + const transformerWithNoticeMessage = new ConfluencePageTransformer({ + noticeMessage, + spaceKey: "space-key", + }); + + // Act + const transformedPages = + await transformerWithNoticeMessage.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + attachments: {}, + content: expect.stringContaining( + "

This page was generated from a markdown file. Do not edit it directly.

", + ), + }, + ]); + }); + + it("should throw error if notice template is invalid", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + ]; + const transformerWithNoticeMessage = new ConfluencePageTransformer({ + noticeTemplate: "{{relativePath}", + spaceKey: "space-key", + }); + + // Act + // Assert + await expect(() => + transformerWithNoticeMessage.transform(pages), + ).rejects.toThrow(InvalidTemplateError); + }); + + it("should render notice template to page content", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + ]; + const noticeTemplate = `Better visit https://foo/{{relativePathWithoutExtension}}. This page was generated from {{relativePath}} with title {{title}}. {{default}}`; + const transformerWithNoticeTemplate = new ConfluencePageTransformer({ + noticeTemplate, + spaceKey: "space-key", + }); + + // Act + const transformedPages = + await transformerWithNoticeTemplate.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + attachments: {}, + content: expect.stringContaining( + "

Better visit https://foo/./page-1. This page was generated from ./page-1.md with title Page 1. AUTOMATION NOTICE: This page is synced automatically, changes made manually will be lost

", + ), + }, + ]); + }); + + it("should render notice template with notice message to page content", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "Page 1 content", + ancestors: [], + }, + ]; + const noticeTemplate = `This page was generated from {{relativePath}} with title {{title}}. {{message}}`; + const noticeMessage = "Do not edit it directly."; + const transformerWithNoticeTemplate = new ConfluencePageTransformer({ + noticeTemplate, + noticeMessage, + spaceKey: "space-key", + }); + + // Act + const transformedPages = + await transformerWithNoticeTemplate.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + attachments: {}, + content: expect.stringContaining( + "

This page was generated from ./page-1.md with title Page 1. Do not edit it directly.

", + ), + }, + ]); + }); + }); + + describe("mermaid code blocks", () => { + let tmpDir: DirResult; + let mermaidCodeBlock: string; + let pages: ConfluenceSyncPage[]; + let transformedPages: ConfluenceInputPage[]; + let expectedMermaidCodeBlock: string; + + beforeEach(async () => { + tmpDir = dirSync({ unsafeCleanup: true }); + mermaidCodeBlock = dedent`\`\`\`mermaid + graph LR + Start --> Stop + \`\`\` + `; + expectedMermaidCodeBlock = remark() + .use(remarkGfm) + .use(remarkRehype) + .use(rehypeStringify) + .processSync(mermaidCodeBlock) + .toString(); + pages = [ + { + title: "Page 1", + path: join(tmpDir.name, "./page-1.md"), + relativePath: "./page-1.md", + content: mermaidCodeBlock, + ancestors: [], + }, + ]; + transformedPages = await transformer.transform(pages); + }); + + // FIXME: This test should throw error. + // It changes due to lack of time to debug possible issues with mermaid cli. + it("should throw error if mermaid code block is invalid", async () => { + // Arrange + const invalidPages = [ + { + title: "Page 1", + path: join(tmpDir.name, "./page-1.md"), + relativePath: "./page-1.md", + content: "```mermaid\ninvalid mermaid code block\n```", + ancestors: [], + }, + ]; + + // Act + // Assert + await expect(transformer.transform(invalidPages)).resolves.not.toThrow(); + }); + + // TODO: Enable this test when mermaid deps are fixed in the runners CI + // eslint-disable-next-line jest/no-disabled-tests + it.skip("should remove mermaid code block", async () => { + // Arrange + // Act + // Assert + expect(transformedPages[0].content).not.toContain( + expectedMermaidCodeBlock, + ); + }); + + // TODO: Enable this test when mermaid deps are fixed in the runners CI + // eslint-disable-next-line jest/no-disabled-tests + it.skip("should create mermaid svg diagram in file path", async () => { + // Arrange + // Act + // Assert + const autogeneratedImages = await glob("autogenerated-*.svg", { + cwd: resolve(tmpDir.name, "mermaid-diagrams"), + }); + + expect(autogeneratedImages).toHaveLength(1); + + const autogeneratedSvg = autogeneratedImages[0]; + + expect(autogeneratedSvg).toBeDefined(); + }); + + // TODO: Enable this test when images are enabled + it.todo("should add image link to page content"); + }); + + describe("details blocks", () => { + it("should replace a details tag", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: dedent` +
SummaryDetails
+ `, + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining( + 'Summary

Details

', + ), + attachments: {}, + }, + ]); + }); + + it("should throw error if details tag is missing summary", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: "
Details
", + ancestors: [], + }, + ]; + + // Act + // Assert + await expect(transformer.transform(pages)).rejects.toThrow(); + }); + + it("should replace nested details tags", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: dedent` +
+ Summary +
Summary 2Details 2
+
+ `, + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining( + 'SummarySummary 2

Details 2

', + ), + attachments: {}, + }, + ]); + }); + + it("should do nothing to other tags inside a details tag", async () => { + // Arrange + const pages = [ + { + title: "Page 1", + path: join(__dirname, "./page-1.md"), + relativePath: "./page-1.md", + content: dedent` +
+ Summary +

paragraph

+
+ `, + ancestors: [], + }, + ]; + + // Act + const transformedPages = await transformer.transform(pages); + + // Assert + expect(transformedPages).toEqual([ + { + title: "Page 1", + ancestors: [], + content: expect.stringContaining( + 'Summary

paragraph

', + ), + attachments: {}, + }, + ]); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-attachments-images.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-attachments-images.test.ts new file mode 100644 index 00000000..affc2e4b --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-attachments-images.test.ts @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +import { rehype } from "rehype"; +import rehypeStringify from "rehype-stringify"; +import remarkFrontmatter from "remark-frontmatter"; +import remarkParse from "remark-parse"; +import remarkParseFrontmatter from "remark-parse-frontmatter"; +import remarkRehype from "remark-rehype"; +import remarkStringify from "remark-stringify"; +import { toVFile } from "to-vfile"; +import { dedent } from "ts-dedent"; +import { unified } from "unified"; + +import rehypeAddAttachmentsImages from "@src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images"; + +describe("rehypeAddAttachmentsImages", () => { + it("should be defined", () => { + expect(rehypeAddAttachmentsImages).toBeDefined(); + }); + + it("should be return an empty images array", () => { + // Arrange + const markdown = dedent` +

Hello World

+ `; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkFrontmatter) + .use(remarkParseFrontmatter) + .use(remarkRehype) + .use(rehypeAddAttachmentsImages) + .use(rehypeStringify) + .processSync(markdown); + + // Assert + expect(file.data.images).toEqual({}); + }); + + it("should add attachments images with Images tag in images object", () => { + // Arrange + const html = dedent` + image.jpg + `; + + // Act + const file = rehype().use(rehypeAddAttachmentsImages).processSync(html); + + // eslint-disable-next-line jest/no-conditional-in-test + const base = file.dirname ? path.resolve(file.cwd, file.dirname) : file.cwd; + + // Assert + expect(file.value).toContain('image.jpg'); + expect(file.data.images).toEqual({ + "image.jpg": path.resolve(base, "./image.jpg"), + }); + }); + + it("should add attachments images without link", () => { + // Arrange + const html = dedent` + image.jpg + image-2.jpg + `; + + // Act + const file = rehype().use(rehypeAddAttachmentsImages).processSync(html); + + // eslint-disable-next-line jest/no-conditional-in-test + const base = file.dirname ? path.resolve(file.cwd, file.dirname) : file.cwd; + + // Assert + expect(file.value).toContain('image.jpg'); + expect(file.value).toContain('image-2.jpg'); + expect(file.data.images).toEqual({ + "image-2.jpg": path.resolve(base, "./image-2.jpg"), + "image.jpg": path.resolve(base, "./image.jpg"), + }); + }); + + it("dirname property in output files should be equal to provided dirname parameter", () => { + // Arrange + const html = dedent` + image.jpg + image-2.jpg + `; + + // Act + const file = rehype() + .use(rehypeAddAttachmentsImages) + .processSync(toVFile({ dirname: "images", path: "img", value: html })); + + // Assert + expect(file.dirname).toBe("images"); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-notice.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-notice.test.ts new file mode 100644 index 00000000..309e6a29 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-notice.test.ts @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { rehype } from "rehype"; + +import rehypeAddNotice from "@src/lib/confluence/transformer/support/rehype/rehype-add-notice"; + +describe("rehype-add-notice", () => { + it("should be defined", () => { + expect(rehypeAddNotice).toBeDefined(); + }); + + it("should add notice to the AST", () => { + const tree = ``; + + const actualTree = rehype() + .data("settings", { fragment: true }) + .use(rehypeAddNotice, { noticeMessage: "Notice message" }) + .processSync(tree) + .toString(); + + expect(actualTree).toEqual( + expect.stringContaining(`

Notice message

`), + ); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-remove-links.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-remove-links.test.ts new file mode 100644 index 00000000..366de876 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-remove-links.test.ts @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { rehype } from "rehype"; +import { dedent } from "ts-dedent"; + +import rehypeRemoveLinks from "@src/lib/confluence/transformer/support/rehype/rehype-remove-links"; + +describe("rehypeRemoveLinks", () => { + it("should be defined", () => { + expect(rehypeRemoveLinks).toBeDefined(); + }); + + it("should not remove other elements", () => { + // Arrange + const html = "

Hello world

"; + + // Act & Assert + expect( + rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: true, images: true }) + .processSync(html) + .toString(), + ).toContain("

Hello world

"); + }); + + it("should not remove any link if both options are false", () => { + // Arrange + const html = dedent` + External Link + Relative link + + `; + + // Act & Assert + expect( + rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: false, images: false }) + .processSync(html) + .toString(), + ).toContain(dedent` + External Link + Relative link + + `); + }); + + it("should remove images", () => { + // Arrange + const html = dedent` + External Link + Relative link + + `; + + // Act & Assert + expect( + rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: false, images: true }) + .processSync(html) + .toString(), + ).not.toContain(''); + }); + + it("should not remove links without href", () => { + // Arrange + const html = dedent` + External Link + Relative Link + `; + + // Act + const result = rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: true, images: false }) + .processSync(html) + .toString(); + + // Assert + expect(result).toContain("External Link"); + expect(result).toContain("Relative Link"); + }); + + it("should remove all links", () => { + // Arrange + const html = dedent` + External Link + Relative Link + `; + + // Act + const result = rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: true, images: false }) + .processSync(html) + .toString(); + + // Assert + expect(result).toContain("External Link"); + expect(result).toContain("Relative Link"); + }); + + it("should remove only external links", () => { + // Arrange + const html = dedent` + External Link + Relative Link + `; + + // Act + const result = rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: { external: true }, images: false }) + .processSync(html) + .toString(); + + // Assert + expect(result).toContain("External Link"); + expect(result).toContain('Relative Link'); + }); + + it("should remove only internal links", () => { + // Arrange + const html = dedent` + External Link + Relative Link + `; + + // Act + const result = rehype() + .data("settings", { fragment: true }) + .use(rehypeRemoveLinks, { anchors: { internal: true }, images: false }) + .processSync(html) + .toString(); + + // Assert + expect(result).toContain('External Link'); + expect(result).toContain("Relative Link"); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-details.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-details.test.ts new file mode 100644 index 00000000..c613b1dc --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-details.test.ts @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import rehypeParse from "rehype-parse"; +import rehypeStringify from "rehype-stringify"; +import { unified } from "unified"; + +import rehypeReplaceDetails from "@src/lib/confluence/transformer/support/rehype/rehype-replace-details"; + +describe("rehype-replace-details", () => { + it("should be defined", () => { + expect(rehypeReplaceDetails).toBeDefined(); + }); + + it("should replace
with Confluence expand macro", () => { + // Arrange + const html = "
GreetingsHi
"; + + // Act & Assert + expect( + unified() + .use(rehypeParse) + .use(rehypeStringify) + .use(rehypeReplaceDetails) + .processSync(html) + .toString(), + ).toContain( + 'Greetings

Hi

', + ); + }); + + it("should throw an error if
tag does not have a tag", () => { + // Arrange + const html = "
Hi
"; + + // Act & Assert + expect(() => + unified() + .use(rehypeParse) + .use(rehypeStringify) + .use(rehypeReplaceDetails) + .processSync(html), + ).toThrow("Invalid details tag. The details tag must have a summary tag."); + }); + + it("should replace
with Confluence expand macro with nested tags", () => { + // Arrange + const html = + "
Greetings
HiWorld
World
"; + + // Act & Assert + expect( + unified() + .use(rehypeParse) + .use(rehypeStringify) + .use(rehypeReplaceDetails) + .processSync(html) + .toString(), + ).toContain( + 'GreetingsHi

World

World

', + ); + }); + + it("should do nothing to other tags", () => { + // Arrange + const html = "

paragraph

"; + + // Act & Assert + expect( + unified() + .use(rehypeParse) + .use(rehypeStringify) + .use(rehypeReplaceDetails) + .processSync(html) + .toString(), + ).toContain("

paragraph

"); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-img-tags.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-img-tags.test.ts new file mode 100644 index 00000000..add897d8 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-img-tags.test.ts @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { unified } from "unified"; + +import rehypeReplaceImgTags from "@src/lib/confluence/transformer/support/rehype/rehype-replace-img-tags"; + +describe("rehype-replace-img-tags", () => { + it("should be defined", () => { + expect(rehypeReplaceImgTags).toBeDefined(); + }); + + it("should not replace img elements without src attribute", async () => { + const tree = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "img", + children: [], + }, + ], + }; + const expected = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "img", + children: [], + }, + ], + }; + const actual = await unified().use(rehypeReplaceImgTags).run(tree); + + expect(actual).toEqual(expected); + }); + + it("should replace elements with Confluence storage image macro", async () => { + const tree = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "img", + properties: { + src: "https://example.com/image.png", + }, + children: [], + }, + ], + }; + const expected = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "ac:image", + children: [ + { + type: "element" as const, + tagName: "ri:url", + properties: { + "ri:value": "https://example.com/image.png", + }, + children: [], + }, + ], + }, + ], + }; + const actual = await unified().use(rehypeReplaceImgTags).run(tree); + + expect(actual).toEqual(expected); + }); + + it("should replace elements with Confluence storage image macro for relative paths and not add width property because image doesn't svg", async () => { + const tree = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "img", + properties: { + src: "image.png", + }, + children: [], + }, + ], + }; + const expected = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "ac:image", + properties: {}, + children: [ + { + type: "element" as const, + tagName: "ri:attachment", + properties: { + "ri:filename": "image.png", + }, + children: [], + }, + ], + }, + ], + }; + const actual = await unified().use(rehypeReplaceImgTags).run(tree); + + expect(actual).toEqual(expected); + }); + + it("should replace elements with Confluence storage image macro for relative paths and add width property because the image name contains 'autogenerated' and is svg", async () => { + const tree = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "img", + properties: { + src: "autogenerated-image.svg", + }, + children: [], + }, + ], + }; + const expected = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "ac:image", + properties: { "ac:width": "1000" }, + children: [ + { + type: "element" as const, + tagName: "ri:attachment", + properties: { + "ri:filename": "autogenerated-image.svg", + }, + children: [], + }, + ], + }, + ], + }; + const actual = await unified().use(rehypeReplaceImgTags).run(tree); + + expect(actual).toEqual(expected); + }); + + it("should replace elements with Confluence storage image macro for relative paths but not add width property because the image name not contains 'autogenerated'", async () => { + const tree = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "img", + properties: { + src: "image.svg", + }, + children: [], + }, + ], + }; + const expected = { + type: "root" as const, + children: [ + { + type: "element" as const, + tagName: "ac:image", + properties: {}, + children: [ + { + type: "element" as const, + tagName: "ri:attachment", + properties: { + "ri:filename": "image.svg", + }, + children: [], + }, + ], + }, + ], + }; + const actual = await unified().use(rehypeReplaceImgTags).run(tree); + + expect(actual).toEqual(expected); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-internal-references.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-internal-references.test.ts new file mode 100644 index 00000000..ed92522c --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-internal-references.test.ts @@ -0,0 +1,256 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { join } from "path"; + +import { rehype } from "rehype"; +import { toVFile } from "to-vfile"; + +import type { ConfluenceSyncPage } from "@src/lib/confluence/ConfluenceSync.types"; +import rehypeReplaceInternalReferences from "@src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references"; + +describe("rehype-replace-internal-references", () => { + it("should be defined", () => { + expect(rehypeReplaceInternalReferences).toBeDefined(); + }); + + it("should replace internal references", () => { + // Arrange + const path = join(__dirname, "docs/bar.md"); + const input = `Foo`; + const expected = ``; + const spaceKey = "SPACE_KEY"; + const pagePath = join(__dirname, "docs/foo.md"); + const pageRelativePath = "docs/foo.md"; + const pages = new Map([ + [ + pagePath, + { + title: "Foo", + path: pagePath, + relativePath: pageRelativePath, + content: "", + ancestors: [], + }, + ], + ]); + + // Act + const actual = rehype() + .data("settings", { fragment: true, allowDangerousHtml: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should replace internal references one level above", () => { + // Arrange + const path = join(__dirname, "docs/bar/bar.md"); + const input = `Foo`; + const expected = ``; + const spaceKey = "SPACE_KEY"; + const pagePath = join(__dirname, "docs/foo.md"); + const pageRelativePath = "docs/foo.md"; + const pages = new Map([ + [ + pagePath, + { + title: "Foo", + path: pagePath, + relativePath: pageRelativePath, + content: "", + ancestors: [], + }, + ], + ]); + + // Act + const actual = rehype() + .data("settings", { fragment: true, allowDangerousHtml: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should replace internal references in one level down", () => { + // Arrange + const id = join(__dirname, "docs/bar.md"); + const input = `Foo`; + const expected = ``; + const spaceKey = "SPACE_KEY"; + const pagePath = join(__dirname, "docs/foo/foo.md"); + const pageRelativePath = "docs/foo/foo.md"; + const pages = new Map([ + [ + pagePath, + { + title: "Foo", + path: pagePath, + relativePath: pageRelativePath, + content: "", + ancestors: [], + }, + ], + ]); + + // Act + const actual = rehype() + .data("settings", { fragment: true, allowDangerousHtml: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path: id, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should replace internal references in sibling folder", () => { + // Arrange + const path = join(__dirname, "docs/bar/bar.md"); + const input = `Foo`; + const expected = ``; + const spaceKey = "SPACE_KEY"; + const pagePath = join(__dirname, "docs/foo/foo.md"); + const pageRelativePath = "docs/foo/foo.md"; + const pages = new Map([ + [ + pagePath, + { + title: "Foo", + path: pagePath, + relativePath: pageRelativePath, + content: "", + ancestors: [], + }, + ], + ]); + + // Act + const actual = rehype() + .data("settings", { fragment: true, allowDangerousHtml: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should replace references with no text", () => { + // Arrange + const path = join(__dirname, "docs/bar.md"); + const input = ``; + const expected = ``; + const spaceKey = "SPACE_KEY"; + const pagePath = join(__dirname, "docs/foo.md"); + const pageRelativePath = "docs/foo.md"; + const pages = new Map([ + [ + pagePath, + { + title: "Foo", + path: pagePath, + relativePath: pageRelativePath, + content: "", + ancestors: [], + }, + ], + ]); + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should not replace external references", () => { + // Arrange + const path = join(__dirname, "docs/bar.md"); + const input = `Foo`; + const expected = `Foo`; + const spaceKey = "SPACE_KEY"; + const pages = new Map(); + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should not replace references with no href", () => { + // Arrange + const path = join(__dirname, "docs/bar.md"); + const input = `Foo`; + const expected = `Foo`; + const spaceKey = "SPACE_KEY"; + const pages = new Map(); + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should not replace references to non-existing pages", () => { + // Arrange + const path = join(__dirname, "docs/bar.md"); + const input = `Foo`; + const expected = `Foo`; + const spaceKey = "SPACE_KEY"; + const pages = new Map(); + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceInternalReferences, { spaceKey, pages }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should remove missing pages", () => { + // Arrange + const path = join(__dirname, "docs/bar.md"); + const input = `Foo`; + const expected = `Foo`; + const spaceKey = "SPACE_KEY"; + const pages = new Map(); + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceInternalReferences, { + spaceKey, + pages, + removeMissing: true, + }) + .processSync(toVFile({ path, value: input })) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-strikethrough.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-strikethrough.test.ts new file mode 100644 index 00000000..3aec23e1 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-strikethrough.test.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import rehypeParse from "rehype-parse"; +import rehypeStringify from "rehype-stringify"; +import { unified } from "unified"; + +import rehypeReplaceStrikethrough from "@src/lib/confluence/transformer/support/rehype/rehype-replace-strikethrough"; + +describe("rehype-replace-strikethrough", () => { + it("should be defined", () => { + expect(rehypeReplaceStrikethrough).toBeDefined(); + }); + + it('should replace with ', () => { + // Arrange + const html = "deleted"; + + // Act & Assert + expect( + unified() + .use(rehypeParse) + .use(rehypeStringify) + .use(rehypeReplaceStrikethrough) + .processSync(html) + .toString(), + ).toContain('deleted'); + }); + + it("should do nothing to other tags", () => { + // Arrange + const html = "

paragraph

"; + + // Act & Assert + expect( + unified() + .use(rehypeParse) + .use(rehypeStringify) + .use(rehypeReplaceStrikethrough) + .processSync(html) + .toString(), + ).toContain("

paragraph

"); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-task-list.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-task-list.test.ts new file mode 100644 index 00000000..9dc3b711 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-task-list.test.ts @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { rehype } from "rehype"; +import { dedent } from "ts-dedent"; + +import rehypeReplaceTaskList from "@src/lib/confluence/transformer/support/rehype/rehype-replace-task-list"; + +describe("rehype-replace-task-list", () => { + it("should be defined", () => { + expect(rehypeReplaceTaskList).toBeDefined(); + }); + + it("should replace task lists with correct task status", () => { + // Arrange + const input = dedent` +
    +
  • Telefonica ID number
  • +
  • Office 365
  • +
+ `; + const expected = dedent` + + complete Telefonica ID number + incomplete Office 365 + + `; + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceTaskList) + .processSync(input) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should replace task lists with nested lists", () => { + // Arrange + const input = dedent` +
    +
  • Telefonica ID number +
      +
    • Office 365
    • +
    +
  • +
+ `; + const expected = dedent` + + complete Telefonica ID number + + incomplete Office 365 + + + + `; + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceTaskList) + .processSync(input) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); + + it("should not replace non-task lists", () => { + // Arrange + const input = dedent` +
    +
  • no checkbox
  • +
+ `; + const expected = dedent` +
    +
  • no checkbox
  • +
+ `; + + // Act + const actual = rehype() + .data("settings", { fragment: true }) + .use(rehypeReplaceTaskList) + .processSync(input) + .toString(); + + // Assert + expect(actual).toEqual(expected); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-footnotes.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-footnotes.test.ts new file mode 100644 index 00000000..ffb2aec1 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-footnotes.test.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { remark } from "remark"; +import remarkGfm from "remark-gfm"; +import { dedent } from "ts-dedent"; + +import remarkRemoveFootnotes from "@src/lib/confluence/transformer/support/remark/remark-remove-footnotes"; + +describe("remark-remove-footnotes", () => { + it("should be defined", () => { + expect(remarkRemoveFootnotes).toBeDefined(); + }); + + it("should remove footnotes", () => { + // Arrange + const input = dedent` + This is a paragraph with a footnote[^1]. + + [^1]: This is a footnote. + `; + + // Act + const actual = remark() + .use(remarkGfm) + .use(remarkRemoveFootnotes) + .processSync(input) + .toString(); + + // Assert + expect(actual).toContain(`This is a paragraph with a footnote.`); + expect(actual).not.toContain(`[^1]: This is a footnote.`); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.test.ts new file mode 100644 index 00000000..bc4469d8 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.test.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { remark } from "remark"; +import remarkGfm from "remark-gfm"; +import { dedent } from "ts-dedent"; + +import remarkRemoveMdxCodeBlocks from "@src/lib/confluence/transformer/support/remark/remark-remove-mdx-code-blocks"; + +describe("remark-remove-mdx-code-blocks", () => { + it("should be defined", () => { + expect(remarkRemoveMdxCodeBlocks).toBeDefined(); + }); + + it("should remove mdx-code-block", () => { + // Arrange + const mdxCodeBlock = dedent` + \`\`\`mdx-code-block +

Hello World

+ \`\`\` + `; + const input = dedent` + This is a paragraph with mdx code block + ${mdxCodeBlock} + `; + + // Act + const actual = remark() + .use(remarkGfm) + .use(remarkRemoveMdxCodeBlocks) + .processSync(input) + .toString(); + + // Assert + expect(actual).toContain(`This is a paragraph with mdx code block`); + expect(actual).not.toContain(`Code block test`); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-replace-mermaid.test.ts b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-replace-mermaid.test.ts new file mode 100644 index 00000000..546c3fe0 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-replace-mermaid.test.ts @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { glob } from "glob"; +import { remark } from "remark"; +import remarkGfm from "remark-gfm"; +import type { DirResult, FileResult } from "tmp"; +import { readSync } from "to-vfile"; +import { dedent } from "ts-dedent"; +import type { VFile } from "vfile"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync, fileSync } = new TempFiles(); + +import remarkReplaceMermaid from "@src/lib/confluence/transformer/support/remark/remark-replace-mermaid"; + +describe("remark-replace-mermaid", () => { + let tmpDir: DirResult; + + beforeEach(() => { + tmpDir = dirSync({ unsafeCleanup: true }); + }); + + it("should be defined", () => { + expect(remarkReplaceMermaid).toBeDefined(); + }); + + it("should not replace no mermaid code", () => { + // Arrange + const input = "This is a paragraph with no mermaid code"; + + // Act + const actual = remark() + .use(remarkGfm) + .use(remarkReplaceMermaid, { outDir: tmpDir.name }) + .processSync(input) + .toString(); + + // Assert + expect(actual).toContain(input); + }); + + // FIXME: This test should throw error. + // It changes due to lack of time to debug possible issues with mermaid cli. + it("should not replace invalid mermaid code", () => { + // Arrange + const input = dedent`\`\`\`mermaid + unknown + \`\`\``; + + // Act & Assert + expect(() => + remark() + .use(remarkGfm) + .use(remarkReplaceMermaid, { outDir: tmpDir.name }) + .processSync(input), + ).not.toThrow(); + }); + + // TODO: Enable this test when mermaid deps are fixed in the runners CI + // eslint-disable-next-line jest/no-disabled-tests + describe.skip("mermaid code", () => { + let content: string; + let vFile: VFile; + let file: FileResult; + + beforeEach(() => { + content = dedent`\`\`\`mermaid + graph LR + A-->B + \`\`\``; + file = fileSync({ dir: tmpDir.name, postfix: ".md" }); + writeFileSync(file.name, content); + vFile = readSync(file.name); + }); + + it("should replace mermaid code", () => { + // Arrange + // Act + const actual = remark() + .use(remarkGfm) + .use(remarkReplaceMermaid, { outDir: tmpDir.name }) + .processSync(vFile) + .toString(); + + // Assert + expect(actual).not.toContain(content); + }); + + it("should create an image in given directory", async () => { + // Arrange + // Act + const result = remark() + .use(remarkGfm) + .use(remarkReplaceMermaid, { outDir: tmpDir.name }) + .processSync(vFile) + .toString(); + + // Assert + const autogeneratedImages = await glob("autogenerated-*.svg", { + cwd: tmpDir.name, + }); + + expect(autogeneratedImages).toHaveLength(1); + + const autogeneratedSvg = autogeneratedImages[0]; + + expect(autogeneratedSvg).toBeDefined(); + expect(result).toContain(`![](${join(tmpDir.name, autogeneratedSvg)})`); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPages.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPages.test.ts new file mode 100644 index 00000000..e3dc5beb --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPages.test.ts @@ -0,0 +1,779 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { writeFileSync } from "node:fs"; +import { basename, join, resolve } from "node:path"; + +import type { ConfigInterface } from "@mocks-server/config"; +import { Config } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import type { DirResult } from "tmp"; +import { dedent } from "ts-dedent"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync, fileSync } = new TempFiles(); + +import type { + DocusaurusPagesInterface, + DocusaurusPagesOptions, + FilesPatternOption, + ModeOption, +} from "@src/lib"; +import { DocusaurusPages } from "@src/lib/docusaurus/DocusaurusPages"; +import * as typesValidations from "@src/lib/support/typesValidations"; + +const CONFIG = { + config: { + readArguments: false, + readFile: false, + readEnvironment: false, + }, +}; + +describe("docusaurusPages", () => { + let dir: DirResult; + let config: ConfigInterface; + let logger: LoggerInterface; + let docusaurusPagesOptions: DocusaurusPagesOptions; + + beforeEach(async () => { + dir = dirSync({ unsafeCleanup: true }); + config = new Config({ + moduleName: "markdown-confluence-sync", + }); + config.addOption({ + name: "mode", + type: "string", + default: "tree", + }); + config.addOption({ + name: "filesPattern", + type: "string", + default: "", + }); + logger = new Logger("", { level: "silent" }); + docusaurusPagesOptions = { + config, + logger, + mode: config.option("mode") as ModeOption, + }; + }); + + afterEach(() => { + dir.removeCallback(); + }); + + it("should be defined", () => { + expect(DocusaurusPages).toBeDefined(); + }); + + describe("read", () => { + it("should initialized once", async () => { + // Arrange + const docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: dir.name, + }); + + // Act + await docusaurusPages.read(); + await docusaurusPages.read(); + + // Assert + expect(docusaurusPages).toBeInstanceOf(DocusaurusPages); + }); + + it("should fail if the directory does not exist", async () => { + // Arrange + const docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: "foo", + }); + + // Act + // Assert + await expect(async () => await docusaurusPages.read()).rejects.toThrow( + `Path ${resolve(process.cwd(), "foo")} does not exist`, + ); + }); + + it("should build a tree from a directory", async () => { + // Arrange + const docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: dir.name, + }); + + // Act + // Assert + expect(docusaurusPages).toBeInstanceOf(DocusaurusPages); + }); + + describe("with valid directory", () => { + let docusaurusPages: DocusaurusPagesInterface; + + beforeEach(async () => { + docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: dir.name, + }); + }); + + it("should ignore index.md file in the root directory", async () => { + // Arrange + const file = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Category + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const flattened = await docusaurusPages.read(); + + // Assert + expect(flattened).toHaveLength(0); + }); + + it("should ignore files in the root directory that are not configured to be synced to confluence", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name }); + const file = fileSync({ dir: categoryDir.name, name: "index.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Category + sync_to_confluence: false + --- + + # Hello World + `, + ); + const page = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + page.name, + dedent` + --- + title: Page + sync_to_confluence: false + --- + + # Hello World + `, + ); + + // Act + const flattened = await docusaurusPages.read(); + + // Assert + expect(flattened).toHaveLength(0); + }); + + it("should return a list of DocusaurusPage from the root directory files and subdirectories", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name, name: "category" }); + const categoryIndex = fileSync({ + dir: categoryDir.name, + name: "index.md", + }); + writeFileSync( + categoryIndex.name, + dedent` + --- + title: Category + sync_to_confluence: true + --- + + # Hello World + `, + ); + const file = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(2); + expect(pages[0].path).toBe(join(categoryDir.name, "index.md")); + expect(pages[0].relativePath).toBe(join("category", "index.md")); + expect(pages[0].title).toBe("Category"); + expect(pages[0].content).toContain("Hello World"); + expect(pages[0].ancestors).toEqual([]); + expect(pages[1].path).toBe(file.name); + expect(pages[1].relativePath).toBe("page.md"); + expect(pages[1].title).toBe("Page"); + expect(pages[1].content).toContain("Hello World"); + expect(pages[1].ancestors).toEqual([]); + }); + + it("should return a list of DocusaurusPage from the root directory subdirectories with ancestors", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name, name: "category" }); + const categoryIndex = fileSync({ + dir: categoryDir.name, + name: "index.md", + }); + writeFileSync( + categoryIndex.name, + dedent` + --- + title: Category + sync_to_confluence: true + confluence_short_name: Cat. + --- + + # Hello World + `, + ); + const categoryPage = fileSync({ + dir: categoryDir.name, + name: "page.md", + }); + writeFileSync( + categoryPage.name, + dedent` + --- + title: Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + const subcategoryDir = dirSync({ + dir: categoryDir.name, + name: "subcategory", + }); + const subcategoryIndex = fileSync({ + dir: subcategoryDir.name, + name: "index.md", + }); + writeFileSync( + subcategoryIndex.name, + dedent` + --- + title: Subcategory + sync_to_confluence: true + confluence_short_name: Sub-cat. + --- + + # Hello World + `, + ); + const subcategoryPage = fileSync({ + dir: subcategoryDir.name, + name: "page.md", + }); + writeFileSync( + subcategoryPage.name, + dedent` + --- + title: Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(4); + + expect(pages[0].path).toBe(join(categoryDir.name, "index.md")); + expect(pages[0].relativePath).toBe(join("category", "index.md")); + expect(pages[0].title).toBe("Category"); + expect(pages[0].content).toContain("Hello World"); + expect(pages[0].ancestors).toEqual([]); + expect(pages[0].name).toBe("Cat."); + + expect(pages[1].path).toBe(categoryPage.name); + expect(pages[1].relativePath).toBe(join("category", "page.md")); + expect(pages[1].title).toBe("Page"); + expect(pages[1].content).toContain("Hello World"); + expect(pages[1].ancestors).toEqual([ + join(categoryDir.name, "index.md"), + ]); + + expect(pages[2].path).toBe(join(subcategoryDir.name, "index.md")); + expect(pages[2].relativePath).toBe( + join("category", "subcategory", "index.md"), + ); + expect(pages[2].title).toBe("Subcategory"); + expect(pages[2].content).toContain("Hello World"); + expect(pages[2].ancestors).toEqual([ + join(categoryDir.name, "index.md"), + ]); + expect(pages[2].name).toBe("Sub-cat."); + + expect(pages[3].path).toBe(subcategoryPage.name); + expect(pages[3].relativePath).toBe( + join("category", "subcategory", "page.md"), + ); + expect(pages[3].title).toBe("Page"); + expect(pages[3].content).toContain("Hello World"); + expect(pages[3].ancestors).toEqual([ + join(categoryDir.name, "index.md"), + join(subcategoryDir.name, "index.md"), + ]); + }); + + it("should fail if the short name is an empty string in the frontmatter", async () => { + const file = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Page + sync_to_confluence: true + confluence_short_name: "" + --- + + # Hello World + `, + ); + + await expect(docusaurusPages.read()).rejects.toThrow( + expect.objectContaining({ + name: "Error", + message: expect.stringContaining("Invalid markdown format: "), + }), + ); + }); + + it("should prioritize the confluence_title from the frontmatter over the title", async () => { + // Arrange + const file = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Page + confluence_title: Confluence Title + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(1); + expect(pages[0].title).toBe("Confluence Title"); + }); + + it("should fail if the confluence_title is an empty string in the frontmatter", async () => { + const file = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Page + confluence_title: "" + sync_to_confluence: true + --- + + # Hello World + `, + ); + + await expect(docusaurusPages.read()).rejects.toThrow( + expect.objectContaining({ + name: "Error", + message: expect.stringContaining("Invalid markdown format: "), + }), + ); + }); + }); + + describe("when file is mdx", () => { + let docusaurusPages: DocusaurusPagesInterface; + + beforeEach(async () => { + docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: dir.name, + }); + }); + + it("should read pages in mdx files directory", async () => { + // Arrange + const mdxFileDir = dirSync({ dir: dir.name, name: "mdx-files" }); + const indexFile = fileSync({ dir: mdxFileDir.name, name: "index.mdx" }); + writeFileSync( + indexFile.name, + dedent` + --- + title: File Mdx + sync_to_confluence: true + --- + + # Hello World + `, + ); + + const mdxPageDir = fileSync({ + dir: mdxFileDir.name, + name: "mdxPage.mdx", + }); + writeFileSync( + mdxPageDir.name, + dedent` + --- + title: Mdx Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(2); + }); + }); + + describe("index files", () => { + describe.each([ + "README.md", + "README.mdx", + "directory-name.md", + "directory-name.mdx", + ])("when index file is %s", (indexFileName) => { + let docusaurusPages: DocusaurusPagesInterface; + + beforeEach(async () => { + docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: dir.name, + }); + }); + + it(`should read a ${indexFileName} file in the directory as index file`, async () => { + // Arrange + const readmeDir = dirSync({ dir: dir.name, name: "directory-name" }); + const indexFile = fileSync({ + dir: readmeDir.name, + name: indexFileName, + }); + writeFileSync( + indexFile.name, + dedent` + --- + title: Title + sync_to_confluence: true + --- + + # Hello World + `, + ); + + const mdxPageDir = fileSync({ + dir: readmeDir.name, + name: "mdxPage.mdx", + }); + writeFileSync( + mdxPageDir.name, + dedent` + --- + title: Mdx Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(2); + expect(pages[0].ancestors).toEqual([]); + expect(pages[1].ancestors).toEqual([ + join(readmeDir.name, indexFileName), + ]); + }); + }); + + describe("when there are multiple index files in the directory", () => { + let docusaurusPages: DocusaurusPagesInterface; + + beforeEach(async () => { + logger = new Logger("docusaurus-pages-index-files", { + level: "warn", + }); + docusaurusPagesOptions = { + config, + logger, + mode: config.option("mode") as ModeOption, + }; + docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + docsDir: dir.name, + }); + }); + + it("should read the file with higher order of precedence as index file", async () => { + // Arrange + const exampleDir = dirSync({ dir: dir.name, name: "example" }); + const indexFile = fileSync({ + dir: exampleDir.name, + name: "index.md", + }); + writeFileSync( + indexFile.name, + dedent` + --- + title: index.md + sync_to_confluence: true + --- + # Hello World + `, + ); + + const indexFile2 = fileSync({ + dir: exampleDir.name, + name: "example.md", + }); + writeFileSync( + indexFile2.name, + dedent` + --- + title: example.md + sync_to_confluence: true + --- + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(1); + expect(pages[0].title).toBe("index.md"); + expect(pages[0].ancestors).toEqual([]); + }); + + it("should log a warning message", async () => { + // Arrange + const exampleDir = dirSync({ dir: dir.name, name: "example" }); + const indexFile = fileSync({ + dir: exampleDir.name, + name: "index.md", + }); + writeFileSync( + indexFile.name, + dedent` + --- + title: index.md + sync_to_confluence: true + --- + # Hello World + `, + ); + + const indexFile2 = fileSync({ + dir: exampleDir.name, + name: "example.md", + }); + writeFileSync( + indexFile2.name, + dedent` + --- + title: example.md + sync_to_confluence: true + --- + # Hello World + `, + ); + + // Act + await docusaurusPages.read(); + + // Assert + expect(logger.globalStore).toEqual( + expect.arrayContaining([ + expect.stringContaining( + `Multiple index files found in ${basename( + exampleDir.name, + )} directory. Using ${basename(indexFile.name)} as index file. Ignoring the rest.`, + ), + ]), + ); + }); + }); + }); + + describe("when flat mode is active", () => { + let docusaurusPages: DocusaurusPagesInterface; + + it("should throw an error when 'filesPattern' option is empty", async () => { + docusaurusPages = new DocusaurusPages(docusaurusPagesOptions); + await config.load({ + ...CONFIG, + mode: "flat", + }); + + // Assert + await expect(async () => await docusaurusPages.read()).rejects.toThrow( + `File pattern can't be empty in flat mode`, + ); + }); + + it("should read the pages whose filenames match the glob pattern provided in the filesPattern option", async () => { + const spy = jest.spyOn(process, "cwd"); + spy.mockReturnValue(dir.name); + + // Arrange + await config.load({ + ...CONFIG, + mode: "flat", + filesPattern: "**/page*", + }); + docusaurusPages = new DocusaurusPages({ + ...docusaurusPagesOptions, + filesPattern: config.option("filesPattern") as FilesPatternOption, + }); + + const categoryDir = dirSync({ dir: dir.name, name: "category" }); + const categoryIndex = fileSync({ + dir: categoryDir.name, + name: "index.md", + }); + writeFileSync( + categoryIndex.name, + dedent` + --- + title: Category + sync_to_confluence: true + confluence_short_name: Cat. + --- + + # Hello World + `, + ); + const categoryPage = fileSync({ + dir: categoryDir.name, + name: "page.md", + }); + writeFileSync( + categoryPage.name, + dedent` + --- + title: Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + const subcategoryDir = dirSync({ + dir: categoryDir.name, + name: "subcategory", + }); + const subcategoryIndex = fileSync({ + dir: subcategoryDir.name, + name: "index.md", + }); + writeFileSync( + subcategoryIndex.name, + dedent` + --- + title: Subcategory + sync_to_confluence: true + confluence_short_name: Sub-cat. + --- + + # Hello World + `, + ); + const subcategoryPage = fileSync({ + dir: subcategoryDir.name, + name: "page.mdx", + }); + writeFileSync( + subcategoryPage.name, + dedent` + --- + title: Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const pages = await docusaurusPages.read(); + + // Assert + expect(pages).toHaveLength(2); + }); + + it("when DocusaurusPages read method is called twice, the initial validation should be called only once", async () => { + // Arrange + const spy = jest.spyOn(process, "cwd"); + spy.mockReturnValue(dir.name); + + const spyIsStringWithLength = jest.spyOn( + typesValidations, + "isStringWithLength", + ); + + // Arrange + await config.load({ + ...CONFIG, + mode: "flat", + filesPattern: "**/page*", + }); + docusaurusPages = new DocusaurusPages({ + ...docusaurusPagesOptions, + filesPattern: config.option("filesPattern") as FilesPatternOption, + }); + + // Act + await docusaurusPages.read(); + await docusaurusPages.read(); + + // Assert + expect(spyIsStringWithLength).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPagesFactory.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPagesFactory.test.ts new file mode 100644 index 00000000..3efaf6e9 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPagesFactory.test.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { + ConfigInterface, + ConfigNamespaceInterface, +} from "@mocks-server/config"; +import { Config } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import type { SyncModes } from "@tid-cross/confluence-sync"; + +import { DocusaurusPagesFactory } from "@src/lib/docusaurus/DocusaurusPagesFactory"; +import type { DocusaurusPagesFactoryOptions } from "@src/lib/docusaurus/DocusaurusPagesFactory.types"; + +describe("docusaurusPagesFactory", () => { + let config: ConfigInterface; + let namespace: ConfigNamespaceInterface; + let logger: LoggerInterface; + let docusaurusPagesOptions: DocusaurusPagesFactoryOptions; + + beforeEach(async () => { + config = new Config({ + moduleName: "markdown-confluence-sync", + }); + namespace = config.addNamespace("docusaurus"); + logger = new Logger("", { level: "silent" }); + // @ts-expect-error Ignore to check different value for mode option + docusaurusPagesOptions = { config: namespace, logger }; + }); + + it(`should throw error with text "must be one of "tree" or "flat"" when docusaurus mode isn't valid mode`, async () => { + await expect(async () => + DocusaurusPagesFactory.fromMode( + "foo" as SyncModes, + docusaurusPagesOptions, + ), + ).rejects.toThrow( + expect.objectContaining({ + message: expect.stringContaining(`must be one of "tree" or "flat"`), + }), + ); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-admonitions.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-admonitions.test.ts new file mode 100644 index 00000000..6411db25 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-admonitions.test.ts @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import remarkDirective from "remark-directive"; +import remarkParse from "remark-parse"; +import remarkStringify from "remark-stringify"; +import { dedent } from "ts-dedent"; +import { unified } from "unified"; + +import remarkReplaceAdmonitions from "@src/lib/docusaurus/pages/support/remark/remark-replace-admonitions"; + +describe("remarkReplaceAdmonitions", () => { + it("should be defined", () => { + expect(remarkReplaceAdmonitions).toBeDefined(); + }); + + it("should remove admonitions in single paragraph", () => { + // Arrange + const markdown = `:::note +This is a note +:::`; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkDirective) + .use(remarkReplaceAdmonitions) + .processSync(markdown); + + // Assert + expect(file.toString()).toEqual( + expect.stringContaining(dedent`> **Note:** + > + > This is a note + `), + ); + }); + + it("should remove admonitions with internal nodes in single paragraph", () => { + // Arrange + const markdown = dedent`:::note + **Hello World** + :::`; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkDirective) + .use(remarkReplaceAdmonitions) + .processSync(markdown); + + // Assert + expect(file.toString()).toEqual( + expect.stringContaining(dedent`> **Note:** + > + > **Hello World** + `), + ); + }); + + it("should remove admonitions between multiple paragraphs", () => { + // Arrange + const markdown = dedent`:::note + This is the first part of a note + + This is the second part of a note + :::`; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkDirective) + .use(remarkReplaceAdmonitions) + .processSync(markdown); + + // Assert + expect(file.toString()).toEqual( + expect.stringContaining(dedent`> **Note:** + > + > This is the first part of a note + > + > This is the second part of a note + `), + ); + }); + + it("should replace endless admonition", () => { + // Arrange + const markdown = dedent`:::note + This is the first part of a note`; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkDirective) + .use(remarkReplaceAdmonitions) + .processSync(markdown); + + // Assert + expect(file.toString()).toEqual( + expect.stringContaining(dedent`> **Note:** + > + > This is the first part of a note + `), + ); + }); + + it("should replace admonition with title", () => { + // Arrange + const markdown = dedent`:::note[Note] + This is a note with title + :::`; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkDirective) + .use(remarkReplaceAdmonitions) + .processSync(markdown); + + // Assert + expect(file.toString()).toEqual( + expect.stringContaining(dedent`> **Note: Note** + > + > This is a note with title + `), + ); + }); + + describe("admonition types", () => { + it.each([ + ["note", "Note"], + ["tip", "Tip"], + ["info", "Info"], + ["caution", "Caution"], + ["danger", "Danger"], + ])("should replace admonition type %s with %s", (type, expected) => { + // Arrange + const markdown = dedent`:::${type} + This is a ${type} + :::`; + + // Act + const file = unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkDirective) + .use(remarkReplaceAdmonitions) + .processSync(markdown); + + // Assert + expect(file.toString()).toEqual( + expect.stringContaining(dedent`> **${expected}:** + > + > This is a ${type} + `), + ); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-tabs.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-tabs.test.ts new file mode 100644 index 00000000..77fca0ee --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-tabs.test.ts @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { remark } from "remark"; +import remarkMdx from "remark-mdx"; +import { dedent } from "ts-dedent"; + +import { InvalidTabItemMissingLabelError } from "@src/lib/docusaurus/pages/errors/InvalidTabItemMissingLabelError"; +import { InvalidTabsFormatError } from "@src/lib/docusaurus/pages/errors/InvalidTabsFormatError"; +import remarkReplaceTabs from "@src/lib/docusaurus/pages/support/remark/remark-replace-tabs"; + +describe("remarkReplaceTabs", () => { + it("should be defined", () => { + expect(remarkReplaceTabs).toBeDefined(); + }); + + it("should replace tabs", () => { + // Arrange + const tabs = ` + + +Tab Item Content + + +`; + + // Act + const file = remark() + .use(remarkMdx) + .use(remarkReplaceTabs) + .processSync(tabs); + + // Assert + expect(file.toString()).toContain(dedent`* File tree + + Tab Item Content`); + }); + + it("should throw InvalidTabsFormatError when Tabs tag does not have only TabItem children", () => { + // Arrange + const tabs = ` + + +Tab Item Content + + + +`; + + // Act + expect(() => { + remark().use(remarkMdx).use(remarkReplaceTabs).processSync(tabs); + }).toThrow(new InvalidTabsFormatError()); + }); + + it("should throw InvalidTabsFormatError when TabItem tag does not have a label property", () => { + // Arrange + const tabs = ` + + +Tab Item Content + + +`; + + // Act + expect(() => { + remark().use(remarkMdx).use(remarkReplaceTabs).processSync(tabs); + }).toThrow(new InvalidTabItemMissingLabelError()); + }); + + it("should replace tabs with nested tabs", () => { + // Arrange + const tabs = ` + + +Tab Item Content + + +Tab Inside + + + + +Tab Item Content 2 + + +`; + + // Act + const file = remark() + .use(remarkMdx) + .use(remarkReplaceTabs) + .processSync(tabs); + + // Assert + expect(file.toString()).toContain(dedent`* File tree + + Tab Item Content + + * File tree + + Tab Inside + + * File tree 2 + + Tab Item Content 2`); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-transform-details.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-transform-details.test.ts new file mode 100644 index 00000000..3beafab8 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-transform-details.test.ts @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { remark } from "remark"; +import remarkMdx from "remark-mdx"; +import { dedent } from "ts-dedent"; + +import remarkRemoveMdxCode from "@src/lib/docusaurus/pages/support/remark/remark-remove-mdx-code"; +import remarkTransformDetails from "@src/lib/docusaurus/pages/support/remark/remark-transform-details"; + +describe("remarkTransformDetails", () => { + it("should be defined", () => { + expect(remarkTransformDetails).toBeDefined(); + }); + + it("should prevent details tags from being removed", () => { + // Arrange + const details = ` + This block should be removed + + + + Tab 1 Content + + + Tab 2 Content + + + + This block shouldn't be removed + +
+ Details title + Details Content +
+ `; + + // Act + const file = remark() + .use(remarkMdx) + .use(remarkTransformDetails) + .use(remarkRemoveMdxCode) + .processSync(details); + + // Assert + expect(file.toString()).toContain(dedent`This block should be removed + + This block shouldn't be removed + +
+ Details title + Details Content +
`); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-validate-frontmatter.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-validate-frontmatter.test.ts new file mode 100644 index 00000000..ae593505 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-validate-frontmatter.test.ts @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import remarkFrontmatter from "remark-frontmatter"; +import remarkParse from "remark-parse"; +import remarkParseFrontmatter from "remark-parse-frontmatter"; +import remarkStringify from "remark-stringify"; +import { dedent } from "ts-dedent"; +import { unified } from "unified"; +import { VFile } from "vfile"; + +import remarkValidateFrontmatter from "@src/lib/docusaurus/pages/support/remark/remark-validate-frontmatter"; +import { FrontMatterValidator } from "@src/lib/docusaurus/pages/support/validators/FrontMatterValidator"; + +describe("remark-validate-frontmatter", () => { + it("should be defined", () => { + expect(remarkValidateFrontmatter).toBeDefined(); + }); + + it("should fail if the file fails FrontMatter validation", () => { + // Arrange + const invalidMarkdown = dedent` + --- + --- + + # My Title + `; + + // Act + // Assert + expect(() => + unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkFrontmatter) + .use(remarkParseFrontmatter) + .use(remarkValidateFrontmatter, FrontMatterValidator) + .processSync(new VFile(invalidMarkdown)), + ).toThrow(); + }); + + it("should fail if it does not include a FrontMatter validation", () => { + // Arrange + const invalidMarkdown = dedent` + --- + --- + + # My Title + `; + + // Act + // Assert + expect(() => + unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkFrontmatter) + .use(remarkParseFrontmatter) + .use(remarkValidateFrontmatter) + .processSync(new VFile(invalidMarkdown)), + ).toThrow(); + }); + + it("should process if the file success FrontMatter validation", () => { + // Arrange + const invalidMarkdown = dedent` + --- + title: My Title + --- + + # My Title + `; + + // Act + // Assert + expect(() => + unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkFrontmatter) + .use(remarkParseFrontmatter) + .use(remarkValidateFrontmatter, FrontMatterValidator) + .processSync(new VFile(invalidMarkdown)), + ).not.toThrow(); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTree.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTree.test.ts new file mode 100644 index 00000000..db6bd37b --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTree.test.ts @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { writeFileSync } from "fs"; +import { join } from "node:path"; + +import type { LogMessage } from "@mocks-server/logger"; +import { Logger } from "@mocks-server/logger"; +import type { DirResult } from "tmp"; +import { dedent } from "ts-dedent"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync, fileSync } = new TempFiles(); + +import { DocusaurusDocTree } from "@src/lib/docusaurus/tree/DocusaurusDocTree"; + +describe("docusaurusDocTree", () => { + let dir: DirResult; + + beforeEach(() => { + dir = dirSync({ unsafeCleanup: true }); + }); + + afterEach(() => { + dir.removeCallback(); + }); + + it("should be defined", () => { + expect(DocusaurusDocTree).toBeDefined(); + }); + + it("should fail if the directory does not exist", () => { + expect(() => new DocusaurusDocTree("foo")).toThrow( + "Path foo does not exist", + ); + }); + + it("should build a tree from a directory", () => { + expect(new DocusaurusDocTree(dir.name)).toBeInstanceOf(DocusaurusDocTree); + }); + + describe("flattened", () => { + it("should return a list of DocusaurusDocTreeItem from the root directory files and subdirectories", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name, name: "category" }); + const categoryIndex = fileSync({ + dir: categoryDir.name, + name: "index.md", + }); + writeFileSync( + categoryIndex.name, + dedent` + --- + title: Category + sync_to_confluence: true + --- + + # Hello World + `, + ); + const file = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const tree = new DocusaurusDocTree(dir.name); + const flattened = await tree.flatten(); + + // Assert + expect(flattened).toHaveLength(2); + expect(flattened[0].path).toBe(join(categoryDir.name, "index.md")); + expect(flattened[0].isCategory).toBe(true); + expect(flattened[0].meta.title).toBe("Category"); + expect(flattened[0].content).toContain("Hello World"); + expect(flattened[1].path).toBe(file.name); + expect(flattened[1].isCategory).toBe(false); + expect(flattened[1].meta.title).toBe("Page"); + expect(flattened[1].content).toContain("Hello World"); + }); + + it("should ignore index.md in the root directory", async () => { + // Arrange + const logs: LogMessage[] = []; + const logger = new Logger( + "DocusaurusDocTree", + { level: "warn" }, + { globalStore: logs }, + ); + logger.setLevel("silent", { transport: "console" }); + const file = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + file.name, + dedent` + --- + title: Category + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const tree = new DocusaurusDocTree(dir.name, { logger }); + const flattened = await tree.flatten(); + + // Assert + expect(flattened).toHaveLength(0); + expect(logs).toContainEqual( + expect.stringContaining("Ignoring index.md file in root directory."), + ); + }); + + it("should ignore files in the root directory that are not configured to be synced to confluence", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name }); + const categoryIndex = fileSync({ + dir: categoryDir.name, + name: "index.md", + }); + writeFileSync( + categoryIndex.name, + dedent` + --- + title: Category + sync_to_confluence: false + --- + + # Hello World + `, + ); + const page = fileSync({ dir: dir.name, name: "page.md" }); + writeFileSync( + page.name, + dedent` + --- + title: Page + sync_to_confluence: false + --- + + # Hello World + `, + ); + + // Act + const tree = new DocusaurusDocTree(dir.name); + const flattened = await tree.flatten(); + + // Assert + expect(flattened).toHaveLength(0); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeCategory.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeCategory.test.ts new file mode 100644 index 00000000..f59d9c55 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeCategory.test.ts @@ -0,0 +1,530 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { randomUUID } from "node:crypto"; +import { writeFileSync } from "node:fs"; +import { basename, join } from "node:path"; + +import type { DirResult, FileResult } from "tmp"; +import { dedent } from "ts-dedent"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync, fileSync } = new TempFiles(); + +import { InvalidMarkdownFormatException } from "@src/lib/docusaurus/pages/errors/InvalidMarkdownFormatException"; +import { InvalidPathException } from "@src/lib/docusaurus/pages/errors/InvalidPathException"; +import { PathNotExistException } from "@src/lib/docusaurus/pages/errors/PathNotExistException"; +import { DocusaurusDocTreeCategory } from "@src/lib/docusaurus/tree/DocusaurusDocTreeCategory"; + +describe("docusaurusDocTreeCategory", () => { + let dir: DirResult; + + beforeEach(() => { + dir = dirSync({ unsafeCleanup: true }); + }); + + afterEach(() => { + dir.removeCallback(); + }); + + it("should be defined", () => { + expect(DocusaurusDocTreeCategory).toBeDefined(); + }); + + it("should fail if the path does not exist", () => { + expect(() => new DocusaurusDocTreeCategory(`/tmp/${randomUUID()}`)).toThrow( + PathNotExistException, + ); + }); + + it("should fail if the path is not a directory", () => { + // Arrange + const file = fileSync({ dir: dir.name, postfix: ".txt" }); + + // Act + // Assert + expect(() => new DocusaurusDocTreeCategory(file.name)).toThrow( + InvalidPathException, + ); + }); + + it("should fail if the path index.md file does not have a valid format", () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name }); + const categoryIndex = fileSync({ dir: categoryDir.name, name: "index.md" }); + writeFileSync(categoryIndex.name, "# Hello World"); + + // Act + // Assert + expect(() => new DocusaurusDocTreeCategory(categoryDir.name)).toThrow( + InvalidMarkdownFormatException, + ); + }); + + it("should build a category if the path does not have an index.md file", () => { + // Arrange + const emptyDir = dirSync({ dir: dir.name }); + + // Act + const category = new DocusaurusDocTreeCategory(emptyDir.name); + + // Assert + expect(category).toBeDefined(); + expect(category.isCategory).toBe(true); + expect(category.path).toBe(join(emptyDir.name, "index.md")); + expect(category.meta).toBeDefined(); + expect(category.meta.title).toBe(basename(emptyDir.name)); + expect(category.meta.syncToConfluence).toBeTruthy(); + expect(category.content).toBe(""); + }); + + it("should build a category from a directory with an index.md file", () => { + // Arrange + const index = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + index.name, + dedent` + --- + title: Title + --- + + # Hello World + `, + ); + + // Act + const category = new DocusaurusDocTreeCategory(dir.name); + + // Assert + expect(category).toBeDefined(); + expect(category.isCategory).toBe(true); + expect(category.path).toBe(join(dir.name, "index.md")); + expect(category.meta).toBeDefined(); + expect(category.meta.title).toBe("Title"); + expect(category.content).toContain("Hello World"); + }); + + it("should inherit the confluence short name from index.md file", () => { + // Arrange + const index = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + index.name, + dedent` + --- + title: Title + confluence_short_name: Title Name + --- + + # Hello World + `, + ); + + // Act + const category = new DocusaurusDocTreeCategory(dir.name); + + // Assert + expect(category.meta.confluenceShortName).toBe("Title Name"); + }); + + it("should inherit the confluence title from index.md file", () => { + // Arrange + const index = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + index.name, + dedent` + --- + title: Title + confluence_title: Title Name + --- + + # Hello World + `, + ); + + // Act + const category = new DocusaurusDocTreeCategory(dir.name); + + // Assert + expect(category.meta.confluenceTitle).toBe("Title Name"); + }); + + describe("extend category metadata", () => { + describe("with yaml format", () => { + it("should failed if the _category_.yml file is not a valid YAML", () => { + // Arrange + const categoryYml = fileSync({ dir: dir.name, name: "_category_.yml" }); + writeFileSync(categoryYml.name, "test: 'should fail"); + + // Act + // Assert + expect(() => new DocusaurusDocTreeCategory(dir.name)).toThrow(); + }); + + it("should extend the metadata from the _category_.yml file", () => { + // Arrange + const categoryYml = fileSync({ dir: dir.name, name: "_category_.yml" }); + writeFileSync( + categoryYml.name, + dedent`--- + label: Category Title + `, + ); + // Act + const category = new DocusaurusDocTreeCategory(dir.name); + + // Assert + try { + expect(category.meta?.title).toBe("Category Title"); + } finally { + categoryYml.removeCallback(); + } + }); + + it("should extend the metadata from the _category_.yaml file", () => { + // Arrange + const categoryYml = fileSync({ + dir: dir.name, + name: "_category_.yaml", + }); + writeFileSync( + categoryYml.name, + dedent`--- + label: Category Title + `, + ); + // Act + const category = new DocusaurusDocTreeCategory(dir.name); + + // Assert + expect(category.meta?.title).toBe("Category Title"); + }); + }); + + describe("with json format", () => { + it("should failed if the _category_.json file is not a valid YAML", () => { + // Arrange + const categoryJson = fileSync({ + dir: dir.name, + name: "_category_.json", + }); + writeFileSync(categoryJson.name, `{"test": "should fail"`); + + // Act + // Assert + expect(() => new DocusaurusDocTreeCategory(dir.name)).toThrow(); + }); + + it("should extend the metadata from the _category_.json file", () => { + // Arrange + const categoryJson = fileSync({ + dir: dir.name, + name: "_category_.json", + }); + writeFileSync( + categoryJson.name, + dedent`--- + { + "label": "Category Title" + } + `, + ); + // Act + const category = new DocusaurusDocTreeCategory(dir.name); + + // Assert + expect(category.meta?.title).toBe("Category Title"); + }); + }); + }); + + describe("visited", () => { + it("should return an empty array if the category is not configured to sync to Confluence", async () => { + // Arrange + const category = new DocusaurusDocTreeCategory(dir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(0); + }); + + describe("without index.md file", () => { + it("should return an empty array if all its children are not configured to sync to Confluence", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name }); + const subCategory = dirSync({ dir: categoryDir.name }); + const subCategoryIndex = fileSync({ + dir: subCategory.name, + name: "index.md", + }); + writeFileSync( + subCategoryIndex.name, + dedent` + --- + title: Child Title + --- + + # Hello World + `, + ); + const categoryPage = fileSync({ + dir: categoryDir.name, + name: "page.md", + }); + writeFileSync( + categoryPage.name, + dedent` + --- + title: Page Title + --- + + # Hello World + `, + ); + const category = new DocusaurusDocTreeCategory(categoryDir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toEqual([]); + }); + + it("should return an array with the category and its children if at least one of its children is configured to sync to Confluence", async () => { + // Arrange + const categoryDir = dirSync({ dir: dir.name }); + const subCategory = dirSync({ + dir: categoryDir.name, + name: "subcategory", + }); + const subCategoryIndex = fileSync({ + dir: subCategory.name, + name: "index.md", + }); + writeFileSync( + subCategoryIndex.name, + dedent` + --- + title: Subcategory Title + sync_to_confluence: true + --- + + # Hello World + `, + ); + const categoryPage = fileSync({ + dir: categoryDir.name, + name: "page.md", + }); + writeFileSync( + categoryPage.name, + dedent` + --- + title: Page Title + sync_to_confluence: true + --- + + # Hello World + `, + ); + const category = new DocusaurusDocTreeCategory(categoryDir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(3); + expect(result[0]).toBe(category); + expect(result[1]).toBeDefined(); + expect(result[1].meta.title).toBe("Page Title"); + expect(result[1].isCategory).toBe(false); + expect(result[1].path).toContain(categoryPage.name); + expect(result[1].content).toContain("Hello World"); + expect(result[2]).toBeDefined(); + expect(result[2].meta.title).toBe("Subcategory Title"); + expect(result[2].isCategory).toBe(true); + expect(result[2].path).toContain(subCategory.name); + expect(result[2].content).toContain("Hello World"); + }); + }); + + describe("with index.md file", () => { + let index: FileResult; + + beforeEach(() => { + index = fileSync({ dir: dir.name, name: "index.md" }); + }); + + it("should return an empty array if the category is not configured to sync to Confluence", async () => { + // Arrange + writeFileSync( + index.name, + dedent` + --- + title: Title + sync_to_confluence: false + --- + + # Hello World + `, + ); + const category = new DocusaurusDocTreeCategory(dir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(0); + }); + + describe("configured to sync to Confluence", () => { + beforeEach(() => { + writeFileSync( + index.name, + dedent` + --- + title: Title + sync_to_confluence: true + --- + + # Hello World + `, + ); + }); + + it("should return an array with the category if the category is configured to sync to Confluence", async () => { + // Arrange + const category = new DocusaurusDocTreeCategory(dir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(1); + expect(result[0]).toBe(category); + }); + + it("should return an array with the category and no children if the category is configured to sync to Confluence and the children are not configured to sync to Confluence", async () => { + // Arrange + const childMd = fileSync({ dir: dir.name, postfix: ".md" }); + writeFileSync( + childMd.name, + dedent` + --- + title: Child Page + sync_to_confluence: false + --- + + # Hello World + `, + ); + const childDir = dirSync({ dir: dir.name }); + const childIndex = fileSync({ dir: childDir.name, name: "index.md" }); + writeFileSync( + childIndex.name, + dedent` + --- + title: Child Category + sync_to_confluence: false + --- + + # Hello World + `, + ); + + const category = new DocusaurusDocTreeCategory(dir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(1); + expect(result[0]).toBe(category); + }); + + it("should return an array with the category if is not configured to sync to Confluence", async () => { + // Arrange + const childMd = fileSync({ dir: dir.name, postfix: ".mdx" }); + writeFileSync( + childMd.name, + dedent` + --- + title: Child Page + sync_to_confluence: false + --- + + # Hello World + `, + ); + + const category = new DocusaurusDocTreeCategory(dir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(1); + expect(result[0]).toBe(category); + }); + + it("should return an array with the category and its children if the category is configured to sync to Confluence and the children are configured to sync to Confluence", async () => { + // Arrange + const childPage = fileSync({ dir: dir.name, name: "child-page.md" }); + writeFileSync( + childPage.name, + dedent` + --- + title: Child Page + sync_to_confluence: true + --- + + # Hello World + `, + ); + const childDir = dirSync({ dir: dir.name, name: "child-category" }); + const childIndex = fileSync({ dir: childDir.name, name: "index.md" }); + writeFileSync( + childIndex.name, + dedent` + --- + title: Child Category + sync_to_confluence: true + --- + + # Hello World + `, + ); + const category = new DocusaurusDocTreeCategory(dir.name); + + // Act + const result = await category.visit(); + + // Assert + expect(result).toHaveLength(3); + expect(result[0]).toBe(category); + + const actualChildCategory = result[1]; + + expect(actualChildCategory.path).toBe( + join(childDir.name, "index.md"), + ); + expect(actualChildCategory.isCategory).toBe(true); + expect(actualChildCategory.meta).toEqual( + expect.objectContaining({ title: "Child Category" }), + ); + expect(actualChildCategory.content).toContain("Hello World"); + + const actualChildPage = result[2]; + + expect(actualChildPage.path).toEqual(childPage.name); + expect(actualChildPage.isCategory).toBe(false); + expect(actualChildPage.meta).toEqual( + expect.objectContaining({ title: "Child Page" }), + ); + expect(actualChildPage.content).toContain("Hello World"); + }); + }); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeItemFactory.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeItemFactory.test.ts new file mode 100644 index 00000000..b02cacc7 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeItemFactory.test.ts @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { writeFileSync } from "fs"; + +import type { DirResult } from "tmp"; +import { dedent } from "ts-dedent"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync, fileSync } = new TempFiles(); + +import { DocusaurusDocItemFactory } from "@src/lib/docusaurus/tree/DocusaurusDocItemFactory"; +import { DocusaurusDocTreeCategory } from "@src/lib/docusaurus/tree/DocusaurusDocTreeCategory"; +import { DocusaurusDocTreePage } from "@src/lib/docusaurus/tree/DocusaurusDocTreePage"; +import { DocusaurusDocTreePageMdx } from "@src/lib/docusaurus/tree/DocusaurusDocTreePageMdx"; + +describe("docusaurusDocTreeItemFactory", () => { + let dir: DirResult; + + beforeEach(() => { + dir = dirSync({ unsafeCleanup: true }); + }); + + afterEach(() => { + dir.removeCallback(); + }); + + it("should be defined", () => { + expect(DocusaurusDocItemFactory).toBeDefined(); + }); + + it("should return a DocusaurusDocTreeCategory when the path is a directory", () => { + // Arrange + const docsDir = dirSync({ dir: dir.name }); + const index = fileSync({ dir: docsDir.name, name: "index.md" }); + writeFileSync( + index.name, + dedent` + --- + title: Category + --- + + # Hello World + `, + ); + + // Act + const result = DocusaurusDocItemFactory.fromPath(docsDir.name); + + // Assert + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(DocusaurusDocTreeCategory); + }); + + it("should return a DocusaurusDocTreePage when the path is a file", () => { + // Arrange + const file = fileSync({ dir: dir.name, postfix: ".md" }); + writeFileSync( + file.name, + dedent` + --- + title: Hello World + --- + + # Hello World + `, + ); + + // Act + const result = DocusaurusDocItemFactory.fromPath(file.name); + + // Assert + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(DocusaurusDocTreePage); + }); + + it("should return a DocusaurusDocTreePageMdx when the path is a mdx file", () => { + // Arrange + const file = fileSync({ dir: dir.name, postfix: ".mdx" }); + writeFileSync( + file.name, + dedent` + --- + title: Hello World + --- + + import Tabs from '@theme/Tabs'; + import TabItem from '@theme/TabItem'; + + # Hello World + :::note title + Hello World + ::: + `, + ); + + // Act + const result = DocusaurusDocItemFactory.fromPath(file.name); + + // Assert + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(DocusaurusDocTreePageMdx); + expect(result.content).toContain( + dedent(`--- + title: Hello World + --- + + import Tabs from '@theme/Tabs'; + import TabItem from '@theme/TabItem'; + + # Hello World + + > **Note: title** + > + > Hello World + `), + ); + }); + + it("should return a DocusaurusDocTreePageMdx when the path is a mdx file and it's not index file", () => { + // Arrange + const file = fileSync({ dir: dir.name, name: "page.mdx" }); + writeFileSync( + file.name, + dedent` +--- +title: Hello World +confluence_short_name: Page Mdx +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + + Tab Item Content + +:::note + Hello World +::: + + + + Tab Inside + + + + + + Tab Item Content 2 + + + `, + ); + + // Act + const result = DocusaurusDocItemFactory.fromPath(file.name); + + // Assert + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(DocusaurusDocTreePageMdx); + expect(result.content).toContain( + dedent(`--- + title: Hello World + confluence_short_name: Page Mdx + --- + + import Tabs from '@theme/Tabs'; + import TabItem from '@theme/TabItem'; + + * File tree + + Tab Item Content + + > **Note:** + > + > Hello World + + * File tree + + Tab Inside + + * File tree 2 + + Tab Item Content 2`), + ); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreePage.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreePage.test.ts new file mode 100644 index 00000000..507dc560 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreePage.test.ts @@ -0,0 +1,321 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { randomUUID } from "node:crypto"; +import { writeFileSync } from "node:fs"; + +import { Logger } from "@mocks-server/logger"; +import type { DirResult, FileResult } from "tmp"; +import { dedent } from "ts-dedent"; + +import { TempFiles } from "@support/utils/TempFiles"; +const { dirSync, fileSync } = new TempFiles(); + +import { InvalidMarkdownFormatException } from "@src/lib/docusaurus/pages/errors/InvalidMarkdownFormatException"; +import { InvalidPathException } from "@src/lib/docusaurus/pages/errors/InvalidPathException"; +import { PathNotExistException } from "@src/lib/docusaurus/pages/errors/PathNotExistException"; +import { DocusaurusDocTreePage } from "@src/lib/docusaurus/tree/DocusaurusDocTreePage"; + +describe("docusaurusDocTreePage", () => { + let dir: DirResult; + let file: FileResult; + + beforeEach(() => { + dir = dirSync({ unsafeCleanup: true }); + file = fileSync({ dir: dir.name, postfix: ".md" }); + }); + + afterEach(() => { + dir.removeCallback(); + }); + + it("should be defined", () => { + expect(DocusaurusDocTreePage).toBeDefined(); + }); + + it("should fail if the file does not exist", () => { + expect(() => new DocusaurusDocTreePage(`/tmp/${randomUUID()}.md`)).toThrow( + PathNotExistException, + ); + }); + + it("should fail if the path is not a file", () => { + // Arrange + const emptyDir = dirSync({ dir: dir.name }); + + // Act + // Assert + expect(() => new DocusaurusDocTreePage(emptyDir.name)).toThrow( + InvalidPathException, + ); + }); + + it("should fail if the file is not a Markdown file", () => { + // Arrange + const txtFile = fileSync({ dir: dir.name, postfix: ".txt" }); + + // Act + // Assert + expect(() => new DocusaurusDocTreePage(txtFile.name)).toThrow( + InvalidPathException, + ); + }); + + it("should fail if the file does not contain frontmatter metadata", () => { + // Arrange + const mdFile = fileSync({ dir: dir.name, postfix: ".md" }); + writeFileSync(mdFile.name, "# Hello World"); + + // Act + // Assert + expect(() => new DocusaurusDocTreePage(mdFile.name)).toThrow( + InvalidMarkdownFormatException, + ); + }); + + it("should fail if the file does not contain title in frontmatter metadata", () => { + // Arrange + const mdFile = fileSync({ dir: dir.name, postfix: ".md" }); + writeFileSync( + mdFile.name, + dedent` + --- + test: Test + --- + + # Hello World + `, + ); + + // Act + // Assert + expect(() => new DocusaurusDocTreePage(mdFile.name)).toThrow( + InvalidMarkdownFormatException, + ); + }); + + it("should build a page from a file", () => { + // Arrange + writeFileSync( + file.name, + dedent` + --- + title: Test + sync_to_confluence: true + --- + + # Hello World + `, + ); + + // Act + const page = new DocusaurusDocTreePage(file.name); + + // Assert + expect(page.isCategory).toBe(false); + expect(page.path).toBe(file.name); + expect(page.content).toContain("Hello World"); + expect(page.meta).toBeDefined(); + expect(page.meta.title).toBe("Test"); + expect(page.meta.syncToConfluence).toBe(true); + }); + + it("should set syncToConfluence to false if not specified", () => { + // Arrange + writeFileSync( + file.name, + dedent` + --- + title: Test + --- + + # Hello World + `, + ); + + // Act + const page = new DocusaurusDocTreePage(file.name); + + // Assert + expect(page.meta.syncToConfluence).toBe(false); + }); + + describe("confluence short name", () => { + it("should read name from metadata", () => { + // Arrange + const indexFile = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + indexFile.name, + dedent` + --- + title: Page + confluence_short_name: Page name + --- + + # Hello World + `, + ); + + // Act + const page = new DocusaurusDocTreePage(indexFile.name); + + // Assert + expect(page.meta.confluenceShortName).toBe("Page name"); + }); + + it("should log warning if file is not index.md", () => { + // Arrange + const testFile = fileSync({ dir: dir.name, name: "test.md" }); + writeFileSync( + testFile.name, + dedent` + --- + title: Test + confluence_short_name: Test name + --- + + # Hello World + `, + ); + const logger = new Logger("docusaurus-doc-tree-page", { level: "warn" }); + logger.setLevel("silent", { transport: "console" }); + + // Act + const page = new DocusaurusDocTreePage(testFile.name, { logger }); + + // Assert + expect(page.meta.confluenceShortName).toBe("Test name"); + expect(logger.store).toEqual( + expect.arrayContaining([ + expect.stringContaining( + "An unnecessary confluence short name has been set for test.md that is not an index file. This confluence short name will be ignored.", + ), + ]), + ); + }); + }); + + describe("confluence title", () => { + it("should read title from metadata", () => { + // Arrange + const indexFile = fileSync({ dir: dir.name, name: "index.md" }); + writeFileSync( + indexFile.name, + dedent` + --- + title: Page + confluence_title: Page title + --- + + # Hello World + `, + ); + + // Act + const page = new DocusaurusDocTreePage(indexFile.name); + + // Assert + expect(page.meta.confluenceTitle).toBe("Page title"); + }); + }); + + describe("docusaurus admonitions", () => { + it("should process docusaurus admonitions with title", () => { + // Arrange + writeFileSync( + file.name, + dedent` + --- + title: Test + --- + + # Hello World + + :::note Note title + This is a note + ::: + `, + ); + + // Act + const page = new DocusaurusDocTreePage(file.name); + + // Assert + expect(page.content).toContain("Hello World"); + expect(page.content).toContain("This is a note"); + expect(page.content).toContain("Note: Note title"); + }); + + it("should process docusaurus admonitions without title", () => { + // Arrange + writeFileSync( + file.name, + dedent` + --- + title: Test + --- + + # Hello World + + :::note + This is a note + ::: + `, + ); + + // Act + const page = new DocusaurusDocTreePage(file.name); + + // Assert + expect(page.content).toContain("Hello World"); + expect(page.content).toContain("This is a note"); + expect(page.content).toContain("Note:"); + }); + }); + + describe("visited", () => { + it("should return an empty array if the page is not to be synced", async () => { + // Arrange + writeFileSync( + file.name, + dedent` + --- + title: Test + sync_to_confluence: false + --- + + # Hello World + `, + ); + const page = new DocusaurusDocTreePage(file.name); + + // Act + const result = await page.visit(); + + // Assert + expect(result).toEqual([]); + }); + + it("should return an array with the page if the page is to be synced", async () => { + // Arrange + writeFileSync( + file.name, + dedent` + --- + title: Test + sync_to_confluence: true + --- + + # Hello World + `, + ); + const page = new DocusaurusDocTreePage(file.name); + + // Act + const result = await page.visit(); + + // Assert + expect(result).toEqual([page]); + }); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/errors/CategoryIndexNotFoundException.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/errors/CategoryIndexNotFoundException.test.ts new file mode 100644 index 00000000..0003e74f --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/errors/CategoryIndexNotFoundException.test.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { CategoryIndexNotFoundException } from "@src/lib/docusaurus/tree/errors/CategoryIndexNotFoundException"; + +describe("custom error CategoryIndexNotFoundException", () => { + it("should have message 'Category index not found: test'", async () => { + await expect( + Promise.reject( + new CategoryIndexNotFoundException("test", { cause: "error test" }), + ), + ).rejects.toThrow("Category index not found: test"); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/docusaurus/utils/files.test.ts b/components/markdown-confluence-sync/test/unit/specs/docusaurus/utils/files.test.ts new file mode 100644 index 00000000..cdd9bebe --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/utils/files.test.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { buildIndexFileRegExp } from "@src/lib/docusaurus/util/files"; + +describe("file utility functions", () => { + it("should export buildIndexFileRegExp function", () => { + expect(buildIndexFileRegExp).toBeDefined(); + }); + + it("should call buildIndexFileRegExp function and response contains windows path separator", async () => { + const result = buildIndexFileRegExp("\\", "foo"); + + expect(result.toString()).toMatch("/\\\\("); + }); + + it("should call buildIndexFileRegExp function and response contains linux path separator", async () => { + const result = buildIndexFileRegExp("/", "foo"); + + expect(result.toString()).toMatch("/\\/("); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/support/unist/unist-util-replace.test.ts b/components/markdown-confluence-sync/test/unit/specs/support/unist/unist-util-replace.test.ts new file mode 100644 index 00000000..b8e6a851 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/support/unist/unist-util-replace.test.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import { u } from "unist-builder"; + +import { replace } from "@src/lib/support/unist/unist-util-replace"; + +describe("unist-util-replace", () => { + it("should be defined", () => { + expect(replace).toBeDefined(); + }); + + it("should replace a node", () => { + const tree = u("root", [u("paragraph", [u("text", "Hello, world!")])]); + + replace(tree, "text", (node) => ({ + type: "text" as const, + value: node.value.toUpperCase(), + })); + + expect(tree).toEqual( + u("root", [u("paragraph", [u("text", "HELLO, WORLD!")])]), + ); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/support/mocks/ConfluencePageTransformer.ts b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluencePageTransformer.ts new file mode 100644 index 00000000..ef5d26b7 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluencePageTransformer.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("@src/lib/confluence/transformer/ConfluencePageTransformer"); + +import * as customConfluencePageLib from "@src/lib/confluence/transformer/ConfluencePageTransformer"; + +export const customConfluencePage = { + transform: jest.fn(), +}; + +jest + .spyOn(customConfluencePageLib, "ConfluencePageTransformer") + .mockImplementation(() => { + return customConfluencePage; + }); diff --git a/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSync.ts b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSync.ts new file mode 100644 index 00000000..57a02213 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSync.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("@src/lib/confluence/ConfluenceSync"); + +import * as confluenceSyncMock from "@src/lib/confluence/ConfluenceSync"; + +export const customConfluenceSync = { + sync: jest.fn(), +}; + +/* ts ignore next line because it expects a mock with the same parameters as the ConfluenceSync class + * but there are a lot of them useless for the test */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-next-line +jest.spyOn(confluenceSyncMock, "ConfluenceSync").mockImplementation(() => { + return customConfluenceSync; +}); diff --git a/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSyncPages.ts b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSyncPages.ts new file mode 100644 index 00000000..509caf2f --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSyncPages.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("@tid-cross/confluence-sync"); + +import * as confluenceSyncPagesMock from "@tid-cross/confluence-sync"; + +export const customConfluenceSyncPages = { + sync: jest.fn(), +}; + +/* ts ignore next line because it expects a mock with the same parameters as the ConfluenceSyncPages class + * but there are a lot of them useless for the test */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore-next-line +jest + .spyOn(confluenceSyncPagesMock, "ConfluenceSyncPages") + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line + .mockImplementation(() => { + return customConfluenceSyncPages; + }); diff --git a/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusPages.ts b/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusPages.ts new file mode 100644 index 00000000..98fca254 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusPages.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("@src/lib/docusaurus/DocusaurusPages"); + +import * as customDocusaurusPagesLib from "@src/lib/docusaurus/DocusaurusPages"; + +export const customDocusaurusPages = { + read: jest.fn(), +}; + +jest + .spyOn(customDocusaurusPagesLib, "DocusaurusPages") + .mockImplementation(() => { + return customDocusaurusPages; + }); diff --git a/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusToConfluence.ts b/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusToConfluence.ts new file mode 100644 index 00000000..da86683d --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusToConfluence.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +jest.mock("@src/lib/MarkdownConfluenceSync"); + +import * as customMarkdownConfluenceSyncClass from "@src/lib/MarkdownConfluenceSync"; + +export const customMarkdownConfluenceSync = { + sync: jest.fn(), +}; + +jest + .spyOn(customMarkdownConfluenceSyncClass, "MarkdownConfluenceSync") + .mockImplementation(() => { + return customMarkdownConfluenceSync; + }); diff --git a/components/markdown-confluence-sync/test/unit/support/utils/TempFiles.ts b/components/markdown-confluence-sync/test/unit/support/utils/TempFiles.ts new file mode 100644 index 00000000..01e06d6a --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/utils/TempFiles.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DirOptions, FileOptions } from "tmp"; +import { fileSync, dirSync } from "tmp"; + +/** + * Class to wrap tmp package functions and solve problems with windows + */ +export const TempFiles = class TempFiles { + public dirSync(this: void, options: DirOptions) { + return dirSync({ ...options }); + } + + /** + * FIX: Add discardDescriptor option to correct error when removing temporary + * files in Windows when using the removeCallback option of the dirSync function + * @param {FileOptions} options + * @returns {FileResult} + */ + public fileSync(this: void, options: FileOptions = {}) { + return fileSync({ discardDescriptor: true, ...options }); + } +}; diff --git a/components/markdown-confluence-sync/test/unit/tsconfig.json b/components/markdown-confluence-sync/test/unit/tsconfig.json new file mode 100644 index 00000000..465c29d1 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "moduleResolution": "node", + "paths": { + "@src/*": ["../../src/*"], + "@support/*": ["./support/*"] + } + }, + "include": ["**/*", "../../src/types/*"] +} diff --git a/components/markdown-confluence-sync/tsconfig.base.json b/components/markdown-confluence-sync/tsconfig.base.json new file mode 100644 index 00000000..ae004a59 --- /dev/null +++ b/components/markdown-confluence-sync/tsconfig.base.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "skipLibCheck": true, + "rootDir": ".", + "outDir": "dist", + "declaration": true, + "target": "ES2022", + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true, + "moduleResolution": "bundler", + "module": "ESNext", + "useDefineForClassFields": true, + "importsNotUsedAsValues": "remove", + "forceConsistentCasingInFileNames": true, + "noUnusedParameters": true, + "isolatedModules": true, + "strictPropertyInitialization": false, + "rootDirs": [ + "./src", + "./node_modules/@docusaurus" + ] + }, + "include": [ + "src/types/**/*" + ] +} diff --git a/components/markdown-confluence-sync/tsconfig.json b/components/markdown-confluence-sync/tsconfig.json new file mode 100644 index 00000000..cfed2d8b --- /dev/null +++ b/components/markdown-confluence-sync/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*", "types/**/*"] +} diff --git a/cspell.config.cjs b/cspell.config.cjs new file mode 100644 index 00000000..9bf1a319 --- /dev/null +++ b/cspell.config.cjs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +const { createConfig } = require("./components/cspell-config/index.js"); + +module.exports = createConfig(); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..01566e83 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +import config from "./components/eslint-config/index.js"; +export default config; diff --git a/nx.json b/nx.json new file mode 100644 index 00000000..dd23b874 --- /dev/null +++ b/nx.json @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +// SPDX-License-Identifier: MIT + +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "npmScope": "@tid-cross", + "defaultBase": "origin/main", + "parallel": 2, + "namedInputs": { + // Root workspace configuration is an input for all targets in all projects + "sharedGlobals": [ + "{workspaceRoot}/package.json", + "{workspaceRoot}/pnpm-lock.yaml", + "{workspaceRoot}/nx.json", + "{workspaceRoot}/pnpm-workspace.yaml", + { "runtime": "node --version" } + ], + // By default, all projects depend on the whole workspace configuration and their own source code + "default": [ + "{projectRoot}/**/*", + "sharedGlobals" + ], + // Usual input for the build targets. It excludes test files, mocks, and other non-production files. This should be overridden in projects that have different test files + "production": [ + "!{projectRoot}/**/*.spec.*", + "!{projectRoot}/**/*.test.*", + "!{projectRoot}/test/**/*", + "!{projectRoot}/mocks/**/*", + "!{projectRoot}/*", + "{projectRoot}/package.json", + "{projectRoot}/README.md", + "{projectRoot}/CHANGELOG.md", + "!{workspaceRoot}/components/cspell-config/**/*", + "!{workspaceRoot}/components/eslint-config/**/*" + ] + }, + "targetDefaults": { + "lint": { + "cache": true, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default" + ], + "dependsOn": [ + { + "target": "eslint:config", + "projects": ["eslint-config"] + } + ] + }, + "check:spell": { + "cache": true, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default" + ], + "dependsOn": [ + { + "target": "cspell:config", + "projects": ["cspell-config"] + } + ] + }, + "check:types": { + "cache": true, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default" + ], + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "cache": true, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default" + ], + "outputs": ["{projectRoot}/coverage"], + "dependsOn": [ + "^build" + ] + }, + "build": { + "cache": true, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default", + "production" + ], + "dependsOn": [ + "^build" + ], + "outputs": [ + "{projectRoot}/dist", + "{projectRoot}/package.json", + "{projectRoot}/README.md", + "{projectRoot}/CHANGELOG.md", + "{projectRoot}/bin" + ] + }, + "test:component": { + "cache": true, + "parallelism": false, + "inputs": [ + { "dependentTasksOutputFiles": "**/*", "transitive": true }, + "default" + ], + "dependsOn": [ + "build", + "^build" + ] + }, + "check:all": { + "dependsOn": [ + "cspell:config", + "eslint:config", + "check:spell", + "lint", + "check:types", + "test:unit", + "build", + "test:component" + ] + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..facd1c29 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "cross-confluence-tools", + "private": true, + "version": "1.0.0", + "type": "module", + "description": "Cross-Cutting team Confluence", + "packageManager": "pnpm@9.4.0", + "scripts": { + "nx": "nx", + "eslint": "eslint", + "lint": "eslint *.* --no-warn-ignored", + "lint:staged": "lint-staged", + "prepare": "husky install", + "check:spell": "cspell *.* .husky/*.* .github/*.*" + }, + "devDependencies": { + "@babel/core": "7.26.0", + "@babel/preset-env": "7.26.0", + "@babel/preset-typescript": "7.26.0", + "@eslint/js": "9.13.0", + "@eslint/json": "0.6.0", + "@eslint/markdown": "6.2.1", + "@types/jest": "29.5.14", + "@types/node": "22.9.0", + "@typescript-eslint/eslint-plugin": "8.14.0", + "@typescript-eslint/parser": "8.14.0", + "babel-plugin-module-resolver": "5.0.2", + "cross-env": "7.0.3", + "cspell": "8.15.5", + "eslint": "9.7.0", + "eslint-config-prettier": "9.1.0", + "eslint-import-resolver-alias": "1.1.2", + "eslint-import-resolver-typescript": "3.6.3", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-jest": "28.9.0", + "eslint-plugin-prettier": "5.1.3", + "globals": "15.12.0", + "husky": "9.0.11", + "jest": "29.7.0", + "jest-sonar": "0.2.16", + "lint-staged": "15.2.10", + "nx": "20.1.0", + "typescript": "5.6.3" + }, + "lint-staged": { + "*.js": "eslint", + "*.mjs": "eslint", + "*.cjs": "eslint", + "*.json": "eslint", + "*.md": "eslint", + "*.*": "cspell --no-must-find-files" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..2bf088c5 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,12897 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@babel/core': + specifier: 7.26.0 + version: 7.26.0 + '@babel/preset-env': + specifier: 7.26.0 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: 7.26.0 + version: 7.26.0(@babel/core@7.26.0) + '@eslint/js': + specifier: 9.13.0 + version: 9.13.0 + '@eslint/json': + specifier: 0.6.0 + version: 0.6.0 + '@eslint/markdown': + specifier: 6.2.1 + version: 6.2.1 + '@types/jest': + specifier: 29.5.14 + version: 29.5.14 + '@types/node': + specifier: 22.9.0 + version: 22.9.0 + '@typescript-eslint/eslint-plugin': + specifier: 8.14.0 + version: 8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: 8.14.0 + version: 8.14.0(eslint@9.7.0)(typescript@5.6.3) + babel-plugin-module-resolver: + specifier: 5.0.2 + version: 5.0.2 + cross-env: + specifier: 7.0.3 + version: 7.0.3 + cspell: + specifier: 8.15.5 + version: 8.15.5 + eslint: + specifier: 9.7.0 + version: 9.7.0 + eslint-config-prettier: + specifier: 9.1.0 + version: 9.1.0(eslint@9.7.0) + eslint-import-resolver-alias: + specifier: 1.1.2 + version: 1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0)) + eslint-import-resolver-typescript: + specifier: 3.6.3 + version: 3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0) + eslint-plugin-import: + specifier: 2.31.0 + version: 2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + eslint-plugin-jest: + specifier: 28.9.0 + version: 28.9.0(@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3) + eslint-plugin-prettier: + specifier: 5.1.3 + version: 5.1.3(eslint-config-prettier@9.1.0(eslint@9.7.0))(eslint@9.7.0)(prettier@3.3.3) + globals: + specifier: 15.12.0 + version: 15.12.0 + husky: + specifier: 9.0.11 + version: 9.0.11 + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@22.9.0) + jest-sonar: + specifier: 0.2.16 + version: 0.2.16 + lint-staged: + specifier: 15.2.10 + version: 15.2.10 + nx: + specifier: 20.1.0 + version: 20.1.0 + typescript: + specifier: 5.6.3 + version: 5.6.3 + + components/child-process-manager: + dependencies: + cross-spawn: + specifier: 7.0.3 + version: 7.0.3 + tree-kill: + specifier: 1.2.2 + version: 1.2.2 + devDependencies: + '@types/cross-spawn': + specifier: 6.0.6 + version: 6.0.6 + + components/confluence-sync: + dependencies: + '@mocks-server/logger': + specifier: 2.0.0-beta.2 + version: 2.0.0-beta.2 + axios: + specifier: 1.6.7 + version: 1.6.7 + confluence.js: + specifier: 1.7.4 + version: 1.7.4 + fastq: + specifier: 1.17.1 + version: 1.17.1 + devDependencies: + '@mocks-server/admin-api-client': + specifier: 8.0.0-beta.2 + version: 8.0.0-beta.2 + '@mocks-server/core': + specifier: 5.0.0-beta.3 + version: 5.0.0-beta.3 + '@mocks-server/main': + specifier: 5.0.0-beta.4 + version: 5.0.0-beta.4 + '@types/tmp': + specifier: 0.2.6 + version: 0.2.6 + cross-fetch: + specifier: 4.0.0 + version: 4.0.0 + start-server-and-test: + specifier: 2.0.8 + version: 2.0.8 + tmp: + specifier: 0.2.3 + version: 0.2.3 + + components/cspell-config: + dependencies: + deepmerge: + specifier: 4.3.1 + version: 4.3.1 + + components/eslint-config: {} + + components/markdown-confluence-sync: + dependencies: + '@mermaid-js/mermaid-cli': + specifier: 11.4.0 + version: 11.4.0(puppeteer@19.11.1(typescript@5.6.3)) + '@mocks-server/config': + specifier: 2.0.0-beta.3 + version: 2.0.0-beta.3 + '@mocks-server/logger': + specifier: 2.0.0-beta.2 + version: 2.0.0-beta.2 + '@tid-cross/confluence-sync': + specifier: workspace:* + version: link:../confluence-sync + fs-extra: + specifier: 11.2.0 + version: 11.2.0 + handlebars: + specifier: 4.7.8 + version: 4.7.8 + hast: + specifier: 1.0.0 + version: 1.0.0 + hast-util-to-string: + specifier: 2.0.0 + version: 2.0.0 + mdast-util-mdx: + specifier: 3.0.0 + version: 3.0.0 + mdast-util-mdx-jsx: + specifier: 3.1.3 + version: 3.1.3 + mdast-util-to-markdown: + specifier: 2.1.1 + version: 2.1.1 + rehype-raw: + specifier: 5.1.0 + version: 5.1.0 + rehype-stringify: + specifier: 9.0.4 + version: 9.0.4 + remark: + specifier: 14.0.3 + version: 14.0.3 + remark-directive: + specifier: 2.0.1 + version: 2.0.1 + remark-frontmatter: + specifier: 4.0.1 + version: 4.0.1 + remark-gfm: + specifier: 3.0.1 + version: 3.0.1 + remark-mdx: + specifier: 2.3.0 + version: 2.3.0 + remark-parse: + specifier: 10.0.2 + version: 10.0.2 + remark-parse-frontmatter: + specifier: 1.0.3 + version: 1.0.3 + remark-rehype: + specifier: 10.1.0 + version: 10.1.0 + remark-stringify: + specifier: 10.0.3 + version: 10.0.3 + remark-unlink: + specifier: 4.0.1 + version: 4.0.1 + to-vfile: + specifier: 7.2.4 + version: 7.2.4 + unified: + specifier: 10.1.2 + version: 10.1.2 + unist-util-find: + specifier: 1.0.4 + version: 1.0.4 + unist-util-find-after: + specifier: 4.0.1 + version: 4.0.1 + unist-util-is: + specifier: 5.2.1 + version: 5.2.1 + unist-util-remove: + specifier: 3.1.1 + version: 3.1.1 + unist-util-visit: + specifier: 4.1.2 + version: 4.1.2 + unist-util-visit-parents: + specifier: 5.1.3 + version: 5.1.3 + vfile: + specifier: 5.3.7 + version: 5.3.7 + which: + specifier: 3.0.1 + version: 3.0.1 + yaml: + specifier: 2.3.4 + version: 2.3.4 + zod: + specifier: 3.22.4 + version: 3.22.4 + devDependencies: + '@mocks-server/admin-api-client': + specifier: 8.0.0-beta.2 + version: 8.0.0-beta.2 + '@mocks-server/core': + specifier: 5.0.0-beta.3 + version: 5.0.0-beta.3 + '@mocks-server/main': + specifier: 5.0.0-beta.4 + version: 5.0.0-beta.4 + '@tid-cross/child-process-manager': + specifier: workspace:* + version: link:../child-process-manager + '@types/fs-extra': + specifier: 11.0.4 + version: 11.0.4 + '@types/glob': + specifier: 8.1.0 + version: 8.1.0 + '@types/hast': + specifier: 2.3.10 + version: 2.3.10 + '@types/mdast': + specifier: 3.0.15 + version: 3.0.15 + '@types/tmp': + specifier: 0.2.6 + version: 0.2.6 + '@types/unist': + specifier: 2.0.11 + version: 2.0.11 + '@types/which': + specifier: 3.0.4 + version: 3.0.4 + babel-plugin-transform-import-meta: + specifier: 2.2.1 + version: 2.2.1(@babel/core@7.26.0) + cross-fetch: + specifier: 4.0.0 + version: 4.0.0 + glob: + specifier: 10.3.10 + version: 10.3.10 + rehype: + specifier: 12.0.1 + version: 12.0.1 + rehype-parse: + specifier: 8.0.5 + version: 8.0.5 + start-server-and-test: + specifier: 2.0.8 + version: 2.0.8 + tmp: + specifier: 0.2.3 + version: 0.2.3 + ts-dedent: + specifier: 2.2.0 + version: 2.2.0 + unist-builder: + specifier: 4.0.0 + version: 4.0.0 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.18.13': + resolution: {integrity: sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.25.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.25.9': + resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.3': + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.25.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-simple-access@7.25.9': + resolution: {integrity: sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.25.9': + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.25.9': + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.25.9': + resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.25.9': + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.25.9': + resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.25.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.25.9': + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.25.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.25.9': + resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-typescript@7.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/register@7.18.9': + resolution: {integrity: sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@braintree/sanitize-url@7.1.0': + resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@cspell/cspell-bundled-dicts@8.15.5': + resolution: {integrity: sha512-Su1gnTBbE7ouMQvM4DISUmP6sZiFyQRE+ODvjBzW+c/x9ZLbVp+2hBEEmxFSn5fdZCJzPOMwzwsjlLYykb9iUg==} + engines: {node: '>=18'} + + '@cspell/cspell-json-reporter@8.15.5': + resolution: {integrity: sha512-yXd7KDBfUkA6y+MrOqK3q/UWorZgLIgyCZoFb0Pj67OU2ZMtgJ1VGFXAdzpKAEgEmdcblyoFzHkleYbg08qS6g==} + engines: {node: '>=18'} + + '@cspell/cspell-pipe@8.15.5': + resolution: {integrity: sha512-X8QY73060hkR8040jabNJsvydeTG0owpqr9S0QJDdhG1z8uzenNcwR3hfwaIwQq5d6sIKcDFZY5qrO4x6eEAMw==} + engines: {node: '>=18'} + + '@cspell/cspell-resolver@8.15.5': + resolution: {integrity: sha512-ejzUGLEwI8TQWXovQzzvAgSNToRrQe3h97YrH2XaB9rZDKkeA7dIBZDQ/OgOfidO+ZAsPIOxdHai3CBzEHYX3A==} + engines: {node: '>=18'} + + '@cspell/cspell-service-bus@8.15.5': + resolution: {integrity: sha512-zZJRRvNhvUJntnw8sX4J5gE4uIHpX2oe+Tqs3lu2vRwogadNEXE4QNJbEQyQqgMePgmqULtRdxSBzG4wy4HoQg==} + engines: {node: '>=18'} + + '@cspell/cspell-types@8.15.5': + resolution: {integrity: sha512-bMRq9slD/D0vXckxe9vubG02HXrV4cASo6Ytkaw8rTfxMKpkBgxJWjFWphCFLOCICD71q45fUSg+W5vCp83f/Q==} + engines: {node: '>=18'} + + '@cspell/dict-ada@4.0.5': + resolution: {integrity: sha512-6/RtZ/a+lhFVmrx/B7bfP7rzC4yjEYe8o74EybXcvu4Oue6J4Ey2WSYj96iuodloj1LWrkNCQyX5h4Pmcj0Iag==} + + '@cspell/dict-al@1.0.3': + resolution: {integrity: sha512-V1HClwlfU/qwSq2Kt+MkqRAsonNu3mxjSCDyGRecdLGIHmh7yeEeaxqRiO/VZ4KP+eVSiSIlbwrb5YNFfxYZbw==} + + '@cspell/dict-aws@4.0.7': + resolution: {integrity: sha512-PoaPpa2NXtSkhGIMIKhsJUXB6UbtTt6Ao3x9JdU9kn7fRZkwD4RjHDGqulucIOz7KeEX/dNRafap6oK9xHe4RA==} + + '@cspell/dict-bash@4.1.8': + resolution: {integrity: sha512-I2CM2pTNthQwW069lKcrVxchJGMVQBzru2ygsHCwgidXRnJL/NTjAPOFTxN58Jc1bf7THWghfEDyKX/oyfc0yg==} + + '@cspell/dict-companies@3.1.7': + resolution: {integrity: sha512-ncVs/efuAkP1/tLDhWbXukBjgZ5xOUfe03neHMWsE8zvXXc5+Lw6TX5jaJXZLOoES/f4j4AhRE20jsPCF5pm+A==} + + '@cspell/dict-cpp@5.1.23': + resolution: {integrity: sha512-59VUam6bYWzn50j8FASWWLww0rBPA0PZfjMZBvvt0aqMpkvXzoJPnAAI4eDDSibPWVHKutjpqLmast+uMLHVsQ==} + + '@cspell/dict-cryptocurrencies@5.0.3': + resolution: {integrity: sha512-bl5q+Mk+T3xOZ12+FG37dB30GDxStza49Rmoax95n37MTLksk9wBo1ICOlPJ6PnDUSyeuv4SIVKgRKMKkJJglA==} + + '@cspell/dict-csharp@4.0.5': + resolution: {integrity: sha512-c/sFnNgtRwRJxtC3JHKkyOm+U3/sUrltFeNwml9VsxKBHVmvlg4tk4ar58PdpW9/zTlGUkWi2i85//DN1EsUCA==} + + '@cspell/dict-css@4.0.16': + resolution: {integrity: sha512-70qu7L9z/JR6QLyJPk38fNTKitlIHnfunx0wjpWQUQ8/jGADIhMCrz6hInBjqPNdtGpYm8d1dNFyF8taEkOgrQ==} + + '@cspell/dict-dart@2.2.4': + resolution: {integrity: sha512-of/cVuUIZZK/+iqefGln8G3bVpfyN6ZtH+LyLkHMoR5tEj+2vtilGNk9ngwyR8L4lEqbKuzSkOxgfVjsXf5PsQ==} + + '@cspell/dict-data-science@2.0.5': + resolution: {integrity: sha512-nNSILXmhSJox9/QoXICPQgm8q5PbiSQP4afpbkBqPi/u/b3K9MbNH5HvOOa6230gxcGdbZ9Argl2hY/U8siBlg==} + + '@cspell/dict-django@4.1.3': + resolution: {integrity: sha512-yBspeL3roJlO0a1vKKNaWABURuHdHZ9b1L8d3AukX0AsBy9snSggc8xCavPmSzNfeMDXbH+1lgQiYBd3IW03fg==} + + '@cspell/dict-docker@1.1.11': + resolution: {integrity: sha512-s0Yhb16/R+UT1y727ekbR/itWQF3Qz275DR1ahOa66wYtPjHUXmhM3B/LT3aPaX+hD6AWmK23v57SuyfYHUjsw==} + + '@cspell/dict-dotnet@5.0.8': + resolution: {integrity: sha512-MD8CmMgMEdJAIPl2Py3iqrx3B708MbCIXAuOeZ0Mzzb8YmLmiisY7QEYSZPg08D7xuwARycP0Ki+bb0GAkFSqg==} + + '@cspell/dict-elixir@4.0.6': + resolution: {integrity: sha512-TfqSTxMHZ2jhiqnXlVKM0bUADtCvwKQv2XZL/DI0rx3doG8mEMS8SGPOmiyyGkHpR/pGOq18AFH3BEm4lViHIw==} + + '@cspell/dict-en-common-misspellings@2.0.7': + resolution: {integrity: sha512-qNFo3G4wyabcwnM+hDrMYKN9vNVg/k9QkhqSlSst6pULjdvPyPs1mqz1689xO/v9t8e6sR4IKc3CgUXDMTYOpA==} + + '@cspell/dict-en-gb@1.1.33': + resolution: {integrity: sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g==} + + '@cspell/dict-en_us@4.3.27': + resolution: {integrity: sha512-7JYHahRWpi0VykWFTSM03KL/0fs6YtYfpOaTAg4N/d0wB2GfwVG/FJ/SBCjD4LBc6Rx9dzdo95Hs4BB8GPQbOA==} + + '@cspell/dict-filetypes@3.0.8': + resolution: {integrity: sha512-D3N8sm/iptzfVwsib/jvpX+K/++rM8SRpLDFUaM4jxm8EyGmSIYRbKZvdIv5BkAWmMlTWoRqlLn7Yb1b11jKJg==} + + '@cspell/dict-flutter@1.0.3': + resolution: {integrity: sha512-52C9aUEU22ptpgYh6gQyIdA4MP6NPwzbEqndfgPh3Sra191/kgs7CVqXiO1qbtZa9gnYHUoVApkoxRE7mrXHfg==} + + '@cspell/dict-fonts@4.0.3': + resolution: {integrity: sha512-sPd17kV5qgYXLteuHFPn5mbp/oCHKgitNfsZLFC3W2fWEgZlhg4hK+UGig3KzrYhhvQ8wBnmZrAQm0TFKCKzsA==} + + '@cspell/dict-fsharp@1.0.4': + resolution: {integrity: sha512-G5wk0o1qyHUNi9nVgdE1h5wl5ylq7pcBjX8vhjHcO4XBq20D5eMoXjwqMo/+szKAqzJ+WV3BgAL50akLKrT9Rw==} + + '@cspell/dict-fullstack@3.2.3': + resolution: {integrity: sha512-62PbndIyQPH11mAv0PyiyT0vbwD0AXEocPpHlCHzfb5v9SspzCCbzQ/LIBiFmyRa+q5LMW35CnSVu6OXdT+LKg==} + + '@cspell/dict-gaming-terms@1.0.8': + resolution: {integrity: sha512-7OL0zTl93WFWhhtpXFrtm9uZXItC3ncAs8d0iQDMMFVNU1rBr6raBNxJskxE5wx2Ant12fgI66ZGVagXfN+yfA==} + + '@cspell/dict-git@3.0.3': + resolution: {integrity: sha512-LSxB+psZ0qoj83GkyjeEH/ZViyVsGEF/A6BAo8Nqc0w0HjD2qX/QR4sfA6JHUgQ3Yi/ccxdK7xNIo67L2ScW5A==} + + '@cspell/dict-golang@6.0.16': + resolution: {integrity: sha512-hZOBlgcguv2Hdc93n2zjdAQm1j3grsN9T9WhPnQ1wh2vUDoCLEujg+6gWhjcLb8ECOcwZTWgNyQLWeOxEsAj/w==} + + '@cspell/dict-google@1.0.4': + resolution: {integrity: sha512-JThUT9eiguCja1mHHLwYESgxkhk17Gv7P3b1S7ZJzXw86QyVHPrbpVoMpozHk0C9o+Ym764B7gZGKmw9uMGduQ==} + + '@cspell/dict-haskell@4.0.4': + resolution: {integrity: sha512-EwQsedEEnND/vY6tqRfg9y7tsnZdxNqOxLXSXTsFA6JRhUlr8Qs88iUUAfsUzWc4nNmmzQH2UbtT25ooG9x4nA==} + + '@cspell/dict-html-symbol-entities@4.0.3': + resolution: {integrity: sha512-aABXX7dMLNFdSE8aY844X4+hvfK7977sOWgZXo4MTGAmOzR8524fjbJPswIBK7GaD3+SgFZ2yP2o0CFvXDGF+A==} + + '@cspell/dict-html@4.0.10': + resolution: {integrity: sha512-I9uRAcdtHbh0wEtYZlgF0TTcgH0xaw1B54G2CW+tx4vHUwlde/+JBOfIzird4+WcMv4smZOfw+qHf7puFUbI5g==} + + '@cspell/dict-java@5.0.10': + resolution: {integrity: sha512-pVNcOnmoGiNL8GSVq4WbX/Vs2FGS0Nej+1aEeGuUY9CU14X8yAVCG+oih5ZoLt1jaR8YfR8byUF8wdp4qG4XIw==} + + '@cspell/dict-julia@1.0.4': + resolution: {integrity: sha512-bFVgNX35MD3kZRbXbJVzdnN7OuEqmQXGpdOi9jzB40TSgBTlJWA4nxeAKV4CPCZxNRUGnLH0p05T/AD7Aom9/w==} + + '@cspell/dict-k8s@1.0.9': + resolution: {integrity: sha512-Q7GELSQIzo+BERl2ya/nBEnZeQC+zJP19SN1pI6gqDYraM51uYJacbbcWLYYO2Y+5joDjNt/sd/lJtLaQwoSlA==} + + '@cspell/dict-latex@4.0.3': + resolution: {integrity: sha512-2KXBt9fSpymYHxHfvhUpjUFyzrmN4c4P8mwIzweLyvqntBT3k0YGZJSriOdjfUjwSygrfEwiuPI1EMrvgrOMJw==} + + '@cspell/dict-lorem-ipsum@4.0.3': + resolution: {integrity: sha512-WFpDi/PDYHXft6p0eCXuYnn7mzMEQLVeqpO+wHSUd+kz5ADusZ4cpslAA4wUZJstF1/1kMCQCZM6HLZic9bT8A==} + + '@cspell/dict-lua@4.0.6': + resolution: {integrity: sha512-Jwvh1jmAd9b+SP9e1GkS2ACbqKKRo9E1f9GdjF/ijmooZuHU0hPyqvnhZzUAxO1egbnNjxS/J2T6iUtjAUK2KQ==} + + '@cspell/dict-makefile@1.0.3': + resolution: {integrity: sha512-R3U0DSpvTs6qdqfyBATnePj9Q/pypkje0Nj26mQJ8TOBQutCRAJbr2ZFAeDjgRx5EAJU/+8txiyVF97fbVRViw==} + + '@cspell/dict-markdown@2.0.7': + resolution: {integrity: sha512-F9SGsSOokFn976DV4u/1eL4FtKQDSgJHSZ3+haPRU5ki6OEqojxKa8hhj4AUrtNFpmBaJx/WJ4YaEzWqG7hgqg==} + peerDependencies: + '@cspell/dict-css': ^4.0.16 + '@cspell/dict-html': ^4.0.10 + '@cspell/dict-html-symbol-entities': ^4.0.3 + '@cspell/dict-typescript': ^3.1.11 + + '@cspell/dict-monkeyc@1.0.9': + resolution: {integrity: sha512-Jvf6g5xlB4+za3ThvenYKREXTEgzx5gMUSzrAxIiPleVG4hmRb/GBSoSjtkGaibN3XxGx5x809gSTYCA/IHCpA==} + + '@cspell/dict-node@5.0.5': + resolution: {integrity: sha512-7NbCS2E8ZZRZwlLrh2sA0vAk9n1kcTUiRp/Nia8YvKaItGXLfxYqD2rMQ3HpB1kEutal6hQLVic3N2Yi1X7AaA==} + + '@cspell/dict-npm@5.1.12': + resolution: {integrity: sha512-ZPyOXa7CdluSEZT1poDikD5pYbeUrRXzHmfpH0jVKVV8wdoQgxOy7I/btRprPeuF9ig5cYrLUo77r1iit1boLw==} + + '@cspell/dict-php@4.0.13': + resolution: {integrity: sha512-P6sREMZkhElzz/HhXAjahnICYIqB/HSGp1EhZh+Y6IhvC15AzgtDP8B8VYCIsQof6rPF1SQrFwunxOv8H1e2eg==} + + '@cspell/dict-powershell@5.0.13': + resolution: {integrity: sha512-0qdj0XZIPmb77nRTynKidRJKTU0Fl+10jyLbAhFTuBWKMypVY06EaYFnwhsgsws/7nNX8MTEQuewbl9bWFAbsg==} + + '@cspell/dict-public-licenses@2.0.11': + resolution: {integrity: sha512-rR5KjRUSnVKdfs5G+gJ4oIvQvm8+NJ6cHWY2N+GE69/FSGWDOPHxulCzeGnQU/c6WWZMSimG9o49i9r//lUQyA==} + + '@cspell/dict-python@4.2.12': + resolution: {integrity: sha512-U25eOFu+RE0aEcF2AsxZmq3Lic7y9zspJ9SzjrC0mfJz+yr3YmSCw4E0blMD3mZoNcf7H/vMshuKIY5AY36U+Q==} + + '@cspell/dict-r@2.0.4': + resolution: {integrity: sha512-cBpRsE/U0d9BRhiNRMLMH1PpWgw+N+1A2jumgt1if9nBGmQw4MUpg2u9I0xlFVhstTIdzXiLXMxP45cABuiUeQ==} + + '@cspell/dict-ruby@5.0.7': + resolution: {integrity: sha512-4/d0hcoPzi5Alk0FmcyqlzFW9lQnZh9j07MJzPcyVO62nYJJAGKaPZL2o4qHeCS/od/ctJC5AHRdoUm0ktsw6Q==} + + '@cspell/dict-rust@4.0.10': + resolution: {integrity: sha512-6o5C8566VGTTctgcwfF3Iy7314W0oMlFFSQOadQ0OEdJ9Z9ERX/PDimrzP3LGuOrvhtEFoK8pj+BLnunNwRNrw==} + + '@cspell/dict-scala@5.0.6': + resolution: {integrity: sha512-tl0YWAfjUVb4LyyE4JIMVE8DlLzb1ecHRmIWc4eT6nkyDqQgHKzdHsnusxFEFMVLIQomgSg0Zz6hJ5S1E4W4ww==} + + '@cspell/dict-software-terms@4.1.15': + resolution: {integrity: sha512-mxX6jIDA6u7BkR2NkxycA+hf41LsaaQTN/9a6hY2UK9vwNS1cAgAIxUr7YDGU3kZ3sqg58XOYX/KFw/PJtMRmQ==} + + '@cspell/dict-sql@2.1.8': + resolution: {integrity: sha512-dJRE4JV1qmXTbbGm6WIcg1knmR6K5RXnQxF4XHs5HA3LAjc/zf77F95i5LC+guOGppVF6Hdl66S2UyxT+SAF3A==} + + '@cspell/dict-svelte@1.0.5': + resolution: {integrity: sha512-sseHlcXOqWE4Ner9sg8KsjxwSJ2yssoJNqFHR9liWVbDV+m7kBiUtn2EB690TihzVsEmDr/0Yxrbb5Bniz70mA==} + + '@cspell/dict-swift@2.0.4': + resolution: {integrity: sha512-CsFF0IFAbRtYNg0yZcdaYbADF5F3DsM8C4wHnZefQy8YcHP/qjAF/GdGfBFBLx+XSthYuBlo2b2XQVdz3cJZBw==} + + '@cspell/dict-terraform@1.0.6': + resolution: {integrity: sha512-Sqm5vGbXuI9hCFcr4w6xWf4Y25J9SdleE/IqfM6RySPnk8lISEmVdax4k6+Kinv9qaxyvnIbUUN4WFLWcBPQAg==} + + '@cspell/dict-typescript@3.1.11': + resolution: {integrity: sha512-FwvK5sKbwrVpdw0e9+1lVTl8FPoHYvfHRuQRQz2Ql5XkC0gwPPkpoyD1zYImjIyZRoYXk3yp9j8ss4iz7A7zoQ==} + + '@cspell/dict-vue@3.0.3': + resolution: {integrity: sha512-akmYbrgAGumqk1xXALtDJcEcOMYBYMnkjpmGzH13Ozhq1mkPF4VgllFQlm1xYde+BUKNnzMgPEzxrL2qZllgYA==} + + '@cspell/dynamic-import@8.15.5': + resolution: {integrity: sha512-xfLRVi8zHKCGK1fg1ixXQ0bAlIU9sGm7xfbTmGG8TQt+iaKHVMIlt+XeCAo0eE7aKjIaIfqcC/PCIdUJiODuGA==} + engines: {node: '>=18.0'} + + '@cspell/filetypes@8.15.5': + resolution: {integrity: sha512-ljEFUp61mw5RWZ3S6ke6rvGKy8m4lZZjRd5KO07RYyGwSeLa4PX9AyTgSzuqXiN9y1BwogD3xolCMfPsMrtZIQ==} + engines: {node: '>=18'} + + '@cspell/strong-weak-map@8.15.5': + resolution: {integrity: sha512-7VzDAXsJPDXllTIi9mvQwd7PR43TPk1Ix3ocLTZDVNssf1cQbmLiQX6YWk0k8FWGfIPoIMlByw4tTSizRJcTcw==} + engines: {node: '>=18'} + + '@cspell/url@8.15.5': + resolution: {integrity: sha512-z8q7LUppFiNvytX2qrKDkXcsmOrwjqFf/5RkcpNppDezLrFejaMZu4BEVNcPrFCeS2J04K+uksNL1LYSob8jCg==} + engines: {node: '>=18.0'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@emnapi/core@1.3.1': + resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.17.1': + resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.13.0': + resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.7.0': + resolution: {integrity: sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/json@0.6.0': + resolution: {integrity: sha512-xlYoULv2QIeJnjFP4RVbPMpaGplsYo0vSIBpXP/QRnoi7oDYhVZ4u3wE5UUwI8hnhTQUMozrDhyuVFXMQ1HkMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/markdown@6.2.1': + resolution: {integrity: sha512-cKVd110hG4ICHmWhIwZJfKmmJBvbiDWyrHODJknAtudKgZtlROGoLX9UEOA0o746zC0hCY4UV4vR+aOGW9S6JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.2': + resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@hapi/boom@9.1.4': + resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==} + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/momoa@2.0.4': + resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} + engines: {node: '>=10.10.0'} + + '@humanwhocodes/momoa@3.3.3': + resolution: {integrity: sha512-5EKzSg1FH5wpg0HXBsglgC5u9U4qFgvZX7u8oVDP6XH6Mh9kmz4iQZV9/88xMdQ/UGQNxckf5njK65gU9jjS0w==} + engines: {node: '>=18'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.1.33': + resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@mermaid-js/mermaid-cli@11.4.0': + resolution: {integrity: sha512-NNLhoW4o9y3bYCd44f4Uk/APXRuq/qrtAet3oHXtVAqYiO6NlvYF/RdLW/pIQPljX+BQ/oXXotXHckmjgriWWQ==} + engines: {node: ^18.19 || >=20.0} + hasBin: true + peerDependencies: + puppeteer: ^23 + + '@mermaid-js/parser@0.3.0': + resolution: {integrity: sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==} + + '@mocks-server/admin-api-client@8.0.0-beta.2': + resolution: {integrity: sha512-n7rFjiT4a+zDd1D5SRZAILF42h+WdMyZJTmYP6ev6XMyiAB65j968IR3NkAqO696pMSa/92hxelpdXIYcSstkw==} + engines: {node: '>=14.0.0'} + + '@mocks-server/admin-api-paths@5.0.0': + resolution: {integrity: sha512-HlFhNnHtvY9ZZGthaatAFvGFofTCs1Yj3kxeb5EmvdHBgNC+dZxfMjXWCvuIhp7qFNaTFarPgGJ3Zsm7+o/siw==} + engines: {node: '>=14.x'} + + '@mocks-server/config@2.0.0-beta.3': + resolution: {integrity: sha512-CLd3DN3rOtYO0eIPUYd1nuHNNMehzTrBP23rRipvma4ylvzqUNvjbSpFY1vyvpV9kI6Y2WlpthrK7KCIqQx5Fg==} + engines: {node: '>=14.x'} + + '@mocks-server/core@5.0.0-beta.3': + resolution: {integrity: sha512-aclKGMHFNFN3/W8CYweyYVdJ9/icwM5vnUHYOwdWPzdFv+R3bUtLiDIKWF8kKfOGAky9WMl7HrjT0M01DUt48Q==} + engines: {node: '>=14.x'} + + '@mocks-server/logger@2.0.0-beta.2': + resolution: {integrity: sha512-5IVbkwJabyJ/0Mo6tDT94UuIKWQFDyJtVsrTZrihhq0ZLxrvA6E7XtkLveSqIDmFR6C/uDQHL9Gr4GaihJIzhw==} + engines: {node: '>=14.x'} + + '@mocks-server/main@5.0.0-beta.4': + resolution: {integrity: sha512-NszzWd1MzMCzOuGlWwYgiAy9fQHyrdHJ49xNq5/ihwL9l3LUoR2uimOdYvMPRUDmt7Ci1F4KWh5CmGelrUiUeA==} + engines: {node: '>=14.0.0'} + hasBin: true + + '@mocks-server/nested-collections@3.0.0-beta.2': + resolution: {integrity: sha512-72djrxWBiVaVt+KENojc9RulSDgO5nyrQk5owQ5lAuJrCL9+VRgx06463LbORkgvjTk13w/jiL1603qSJeDdLQ==} + engines: {node: '>=14.x'} + + '@mocks-server/plugin-admin-api@5.0.0-beta.4': + resolution: {integrity: sha512-U1RKFVya20qhHkGEUIitxitSg7T+Bv1fe2svOL4XQDftR+62tgtXHYYdS54sruC0dnwDzyH93qi/LSkuSUh4aQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mocks-server/core': 5.0.0-beta.3 + + '@mocks-server/plugin-inquirer-cli@5.0.0-beta.4': + resolution: {integrity: sha512-2rZNJUK+9PtUnH4NOrD72DtYsHK9y4GgLI5hOeixnHZKPonMiHmse/DftMtESrQTtI00q0mLIrM7IZSwpBjYnQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mocks-server/core': 5.0.0-beta.3 + + '@mocks-server/plugin-openapi@3.0.0-beta.4': + resolution: {integrity: sha512-uj+O71G+swxxg0UtTVZ68tl2uftqd7SoGKTj2B8Ru3F2uagClMNB1RL6T62IVSSi1xlWb6A7h+lmtywKnhwluw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mocks-server/core': 5.0.0-beta.3 + + '@mocks-server/plugin-proxy@5.0.0-beta.4': + resolution: {integrity: sha512-FQBU8V6A9r4qYsnV+sB6uaL2xHMXgh89C+hpl7DN+UIzRn7+vgZdy951VffFtYEMKDT8p0kIEiWJz4AeJB3bXQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mocks-server/core': 5.0.0-beta.3 + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@nx/nx-darwin-arm64@20.1.0': + resolution: {integrity: sha512-fel9LpSWuwY0cGAsRFEPxLp6J5VcK/5sjeWA0lZWrFf1oQJCOlKBfkxzi384Nd7eK5JSjxIXrpYfRLaqSbp+IA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@20.1.0': + resolution: {integrity: sha512-l1DB8dk2rCLGgXW26HmFOKYpUCF259LRus8z+z7dsFv5vz9TeN+fk5m9aAdiENgMA2cGlndQQW+E8UIo3yv+9g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@20.1.0': + resolution: {integrity: sha512-f8uMRIhiOA/73cIjiyS3gpKvaAtsHgyUkkoCOPc4xdxoSLAjlxb6VOUPIFj9rzLA6qQXImVpsiNPG+p1sJ1GAQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@20.1.0': + resolution: {integrity: sha512-M7pay8hFJQZ3uJHlr5hZK/8o1BcHt95hy/SU7Azt7+LKQGOy42tXhHO30As9APzXqRmvoA2Iq1IyrJJicrz+Ew==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@20.1.0': + resolution: {integrity: sha512-A5+Kpk1uwYIj6CPm0DWLVz5wNTN4ewNl7ajLS9YJOi4yHx/FhfMMyPj4ZnbTpc4isuvgZwBZNl8kwFb2RdXq4w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-arm64-musl@20.1.0': + resolution: {integrity: sha512-pWIQPt9Fst1O4dgrWHdU1b+5wpfLmsmaSeRvLQ9b2VFp3tKGko4ie0skme62TuMgpcqMWDBFKs8KgbHESOi7vw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-x64-gnu@20.1.0': + resolution: {integrity: sha512-sOpeGOHznk2ztCXzKhRPAKG3WtwaQUsfQ/3aYDXE6z+rSfyZTGY29M/a9FbdjI4cLJX+NdLAAMj15c3VecJ65g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@nx/nx-linux-x64-musl@20.1.0': + resolution: {integrity: sha512-SxnQJhjOvuOfUZnF4Wt4/O/l1e21qpACZzfMaPIvmiTLk9zPJZWtfgbqlKtTXHKWq9DfIUZQqZXRIpHPM1sDZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@nx/nx-win32-arm64-msvc@20.1.0': + resolution: {integrity: sha512-Z/KoaAA+Rg9iqqOPkKZV61MejPoJBOHlecFpq0G4TgKMJEJ/hEsjojq5usO1fUGAbvQT/SXL3pYWgZwhD3VEHw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@20.1.0': + resolution: {integrity: sha512-pbxacjLsW9vXD9FibvU8Pal/r5+Yq6AaW6I57CDi7jsLt+K6jcS0fP4FlfXT8QFWdx9+bOKNfOsKEIwpirMN1w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@puppeteer/browsers@0.5.0': + resolution: {integrity: sha512-Uw6oB7VvmPRLE4iKsjuOh8zgDabhNX67dzo8U/BB0f9527qx+4eeUs+korU98OhG5C4ubg7ufBgVi63XYwS6TQ==} + engines: {node: '>=14.1.0'} + hasBin: true + peerDependencies: + typescript: '>= 4.7.4' + peerDependenciesMeta: + typescript: + optional: true + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sindresorhus/is@0.14.0': + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@szmarczak/http-timer@1.1.2': + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.6': + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.0.3': + resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/dompurify@3.0.5': + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/geojson@7946.0.14': + resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} + + '@types/glob@8.1.0': + resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/parse5@5.0.3': + resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} + + '@types/parse5@6.0.3': + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/tmp@0.2.6': + resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/which@3.0.4': + resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@8.14.0': + resolution: {integrity: sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.14.0': + resolution: {integrity: sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.14.0': + resolution: {integrity: sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.14.0': + resolution: {integrity: sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.14.0': + resolution: {integrity: sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.14.0': + resolution: {integrity: sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.14.0': + resolution: {integrity: sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.14.0': + resolution: {integrity: sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@yarnpkg/parsers@3.0.2': + resolution: {integrity: sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==} + engines: {node: '>=18.12.0'} + + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.11.0: + resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atlassian-jwt@2.0.3: + resolution: {integrity: sha512-G9oO3HHS1UKgsLRXj6nNKv2TY6g3PleBCdzHwbFeVKg+18GBFIMRz+ApxuOuWAgcL7RngNFF5rGNtw1Ss3hvTg==} + engines: {node: '>= 0.4.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} + + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-module-resolver@5.0.2: + resolution: {integrity: sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==} + + babel-plugin-polyfill-corejs2@0.4.12: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.3: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-transform-import-meta@2.2.1: + resolution: {integrity: sha512-AxNh27Pcg8Kt112RGa3Vod2QS2YXKKJ6+nSvRtv7qQTJAdx0MZa4UHZ4lnxHUWA2MNbLuZQv5FVab4P1CoLOWw==} + peerDependencies: + '@babel/core': ^7.10.0 + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-ajv-errors@1.2.0: + resolution: {integrity: sha512-UW+IsFycygIo7bclP9h5ugkNH8EjCSgqyFB/yQ4Hqqa1OEYDtb0uFIkYE0b6+CjkgJYVM5UKI/pJPxjYe9EZlA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + ajv: 4.11.8 - 8 + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-parser@1.20.0: + resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001680: + resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk-template@1.1.0: + resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==} + engines: {node: '>=14.16'} + + chalk@4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chromium-bidi@0.4.7: + resolution: {integrity: sha512-6+mJuFXwTMU6I3vYLs6IL8A1DyQTPjCfIL971X0aMPVGRbGnNfl6i6Cl0NMbxi2bRYLGESt9T2ZIMRM5PAEcIQ==} + peerDependencies: + devtools-protocol: '*' + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + + clear-module@4.1.2: + resolution: {integrity: sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==} + engines: {node: '>=8'} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} + + confluence.js@1.7.4: + resolution: {integrity: sha512-MBTpAQ5EHTnVAaMDlTiRMqJau5EMejnoZMrvE/2QsMZo7dFw+OgadLIKr43mYQcN/qZ0Clagw0iHb4A3FaC5OQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cosmiconfig@7.0.1: + resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} + engines: {node: '>=10'} + + cosmiconfig@8.1.3: + resolution: {integrity: sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==} + engines: {node: '>=14'} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-fetch@3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + cspell-config-lib@8.15.5: + resolution: {integrity: sha512-16XBjAlUWO46uEuUKHQvSeiU7hQzG9Pqg6lwKQOyZ/rVLZyihk7JGtnWuG83BbW0RFokB/BcgT1q6OegWJiEZw==} + engines: {node: '>=18'} + + cspell-dictionary@8.15.5: + resolution: {integrity: sha512-L+4MD3KItFGsxR8eY2ed6InsD7hZU1TIAeV2V4sG0wIbUXJXbPFxBTNZJrPLxTzAeCutHmkZwAl4ZCGu18bgtw==} + engines: {node: '>=18'} + + cspell-gitignore@8.15.5: + resolution: {integrity: sha512-z5T0Xswfiu2NbkoVdf6uwEWzOgxCBb3L8kwB6lxzK5iyQDW2Bqlk+5b6KQaY38OtjTjJ9zzIJfFN3MfFlMFd3A==} + engines: {node: '>=18'} + hasBin: true + + cspell-glob@8.15.5: + resolution: {integrity: sha512-VpfP16bRbkHEyGGjf6/EifFxETfS7lpcHbYt1tRi6VhCv1FTMqbB7H7Aw+DQkDezOUN8xdw0gYe/fk5AJGOBDg==} + engines: {node: '>=18'} + + cspell-grammar@8.15.5: + resolution: {integrity: sha512-2YnlSATtWHNL6cgx1qmTsY5ZO0zu8VdEmfcLQKgHr67T7atLRUnWAlmh06WMLd5qqp8PpWNPaOJF2prEYAXsUA==} + engines: {node: '>=18'} + hasBin: true + + cspell-io@8.15.5: + resolution: {integrity: sha512-6kBK+EGTG9hiUDfB55r3xbhc7YUA5vJTXoc65pe9PXd4vgXXfrPRuy+5VRtvgSMoQj59oWOQw3ZqTAR95gbGnw==} + engines: {node: '>=18'} + + cspell-lib@8.15.5: + resolution: {integrity: sha512-DGieMWc82ouHb6Rq2LRKAlG4ExeQL1D5uvemgaouVHMZq4GvPtVaTwA6qHhw772/5z665oOVsRCicYbDtP4V3w==} + engines: {node: '>=18'} + + cspell-trie-lib@8.15.5: + resolution: {integrity: sha512-DAEkp51aFgpp9DFuJkNki0kVm2SVR1Hp0hD3Pnta7S4X2h5424TpTVVPltAIWtcdxRLGbX6N2x26lTI4K/YfpQ==} + engines: {node: '>=18'} + + cspell@8.15.5: + resolution: {integrity: sha512-Vp1WI6axghVvenZS7GUlsZf6JFF7jDXdV5f4nXWjrZLbTrH+wbnFEO2mg+QJWa4IN35igjNYeu9TbA9/EGJzog==} + engines: {node: '>=18'} + hasBin: true + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.30.3: + resolution: {integrity: sha512-HncJ9gGJbVtw7YXtIs3+6YAFSSiKsom0amWc33Z7QbylbY2JGMrA0yz4EwrdTScZxnwclXeEZHzO5pxoy0ZE4g==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.11: + resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + engines: {node: '>=0.10.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + devtools-protocol@0.0.1107588: + resolution: {integrity: sha512-yIR+pG9x65Xko7bErCUSQaDLrO/P1p3JUzEk7JCU4DowPcGHkTGUGQapcfcLc4qj0UaALwZ+cr0riFgiqpixcg==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dompurify@3.1.6: + resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv-expand@11.0.6: + resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.56: + resolution: {integrity: sha512-7lXb9dAvimCFdvUMTyucD4mnIndt/xhRKFAlky0CyFogdnNmdPQNoHI23msF/2V4mpTxMzgMdjK4+YRlFlRQZw==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + entities@4.3.0: + resolution: {integrity: sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==} + engines: {node: '>=0.12'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.23.4: + resolution: {integrity: sha512-HR1gxH5OaiN7XH7uiWH0RLw0RcFySiSoW1ctxmD1ahTw3uGBtkmm/ng0tDU1OtYx5OK6EOL5Y6O21cDflG3Jcg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@2.1.1: + resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} + engines: {node: '>=8'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-alias@1.1.2: + resolution: {integrity: sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==} + engines: {node: '>= 4'} + peerDependencies: + eslint-plugin-import: '>=1.4.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.6.3: + resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jest@28.9.0: + resolution: {integrity: sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==} + engines: {node: ^16.10.0 || ^18.12.0 || >=20.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 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + + eslint-plugin-prettier@5.1.3: + resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.7.0: + resolution: {integrity: sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@2.1.0: + resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==} + + estree-util-visit@1.2.1: + resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + express-http-proxy@1.6.3: + resolution: {integrity: sha512-/l77JHcOUrDUX8V67E287VEUQT0lbm71gdGVoodnlWBziarYKgMcpqT7xvh/HM8Jv52phw8Bd8tY+a7QjOr7Yg==} + engines: {node: '>=6.0.0'} + + express-request-id@1.4.1: + resolution: {integrity: sha512-qpxK6XhDYtdx9FvxwCHkUeZVWtkGbWR87hBAzGECfwYF/QQCPXEwwB2/9NGkOR1tT7/aLs9mma3CT0vjSzuZVw==} + + express@4.18.1: + resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==} + engines: {node: '>= 0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-entry-cache@9.1.0: + resolution: {integrity: sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==} + engines: {node: '>=18'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + + find-babel-config@2.1.2: + resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} + + find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@5.0.0: + resolution: {integrity: sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==} + engines: {node: '>=18'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + formidable@2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensequence@7.0.0: + resolution: {integrity: sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==} + engines: {node: '>=18'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + glob@7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globule@1.3.4: + resolution: {integrity: sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==} + engines: {node: '>= 0.10'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphlib@2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-yarn@2.1.0: + resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-to-hyperscript@9.0.1: + resolution: {integrity: sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==} + + hast-util-from-parse5@6.0.1: + resolution: {integrity: sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==} + + hast-util-from-parse5@7.1.2: + resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} + + hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + + hast-util-parse-selector@3.1.1: + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} + + hast-util-raw@6.1.0: + resolution: {integrity: sha512-5FoZLDHBpka20OlZZ4I/+RBw5piVQ8iI1doEvffQhx5CbCyTtP8UCq8Tw6NmTAMtXgsQxmhW7Ly8OdFre5/YMQ==} + + hast-util-raw@7.2.3: + resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} + + hast-util-to-html@8.0.4: + resolution: {integrity: sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==} + + hast-util-to-parse5@6.0.0: + resolution: {integrity: sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==} + + hast-util-to-parse5@7.1.0: + resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} + + hast-util-to-string@2.0.0: + resolution: {integrity: sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==} + + hast-util-whitespace@2.0.1: + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + + hast@1.0.0: + resolution: {integrity: sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA==} + deprecated: Renamed to rehype + + hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + + hastscript@7.2.0: + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + + hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-void-elements@1.0.5: + resolution: {integrity: sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.0.11: + resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-lazy@2.1.0: + resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} + engines: {node: '>=4'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inquirer-autocomplete-prompt@1.4.0: + resolution: {integrity: sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==} + engines: {node: '>=10'} + peerDependencies: + inquirer: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + inquirer@8.2.4: + resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} + engines: {node: '>=12.0.0'} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + + is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-bun-module@1.2.1: + resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-npm@5.0.0: + resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==} + engines: {node: '>=10'} + + is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + + is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-yarn-global@0.3.0: + resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-sonar@0.2.16: + resolution: {integrity: sha512-ES6Z9BbIVDELtbz+/b6pv41B2qOfp38cQpoCLqei21FtlkG/GzhyQ0M3egEIM+erpJOkpRKM8Tc8/YQtHdiTXA==} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-refs@3.0.15: + resolution: {integrity: sha512-0vOQd9eLNBL18EGl5yYaO44GhixmImes2wiYn9Z3sag3QnehWrYWlB9AFtMxCL2Bj3fyxgDYkxGFEU/chlYssw==} + engines: {node: '>=0.8'} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + jsuri@1.3.1: + resolution: {integrity: sha512-LLdAeqOf88/X0hylAI7oSir6QUsz/8kOW0FcJzzu/SJRfORA/oPHycAOthkNp7eLPlTAbqVDFbqNRHkRVzEA3g==} + + katex@0.16.11: + resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==} + hasBin: true + + keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + langium@3.0.0: + resolution: {integrity: sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==} + engines: {node: '>=16.0.0'} + + latest-version@5.1.0: + resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} + engines: {node: '>=8'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lint-staged@15.2.10: + resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.iteratee@4.7.0: + resolution: {integrity: sha512-yv3cSQZmfpbIKo4Yo45B1taEvxjNvcpF1CEOc0Y6dEyvhPIfEJE3twDwPgWTPQubcSgXyBwBKG6wpQvWMDOf6Q==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@13.0.3: + resolution: {integrity: sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==} + engines: {node: '>= 18'} + hasBin: true + + mdast-squeeze-paragraphs@5.2.1: + resolution: {integrity: sha512-npINYQrt0E5AvSvM7ZxIIyrG/7DX+g8jKWcJMudrcjI+b1eNOKbbu+wTo6cKvy5IzH159IPfpWoRVH7kwEmnug==} + + mdast-util-definitions@5.1.2: + resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} + + mdast-util-directive@2.2.4: + resolution: {integrity: sha512-sK3ojFP+jpj1n7Zo5ZKvoxP1MvLyzVG63+gm40Z/qI00avzdPCYxt7RBMgofwAva9gBjbDBWVRB/i+UD+fUCzQ==} + + mdast-util-find-and-replace@2.2.2: + resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-frontmatter@1.0.1: + resolution: {integrity: sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw==} + + mdast-util-gfm-autolink-literal@1.0.3: + resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@1.0.2: + resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@1.0.3: + resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@1.0.7: + resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@1.0.2: + resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@2.0.2: + resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-mdx-expression@1.3.2: + resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@2.1.4: + resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==} + + mdast-util-mdx-jsx@3.1.3: + resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} + + mdast-util-mdx@2.0.1: + resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@1.3.1: + resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@3.0.1: + resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@12.3.0: + resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} + + mdast-util-to-markdown@1.5.0: + resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} + + mdast-util-to-markdown@2.1.1: + resolution: {integrity: sha512-OrkcCoqAkEg9b1ykXBrA0ehRc8H4fGU/03cACmW2xXzau1+dIdS+qJugh1Cqex3hMumSBgSE/5pc7uqP12nLAw==} + + mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.4.0: + resolution: {integrity: sha512-mxCfEYvADJqOiHfGpJXLs4/fAjHz448rH0pfY5fAoxiz70rQiDSzUUy4dNET2T08i46IVpjohPd6WWbzmRHiPA==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + + micromark-core-commonmark@2.0.1: + resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + + micromark-extension-directive@2.2.1: + resolution: {integrity: sha512-ZFKZkNaEqAP86IghX1X7sE8NNnx6kFNq9mSBRvEHjArutTCJZ3LYg6VH151lXVb1JHpmIcW/7rX25oMoIHuSug==} + + micromark-extension-frontmatter@1.1.1: + resolution: {integrity: sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ==} + + micromark-extension-gfm-autolink-literal@1.0.5: + resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@1.1.2: + resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@1.0.7: + resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@1.0.7: + resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} + + micromark-extension-gfm-table@2.1.0: + resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + + micromark-extension-gfm-tagfilter@1.0.2: + resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@1.0.5: + resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@2.0.3: + resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@1.0.8: + resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==} + + micromark-extension-mdx-jsx@1.0.5: + resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==} + + micromark-extension-mdx-md@1.0.1: + resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==} + + micromark-extension-mdxjs-esm@1.0.5: + resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==} + + micromark-extension-mdxjs@1.0.1: + resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} + + micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + + micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + + micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + + micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + + micromark-factory-mdx-expression@1.0.9: + resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + + micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + + micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + + micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + + micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + + micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + + micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + + micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + + micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + + micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + + micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + + micromark-util-decode-numeric-character-reference@2.0.1: + resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + + micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + + micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + + micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-events-to-acorn@1.2.3: + resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==} + + micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + + micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} + + micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + + micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + + micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + + micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + + micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + + micromark-util-subtokenize@2.0.1: + resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + + micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + minimatch@3.0.8: + resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.0: + resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.7.3: + resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + native-promise-only@0.8.1: + resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-machine-id@1.1.12: + resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + node-watch@0.7.3: + resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} + engines: {node: '>=6'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nx@20.1.0: + resolution: {integrity: sha512-d8Ywh1AvG3szYqWEHg2n9DHh/hF0jtVhMZKxwsr7n+kSVxp7gE/rHCCfOo8H+OmP030qXoox5e4Ovp7H9CEJnA==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.8.0 + '@swc/core': ^1.3.85 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + oauth@0.10.0: + resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openapi-types@12.0.2: + resolution: {integrity: sha512-GuTo7FyZjOIWVhIhQSWJVaws6A82sWIGyQogxxYBYKZ0NBdyP2CYSIgOwFfSB+UVoPExk/YzFpyYitHS8KVZtA==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.3.0: + resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} + engines: {node: '>=10'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json@6.5.0: + resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} + engines: {node: '>=8'} + + package-manager-detector@0.2.2: + resolution: {integrity: sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parent-module@2.0.0: + resolution: {integrity: sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==} + engines: {node: '>=8'} + + parse-entities@4.0.1: + resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-loader@1.0.12: + resolution: {integrity: sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@1.2.1: + resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pupa@2.1.1: + resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} + engines: {node: '>=8'} + + puppeteer-core@19.11.1: + resolution: {integrity: sha512-qcuC2Uf0Fwdj9wNtaTZ2OvYRraXpAK+puwwVW8ofOhOgLPZyz1c68tsorfIZyCUOpyBisjr+xByu7BMbEYMepA==} + engines: {node: '>=14.14.0'} + peerDependencies: + typescript: '>= 4.7.4' + peerDependenciesMeta: + typescript: + optional: true + + puppeteer@19.11.1: + resolution: {integrity: sha512-39olGaX2djYUdhaQQHDZ0T0GwEp+5f9UB9HmEP0qHfdQHIq0xGQZuAZ5TLnJIc/88SrPLpEflPC+xUqOTv3c5g==} + deprecated: < 22.8.2 is no longer supported + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.10.3: + resolution: {integrity: sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==} + engines: {node: '>=0.6'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + + regexpu-core@6.1.1: + resolution: {integrity: sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==} + engines: {node: '>=4'} + + registry-auth-token@4.2.2: + resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} + engines: {node: '>=6.0.0'} + + registry-url@5.1.0: + resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} + engines: {node: '>=8'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.11.2: + resolution: {integrity: sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==} + hasBin: true + + rehype-parse@8.0.5: + resolution: {integrity: sha512-Ds3RglaY/+clEX2U2mHflt7NlMA72KspZ0JLUJgBBLpRddBcEw3H8uYZQliQriku22NZpYMfjDdSgHcjxue24A==} + + rehype-raw@5.1.0: + resolution: {integrity: sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA==} + + rehype-stringify@9.0.4: + resolution: {integrity: sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==} + + rehype@12.0.1: + resolution: {integrity: sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==} + + remark-directive@2.0.1: + resolution: {integrity: sha512-oosbsUAkU/qmUE78anLaJePnPis4ihsE7Agp0T/oqTzvTea8pOiaYEtfInU/+xMOVTS9PN5AhGOiaIVe4GD8gw==} + + remark-frontmatter@4.0.1: + resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==} + + remark-gfm@3.0.1: + resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + + remark-mdx@2.3.0: + resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==} + + remark-parse-frontmatter@1.0.3: + resolution: {integrity: sha512-2hqW4Nod8pEkP4kdui7jOvCwcTfYBfgAb3XJhYYTZGrTMBcxFzQ7h7ay6OwmpJME5BOhORpZ7eY5+K4OHHIh4Q==} + engines: {node: '>=12'} + + remark-parse@10.0.2: + resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} + + remark-rehype@10.1.0: + resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + + remark-stringify@10.0.3: + resolution: {integrity: sha512-koyOzCMYoUHudypbj4XpnAKFbkddRMYZHwghnxd7ue5210WzGw6kOBwauJTRUMq16jsovXx8dYNvSSWP89kZ3A==} + + remark-unlink@4.0.1: + resolution: {integrity: sha512-TQJ0J/O5k0TG+9UqKGwt4nOGRXjIijd7Z2p83f1iVPAkgOFjYj6pfx03ixKEoYJYi0spzah59L3mUVP9GP+pag==} + + remark@14.0.3: + resolution: {integrity: sha512-bfmJW1dmR2LvaMJuAnE88pZP9DktIFYXazkTfOIKZzi3Knk9lT0roItIA24ydOucI3bV/g/tXBA6hzqq3FV9Ew==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + revalidator@0.3.1: + resolution: {integrity: sha512-orq+Nw+V5pDpQwGEuN2n1AgJ+0A8WqhFHKt5KgkxfAowUKgO1CWV32IR3TNB4g9/FX3gJt9qBJO8DYlwonnB0Q==} + engines: {node: '>= 0.8.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver-diff@3.1.1: + resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} + engines: {node: '>=8'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + start-server-and-test@2.0.8: + resolution: {integrity: sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==} + engines: {node: '>=16'} + hasBin: true + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-object@0.3.0: + resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} + + stylis@4.3.4: + resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + + superagent@7.1.6: + resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} + engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swagger-ui-dist@4.14.0: + resolution: {integrity: sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw==} + + synckit@0.8.8: + resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-vfile@7.2.4: + resolution: {integrity: sha512-2eQ+rJ2qGbyw3senPI0qjuM7aut8IYXK6AEoOWb+fJx/mQYzviTckm1wDjq91QYHAPBTYzmdJXxMFA6Mk14mdw==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + unist-builder@4.0.0: + resolution: {integrity: sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==} + + unist-util-find-after@4.0.1: + resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} + + unist-util-find@1.0.4: + resolution: {integrity: sha512-T5vI7IkhroDj7KxAIy057VbIeGnCXfso4d4GoUsjbAmDLQUkzAeszlBtzx1+KHgdsYYBygaqUBvrbYCfePedZw==} + + unist-util-generated@2.0.1: + resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position-from-estree@1.1.2: + resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==} + + unist-util-position@3.1.0: + resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} + + unist-util-position@4.0.4: + resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} + + unist-util-remove-position@4.0.2: + resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} + + unist-util-remove@3.1.1: + resolution: {integrity: sha512-kfCqZK5YVY5yEa89tvpl7KnBBHu2c6CzMkqHUrlOqaRgGOMp0sMvwWOVrbAtj03KhovQB7i96Gda72v/EFE0vw==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-notifier@5.1.0: + resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} + engines: {node: '>=10'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-location@3.2.0: + resolution: {integrity: sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==} + + vfile-location@4.1.0: + resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + + vfile-message@3.1.4: + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@4.2.1: + resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} + + vfile@5.3.7: + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + wait-on@8.0.1: + resolution: {integrity: sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==} + engines: {node: '>=12.0.0'} + hasBin: true + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-namespaces@1.1.4: + resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + winston-array-transport@1.1.10: + resolution: {integrity: sha512-9VQ4NWDWG9MPGh9qdz7hkRVONwp6td2UihpRQlScFdHRp/XrfcgbuhMGS7ddxRVAB3bhVNnyHIi6+XDSaU1ulQ==} + engines: {node: '>=10'} + + winston-transport@4.5.0: + resolution: {integrity: sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==} + engines: {node: '>= 6.4.0'} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.8.2: + resolution: {integrity: sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.1.1: + resolution: {integrity: sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==} + engines: {node: '>= 14'} + + yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + + yaml@2.5.1: + resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} + engines: {node: '>= 14'} + hasBin: true + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.1: + resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + + zwitch@1.0.5: + resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.2 + tinyexec: 0.3.1 + + '@antfu/utils@0.7.10': {} + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.18.13': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.18.13) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 1.9.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.25.9 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.1.1 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.3.7 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.18.13)': + dependencies: + '@babel/core': 7.18.13 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-simple-access@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-simple-access': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/preset-env@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.0 + esutils: 2.0.3 + + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/register@7.18.9(@babel/core@7.18.13)': + dependencies: + '@babel/core': 7.18.13 + clone-deep: 4.0.1 + find-cache-dir: 2.1.0 + make-dir: 2.1.0 + pirates: 4.0.6 + source-map-support: 0.5.21 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bcoe/v8-coverage@0.2.3': {} + + '@braintree/sanitize-url@7.1.0': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@colors/colors@1.5.0': {} + + '@colors/colors@1.6.0': {} + + '@cspell/cspell-bundled-dicts@8.15.5': + dependencies: + '@cspell/dict-ada': 4.0.5 + '@cspell/dict-al': 1.0.3 + '@cspell/dict-aws': 4.0.7 + '@cspell/dict-bash': 4.1.8 + '@cspell/dict-companies': 3.1.7 + '@cspell/dict-cpp': 5.1.23 + '@cspell/dict-cryptocurrencies': 5.0.3 + '@cspell/dict-csharp': 4.0.5 + '@cspell/dict-css': 4.0.16 + '@cspell/dict-dart': 2.2.4 + '@cspell/dict-django': 4.1.3 + '@cspell/dict-docker': 1.1.11 + '@cspell/dict-dotnet': 5.0.8 + '@cspell/dict-elixir': 4.0.6 + '@cspell/dict-en-common-misspellings': 2.0.7 + '@cspell/dict-en-gb': 1.1.33 + '@cspell/dict-en_us': 4.3.27 + '@cspell/dict-filetypes': 3.0.8 + '@cspell/dict-flutter': 1.0.3 + '@cspell/dict-fonts': 4.0.3 + '@cspell/dict-fsharp': 1.0.4 + '@cspell/dict-fullstack': 3.2.3 + '@cspell/dict-gaming-terms': 1.0.8 + '@cspell/dict-git': 3.0.3 + '@cspell/dict-golang': 6.0.16 + '@cspell/dict-google': 1.0.4 + '@cspell/dict-haskell': 4.0.4 + '@cspell/dict-html': 4.0.10 + '@cspell/dict-html-symbol-entities': 4.0.3 + '@cspell/dict-java': 5.0.10 + '@cspell/dict-julia': 1.0.4 + '@cspell/dict-k8s': 1.0.9 + '@cspell/dict-latex': 4.0.3 + '@cspell/dict-lorem-ipsum': 4.0.3 + '@cspell/dict-lua': 4.0.6 + '@cspell/dict-makefile': 1.0.3 + '@cspell/dict-markdown': 2.0.7(@cspell/dict-css@4.0.16)(@cspell/dict-html-symbol-entities@4.0.3)(@cspell/dict-html@4.0.10)(@cspell/dict-typescript@3.1.11) + '@cspell/dict-monkeyc': 1.0.9 + '@cspell/dict-node': 5.0.5 + '@cspell/dict-npm': 5.1.12 + '@cspell/dict-php': 4.0.13 + '@cspell/dict-powershell': 5.0.13 + '@cspell/dict-public-licenses': 2.0.11 + '@cspell/dict-python': 4.2.12 + '@cspell/dict-r': 2.0.4 + '@cspell/dict-ruby': 5.0.7 + '@cspell/dict-rust': 4.0.10 + '@cspell/dict-scala': 5.0.6 + '@cspell/dict-software-terms': 4.1.15 + '@cspell/dict-sql': 2.1.8 + '@cspell/dict-svelte': 1.0.5 + '@cspell/dict-swift': 2.0.4 + '@cspell/dict-terraform': 1.0.6 + '@cspell/dict-typescript': 3.1.11 + '@cspell/dict-vue': 3.0.3 + + '@cspell/cspell-json-reporter@8.15.5': + dependencies: + '@cspell/cspell-types': 8.15.5 + + '@cspell/cspell-pipe@8.15.5': {} + + '@cspell/cspell-resolver@8.15.5': + dependencies: + global-directory: 4.0.1 + + '@cspell/cspell-service-bus@8.15.5': {} + + '@cspell/cspell-types@8.15.5': {} + + '@cspell/dict-ada@4.0.5': {} + + '@cspell/dict-al@1.0.3': {} + + '@cspell/dict-aws@4.0.7': {} + + '@cspell/dict-bash@4.1.8': {} + + '@cspell/dict-companies@3.1.7': {} + + '@cspell/dict-cpp@5.1.23': {} + + '@cspell/dict-cryptocurrencies@5.0.3': {} + + '@cspell/dict-csharp@4.0.5': {} + + '@cspell/dict-css@4.0.16': {} + + '@cspell/dict-dart@2.2.4': {} + + '@cspell/dict-data-science@2.0.5': {} + + '@cspell/dict-django@4.1.3': {} + + '@cspell/dict-docker@1.1.11': {} + + '@cspell/dict-dotnet@5.0.8': {} + + '@cspell/dict-elixir@4.0.6': {} + + '@cspell/dict-en-common-misspellings@2.0.7': {} + + '@cspell/dict-en-gb@1.1.33': {} + + '@cspell/dict-en_us@4.3.27': {} + + '@cspell/dict-filetypes@3.0.8': {} + + '@cspell/dict-flutter@1.0.3': {} + + '@cspell/dict-fonts@4.0.3': {} + + '@cspell/dict-fsharp@1.0.4': {} + + '@cspell/dict-fullstack@3.2.3': {} + + '@cspell/dict-gaming-terms@1.0.8': {} + + '@cspell/dict-git@3.0.3': {} + + '@cspell/dict-golang@6.0.16': {} + + '@cspell/dict-google@1.0.4': {} + + '@cspell/dict-haskell@4.0.4': {} + + '@cspell/dict-html-symbol-entities@4.0.3': {} + + '@cspell/dict-html@4.0.10': {} + + '@cspell/dict-java@5.0.10': {} + + '@cspell/dict-julia@1.0.4': {} + + '@cspell/dict-k8s@1.0.9': {} + + '@cspell/dict-latex@4.0.3': {} + + '@cspell/dict-lorem-ipsum@4.0.3': {} + + '@cspell/dict-lua@4.0.6': {} + + '@cspell/dict-makefile@1.0.3': {} + + '@cspell/dict-markdown@2.0.7(@cspell/dict-css@4.0.16)(@cspell/dict-html-symbol-entities@4.0.3)(@cspell/dict-html@4.0.10)(@cspell/dict-typescript@3.1.11)': + dependencies: + '@cspell/dict-css': 4.0.16 + '@cspell/dict-html': 4.0.10 + '@cspell/dict-html-symbol-entities': 4.0.3 + '@cspell/dict-typescript': 3.1.11 + + '@cspell/dict-monkeyc@1.0.9': {} + + '@cspell/dict-node@5.0.5': {} + + '@cspell/dict-npm@5.1.12': {} + + '@cspell/dict-php@4.0.13': {} + + '@cspell/dict-powershell@5.0.13': {} + + '@cspell/dict-public-licenses@2.0.11': {} + + '@cspell/dict-python@4.2.12': + dependencies: + '@cspell/dict-data-science': 2.0.5 + + '@cspell/dict-r@2.0.4': {} + + '@cspell/dict-ruby@5.0.7': {} + + '@cspell/dict-rust@4.0.10': {} + + '@cspell/dict-scala@5.0.6': {} + + '@cspell/dict-software-terms@4.1.15': {} + + '@cspell/dict-sql@2.1.8': {} + + '@cspell/dict-svelte@1.0.5': {} + + '@cspell/dict-swift@2.0.4': {} + + '@cspell/dict-terraform@1.0.6': {} + + '@cspell/dict-typescript@3.1.11': {} + + '@cspell/dict-vue@3.0.3': {} + + '@cspell/dynamic-import@8.15.5': + dependencies: + import-meta-resolve: 4.1.0 + + '@cspell/filetypes@8.15.5': {} + + '@cspell/strong-weak-map@8.15.5': {} + + '@cspell/url@8.15.5': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@emnapi/core@1.3.1': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.8.1 + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.0.1': + dependencies: + tslib: 2.8.1 + + '@eslint-community/eslint-utils@4.4.1(eslint@9.7.0)': + dependencies: + eslint: 9.7.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.17.1': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.13.0': {} + + '@eslint/js@9.7.0': {} + + '@eslint/json@0.6.0': + dependencies: + '@eslint/plugin-kit': 0.2.2 + '@humanwhocodes/momoa': 3.3.3 + + '@eslint/markdown@6.2.1': + dependencies: + '@eslint/plugin-kit': 0.2.2 + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.2': + dependencies: + levn: 0.4.1 + + '@hapi/boom@9.1.4': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/momoa@2.0.4': {} + + '@humanwhocodes/momoa@3.3.3': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.1.33': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/types': 2.0.0 + debug: 4.3.7 + kolorist: 1.8.0 + local-pkg: 0.5.0 + mlly: 1.7.3 + transitivePeerDependencies: + - supports-color + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + 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 + '@types/node': 22.9.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.9.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.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.9.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + 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.25 + '@types/node': 22.9.0 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.1.7 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.26.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.9.0 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mermaid-js/mermaid-cli@11.4.0(puppeteer@19.11.1(typescript@5.6.3))': + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + mermaid: 11.4.0 + puppeteer: 19.11.1(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + + '@mermaid-js/parser@0.3.0': + dependencies: + langium: 3.0.0 + + '@mocks-server/admin-api-client@8.0.0-beta.2': + dependencies: + '@mocks-server/admin-api-paths': 5.0.0 + '@mocks-server/config': 2.0.0-beta.3 + '@mocks-server/core': 5.0.0-beta.3 + cross-fetch: 3.1.5 + transitivePeerDependencies: + - encoding + - supports-color + + '@mocks-server/admin-api-paths@5.0.0': {} + + '@mocks-server/config@2.0.0-beta.3': + dependencies: + ajv: 8.11.0 + better-ajv-errors: 1.2.0(ajv@8.11.0) + commander: 8.3.0 + cosmiconfig: 7.0.1 + deepmerge: 4.2.2 + fs-extra: 10.1.0 + is-promise: 4.0.0 + lodash: 4.17.21 + + '@mocks-server/core@5.0.0-beta.3': + dependencies: + '@babel/core': 7.18.13 + '@babel/register': 7.18.9(@babel/core@7.18.13) + '@hapi/boom': 9.1.4 + '@mocks-server/config': 2.0.0-beta.3 + '@mocks-server/logger': 2.0.0-beta.2 + '@mocks-server/nested-collections': 3.0.0-beta.2 + ajv: 8.11.0 + better-ajv-errors: 1.2.0(ajv@8.11.0) + body-parser: 1.20.0 + cors: 2.8.5 + express: 4.18.1 + express-request-id: 1.4.1 + fs-extra: 10.1.0 + globule: 1.3.4 + handlebars: 4.7.7 + is-promise: 4.0.0 + lodash: 4.17.21 + node-watch: 0.7.3 + update-notifier: 5.1.0 + winston: 3.8.2 + winston-array-transport: 1.1.10 + yaml: 2.1.1 + transitivePeerDependencies: + - supports-color + + '@mocks-server/logger@2.0.0-beta.2': + dependencies: + chalk: 4.1.1 + winston: 3.8.2 + winston-array-transport: 1.1.10 + + '@mocks-server/main@5.0.0-beta.4': + dependencies: + '@mocks-server/core': 5.0.0-beta.3 + '@mocks-server/plugin-admin-api': 5.0.0-beta.4(@mocks-server/core@5.0.0-beta.3) + '@mocks-server/plugin-inquirer-cli': 5.0.0-beta.4(@mocks-server/core@5.0.0-beta.3) + '@mocks-server/plugin-openapi': 3.0.0-beta.4(@mocks-server/core@5.0.0-beta.3) + '@mocks-server/plugin-proxy': 5.0.0-beta.4(@mocks-server/core@5.0.0-beta.3) + deepmerge: 4.2.2 + transitivePeerDependencies: + - supports-color + + '@mocks-server/nested-collections@3.0.0-beta.2': {} + + '@mocks-server/plugin-admin-api@5.0.0-beta.4(@mocks-server/core@5.0.0-beta.3)': + dependencies: + '@hapi/boom': 9.1.4 + '@mocks-server/admin-api-paths': 5.0.0 + '@mocks-server/core': 5.0.0-beta.3 + body-parser: 1.20.0 + cors: 2.8.5 + express: 4.18.1 + express-request-id: 1.4.1 + openapi-types: 12.0.2 + swagger-ui-dist: 4.14.0 + transitivePeerDependencies: + - supports-color + + '@mocks-server/plugin-inquirer-cli@5.0.0-beta.4(@mocks-server/core@5.0.0-beta.3)': + dependencies: + '@mocks-server/core': 5.0.0-beta.3 + chalk: 4.1.1 + inquirer: 8.2.4 + inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.4) + lodash: 4.17.21 + node-emoji: 1.11.0 + + '@mocks-server/plugin-openapi@3.0.0-beta.4(@mocks-server/core@5.0.0-beta.3)': + dependencies: + '@mocks-server/core': 5.0.0-beta.3 + json-refs: 3.0.15 + openapi-types: 12.0.2 + transitivePeerDependencies: + - supports-color + + '@mocks-server/plugin-proxy@5.0.0-beta.4(@mocks-server/core@5.0.0-beta.3)': + dependencies: + '@mocks-server/core': 5.0.0-beta.3 + express-http-proxy: 1.6.3 + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.3.1 + '@emnapi/runtime': 1.3.1 + '@tybys/wasm-util': 0.9.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@nx/nx-darwin-arm64@20.1.0': + optional: true + + '@nx/nx-darwin-x64@20.1.0': + optional: true + + '@nx/nx-freebsd-x64@20.1.0': + optional: true + + '@nx/nx-linux-arm-gnueabihf@20.1.0': + optional: true + + '@nx/nx-linux-arm64-gnu@20.1.0': + optional: true + + '@nx/nx-linux-arm64-musl@20.1.0': + optional: true + + '@nx/nx-linux-x64-gnu@20.1.0': + optional: true + + '@nx/nx-linux-x64-musl@20.1.0': + optional: true + + '@nx/nx-win32-arm64-msvc@20.1.0': + optional: true + + '@nx/nx-win32-x64-msvc@20.1.0': + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.1.1': {} + + '@puppeteer/browsers@0.5.0(typescript@5.6.3)': + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + proxy-from-env: 1.1.0 + tar-fs: 2.1.1 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@rtsao/scc@1.1.0': {} + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sindresorhus/is@0.14.0': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@szmarczak/http-timer@1.1.2': + dependencies: + defer-to-connect: 1.1.3 + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.6 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 22.9.0 + + '@types/d3-array@3.2.1': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.14 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.6': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.14 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.0.3': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.3': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.0 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.3 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/dompurify@3.0.5': + dependencies: + '@types/trusted-types': 2.0.7 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + + '@types/estree@1.0.6': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.9.0 + + '@types/geojson@7946.0.14': {} + + '@types/glob@8.1.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.9.0 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.9.0 + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 2.0.11 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json5@0.0.29': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.9.0 + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.9.0 + + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 2.0.11 + + '@types/minimatch@5.1.2': {} + + '@types/ms@0.7.34': {} + + '@types/node@22.9.0': + dependencies: + undici-types: 6.19.8 + + '@types/parse-json@4.0.2': {} + + '@types/parse5@5.0.3': {} + + '@types/parse5@6.0.3': {} + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.9.0 + + '@types/stack-utils@2.0.3': {} + + '@types/tmp@0.2.6': {} + + '@types/triple-beam@1.3.5': {} + + '@types/trusted-types@2.0.7': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/which@3.0.4': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.9.0 + optional: true + + '@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/type-utils': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.14.0 + eslint: 9.7.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.14.0 + debug: 4.3.7 + eslint: 9.7.0 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.14.0': + dependencies: + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 + + '@typescript-eslint/type-utils@8.14.0(eslint@9.7.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + debug: 4.3.7 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.14.0': {} + + '@typescript-eslint/typescript-estree@8.14.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.14.0(eslint@9.7.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.7.0) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + eslint: 9.7.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.14.0': + dependencies: + '@typescript-eslint/types': 8.14.0 + eslint-visitor-keys: 3.4.3 + + '@yarnpkg/lockfile@1.1.0': {} + + '@yarnpkg/parsers@3.0.2': + dependencies: + js-yaml: 3.14.1 + tslib: 2.8.1 + + '@zkochan/js-yaml@0.0.7': + dependencies: + argparse: 2.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.11.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + + array-flatten@1.1.1: {} + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + + array-timsort@1.0.3: {} + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + asap@2.0.6: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + atlassian-jwt@2.0.3: + dependencies: + jsuri: 1.3.1 + lodash: 4.17.21 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axios@1.6.7: + dependencies: + follow-redirects: 1.15.9(debug@4.3.7) + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axios@1.7.7(debug@4.3.7): + dependencies: + follow-redirects: 1.15.9(debug@4.3.7) + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-jest@29.7.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.25.9 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-plugin-module-resolver@5.0.2: + dependencies: + find-babel-config: 2.1.2 + glob: 9.3.5 + pkg-up: 3.1.0 + reselect: 4.1.8 + resolve: 1.22.8 + + babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-import-meta@2.2.1(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/template': 7.25.9 + tslib: 2.8.1 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@29.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-ajv-errors@1.2.0(ajv@8.11.0): + dependencies: + '@babel/code-frame': 7.26.2 + '@humanwhocodes/momoa': 2.0.4 + ajv: 8.11.0 + chalk: 4.1.2 + jsonpointer: 5.0.1 + leven: 3.1.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.7.2: {} + + body-parser@1.20.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.10.3 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001680 + electron-to-chromium: 1.5.56 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-crc32@0.2.13: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + cacheable-request@6.1.0: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001680: {} + + ccount@2.0.1: {} + + chalk-template@1.1.0: + dependencies: + chalk: 5.3.0 + + chalk@4.1.1: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + char-regex@1.0.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chardet@0.7.0: {} + + check-more-types@2.24.0: {} + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chownr@1.1.4: {} + + chromium-bidi@0.4.7(devtools-protocol@0.0.1107588): + dependencies: + devtools-protocol: 0.0.1107588 + mitt: 3.0.0 + + ci-info@2.0.0: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.1: {} + + clear-module@4.1.2: + dependencies: + parent-module: 2.0.0 + resolve-from: 5.0.0 + + cli-boxes@2.2.1: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.6.1: {} + + cli-spinners@2.9.2: {} + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cli-width@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@1.0.4: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorette@2.0.20: {} + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@1.0.8: {} + + comma-separated-tokens@2.0.3: {} + + commander@12.1.0: {} + + commander@4.1.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + comment-json@4.2.5: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + + commondir@1.0.1: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + configstore@5.0.1: + dependencies: + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 + + confluence.js@1.7.4: + dependencies: + atlassian-jwt: 2.0.3 + axios: 1.7.7(debug@4.3.7) + form-data: 4.0.1 + oauth: 0.10.0 + tslib: 2.8.1 + transitivePeerDependencies: + - debug + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.5.0: {} + + cookiejar@2.1.4: {} + + core-js-compat@3.39.0: + dependencies: + browserslist: 4.24.2 + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cosmiconfig@7.0.1: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cosmiconfig@8.1.3: + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + + create-jest@29.7.0(@types/node@22.9.0): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.9.0) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + + cross-fetch@3.1.5: + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + + cross-fetch@4.0.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-random-string@2.0.0: {} + + cspell-config-lib@8.15.5: + dependencies: + '@cspell/cspell-types': 8.15.5 + comment-json: 4.2.5 + yaml: 2.6.0 + + cspell-dictionary@8.15.5: + dependencies: + '@cspell/cspell-pipe': 8.15.5 + '@cspell/cspell-types': 8.15.5 + cspell-trie-lib: 8.15.5 + fast-equals: 5.0.1 + + cspell-gitignore@8.15.5: + dependencies: + '@cspell/url': 8.15.5 + cspell-glob: 8.15.5 + cspell-io: 8.15.5 + find-up-simple: 1.0.0 + + cspell-glob@8.15.5: + dependencies: + '@cspell/url': 8.15.5 + micromatch: 4.0.8 + + cspell-grammar@8.15.5: + dependencies: + '@cspell/cspell-pipe': 8.15.5 + '@cspell/cspell-types': 8.15.5 + + cspell-io@8.15.5: + dependencies: + '@cspell/cspell-service-bus': 8.15.5 + '@cspell/url': 8.15.5 + + cspell-lib@8.15.5: + dependencies: + '@cspell/cspell-bundled-dicts': 8.15.5 + '@cspell/cspell-pipe': 8.15.5 + '@cspell/cspell-resolver': 8.15.5 + '@cspell/cspell-types': 8.15.5 + '@cspell/dynamic-import': 8.15.5 + '@cspell/filetypes': 8.15.5 + '@cspell/strong-weak-map': 8.15.5 + '@cspell/url': 8.15.5 + clear-module: 4.1.2 + comment-json: 4.2.5 + cspell-config-lib: 8.15.5 + cspell-dictionary: 8.15.5 + cspell-glob: 8.15.5 + cspell-grammar: 8.15.5 + cspell-io: 8.15.5 + cspell-trie-lib: 8.15.5 + env-paths: 3.0.0 + fast-equals: 5.0.1 + gensequence: 7.0.0 + import-fresh: 3.3.0 + resolve-from: 5.0.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + xdg-basedir: 5.1.0 + + cspell-trie-lib@8.15.5: + dependencies: + '@cspell/cspell-pipe': 8.15.5 + '@cspell/cspell-types': 8.15.5 + gensequence: 7.0.0 + + cspell@8.15.5: + dependencies: + '@cspell/cspell-json-reporter': 8.15.5 + '@cspell/cspell-pipe': 8.15.5 + '@cspell/cspell-types': 8.15.5 + '@cspell/dynamic-import': 8.15.5 + '@cspell/url': 8.15.5 + chalk: 5.3.0 + chalk-template: 1.1.0 + commander: 12.1.0 + cspell-dictionary: 8.15.5 + cspell-gitignore: 8.15.5 + cspell-glob: 8.15.5 + cspell-io: 8.15.5 + cspell-lib: 8.15.5 + fast-json-stable-stringify: 2.1.0 + file-entry-cache: 9.1.0 + get-stdin: 9.0.0 + semver: 7.6.3 + tinyglobby: 0.2.10 + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.3): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.30.3 + + cytoscape-fcose@2.2.0(cytoscape@3.30.3): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.30.3 + + cytoscape@3.30.3: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.11: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + decompress-response@3.3.0: + dependencies: + mimic-response: 1.0.1 + + dedent@1.5.3: {} + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.2.2: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@1.1.3: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-newline@3.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + devtools-protocol@0.0.1107588: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff-sequences@29.6.3: {} + + diff@5.2.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dompurify@3.1.6: {} + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv-expand@11.0.6: + dependencies: + dotenv: 16.4.5 + + dotenv@16.4.5: {} + + duplexer3@0.1.5: {} + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.56: {} + + emittery@0.13.1: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enabled@2.0.0: {} + + encodeurl@1.0.2: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + entities@4.3.0: {} + + env-paths@3.0.0: {} + + environment@1.1.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.23.4: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.3 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + es6-promise@4.2.8: {} + + escalade@3.2.0: {} + + escape-goat@2.1.1: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.0(eslint@9.7.0): + dependencies: + eslint: 9.7.0 + + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0)): + dependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.15.1 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.3.7 + enhanced-resolve: 5.17.1 + eslint: 9.7.0 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0))(eslint@9.7.0) + fast-glob: 3.3.2 + get-tsconfig: 4.8.1 + is-bun-module: 1.2.1 + is-glob: 4.0.3 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0))(eslint@9.7.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + eslint: 9.7.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.7.0): + 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 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.7.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@9.7.0))(eslint@9.7.0) + hasown: 2.0.2 + is-core-module: 2.15.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 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jest@28.9.0(@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3): + dependencies: + '@typescript-eslint/utils': 8.14.0(eslint@9.7.0)(typescript@5.6.3) + eslint: 9.7.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.7.0)(typescript@5.6.3))(eslint@9.7.0)(typescript@5.6.3) + jest: 29.7.0(@types/node@22.9.0) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0(eslint@9.7.0))(eslint@9.7.0)(prettier@3.3.3): + dependencies: + eslint: 9.7.0 + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.8.8 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@9.7.0) + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.7.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.7.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.17.1 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.7.0 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@2.1.0: {} + + estree-util-visit@1.2.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 2.0.11 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + eventemitter3@5.0.1: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + 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 + + express-http-proxy@1.6.3: + dependencies: + debug: 3.2.7 + es6-promise: 4.2.8 + raw-body: 2.5.2 + transitivePeerDependencies: + - supports-color + + express-request-id@1.4.1: + dependencies: + uuid: 3.4.0 + + express@4.18.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.0 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.10.3 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extract-zip@2.0.1: + dependencies: + debug: 4.3.7 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-equals@5.0.1: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fault@2.0.1: + dependencies: + format: 0.2.2 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fecha@4.2.3: {} + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-entry-cache@9.1.0: + dependencies: + flat-cache: 5.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.2.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-babel-config@2.1.2: + dependencies: + json5: 2.2.3 + + find-cache-dir@2.1.0: + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + + find-up-simple@1.0.0: {} + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flat-cache@5.0.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flat@5.0.2: {} + + flatted@3.3.1: {} + + fn.name@1.1.0: {} + + follow-redirects@1.15.9(debug@4.3.7): + optionalDependencies: + debug: 4.3.7 + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + format@0.2.2: {} + + formidable@2.1.2: + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.13.0 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + from@0.1.7: {} + + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.1 + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + functions-have-names: 1.2.3 + + functions-have-names@1.2.3: {} + + gensequence@7.0.0: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-package-type@0.1.0: {} + + get-stdin@9.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.2 + + get-stream@6.0.1: {} + + get-stream@8.0.1: {} + + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.3.10: + dependencies: + foreground-child: 3.3.0 + jackspeak: 2.3.6 + minimatch: 9.0.5 + minipass: 7.1.2 + path-scurry: 1.11.1 + + glob@7.1.7: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.12.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + + globule@1.3.4: + dependencies: + glob: 7.1.7 + lodash: 4.17.21 + minimatch: 3.0.8 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + got@9.6.0: + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + + hachure-fill@0.5.2: {} + + handlebars@4.7.7: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.0.2: {} + + has-flag@4.0.0: {} + + has-own-prop@2.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + has-yarn@2.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-to-hyperscript@9.0.1: + dependencies: + '@types/unist': 2.0.11 + comma-separated-tokens: 1.0.8 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + style-to-object: 0.3.0 + unist-util-is: 4.1.0 + web-namespaces: 1.1.4 + + hast-util-from-parse5@6.0.1: + dependencies: + '@types/parse5': 5.0.3 + hastscript: 6.0.0 + property-information: 5.6.0 + vfile: 4.2.1 + vfile-location: 3.2.0 + web-namespaces: 1.1.4 + + hast-util-from-parse5@7.1.2: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + hastscript: 7.2.0 + property-information: 6.5.0 + vfile: 5.3.7 + vfile-location: 4.1.0 + web-namespaces: 2.0.1 + + hast-util-parse-selector@2.2.5: {} + + hast-util-parse-selector@3.1.1: + dependencies: + '@types/hast': 2.3.10 + + hast-util-raw@6.1.0: + dependencies: + '@types/hast': 2.3.10 + hast-util-from-parse5: 6.0.1 + hast-util-to-parse5: 6.0.0 + html-void-elements: 1.0.5 + parse5: 6.0.1 + unist-util-position: 3.1.0 + unist-util-visit: 2.0.3 + vfile: 4.2.1 + web-namespaces: 1.1.4 + xtend: 4.0.2 + zwitch: 1.0.5 + + hast-util-raw@7.2.3: + dependencies: + '@types/hast': 2.3.10 + '@types/parse5': 6.0.3 + hast-util-from-parse5: 7.1.2 + hast-util-to-parse5: 7.1.0 + html-void-elements: 2.0.1 + parse5: 6.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@8.0.4: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-raw: 7.2.3 + hast-util-whitespace: 2.0.1 + html-void-elements: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@6.0.0: + dependencies: + hast-to-hyperscript: 9.0.1 + property-information: 5.6.0 + web-namespaces: 1.1.4 + xtend: 4.0.2 + zwitch: 1.0.5 + + hast-util-to-parse5@7.1.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@2.0.0: + dependencies: + '@types/hast': 2.3.10 + + hast-util-whitespace@2.0.1: {} + + hast@1.0.0: {} + + hastscript@6.0.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + + hastscript@7.2.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 3.1.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + hexoid@1.0.0: {} + + html-escaper@2.0.2: {} + + html-void-elements@1.0.5: {} + + html-void-elements@2.0.1: {} + + http-cache-semantics@4.1.1: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + husky@9.0.11: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-lazy@2.1.0: {} + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + ini@4.1.1: {} + + inline-style-parser@0.1.1: {} + + inquirer-autocomplete-prompt@1.4.0(inquirer@8.2.4): + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + figures: 3.2.0 + inquirer: 8.2.4 + run-async: 2.4.1 + rxjs: 6.6.7 + + inquirer@8.2.4: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-bigint@1.0.4: + dependencies: + has-bigints: 1.0.2 + + is-boolean-object@1.1.2: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-buffer@2.0.5: {} + + is-bun-module@1.2.1: + dependencies: + semver: 7.6.3 + + is-callable@1.2.7: {} + + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 + + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-interactive@1.0.0: {} + + is-negative-zero@2.0.3: {} + + is-npm@5.0.0: {} + + is-number-object@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-promise@4.0.0: {} + + is-regex@1.1.4: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-string@1.0.7: + dependencies: + has-tostringtag: 1.0.2 + + is-symbol@1.0.4: + dependencies: + has-symbols: 1.0.3 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-yarn-global@0.3.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.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 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.9.0): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.9.0) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.9.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.9.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.1.7 + graceful-fs: 4.2.11 + 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.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.9.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.9.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.26.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + jest-runner@29.7.0: + 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 + '@types/node': 22.9.0 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + 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 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + 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 + '@types/node': 22.9.0 + chalk: 4.1.2 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 + glob: 7.1.7 + graceful-fs: 4.2.11 + 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 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + 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.6.3 + transitivePeerDependencies: + - supports-color + + jest-sonar@0.2.16: + dependencies: + entities: 4.3.0 + strip-ansi: 6.0.1 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.9.0 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.9.0): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.9.0) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + json-buffer@3.0.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-refs@3.0.15: + dependencies: + commander: 4.1.1 + graphlib: 2.1.8 + js-yaml: 3.14.1 + lodash: 4.17.21 + native-promise-only: 0.8.1 + path-loader: 1.0.12 + slash: 3.0.0 + uri-js: 4.4.1 + transitivePeerDependencies: + - supports-color + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + + jsuri@1.3.1: {} + + katex@0.16.11: + dependencies: + commander: 8.3.0 + + keyv@3.1.0: + dependencies: + json-buffer: 3.0.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + kolorist@1.8.0: {} + + kuler@2.0.0: {} + + langium@3.0.0: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + latest-version@5.1.0: + dependencies: + package-json: 6.5.0 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lazy-ass@1.6.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.2: {} + + lines-and-columns@1.2.4: {} + + lines-and-columns@2.0.3: {} + + lint-staged@15.2.10: + dependencies: + chalk: 5.3.0 + commander: 12.1.0 + debug: 4.3.7 + execa: 8.0.1 + lilconfig: 3.1.2 + listr2: 8.2.5 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.5.1 + transitivePeerDependencies: + - supports-color + + listr2@8.2.5: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + local-pkg@0.5.0: + dependencies: + mlly: 1.7.3 + pkg-types: 1.2.1 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.debounce@4.0.8: {} + + lodash.iteratee@4.7.0: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + longest-streak@3.1.0: {} + + lowercase-keys@1.0.1: {} + + lowercase-keys@2.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-stream@0.1.0: {} + + markdown-table@3.0.4: {} + + marked@13.0.3: {} + + mdast-squeeze-paragraphs@5.2.1: + dependencies: + '@types/mdast': 3.0.15 + unist-util-visit: 4.1.2 + + mdast-util-definitions@5.1.2: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + unist-util-visit: 4.1.2 + + mdast-util-directive@2.2.4: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-visit-parents: 5.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@2.2.2: + dependencies: + '@types/mdast': 3.0.15 + escape-string-regexp: 5.0.0 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + decode-named-character-reference: 1.0.2 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@1.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + micromark-extension-frontmatter: 1.1.1 + + mdast-util-gfm-autolink-literal@1.0.3: + dependencies: + '@types/mdast': 3.0.15 + ccount: 2.0.1 + mdast-util-find-and-replace: 2.2.2 + micromark-util-character: 1.2.0 + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.0 + + mdast-util-gfm-footnote@1.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + micromark-util-normalize-identifier: 1.1.0 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + micromark-util-normalize-identifier: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@1.0.3: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@1.0.7: + dependencies: + '@types/mdast': 3.0.15 + markdown-table: 3.0.4 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@1.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@2.0.2: + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-gfm-autolink-literal: 1.0.3 + mdast-util-gfm-footnote: 1.0.2 + mdast-util-gfm-strikethrough: 1.0.3 + mdast-util-gfm-table: 1.0.7 + mdast-util-gfm-task-list-item: 1.0.2 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@1.3.2: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@2.1.4: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + ccount: 2.0.1 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-remove-position: 4.0.2 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.3: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@2.0.1: + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdx-jsx: 2.1.4 + mdast-util-mdxjs-esm: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@1.3.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.1 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@3.0.1: + dependencies: + '@types/mdast': 3.0.15 + unist-util-is: 5.2.1 + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@12.3.0: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-definitions: 5.1.2 + micromark-util-sanitize-uri: 1.2.0 + trim-lines: 3.0.1 + unist-util-generated: 2.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + + mdast-util-to-markdown@1.5.0: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + longest-streak: 3.1.0 + mdast-util-phrasing: 3.0.1 + mdast-util-to-string: 3.2.0 + micromark-util-decode-string: 1.1.0 + unist-util-visit: 4.1.2 + zwitch: 2.0.4 + + mdast-util-to-markdown@2.1.1: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-decode-string: 2.0.0 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@3.2.0: + dependencies: + '@types/mdast': 3.0.15 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + media-typer@0.3.0: {} + + merge-descriptors@1.0.1: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + mermaid@11.4.0: + dependencies: + '@braintree/sanitize-url': 7.1.0 + '@iconify/utils': 2.1.33 + '@mermaid-js/parser': 0.3.0 + '@types/d3': 7.4.3 + '@types/dompurify': 3.0.5 + cytoscape: 3.30.3 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.30.3) + cytoscape-fcose: 2.2.0(cytoscape@3.30.3) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.11 + dayjs: 1.11.13 + dompurify: 3.1.6 + katex: 0.16.11 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 13.0.3 + roughjs: 4.6.6 + stylis: 4.3.4 + ts-dedent: 2.2.0 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromark-core-commonmark@1.1.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-core-commonmark@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-directive@2.2.1: + dependencies: + micromark-factory-space: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + parse-entities: 4.0.1 + uvu: 0.5.6 + + micromark-extension-frontmatter@1.1.1: + dependencies: + fault: 2.0.1 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm-autolink-literal@1.0.5: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-footnote@1.1.2: + dependencies: + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-strikethrough@1.0.7: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-table@1.0.7: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-table@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm-tagfilter@1.0.2: + dependencies: + micromark-util-types: 1.1.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-extension-gfm-task-list-item@1.0.5: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-gfm@2.0.3: + dependencies: + micromark-extension-gfm-autolink-literal: 1.0.5 + micromark-extension-gfm-footnote: 1.1.2 + micromark-extension-gfm-strikethrough: 1.0.7 + micromark-extension-gfm-table: 1.0.7 + micromark-extension-gfm-tagfilter: 1.0.2 + micromark-extension-gfm-task-list-item: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-extension-mdx-expression@1.0.8: + dependencies: + '@types/estree': 1.0.6 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-mdx-jsx@1.0.5: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.6 + estree-util-is-identifier-name: 2.1.0 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + + micromark-extension-mdx-md@1.0.1: + dependencies: + micromark-util-types: 1.1.0 + + micromark-extension-mdxjs-esm@1.0.5: + dependencies: + '@types/estree': 1.0.6 + micromark-core-commonmark: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + + micromark-extension-mdxjs@1.0.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + micromark-extension-mdx-expression: 1.0.8 + micromark-extension-mdx-jsx: 1.0.5 + micromark-extension-mdx-md: 1.0.1 + micromark-extension-mdxjs-esm: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-destination@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-destination@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-label@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-label@2.0.0: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-mdx-expression@1.0.9: + dependencies: + '@types/estree': 1.0.6 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-types: 2.0.0 + + micromark-factory-title@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-title@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-factory-whitespace@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-whitespace@2.0.0: + dependencies: + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-chunked@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-chunked@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-classify-character@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-classify-character@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-combine-extensions@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-combine-extensions@2.0.0: + dependencies: + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-decode-numeric-character-reference@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-decode-numeric-character-reference@2.0.1: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-decode-string@1.1.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-decode-string@2.0.0: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-symbol: 2.0.0 + + micromark-util-encode@1.1.0: {} + + micromark-util-encode@2.0.0: {} + + micromark-util-events-to-acorn@1.2.3: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.6 + '@types/unist': 2.0.11 + estree-util-visit: 1.2.1 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + + micromark-util-html-tag-name@1.2.0: {} + + micromark-util-html-tag-name@2.0.0: {} + + micromark-util-normalize-identifier@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-normalize-identifier@2.0.0: + dependencies: + micromark-util-symbol: 2.0.0 + + micromark-util-resolve-all@1.1.0: + dependencies: + micromark-util-types: 1.1.0 + + micromark-util-resolve-all@2.0.0: + dependencies: + micromark-util-types: 2.0.0 + + micromark-util-sanitize-uri@1.2.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-subtokenize@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-util-subtokenize@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.0: {} + + micromark@3.2.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + micromark@4.0.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + mimic-response@1.0.1: {} + + minimatch@3.0.8: + dependencies: + brace-expansion: 1.1.11 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@8.0.4: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@4.2.8: {} + + minipass@7.1.2: {} + + mitt@3.0.0: {} + + mkdirp-classic@0.5.3: {} + + mlly@1.7.3: + dependencies: + acorn: 8.14.0 + pathe: 1.1.2 + pkg-types: 1.2.1 + ufo: 1.5.4 + + mri@1.2.0: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + native-promise-only@0.8.1: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.17.21 + + node-fetch@2.6.7: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-int64@0.4.0: {} + + node-machine-id@1.1.12: {} + + node-releases@2.0.18: {} + + node-watch@0.7.3: {} + + normalize-path@3.0.0: {} + + normalize-url@4.5.1: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nx@20.1.0: + dependencies: + '@napi-rs/wasm-runtime': 0.2.4 + '@yarnpkg/lockfile': 1.1.0 + '@yarnpkg/parsers': 3.0.2 + '@zkochan/js-yaml': 0.0.7 + axios: 1.7.7(debug@4.3.7) + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + dotenv: 16.4.5 + dotenv-expand: 11.0.6 + enquirer: 2.3.6 + figures: 3.2.0 + flat: 5.0.2 + front-matter: 4.0.2 + ignore: 5.3.2 + jest-diff: 29.7.0 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.3 + minimatch: 9.0.3 + node-machine-id: 1.1.12 + npm-run-path: 4.0.1 + open: 8.4.2 + ora: 5.3.0 + semver: 7.6.3 + string-width: 4.2.3 + tar-stream: 2.2.0 + tmp: 0.2.3 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + yargs: 17.7.1 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 20.1.0 + '@nx/nx-darwin-x64': 20.1.0 + '@nx/nx-freebsd-x64': 20.1.0 + '@nx/nx-linux-arm-gnueabihf': 20.1.0 + '@nx/nx-linux-arm64-gnu': 20.1.0 + '@nx/nx-linux-arm64-musl': 20.1.0 + '@nx/nx-linux-x64-gnu': 20.1.0 + '@nx/nx-linux-x64-musl': 20.1.0 + '@nx/nx-win32-arm64-msvc': 20.1.0 + '@nx/nx-win32-x64-msvc': 20.1.0 + transitivePeerDependencies: + - debug + + oauth@0.10.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.3: {} + + object-keys@1.1.1: {} + + object.assign@4.1.5: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-object-atoms: 1.0.0 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openapi-types@12.0.2: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.3.0: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-cancelable@1.1.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json@6.5.0: + dependencies: + got: 9.6.0 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 6.3.1 + + package-manager-detector@0.2.2: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parent-module@2.0.0: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.1: + dependencies: + '@types/unist': 2.0.11 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@6.0.1: {} + + parseurl@1.3.3: {} + + path-data-parser@0.1.0: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-loader@1.0.12: + dependencies: + native-promise-only: 0.8.1 + superagent: 7.1.6 + transitivePeerDependencies: + - supports-color + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@0.1.7: {} + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pidtree@0.6.0: {} + + pify@4.0.1: {} + + pirates@4.0.6: {} + + pkg-dir@3.0.0: + dependencies: + find-up: 3.0.0 + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@1.2.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.3 + pathe: 1.1.2 + + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + possible-typed-array-names@1.0.0: {} + + prelude-ls@1.2.1: {} + + prepend-http@2.0.0: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.3.3: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + progress@2.0.3: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + + property-information@6.5.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode@2.3.1: {} + + pupa@2.1.1: + dependencies: + escape-goat: 2.1.1 + + puppeteer-core@19.11.1(typescript@5.6.3): + dependencies: + '@puppeteer/browsers': 0.5.0(typescript@5.6.3) + chromium-bidi: 0.4.7(devtools-protocol@0.0.1107588) + cross-fetch: 3.1.5 + debug: 4.3.4 + devtools-protocol: 0.0.1107588 + extract-zip: 2.0.1 + https-proxy-agent: 5.0.1 + proxy-from-env: 1.1.0 + tar-fs: 2.1.1 + unbzip2-stream: 1.4.3 + ws: 8.13.0 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + puppeteer@19.11.1(typescript@5.6.3): + dependencies: + '@puppeteer/browsers': 0.5.0(typescript@5.6.3) + cosmiconfig: 8.1.3 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + proxy-from-env: 1.1.0 + puppeteer-core: 19.11.1(typescript@5.6.3) + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - typescript + - utf-8-validate + + pure-rand@6.1.0: {} + + qs@6.10.3: + dependencies: + side-channel: 1.0.6 + + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@2.5.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-is@18.3.1: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.26.0 + + regexp.prototype.flags@1.5.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + regexpu-core@6.1.1: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.11.2 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + registry-auth-token@4.2.2: + dependencies: + rc: 1.2.8 + + registry-url@5.1.0: + dependencies: + rc: 1.2.8 + + regjsgen@0.8.0: {} + + regjsparser@0.11.2: + dependencies: + jsesc: 3.0.2 + + rehype-parse@8.0.5: + dependencies: + '@types/hast': 2.3.10 + hast-util-from-parse5: 7.1.2 + parse5: 6.0.1 + unified: 10.1.2 + + rehype-raw@5.1.0: + dependencies: + hast-util-raw: 6.1.0 + + rehype-stringify@9.0.4: + dependencies: + '@types/hast': 2.3.10 + hast-util-to-html: 8.0.4 + unified: 10.1.2 + + rehype@12.0.1: + dependencies: + '@types/hast': 2.3.10 + rehype-parse: 8.0.5 + rehype-stringify: 9.0.4 + unified: 10.1.2 + + remark-directive@2.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-directive: 2.2.4 + micromark-extension-directive: 2.2.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-frontmatter@4.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-frontmatter: 1.0.1 + micromark-extension-frontmatter: 1.1.1 + unified: 10.1.2 + + remark-gfm@3.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-gfm: 2.0.2 + micromark-extension-gfm: 2.0.3 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-mdx@2.3.0: + dependencies: + mdast-util-mdx: 2.0.1 + micromark-extension-mdxjs: 1.0.1 + transitivePeerDependencies: + - supports-color + + remark-parse-frontmatter@1.0.3: + dependencies: + revalidator: 0.3.1 + unist-util-find: 1.0.4 + yaml: 1.10.2 + + remark-parse@10.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-rehype@10.1.0: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-to-hast: 12.3.0 + unified: 10.1.2 + + remark-stringify@10.0.3: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + unified: 10.1.2 + + remark-unlink@4.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-squeeze-paragraphs: 5.2.1 + unified: 10.1.2 + unist-util-visit: 4.1.2 + + remark@14.0.3: + dependencies: + '@types/mdast': 3.0.15 + remark-parse: 10.0.2 + remark-stringify: 10.0.3 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + repeat-string@1.6.1: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + reselect@4.1.8: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.2: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@1.0.2: + dependencies: + lowercase-keys: 1.0.1 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.0.4: {} + + revalidator@0.3.1: {} + + rfdc@1.4.1: {} + + robust-predicates@3.0.2: {} + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver-diff@3.1.1: + dependencies: + semver: 6.3.1 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.6.3: {} + + send@0.18.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.15.0: + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + space-separated-tokens@1.1.5: {} + + space-separated-tokens@2.0.2: {} + + split@0.3.3: + dependencies: + through: 2.3.8 + + sprintf-js@1.0.3: {} + + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + start-server-and-test@2.0.8: + dependencies: + arg: 5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.3.7 + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 8.0.1(debug@4.3.7) + transitivePeerDependencies: + - supports-color + + statuses@2.0.1: {} + + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + + string-argv@0.3.2: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.4 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + style-to-object@0.3.0: + dependencies: + inline-style-parser: 0.1.1 + + stylis@4.3.4: {} + + superagent@7.1.6: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.7 + fast-safe-stringify: 2.1.1 + form-data: 4.0.1 + formidable: 2.1.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.13.0 + readable-stream: 3.6.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swagger-ui-dist@4.14.0: {} + + synckit@0.8.8: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + tapable@2.2.1: {} + + tar-fs@2.1.1: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.1.7 + minimatch: 3.1.2 + + text-hex@1.0.0: {} + + text-table@0.2.0: {} + + through@2.3.8: {} + + tinyexec@0.3.1: {} + + tinyglobby@0.2.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.2.3: {} + + tmpl@1.0.5: {} + + to-readable-stream@1.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-vfile@7.2.4: + dependencies: + is-buffer: 2.0.5 + vfile: 5.3.7 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + trim-lines@3.0.1: {} + + triple-beam@1.4.1: {} + + trough@2.2.0: {} + + ts-api-utils@1.4.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + ts-dedent@2.2.0: {} + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript@5.6.3: {} + + ufo@1.5.4: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + + undici-types@6.19.8: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unified@10.1.2: + dependencies: + '@types/unist': 2.0.11 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 5.3.7 + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + unist-builder@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-find-after@4.0.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-find@1.0.4: + dependencies: + lodash.iteratee: 4.7.0 + unist-util-visit: 2.0.3 + + unist-util-generated@2.0.1: {} + + unist-util-is@4.1.0: {} + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.11 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@1.1.2: + dependencies: + '@types/unist': 2.0.11 + + unist-util-position@3.1.0: {} + + unist-util-position@4.0.4: + dependencies: + '@types/unist': 2.0.11 + + unist-util-remove-position@4.0.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-visit: 4.1.2 + + unist-util-remove@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-notifier@5.1.0: + dependencies: + boxen: 5.1.2 + chalk: 4.1.2 + configstore: 5.0.1 + has-yarn: 2.1.0 + import-lazy: 2.1.0 + is-ci: 2.0.0 + is-installed-globally: 0.4.0 + is-npm: 5.0.0 + is-yarn-global: 0.3.0 + latest-version: 5.1.0 + pupa: 2.1.1 + semver: 7.6.3 + semver-diff: 3.1.1 + xdg-basedir: 4.0.0 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse-lax@3.0.0: + dependencies: + prepend-http: 2.0.0 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@3.4.0: {} + + uuid@9.0.1: {} + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.2.0 + kleur: 4.1.5 + sade: 1.8.1 + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vary@1.1.2: {} + + vfile-location@3.2.0: {} + + vfile-location@4.1.0: + dependencies: + '@types/unist': 2.0.11 + vfile: 5.3.7 + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + + vfile-message@3.1.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 3.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@4.2.1: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 2.0.3 + vfile-message: 2.0.4 + + vfile@5.3.7: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + wait-on@8.0.1(debug@4.3.7): + dependencies: + axios: 1.7.7(debug@4.3.7) + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.1 + transitivePeerDependencies: + - debug + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-namespaces@1.1.4: {} + + web-namespaces@2.0.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.0.2: + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@3.0.1: + dependencies: + isexe: 2.0.0 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + winston-array-transport@1.1.10: + dependencies: + winston-transport: 4.5.0 + + winston-transport@4.5.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.8.2: + dependencies: + '@colors/colors': 1.5.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@8.13.0: {} + + xdg-basedir@4.0.0: {} + + xdg-basedir@5.1.0: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.2: {} + + yaml@2.1.1: {} + + yaml@2.3.4: {} + + yaml@2.5.1: {} + + yaml@2.6.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.1: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + zod@3.22.4: {} + + zwitch@1.0.5: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..2ee21ac0 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital and contributors +# SPDX-License-Identifier: MIT + +packages: + - "components/*"