diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml new file mode 100644 index 00000000..43e32adf --- /dev/null +++ b/.github/actions/install/action.yml @@ -0,0 +1,22 @@ +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..b43582f3 --- /dev/null +++ b/.github/actions/run-nx-target/action.yml @@ -0,0 +1,49 @@ +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 + - name: Restore Nx cache + uses: actions/cache/restore@v4 + id: cache-restore + with: + path: .nx + key: nx-cache + - 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 }} + - 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 + - name: Lint + if: github.event_name != 'pull_request' + shell: bash + run: NX_REJECT_UNKNOWN_LOCAL_CACHE=0 pnpm nx run-many --target=${{ inputs.target }} --all + - name: Delete Previous Nx Cache + if: ${{ steps.cache-restore.outputs.cache-hit }} + continue-on-error: true + shell: bash + run: | + gh extension install actions/gh-actions-cache + gh actions-cache delete "nx-cache" --confirm + env: + GH_TOKEN: ${{ inputs.github_token }} + - name: Save Nx cache + uses: actions/cache/save@v4 + with: + path: .nx + key: nx-cache diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..40145f69 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,88 @@ +name: "Check and build" +on: + push: + branches: + - release + - main + - chore/CRC-28/single-job + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + actions: write + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + env: + cache-key: nx-cache + steps: + - name: Check out + uses: actions/checkout@v4 + - uses: ./.github/actions/run-nx-target + with: + target: lint + check-spell: + needs: lint + name: Check spell + runs-on: ubuntu-latest + env: + cache-key: nx-cache + steps: + - name: Check out + uses: actions/checkout@v4 + - uses: ./.github/actions/run-nx-target + with: + target: check:spell + check-types: + needs: check-spell + name: Check types + runs-on: ubuntu-latest + env: + cache-key: nx-cache + steps: + - name: Check out + uses: actions/checkout@v4 + - uses: ./.github/actions/run-nx-target + with: + target: check:types + test-unit: + needs: check-types + name: Test unit + runs-on: ubuntu-latest + env: + cache-key: nx-cache + steps: + - name: Check out + uses: actions/checkout@v4 + - uses: ./.github/actions/run-nx-target + with: + target: test:unit + build: + needs: test-unit + name: Build + runs-on: ubuntu-latest + env: + cache-key: nx-cache + steps: + - name: Check out + uses: actions/checkout@v4 + - uses: ./.github/actions/run-nx-target + with: + target: build + test-component: + needs: build + name: Test component + runs-on: ubuntu-latest + env: + cache-key: nx-cache + steps: + - name: Check out + uses: actions/checkout@v4 + - uses: ./.github/actions/run-nx-target + with: + target: test:component 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..6eaba2ee --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +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..91feb169 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "nrwl.angular-console" + ] +} 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..69fd0faf --- /dev/null +++ b/components/child-process-manager/CHANGELOG.md @@ -0,0 +1,14 @@ +# 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 + +## [Unreleased] diff --git a/components/child-process-manager/README.md b/components/child-process-manager/README.md new file mode 100644 index 00000000..11d166b6 --- /dev/null +++ b/components/child-process-manager/README.md @@ -0,0 +1,97 @@ +# child-process-manager + +This library allows to create a child process using `cross-spawn`, which provides compatibility across different operating systems, and manage its lifecycle. It mainly provides: + +* Allows to handle child processes as promises. The promise is resolved when the process finish successfully, and rejected when it fails. +* Allows to kill the child process when the parent process is killed. +* Allows to get logs from the child process. + +## Table of Contents + +- [Usage](#usage) + - [Installation](#installation) + - [Example](#example) +- [Development](#development) + - [Installation](#installation-1) + - [Monorepo tool](#monorepo-tool) + - [Unit tests](#unit-tests) + - [Build](#build) + - [NPM scripts reference](#npm-scripts-reference) + +## Usage + +### Installation + +This package is not published in NPM, so, for the moment it can be used only in this repository through PNPM workspaces. To use it, you have to add it to your dependencies in the `package.json` file: + +```json title="package.json" +{ + "dependencies": { + "@telefonica-cross/child-process-manager": "workspace:*" + } +} +``` + +### Example + +Import the library, create a child process and wait for it to finish. It will return the exit code and the logs in the resolved object. + +```js title="Example" +import { ChildProcessManager } from '@telefonica-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 +``` + +## Development + +### Installation + +TypeScript components of the IDP project 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) of all TypeScript components. + +### Monorepo tool + +Note that this component is part of a monorepo, so you can execute any command of the components from the root folder, and Nx will take care of executing the dependent commands in the right order. Any command described here should be executed from the root folder of the repository, using Nx. + +For example, a command like this: + +```sh title="Execute unit tests of the component inside its folder" +pnpm run test:unit +``` + +Should be executed like this: + +```sh title="Execute unit tests of the component, and all needed dependencies, from root folder" +pnpm nx test:unit child-process-manager +``` + +### Unit tests + +Unit tests are executed using [Jest](https://jestjs.io/). To run them, execute the following command: + +```sh +pnpm run test:unit +``` + +### Build + +This command generates the library into the `dist` directory, which is the one defined as the entry point in the `package.json` file. __Note that other components in the repository won't be able to use the library until this command is executed.__ + +```sh +pnpm run build +``` + +### NPM scripts reference + +- `test:unit` - Run unit tests. +- `build` - Build the library. +- `check:types` - Checks the TypeScript types. +- `lint` - Lint the code. +- `lint:fix` - Fix lint errors. + diff --git a/components/child-process-manager/babel.config.js b/components/child-process-manager/babel.config.js new file mode 100644 index 00000000..566be52a --- /dev/null +++ b/components/child-process-manager/babel.config.js @@ -0,0 +1,23 @@ +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..a7342607 --- /dev/null +++ b/components/child-process-manager/cspell.config.js @@ -0,0 +1,3 @@ +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..09ef6650 --- /dev/null +++ b/components/child-process-manager/eslint.config.mjs @@ -0,0 +1,29 @@ +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..9b449236 --- /dev/null +++ b/components/child-process-manager/jest.config.js @@ -0,0 +1,43 @@ +// 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: 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..91955cf6 --- /dev/null +++ b/components/child-process-manager/package.json @@ -0,0 +1,40 @@ +{ + "name": "@telefonica-cross/child-process-manager", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "check:ci": "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:ci", + "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..40700270 --- /dev/null +++ b/components/child-process-manager/project.json @@ -0,0 +1,12 @@ +{ + "$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..49601f4f --- /dev/null +++ b/components/child-process-manager/src/ChildProcessManager.ts @@ -0,0 +1,119 @@ +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"; + +const ENCODING_TYPE = "utf8"; + +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; + + constructor( + commandAndArguments: string[], + options: ChildProcessManagerOptions = {}, + ) { + this._command = this._getCommandToExecute(commandAndArguments); + this._silent = options.silent || false; + this._cwd = options.cwd || process.cwd(); + this._env = options.env; + this._logger = new Logger({ silent: this._silent }); + this._cliProcess = null; + } + + public get exitPromise() { + return this._exitPromise; + } + + public get exitCode() { + return this._exitCode; + } + + public get logs() { + return this._logger.logs; + } + + 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, + }, + }); + + 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; + } + + public async kill(): Promise { + if (this._cliProcess?.pid) { + treeKill(this._cliProcess.pid); + return this._exitPromise; + } + return { + exitCode: null, + logs: [], + }; + } + + 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..c016bb5a --- /dev/null +++ b/components/child-process-manager/src/ChildProcessManager.types.ts @@ -0,0 +1,54 @@ +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..e1270711 --- /dev/null +++ b/components/child-process-manager/src/Logger.ts @@ -0,0 +1,45 @@ +import type { + LoggerInterface, + LoggerOptions, + LoggerConstructor, + Logs, +} from "./Logger.types"; + +export function log(...args: unknown[]) { + // eslint-disable-next-line no-console + return console.log(...args); +} + +export const Logger: LoggerConstructor = class Logger + implements LoggerInterface +{ + private _silent: boolean; + private _logs: string[]; + + constructor(options: LoggerOptions) { + this._silent = options.silent || false; + this._logs = []; + + this.log = this.log.bind(this); + } + + public get logs(): Logs { + return this._logs; + } + + 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..282c7863 --- /dev/null +++ b/components/child-process-manager/src/Logger.types.ts @@ -0,0 +1,25 @@ +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..e72059e7 --- /dev/null +++ b/components/child-process-manager/src/index.ts @@ -0,0 +1,2 @@ +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..36be30fb --- /dev/null +++ b/components/child-process-manager/src/types.ts @@ -0,0 +1,2 @@ +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..fa38700f --- /dev/null +++ b/components/child-process-manager/test/unit/specs/Component.spec.ts @@ -0,0 +1,166 @@ +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..69fd0faf --- /dev/null +++ b/components/confluence-sync/CHANGELOG.md @@ -0,0 +1,14 @@ +# 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 + +## [Unreleased] diff --git a/components/confluence-sync/README.md b/components/confluence-sync/README.md new file mode 100644 index 00000000..5e1ed741 --- /dev/null +++ b/components/confluence-sync/README.md @@ -0,0 +1,262 @@ +# confluence-sync-pages + +This library is used to sync Confluence pages. It has two modes: tree and flat: +* 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. +* In flat mode, the library receives a list of Confluence pages, which can't be nested. Then, the library creates/deletes/updates the corresponding Confluence pages 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 under the root page not present in the list will be deleted. + * In flat mode, a Confluence id can be also provided for each page, and then it will be used to update the corresponding Confluence page. In such case, the root page is ignored. So, if all the pages have an id, the root page is not needed. + + +## Table of Contents + +- [Features](#features) +- [Usage](#usage) + - [Installation](#installation) + - [Example](#example) + - [API](#api) + - [`ConfluenceSyncPages`](#confluencesyncpages) + - [`sync`](#sync) +- [Development](#development) + - [Installation](#installation-1) + - [Monorepo tool](#monorepo-tool) + - [Unit tests](#unit-tests) + - [Component tests](#component-tests) + - [Build](#build) + - [NPM scripts reference](#npm-scripts-reference) + +## Features + +The library supports the following features: + +Sync Mode: Tree +* Create Confluence pages from a list of pages with their corresponding paths, under a root page. +* Support for nested pages. +* Create Confluence pages if they don't exist. +* Update Confluence pages if they already exist. +* Delete Confluence pages that are not present in the list. +* Support for images. + +Sync Mode: Flat +* Update Confluence pages from a list of pages with id that already exist. +* Create Confluence pages from a list of pages without id under the root page. +* Update Confluence pages without id, if they already exist under the root page. +* Delete Confluence pages under the root page that are not present in the list. +* Support for images. + +## Usage + +### Installation + +This package is not published in NPM, so, for the moment it can be used only in this repository through PNPM workspaces. To use it, you have to add it to your dependencies in the `package.json` file: + +```json title="package.json" +{ + "dependencies": { + "@telefonica-cross/confluence-sync": "workspace:*" + } +} +``` + +### Sync Mode: Tree + +- By default, the library will run in tree mode. If you want to run it in flat mode, you have to set the `syncMode` property of the configuration object to `flat`. +- The images are uploaded to Confluence as attachments. The library will create a new attachment if it doesn't exist, or delete it and create it again if it already exists. +- The library assumes that the Confluence instance is using the default page hierarchy, where pages are organized in spaces. The library will create all the pages under a root page of the space. +- The root page must exist before running the sync process. +- The ancestors of each page should be ordered 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']`. +- The first ancestor of each page must be the root page, if not, it will be added automatically. +- As a consequence of the previous point, the library doesn't support to update the root page. +- 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 pages not present in the list will be deleted. +- The library doesn't support to move pages from one parent to another. If you want to move a page, you should delete it and create it again under the new parent. +- 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`. + +#### Example + +Once installed, you can import it and use it in your code, and then execute it passing a list of pages to sync: + +```js title="Example" +import { ConfluenceSyncPages } from '@telefonica-cross/confluence-sync'; + +const confluenceSyncPages = new ConfluenceSyncPages({ + url: "https://confluence.tid.es", + personalAccessToken: "*******", + spaceId: "CTO", + 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', + }, + } +]); +``` + +### Sync Mode: Flat + +- By default, the library will run in tree mode. If you want to run it in flat mode, you have to set the `syncMode` property of the configuration object to `flat`. +- The images are uploaded to Confluence as attachments. The library will create a new attachment if it doesn't exist, or delete it and create it again if it already exists. +- The pages provided with id must exist before running the sync process. +- If all the pages provided have an id, rootPageId is not required. +- The pages provided with id will be updated, the pages provided without id will be created/updated under the root page. +- If rootPageId is provided, the pages under the root page that are not present in the input will be deleted. +- Pages without id must not have ancestors. Those pages will be created/updated under the root page. + +#### Example + +```js title="ExampleWithFlatMode" +import { ConfluenceSyncPages, SyncModes } from '@telefonica-cross/confluence-sync'; + +const confluenceSyncPages = new ConfluenceSyncPages({ + url: "https://confluence.tid.es", + personalAccessToken: "*******", + spaceId: "CTO", + 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` + +This class is 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. + +## Development + +### Installation + +TypeScript components of the IDP project 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) of all TypeScript components. + +### Monorepo tool + +Note that this component is part of a monorepo, so you can execute any command of the components from the root folder, and Nx will take care of executing the dependent commands in the right order. Any command described here should be executed from the root folder of the repository, using Nx. + +For example, a command like this: + +```sh title="Execute unit tests of the component inside its folder" +pnpm run test:unit +``` + +Should be executed like this: + +```sh title="Execute unit tests of the component, and all needed dependencies, from root folder" +pnpm nx test:unit confluence-sync-pages +``` + +### Unit tests + +Unit tests are executed using [Jest](https://jestjs.io/). To run them, execute the following command: + +```sh +pnpm run test:unit +``` + +### Component tests + +Component tests are executed also using [Jest](https://jestjs.io/). But in this case, no dependencies are mocked, so the tests are executed against the real dependencies. A [mock server](https://www.mocks-server.org/) is used to simulate the Confluence API. To run them, execute the following command, which will start the mock server and execute the tests: + +```sh +pnpm run test:component +``` + +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 run confluence:mock +``` + +And, in a separate terminal: + +```sh +pnpm run test:component:run +``` + +### Build + +This command generates the library into the `dist` directory, which is the one defined as the entry point in the `package.json` file. __Note that other components in the repository won't be able to use the library until this command is executed.__ + +```sh +pnpm run build +``` + +### NPM scripts reference + +- `test:unit` - Run unit tests. +- `test:component` - Run component tests. +- `test:component:run` - Run component tests without starting the mock server. +- `confluence:mock` - Start the mock server. +- `build` - Build the library. +- `check:types` - Checks the TypeScript types. +- `lint` - Lint the code. +- `lint:fix` - Fix lint errors. + diff --git a/components/confluence-sync/babel.config.js b/components/confluence-sync/babel.config.js new file mode 100644 index 00000000..566be52a --- /dev/null +++ b/components/confluence-sync/babel.config.js @@ -0,0 +1,23 @@ +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/cross-confluence-tools.code-workspace b/components/confluence-sync/cross-confluence-tools.code-workspace new file mode 100644 index 00000000..a3951baa --- /dev/null +++ b/components/confluence-sync/cross-confluence-tools.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "../.." + } + ], + "settings": { + "files.associations": { + "nx.json": "jsonc", + "**/project.json": "jsonc" + } + } +} \ No newline at end of file diff --git a/components/confluence-sync/cspell.config.js b/components/confluence-sync/cspell.config.js new file mode 100644 index 00000000..a7342607 --- /dev/null +++ b/components/confluence-sync/cspell.config.js @@ -0,0 +1,3 @@ +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..037250e0 --- /dev/null +++ b/components/confluence-sync/eslint.config.mjs @@ -0,0 +1,56 @@ +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..ea694e5b --- /dev/null +++ b/components/confluence-sync/jest.component.config.js @@ -0,0 +1,19 @@ +// 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, + + // 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..9b449236 --- /dev/null +++ b/components/confluence-sync/jest.unit.config.js @@ -0,0 +1,43 @@ +// 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: 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..87205f95 --- /dev/null +++ b/components/confluence-sync/mocks.config.js @@ -0,0 +1,28 @@ +// 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..a736427f --- /dev/null +++ b/components/confluence-sync/mocks/collections.ts @@ -0,0 +1,73 @@ +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..7da238b3 --- /dev/null +++ b/components/confluence-sync/mocks/routes/Confluence.ts @@ -0,0 +1,434 @@ +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..78c6995c --- /dev/null +++ b/components/confluence-sync/mocks/routes/Spy.ts @@ -0,0 +1,45 @@ +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..7c045f60 --- /dev/null +++ b/components/confluence-sync/mocks/support/SpyStorage.ts @@ -0,0 +1,24 @@ +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..184bcb85 --- /dev/null +++ b/components/confluence-sync/mocks/support/SpyStorage.types.ts @@ -0,0 +1,15 @@ +/** 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..909ab540 --- /dev/null +++ b/components/confluence-sync/mocks/support/fixtures/ConfluencePages.ts @@ -0,0 +1,448 @@ +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..db44c535 --- /dev/null +++ b/components/confluence-sync/mocks/support/fixtures/HierarchicalConfluencePages.ts @@ -0,0 +1,247 @@ +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..6c8cc101 --- /dev/null +++ b/components/confluence-sync/package.json @@ -0,0 +1,54 @@ +{ + "name": "@telefonica-cross/confluence-sync", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "check:ci": "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:ci", + "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..db66de8e --- /dev/null +++ b/components/confluence-sync/project.json @@ -0,0 +1,12 @@ +{ + "$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..5249a122 --- /dev/null +++ b/components/confluence-sync/src/ConfluenceSyncPages.ts @@ -0,0 +1,409 @@ +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..91ed0eae --- /dev/null +++ b/components/confluence-sync/src/ConfluenceSyncPages.types.ts @@ -0,0 +1,137 @@ +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..ce425f6c --- /dev/null +++ b/components/confluence-sync/src/confluence/CustomConfluenceClient.ts @@ -0,0 +1,268 @@ +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..4c3c5a0b --- /dev/null +++ b/components/confluence-sync/src/confluence/CustomConfluenceClient.types.ts @@ -0,0 +1,93 @@ +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..7f01e009 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/AttachmentsNotFoundError.ts @@ -0,0 +1,8 @@ +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..cf0a9851 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/AxiosErrors.ts @@ -0,0 +1,29 @@ +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..bf68343e --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/CreateAttachmentsError.ts @@ -0,0 +1,8 @@ +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..383828df --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/CreatePageError.ts @@ -0,0 +1,8 @@ +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..e3e939d7 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/DeletePageError.ts @@ -0,0 +1,5 @@ +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..be6c8175 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/PageNotFoundError.ts @@ -0,0 +1,5 @@ +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..26c52266 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/UpdatePageError.ts @@ -0,0 +1,8 @@ +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..2540fa89 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/BadRequestError.ts @@ -0,0 +1,11 @@ +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..c71b637f --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/InternalServerError.ts @@ -0,0 +1,11 @@ +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..1d6be13f --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/UnauthorizedError.ts @@ -0,0 +1,11 @@ +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..6c712b14 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/UnexpectedError.ts @@ -0,0 +1,5 @@ +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..81550cc0 --- /dev/null +++ b/components/confluence-sync/src/confluence/errors/axios/UnknownAxiosError.ts @@ -0,0 +1,11 @@ +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..8d276cb2 --- /dev/null +++ b/components/confluence-sync/src/confluence/types.ts @@ -0,0 +1 @@ +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..e611688a --- /dev/null +++ b/components/confluence-sync/src/errors/CompoundError.ts @@ -0,0 +1,8 @@ +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..e86e33e6 --- /dev/null +++ b/components/confluence-sync/src/errors/NotAncestorsExpectedValidationError.ts @@ -0,0 +1,16 @@ +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..7c08fedb --- /dev/null +++ b/components/confluence-sync/src/errors/PendingPagesToSyncError.ts @@ -0,0 +1,18 @@ +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..148b9185 --- /dev/null +++ b/components/confluence-sync/src/errors/RootPageRequiredException.ts @@ -0,0 +1,26 @@ +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..a6ad7e3d --- /dev/null +++ b/components/confluence-sync/src/index.ts @@ -0,0 +1,3 @@ +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..adf232f1 --- /dev/null +++ b/components/confluence-sync/src/support/Pages.ts @@ -0,0 +1,11 @@ +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..7b4e721b --- /dev/null +++ b/components/confluence-sync/src/types.ts @@ -0,0 +1,7 @@ +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..428056b7 --- /dev/null +++ b/components/confluence-sync/test/component/specs/Sync.spec.ts @@ -0,0 +1,720 @@ +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..fe8c2920 --- /dev/null +++ b/components/confluence-sync/test/component/support/fixtures/Pages.ts @@ -0,0 +1,222 @@ +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..b2eb474e --- /dev/null +++ b/components/confluence-sync/test/component/support/mock/Client.ts @@ -0,0 +1,50 @@ +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..2b03a997 --- /dev/null +++ b/components/confluence-sync/test/unit/specs/ConfluenceSyncPages.spec.ts @@ -0,0 +1,696 @@ +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..2e9a6ce5 --- /dev/null +++ b/components/confluence-sync/test/unit/specs/confluence/CustomConfluenceClient.test.ts @@ -0,0 +1,691 @@ +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..39e19eb5 --- /dev/null +++ b/components/confluence-sync/test/unit/support/Logs.ts @@ -0,0 +1,3 @@ +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..d627febb --- /dev/null +++ b/components/confluence-sync/test/unit/support/fixtures/Pages.ts @@ -0,0 +1,178 @@ +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..d287d6ae --- /dev/null +++ b/components/confluence-sync/test/unit/support/mocks/ConfluenceClient.ts @@ -0,0 +1,24 @@ +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..69e60e10 --- /dev/null +++ b/components/confluence-sync/test/unit/support/mocks/CustomConfluenceClient.ts @@ -0,0 +1,19 @@ +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..77219db3 --- /dev/null +++ b/components/confluence-sync/test/unit/support/utils/TempFiles.ts @@ -0,0 +1,21 @@ +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/cspell.config.js b/components/cspell-config/cspell.config.js new file mode 100644 index 00000000..9ecee6c7 --- /dev/null +++ b/components/cspell-config/cspell.config.js @@ -0,0 +1,3 @@ +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..14ac2da2 --- /dev/null +++ b/components/cspell-config/dictionaries/company.txt @@ -0,0 +1,2 @@ +Telefónica +testing 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..8b137891 --- /dev/null +++ b/components/cspell-config/dictionaries/people.txt @@ -0,0 +1 @@ + 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..561ed161 --- /dev/null +++ b/components/cspell-config/eslint.config.mjs @@ -0,0 +1,3 @@ +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..2d8b9faa --- /dev/null +++ b/components/cspell-config/index.js @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Telefónica 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..f377dee9 --- /dev/null +++ b/components/cspell-config/package.json @@ -0,0 +1,19 @@ +{ + "name": "@telefonica-cross/cspell-config", + "version": "0.1.0", + "private": true, + "type": "commonjs", + "scripts": { + "check:ci": "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..1a86f204 --- /dev/null +++ b/components/cspell-config/project.json @@ -0,0 +1,20 @@ +{ + "$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/cspell.config.cjs b/components/eslint-config/cspell.config.cjs new file mode 100644 index 00000000..a7342607 --- /dev/null +++ b/components/eslint-config/cspell.config.cjs @@ -0,0 +1,3 @@ +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..07d02747 --- /dev/null +++ b/components/eslint-config/eslint.config.js @@ -0,0 +1,3 @@ +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..20ea7363 --- /dev/null +++ b/components/eslint-config/index.js @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2024 Telefónica 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..8d661ce2 --- /dev/null +++ b/components/eslint-config/package.json @@ -0,0 +1,16 @@ +{ + "name": "@telefonica-cross/eslint-config", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "check:ci": "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..e76cb455 --- /dev/null +++ b/components/eslint-config/project.json @@ -0,0 +1,20 @@ +{ + "$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..69fd0faf --- /dev/null +++ b/components/markdown-confluence-sync/CHANGELOG.md @@ -0,0 +1,14 @@ +# 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 + +## [Unreleased] diff --git a/components/markdown-confluence-sync/README.md b/components/markdown-confluence-sync/README.md new file mode 100644 index 00000000..2bd3e88b --- /dev/null +++ b/components/markdown-confluence-sync/README.md @@ -0,0 +1,455 @@ +# markdown-confluence-sync + +This library is used to sync Docusaurus Docs with Confluence pages. It reads the Docusaurus Docs and creates/deletes/updates the corresponding Confluence pages. All the pages are created under a parent page, which must be provided in configuration. Note that the parent page must exist before running the sync process, and that all pages not present in the Docusaurus docs will be deleted. + +## Table of Contents + +- [Features](#features) + - [Markdown conversion](#markdown-conversion) + - [Sync mode](#sync-mode) +- [Usage](#usage) + - [Installation](#installation) + - [Using the CLI](#using-the-cli) + - [Configuration](#configuration) + - [Configuration file](#configuration-file) + - [Arguments](#arguments) + - [Using the library](#using-the-library) + - [API](#api) + - [`DocusaurusToConfluence`](#docusaurustoconfluence) + - [`sync`](#sync) + - [Automation notice](#automation-notice) +- [Development](#development) + - [Installation](#installation-1) + - [Monorepo tool](#monorepo-tool) + - [Unit tests](#unit-tests) + - [Component tests](#component-tests) + - [Build](#build) + - [NPM scripts reference](#npm-scripts-reference) + +## Features + +This library requires a Confluence instance with the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/) enabled. It also requires a personal access token to authenticate against the Confluence instance. You can create one in the Personal access tokens section of your Atlassian account. + +It also requires a Docusaurus project with the [docs plugin](https://docusaurus.io/docs/docs-introduction) installed. The library will read the docs from the Docusaurus project and create/delete/update the corresponding Confluence pages following the same hierarchical structure under a provided Confluence page depending on the synchronization mode to use (which will be explained below). + +Pages to be synced must have a `title` property, and a `sync_to_confluence` property set to `true` in the page [frontmatter](https://docusaurus.io/docs/create-doc#doc-front-matter). + +The library has two modes in the configuration for sync pages (`tree` or `flat`): +* `tree` sync mode - mirrors the hierarchical pages structure from given docs folder under a Confluence root page. +* `flat` sync mode - synchronize a list of pages 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. + +You have also must take into account next consideration: + +* Some files are considered index files of categories, these files represent the content of the category itself and contain it's metadata. These **index files**, from now on, are `index.md`, `index.mdx`, `README.md` and `README.mdx`, and files with same name as the category's folder name (eg: for category with name `categoryA` either `.md` and `.mdx` will be considered indices). +* 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.** +* 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. +* Title can be overwritten using the `confluence_title` property in the frontmatter metadata of a Markdown file. This property will replace the title of the page in Confluence. +* Adding ancestors title to its children's title may produce an unnecessarily long titles. To avoid this, you can use the `confluence_short_name` property in the frontmatter metadata of a Markdown file. This property replaces 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 + ``` +* To add a prefix slug to all Confluence pages, set the confluence.rootPageName option. + * For instance, with the provided configuration, the page "`Page C`" will have the title "`[My Project][Category A][Category B] Page C`" in Confluence: + ```javascript + // file:docusaurus.config.js + module.export ={ + confluence: { + rootPageName: "My Project", + }, + }; + ``` + + +When you use `tree` mode, you have also must take into account next considerations: + +* 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`, 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. +* 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. +* Docusaurus provides the ability to specify category item metadata using `_category_.json` or `_category_.yml` files in the respective folder. These files allow you to define various category metadata. Currently, only the **"label"** field is used to overwrite the category page title. Here are a couple of examples to illustrate how this works: + * If there is a folder named `metadata` within the `docs` directory, and it contains a `_category_.yml` file with the following information, the corresponding page in Confluence will be renamed as _Category with Metadata_: + ```yaml + # file:<...docsDir...>/metadata/_category_.yml + --- + label: Category with Metadata + ``` + * Alternatively, if there is a folder named metadata within the docs directory, and it contains an **index file** that sets its title, along with a `_category_.yml` file with the following information, the corresponding page in Confluence will also be renamed as _Category with Metadata_: + ```markdown + + --- + title: Metadata + --- + + # Metadata + ``` + ```yaml + # file:<...docsDir...>/metadata/_category_.yml + --- + label: Category with Metadata + ``` + +For example, when use `tree` mode, the following Docusaurus docs structure: + +```title="Docusaurus project" +docusaurus-website/ +├── docs/ +│ ├── index.md -> IGNORED +│ ├── category-a/ +│ │ ├── index.md -> FOLDER PARENT PAGE +│ │ ├── page-a.md -> category-a/page-a +│ │ ├── page-b.md -> category-a/page-b +│ │ └── category-b/ +│ │ ├── index.md -> FOLDER PARENT PAGE. category-a/category-b +│ │ ├── page-a.md -> category-a/category-b/index.md page +│ │ └── page-b.md -> category-a/category-b/index.md page +│ ├── category-c/ -> PARENT PAGE. Empty page created in Confluence +│ │ ├── page-a.md -> CHILD of category-c/page-a +│ │ └── page-b.md -> CHILD of category-c/page-b +│ ├── category-d/ -> PARENT PAGE. Empty page created in Confluence +│ │ ├── _category_.yml -> RENAME CATEGORY TITLE +│ ├── category-e/ +│ │ ├── _category_.yml -> RENAME CATEGORY TITLE GIVEN BY INDEX +│ │ └── index.md -> FOLDER PARENT PAGE. category-d/category-e +│ ├── page-a.md -> page-a +│ └── page-b.md -> page-b +├── markdown-confluence-sync.config.js +├── docusaurus.config.js <- DEFINE YOUR CONFIGURATION HERE +└── package.json +``` + +When you use `flat` mode, you have also must take into account next considerations: + * `flat` sync mode need a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) to search files by glob + * To add a file pattern we using `filesPattern` option. + * `filesPattern` option use [glob pattern](https://github.com/isaacs/node-glob#glob-primer) to filter files. + * 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 filter to get all files starting with the "check" word. + ```javascript + // file:docusaurus.config.js + module.export ={ + mode: "flat", + filesPattern: "**/check*", + }; + ``` + +### Markdown conversion + +Docusaurus provides some markdown features that are not easy to convert to Confluence HTML format without defining a custom style for them. There are also other details that make difficult to convert links or images, for example. So, for the moment, the library only supports a subset of markdown features. Here is a list of some features that are not supported yet: + +* [Admonitions](https://docusaurus.io/docs/markdown-features/admonitions) - Docusaurus admonitions are converted to block quotes keeping their content. +* Images - Images are removed. +* MDX - MDX files are supported, but you must take into account the following considerations: + * MDX files are processed, but MDX syntax will be removed except for the Tabs tags. + * [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. + ``` + +* Footnotes - Footnotes are removed. +* [Mermaid diagrams](https://docusaurus.io/docs/next/markdown-features/diagrams) - 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.

+
+ ``` + +### Sync mode + +The library has two modes for reading pages (`tree` or `flat`), by default the pages will be read in `tree`. +* The `Tree` mode will create a hierarchy as we have defined it and send the pages to confluence with their ancestors. To activate this mode you can either not add this option, as it will take the default `tree` option: + ```json title="markdown-confluence-sync.config.js" + { + "docsDir": "docs", + "confluence": { + ... + } + } + ``` + + or set the `mode` option to `tree`: + ```json title="markdown-confluence-sync.config.js" + { + "mode": "tree", + "docsDir": "docs", + "confluence": { + ... + } + } + ``` + +* The `flat` mode reads pages by file patterns and structures these pages as children of the root. In case there is a confluence id (`confluence_page_id`) in the read page, it always updates the Confluence page with that id. + + When the mode option is `flat`, the library needs a [glob pattern](https://github.com/isaacs/node-glob#glob-primer) pattern to find the files, which must be defined using the `filesPattern` option. + + ```json title="markdown-confluence-sync.config.js" + { + "mode": "flat", + "filesPattern": "**/check*", + } + ``` + +## Usage + +### Installation + +This package is not published in NPM, so, for the moment it can be used only in this repository through PNPM workspaces. To use it, you have to add it to your dependencies in the `package.json` file. + +```json title="package.json" +{ + "dependencies": { + "markdown-confluence-sync": "workspace:*" + } +} +``` + +### Using the CLI + +Once installed, the library provides an NPM binary named `markdown-confluence-sync` that can be used to sync the docs in a Docusaurus project with Confluence pages. To use it, you can add a script in your `package.json` file: + +```json title="package.json" +{ + "scripts": { + "markdown-confluence-sync": "markdown-confluence-sync" + } +} +``` + +Then, you can run it with: + +```sh +pnpm run markdown-confluence-sync +``` + +#### 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_`. For example, to set the `docsDir` property, you have to set the `MARKDOWN_CONFLUENCE_SYNC_DOCS_DIR` environment variable. + +| 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 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. 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`. 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. + +```json title="markdown-confluence-sync.config.js" +{ + "docsDir": "docs", + "confluence": { + "url": "https://confluence.tid.es", + "personalAccessToken": "*******", + "spaceKey": "CTO", + "rootPageId": "Cross-Cutting+Enablers/IDP/docs" + } +} +``` + +###### 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 +pnpm exec markdown-confluence-sync --docsDir ./docs --logLevel debug +``` + +### Using the library + +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 { DocusaurusToConfluence } from '@telefonica-cross/markdown-confluence-sync'; + +const docusaurusToConfluence = new DocusaurusToConfluence({ + docsDir: path.resolve(__dirname, "..", "docs"); + confluence: { + url: "https://confluence.tid.es", + personalAccessToken: "*******", + spaceKey: "CTO", + rootPageId: "Cross-Cutting+Enablers/IDP/docs" + } +}); + +await docusaurusToConfluence.sync(); +``` + +#### API + +##### `DocusaurusToConfluence` + +This is the main class of the library. It is used to sync the Docusaurus docs with Confluence pages. 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. + +### 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('@telefonica-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. + + + +## Development + +### Installation + +TypeScript components of the IDP project 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) of all TypeScript components. + +### Monorepo tool + +Note that this component is part of a monorepo, so you can execute any command of the components from the root folder, and Nx will take care of executing the dependent commands in the right order. Any command described here should be executed from the root folder of the repository, using Nx. + +For example, a command like this: + +```sh title="Execute unit tests of the component inside its folder" +pnpm run test:unit +``` + +Should be executed like this: + +```sh title="Execute unit tests of the component, and all needed dependencies, from root folder" +pnpm nx test:unit markdown-confluence-sync +``` + +### Unit tests + +Unit tests are executed using [Jest](https://jestjs.io/). To run them, execute the following command: + +```sh +pnpm run test:unit +``` + +### 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. The mock server is in the `confluence-sync-pages` package, and it is started automatically when the component tests are executed. + +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 run test:component +``` + +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 run confluence:mock +``` + +And, in a separate terminal: + +```sh +pnpm run test:component:run +``` + +### Build + +This command generates the library into the `dist` directory, which is the one defined as the entry point in the `package.json` file. __Note that other components in the repository won't be able to use the library until this command is executed.__ + +```sh +pnpm run build +``` + +__Warning: The library's CLI won't work until the build process is executed.__ + +### NPM scripts reference + +- `test:unit` - Run unit tests. +- `build` - Build the library. +- `check:types` - Checks the TypeScript types. +- `lint` - Lint the code. +- `lint:fix` - Fix lint errors. diff --git a/components/markdown-confluence-sync/babel.config.cjs b/components/markdown-confluence-sync/babel.config.cjs new file mode 100644 index 00000000..79c69ccc --- /dev/null +++ b/components/markdown-confluence-sync/babel.config.cjs @@ -0,0 +1,27 @@ +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..5fbf0850 --- /dev/null +++ b/components/markdown-confluence-sync/bin/markdown-confluence-sync.mjs @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +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..a7342607 --- /dev/null +++ b/components/markdown-confluence-sync/cspell.config.cjs @@ -0,0 +1,3 @@ +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..49b56c14 --- /dev/null +++ b/components/markdown-confluence-sync/eslint.config.js @@ -0,0 +1,79 @@ +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..48c4c2aa --- /dev/null +++ b/components/markdown-confluence-sync/jest.component.config.cjs @@ -0,0 +1,26 @@ +// 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..d2495371 --- /dev/null +++ b/components/markdown-confluence-sync/jest.unit.config.cjs @@ -0,0 +1,53 @@ +// 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..87205f95 --- /dev/null +++ b/components/markdown-confluence-sync/mocks.config.cjs @@ -0,0 +1,28 @@ +// 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..a1adb17a --- /dev/null +++ b/components/markdown-confluence-sync/mocks/collections.ts @@ -0,0 +1,75 @@ +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..421f36ab --- /dev/null +++ b/components/markdown-confluence-sync/mocks/routes/Confluence.ts @@ -0,0 +1,410 @@ +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..78c6995c --- /dev/null +++ b/components/markdown-confluence-sync/mocks/routes/Spy.ts @@ -0,0 +1,45 @@ +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..7c045f60 --- /dev/null +++ b/components/markdown-confluence-sync/mocks/support/SpyStorage.ts @@ -0,0 +1,24 @@ +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..184bcb85 --- /dev/null +++ b/components/markdown-confluence-sync/mocks/support/SpyStorage.types.ts @@ -0,0 +1,15 @@ +/** 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..62b42286 --- /dev/null +++ b/components/markdown-confluence-sync/mocks/support/fixtures/ConfluencePages.ts @@ -0,0 +1,536 @@ +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..154dc560 --- /dev/null +++ b/components/markdown-confluence-sync/package.json @@ -0,0 +1,106 @@ +{ + "name": "@telefonica-cross/markdown-confluence-sync", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "check:ci": "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:ci", + "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", + "@telefonica-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", + "@telefonica-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..923b9bea --- /dev/null +++ b/components/markdown-confluence-sync/project.json @@ -0,0 +1,58 @@ +{ + "$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": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "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": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "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..2f011edb --- /dev/null +++ b/components/markdown-confluence-sync/src/Cli.ts @@ -0,0 +1,12 @@ +import { DocusaurusToConfluence } from "./lib/index.js"; + +export async function run() { + const docusaurusToConfluence = new DocusaurusToConfluence({ + config: { + readArguments: true, + readEnvironment: true, + readFile: true, + }, + }); + await docusaurusToConfluence.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..735d0069 --- /dev/null +++ b/components/markdown-confluence-sync/src/index.ts @@ -0,0 +1 @@ +export * from "./lib/index.js"; diff --git a/components/markdown-confluence-sync/src/lib/DocusaurusToConfluence.ts b/components/markdown-confluence-sync/src/lib/DocusaurusToConfluence.ts new file mode 100644 index 00000000..5f5a7ce6 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/DocusaurusToConfluence.ts @@ -0,0 +1,131 @@ +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 { SyncModes } from "@telefonica-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 { + DocusaurusToConfluenceConstructor, + DocusaurusToConfluenceInterface, + Configuration, + LogLevelOption, + LogLevelOptionDefinition, + ModeOptionDefinition, + FilesPatternOptionDefinition, + ModeOption, + FilesPatternOption, +} from "./DocusaurusToConfluence.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 DocusaurusToConfluence: DocusaurusToConfluenceConstructor = class DocusaurusToConfluence + implements DocusaurusToConfluenceInterface +{ + private _docusaurusPages: DocusaurusPagesInterface; + private _confluenceSync: ConfluenceSyncInterface; + private _configuration: ConfigInterface; + 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/DocusaurusToConfluence.types.ts b/components/markdown-confluence-sync/src/lib/DocusaurusToConfluence.types.ts new file mode 100644 index 00000000..29515a44 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/DocusaurusToConfluence.types.ts @@ -0,0 +1,73 @@ +import type { + OptionDefinition, + OptionInterfaceOfType, +} from "@mocks-server/config"; +import type { LogLevel } from "@mocks-server/logger"; +import type { SyncModes } from "@telefonica-cross/confluence-sync"; + +export type FilesPattern = string | string[]; + +declare global { + //eslint-disable-next-line @typescript-eslint/no-namespace + namespace DocusaurusToConfluence { + 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 = DocusaurusToConfluence.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 DocusaurusToConfluence interface */ +export interface DocusaurusToConfluenceConstructor { + /** Returns DocusaurusToConfluence interface + * @returns DocusaurusToConfluence instance {@link DocusaurusToConfluenceInterface}. + */ + // eslint-disable-next-line no-undef + new (options: DocusaurusToConfluence.Config): DocusaurusToConfluenceInterface; +} + +export interface DocusaurusToConfluenceInterface { + /** 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..90c0dc84 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.ts @@ -0,0 +1,208 @@ +import type { LoggerInterface } from "@mocks-server/logger"; +import type { + ConfluenceInputPage, + ConfluenceSyncPagesInterface, +} from "@telefonica-cross/confluence-sync"; +import { + ConfluenceSyncPages, + SyncModes, +} from "@telefonica-cross/confluence-sync"; + +import type { ModeOption } from "../DocusaurusToConfluence.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..561d7ad9 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/ConfluenceSync.types.ts @@ -0,0 +1,112 @@ +import type { + ConfigNamespaceInterface, + OptionInterfaceOfType, + OptionDefinition, +} from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import type { ConfluenceInputPage } from "@telefonica-cross/confluence-sync"; + +import type { ModeOption } from "../DocusaurusToConfluence.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 DocusaurusToConfluence { + 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..d5e5617c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.ts @@ -0,0 +1,220 @@ +import { dirname, resolve } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import type { ConfluenceInputPage } from "@telefonica-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..2d2d18ee --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.types.ts @@ -0,0 +1,55 @@ +import type { LoggerInterface } from "@mocks-server/logger"; +import type { ConfluenceInputPage } from "@telefonica-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..088ccdaa --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidDetailsTagMissingSummaryError.ts @@ -0,0 +1,5 @@ +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..f1f67dfc --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/InvalidTemplateError.ts @@ -0,0 +1 @@ +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..ef573ef7 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/errors/PageIdRequiredException.ts @@ -0,0 +1,7 @@ +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..d8656fd1 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.ts @@ -0,0 +1,40 @@ +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..901e61e4 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-attachments-images.types.ts @@ -0,0 +1 @@ +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..aca7dc9c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.ts @@ -0,0 +1,30 @@ +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..d4ab2266 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-add-notice.types.ts @@ -0,0 +1,4 @@ +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..c7f45ae5 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.ts @@ -0,0 +1,65 @@ +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..7ef03c99 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-remove-links.types.ts @@ -0,0 +1,13 @@ +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..f8b4d5ff --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-details.ts @@ -0,0 +1,158 @@ +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..9f6d6953 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-img-tags.ts @@ -0,0 +1,83 @@ +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..8573ccd3 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.ts @@ -0,0 +1,93 @@ +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..41c18e71 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-internal-references.types.ts @@ -0,0 +1,12 @@ +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..5a2e8349 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-strikethrough.ts @@ -0,0 +1,33 @@ +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..2d1eed45 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/rehype/rehype-replace-task-list.ts @@ -0,0 +1,68 @@ +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..7647fe34 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-footnotes.ts @@ -0,0 +1,22 @@ +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..4575f392 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.ts @@ -0,0 +1,27 @@ +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..38ad62de --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.ts @@ -0,0 +1,102 @@ +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..f1e8cc72 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/confluence/transformer/support/remark/remark-replace-mermaid.types.ts @@ -0,0 +1,3 @@ +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..7e55869e --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.ts @@ -0,0 +1,80 @@ +import { relative } from "node:path"; + +import type { LoggerInterface } from "@mocks-server/logger"; +import { glob } from "glob"; + +import type { FilesPattern } from "../DocusaurusToConfluence.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..e08b565c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusFlatPages.types.ts @@ -0,0 +1,20 @@ +import type { FilesPattern } from "../DocusaurusToConfluence.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..15a8b814 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.ts @@ -0,0 +1,70 @@ +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 "../DocusaurusToConfluence.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..9920a19b --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPages.types.ts @@ -0,0 +1,86 @@ +import type { + OptionInterfaceOfType, + OptionDefinition, + ConfigInterface, +} from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; + +import type { + FilesPatternOption, + ModeOption, +} from "../DocusaurusToConfluence.types.js"; + +export type DocusaurusPageId = string; + +type DocsDirOptionValue = string; + +declare global { + //eslint-disable-next-line @typescript-eslint/no-namespace + namespace DocusaurusToConfluence { + 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..ea9eeab2 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.ts @@ -0,0 +1,34 @@ +import { SyncModes } from "@telefonica-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..d1a8440f --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusPagesFactory.types.ts @@ -0,0 +1,41 @@ +import type { ConfigInterface } from "@mocks-server/config"; +import type { LoggerInterface } from "@mocks-server/logger"; +import type { SyncModes } from "@telefonica-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..a05c3d19 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.ts @@ -0,0 +1,71 @@ +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..d293254f --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/DocusaurusTreePages.types.ts @@ -0,0 +1,18 @@ +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..577b3ed7 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.ts @@ -0,0 +1,96 @@ +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..7b7b9b6c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPage.types.ts @@ -0,0 +1,59 @@ +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..5118b46b --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.ts @@ -0,0 +1,19 @@ +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..b8e666d4 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageFactory.types.ts @@ -0,0 +1,30 @@ +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..41d5760d --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/DocusaurusDocPageMdx.ts @@ -0,0 +1,50 @@ +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..09f43454 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidMarkdownFormatException.ts @@ -0,0 +1,5 @@ +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..36657f0c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidPathException.ts @@ -0,0 +1,5 @@ +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..3ea64445 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabItemMissingLabelError.ts @@ -0,0 +1,5 @@ +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..fa46bf2f --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/InvalidTabsFormatError.ts @@ -0,0 +1,5 @@ +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..c4c3554b --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/errors/PathNotExistException.ts @@ -0,0 +1,5 @@ +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..c5db225f --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-remove-mdx-code.ts @@ -0,0 +1,32 @@ +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..bc3a138d --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-admonitions.ts @@ -0,0 +1,75 @@ +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..ec44422a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-replace-tabs.ts @@ -0,0 +1,81 @@ +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..1898f11e --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-transform-details.ts @@ -0,0 +1,36 @@ +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..6b1fd495 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/remark/remark-validate-frontmatter.ts @@ -0,0 +1,31 @@ +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..fbd04d6b --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/pages/support/validators/FrontMatterValidator.ts @@ -0,0 +1,16 @@ +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..4fa60b8a --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.ts @@ -0,0 +1,24 @@ +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..f95e2401 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocItemFactory.types.ts @@ -0,0 +1,30 @@ +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..fa2b7cf2 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.ts @@ -0,0 +1,49 @@ +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..0813fa05 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTree.types.ts @@ -0,0 +1,55 @@ +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..34505515 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.ts @@ -0,0 +1,152 @@ +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..ea05d982 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreeCategory.types.ts @@ -0,0 +1,78 @@ +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..53c2d4ab --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.ts @@ -0,0 +1,35 @@ +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..9989e1f4 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePage.types.ts @@ -0,0 +1,30 @@ +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..40108568 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.ts @@ -0,0 +1,34 @@ +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..6b2bb787 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageFactory.types.ts @@ -0,0 +1,29 @@ +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..1bf24260 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/DocusaurusDocTreePageMdx.ts @@ -0,0 +1,31 @@ +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..e9303925 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/errors/CategoryIndexNotFoundException.ts @@ -0,0 +1,5 @@ +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..2d0b6f3c --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/tree/support/validators/CategoryItemMetadata.ts @@ -0,0 +1,10 @@ +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..7d2ab189 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/util/files.ts @@ -0,0 +1,144 @@ +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..75369632 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/docusaurus/util/files.types.ts @@ -0,0 +1,6 @@ +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..128870bb --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/index.ts @@ -0,0 +1,2 @@ +export * from "./types.js"; +export * from "./DocusaurusToConfluence.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..a4df015f --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/support/typesValidations.ts @@ -0,0 +1,8 @@ +/** + * 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..fe826f42 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.ts @@ -0,0 +1,28 @@ +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..1844c7da --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/support/unist/unist-util-replace.types.ts @@ -0,0 +1,78 @@ +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..be45bbbf --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/types.ts @@ -0,0 +1,3 @@ +export * from "./DocusaurusToConfluence.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..cdb899c7 --- /dev/null +++ b/components/markdown-confluence-sync/src/lib/util/paths.ts @@ -0,0 +1,15 @@ +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..21462054 --- /dev/null +++ b/components/markdown-confluence-sync/src/types.ts @@ -0,0 +1 @@ +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..2bd05ef6 --- /dev/null +++ b/components/markdown-confluence-sync/src/types/unist-util-find.d.ts @@ -0,0 +1 @@ +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..fbd316c0 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/config-file-wrong/markdown-confluence-sync.config.cjs @@ -0,0 +1,10 @@ +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..b5152abc --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/config-file/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-default-root/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-empty-root/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-alternative-index-files/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..a049b44d --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-page-id/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-confluence-title/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-files-in-root/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mdx-files/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..e0d873de --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-mermaid-diagrams/markdown-confluence-sync.config.cjs @@ -0,0 +1,9 @@ +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..2ebe560c --- /dev/null +++ b/components/markdown-confluence-sync/test/component/fixtures/mock-server-with-root-page-name/markdown-confluence-sync.config.cjs @@ -0,0 +1,10 @@ +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..b767d6e5 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/config.spec.ts @@ -0,0 +1,173 @@ +import { ChildProcessManager } from "@telefonica-cross/child-process-manager"; +import type { ChildProcessManagerInterface } from "@telefonica-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..575c2354 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/flat.spec.ts @@ -0,0 +1,268 @@ +import type { ChildProcessManagerInterface } from "@telefonica-cross/child-process-manager"; +import { ChildProcessManager } from "@telefonica-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..6caec263 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/specs/sync.spec.ts @@ -0,0 +1,1680 @@ +import { rm } from "fs/promises"; +import { resolve } from "path"; + +import type { ChildProcessManagerInterface } from "@telefonica-cross/child-process-manager"; +import { ChildProcessManager } from "@telefonica-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..39e19eb5 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Logs.ts @@ -0,0 +1,3 @@ +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..60377429 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Mock.ts @@ -0,0 +1,50 @@ +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..184bcb85 --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Mock.types.ts @@ -0,0 +1,15 @@ +/** 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..1f4d0bdd --- /dev/null +++ b/components/markdown-confluence-sync/test/component/support/Paths.ts @@ -0,0 +1,11 @@ +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..652ea775 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/Cli.test.ts @@ -0,0 +1,20 @@ +import { customDocusaurusToConfluence } 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 () => { + customDocusaurusToConfluence.sync.mockReturnValue(true); + await run(); + + await expect(customDocusaurusToConfluence.sync).toHaveBeenCalled(); + }); +}); diff --git a/components/markdown-confluence-sync/test/unit/specs/DocusaurusToConfluence.test.ts b/components/markdown-confluence-sync/test/unit/specs/DocusaurusToConfluence.test.ts new file mode 100644 index 00000000..e49afbde --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/DocusaurusToConfluence.test.ts @@ -0,0 +1,43 @@ +import { customConfluenceSync } from "@support/mocks/ConfluenceSync"; +import { customDocusaurusPages } from "@support/mocks/DocusaurusPages"; + +import { DocusaurusToConfluence } from "@src/lib"; + +const CONFIG = { + config: { + readArguments: false, + readFile: false, + readEnvironment: false, + }, +}; + +describe("docusaurusToConfluence", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(DocusaurusToConfluence).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 DocusaurusToConfluence()).toThrow( + "Please provide configuration", + ); + }); + + it("when called twice, it should send to synchronize the pages to confluence twice", async () => { + // Arrange + const docusaurusToConfluence = new DocusaurusToConfluence(CONFIG); + customDocusaurusPages.read.mockResolvedValue([]); + + // Act + await docusaurusToConfluence.sync(); + await docusaurusToConfluence.sync(); + + // Assert + expect(customConfluenceSync.sync.mock.calls).toHaveLength(2); + }); +}); 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..f2dffd64 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/Library.spec.ts @@ -0,0 +1,7 @@ +import * as library from "@src/index"; + +describe("library", () => { + it("should export DocusaurusToConfluence class", () => { + expect(library.DocusaurusToConfluence).toBeDefined(); + }); +}); 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..5b81842c --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/ConfluenceSync.test.ts @@ -0,0 +1,317 @@ +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..4feec8d4 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/ConfluencePageTransformer.test.ts @@ -0,0 +1,723 @@ +import { join, resolve } from "path"; + +import type { ConfluenceInputPage } from "@telefonica-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..d1cfa7ac --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-attachments-images.test.ts @@ -0,0 +1,98 @@ +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..383a88a0 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-add-notice.test.ts @@ -0,0 +1,23 @@ +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..c5476d65 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-remove-links.test.ts @@ -0,0 +1,140 @@ +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..e9c4629e --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-details.test.ts @@ -0,0 +1,75 @@ +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..e2d3d5f4 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-img-tags.test.ts @@ -0,0 +1,190 @@ +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..b38922b4 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-internal-references.test.ts @@ -0,0 +1,253 @@ +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..ae2ffe10 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-strikethrough.test.ts @@ -0,0 +1,41 @@ +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..3a533e9d --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/rehype/rehype-replace-task-list.test.ts @@ -0,0 +1,92 @@ +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..56e10660 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-footnotes.test.ts @@ -0,0 +1,31 @@ +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..6a592d34 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-remove-mdx-code-blocks.test.ts @@ -0,0 +1,35 @@ +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..784d2edd --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/confluence/transformer/support/remark/remark-replace-mermaid.test.ts @@ -0,0 +1,112 @@ +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..8f621988 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPages.test.ts @@ -0,0 +1,776 @@ +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..d633b9c4 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/DocusaurusPagesFactory.test.ts @@ -0,0 +1,41 @@ +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 "@telefonica-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..5dd2fbab --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-admonitions.test.ts @@ -0,0 +1,162 @@ +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..4cc0ec7d --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-replace-tabs.test.ts @@ -0,0 +1,106 @@ +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..36a49648 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-transform-details.test.ts @@ -0,0 +1,52 @@ +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..1f3745bf --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/pages/support/remark/remark-validate-frontmatter.test.ts @@ -0,0 +1,83 @@ +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..bcafc6b2 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTree.test.ts @@ -0,0 +1,159 @@ +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..6b5c3a0f --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeCategory.test.ts @@ -0,0 +1,527 @@ +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..a43a1522 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreeItemFactory.test.ts @@ -0,0 +1,187 @@ +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..2cac6b80 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/DocusaurusDocTreePage.test.ts @@ -0,0 +1,318 @@ +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..34d1e1a7 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/tree/errors/CategoryIndexNotFoundException.test.ts @@ -0,0 +1,11 @@ +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..464461a0 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/docusaurus/utils/files.test.ts @@ -0,0 +1,19 @@ +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..ba51d717 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/specs/support/unist/unist-util-replace.test.ts @@ -0,0 +1,22 @@ +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..d04bc48f --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluencePageTransformer.ts @@ -0,0 +1,13 @@ +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..327582b6 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSync.ts @@ -0,0 +1,15 @@ +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..bcf5cc4f --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/ConfluenceSyncPages.ts @@ -0,0 +1,19 @@ +jest.mock("@telefonica-cross/confluence-sync"); + +import * as confluenceSyncPagesMock from "@telefonica-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..903165f9 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusPages.ts @@ -0,0 +1,13 @@ +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..8ade3967 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/mocks/DocusaurusToConfluence.ts @@ -0,0 +1,13 @@ +jest.mock("@src/lib/DocusaurusToConfluence"); + +import * as customDocusaurusToConfluenceClass from "@src/lib/DocusaurusToConfluence"; + +export const customDocusaurusToConfluence = { + sync: jest.fn(), +}; + +jest + .spyOn(customDocusaurusToConfluenceClass, "DocusaurusToConfluence") + .mockImplementation(() => { + return customDocusaurusToConfluence; + }); 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..e3bdeab1 --- /dev/null +++ b/components/markdown-confluence-sync/test/unit/support/utils/TempFiles.ts @@ -0,0 +1,22 @@ +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 }); + } + // eslint-disable-next-line +} 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..1f278a7f --- /dev/null +++ b/cspell.config.cjs @@ -0,0 +1,3 @@ +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..8fbfb97e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,2 @@ +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..bb448a8f --- /dev/null +++ b/nx.json @@ -0,0 +1,125 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "npmScope": "@telefonica-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": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "dependsOn": [ + { + "target": "eslint:config", + "projects": ["eslint-config"] + } + ] + }, + "check:spell": { + "cache": true, + "inputs": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "dependsOn": [ + { + "target": "cspell:config", + "projects": ["cspell-config"] + } + ] + }, + "check:types": { + "cache": true, + "inputs": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "cache": true, + "inputs": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "outputs": ["{projectRoot}/coverage"], + "dependsOn": [ + "^build" + ] + }, + "build": { + "cache": true, + "inputs": [ + "default", + "production", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "dependsOn": [ + "^build" + ], + "outputs": [ + "{projectRoot}/dist", + "{projectRoot}/package.json", + "{projectRoot}/README.md", + "{projectRoot}/CHANGELOG.md", + "{projectRoot}/bin" + ] + }, + "test:component": { + "cache": true, + "parallelism": false, + "inputs": [ + "default", + { "dependentTasksOutputFiles": "**/*", "transitive": true } + ], + "dependsOn": [ + "build", + "^build" + ] + }, + "check:ci": { + "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..e3602c6d --- /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 + '@telefonica-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 + '@telefonica-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..303734bb --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "components/*"