diff --git a/README.md b/README.md index cdd37ea..84de964 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,14 @@ emulsify component install card emulsify component create promo-card --directory molecules --format default ``` +Built-in platforms are `drupal`, `wordpress`, and `none`. For WordPress child themes, use the WordPress platform and starter: + +```bash +emulsify init "My Theme" ./wp-content/themes --platform wordpress +``` + +When WordPress is auto-detected, Emulsify initializes child themes into the detected themes directory, such as `wp-content/themes/my-theme` or `web/app/themes/my-theme` for Bedrock. + For non-interactive environments, pass the flags that normally prompt for input: ```bash diff --git a/docs/README.md b/docs/README.md index 5dffb77..a4e2867 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,16 +2,16 @@ These docs expand on the short project README and are organized by the task a project user or maintainer is usually trying to complete. -| Topic | Use This When | -| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| [CLI Reference](./cli-reference.md) | Looking up commands, aliases, options, and examples. | -| [Project Initialization](./project-initialization.md) | Creating a new Emulsify project from a starter, including Drupal and non-interactive examples. | -| [Systems](./systems.md) | Listing built-in systems, installing systems, using custom system repositories, or understanding system configuration. | -| [Components](./components.md) | Listing installable components, installing components and dependencies, using dry runs, and creating local components. | -| [Project Configuration](./configuration.md) | Understanding `project.emulsify.json`, system and variant references, structure mappings, and validation. | -| [Component Template Overrides](./component-template-overrides.md) | Replacing the built-in `component create` templates with project-level templates. | -| [Hooks And Cache](./hooks-and-cache.md) | Understanding starter hooks, system install hooks, script execution, and the `~/.emulsify/cache` repository cache. | -| [Development](./development.md) | Setting up this repository, understanding source layout, and running checks. | -| [Release](./release.md) | Understanding CI, develop version bumps, semantic-release, and npm publishing. | -| [Contributors](./contributors.md) | Viewing project contributors moved out of the root README. | -| [Website Usage Copy](./emulsify-info-cli-updates.md) | Copy-ready usage content for the `emulsify.info` CLI page. | +| Topic | Use This When | +| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| [CLI Reference](./cli-reference.md) | Looking up commands, aliases, options, and examples. | +| [Project Initialization](./project-initialization.md) | Creating a new Emulsify project from a starter, including Drupal, WordPress, and non-interactive examples. | +| [Systems](./systems.md) | Listing built-in systems, installing systems, understanding variant compatibility, or using custom system repositories. | +| [Components](./components.md) | Listing installable components, installing components and dependencies, using dry runs, and creating local components. | +| [Project Configuration](./configuration.md) | Understanding `project.emulsify.json`, system and variant references, structure mappings, and validation. | +| [Component Template Overrides](./component-template-overrides.md) | Replacing the built-in `component create` templates with project-level templates. | +| [Hooks And Cache](./hooks-and-cache.md) | Understanding starter hooks, system install hooks, script execution, and the `~/.emulsify/cache` repository cache. | +| [Development](./development.md) | Setting up this repository, understanding source layout, and running checks. | +| [Release](./release.md) | Understanding CI, develop version bumps, semantic-release, and npm publishing. | +| [Contributors](./contributors.md) | Viewing project contributors moved out of the root README. | +| [Website Usage Copy](./emulsify-info-cli-updates.md) | Copy-ready usage content for the `emulsify.info` CLI page. | diff --git a/docs/cli-reference.md b/docs/cli-reference.md index b8002cc..87c0a67 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -29,21 +29,29 @@ emulsify init [name] [path] Options: -| Option | Description | -| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `-m, --machineName ` | Machine-friendly project folder and config name. If omitted, the CLI derives it from the project name. Drupal machine names use underscores. | -| `-s, --starter ` | Starter Git repository to clone. | -| `-c, --checkout ` | Starter commit, branch, or tag to check out after clone. | -| `-p, --platform ` | Platform to use when auto-detection is unavailable or should be overridden. Built-in starters are currently available for `drupal` and `none`. | -| `-y, --yes` | Accept default values for missing init options without prompting. | +| Option | Description | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `-m, --machineName ` | Machine-friendly project folder and config name. If omitted, the CLI derives it from the project name. Drupal machine names use underscores. | +| `-s, --starter ` | Starter Git repository to clone. | +| `-c, --checkout ` | Starter commit, branch, or tag to check out after clone. | +| `-p, --platform ` | Platform to use when auto-detection is unavailable or should be overridden. Built-in platforms are `drupal`, `wordpress`, and `none`. | +| `-y, --yes` | Accept default values for missing init options without prompting. | -When `--platform` is not provided and the platform cannot be detected, interactive terminals prompt with `drupal` and `none`. +When `--platform` is not provided and the platform cannot be detected, interactive terminals prompt with `drupal`, `wordpress`, and `none`. + +WordPress auto-detection creates child themes inside the detected themes directory: + +- Standard WordPress: `wp-content/themes/` +- Bedrock: `web/app/themes/` + +The built-in WordPress starter is `https://github.com/emulsify-ds/emulsify-wordpress-starter`. Examples: ```bash emulsify init "My Project" ./projects --platform none emulsify init "My Theme" ./web/themes/custom --platform drupal +emulsify init "My Theme" ./wp-content/themes --platform wordpress emulsify init "My Theme" ./web/themes/custom --platform drupal --machineName my_custom_theme emulsify init "My Project" ./projects --starter https://github.com/emulsify-ds/emulsify-starter --checkout main --platform none ``` @@ -93,6 +101,14 @@ emulsify system install --repository https://github.com/example/example-system.g Selecting `create a new system` writes `system.emulsify.json` in the current Emulsify project root. Complete the generated system name, repository, structures, variants, and components before using it to install or generate components. +System variant compatibility is selected from each variant's `platform` expression. Examples: + +- `"platform": "wordpress"` matches WordPress projects. +- `"platform": "drupal || wordpress"` matches Drupal and WordPress projects. +- `"platform": "none"` is generic and can be installed by any concrete project platform. + +Project configuration uses only concrete `project.platform` values: `drupal`, `wordpress`, or `none`. Only system variants use `||` expressions. A project with `project.platform: "none"` can install any component library system; when multiple variants are equally compatible, the CLI prompts in an interactive terminal or errors in non-interactive mode. + ## `component list` ```bash diff --git a/docs/configuration.md b/docs/configuration.md index dbdf885..6322efc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -19,12 +19,14 @@ Emulsify CLI stores project state in `project.emulsify.json`. Commands search fo } ``` -| Field | Required | Description | -| --------------------- | -------- | ------------------------------------------------ | -| `project.platform` | Yes | Platform name for starter and variant selection. | -| `project.name` | Yes | Human-facing project name provided during init. | -| `project.machineName` | Yes | Machine-friendly folder/config name. | -| `starter.repository` | Yes | Starter repository used to create the project. | +| Field | Required | Description | +| --------------------- | -------- | ------------------------------------------------------------------------------------------- | +| `project.platform` | Yes | Concrete platform name for starter and variant selection: `drupal`, `wordpress`, or `none`. | +| `project.name` | Yes | Human-facing project name provided during init. | +| `project.machineName` | Yes | Machine-friendly folder/config name. | +| `starter.repository` | Yes | Starter repository used to create the project. | + +`project.platform` never uses compatibility expressions. Values such as `drupal || wordpress` are valid only on system variant `platform` fields. ## System And Variant Configuration @@ -48,15 +50,25 @@ Emulsify CLI stores project state in `project.emulsify.json`. Commands search fo } ``` -| Field | Required After System Install | Description | -| ---------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `system.repository` | Yes | Git repository containing the selected system. Must be a URL ending in `.git` for commands that parse the system name. | -| `system.checkout` | Yes | Commit, branch, or tag used for the cached system. | -| `variant.platform` | Yes | Platform variant selected from the system. Built-in schemas currently allow `none` and `drupal`. | -| `variant.structureImplementations` | Yes | Project-relative directories for each named system structure. | +| Field | Required After System Install | Description | +| ---------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `system.repository` | Yes | Git repository containing the selected system. Must be a URL ending in `.git` for commands that parse the system name. | +| `system.checkout` | Yes | Commit, branch, or tag used for the cached system. | +| `variant.platform` | Yes | Selected system variant platform string. It may be concrete, such as `wordpress`, or an expression such as `drupal \|\| wordpress`. | +| `variant.structureImplementations` | Yes | Project-relative directories for each named system structure. | Component commands require both `system` and `variant` to be present. +Variant compatibility examples: + +```json +{ "platform": "wordpress" } +{ "platform": "drupal || wordpress" } +{ "platform": "none" } +``` + +`variant.platform: "none"` is generic and can be selected by any concrete project platform. A project with `project.platform: "none"` can install any compatible component library system; if multiple variants are equally compatible, the CLI prompts in an interactive terminal or errors in non-interactive mode. + ## Structure Implementations Structure implementations tell the CLI where installed or generated components belong. @@ -107,9 +119,9 @@ Common validation issues: Manual edits are safe when they preserve the schema and match the installed system. Be careful changing: -| Field | Risk | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `system.checkout` | The local cache path depends on checkout. Changing it may cause the CLI to clone or load a different system version. | -| `system.repository` | Component commands parse the system name from this URL and expect a `.git` suffix. | -| `variant.structureImplementations` | Install and create destinations are calculated from these directories. | -| `project.platform` | System install selects variants from this value. Existing system and variant config may no longer match. | +| Field | Risk | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `system.checkout` | The local cache path depends on checkout. Changing it may cause the CLI to clone or load a different system version. | +| `system.repository` | Component commands parse the system name from this URL and expect a `.git` suffix. | +| `variant.structureImplementations` | Install and create destinations are calculated from these directories. | +| `project.platform` | System install selects variants from this concrete value. Existing system and variant config may no longer match. Do not change it to a `\|\|` expression. | diff --git a/docs/development.md b/docs/development.md index 912fa3e..08e0c80 100644 --- a/docs/development.md +++ b/docs/development.md @@ -36,7 +36,7 @@ emulsify --help | Script | Purpose | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| `npm run build` | Generate schema types, compile TypeScript, copy `package.json` to `dist`, and install production dependencies in `dist`. | +| `npm run build` | Generate schema types, compile TypeScript, copy package metadata to `dist`, and install production dependencies in `dist`. | | `npm run build-schema-types` | Generate TypeScript definitions from JSON Schema files. | | `npm run build-ts` | Compile TypeScript with `tsconfig.dist.json`. | | `npm run watch` | Rebuild when files in `src` change. | @@ -75,4 +75,4 @@ npm run build npm run test ``` -`npm run build` may report npm audit warnings from dependencies after the production install in `dist`; those warnings are not TypeScript build failures. +`npm run build` installs production dependencies in `dist` from the root lockfile. diff --git a/docs/emulsify-info-cli-updates.md b/docs/emulsify-info-cli-updates.md index 7987e43..680530d 100644 --- a/docs/emulsify-info-cli-updates.md +++ b/docs/emulsify-info-cli-updates.md @@ -40,22 +40,26 @@ Options: - `--machineName `: Sets the machine-friendly project name. When omitted, Emulsify CLI derives it from the project name. - `--starter `: Uses a specific starter repository. - `--checkout `: Checks out a specific starter commit, branch, or tag. -- `--platform `: Sets the project platform when auto-detection is unavailable or should be overridden. Built-in starters are available for `none` and `drupal`. +- `--platform `: Sets the project platform when auto-detection is unavailable or should be overridden. Built-in platforms are `drupal`, `wordpress`, and `none`. - `--yes`: Accepts default init values for missing options without prompting. Built-in starter repositories: - `https://github.com/emulsify-ds/emulsify-starter` - `https://github.com/emulsify-ds/emulsify-drupal-starter` +- `https://github.com/emulsify-ds/emulsify-wordpress-starter` Examples: ```bash emulsify init "My Project" ./projects --platform none emulsify init "My Theme" ./web/themes/custom --platform drupal --yes +emulsify init "My Theme" ./wp-content/themes --platform wordpress emulsify init "My Project" ./projects --platform none --starter https://github.com/emulsify-ds/emulsify-starter --checkout main ``` +When WordPress is auto-detected, Emulsify initializes child themes into the detected themes directory, such as `wp-content/themes/` or `web/app/themes/` for Bedrock. + ## Systems `emulsify system list` lists the built-in systems that Emulsify CLI can install. `emulsify system ls` is the same command. @@ -72,6 +76,8 @@ Built-in systems in this CLI version: `emulsify system install [name]` installs a system in the current Emulsify project. The command installs required components by default. +System variants can declare platform compatibility with values such as `"wordpress"`, `"drupal || wordpress"`, or generic `"none"`. Project configuration uses only concrete `project.platform` values: `drupal`, `wordpress`, or `none`. + Options: - `--repository `: Installs a system from a specific Git repository. Custom repository URLs must end in `.git`. diff --git a/docs/project-initialization.md b/docs/project-initialization.md index bc6fed7..3bedc1b 100644 --- a/docs/project-initialization.md +++ b/docs/project-initialization.md @@ -12,9 +12,14 @@ The command clones a starter, writes `project.emulsify.json`, installs dependenc The CLI tries to determine the platform from the current working directory before prompting or using defaults. +Built-in project platforms are `drupal`, `wordpress`, and `none`. + | Detection | Result | | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Drupal Composer project with `extra.drupal-scaffold.locations.web-root` | Platform `drupal`, target parent `/themes/custom`. | +| Standard WordPress site with `wp-content/themes` | Platform `wordpress`, target parent `wp-content/themes`. | +| Bedrock WordPress site with `web/app/themes` | Platform `wordpress`, target parent `web/app/themes`. | +| Composer/custom web-root WordPress site with `web/wp-content/themes` | Platform `wordpress`, target parent `web/wp-content/themes`. | | Existing Emulsify project found by `project.emulsify.json` | Platform `none`, target parent `/web/themes/custom`. | | No detectable platform | Use `--platform`, prompt in an interactive terminal, or use `--yes` to accept defaults. | @@ -23,15 +28,17 @@ When the platform cannot be detected and stdin is a TTY, the prompt choices are: ```text ? Platform: ❯ drupal + wordpress none ``` Built-in starter repositories: -| Platform | Repository | Checkout | -| -------- | -------------------------------------------------------- | -------- | -| `none` | `https://github.com/emulsify-ds/emulsify-starter` | `main` | -| `drupal` | `https://github.com/emulsify-ds/emulsify-drupal-starter` | `main` | +| Platform | Repository | Checkout | +| ----------- | ----------------------------------------------------------- | -------- | +| `none` | `https://github.com/emulsify-ds/emulsify-starter` | `main` | +| `drupal` | `https://github.com/emulsify-ds/emulsify-drupal-starter` | `main` | +| `wordpress` | `https://github.com/emulsify-ds/emulsify-wordpress-starter` | `main` | You can override starter resolution with `--starter` and `--checkout`. @@ -91,16 +98,35 @@ Creates: ./web/themes/custom/my_theme ``` +WordPress child themes use hyphens and are initialized into the detected themes directory: + +```bash +emulsify init "My Theme" --platform wordpress +``` + +When run inside a standard WordPress site, this creates: + +```text +wp-content/themes/my-theme +``` + +When run inside a Bedrock site, this creates: + +```text +web/app/themes/my-theme +``` + If the target already exists, initialization stops with an error and does not overwrite the directory. ## Machine Names If `--machineName` is omitted, the CLI derives one from the project name by removing non-alphanumeric characters, replacing spaces, and lowercasing the result. -| Platform | Project Name | Derived Machine Name | -| -------- | ---------------- | -------------------- | -| `none` | `Marketing Site` | `marketing-site` | -| `drupal` | `My Theme` | `my_theme` | +| Platform | Project Name | Derived Machine Name | +| ----------- | ---------------- | -------------------- | +| `none` | `Marketing Site` | `marketing-site` | +| `drupal` | `My Theme` | `my_theme` | +| `wordpress` | `My Theme` | `my-theme` | Use `--machineName` when the folder name or Drupal theme machine name must be exact. @@ -126,7 +152,7 @@ With `--yes`, missing values use the current defaults: Explicit arguments and flags still take precedence over `--yes` defaults. -In non-interactive mode, provide `--platform drupal` or `--platform none` when auto-detection is unavailable. +In non-interactive mode, provide `--platform drupal`, `--platform wordpress`, or `--platform none` when auto-detection is unavailable. ## Custom Starter @@ -143,7 +169,9 @@ If `--checkout` is omitted, the starter repository default branch is cloned. ## Generated Project Configuration -After a Drupal init, the generated `project.emulsify.json` looks like this: +After init, the generated `project.emulsify.json` stores a concrete platform value. It never stores compatibility expressions such as `drupal || wordpress` in `project.platform`. + +For a Drupal init, the file looks like this: ```json { diff --git a/docs/systems.md b/docs/systems.md index 84c0339..227445d 100644 --- a/docs/systems.md +++ b/docs/systems.md @@ -49,7 +49,7 @@ For a built-in system, the command: 3. Checks out the latest Git tag when the built-in system reference does not specify a checkout. 4. Clones the system into the local Emulsify cache. 5. Reads and validates `system.emulsify.json` from the cached system. -6. Selects the variant that matches `project.platform`. +6. Selects the best compatible variant for `project.platform`. 7. Writes `system` and `variant` entries into `project.emulsify.json`. 8. Installs components marked `required: true`. 9. Installs variant-level general files and directories. @@ -119,7 +119,7 @@ The scaffold is intentionally minimal and must be completed before it represents } ``` -The generated variant platform follows the current project platform when it is `drupal` or `none`. If `system.emulsify.json` already exists, the CLI stops rather than overwrite it. +The generated variant platform follows the current project platform when it is `drupal`, `wordpress`, or `none`. If `system.emulsify.json` already exists, the CLI stops rather than overwrite it. Creating a new system definition does not clone a remote system, install components, install general assets, run install hooks, or update `project.emulsify.json`. @@ -153,7 +153,7 @@ After a successful install, `project.emulsify.json` includes system and variant } ``` -The exact checkout and structure mappings come from the installed system. +The exact checkout, selected variant `platform` string, and structure mappings come from the installed system. If the selected system variant used `"platform": "drupal || wordpress"`, that original expression is stored in the project `variant.platform` value so later component commands can rehydrate the same variant. ## System Repository Shape @@ -169,15 +169,37 @@ A system repository must contain `system.emulsify.json` at its root. The schema Each variant used by the CLI must include: -| Field | Required | Purpose | -| -------------------------- | -------- | ----------------------------------------------------------------------- | -| `platform` | Yes | Variant platform. Built-in schemas currently allow `none` and `drupal`. | -| `structureImplementations` | Yes | Mapping from structure names to project-relative directories. | -| `components` | Yes | Installable component definitions. | -| `directories` | No | General directories copied during system install. | -| `files` | No | General files copied during system install. | +| Field | Required | Purpose | +| -------------------------- | -------- | ------------------------------------------------------------- | +| `platform` | Yes | Variant platform compatibility expression. | +| `structureImplementations` | Yes | Mapping from structure names to project-relative directories. | +| `components` | Yes | Installable component definitions. | +| `directories` | No | General directories copied during system install. | +| `files` | No | General files copied during system install. | -To be installable, a system must include a variant whose `platform` matches the current project's `project.platform`. +## Variant Platform Compatibility + +System variants can target one platform or multiple compatible platforms: + +```json +{ "platform": "wordpress" } +{ "platform": "drupal || wordpress" } +{ "platform": "none" } +``` + +Supported platform tokens are `drupal`, `wordpress`, and `none`. + +`none` on a variant means generic. A variant with `"platform": "none"` can be installed by any concrete project platform. + +Project configuration is different: `project.platform` is always a concrete value (`drupal`, `wordpress`, or `none`). Do not put `||` expressions in `project.platform`; only system variants use compatibility expressions. + +When installing a system, the CLI prefers: + +1. An exact platform match. +2. An expression containing the concrete project platform, such as `drupal || wordpress`. +3. A generic `none` variant. + +A project with `project.platform: "none"` can install any component library system. If more than one variant is equally compatible and no single best match is obvious, the CLI prompts in an interactive terminal or fails with a clear non-interactive error. Minimal example: @@ -194,7 +216,7 @@ Minimal example: ], "variants": [ { - "platform": "drupal", + "platform": "drupal || wordpress", "structureImplementations": [ { "name": "base", diff --git a/package-lock.json b/package-lock.json index e7b166d..ce0e62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "colorette": "^2.0.20", "commander": "^15.0.0", "consola": "^3.4.2", - "fs-extra": "^11.3.5", + "fs-extra": "^11.3.6", "progress": "^2.0.3", "simple-git": "^3.36.0" }, @@ -24,22 +24,22 @@ "emulsify": "dist/index.js" }, "devDependencies": { - "@commitlint/cli": "^21.0.2", - "@commitlint/config-conventional": "^21.0.2", + "@commitlint/cli": "^21.2.0", + "@commitlint/config-conventional": "^21.2.0", "@semantic-release/commit-analyzer": "^13.0.1", - "@semantic-release/github": "^12.0.8", + "@semantic-release/github": "^12.0.9", "@semantic-release/npm": "^13.1.5", "@semantic-release/release-notes-generator": "^14.1.1", "@types/fs-extra": "^11.0.4", "@types/jest": "^30.0.0", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/progress": "^2.0.7", "husky": "^9.1.7", "jest": "^30.4.2", "json-schema-to-typescript": "^15.0.4", - "lint-staged": "^17.0.7", + "lint-staged": "^17.0.8", "nodemon": "^3.1.14", - "prettier": "^3.8.4", + "prettier": "^3.9.4", "semantic-release": "^25.0.5", "ts-jest": "^29.4.11", "ts-node": "^10.9.2", @@ -107,13 +107,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -122,9 +122,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -132,21 +132,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -173,14 +173,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -190,14 +190,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -217,9 +217,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -227,29 +227,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -269,9 +269,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -279,9 +279,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -289,9 +289,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -299,27 +299,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -568,33 +568,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -602,14 +602,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -634,17 +634,18 @@ } }, "node_modules/@commitlint/cli": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.0.2.tgz", - "integrity": "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.2.0.tgz", + "integrity": "sha512-4GLVIhUaT3c3GBlQ0GB80/5H3xXdn/Tgw4lrsuoOQVDu2wl4Xw0GuzSar8xZKSMv4H3xaKaQXmvH91GmdyYBZA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/format": "^21.0.1", - "@commitlint/lint": "^21.0.2", - "@commitlint/load": "^21.0.2", - "@commitlint/read": "^21.0.2", - "@commitlint/types": "^21.0.1", + "@commitlint/config-conventional": "^21.2.0", + "@commitlint/format": "^21.2.0", + "@commitlint/lint": "^21.2.0", + "@commitlint/load": "^21.2.0", + "@commitlint/read": "^21.2.0", + "@commitlint/types": "^21.2.0", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, @@ -656,27 +657,27 @@ } }, "node_modules/@commitlint/config-conventional": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.0.2.tgz", - "integrity": "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.2.0.tgz", + "integrity": "sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-conventionalcommits": "^9.2.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-conventionalcommits": "^10.0.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/config-validator": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.0.1.tgz", - "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.2.0.tgz", + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "ajv": "^8.11.0" }, "engines": { @@ -684,13 +685,13 @@ } }, "node_modules/@commitlint/ensure": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.0.1.tgz", - "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.2.0.tgz", + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0" }, "engines": { @@ -708,13 +709,13 @@ } }, "node_modules/@commitlint/format": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.0.1.tgz", - "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.2.0.tgz", + "integrity": "sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "picocolors": "^1.1.1" }, "engines": { @@ -722,13 +723,13 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz", - "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.2.0.tgz", + "integrity": "sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", + "@commitlint/types": "^21.2.0", "semver": "^7.6.0" }, "engines": { @@ -736,32 +737,32 @@ } }, "node_modules/@commitlint/lint": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.0.2.tgz", - "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.2.0.tgz", + "integrity": "sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/is-ignored": "^21.0.2", - "@commitlint/parse": "^21.0.2", - "@commitlint/rules": "^21.0.2", - "@commitlint/types": "^21.0.1" + "@commitlint/is-ignored": "^21.2.0", + "@commitlint/parse": "^21.2.0", + "@commitlint/rules": "^21.2.0", + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@commitlint/load": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.0.2.tgz", - "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.2.0.tgz", + "integrity": "sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", "@commitlint/execute-rule": "^21.0.1", - "@commitlint/resolve-extends": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/resolve-extends": "^21.2.0", + "@commitlint/types": "^21.2.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", @@ -773,9 +774,9 @@ } }, "node_modules/@commitlint/message": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.0.2.tgz", - "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.2.0.tgz", + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==", "dev": true, "license": "MIT", "engines": { @@ -783,29 +784,85 @@ } }, "node_modules/@commitlint/parse": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.0.2.tgz", - "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.2.0.tgz", + "integrity": "sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" + "@commitlint/types": "^21.2.0", + "conventional-changelog-angular": "^9.0.0", + "conventional-commits-parser": "^7.0.0" }, "engines": { "node": ">=22.12.0" } }, + "node_modules/@commitlint/parse/node_modules/@simple-libs/stream-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@commitlint/parse/node_modules/conventional-changelog-angular": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.2.0.tgz", + "integrity": "sha512-2EihZvM1KmbBGwuUow8lNXLe+ECRFsyOcV8X0197/WI7Rvvgfi55Oicfw0wxJTXGGIVXSfj+xBoNj+cxLlIiVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@conventional-changelog/template": "^1.2.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@commitlint/parse/node_modules/conventional-commits-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.0.0.tgz", + "integrity": "sha512-dZe/p+FgGQLNJFqaCgNdl8j6a7gI8xuaN30Wy5g7nvyK3jqOpNUEUZ3pGJ5D68h89uRh038FtkeOk/bnGmYkmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0", + "meow": "^14.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@commitlint/parse/node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@commitlint/read": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.0.2.tgz", - "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.2.0.tgz", + "integrity": "sha512-ELx8Ovh/JoAw5lpvDgxc6Y0We9skf2IPI2RFN+gnYgDGjRdMSF8zeodxhZmcclLWzfUIF7hXLOa8gOlllYcvBA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/top-level": "^21.0.2", - "@commitlint/types": "^21.0.1", + "@commitlint/top-level": "^21.2.0", + "@commitlint/types": "^21.2.0", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" }, @@ -814,14 +871,14 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", - "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.2.0.tgz", + "integrity": "sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^21.0.1", - "@commitlint/types": "^21.0.1", + "@commitlint/config-validator": "^21.2.0", + "@commitlint/types": "^21.2.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" @@ -831,16 +888,16 @@ } }, "node_modules/@commitlint/rules": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.0.2.tgz", - "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.2.0.tgz", + "integrity": "sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^21.0.1", - "@commitlint/message": "^21.0.2", + "@commitlint/ensure": "^21.2.0", + "@commitlint/message": "^21.2.0", "@commitlint/to-lines": "^21.0.1", - "@commitlint/types": "^21.0.1" + "@commitlint/types": "^21.2.0" }, "engines": { "node": ">=22.12.0" @@ -857,9 +914,9 @@ } }, "node_modules/@commitlint/top-level": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.0.2.tgz", - "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.2.0.tgz", + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -870,19 +927,62 @@ } }, "node_modules/@commitlint/types": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.0.1.tgz", - "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==", + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.2.0.tgz", + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", "dev": true, "license": "MIT", "dependencies": { - "conventional-commits-parser": "^6.3.0", + "conventional-commits-parser": "^7.0.0", "picocolors": "^1.1.1" }, "engines": { "node": ">=22.12.0" } }, + "node_modules/@commitlint/types/node_modules/@simple-libs/stream-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@commitlint/types/node_modules/conventional-commits-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.0.0.tgz", + "integrity": "sha512-dZe/p+FgGQLNJFqaCgNdl8j6a7gI8xuaN30Wy5g7nvyK3jqOpNUEUZ3pGJ5D68h89uRh038FtkeOk/bnGmYkmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0", + "meow": "^14.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@commitlint/types/node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@conventional-changelog/git-client": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", @@ -910,6 +1010,16 @@ } } }, + "node_modules/@conventional-changelog/template": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.2.0.tgz", + "integrity": "sha512-12qHxvlKjHmP0PQ+17EREgC7lWyLwbph1RKcZQZ7k7ZWGmrxfxC9gadHGfvzr0g0u8BhiBGg3tks93txodlyRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1436,9 +1546,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { @@ -2356,9 +2466,9 @@ } }, "node_modules/@semantic-release/github": { - "version": "12.0.8", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-12.0.8.tgz", - "integrity": "sha512-tej5AAgK5X9wHRoDmYhecMXEHEkFeGOY1XsEblKxu8pIQwahzf1STYyr7iPU6Lpbg6C5I3N2w/ocXrBo+L7jhw==", + "version": "12.0.9", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-12.0.9.tgz", + "integrity": "sha512-ODIqb0V3QqndipryEEiaBxUQCFjvv7Oese5Dt4omMGa60YRNEW0Sx3K+zri0uac2Y6S9nOlMehciWIzvvRCTGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2388,9 +2498,9 @@ } }, "node_modules/@semantic-release/github/node_modules/undici": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", - "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { @@ -2841,9 +2951,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3624,9 +3734,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3686,9 +3796,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -3712,9 +3822,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -3732,11 +3842,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3798,9 +3908,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", "dev": true, "funding": [ { @@ -4347,16 +4457,16 @@ } }, "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.2.0.tgz", + "integrity": "sha512-UtlM9GqolY7OmlQh5L/UEVoKsTUpTgUVy1PU8JN5gl5Ydaejb7WRklGliG1SKPxxj7hzA173eG3Kt5fYWE2pmg==", "dev": true, "license": "ISC", "dependencies": { - "compare-func": "^2.0.0" + "@conventional-changelog/template": "^1.2.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/conventional-changelog-writer": { @@ -4644,9 +4754,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", + "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", "dev": true, "license": "ISC" }, @@ -4868,9 +4978,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", "dev": true, "license": "MIT", "workspaces": [ @@ -5017,9 +5127,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "funding": [ { "type": "github", @@ -5155,9 +5265,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", - "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -5287,6 +5397,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", + "deprecated": "Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.", "dev": true, "license": "MIT", "dependencies": { @@ -7056,10 +7167,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7175,9 +7296,9 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", - "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", + "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==", "dev": true, "license": "MIT", "dependencies": { @@ -7810,11 +7931,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nodemon": { "version": "3.1.14", @@ -7923,9 +8047,9 @@ } }, "node_modules/npm": { - "version": "11.12.1", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.12.1.tgz", - "integrity": "sha512-zcoUuF1kezGSAo0CqtvoLXX3mkRqzuqYdL6Y5tdo8g69NVV3CkjQ6ZBhBgB4d7vGkPcV6TcvLi3GRKPDFX+xTA==", + "version": "11.18.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.18.0.tgz", + "integrity": "sha512-T67M4L5wNm0cZ7EBLErcEkY1SmzEW/WJ+SADBzsFUY1UdAPfFHXFQtZ6SEXiK0+vzXysCvAsepbMaBTwnrAD+w==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -8004,8 +8128,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.4.2", - "@npmcli/config": "^10.8.1", + "@npmcli/arborist": "^9.9.0", + "@npmcli/config": "^10.12.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -8023,46 +8147,46 @@ "fs-minipass": "^3.0.3", "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "hosted-git-info": "^9.0.2", + "hosted-git-info": "^9.0.3", "ini": "^6.0.0", "init-package-json": "^8.2.5", - "is-cidr": "^6.0.3", + "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.5", - "libnpmexec": "^10.2.5", - "libnpmfund": "^7.0.19", + "libnpmdiff": "^8.1.11", + "libnpmexec": "^10.3.1", + "libnpmfund": "^7.0.25", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.5", - "libnpmpublish": "^11.1.3", + "libnpmpack": "^9.1.11", + "libnpmpublish": "^11.2.0", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", - "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.5", - "minimatch": "^10.2.4", + "libnpmversion": "^8.0.4", + "make-fetch-happen": "^15.0.6", + "minimatch": "^10.2.5", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^12.2.0", + "node-gyp": "^12.4.0", "nopt": "^9.0.0", "npm-audit-report": "^7.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.2", "npm-pick-manifest": "^11.0.3", - "npm-profile": "^12.0.1", + "npm-profile": "^12.0.2", "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", - "pacote": "^21.5.0", + "pacote": "^21.5.1", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", - "semver": "^7.7.4", + "semver": "^7.8.5", "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.11", + "tar": "^7.5.19", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", @@ -8118,7 +8242,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { - "version": "4.0.0", + "version": "4.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -8134,7 +8258,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.4.2", + "version": "9.9.0", "dev": true, "inBundle": true, "license": "ISC", @@ -8182,7 +8306,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.8.1", + "version": "10.12.0", "dev": true, "inBundle": true, "license": "ISC", @@ -8376,7 +8500,7 @@ } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.2.0", + "version": "3.2.1", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -8385,7 +8509,7 @@ } }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", + "version": "0.5.1", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -8424,13 +8548,13 @@ } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.1.0", + "version": "3.1.1", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { @@ -8499,7 +8623,7 @@ } }, "node_modules/npm/node_modules/bin-links": { - "version": "6.0.0", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -8527,7 +8651,7 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "5.0.4", + "version": "5.0.7", "dev": true, "inBundle": true, "license": "MIT", @@ -8596,7 +8720,7 @@ } }, "node_modules/npm/node_modules/cidr-regex": { - "version": "5.0.3", + "version": "5.0.5", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -8652,7 +8776,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "8.0.3", + "version": "8.0.4", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -8720,7 +8844,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/hosted-git-info": { - "version": "9.0.2", + "version": "9.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -8819,7 +8943,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.1.0", + "version": "10.2.0", "dev": true, "inBundle": true, "license": "MIT", @@ -8828,12 +8952,12 @@ } }, "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.3", + "version": "6.0.4", "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "^5.0.1" + "cidr-regex": "^5.0.4" }, "engines": { "node": ">=20" @@ -8901,12 +9025,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.1.5", + "version": "8.1.11", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.2", + "@npmcli/arborist": "^9.9.0", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -8920,13 +9044,13 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.2.5", + "version": "10.3.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.4.2", + "@npmcli/arborist": "^9.9.0", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -8943,12 +9067,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.19", + "version": "7.0.25", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.2" + "@npmcli/arborist": "^9.9.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -8968,12 +9092,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.1.5", + "version": "9.1.11", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.2", + "@npmcli/arborist": "^9.9.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -8983,7 +9107,7 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.1.3", + "version": "11.2.0", "dev": true, "inBundle": true, "license": "ISC", @@ -9027,7 +9151,7 @@ } }, "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.3", + "version": "8.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -9043,7 +9167,7 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.7", + "version": "11.5.1", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -9052,7 +9176,7 @@ } }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "15.0.5", + "version": "15.0.6", "dev": true, "inBundle": true, "license": "ISC", @@ -9075,12 +9199,12 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "10.2.4", + "version": "10.2.5", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -9128,35 +9252,17 @@ } }, "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", + "version": "1.0.6", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.3" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", "dev": true, @@ -9236,7 +9342,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "12.2.0", + "version": "12.4.0", "dev": true, "inBundle": true, "license": "MIT", @@ -9244,12 +9350,12 @@ "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", + "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { @@ -9360,13 +9466,13 @@ } }, "node_modules/npm/node_modules/npm-profile": { - "version": "12.0.1", + "version": "12.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0" + "proc-log": "^6.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -9413,7 +9519,7 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "21.5.0", + "version": "21.5.1", "dev": true, "inBundle": true, "license": "ISC", @@ -9474,7 +9580,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.1", + "version": "7.1.4", "dev": true, "inBundle": true, "license": "MIT", @@ -9571,7 +9677,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.7.4", + "version": "7.8.5", "dev": true, "inBundle": true, "license": "ISC", @@ -9595,17 +9701,17 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "4.1.0", + "version": "4.1.1", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.1", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.1.0", - "@sigstore/tuf": "^4.0.1", - "@sigstore/verify": "^3.1.0" + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -9622,12 +9728,12 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.8.7", + "version": "2.8.9", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -9696,7 +9802,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.11", + "version": "7.5.19", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -9724,13 +9830,13 @@ "license": "MIT" }, "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.15", + "version": "0.2.17", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -9757,7 +9863,7 @@ } }, "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", "dev": true, "inBundle": true, "license": "MIT", @@ -9791,6 +9897,15 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm/node_modules/undici": { + "version": "6.27.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -10307,9 +10422,9 @@ } }, "node_modules/prettier": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", - "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", "dev": true, "license": "MIT", "bin": { @@ -12025,9 +12140,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index a7c338b..cfce0f8 100644 --- a/package.json +++ b/package.json @@ -37,25 +37,25 @@ "version:develop": "node --import ./src/register.mjs ./src/scripts/versionDevelop.ts", "semantic-release": "semantic-release --config ./release.config.cjs", "prepare": "husky install", - "postbuild": "cp package.json dist/package.json && cd dist && npm install --only=production --ignore-scripts" + "postbuild": "cp package.json package-lock.json dist/ && cd dist && npm ci --omit=dev --ignore-scripts" }, "devDependencies": { - "@commitlint/cli": "^21.0.2", - "@commitlint/config-conventional": "^21.0.2", + "@commitlint/cli": "^21.2.0", + "@commitlint/config-conventional": "^21.2.0", "@semantic-release/commit-analyzer": "^13.0.1", - "@semantic-release/github": "^12.0.8", + "@semantic-release/github": "^12.0.9", "@semantic-release/npm": "^13.1.5", "@semantic-release/release-notes-generator": "^14.1.1", "@types/fs-extra": "^11.0.4", "@types/jest": "^30.0.0", - "@types/node": "^26.0.0", + "@types/node": "^26.1.0", "@types/progress": "^2.0.7", "husky": "^9.1.7", "jest": "^30.4.2", "json-schema-to-typescript": "^15.0.4", - "lint-staged": "^17.0.7", + "lint-staged": "^17.0.8", "nodemon": "^3.1.14", - "prettier": "^3.8.4", + "prettier": "^3.9.4", "semantic-release": "^25.0.5", "ts-jest": "^29.4.11", "ts-node": "^10.9.2", @@ -69,7 +69,7 @@ "colorette": "^2.0.20", "commander": "^15.0.0", "consola": "^3.4.2", - "fs-extra": "^11.3.5", + "fs-extra": "^11.3.6", "progress": "^2.0.3", "simple-git": "^3.36.0" } diff --git a/src/handlers/componentCreate.test.ts b/src/handlers/componentCreate.test.ts index a9a5b90..641adac 100644 --- a/src/handlers/componentCreate.test.ts +++ b/src/handlers/componentCreate.test.ts @@ -203,6 +203,10 @@ describe('componentCreate', () => { }); it('throws when the configured variant is not found', async () => { + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; getEmulsifyConfigMock.mockResolvedValueOnce({ ...projectConfig, variant: { @@ -210,6 +214,10 @@ describe('componentCreate', () => { platform: 'none', }, }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [wordpressVariant], + }); await expect(componentCreate('button', {})).rejects.toThrow( 'Unable to find configuration for the variant none within the system compound.', diff --git a/src/handlers/componentInstall.test.ts b/src/handlers/componentInstall.test.ts index dfc6ba4..ad515f4 100644 --- a/src/handlers/componentInstall.test.ts +++ b/src/handlers/componentInstall.test.ts @@ -191,6 +191,10 @@ describe('componentInstall', () => { }); it('throws when the configured variant is not found', async () => { + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; getEmulsifyConfigMock.mockResolvedValueOnce({ ...projectConfig, variant: { @@ -198,6 +202,10 @@ describe('componentInstall', () => { platform: 'none', }, }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [wordpressVariant], + }); await expect(componentInstall('button', {})).rejects.toThrow( 'Unable to find configuration for the variant none within the system compound.', diff --git a/src/handlers/componentList.test.ts b/src/handlers/componentList.test.ts index 5fb0f30..7437387 100644 --- a/src/handlers/componentList.test.ts +++ b/src/handlers/componentList.test.ts @@ -165,6 +165,10 @@ describe('componentList', () => { }); it('throws when the configured variant is not found', async () => { + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; getEmulsifyConfigMock.mockResolvedValueOnce({ ...projectConfig, variant: { @@ -172,6 +176,10 @@ describe('componentList', () => { platform: 'none', }, }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [wordpressVariant], + }); await expect(componentList()).rejects.toThrow( 'Unable to find configuration for the variant none within the system compound.', diff --git a/src/handlers/hofs/withEmulsifySystem.test.ts b/src/handlers/hofs/withEmulsifySystem.test.ts index 659ff4e..0fb2f62 100644 --- a/src/handlers/hofs/withEmulsifySystem.test.ts +++ b/src/handlers/hofs/withEmulsifySystem.test.ts @@ -167,6 +167,10 @@ describe('withEmulsifySystem', () => { }); it('throws a typed error when the configured variant is not found', async () => { + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; getEmulsifyConfigMock.mockResolvedValueOnce({ ...projectConfig, variant: { @@ -174,6 +178,10 @@ describe('withEmulsifySystem', () => { platform: 'none', }, }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [wordpressVariant], + }); await expect(withEmulsifySystem('install components')).rejects.toThrow( 'Unable to find configuration for the variant none within the system compound.', @@ -190,4 +198,49 @@ describe('withEmulsifySystem', () => { expect(cloneIntoCacheMock).toHaveBeenCalledWith('systems', ['compound']); expect(cloneSystemMock).toHaveBeenCalledWith(projectConfig.system); }); + + it('resolves a selected variant stored as a platform compatibility expression', async () => { + const expressionProjectConfig = { + ...projectConfig, + variant: { + ...projectConfig.variant, + platform: 'drupal || wordpress', + }, + }; + const expressionVariant = { + ...variant, + platform: 'drupal || wordpress', + }; + const expressionSystem = { + ...system, + variants: [expressionVariant], + }; + getEmulsifyConfigMock.mockResolvedValueOnce(expressionProjectConfig); + getJsonFromCachedFileMock.mockResolvedValueOnce(expressionSystem); + + await expect(withEmulsifySystem('install components')).resolves.toEqual({ + emulsifyConfig: expressionProjectConfig, + systemName: 'compound', + systemConf: expressionSystem, + variantConf: expressionVariant, + }); + }); + + it('falls back to project platform compatibility when the stored variant expression is stale', async () => { + const staleProjectConfig = { + ...projectConfig, + variant: { + ...projectConfig.variant, + platform: 'drupal || wordpress', + }, + }; + getEmulsifyConfigMock.mockResolvedValueOnce(staleProjectConfig); + + await expect(withEmulsifySystem('install components')).resolves.toEqual({ + emulsifyConfig: staleProjectConfig, + systemName: 'compound', + systemConf: system, + variantConf: variant, + }); + }); }); diff --git a/src/handlers/hofs/withEmulsifySystem.ts b/src/handlers/hofs/withEmulsifySystem.ts index 33b5e94..46d9bd2 100644 --- a/src/handlers/hofs/withEmulsifySystem.ts +++ b/src/handlers/hofs/withEmulsifySystem.ts @@ -16,6 +16,10 @@ import cloneIntoCache from '../../util/cache/cloneIntoCache.js'; import getJsonFromCachedFile from '../../util/cache/getJsonFromCachedFile.js'; import getGitRepoNameFromUrl from '../../util/getGitRepoNameFromUrl.js'; import getEmulsifyConfig from '../../util/project/getEmulsifyConfig.js'; +import { + selectCompatiblePlatformVariant, + selectExactPlatformVariant, +} from '../../util/platform/platformCompatibility.js'; type EmulsifyProjectConfigurationWithSystem = EmulsifyProjectConfiguration & { system: NonNullable; @@ -105,9 +109,20 @@ export async function withEmulsifySystem( } const variantName = variantReference.platform; - const variantConf = systemConf.variants?.find( - ({ platform }) => platform === variantName, + const exactVariantSelection = selectExactPlatformVariant( + systemConf.variants, + variantName, + ); + const compatibleVariantSelection = selectCompatiblePlatformVariant( + systemConf.variants, + emulsifyConfig.project.platform, ); + const variantConf = + exactVariantSelection.status === 'selected' + ? exactVariantSelection.variant + : compatibleVariantSelection.status === 'selected' + ? compatibleVariantSelection.variant + : undefined; if (!variantConf) { throw new EmulsifySystemError( `Unable to find configuration for the variant ${variantName} within the system ${systemName}.`, diff --git a/src/handlers/init.test.ts b/src/handlers/init.test.ts index 759b5bd..da4c49f 100644 --- a/src/handlers/init.test.ts +++ b/src/handlers/init.test.ts @@ -141,6 +141,37 @@ describe('init', () => { expect(logMock).toHaveBeenCalledWith('info', systemSelectionMessage); }); + it('clones the WordPress starter into the detected themes directory when WordPress is auto-detected', async () => { + expect.assertions(2); + getPlatformInfoMock.mockReturnValueOnce({ + root, + name: 'wordpress', + emulsifyParentDirectory: `${root}/wp-content/themes`, + }); + + await init(progress)('My Theme'); + + expect(gitCloneMock).toHaveBeenCalledWith( + 'https://github.com/emulsify-ds/emulsify-wordpress-starter', + '/home/uname/Projects/cornflake/wp-content/themes/my-theme', + { '--branch': 'main' }, + ); + expect(writeJsonFileMock).toHaveBeenCalledWith( + '/home/uname/Projects/cornflake/wp-content/themes/my-theme/project.emulsify.json', + { + project: { + platform: 'wordpress', + machineName: 'my-theme', + name: 'My Theme', + }, + starter: { + repository: + 'https://github.com/emulsify-ds/emulsify-wordpress-starter', + }, + }, + ); + }); + it('uses the progress obj to display information on the init process', async () => { expect.assertions(5); await init(progress)('cornflake'); @@ -212,6 +243,35 @@ describe('init', () => { ); }); + it('can clone the WordPress starter based on platform input', async () => { + expect.assertions(2); + getPlatformInfoMock.mockReturnValueOnce(undefined); + + await init(progress)('cornflake', `${root}`, { + platform: 'wordpress', + }); + + expect(gitCloneMock).toHaveBeenCalledWith( + 'https://github.com/emulsify-ds/emulsify-wordpress-starter', + '/home/uname/Projects/cornflake/cornflake', + { '--branch': 'main' }, + ); + expect(writeJsonFileMock).toHaveBeenCalledWith( + '/home/uname/Projects/cornflake/cornflake/project.emulsify.json', + { + project: { + platform: 'wordpress', + machineName: 'cornflake', + name: 'cornflake', + }, + starter: { + repository: + 'https://github.com/emulsify-ds/emulsify-wordpress-starter', + }, + }, + ); + }); + it('uses flag-provided values without prompting in non-interactive mode', async () => { expect.assertions(5); getPlatformInfoMock.mockReturnValueOnce(undefined); @@ -248,7 +308,7 @@ describe('init', () => { ); }); - it('prompts for the platform with drupal and none choices when missing in an interactive terminal', async () => { + it('prompts for the platform with drupal, wordpress, and none choices when missing in an interactive terminal', async () => { expect.assertions(5); setStdinIsTTY(true); getPlatformInfoMock.mockReturnValueOnce(undefined); @@ -262,7 +322,7 @@ describe('init', () => { expect(selectMock).toHaveBeenCalledTimes(1); expect(selectMock).toHaveBeenNthCalledWith(1, { message: 'Platform:', - choices: ['drupal', 'none'], + choices: ['drupal', 'wordpress', 'none'], default: 'drupal', }); expect(writeJsonFileMock).toHaveBeenCalledWith( @@ -359,13 +419,13 @@ describe('init', () => { ); }); - it('throws if no repository is found or specified', async () => { + it('throws if a detected platform is not supported', async () => { expect.assertions(1); getPlatformInfoMock.mockReturnValueOnce({ name: 'invalid', }); await expect(init(progress)('cornflake', root)).rejects.toThrow( - 'Unable to find an Emulsify starter for your project. Please specify one using the --starter flag: emulsify init myTheme --starter https://github.com/emulsify-ds/emulsify-starter', + 'Unsupported platform "invalid". Supported platforms are "none", "drupal", and "wordpress".', ); }); @@ -397,7 +457,7 @@ describe('init', () => { expect(select).toHaveBeenCalledTimes(1); expect(select).toHaveBeenNthCalledWith(1, { message: 'Platform:', - choices: ['drupal', 'none'], + choices: ['drupal', 'wordpress', 'none'], default: 'drupal', }); expect(gitCloneMock).toHaveBeenCalled(); diff --git a/src/handlers/init.ts b/src/handlers/init.ts index ecfffac..d99eacb 100644 --- a/src/handlers/init.ts +++ b/src/handlers/init.ts @@ -23,6 +23,7 @@ import executeScript from '../util/fs/executeScript.js'; import getInitSuccessMessageForPlatform from '../util/platform/getInitSuccessMessageForPlatform.js'; import log from '../lib/log.js'; import CliError from '../lib/CliError.js'; +import { isPlatform } from '../util/platform/platformCompatibility.js'; const git = simpleGit(); @@ -31,6 +32,7 @@ const DEFAULT_PROJECT_NAME = 'emulsifyTheme'; const DEFAULT_PLATFORM: Platform = 'drupal'; const PLATFORM_CHOICES = [ 'drupal', + 'wordpress', 'none', ] as const satisfies readonly Platform[]; @@ -94,9 +96,15 @@ export default function init(progress: InstanceType) { } // If no platform name is given, and none can be detected, exit and error. - let platformName = (options?.platform || autoPlatformName) as - | Platform - | undefined; + const requestedPlatformName = options?.platform || autoPlatformName; + let platformName = isPlatform(requestedPlatformName) + ? requestedPlatformName + : undefined; + if (requestedPlatformName && !platformName) { + throw new CliError( + `Unsupported platform "${requestedPlatformName}". Supported platforms are "none", "drupal", and "wordpress".`, + ); + } if (!platformName) { if (acceptDefaults) { platformName = DEFAULT_PLATFORM; diff --git a/src/handlers/systemInstall.test.ts b/src/handlers/systemInstall.test.ts index 8ad88a5..07f5fdd 100644 --- a/src/handlers/systemInstall.test.ts +++ b/src/handlers/systemInstall.test.ts @@ -18,7 +18,7 @@ jest.mock('../util/fs/writeToJsonFile', () => jest.fn()); jest.mock('@inquirer/prompts'); import fs from 'fs'; -import type { EmulsifySystem } from '@emulsify-cli/config'; +import type { EmulsifySystem, Platform } from '@emulsify-cli/config'; import { select } from '@inquirer/prompts'; import log from '../lib/log.js'; import { EMULSIFY_SYSTEM_CONFIG_FILE } from '../lib/constants.js'; @@ -118,7 +118,7 @@ const availableSystems = [ }, ]; -function customSystemDefinition(platform: 'drupal' | 'none'): EmulsifySystem { +function customSystemDefinition(platform: Platform): EmulsifySystem { return { name: 'custom-system', homepage: 'https://example.com/custom-system', @@ -346,6 +346,29 @@ describe('systemInstall', () => { ); }); + it('uses the current wordpress platform in the custom system definition', async () => { + setStdinIsTTY(true); + selectMock.mockResolvedValueOnce('create a new system'); + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'wordpress', + }, + }); + findFileInCurrentPathMock.mockReturnValueOnce( + '/project/project.emulsify.json', + ); + existsSyncMock.mockReturnValueOnce(false); + + await systemInstall(undefined, {}); + + expect(writeToJsonFileMock).toHaveBeenCalledWith( + '/project/system.emulsify.json', + customSystemDefinition('wordpress'), + ); + }); + it('does not overwrite an existing custom system definition', async () => { setStdinIsTTY(true); selectMock.mockResolvedValueOnce('create a new system'); @@ -440,11 +463,34 @@ describe('systemInstall', () => { consoleErrorMock.mockRestore(); }); + it('throws validation errors from invalid system variant platform expressions', async () => { + const consoleErrorMock = jest + .spyOn(console, 'error') + .mockImplementation(jest.fn()); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [ + { + ...variant, + platform: 'drupal && wordpress', + }, + ], + }); + + await expect(systemInstall('compound', {})).rejects.toThrow( + 'The system install failed due to the validation errors reported above. Please fix the the errors in the "compound" configuration and try again.', + ); + + expect(consoleErrorMock).toHaveBeenCalled(); + + consoleErrorMock.mockRestore(); + }); + it('throws when the requested variant is not found', async () => { await expect( systemInstall('compound', { variant: 'none' }), ).rejects.toThrow( - 'Unable to find a variant (none) within the system (compound). Please check your Emulsify project config and make sure the project.platform value is correct, or select a system with a variant that is compatible with the platform you are using.', + 'Unable to find a compatible variant matching "none" for project platform "drupal" within the system (compound). Available variant platform expressions: drupal.', ); }); @@ -455,7 +501,7 @@ describe('systemInstall', () => { }); await expect(systemInstall('compound', {})).rejects.toThrow( - 'Unable to find a variant (drupal) within the system (compound). Please check your Emulsify project config and make sure the project.platform value is correct, or select a system with a variant that is compatible with the platform you are using.', + 'Unable to find a compatible variant for project platform "drupal" within the system (compound). Available variant platform expressions: none.', ); }); @@ -513,6 +559,319 @@ describe('systemInstall', () => { ); }); + it('installs a wordpress system variant for a wordpress project', async () => { + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'wordpress', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [wordpressVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'wordpress', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }); + + it('prefers an exact wordpress variant over shared or generic variants for a wordpress project', async () => { + const genericVariant = { + ...variant, + platform: 'none', + }; + const sharedVariant = { + ...variant, + platform: 'drupal || wordpress', + }; + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'wordpress', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [genericVariant, sharedVariant, wordpressVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'wordpress', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }); + + it('installs a shared system variant for a drupal project', async () => { + const expressionVariant = { + ...variant, + platform: 'drupal || wordpress', + }; + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [expressionVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith({ + system: { + repository: 'https://github.com/emulsify-ds/compound.git', + checkout: 'v1.0.0', + }, + variant: { + platform: 'drupal || wordpress', + structureImplementations: variant.structureImplementations, + }, + }); + expect(logMock).toHaveBeenCalledWith( + 'success', + 'Successfully installed the compound system using the drupal || wordpress variant.', + ); + }); + + it('installs a shared system variant for a wordpress project', async () => { + const expressionVariant = { + ...variant, + platform: 'drupal || wordpress', + }; + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'wordpress', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [expressionVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'drupal || wordpress', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }); + + it('prefers a shared expression over a generic none variant for a concrete project', async () => { + const genericVariant = { + ...variant, + platform: 'none', + }; + const expressionVariant = { + ...variant, + platform: 'drupal || wordpress', + }; + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [genericVariant, expressionVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'drupal || wordpress', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }); + + it.each(['drupal', 'wordpress'] as const)( + 'allows a %s project platform to install a generic none variant', + async (platform) => { + const genericVariant = { + ...variant, + platform: 'none', + }; + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform, + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [genericVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'none', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }, + ); + + it.each(['drupal', 'wordpress'] as const)( + 'allows a none project platform to install a %s-only system variant', + async (platform) => { + const platformVariant = { + ...variant, + platform, + }; + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'none', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [platformVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform, + structureImplementations: variant.structureImplementations, + }, + }), + ); + }, + ); + + it('throws a clear error when a none project platform matches multiple variants in a non-interactive terminal', async () => { + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'none', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [ + { + ...variant, + platform: 'drupal', + }, + { + ...variant, + platform: 'wordpress', + }, + ], + }); + + await expect(systemInstall('compound', {})).rejects.toThrow( + 'Multiple compatible variants were found for project platform "none" within the system (compound): drupal, wordpress. Run this command in an interactive terminal or specify a variant.', + ); + }); + + it('prompts when a none project platform matches multiple variants in an interactive terminal', async () => { + setStdinIsTTY(true); + selectMock.mockResolvedValueOnce('wordpress'); + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'none', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [ + { + ...variant, + platform: 'drupal', + }, + { + ...variant, + platform: 'wordpress', + }, + ], + }); + + await systemInstall('compound', {}); + + expect(selectMock).toHaveBeenCalledWith({ + message: 'Choose a compound variant for project platform "none":', + choices: ['drupal', 'wordpress'], + }); + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'wordpress', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }); + + it('allows a none project platform to prefer an exact none variant', async () => { + const wordpressVariant = { + ...variant, + platform: 'wordpress', + }; + const genericVariant = { + ...variant, + platform: 'none', + }; + getEmulsifyConfigMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'none', + }, + }); + getJsonFromCachedFileMock.mockResolvedValueOnce({ + ...system, + variants: [wordpressVariant, genericVariant], + }); + + await systemInstall('compound', {}); + + expect(setEmulsifyConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + variant: { + platform: 'none', + structureImplementations: variant.structureImplementations, + }, + }), + ); + }); + it('installs no required components when none are marked required', async () => { getJsonFromCachedFileMock.mockResolvedValueOnce({ ...system, diff --git a/src/handlers/systemInstall.ts b/src/handlers/systemInstall.ts index cc18194..ac5b7a7 100644 --- a/src/handlers/systemInstall.ts +++ b/src/handlers/systemInstall.ts @@ -32,6 +32,12 @@ import getEmulsifyConfig from '../util/project/getEmulsifyConfig.js'; import findFileInCurrentPath from '../util/fs/findFileInCurrentPath.js'; import executeScript from '../util/fs/executeScript.js'; import writeToJsonFile from '../util/fs/writeToJsonFile.js'; +import { + getVariantPlatformExpressions, + isPlatform, + selectCompatiblePlatformVariant, + selectExactPlatformVariant, +} from '../util/platform/platformCompatibility.js'; const CREATE_NEW_SYSTEM_CHOICE = 'create a new system'; const CANCEL_SYSTEM_INSTALL_CHOICE = 'cancel'; @@ -115,8 +121,95 @@ async function promptForSystemInstallChoice(): Promise { return selectedSystem; } +function getVariantSelectionErrorMessage( + systemConf: EmulsifySystem, + projectPlatform: Platform, + requestedVariant: string | void, +): string { + const availableVariants = + getVariantPlatformExpressions(systemConf.variants).join(', ') || 'none'; + const requestedVariantMessage = requestedVariant + ? ` matching "${requestedVariant}"` + : ''; + + return `Unable to find a compatible variant${requestedVariantMessage} for project platform "${projectPlatform}" within the system (${systemConf.name}). Available variant platform expressions: ${availableVariants}.`; +} + +async function promptForVariantChoice( + variants: T[], + projectPlatform: Platform, + systemName: string, +): Promise { + const selectedPlatform = await select({ + message: `Choose a ${systemName} variant for project platform "${projectPlatform}":`, + choices: variants.map(({ platform }) => platform), + }); + return variants.find(({ platform }) => platform === selectedPlatform) as T; +} + +async function resolveSystemVariant( + systemConf: EmulsifySystem, + projectPlatform: Platform, + requestedVariant: string | void, +) { + if (requestedVariant) { + const selection = selectExactPlatformVariant( + systemConf.variants, + requestedVariant, + ); + if (selection.status === 'selected') { + return selection.variant; + } + + if (selection.status === 'ambiguous' && process.stdin.isTTY === true) { + return await promptForVariantChoice( + selection.variants, + projectPlatform, + systemConf.name, + ); + } + + throw new CliError( + getVariantSelectionErrorMessage( + systemConf, + projectPlatform, + requestedVariant, + ), + ); + } + + const selection = selectCompatiblePlatformVariant( + systemConf.variants, + projectPlatform, + ); + if (selection.status === 'selected') { + return selection.variant; + } + + if (selection.status === 'ambiguous') { + if (process.stdin.isTTY === true) { + return await promptForVariantChoice( + selection.variants, + projectPlatform, + systemConf.name, + ); + } + + const compatibleVariants = selection.variants + .map(({ platform }) => platform) + .join(', '); + throw new CliError( + `Multiple compatible variants were found for project platform "${projectPlatform}" within the system (${systemConf.name}): ${compatibleVariants}. Run this command in an interactive terminal or specify a variant.`, + ); + } + + throw new CliError( + getVariantSelectionErrorMessage(systemConf, projectPlatform, undefined), + ); +} + function getCustomSystemPlatform(platform: string): Platform { - return platform === 'drupal' || platform === 'none' ? platform : 'none'; + return isPlatform(platform) ? platform : 'none'; } function buildCustomSystemDefinition(platform: Platform): EmulsifySystem { @@ -275,24 +368,19 @@ export default async function systemInstall( ); } - // Extract the variant name, and error if no variant is determinable. - const variantName: string | void = - options.variant || projectConfig.project.platform; - if (!variantName) { + const projectPlatform = projectConfig.project.platform; + if (!isPlatform(projectPlatform)) { throw new CliError( 'Unable to determine a variant for the specified system. Please either pass in a valid variant using the --variant flag.', ); } // @TODO: clone variants into their own cache bucket if a reference is provided. - const variantConf = systemConf.variants?.find( - ({ platform }) => platform === variantName, + const variantConf = await resolveSystemVariant( + systemConf, + projectPlatform, + options.variant, ); - if (!variantConf) { - throw new CliError( - `Unable to find a variant (${variantName}) within the system (${systemConf.name}). Please check your Emulsify project config and make sure the project.platform value is correct, or select a system with a variant that is compatible with the platform you are using.`, - ); - } // Update emulsify project config. try { diff --git a/src/index.ts b/src/index.ts index ed029e5..c1f3837 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,7 +44,9 @@ function getRootHelp(): string { ' -m, --machineName Set the project folder/config machine name.', ' -s, --starter Use a custom starter repository.', ' -c, --checkout Checkout for the starter repository.', - ' -p, --platform Select the project platform when auto-detection is unavailable.', + ' -p, --platform ', + ' Select the project platform when auto-detection is unavailable.', + ' Built-in platforms: drupal, wordpress, none.', ' -y, --yes Accept defaults for missing init values without prompting.', '', ' system list', @@ -110,7 +112,7 @@ program 'Starter commit, branch, or tag to check out after clone.', ) .option( - '-p --platform ', + '-p --platform ', 'Project platform to use when auto-detection is unavailable or should be overridden.', ) .option( diff --git a/src/schemas/emulsifyProjectConfig.json b/src/schemas/emulsifyProjectConfig.json index da77ccd..e75d762 100644 --- a/src/schemas/emulsifyProjectConfig.json +++ b/src/schemas/emulsifyProjectConfig.json @@ -9,7 +9,7 @@ "description": "Contains information about the Emulsify project.", "properties": { "platform": { - "type": "string", + "$ref": "variant.json#/definitions/platform", "description": "Name of the platform Emulsify is being used within." }, "name": { @@ -57,7 +57,7 @@ "description": "Contains information about the Emulsify system this project is utilizing", "properties": { "platform": { - "$ref": "variant.json#/definitions/platform" + "$ref": "variant.json#/definitions/platformExpression" }, "structureImplementations": { "$ref": "variant.json#/definitions/structureImplementations" diff --git a/src/schemas/system.json b/src/schemas/system.json index 2622bf6..3196271 100644 --- a/src/schemas/system.json +++ b/src/schemas/system.json @@ -47,7 +47,7 @@ "type": "object", "properties": { "platform": { - "$ref": "variant.json#/definitions/platform" + "$ref": "variant.json#/definitions/platformExpression" }, "structureImplementations": { "$ref": "variant.json#/definitions/structureImplementations" diff --git a/src/schemas/variant.json b/src/schemas/variant.json index 52899b2..0b5dfb6 100644 --- a/src/schemas/variant.json +++ b/src/schemas/variant.json @@ -6,8 +6,33 @@ "definitions": { "platform": { "type": "string", - "description": "Name of the variant, usually indicating the platform for which the variant is intended, such as WordPress, or Drupal9", - "enum": ["none", "drupal"] + "description": "Concrete platform Emulsify is being used within.", + "enum": ["none", "drupal", "wordpress"] + }, + "platformExpression": { + "description": "Platform compatibility expression for a variant. Accepts a concrete platform or multiple concrete platforms joined by ' || '.", + "anyOf": [ + { + "$ref": "#/definitions/platform" + }, + { + "type": "string", + "enum": [ + "none || drupal", + "none || wordpress", + "drupal || none", + "drupal || wordpress", + "wordpress || none", + "wordpress || drupal", + "none || drupal || wordpress", + "none || wordpress || drupal", + "drupal || none || wordpress", + "drupal || wordpress || none", + "wordpress || none || drupal", + "wordpress || drupal || none" + ] + } + ] }, "structureImplementations": { "type": "array", @@ -119,7 +144,7 @@ }, "properties": { "platform": { - "$ref": "#/definitions/platform" + "$ref": "#/definitions/platformExpression" }, "structureImplementations": { "$ref": "#/definitions/structureImplementations" diff --git a/src/types/_emulsifyProjectConfig.d.ts b/src/types/_emulsifyProjectConfig.d.ts index 26d3a5d..cdc8d21 100644 --- a/src/types/_emulsifyProjectConfig.d.ts +++ b/src/types/_emulsifyProjectConfig.d.ts @@ -27,7 +27,7 @@ export interface EmulsifyProjectConfiguration { /** * Name of the platform Emulsify is being used within. */ - platform: string; + platform: 'none' | 'drupal' | 'wordpress'; /** * Name of the project, such as 'Carmen Sandiego'. */ @@ -64,9 +64,24 @@ export interface EmulsifyProjectConfiguration { */ variant?: { /** - * Name of the variant, usually indicating the platform for which the variant is intended, such as WordPress, or Drupal9 + * Platform compatibility expression for a variant. Accepts a concrete platform or multiple concrete platforms joined by ' || '. */ - platform: 'none' | 'drupal'; + platform: + | ('none' | 'drupal' | 'wordpress') + | ( + | 'none || drupal' + | 'none || wordpress' + | 'drupal || none' + | 'drupal || wordpress' + | 'wordpress || none' + | 'wordpress || drupal' + | 'none || drupal || wordpress' + | 'none || wordpress || drupal' + | 'drupal || none || wordpress' + | 'drupal || wordpress || none' + | 'wordpress || none || drupal' + | 'wordpress || drupal || none' + ); structureImplementations: StructureImplementations; /** * Git repository containing the system this project is utilizing diff --git a/src/types/_system.d.ts b/src/types/_system.d.ts index ffd34f2..a40862c 100644 --- a/src/types/_system.d.ts +++ b/src/types/_system.d.ts @@ -117,9 +117,24 @@ export interface EmulsifySystem { */ variants?: { /** - * Name of the variant, usually indicating the platform for which the variant is intended, such as WordPress, or Drupal9 + * Platform compatibility expression for a variant. Accepts a concrete platform or multiple concrete platforms joined by ' || '. */ - platform: 'none' | 'drupal'; + platform: + | ('none' | 'drupal' | 'wordpress') + | ( + | 'none || drupal' + | 'none || wordpress' + | 'drupal || none' + | 'drupal || wordpress' + | 'wordpress || none' + | 'wordpress || drupal' + | 'none || drupal || wordpress' + | 'none || wordpress || drupal' + | 'drupal || none || wordpress' + | 'drupal || wordpress || none' + | 'wordpress || none || drupal' + | 'wordpress || drupal || none' + ); structureImplementations: StructureImplementations; components: Components; directories?: Directories; diff --git a/src/types/_variant.d.ts b/src/types/_variant.d.ts index 8706206..28947f0 100644 --- a/src/types/_variant.d.ts +++ b/src/types/_variant.d.ts @@ -6,9 +6,28 @@ */ /** - * Name of the variant, usually indicating the platform for which the variant is intended, such as WordPress, or Drupal9 + * Platform compatibility expression for a variant. Accepts a concrete platform or multiple concrete platforms joined by ' || '. */ -export type Platform = 'none' | 'drupal'; +export type PlatformExpression = + | Platform + | ( + | 'none || drupal' + | 'none || wordpress' + | 'drupal || none' + | 'drupal || wordpress' + | 'wordpress || none' + | 'wordpress || drupal' + | 'none || drupal || wordpress' + | 'none || wordpress || drupal' + | 'drupal || none || wordpress' + | 'drupal || wordpress || none' + | 'wordpress || none || drupal' + | 'wordpress || drupal || none' + ); +/** + * Concrete platform Emulsify is being used within. + */ +export type Platform = 'none' | 'drupal' | 'wordpress'; /** * Array containing an object for each structure specified in the system to which this variant belongs */ @@ -91,7 +110,7 @@ export type Files = { }[]; export interface EmulsifyVariant { - platform: Platform; + platform: PlatformExpression; structureImplementations: StructureImplementations; components: Components; directories?: Directories; diff --git a/src/types/handlers.d.ts b/src/types/handlers.d.ts index b905eb1..98c5589 100644 --- a/src/types/handlers.d.ts +++ b/src/types/handlers.d.ts @@ -2,13 +2,15 @@ * Module that exports types pertaining to command handlers. */ declare module '@emulsify-cli/handlers' { + import type { Platform, PlatformExpression } from '@emulsify-cli/config'; + export type InitHandlerOptions = { /** Git repository of the Emulsify starter to clone. */ starter?: string | void; /** Commit, branch, or tag to checkout after cloning the starter repository. */ checkout?: string | void; /** Platform name to use when platform auto-detection is unavailable or should be overridden. */ - platform?: string | void; + platform?: Platform | void; /** Machine-friendly project folder/config name. */ machineName?: string | void; /** Accept default values for missing init options without prompting. */ @@ -18,7 +20,7 @@ declare module '@emulsify-cli/handlers' { export type InstallSystemHandlerOptions = { repository?: string | void; checkout?: string | void; - variant?: string | void; + variant?: PlatformExpression | void; all?: boolean; }; diff --git a/src/util/getAvailableStarters.test.ts b/src/util/getAvailableStarters.test.ts index 3bdeefe..99ada6e 100644 --- a/src/util/getAvailableStarters.test.ts +++ b/src/util/getAvailableStarters.test.ts @@ -17,6 +17,12 @@ describe('getAvailableStarters', () => { repository: 'https://github.com/emulsify-ds/emulsify-drupal-starter', checkout: 'main', }, + { + platform: 'wordpress', + platformMajorVersion: 6, + repository: 'https://github.com/emulsify-ds/emulsify-wordpress-starter', + checkout: 'main', + }, ]; expect(getAvailableStarters()).toEqual(expected); diff --git a/src/util/getAvailableStarters.ts b/src/util/getAvailableStarters.ts index 8cbba3c..5a69d83 100644 --- a/src/util/getAvailableStarters.ts +++ b/src/util/getAvailableStarters.ts @@ -19,5 +19,11 @@ export default function getAvailableStarters(): EmulsifyStarter[] { repository: 'https://github.com/emulsify-ds/emulsify-drupal-starter', checkout: 'main', }, + { + platform: 'wordpress', + platformMajorVersion: 6, + repository: 'https://github.com/emulsify-ds/emulsify-wordpress-starter', + checkout: 'main', + }, ]; } diff --git a/src/util/platform/getPlatformInfo.test.ts b/src/util/platform/getPlatformInfo.test.ts index a6aa57f..7c09ace 100644 --- a/src/util/platform/getPlatformInfo.test.ts +++ b/src/util/platform/getPlatformInfo.test.ts @@ -1,11 +1,17 @@ jest.mock('./getDrupalInfo', () => jest.fn()); +jest.mock('./getWordPressInfo', () => jest.fn()); jest.mock('./getNoPlatformInfo', () => jest.fn()); import getDrupalInfo from './getDrupalInfo.js'; +import getWordPressInfo from './getWordPressInfo.js'; import getNoPlatformInfo from './getNoPlatformInfo.js'; import getPlatformInfo from './getPlatformInfo.js'; describe('getPlatformInfo', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + it('can return information about Drupal, if the user is currently within a Drupal instance', async () => { expect.assertions(1); (getDrupalInfo as jest.Mock).mockResolvedValueOnce({ @@ -23,26 +29,52 @@ describe('getPlatformInfo', () => { await expect(getPlatformInfo()).resolves.toEqual(expected); }); - it('returns no platform info if the user (cwd) is not within any detectable platform', async () => { - expect.assertions(1); + it('prefers Drupal when both Drupal and WordPress signals appear', async () => { + expect.assertions(2); (getDrupalInfo as jest.Mock).mockResolvedValueOnce({ - name: 'none', + name: 'drupal', + root: '/home/uname/Projects/cornflake/web', + platformMajorVersion: 11, + }); + (getWordPressInfo as jest.Mock).mockResolvedValueOnce({ + name: 'wordpress', + root: '/home/uname/Projects/cornflake', + emulsifyParentDirectory: + '/home/uname/Projects/cornflake/wp-content/themes', + }); + + await expect(getPlatformInfo()).resolves.toEqual({ + name: 'drupal', + root: '/home/uname/Projects/cornflake/web', + platformMajorVersion: 11, + }); + expect(getWordPressInfo).not.toHaveBeenCalled(); + }); + + it('can return information about WordPress, if the user is currently within a WordPress instance', async () => { + expect.assertions(1); + (getDrupalInfo as jest.Mock).mockResolvedValueOnce(undefined); + (getWordPressInfo as jest.Mock).mockResolvedValueOnce({ + name: 'wordpress', root: '/home/uname/Projects/cornflake', - platformMajorVersion: 1, + emulsifyParentDirectory: + '/home/uname/Projects/cornflake/wp-content/themes', }); const expected = { - name: 'none', - platformMajorVersion: 1, + name: 'wordpress', root: '/home/uname/Projects/cornflake', + emulsifyParentDirectory: + '/home/uname/Projects/cornflake/wp-content/themes', }; await expect(getPlatformInfo()).resolves.toEqual(expected); }); - it('returns no platform info if getDrupalInfo returns null', async () => { + it('returns no platform info if the user (cwd) is not within Drupal or WordPress but an Emulsify project is detected', async () => { expect.assertions(1); (getDrupalInfo as jest.Mock).mockResolvedValueOnce(null); + (getWordPressInfo as jest.Mock).mockResolvedValueOnce(null); (getNoPlatformInfo as jest.Mock).mockResolvedValueOnce({ name: 'none' }); const result = await getPlatformInfo(); diff --git a/src/util/platform/getPlatformInfo.ts b/src/util/platform/getPlatformInfo.ts index 26fc697..332e1da 100644 --- a/src/util/platform/getPlatformInfo.ts +++ b/src/util/platform/getPlatformInfo.ts @@ -2,15 +2,22 @@ import type { PlatformInstanceInfo } from '@emulsify-cli/internal'; import getDrupalInfo from './getDrupalInfo.js'; import getNoPlatformInfo from './getNoPlatformInfo.js'; +import getWordPressInfo from './getWordPressInfo.js'; /** * Returns information about the platform the user is currently within (cwd), if it * exists and is detectable. */ export default async function getPlatformInfo(): Promise { - // @TODO: add support for more platforms, such as wordpress. const drupal = await getDrupalInfo(); - const noPlatform = await getNoPlatformInfo(); - const platform = drupal ? drupal : noPlatform; - return platform; + if (drupal) { + return drupal; + } + + const wordpress = await getWordPressInfo(); + if (wordpress) { + return wordpress; + } + + return await getNoPlatformInfo(); } diff --git a/src/util/platform/getWordPressInfo.test.ts b/src/util/platform/getWordPressInfo.test.ts new file mode 100644 index 0000000..ba1afef --- /dev/null +++ b/src/util/platform/getWordPressInfo.test.ts @@ -0,0 +1,72 @@ +import process from 'process'; +import fs from 'fs'; +import { clearFoundFileCache } from '../fs/findFileInCurrentPath.js'; +import getWordPressInfo from './getWordPressInfo.js'; + +const cwd = jest.spyOn(process, 'cwd'); +const existsSync = jest.spyOn(fs, 'existsSync'); + +describe('getWordPressInfo', () => { + beforeEach(() => { + clearFoundFileCache(); + cwd.mockReset(); + existsSync.mockReset(); + }); + + it('detects a standard WordPress themes directory', async () => { + cwd.mockReturnValue( + '/home/uname/Projects/cornflake/wp-content/themes/my-theme', + ); + existsSync.mockImplementation( + (path) => + String(path) === '/home/uname/Projects/cornflake/wp-content/themes', + ); + + await expect(getWordPressInfo()).resolves.toEqual({ + name: 'wordpress', + root: '/home/uname/Projects/cornflake', + emulsifyParentDirectory: + '/home/uname/Projects/cornflake/wp-content/themes', + }); + }); + + it('detects a Bedrock themes directory', async () => { + cwd.mockReturnValue( + '/home/uname/Projects/cornflake/web/app/themes/my-theme', + ); + existsSync.mockImplementation( + (path) => + String(path) === '/home/uname/Projects/cornflake/web/app/themes', + ); + + await expect(getWordPressInfo()).resolves.toEqual({ + name: 'wordpress', + root: '/home/uname/Projects/cornflake', + emulsifyParentDirectory: '/home/uname/Projects/cornflake/web/app/themes', + }); + }); + + it('detects a Composer web-root WordPress themes directory', async () => { + cwd.mockReturnValue( + '/home/uname/Projects/cornflake/web/wp-content/themes/my-theme', + ); + existsSync.mockImplementation( + (path) => + String(path) === '/home/uname/Projects/cornflake/web/wp-content/themes', + ); + + await expect(getWordPressInfo()).resolves.toEqual({ + name: 'wordpress', + root: '/home/uname/Projects/cornflake', + emulsifyParentDirectory: + '/home/uname/Projects/cornflake/web/wp-content/themes', + }); + }); + + it('returns void if no WordPress themes directory is found', async () => { + cwd.mockReturnValue('/home/uname/Projects/cornflake'); + existsSync.mockReturnValue(false); + + await expect(getWordPressInfo()).resolves.toBe(undefined); + }); +}); diff --git a/src/util/platform/getWordPressInfo.ts b/src/util/platform/getWordPressInfo.ts new file mode 100644 index 0000000..4dc39f4 --- /dev/null +++ b/src/util/platform/getWordPressInfo.ts @@ -0,0 +1,61 @@ +import type { PlatformInstanceInfo } from '@emulsify-cli/internal'; +import { dirname } from 'path'; +import findFileInCurrentPath from '../fs/findFileInCurrentPath.js'; + +const WORDPRESS_THEMES_DIRECTORIES = [ + 'wp-content/themes', + 'web/app/themes', + 'web/wp-content/themes', +] as const; + +function getRootFromThemesDirectory( + themesDirectory: string, + relativeThemesDirectory: string, +): string { + return relativeThemesDirectory + .split('/') + .reduce((root) => dirname(root), themesDirectory); +} + +/** + * Looks for a WordPress project within the cwd, and returns information about + * the platform, site root, and themes directory. + */ +export default async function getWordPressInfo(): Promise { + try { + const detectedThemesDirectories = WORDPRESS_THEMES_DIRECTORIES.map( + (relativeThemesDirectory) => { + const themesDirectory = findFileInCurrentPath(relativeThemesDirectory); + if (!themesDirectory) { + return undefined; + } + + return { + relativeThemesDirectory, + themesDirectory, + root: getRootFromThemesDirectory( + themesDirectory, + relativeThemesDirectory, + ), + }; + }, + ).filter((info) => info !== undefined); + + const detected = detectedThemesDirectories.sort( + (a, b) => + b.themesDirectory.length - a.themesDirectory.length || + b.relativeThemesDirectory.length - a.relativeThemesDirectory.length, + )[0]; + if (!detected) { + return undefined; + } + + return { + name: 'wordpress', + root: detected.root, + emulsifyParentDirectory: detected.themesDirectory, + }; + } catch { + return undefined; + } +} diff --git a/src/util/platform/platformCompatibility.test.ts b/src/util/platform/platformCompatibility.test.ts new file mode 100644 index 0000000..11bf3ac --- /dev/null +++ b/src/util/platform/platformCompatibility.test.ts @@ -0,0 +1,213 @@ +import { + getVariantPlatformExpressions, + isPlatform, + normalizePlatformExpression, + parsePlatformExpression, + platformExpressionMatchesProject, + selectCompatiblePlatformVariant, + selectExactPlatformVariant, +} from './platformCompatibility.js'; + +const drupalVariant = { platform: 'drupal', name: 'drupal' }; +const wordpressVariant = { platform: 'wordpress', name: 'wordpress' }; +const sharedVariant = { platform: 'drupal || wordpress', name: 'shared' }; +const genericVariant = { platform: 'none', name: 'generic' }; + +describe('platformCompatibility', () => { + describe('isPlatform', () => { + it('validates known platform values', () => { + expect(isPlatform('drupal')).toBe(true); + expect(isPlatform('wordpress')).toBe(true); + expect(isPlatform('none')).toBe(true); + expect(isPlatform('java')).toBe(false); + expect(isPlatform(undefined)).toBe(false); + }); + }); + + describe('parsePlatformExpression', () => { + it('parses and trims compatibility expressions', () => { + expect(parsePlatformExpression(' drupal || wordpress ')).toEqual([ + 'drupal', + 'wordpress', + ]); + }); + + it('deduplicates platform expressions', () => { + expect(parsePlatformExpression('drupal || drupal')).toEqual(['drupal']); + }); + + it('rejects empty expressions', () => { + expect(() => parsePlatformExpression(' || ')).toThrow( + 'Invalid platform compatibility expression: || ', + ); + }); + + it('rejects unknown platform values', () => { + expect(() => parsePlatformExpression('drupal || java')).toThrow( + 'Invalid platform compatibility expression: drupal || java', + ); + }); + }); + + it('normalizes platform expression whitespace', () => { + expect(normalizePlatformExpression('drupal|| wordpress')).toBe( + 'drupal || wordpress', + ); + }); + + describe('platformExpressionMatchesProject', () => { + it('matches concrete and generic expressions for concrete projects', () => { + expect(platformExpressionMatchesProject('wordpress', 'wordpress')).toBe( + true, + ); + expect( + platformExpressionMatchesProject('drupal || wordpress', 'wordpress'), + ).toBe(true); + expect(platformExpressionMatchesProject('none', 'wordpress')).toBe(true); + expect(platformExpressionMatchesProject('drupal', 'wordpress')).toBe( + false, + ); + }); + + it('allows none projects to match any expression', () => { + expect(platformExpressionMatchesProject('drupal', 'none')).toBe(true); + expect(platformExpressionMatchesProject('wordpress', 'none')).toBe(true); + }); + }); + + describe('getVariantPlatformExpressions', () => { + it('returns variant platform strings', () => { + expect( + getVariantPlatformExpressions([drupalVariant, sharedVariant]), + ).toEqual(['drupal', 'drupal || wordpress']); + }); + + it('returns an empty list when variants are missing', () => { + expect(getVariantPlatformExpressions(undefined)).toEqual([]); + }); + }); + + describe('selectCompatiblePlatformVariant', () => { + it('prefers exact matches over shared and generic variants', () => { + expect( + selectCompatiblePlatformVariant( + [genericVariant, sharedVariant, wordpressVariant], + 'wordpress', + ), + ).toEqual({ + status: 'selected', + variant: wordpressVariant, + }); + }); + + it('prefers shared expressions over generic variants', () => { + expect( + selectCompatiblePlatformVariant( + [genericVariant, sharedVariant], + 'wordpress', + ), + ).toEqual({ + status: 'selected', + variant: sharedVariant, + }); + }); + + it('selects generic variants for concrete projects', () => { + expect( + selectCompatiblePlatformVariant([genericVariant], 'wordpress'), + ).toEqual({ + status: 'selected', + variant: genericVariant, + }); + }); + + it('selects any single variant for none projects', () => { + expect(selectCompatiblePlatformVariant([drupalVariant], 'none')).toEqual({ + status: 'selected', + variant: drupalVariant, + }); + }); + + it('reports ambiguity when multiple variants share the best rank', () => { + expect( + selectCompatiblePlatformVariant( + [ + { platform: 'drupal || wordpress', name: 'shared-a' }, + { platform: 'wordpress || drupal', name: 'shared-b' }, + ], + 'wordpress', + ), + ).toEqual({ + status: 'ambiguous', + variants: [ + { platform: 'drupal || wordpress', name: 'shared-a' }, + { platform: 'wordpress || drupal', name: 'shared-b' }, + ], + }); + }); + + it('reports ambiguity when none projects match multiple variants', () => { + expect( + selectCompatiblePlatformVariant( + [drupalVariant, wordpressVariant], + 'none', + ), + ).toEqual({ + status: 'ambiguous', + variants: [drupalVariant, wordpressVariant], + }); + }); + + it('returns none when no variant is compatible', () => { + expect( + selectCompatiblePlatformVariant([drupalVariant], 'wordpress'), + ).toEqual({ + status: 'none', + }); + expect(selectCompatiblePlatformVariant(undefined, 'wordpress')).toEqual({ + status: 'none', + }); + }); + }); + + describe('selectExactPlatformVariant', () => { + it('selects exact normalized platform expressions', () => { + expect( + selectExactPlatformVariant( + [{ platform: 'drupal || wordpress', name: 'shared' }], + 'drupal||wordpress', + ), + ).toEqual({ + status: 'selected', + variant: { platform: 'drupal || wordpress', name: 'shared' }, + }); + }); + + it('reports ambiguity for duplicated exact expressions', () => { + expect( + selectExactPlatformVariant( + [ + { platform: 'wordpress', name: 'one' }, + { platform: 'wordpress', name: 'two' }, + ], + 'wordpress', + ), + ).toEqual({ + status: 'ambiguous', + variants: [ + { platform: 'wordpress', name: 'one' }, + { platform: 'wordpress', name: 'two' }, + ], + }); + }); + + it('returns none when no exact variant exists', () => { + expect(selectExactPlatformVariant([drupalVariant], 'wordpress')).toEqual({ + status: 'none', + }); + expect(selectExactPlatformVariant(undefined, 'wordpress')).toEqual({ + status: 'none', + }); + }); + }); +}); diff --git a/src/util/platform/platformCompatibility.ts b/src/util/platform/platformCompatibility.ts new file mode 100644 index 0000000..b853142 --- /dev/null +++ b/src/util/platform/platformCompatibility.ts @@ -0,0 +1,154 @@ +import type { Platform } from '@emulsify-cli/config'; + +const PLATFORMS = [ + 'none', + 'drupal', + 'wordpress', +] as const satisfies readonly Platform[]; + +type VariantWithPlatform = { + platform: string; +}; + +type VariantSelectionResult = + | { + status: 'selected'; + variant: T; + } + | { + status: 'ambiguous'; + variants: T[]; + } + | { + status: 'none'; + }; + +export function isPlatform(value: string | void): value is Platform { + return PLATFORMS.includes(value as Platform); +} + +export function parsePlatformExpression(expression: string): Platform[] { + const platforms = expression + .split('||') + .map((platform) => platform.trim()) + .filter((platform) => platform.length > 0); + + if (platforms.length === 0 || !platforms.every(isPlatform)) { + throw new Error(`Invalid platform compatibility expression: ${expression}`); + } + + return [...new Set(platforms)] as Platform[]; +} + +export function normalizePlatformExpression(expression: string): string { + return parsePlatformExpression(expression).join(' || '); +} + +export function platformExpressionMatchesProject( + expression: string, + projectPlatform: Platform, +): boolean { + const platforms = parsePlatformExpression(expression); + + if (projectPlatform === 'none') { + return true; + } + + return platforms.includes(projectPlatform) || platforms.includes('none'); +} + +function getVariantMatchRank( + variantPlatform: string, + projectPlatform: Platform, +): number | undefined { + const platforms = parsePlatformExpression(variantPlatform); + const normalizedVariantPlatform = platforms.join(' || '); + + if (normalizedVariantPlatform === projectPlatform) { + return 0; + } + + if (projectPlatform === 'none') { + return 3; + } + + if (platforms.includes(projectPlatform)) { + return 1; + } + + if (platforms.includes('none')) { + return 2; + } + + return undefined; +} + +export function getVariantPlatformExpressions( + variants: readonly VariantWithPlatform[] | undefined, +): string[] { + return variants?.map(({ platform }) => platform) || []; +} + +export function selectCompatiblePlatformVariant( + variants: readonly T[] | undefined, + projectPlatform: Platform, +): VariantSelectionResult { + const rankedVariants = (variants || []) + .map((variant) => ({ + variant, + rank: getVariantMatchRank(variant.platform, projectPlatform), + })) + .filter( + (match): match is { variant: T; rank: number } => + match.rank !== undefined, + ); + + if (rankedVariants.length === 0) { + return { status: 'none' }; + } + + const bestRank = Math.min(...rankedVariants.map(({ rank }) => rank)); + const bestMatches = rankedVariants + .filter(({ rank }) => rank === bestRank) + .map(({ variant }) => variant); + + if (bestMatches.length === 1) { + return { + status: 'selected', + variant: bestMatches[0], + }; + } + + return { + status: 'ambiguous', + variants: bestMatches, + }; +} + +export function selectExactPlatformVariant( + variants: readonly T[] | undefined, + platformExpression: string, +): VariantSelectionResult { + const normalizedPlatformExpression = + normalizePlatformExpression(platformExpression); + const matches = (variants || []).filter( + ({ platform }) => + normalizePlatformExpression(platform) === normalizedPlatformExpression, + ); + + if (matches.length === 0) { + return { status: 'none' }; + } + + if (matches.length === 1) { + return { + status: 'selected', + variant: matches[0], + }; + } + + return { + status: 'ambiguous', + variants: matches, + }; +} diff --git a/src/util/project/getEmulsifyConfig.test.ts b/src/util/project/getEmulsifyConfig.test.ts index 1960611..0ec1c4c 100644 --- a/src/util/project/getEmulsifyConfig.test.ts +++ b/src/util/project/getEmulsifyConfig.test.ts @@ -31,6 +31,19 @@ describe('getEmulsifyConfig', () => { await expect(getEmulsifyConfig()).resolves.toEqual(projectConfig); }); + it('accepts wordpress as a concrete project platform', async () => { + const wordpressProjectConfig = { + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'wordpress', + }, + }; + loadJsonFileMock.mockResolvedValueOnce(wordpressProjectConfig); + + await expect(getEmulsifyConfig()).resolves.toEqual(wordpressProjectConfig); + }); + it('returns void if no Emulsify config file is found within the users cwd', async () => { findFileMock.mockReturnValueOnce(undefined); await expect(getEmulsifyConfig()).resolves.toBe(undefined); @@ -62,7 +75,38 @@ describe('getEmulsifyConfig', () => { ); }); - it('reports schema-invalid variant platform values', async () => { + it('accepts variant platform compatibility expressions', async () => { + const expressionConfig = { + ...projectConfig, + system: { + repository: 'https://github.com/emulsify-ds/compound.git', + checkout: 'main', + }, + variant: { + platform: 'drupal || wordpress', + structureImplementations: [], + }, + }; + loadJsonFileMock.mockResolvedValueOnce(expressionConfig); + + await expect(getEmulsifyConfig()).resolves.toEqual(expressionConfig); + }); + + it('reports schema-invalid project platform expressions', async () => { + loadJsonFileMock.mockResolvedValueOnce({ + ...projectConfig, + project: { + ...projectConfig.project, + platform: 'drupal || wordpress', + }, + }); + + await expect(getEmulsifyConfig()).rejects.toThrow( + 'Invalid Emulsify project configuration in "/projects/project.emulsify.json": /project/platform must be equal to one of the allowed values', + ); + }); + + it('reports schema-invalid variant platform expressions', async () => { loadJsonFileMock.mockResolvedValueOnce({ ...projectConfig, system: { @@ -70,13 +114,13 @@ describe('getEmulsifyConfig', () => { checkout: 'main', }, variant: { - platform: 'wordpress', + platform: 'drupal && wordpress', structureImplementations: [], }, }); await expect(getEmulsifyConfig()).rejects.toThrow( - 'Invalid Emulsify project configuration in "/projects/project.emulsify.json": /variant/platform must be equal to one of the allowed values', + '/variant/platform must match a schema in anyOf', ); }); }); diff --git a/src/util/system/getAvailableSystems.test.ts b/src/util/system/getAvailableSystems.test.ts index a9ad72f..75002ed 100644 --- a/src/util/system/getAvailableSystems.test.ts +++ b/src/util/system/getAvailableSystems.test.ts @@ -7,12 +7,12 @@ describe('getAvailableSystems', () => { { name: 'compound', repository: 'https://github.com/emulsify-ds/compound.git', - platforms: ['none', 'drupal'], + platforms: ['none', 'drupal', 'wordpress'], }, { name: 'emulsify-ui-kit', repository: 'https://github.com/emulsify-ds/emulsify-ui-kit.git', - platforms: ['none', 'drupal'], + platforms: ['none', 'drupal', 'wordpress'], }, ]); }); diff --git a/src/util/system/getAvailableSystems.ts b/src/util/system/getAvailableSystems.ts index 37f1d60..787f737 100644 --- a/src/util/system/getAvailableSystems.ts +++ b/src/util/system/getAvailableSystems.ts @@ -16,12 +16,12 @@ export default async function getAvailableSystems(): Promise< { name: 'compound', repository: 'https://github.com/emulsify-ds/compound.git', - platforms: ['none', 'drupal'], + platforms: ['none', 'drupal', 'wordpress'], }, { name: 'emulsify-ui-kit', repository: 'https://github.com/emulsify-ds/emulsify-ui-kit.git', - platforms: ['none', 'drupal'], + platforms: ['none', 'drupal', 'wordpress'], }, ]; }